From 449dad27c024ea70c79cd2a97c1eaf0a53b07edf Mon Sep 17 00:00:00 2001 From: Visharad Kashyap <154831195+vishxrad@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:50:08 +0530 Subject: [PATCH 01/12] docs: add integrations directory --- docs/app/(home)/integrations/[slug]/page.tsx | 217 ++++ docs/app/(home)/integrations/data.ts | 529 +++++++++ .../(home)/integrations/integration-logo.tsx | 16 + docs/app/(home)/integrations/page.module.css | 1001 +++++++++++++++++ docs/app/(home)/integrations/page.tsx | 160 +++ .../AppThemeProvider/AppThemeProvider.tsx | 30 +- docs/app/sitemap.ts | 20 +- docs/components/docs-navbar.tsx | 1 + docs/components/site-primary-nav.tsx | 1 + docs/public/integration-logos/ag-ui.svg | 13 + .../public/integration-logos/assistant-ui.svg | 7 + docs/public/integration-logos/base-ui.svg | 1 + docs/public/integration-logos/daisyui.svg | 1 + docs/public/integration-logos/fastapi.svg | 1 + docs/public/integration-logos/google.svg | 1 + docs/public/integration-logos/mastra.svg | 26 + docs/public/integration-logos/mui.svg | 1 + docs/public/integration-logos/nextjs.svg | 1 + docs/public/integration-logos/react.svg | 1 + docs/public/integration-logos/shadcn-ui.svg | 1 + docs/public/integration-logos/supabase.svg | 1 + docs/public/integration-logos/svelte.svg | 1 + docs/public/integration-logos/vercel.svg | 1 + docs/public/integration-logos/vue.svg | 1 + 24 files changed, 2009 insertions(+), 24 deletions(-) create mode 100644 docs/app/(home)/integrations/[slug]/page.tsx create mode 100644 docs/app/(home)/integrations/data.ts create mode 100644 docs/app/(home)/integrations/integration-logo.tsx create mode 100644 docs/app/(home)/integrations/page.module.css create mode 100644 docs/app/(home)/integrations/page.tsx create mode 100644 docs/public/integration-logos/ag-ui.svg create mode 100644 docs/public/integration-logos/assistant-ui.svg create mode 100644 docs/public/integration-logos/base-ui.svg create mode 100644 docs/public/integration-logos/daisyui.svg create mode 100644 docs/public/integration-logos/fastapi.svg create mode 100644 docs/public/integration-logos/google.svg create mode 100644 docs/public/integration-logos/mastra.svg create mode 100644 docs/public/integration-logos/mui.svg create mode 100644 docs/public/integration-logos/nextjs.svg create mode 100644 docs/public/integration-logos/react.svg create mode 100644 docs/public/integration-logos/shadcn-ui.svg create mode 100644 docs/public/integration-logos/supabase.svg create mode 100644 docs/public/integration-logos/svelte.svg create mode 100644 docs/public/integration-logos/vercel.svg create mode 100644 docs/public/integration-logos/vue.svg diff --git a/docs/app/(home)/integrations/[slug]/page.tsx b/docs/app/(home)/integrations/[slug]/page.tsx new file mode 100644 index 000000000..5437d8f69 --- /dev/null +++ b/docs/app/(home)/integrations/[slug]/page.tsx @@ -0,0 +1,217 @@ +import { Footer } from "@/app/(home)/sections/Footer/Footer"; +import { ArrowLeft, ArrowRight, ArrowUpRight, Check, Copy } from "lucide-react"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { + getIntegrationCategory, + getRelatedIntegrations, + integrationBySlug, + integrations, +} from "../data"; +import { IntegrationLogo } from "../integration-logo"; +import styles from "../page.module.css"; + +export const dynamicParams = false; + +export function generateStaticParams(): { slug: string }[] { + return integrations.map((integration) => ({ slug: integration.slug })); +} + +export async function generateMetadata(props: { + params: Promise<{ slug: string }>; +}): Promise { + const { slug } = await props.params; + const integration = integrationBySlug.get(slug); + if (!integration) notFound(); + + return { + title: `${integration.name} integration`, + description: integration.summary, + alternates: { canonical: `/integrations/${integration.slug}` }, + openGraph: { + title: `${integration.name} × OpenUI`, + description: integration.summary, + url: `/integrations/${integration.slug}`, + type: "article", + }, + }; +} + +export default async function IntegrationDetailPage(props: { params: Promise<{ slug: string }> }) { + const { slug } = await props.params; + const integration = integrationBySlug.get(slug); + if (!integration) notFound(); + + const category = getIntegrationCategory(integration.category); + const related = getRelatedIntegrations(integration); + + return ( +
+
+
+ + +
+ +
+
+ {integration.support} + {integration.type} +
+

{integration.name}

+

{integration.summary}

+
+
+
+
+ +
+
+
+
+

Integration overview

+

How it connects to OpenUI

+

{integration.howItWorks}

+
+ +
+

The integration path

+
    +
  1. + 1 +
    +

    Describe the interface

    +

    + Generate a component prompt from the same OpenUI library that will render the + response. +

    +
    +
  2. +
  3. + 2 +
    +

    Stream structured output

    +

    + Let {integration.name} own its part of the stack while OpenUI Lang travels as + incremental text or mapped agent events. +

    +
    +
  4. +
  5. + 3 +
    +

    Render and interact

    +

    + Parse the stream with the matching OpenUI runtime, render real components, and + return validated actions to the application. +

    +
    +
  6. +
+
+ + {integration.install ? ( +
+
+

Start here

+

Install or scaffold

+
+
+ {integration.install} +
+
+ ) : null} + +
+

What stays consistent

+
+ {[ + "One schema for prompting and rendering", + "Progressive rendering while output streams", + "Typed components instead of arbitrary markup", + "Validated actions back into the application", + ].map((item) => ( +
+
+ ))} +
+
+
+ + +
+ +
+ +
+
+ +
+
+ ); +} diff --git a/docs/app/(home)/integrations/data.ts b/docs/app/(home)/integrations/data.ts new file mode 100644 index 000000000..bb196b896 --- /dev/null +++ b/docs/app/(home)/integrations/data.ts @@ -0,0 +1,529 @@ +export type IntegrationCategoryId = "design-systems" | "agent-frameworks" | "examples" | "frontend"; + +export type IntegrationSupport = "Official" | "Community" | "Compatible"; + +export interface IntegrationLink { + label: string; + href: string; + kind: "Docs" | "Example" | "Guide" | "GitHub" | "npm" | "Website" | "Plugin"; +} + +export interface Integration { + slug: string; + name: string; + logo: string; + category: IntegrationCategoryId; + support: IntegrationSupport; + type: string; + summary: string; + howItWorks: string; + install?: string; + links: IntegrationLink[]; +} + +export interface IntegrationCategory { + id: IntegrationCategoryId; + title: string; + shortTitle: string; + description: string; + accent: "blue" | "green" | "orange" | "purple" | "rose" | "teal" | "slate"; +} + +export const integrationCategories: IntegrationCategory[] = [ + { + id: "design-systems", + title: "Design systems & component libraries", + shortTitle: "Design systems", + description: + "Use OpenUI's built-in components or connect the UI library your product already uses.", + accent: "orange", + }, + { + id: "agent-frameworks", + title: "Packages & SDKs", + shortTitle: "Packages", + description: "Connect OpenUI to familiar AI application packages, SDKs, and agent runtimes.", + accent: "purple", + }, + { + id: "examples", + title: "Examples", + shortTitle: "Examples", + description: + "Start from a complete app pattern, then adapt the pieces that match what you are building.", + accent: "green", + }, + { + id: "frontend", + title: "Frameworks & runtimes", + shortTitle: "Frameworks", + description: + "Render OpenUI in the frontend framework or native runtime your team already knows.", + accent: "blue", + }, +]; + +const packageLinks = ( + packageName: string, + sourceDirectory: string, + docsHref?: string, +): IntegrationLink[] => [ + ...(docsHref ? [{ label: "Documentation", href: docsHref, kind: "Docs" as const }] : []), + { + label: "npm package", + href: `https://www.npmjs.com/package/${packageName}`, + kind: "npm", + }, + { + label: "Source code", + href: `https://github.com/thesysdev/openui/tree/main/packages/${sourceDirectory}`, + kind: "GitHub", + }, +]; + +const exampleLink = (directory: string, label = "OpenUI example"): IntegrationLink => ({ + label, + href: `https://github.com/thesysdev/openui/tree/main/examples/${directory}`, + kind: "Example", +}); + +const integrationCatalog: Integration[] = [ + // Design systems and component libraries. + { + slug: "shadcn-ui", + name: "shadcn/ui", + logo: "/integration-logos/shadcn-ui.svg", + category: "design-systems", + support: "Official", + type: "Design system", + summary: + "Wrap shadcn/ui components in an OpenUI library and let the model compose them as streaming interfaces.", + howItWorks: + "Each shadcn component is registered with defineComponent and a Zod prop schema. createLibrary produces both the prompt vocabulary and the renderer mapping used by the example chat app.", + links: [ + { label: "Integration guide", href: "/docs/openui-lang/examples/shadcn-chat", kind: "Guide" }, + exampleLink("shadcn-chat"), + { label: "shadcn/ui", href: "https://ui.shadcn.com", kind: "Website" }, + ], + }, + { + slug: "material-ui", + name: "Material UI", + logo: "/integration-logos/mui.svg", + category: "design-systems", + support: "Official", + type: "Design system", + summary: + "Expose Material UI components to a model without replacing your existing React design system.", + howItWorks: + "The example wraps Material UI components with OpenUI definitions, generates a constrained component prompt, and renders model output back into the same Material UI primitives.", + links: [ + exampleLink("material-ui-chat"), + { + label: "Defining components", + href: "/docs/openui-lang/defining-components", + kind: "Guide", + }, + { label: "Material UI", href: "https://mui.com/material-ui", kind: "Website" }, + ], + }, + { + slug: "daisyui", + name: "daisyUI", + logo: "/integration-logos/daisyui.svg", + category: "design-systems", + support: "Compatible", + type: "Custom library path", + summary: + "Turn selected daisyUI patterns into a model-safe component vocabulary while keeping Tailwind styling in your app.", + howItWorks: + "Create thin React, Vue, or Svelte wrappers for the daisyUI patterns you want the model to use, define their props with Zod, and include those definitions in an OpenUI library.", + links: [ + { + label: "Defining components", + href: "/docs/openui-lang/defining-components", + kind: "Guide", + }, + { label: "daisyUI", href: "https://daisyui.com", kind: "Website" }, + ], + }, + { + slug: "base-ui", + name: "Base UI", + logo: "/integration-logos/base-ui.svg", + category: "design-systems", + support: "Compatible", + type: "Custom library path", + summary: + "Pair Base UI's unstyled accessible primitives with a domain-specific OpenUI component library.", + howItWorks: + "Wrap the Base UI primitives that belong in generated output, keep styling and composition in your application, and expose only stable model-facing props through OpenUI schemas.", + links: [ + { + label: "Defining components", + href: "/docs/openui-lang/defining-components", + kind: "Guide", + }, + { label: "Base UI", href: "https://base-ui.com", kind: "Website" }, + ], + }, + { + slug: "openui-component-library", + name: "OpenUI component library", + logo: "/favicon.svg", + category: "design-systems", + support: "Official", + type: "Component library", + summary: + "Start with production-ready charts, tables, forms, cards, layouts, and chat response components.", + howItWorks: + "@openuidev/react-ui exports a general-purpose openuiLibrary and a chat-optimized openuiChatLibrary. Each includes the render components, schemas, and prompt options needed to keep the model and renderer aligned.", + install: "npm install @openuidev/react-ui @openuidev/react-lang", + links: [ + { label: "Component catalog", href: "/components", kind: "Docs" }, + { label: "React UI API", href: "/docs/api-reference/react-ui", kind: "Docs" }, + exampleLink("openui-dashboard"), + ], + }, + + // Packages, SDKs, and agent runtimes. + { + slug: "langchain-langgraph", + name: "LangChain & LangGraph", + logo: "https://raw.githubusercontent.com/langchain-ai/docs/main/src/images/brand/langchain-icon.png", + category: "agent-frameworks", + support: "Official", + type: "Agent framework", + summary: + "Stream LangGraph protocol events into OpenUI through the official @openuidev/langchain package and AG-UI adapter.", + howItWorks: + "The package transforms LangGraph protocol-v2 message and tool events into AG-UI events, relays OpenUI output over SSE, and lets AgentInterface consume the result without LangChain-specific frontend code.", + install: "npm install @openuidev/langchain @langchain/langgraph", + links: [...packageLinks("@openuidev/langchain", "langchain"), exampleLink("langchain-chat")], + }, + { + slug: "vercel-ai-sdk", + name: "Vercel AI SDK", + logo: "/integration-logos/vercel.svg", + category: "agent-frameworks", + support: "Official", + type: "AI SDK", + summary: + "Use streamText and useChat to deliver progressively rendered OpenUI interfaces in React, Vue, and Svelte apps.", + howItWorks: + "The server adds the OpenUI component prompt to the model call and streams the resulting text. The client accumulates that stream and passes it to the matching OpenUI Renderer, including multi-step tools and interactive actions.", + links: [ + exampleLink("vercel-ai-chat"), + { + label: "OpenUI guide", + href: "/docs/openui-lang/examples/vercel-ai-chat", + kind: "Guide", + }, + { label: "AI SDK", href: "https://ai-sdk.dev", kind: "Website" }, + ], + }, + { + slug: "google-adk", + name: "Google ADK", + logo: "/integration-logos/google.svg", + category: "agent-frameworks", + support: "Official", + type: "Agent SDK", + summary: + "Bridge Google ADK for TypeScript run events into AgentInterface with tools and multi-turn sessions.", + howItWorks: + "A Google ADK Agent and FunctionTool run in a Next.js route. ADK runAsync events are converted into streaming chat-completion chunks that OpenUI's adapter parses and renders.", + links: [ + exampleLink("google-adk"), + { label: "Google ADK", href: "https://github.com/google/adk-js", kind: "GitHub" }, + ], + }, + { + slug: "mastra", + name: "Mastra", + logo: "/integration-logos/mastra.svg", + category: "agent-frameworks", + support: "Official", + type: "Agent framework", + summary: + "Connect a Mastra agent to AgentInterface over AG-UI and render its streamed output as typed interfaces.", + howItWorks: + "Mastra owns the agent and tools, the AG-UI transport serializes the run as SSE, and OpenUI's agUIAdapter drives AgentInterface and the component renderer on the client.", + links: [ + exampleLink("mastra-chat"), + { + label: "Mastra integration guide", + href: "https://mastra.ai/guides/build-your-ui/openui", + kind: "Guide", + }, + { label: "Mastra", href: "https://mastra.ai", kind: "Website" }, + ], + }, + { + slug: "assistant-ui", + name: "assistant-ui", + logo: "/integration-logos/assistant-ui.svg", + category: "agent-frameworks", + support: "Official", + type: "Chat framework", + summary: + "Render streaming OpenUI programs as assistant-ui Tool UI while assistant-ui retains its conversation lifecycle.", + howItWorks: + "The @openuidev/assistant-ui toolkit registers display and human-input tools, generates matching model instructions, renders partial OpenUI Lang, and sends validated form actions back through assistant-ui's tool result flow.", + install: "npm install @openuidev/assistant-ui @assistant-ui/react", + links: [ + ...packageLinks("@openuidev/assistant-ui", "assistant-ui"), + { + label: "assistant-ui guide", + href: "https://www.assistant-ui.com/docs/tools/openui", + kind: "Guide", + }, + { + label: "Runnable example", + href: "https://github.com/assistant-ui/assistant-ui/tree/main/examples/with-openui", + kind: "Example", + }, + ], + }, + { + slug: "ag-ui", + name: "AG-UI", + logo: "/integration-logos/ag-ui.svg", + category: "agent-frameworks", + support: "Official", + type: "Agent protocol", + summary: + "Consume standard AG-UI event streams in OpenUI chat surfaces with the built-in agUIAdapter.", + howItWorks: + "Point fetchLLM or a direct ChatLLM implementation at an AG-UI SSE endpoint and use agUIAdapter() as the stream adapter. OpenUI maps lifecycle, text, tools, and state events into its chat runtime.", + links: [ + { + label: "Adapters and formats", + href: "/docs/agent/reference/adapters-and-formats", + kind: "Docs", + }, + { label: "AG-UI documentation", href: "https://docs.ag-ui.com", kind: "Website" }, + { label: "AG-UI source", href: "https://github.com/ag-ui-protocol/ag-ui", kind: "GitHub" }, + ], + }, + + // Complete examples that new users can run and adapt. + { + slug: "openui-chat-example", + name: "OpenUI Chat", + logo: "/favicon.svg", + category: "examples", + support: "Official", + type: "Starter app", + summary: + "A complete generative UI chat app with streaming responses, tools, threads, and interactive components.", + howItWorks: + "The app combines a server-owned model route with OpenUI's chat component library and streaming renderer, providing a compact starting point for a production chat experience.", + links: [ + exampleLink("openui-chat", "OpenUI Chat source"), + { label: "Agent quickstart", href: "/docs/agent/getting-started/quickstart", kind: "Guide" }, + ], + }, + { + slug: "openui-dashboard-example", + name: "OpenUI Dashboard", + logo: "/favicon.svg", + category: "examples", + support: "Official", + type: "Dashboard example", + summary: + "A focused example of generating charts, tables, metrics, and layouts from a natural-language request.", + howItWorks: + "The model receives a dashboard-oriented OpenUI component vocabulary and streams a structured program that the React renderer turns into a live, responsive dashboard.", + links: [ + exampleLink("openui-dashboard", "Dashboard source"), + { + label: "Dashboard guide", + href: "/docs/openui-lang/examples/dashboard", + kind: "Guide", + }, + ], + }, + { + slug: "fastapi", + name: "FastAPI", + logo: "/integration-logos/fastapi.svg", + category: "examples", + support: "Official", + type: "Backend framework", + summary: + "Stream OpenUI Lang from a Python FastAPI endpoint into a Vite and React AgentInterface frontend.", + howItWorks: + "FastAPI owns the model client and yields newline-delimited streaming chunks. The Vite frontend points OpenUI's readable-stream adapter at that endpoint and renders the response progressively.", + links: [ + exampleLink("fastapi-backend"), + { label: "FastAPI", href: "https://fastapi.tiangolo.com", kind: "Website" }, + { label: "Self-hosting guide", href: "/docs/agent/reference/self-hosting", kind: "Guide" }, + ], + }, + { + slug: "supabase", + name: "Supabase", + logo: "/integration-logos/supabase.svg", + category: "examples", + support: "Official", + type: "Backend platform", + summary: + "Persist OpenUI chat threads with Postgres, anonymous auth, Row Level Security, and realtime updates.", + howItWorks: + "The reference app stores threads and messages in Supabase, scopes rows per user with RLS, updates the sidebar through Realtime, and keeps the model route server-side.", + links: [ + exampleLink("supabase-chat"), + { label: "Supabase", href: "https://supabase.com", kind: "Website" }, + { + label: "Conversation concepts", + href: "/docs/agent/core-concepts/conversations", + kind: "Docs", + }, + ], + }, + { + slug: "open-webui", + name: "Open WebUI", + logo: "https://raw.githubusercontent.com/open-webui/open-webui/main/backend/open_webui/static/favicon-96x96.png", + category: "examples", + support: "Community", + type: "AI platform", + summary: + "Render charts, forms, tables, cards, and follow-ups directly inside Open WebUI conversations.", + howItWorks: + "The plugin gives the model a render_openui tool and returns a self-contained HTML response. A sandboxed iframe loads the OpenUI browser bundle and renders the generated program inline.", + links: [ + { + label: "Plugin source", + href: "https://github.com/thesysdev/openwebui-plugin", + kind: "Plugin", + }, + { + label: "Install guide", + href: "https://openwebui.com/posts/generative_ui_plugin_for_open_webui_6c017d62", + kind: "Guide", + }, + { label: "Open WebUI", href: "https://openwebui.com", kind: "Website" }, + ], + }, + + // Frontend frameworks and runtimes. + { + slug: "react", + name: "React", + logo: "/integration-logos/react.svg", + category: "frontend", + support: "Official", + type: "Native runtime", + summary: + "Define component libraries and render streaming OpenUI Lang with the primary React runtime.", + howItWorks: + "@openuidev/react-lang supplies defineComponent, createLibrary, prompt generation, streaming parsing, and Renderer. Add @openuidev/react-ui when you want the built-in libraries or AgentInterface.", + install: "npm install @openuidev/react-lang @openuidev/react-ui", + links: [ + { label: "React runtime API", href: "/docs/api-reference/react-lang", kind: "Docs" }, + { label: "Quickstart", href: "/docs/openui-lang/quickstart", kind: "Guide" }, + exampleLink("openui-chat"), + ], + }, + { + slug: "nextjs", + name: "Next.js", + logo: "/integration-logos/nextjs.svg", + category: "frontend", + support: "Official", + type: "Web framework", + summary: + "Build full-stack OpenUI apps with server routes, streaming adapters, AgentInterface, and optional Cloud persistence.", + howItWorks: + "A client component renders AgentInterface or Renderer while an App Router endpoint owns model credentials, tools, and the response stream. The CLI's Cloud and self-hosted templates both use this pattern.", + install: "npx @openuidev/cli@latest create", + links: [ + { label: "Agent quickstart", href: "/docs/agent/getting-started/quickstart", kind: "Guide" }, + { + label: "OpenUI Cloud starter", + href: "/docs/agent/getting-started/openui-cloud", + kind: "Guide", + }, + exampleLink("openui-chat"), + ], + }, + { + slug: "vue", + name: "Vue 3", + logo: "/integration-logos/vue.svg", + category: "frontend", + support: "Official", + type: "Native runtime", + summary: + "Define model-renderable Vue components and render streamed OpenUI Lang with @openuidev/vue-lang.", + howItWorks: + "Vue component definitions and Zod schemas form a shared library for prompt generation and rendering. The Vue Renderer updates progressively as OpenUI Lang arrives.", + install: "npm install @openuidev/vue-lang", + links: [ + ...packageLinks("@openuidev/vue-lang", "vue-lang"), + exampleLink("vue-chat"), + { label: "Vue", href: "https://vuejs.org", kind: "Website" }, + ], + }, + { + slug: "react-native", + name: "React Native", + logo: "/integration-logos/react.svg", + category: "frontend", + support: "Official", + type: "Mobile framework", + summary: + "Render model-generated interfaces in a native mobile chat application with a dedicated component library.", + howItWorks: + "The reference project pairs a React Native chat app with a backend that prompts for OpenUI Lang. Native component definitions map the same structured response model to mobile views and actions.", + links: [ + { + label: "Integration guide", + href: "/docs/openui-lang/examples/react-native", + kind: "Guide", + }, + exampleLink("openui-react-native"), + { label: "React Native", href: "https://reactnative.dev", kind: "Website" }, + ], + }, + { + slug: "svelte", + name: "Svelte 5", + logo: "/integration-logos/svelte.svg", + category: "frontend", + support: "Official", + type: "Native runtime", + summary: + "Define Svelte components, generate prompts, and render streamed OpenUI Lang with @openuidev/svelte-lang.", + howItWorks: + "Svelte component definitions and Zod schemas become one component library. The package generates the model prompt and its Renderer resolves streamed statements into Svelte components.", + install: "npm install @openuidev/svelte-lang", + links: [ + ...packageLinks("@openuidev/svelte-lang", "svelte-lang"), + exampleLink("svelte-chat"), + { label: "Svelte", href: "https://svelte.dev", kind: "Website" }, + ], + }, +]; + +export const integrations: Integration[] = integrationCatalog; + +export const integrationBySlug = new Map(integrations.map((item) => [item.slug, item])); + +export function getIntegrationCategory(id: IntegrationCategoryId): IntegrationCategory { + const category = integrationCategories.find((item) => item.id === id); + if (!category) throw new Error(`Unknown integration category: ${id}`); + return category; +} + +export function getIntegrationsByCategory(id: IntegrationCategoryId): Integration[] { + return integrations.filter((item) => item.category === id); +} + +export function getRelatedIntegrations(integration: Integration, limit = 3): Integration[] { + return integrations + .filter((item) => item.category === integration.category && item.slug !== integration.slug) + .slice(0, limit); +} diff --git a/docs/app/(home)/integrations/integration-logo.tsx b/docs/app/(home)/integrations/integration-logo.tsx new file mode 100644 index 000000000..612af03f4 --- /dev/null +++ b/docs/app/(home)/integrations/integration-logo.tsx @@ -0,0 +1,16 @@ +import type { Integration } from "./data"; + +type IntegrationLogoProps = { + integration: Pick; + className: string; +}; + +export function IntegrationLogo({ integration, className }: IntegrationLogoProps) { + return ( + + ); +} diff --git a/docs/app/(home)/integrations/page.module.css b/docs/app/(home)/integrations/page.module.css new file mode 100644 index 000000000..414f5731a --- /dev/null +++ b/docs/app/(home)/integrations/page.module.css @@ -0,0 +1,1001 @@ +.page, +.detailPage { + min-height: 100vh; + background: var(--openui-foreground); + color: var(--openui-text-neutral-primary); +} + +[data-theme="dark"] .page, +[data-theme="dark"] .detailPage { + background: #000; +} + +.contentBand, +.detailBand { + background: linear-gradient( + to bottom, + var(--openui-foreground), + var(--openui-background) 9rem, + var(--openui-background) calc(100% - 9rem), + var(--openui-foreground) + ); +} + +[data-theme="dark"] .contentBand, +[data-theme="dark"] .detailBand { + background: linear-gradient( + to bottom, + #000, + var(--swatch-neutral-950) 24rem, + var(--swatch-neutral-950) calc(100% - 24rem), + #000 + ); +} + +.directory { + max-width: var(--home-container-max); + margin-inline: auto; + padding: 4rem var(--home-section-padding-inline) 8rem; +} + +.directoryIntro { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(18rem, 30rem); + gap: 3rem; + align-items: end; +} + +.sectionEyebrow, +.detailEyebrow { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0 0.875rem; + color: var(--openui-text-neutral-secondary); + font-family: var(--home-font-mono); + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.sectionTitle { + margin: 0; + font-family: var(--home-font-display); + font-size: var(--home-title-size); + font-weight: var(--home-title-weight); + letter-spacing: var(--home-title-tracking); + line-height: var(--home-title-leading); +} + +.directoryDescription { + margin: 0; + color: var(--openui-text-neutral-secondary); + font-size: 0.9375rem; + line-height: 1.65; +} + +.stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: 2.5rem; + border: 1px solid var(--home-hairline); + border-radius: 1.25rem; + background: color-mix(in srgb, var(--openui-foreground) 92%, transparent); + box-shadow: var(--home-card-lift); + overflow: hidden; +} + +.stat { + display: flex; + min-height: 6.5rem; + flex-direction: column; + justify-content: center; + padding: 1.25rem 1.5rem; +} + +.stat + .stat { + border-inline-start: 1px solid var(--home-hairline); +} + +.stat strong { + font-family: var(--home-font-display); + font-size: 1.75rem; + font-weight: 600; + letter-spacing: -0.04em; + line-height: 1; +} + +.stat span { + margin-top: 0.55rem; + color: var(--openui-text-neutral-secondary); + font-size: 0.8125rem; +} + +.categoryNav { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1rem; + padding: 0.75rem; + border: 1px solid var(--home-hairline); + border-radius: 1.25rem; + background: var(--openui-foreground); +} + +.categoryNavLink { + display: inline-flex; + align-items: center; + gap: 0.55rem; + min-height: 2.25rem; + border-radius: 999px; + padding: 0.4rem 0.75rem 0.4rem 0.9rem; + color: var(--openui-text-neutral-secondary); + font-size: 0.8125rem; + font-weight: 500; + text-decoration: none; + transition: + background-color 0.18s ease, + color 0.18s ease; +} + +.categoryNavLink span { + display: grid; + min-width: 1.5rem; + height: 1.5rem; + place-items: center; + border-radius: 999px; + background: var(--openui-highlight-subtle); + color: var(--openui-text-neutral-tertiary); + font-family: var(--home-font-mono); + font-size: 0.6875rem; +} + +.categoryNavLink:hover { + background: var(--openui-highlight-subtle); + color: var(--openui-text-neutral-primary); +} + +.categoryList { + display: grid; + gap: 6rem; + margin-top: 6rem; +} + +.categorySection, +.detailPage { + --category-accent: #64748b; + --category-soft: color-mix(in srgb, var(--category-accent) 11%, transparent); +} + +.categorySection[data-accent="purple"], +.detailPage[data-accent="purple"] { + --category-accent: #7c3aed; +} + +.categorySection[data-accent="blue"], +.detailPage[data-accent="blue"] { + --category-accent: #2563eb; +} + +.categorySection[data-accent="orange"], +.detailPage[data-accent="orange"] { + --category-accent: #d97706; +} + +.categorySection[data-accent="green"], +.detailPage[data-accent="green"] { + --category-accent: #059669; +} + +.categorySection[data-accent="rose"], +.detailPage[data-accent="rose"] { + --category-accent: #e11d48; +} + +.categorySection[data-accent="teal"], +.detailPage[data-accent="teal"] { + --category-accent: #0f766e; +} + +.categorySection[data-accent="slate"], +.detailPage[data-accent="slate"] { + --category-accent: #475569; +} + +[data-theme="dark"] .categorySection, +[data-theme="dark"] .detailPage { + --category-accent: #94a3b8; +} + +[data-theme="dark"] .categorySection[data-accent="purple"], +[data-theme="dark"] .detailPage[data-accent="purple"] { + --category-accent: #a78bfa; +} + +[data-theme="dark"] .categorySection[data-accent="blue"], +[data-theme="dark"] .detailPage[data-accent="blue"] { + --category-accent: #60a5fa; +} + +[data-theme="dark"] .categorySection[data-accent="orange"], +[data-theme="dark"] .detailPage[data-accent="orange"] { + --category-accent: #f59e0b; +} + +[data-theme="dark"] .categorySection[data-accent="green"], +[data-theme="dark"] .detailPage[data-accent="green"] { + --category-accent: #34d399; +} + +[data-theme="dark"] .categorySection[data-accent="rose"], +[data-theme="dark"] .detailPage[data-accent="rose"] { + --category-accent: #fb7185; +} + +[data-theme="dark"] .categorySection[data-accent="teal"], +[data-theme="dark"] .detailPage[data-accent="teal"] { + --category-accent: #5eead4; +} + +.categorySection { + scroll-margin-top: 7rem; +} + +.categoryHeader { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 1rem; + align-items: start; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--home-hairline); +} + +.categoryIndex, +.categoryCount { + display: grid; + width: 2rem; + height: 2rem; + place-items: center; + border-radius: 0.65rem; + background: var(--category-soft); + color: var(--category-accent); + font-family: var(--home-font-mono); + font-size: 0.6875rem; + font-weight: 600; +} + +.categoryHeading h2 { + margin: 0; + font-family: var(--home-font-display); + font-size: 1.5rem; + font-weight: 600; + letter-spacing: -0.025em; + line-height: 1.25; +} + +.categoryHeading p { + max-width: 44rem; + margin: 0.55rem 0 0; + color: var(--openui-text-neutral-secondary); + font-size: 0.875rem; + line-height: 1.55; +} + +.grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.card { + display: flex; + min-height: 17.5rem; + flex-direction: column; + border: 1px solid var(--home-hairline); + border-radius: 1.5rem; + padding: 1.25rem; + background: var(--openui-foreground); + box-shadow: var(--home-card-lift); + color: var(--openui-text-neutral-primary); + text-decoration: none; + transition: + transform 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +.card:hover { + transform: translateY(-3px); + border-color: color-mix(in srgb, var(--category-accent) 45%, var(--home-hairline)); + box-shadow: var(--openui-shadow-m); +} + +.cardTopline, +.cardTitleRow, +.cardMeta { + display: flex; + align-items: center; +} + +.cardTopline { + justify-content: space-between; +} + +.mark, +.detailMark, +.relatedMark { + display: grid; + place-items: center; + overflow: hidden; + background: #fff; +} + +.mark img, +.detailMark img, +.relatedMark img { + display: block; + width: 62%; + height: 62%; + object-fit: contain; +} + +.ctaIcon { + display: grid; + place-items: center; + background: var(--category-soft); + color: var(--category-accent); +} + +.mark { + width: 2.75rem; + height: 2.75rem; + border: 1px solid color-mix(in srgb, var(--category-accent) 18%, transparent); + border-radius: 0.9rem; + font-size: 0.8125rem; + letter-spacing: -0.03em; +} + +.popularityRank { + color: var(--openui-text-neutral-tertiary); + font-family: var(--home-font-mono); + font-size: 0.6875rem; +} + +.cardBody { + margin-top: 2.25rem; +} + +.cardTitleRow { + gap: 0.75rem; + justify-content: space-between; +} + +.card h3 { + margin: 0; + font-family: var(--home-font-display); + font-size: 1.125rem; + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.3; +} + +.cardArrow { + flex-shrink: 0; + color: var(--openui-text-neutral-tertiary); + transform: translateX(-3px); + transition: + color 0.18s ease, + transform 0.18s ease; +} + +.card:hover .cardArrow { + color: var(--category-accent); + transform: translateX(0); +} + +.cardBody p { + margin: 0.75rem 0 0; + color: var(--openui-text-neutral-secondary); + font-size: 0.875rem; + line-height: 1.58; +} + +.cardMeta { + flex-wrap: wrap; + gap: 0.45rem; + margin-top: auto; + padding-top: 1.5rem; +} + +.cardMeta span, +.detailTags span { + display: inline-flex; + min-height: 1.625rem; + align-items: center; + border-radius: 999px; + padding: 0.25rem 0.625rem; + background: var(--openui-highlight-subtle); + color: var(--openui-text-neutral-secondary); + font-size: 0.6875rem; + font-weight: 550; + line-height: 1; +} + +.cardMeta span[data-support="Official"], +.detailTags span[data-support="Official"] { + background: color-mix(in srgb, #059669 12%, transparent); + color: #047857; +} + +.cardMeta span[data-support="Community"], +.detailTags span[data-support="Community"] { + background: color-mix(in srgb, #7c3aed 11%, transparent); + color: #6d28d9; +} + +.cardMeta span[data-support="Compatible"], +.detailTags span[data-support="Compatible"] { + background: color-mix(in srgb, #2563eb 10%, transparent); + color: #1d4ed8; +} + +[data-theme="dark"] .cardMeta span[data-support="Official"], +[data-theme="dark"] .detailTags span[data-support="Official"] { + color: #6ee7b7; +} + +[data-theme="dark"] .cardMeta span[data-support="Community"], +[data-theme="dark"] .detailTags span[data-support="Community"] { + color: #c4b5fd; +} + +[data-theme="dark"] .cardMeta span[data-support="Compatible"], +[data-theme="dark"] .detailTags span[data-support="Compatible"] { + color: #93c5fd; +} + +.directoryCta { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 1.25rem; + align-items: center; + margin-top: 6rem; + border: 1px solid var(--home-hairline); + border-radius: 1.5rem; + padding: 1.5rem; + background: var(--openui-foreground); + box-shadow: var(--home-card-lift); +} + +.ctaIcon { + width: 3rem; + height: 3rem; + border-radius: 1rem; +} + +.directoryCta h2 { + margin: 0; + font-family: var(--home-font-display); + font-size: 1.125rem; + font-weight: 600; +} + +.directoryCta p { + margin: 0.35rem 0 0; + color: var(--openui-text-neutral-secondary); + font-size: 0.8125rem; +} + +.ctaLink, +.detailBackRow a { + display: inline-flex; + align-items: center; + gap: 0.5rem; + border: 1px solid var(--openui-border-default); + border-radius: 999px; + padding: 0.65rem 0.9rem; + background: var(--openui-foreground); + box-shadow: var(--openui-shadow-s); + color: var(--openui-text-neutral-primary); + font-size: 0.8125rem; + font-weight: 600; + text-decoration: none; +} + +/* Detail page */ +.detailHero { + border-bottom: 1px solid var(--home-hairline); + background: + radial-gradient( + circle at 78% 20%, + color-mix(in srgb, var(--category-accent) 11%, transparent), + transparent 31rem + ), + var(--openui-foreground); +} + +[data-theme="dark"] .detailHero { + background: + radial-gradient( + circle at 78% 20%, + color-mix(in srgb, var(--category-accent) 13%, transparent), + transparent 31rem + ), + #000; +} + +.detailHeroInner, +.detailLayout, +.detailBackRow { + max-width: var(--home-container-max); + margin-inline: auto; + padding-inline: var(--home-section-padding-inline); +} + +.detailHeroInner { + padding-block: clamp(4rem, 8vw, 7rem) clamp(4rem, 8vw, 6rem); +} + +.breadcrumbs { + display: flex; + align-items: center; + gap: 0.6rem; + margin-bottom: 2.5rem; + color: var(--openui-text-neutral-tertiary); + font-size: 0.8125rem; +} + +.breadcrumbs a { + color: var(--openui-text-neutral-secondary); + text-decoration: none; +} + +.breadcrumbs a:hover { + color: var(--category-accent); +} + +.detailLockup { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: clamp(1.5rem, 4vw, 3rem); + align-items: start; +} + +.detailMark { + width: clamp(5rem, 10vw, 7rem); + height: clamp(5rem, 10vw, 7rem); + border: 1px solid color-mix(in srgb, var(--category-accent) 22%, transparent); + border-radius: 1.75rem; + box-shadow: var(--home-card-lift); +} + +.detailMark img { + width: 64%; + height: 64%; +} + +.detailTitleBlock { + max-width: 48rem; +} + +.detailTags { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + margin-bottom: 1.25rem; +} + +.detailTitleBlock h1 { + margin: 0; + font-family: var(--home-font-display); + font-size: clamp(2.5rem, 7vw, 4.5rem); + font-weight: 700; + letter-spacing: -0.055em; + line-height: 1.02; + text-wrap: balance; +} + +.detailTitleBlock > p { + max-width: 43rem; + margin: 1.25rem 0 0; + color: var(--openui-text-neutral-secondary); + font-size: clamp(1rem, 2vw, 1.25rem); + line-height: 1.55; +} + +.detailLayout { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(17rem, 20rem); + gap: clamp(3rem, 8vw, 7rem); + padding-block: 5rem 7rem; +} + +.detailArticle { + display: grid; + gap: 4.5rem; + min-width: 0; +} + +.detailArticle section > h2, +.installSection h2 { + max-width: 42rem; + margin: 0; + font-family: var(--home-font-display); + font-size: clamp(1.5rem, 3vw, 2rem); + font-weight: 600; + letter-spacing: -0.035em; + line-height: 1.2; +} + +.detailLead { + max-width: 46rem; + margin: 1rem 0 0; + color: var(--openui-text-neutral-secondary); + font-size: 1rem; + line-height: 1.75; +} + +.flowList { + display: grid; + gap: 0; + margin: 1.75rem 0 0; + padding: 0; + list-style: none; +} + +.flowList li { + position: relative; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 1rem; + padding: 0 0 1.75rem; +} + +.flowList li:not(:last-child)::after { + position: absolute; + top: 2rem; + bottom: 0; + left: 0.9375rem; + width: 1px; + background: var(--home-hairline); + content: ""; +} + +.flowNumber { + position: relative; + z-index: 1; + display: grid; + width: 1.875rem; + height: 1.875rem; + place-items: center; + border-radius: 0.65rem; + background: var(--category-soft); + color: var(--category-accent); + font-family: var(--home-font-mono); + font-size: 0.6875rem; + font-weight: 700; +} + +.flowList h3 { + margin: 0.15rem 0 0; + font-size: 0.9375rem; + font-weight: 600; +} + +.flowList p { + margin: 0.45rem 0 0; + color: var(--openui-text-neutral-secondary); + font-size: 0.875rem; + line-height: 1.6; +} + +.installSection { + display: grid; + gap: 1.5rem; +} + +.codeBlock { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 1rem; + border: 1px solid var(--home-hairline); + border-radius: 1rem; + padding: 1rem 1.125rem; + background: var(--openui-sunk-light); + color: var(--openui-text-neutral-primary); +} + +.codeBlock code { + min-width: 0; + overflow-x: auto; + font-family: var(--home-font-mono); + font-size: 0.8125rem; + white-space: nowrap; +} + +.codeBlock svg { + flex-shrink: 0; + color: var(--openui-text-neutral-tertiary); +} + +.checkGrid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + margin-top: 1.5rem; +} + +.checkItem { + display: flex; + gap: 0.65rem; + align-items: flex-start; + border: 1px solid var(--home-hairline); + border-radius: 0.9rem; + padding: 0.9rem; + background: color-mix(in srgb, var(--openui-foreground) 85%, transparent); + color: var(--openui-text-neutral-secondary); + font-size: 0.8125rem; + line-height: 1.45; +} + +.checkItem svg { + flex-shrink: 0; + margin-top: 0.1rem; + color: var(--category-accent); +} + +.resourceSidebar { + display: flex; + flex-direction: column; + gap: 1rem; + align-self: start; + position: sticky; + top: 7rem; +} + +.resourceCard { + border: 1px solid var(--home-hairline); + border-radius: 1.25rem; + padding: 1rem; + background: var(--openui-foreground); + box-shadow: var(--home-card-lift); +} + +.resourceTitle { + margin: 0 0 0.75rem; + padding: 0.2rem 0.25rem 0.75rem; + border-bottom: 1px solid var(--home-hairline); + color: var(--openui-text-neutral-secondary); + font-family: var(--home-font-mono); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.resourceLinks, +.relatedLinks { + display: grid; + gap: 0.35rem; +} + +.resourceLinks a, +.relatedLinks a { + display: flex; + align-items: center; + gap: 0.75rem; + border-radius: 0.75rem; + padding: 0.7rem; + color: var(--openui-text-neutral-primary); + text-decoration: none; + transition: background-color 0.18s ease; +} + +.resourceLinks a { + justify-content: space-between; +} + +.resourceLinks a:hover, +.relatedLinks a:hover { + background: var(--openui-highlight-subtle); +} + +.resourceLinks a > span { + display: flex; + min-width: 0; + flex-direction: column; + gap: 0.2rem; + font-size: 0.8125rem; + font-weight: 550; +} + +.resourceLinks small { + color: var(--openui-text-neutral-tertiary); + font-size: 0.625rem; + font-weight: 500; +} + +.resourceLinks svg, +.relatedLinks svg { + flex-shrink: 0; + color: var(--openui-text-neutral-tertiary); +} + +.relatedLinks a { + font-size: 0.8125rem; + font-weight: 550; +} + +.relatedLinks a > span:nth-child(2) { + min-width: 0; + flex: 1; +} + +.relatedMark { + width: 1.9rem; + height: 1.9rem; + flex-shrink: 0; + border-radius: 0.6rem; + border: 1px solid var(--home-hairline); +} + +.relatedMark img { + width: 58%; + height: 58%; +} + +.detailBackRow { + display: flex; + justify-content: space-between; + gap: 1rem; + padding-bottom: 8rem; +} + +@media (max-width: 960px) { + .grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .detailLayout { + grid-template-columns: minmax(0, 1fr); + } + + .resourceSidebar { + position: static; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 767px) { + .directory { + padding-block: 3rem 6rem; + } + + .directoryIntro { + grid-template-columns: 1fr; + gap: 1rem; + text-align: center; + } + + .sectionEyebrow { + justify-content: center; + } + + .stats { + grid-template-columns: 1fr; + } + + .stat { + min-height: auto; + align-items: center; + padding: 1rem; + text-align: center; + } + + .stat + .stat { + border-top: 1px solid var(--home-hairline); + border-inline-start: 0; + } + + .categoryNav { + flex-wrap: nowrap; + overflow-x: auto; + scrollbar-width: none; + } + + .categoryNav::-webkit-scrollbar { + display: none; + } + + .categoryNavLink { + flex-shrink: 0; + } + + .categoryList { + gap: 4.5rem; + margin-top: 4.5rem; + } + + .categoryHeader { + grid-template-columns: auto minmax(0, 1fr); + } + + .categoryCount { + display: none; + } + + .grid { + grid-template-columns: 1fr; + } + + .card { + min-height: 15.5rem; + } + + .directoryCta { + grid-template-columns: auto minmax(0, 1fr); + } + + .ctaLink { + grid-column: 1 / -1; + justify-content: center; + } + + .detailHeroInner { + padding-block: 3rem 4rem; + } + + .detailLockup { + grid-template-columns: 1fr; + } + + .detailMark { + width: 4.75rem; + height: 4.75rem; + border-radius: 1.35rem; + } + + .detailTitleBlock h1 { + font-size: clamp(2.25rem, 13vw, 3.5rem); + } + + .detailLayout { + gap: 4rem; + padding-block: 4rem 5rem; + } + + .detailArticle { + gap: 3.5rem; + } + + .checkGrid, + .resourceSidebar { + grid-template-columns: 1fr; + } + + .detailBackRow { + flex-direction: column; + padding-bottom: 6rem; + } + + .detailBackRow a { + justify-content: center; + } +} + +@media (prefers-reduced-motion: reduce) { + .card, + .cardArrow, + .categoryNavLink { + transition: none; + } + + .card:hover { + transform: none; + } +} diff --git a/docs/app/(home)/integrations/page.tsx b/docs/app/(home)/integrations/page.tsx new file mode 100644 index 000000000..165373cde --- /dev/null +++ b/docs/app/(home)/integrations/page.tsx @@ -0,0 +1,160 @@ +import { PageHero, PageHeroAccent } from "@/app/(home)/components/PageHero/PageHero"; +import { Footer } from "@/app/(home)/sections/Footer/Footer"; +import { ArrowRight, Boxes, Layers3 } from "lucide-react"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { getIntegrationsByCategory, integrationCategories, integrations } from "./data"; +import { IntegrationLogo } from "./integration-logo"; +import styles from "./page.module.css"; + +export const metadata: Metadata = { + title: "Integrations", + description: + "Explore OpenUI integrations across design systems, packages, examples, and frontend frameworks.", + alternates: { canonical: "/integrations" }, + openGraph: { + title: "OpenUI integrations", + description: "Build generative UI with the design systems, packages, and frameworks you know.", + url: "/integrations", + type: "website", + }, +}; + +const officialCount = integrations.filter((item) => item.support === "Official").length; + +export default function IntegrationsPage() { + return ( +
+ + Build with the tools +
+ you already use. + + } + subtitle={ + <>Start with a design system, package, example, or framework your team already knows. + } + smallSubtitle + /> + +
+
+
+
+

+

+

+ OpenUI across your stack +

+
+

+ Four simple ways into OpenUI. Popular starting points appear first, and each page + links to the relevant docs or runnable source. +

+
+ +
+
+ {integrations.length} + integrations +
+
+ {integrationCategories.length} + categories +
+
+ {officialCount} + official paths +
+
+ + + +
+ {integrationCategories.map((category, categoryIndex) => { + const items = getIntegrationsByCategory(category.id); + + return ( +
+
+ +
+

{category.title}

+

{category.description}

+
+ {items.length} +
+ +
+ {items.map((item, itemIndex) => ( + +
+ + + {String(itemIndex + 1).padStart(2, "0")} + +
+
+
+

{item.name}

+
+

{item.summary}

+
+
+ {item.support} + {item.type} +
+ + ))} +
+
+ ); + })} +
+ +
+ +
+

Building another integration?

+

+ Share an adapter, component library, example, or tool with the OpenUI community. +

+
+ + Visit the Lab +
+
+
+ +
+
+ ); +} diff --git a/docs/app/components/components/AppThemeProvider/AppThemeProvider.tsx b/docs/app/components/components/AppThemeProvider/AppThemeProvider.tsx index 1d1f19390..0358dd222 100644 --- a/docs/app/components/components/AppThemeProvider/AppThemeProvider.tsx +++ b/docs/app/components/components/AppThemeProvider/AppThemeProvider.tsx @@ -3,7 +3,8 @@ import { fontOverrides, legacyVarCss, swatchVarCss } from "@/shared/theme/openuiThemeBridge"; import type { ThemeMode } from "@components/types"; import { ThemeProvider } from "@openuidev/react-ui/ThemeProvider"; -import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; +import { useTheme } from "next-themes"; +import { createContext, useContext, useEffect, useMemo, type ReactNode } from "react"; interface AppThemeProviderProps { children: ReactNode; @@ -17,21 +18,6 @@ type AppThemeContextValue = { const AppThemeContext = createContext(null); -const THEME_STORAGE_KEY = "openui-theme"; - -const getInitialMode = (): ThemeMode => { - if (typeof window === "undefined") { - return "light"; - } - - const stored = window.localStorage.getItem(THEME_STORAGE_KEY); - if (stored === "light" || stored === "dark") { - return stored; - } - - return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; -}; - export const useAppTheme = (): AppThemeContextValue => { const context = useContext(AppThemeContext); if (!context) { @@ -41,26 +27,26 @@ export const useAppTheme = (): AppThemeContextValue => { }; export default function AppThemeProvider({ children }: AppThemeProviderProps) { - const [mode, setMode] = useState(getInitialMode); + const { resolvedTheme, setTheme } = useTheme(); + const mode: ThemeMode = resolvedTheme === "dark" ? "dark" : "light"; useEffect(() => { document.documentElement.setAttribute("data-theme", mode); document.body.setAttribute("data-theme", mode); - window.localStorage.setItem(THEME_STORAGE_KEY, mode); }, [mode]); const contextValue = useMemo( () => ({ mode, - setMode, - toggleMode: () => setMode((prev) => (prev === "light" ? "dark" : "light")), + setMode: (nextMode: ThemeMode) => setTheme(nextMode), + toggleMode: () => setTheme(mode === "light" ? "dark" : "light"), }), - [mode], + [mode, setTheme], ); return ( - +