diff --git a/docs/app/(home)/integrations/[slug]/page.tsx b/docs/app/(home)/integrations/[slug]/page.tsx new file mode 100644 index 000000000..a8222f52e --- /dev/null +++ b/docs/app/(home)/integrations/[slug]/page.tsx @@ -0,0 +1,212 @@ +import { Footer } from "@/app/(home)/sections/Footer/Footer"; +import { ArrowLeft, ArrowRight, ArrowUpRight, Check } from "lucide-react"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { CopyCommand } from "../copy-command"; +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 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", + "Host application retains its runtime behavior", + ].map((item) => ( +
+
+ ))} +
+
+
+ + +
+ +
+ + Browse {category.shortTitle.toLowerCase()} +
+
+ +
+
+ ); +} diff --git a/docs/app/(home)/integrations/category-nav.tsx b/docs/app/(home)/integrations/category-nav.tsx new file mode 100644 index 000000000..98a0f05ad --- /dev/null +++ b/docs/app/(home)/integrations/category-nav.tsx @@ -0,0 +1,45 @@ +"use client"; + +import type { MouseEvent } from "react"; +import styles from "./page.module.css"; + +interface CategoryNavItem { + count: number; + id: string; + label: string; +} + +export function CategoryNav({ categories }: { categories: CategoryNavItem[] }) { + const scrollToCategory = (event: MouseEvent, id: string) => { + if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { + return; + } + + const target = document.getElementById(id); + if (!target) return; + + event.preventDefault(); + const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + target.scrollIntoView({ behavior: reducedMotion ? "auto" : "smooth", block: "start" }); + + const url = new URL(window.location.href); + url.hash = id; + window.history.pushState(null, "", url); + }; + + return ( + + ); +} diff --git a/docs/app/(home)/integrations/copy-command.tsx b/docs/app/(home)/integrations/copy-command.tsx new file mode 100644 index 000000000..8dfc054ee --- /dev/null +++ b/docs/app/(home)/integrations/copy-command.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { copyText } from "@/lib/copy-text"; +import { Check, Copy } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import styles from "./page.module.css"; + +export function CopyCommand({ command }: { command: string }) { + const [copied, setCopied] = useState(false); + const resetTimer = useRef(null); + + useEffect( + () => () => { + if (resetTimer.current !== null) window.clearTimeout(resetTimer.current); + }, + [], + ); + + async function handleCopy() { + if (!(await copyText(command))) return; + + setCopied(true); + if (resetTimer.current !== null) window.clearTimeout(resetTimer.current); + resetTimer.current = window.setTimeout(() => setCopied(false), 2000); + } + + return ( + + ); +} diff --git a/docs/app/(home)/integrations/data.ts b/docs/app/(home)/integrations/data.ts new file mode 100644 index 000000000..be9f197f3 --- /dev/null +++ b/docs/app/(home)/integrations/data.ts @@ -0,0 +1,450 @@ +export type IntegrationCategoryId = "ai-frameworks" | "design-systems" | "frontend-platforms"; + +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; + 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: "ai-frameworks", + title: "AI frameworks, SDKs & protocols", + shortTitle: "AI stack", + description: + "Connect OpenUI to the agent framework, AI SDK, or protocol your application already uses.", + accent: "purple", + }, + { + 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: "frontend-platforms", + title: "Frontend frameworks & platforms", + shortTitle: "Frontend", + description: + "Run OpenUI in an alternative web, mobile, chat framework, or AI application platform.", + 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", + 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", + 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: "handsontable", + name: "Handsontable", + logo: "https://raw.githubusercontent.com/handsontable/handsontable/develop/docs/public/favicon.png", + category: "design-systems", + type: "Data grid", + summary: + "Connect a live Handsontable spreadsheet to OpenUI so agent tools can analyze and update its data.", + howItWorks: + "The maintained example shares table state between Handsontable and a custom OpenUI SpreadsheetTable component. Tool calls update the server-side data, and the rendered component synchronizes those results back into the live grid.", + links: [ + exampleLink("hands-on-table-chat"), + { label: "Handsontable", href: "https://handsontable.com", kind: "Website" }, + ], + }, + { + slug: "react-email", + name: "React Email", + logo: "/integration-logos/react-email.svg", + category: "design-systems", + type: "Email component library", + summary: + "Generate, preview, and export email-safe interfaces with OpenUI's React Email component package.", + howItWorks: + "@openuidev/react-email provides 44 OpenUI component definitions, an emailLibrary, and prompt rules. OpenUI Lang streams into a React Email preview, which @react-email/render can convert to email-compatible HTML.", + install: "npm install @openuidev/react-email @openuidev/react-lang", + links: [ + ...packageLinks("@openuidev/react-email", "react-email", "/docs/api-reference/react-email"), + exampleLink("react-email"), + { label: "React Email", href: "https://react.email", kind: "Website" }, + ], + }, + + // AI frameworks, SDKs, and protocols. + { + slug: "langchain-langgraph", + name: "LangChain & LangGraph", + logo: "https://raw.githubusercontent.com/langchain-ai/docs/main/src/images/brand/langchain-icon.png", + category: "ai-frameworks", + type: "Agent framework adapter", + summary: + "Connect LangChain and LangGraph to OpenUI through first-party server and stream adapters.", + howItWorks: + "The @openuidev/langchain package transforms LangGraph protocol-v2 events into AG-UI on the server, then agUIAdapter() consumes the stream in AgentInterface. If your backend already returns native LangGraph named-event SSE, use the bundled langGraphAdapter() and langGraphMessageFormat instead.", + install: "npm install @openuidev/langchain @langchain/langgraph", + links: [ + ...packageLinks("@openuidev/langchain", "langchain", "/docs/api-reference/langchain"), + { + label: "Stream adapters", + href: "/docs/agent/reference/adapters-and-formats#langgraphadapter", + kind: "Docs", + }, + exampleLink("langchain-chat"), + ], + }, + { + slug: "vercel-ai-sdk", + name: "Vercel AI SDK", + logo: "/integration-logos/vercel.svg", + category: "ai-frameworks", + type: "AI SDK adapter", + summary: + "Connect Vercel AI SDK UIMessage streams to OpenUI with OpenUI's bundled first-party adapter and message format.", + howItWorks: + "Return the UIMessage SSE produced by streamText().toUIMessageStreamResponse(), then use vercelAIAdapter() with vercelAIMessageFormat in AgentInterface. OpenUI validates AI SDK v6 or v7 chunks and maps streamed text, tool inputs, tool results, multi-step lifecycles, and errors into AG-UI events.", + install: "npm install @openuidev/react-ui ai", + links: [ + { + label: "Vercel AI adapter", + href: "/docs/agent/reference/adapters-and-formats#vercelaiadapter", + kind: "Docs", + }, + ...packageLinks("@openuidev/react-headless", "react-headless"), + exampleLink("vercel-ai-chat"), + { label: "AI SDK", href: "https://ai-sdk.dev", kind: "Website" }, + ], + }, + { + slug: "google-adk", + name: "Google ADK", + logo: "/integration-logos/google.svg", + category: "ai-frameworks", + 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: "ai-frameworks", + 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: "frontend-platforms", + 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: "ai-frameworks", + 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 supported lifecycle, text, tool, and error 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" }, + ], + }, + { + slug: "grok-build", + name: "Grok Build", + logo: "https://media.x.ai/v1/website/spacexai-symbol-black-transparent-6435cf42.png", + category: "ai-frameworks", + type: "Coding agent", + summary: + "Use OpenUI as a generative UI frontend for persistent Grok Build coding-agent sessions.", + howItWorks: + "The maintained local harness talks to Grok Build through its official Agent Client Protocol stdio mode, injects the generated OpenUI rules, and maps reasoning, text, tools, and interactions into AG-UI events for AgentInterface. As shipped, it is an unauthenticated single-user harness and needs authentication, sandboxing, and a stricter permission policy before networked deployment.", + links: [ + exampleLink("harnesses/grok-build", "OpenUI harness"), + { label: "Grok Build", href: "https://github.com/xai-org/grok-build", kind: "GitHub" }, + ], + }, + { + slug: "vercel-eve", + name: "Vercel Eve", + logo: "https://raw.githubusercontent.com/vercel/eve/main/.github/assets/eve.svg", + category: "ai-frameworks", + type: "Coding agent", + summary: + "Connect Vercel Eve's resumable agent sessions to OpenUI while preserving Eve's native runtime.", + howItWorks: + "The maintained harness delivers turns over Eve's native session protocol, resumes its event stream from a stored cursor, and translates Eve text, tool, and failure events into AG-UI for AgentInterface. The example uses an unauthenticated local Eve channel; a deployed version must configure channel authentication and appropriate tool permissions.", + links: [ + { + label: "Integration guide", + href: "/docs/openui-lang/examples/harnesses/vercel-eve", + kind: "Guide", + }, + exampleLink("harnesses/vercel-eve", "OpenUI harness"), + { label: "Vercel Eve", href: "https://github.com/vercel/eve", kind: "GitHub" }, + ], + }, + { + slug: "pi-coding-agent", + name: "Pi Coding Agent", + logo: "/integration-logos/pi.svg", + category: "ai-frameworks", + type: "Coding agent", + summary: + "Embed the Pi coding-agent SDK behind OpenUI and render its answers, reasoning, and tool activity.", + howItWorks: + "The maintained local harness embeds @earendil-works/pi-coding-agent in a Next.js route, injects the prompt generated from OpenUI's component library, and converts Pi text, reasoning, and tool events into an OpenAI-compatible stream consumed by openAIReadableStreamAdapter(). It exposes real filesystem and shell tools, so the shipped unauthenticated harness must not be exposed to a network as-is.", + links: [ + { + label: "Integration guide", + href: "/docs/openui-lang/examples/harnesses/pi-agent-harness", + kind: "Guide", + }, + exampleLink("harnesses/pi-agent-harness", "OpenUI harness"), + { label: "Pi", href: "https://pi.dev", kind: "Website" }, + { label: "Pi source", href: "https://github.com/earendil-works/pi", kind: "GitHub" }, + ], + }, + + { + slug: "open-webui", + name: "Open WebUI", + logo: "https://raw.githubusercontent.com/open-webui/open-webui/main/backend/open_webui/static/favicon-96x96.png", + category: "frontend-platforms", + 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 platforms. + { + slug: "vue", + name: "Vue 3", + logo: "/integration-logos/vue.svg", + category: "frontend-platforms", + 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-platforms", + 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-platforms", + 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; + +const popularityOrder: Record = { + "ai-frameworks": [ + "langchain-langgraph", + "vercel-ai-sdk", + "pi-coding-agent", + "mastra", + "grok-build", + "ag-ui", + "vercel-eve", + "google-adk", + ], + "design-systems": ["shadcn-ui", "material-ui", "handsontable", "react-email"], + "frontend-platforms": ["vue", "svelte", "react-native", "assistant-ui", "open-webui"], +}; + +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[] { + const order = popularityOrder[id]; + return integrations + .filter((item) => item.category === id) + .sort((a, b) => order.indexOf(a.slug) - order.indexOf(b.slug)); +} + +export function getRelatedIntegrations(integration: Integration, limit = 3): Integration[] { + return getIntegrationsByCategory(integration.category) + .filter((item) => 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..32cf5cbf9 --- /dev/null +++ b/docs/app/(home)/integrations/page.module.css @@ -0,0 +1,972 @@ +.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 { + background: linear-gradient( + to bottom, + var(--openui-foreground), + var(--openui-background) 9rem, + var(--openui-background) calc(100% - 9rem), + var(--openui-foreground) + ); +} + +.detailBand { + background: var(--openui-background); +} + +[data-theme="dark"] .contentBand { + background: linear-gradient( + to bottom, + #000, + var(--swatch-neutral-950) 24rem, + var(--swatch-neutral-950) calc(100% - 24rem), + #000 + ); +} + +[data-theme="dark"] .detailBand { + background: var(--swatch-neutral-950); +} + +.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; +} + +.categoryNav { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 2.5rem; + 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: minmax(0, 1fr); + gap: 1rem; + align-items: start; + margin-bottom: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--home-hairline); +} + +.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: 18rem; + flex-direction: column; + border: 1px solid var(--home-hairline); + border-radius: var(--openui-radius-4xl); + 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(-2px); + border-color: var(--openui-border-interactive, var(--openui-text-neutral-tertiary)); + box-shadow: var(--openui-shadow-m); +} + +.card:focus-visible { + outline: 2px solid var(--openui-text-neutral-primary); + outline-offset: 3px; +} + +.cardHeader { + display: flex; + align-items: flex-start; + gap: 1rem; +} + +.cardHeaderContent { + display: flex; + min-width: 0; + flex-direction: column; + gap: 0.75rem; +} + +.tags { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: flex-start; + gap: 0.5rem; +} + +.typeTag { + display: inline-flex; + height: 1.375rem; + align-items: center; + border-radius: 7px; + padding-inline: 0.5rem; + background: var(--openui-highlight-subtle); + color: var(--openui-text-neutral-secondary); + font-size: 0.75rem; + font-weight: 500; + line-height: 1; +} + +.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; + flex-shrink: 0; + border: 1px solid var(--home-hairline); + border-radius: 0.9rem; + font-size: 0.8125rem; + letter-spacing: -0.03em; +} + +.cardTitle { + margin: 0; + font-family: var(--home-font-display); + font-size: var(--home-heading-size); + font-weight: 500; + letter-spacing: var(--home-heading-tracking); + line-height: var(--home-heading-leading); +} + +.cardDescription { + margin: 1.75rem 0 0; + color: var(--openui-text-neutral-secondary); + font-family: var(--home-font-text); + font-size: var(--home-body-size); + font-weight: var(--home-body-weight); + line-height: 1.65; +} + +.cardLinks { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: auto; + padding-top: 1.5rem; +} + +.cardLink { + display: inline-flex; + min-height: 1.75rem; + align-items: center; + justify-content: center; + gap: 0.5rem; + border: 1px solid var(--openui-border-default); + border-radius: var(--openui-radius-full, 999px); + padding: 0.25rem 0.25rem 0.25rem 0.875rem; + background: var(--openui-foreground); + box-shadow: var(--openui-shadow-s); + color: var(--openui-text-neutral-primary); + font-size: 0.875rem; + font-weight: 500; + transition: + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +.cardLinkArrow { + box-sizing: border-box; + flex-shrink: 0; + width: 0; + height: 1.25rem; + padding: 0; + border-radius: 999px; + background: var(--openui-text-neutral-primary); + color: var(--openui-background); + opacity: 0; + overflow: hidden; + transition: + width 0.18s ease, + padding 0.18s ease, + opacity 0.18s ease; +} + +.card:hover .cardLink { + border-color: var(--openui-border-interactive, var(--openui-text-neutral-primary)); + box-shadow: var(--openui-shadow-m); +} + +.card:hover .cardLinkArrow { + width: 1.25rem; + padding: 0.25rem; + opacity: 1; +} + +.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; +} + +.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: var(--openui-foreground); +} + +[data-theme="dark"] .detailHero { + background: #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); +} + +.detailBackLink { + display: inline-flex; + align-items: center; + gap: 0.45rem; + margin-bottom: 2.5rem; + color: var(--openui-text-neutral-secondary); + font-size: 0.8125rem; + font-weight: 550; + text-decoration: none; + transition: color 160ms ease; +} + +.detailBackLink svg { + transition: transform 160ms ease; +} + +.detailBackLink:hover { + color: var(--openui-text-neutral-primary); +} + +.detailBackLink:hover svg { + transform: translateX(-2px); +} + +.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; +} + +.copyCommand { + display: inline-flex; + flex-shrink: 0; + gap: 0.4rem; + align-items: center; + border: 1px solid var(--home-hairline); + border-radius: 0.65rem; + padding: 0.45rem 0.65rem; + background: var(--openui-foreground); + color: var(--openui-text-neutral-secondary); + cursor: pointer; + font-family: var(--home-font-mono); + font-size: 0.6875rem; + transition: + border-color 0.18s ease, + color 0.18s ease, + background 0.18s ease; +} + +.copyCommand:hover { + border-color: color-mix(in srgb, var(--category-accent) 45%, var(--home-hairline)); + background: var(--category-soft); + color: var(--category-accent); +} + +.copyCommand:focus-visible { + outline: 2px solid var(--category-accent); + outline-offset: 2px; +} + +.copyCommand svg { + flex-shrink: 0; +} + +.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: flex-end; + 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; + } + + .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; + } + + .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, + .cardLink, + .cardLinkArrow, + .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..5354f1cf8 --- /dev/null +++ b/docs/app/(home)/integrations/page.tsx @@ -0,0 +1,148 @@ +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 { CategoryNav } from "./category-nav"; +import { getIntegrationsByCategory, integrationCategories } from "./data"; +import { IntegrationLogo } from "./integration-logo"; +import styles from "./page.module.css"; + +export const metadata: Metadata = { + title: "Integrations", + description: + "Explore OpenUI integrations across AI frameworks and protocols, design systems, and frontend platforms.", + alternates: { canonical: "/integrations" }, + openGraph: { + title: "OpenUI integrations", + description: + "Build generative UI with the AI frameworks, design systems, and frontend platforms you know.", + url: "/integrations", + type: "website", + }, +}; + +export default function IntegrationsPage() { + return ( +
+ + Build with the tools +
+ you already use. + + } + subtitle={ + <> + Start with an AI framework, design system, or frontend platform your team already knows. + + } + smallSubtitle + /> + +
+
+
+
+

+

+

+ OpenUI across your stack +

+
+

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

+
+ + ({ + count: getIntegrationsByCategory(category.id).length, + id: category.id, + label: category.shortTitle, + }))} + /> + +
+ {integrationCategories.map((category) => { + const items = getIntegrationsByCategory(category.id); + + return ( +
+
+
+

{category.title}

+

{category.description}

+
+
+ +
+ {items.map((item) => ( + +
+ +
+

{item.name}

+
+ {item.type} +
+
+
+

{item.summary}

+ + + ))} +
+
+ ); + })} +
+ +
+ +
+

Contribute an integration

+

If you want to contribute an integration, please raise an issue.

+
+ + Raise an issue + +
+
+
+ +
+
+ ); +} diff --git a/docs/app/(home)/sections/Footer/Footer.module.css b/docs/app/(home)/sections/Footer/Footer.module.css index 7b6b5d941..0560fa42b 100644 --- a/docs/app/(home)/sections/Footer/Footer.module.css +++ b/docs/app/(home)/sections/Footer/Footer.module.css @@ -26,13 +26,32 @@ /* The mark and the theme control face each other across the footer's width. */ .brandRow { - display: flex; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; - justify-content: space-between; gap: 1.5rem; padding-bottom: var(--footer-air-below); } +.footerNav { + display: flex; + align-items: center; + justify-content: center; +} + +.footerNav a { + color: var(--openui-text-neutral-secondary); + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + transition: color 0.15s ease; +} + +.footerNav a:hover, +.footerNav a:focus-visible { + color: var(--openui-text-neutral-primary); +} + .logoWrap { position: relative; /* Held to the mark's own 2.5:1 ratio, so the wordmark never stretches. */ @@ -184,6 +203,18 @@ background: #000; } +@media (max-width: 600px) { + .brandRow { + grid-template-columns: minmax(0, 1fr) auto; + } + + .footerNav { + grid-column: 1 / -1; + grid-row: 2; + justify-content: flex-start; + } +} + @media (min-width: 1024px) { .contentSection { padding-inline: 2rem; diff --git a/docs/app/(home)/sections/Footer/Footer.tsx b/docs/app/(home)/sections/Footer/Footer.tsx index f689a2977..366071b1e 100644 --- a/docs/app/(home)/sections/Footer/Footer.tsx +++ b/docs/app/(home)/sections/Footer/Footer.tsx @@ -2,6 +2,7 @@ import svgPaths from "@/imports/svg-urruvoh2be"; import { Monitor, Moon, Sun } from "@phosphor-icons/react"; import { useTheme } from "next-themes"; +import Link from "next/link"; import { useId, useSyncExternalStore } from "react"; import styles from "./Footer.module.css"; @@ -184,6 +185,9 @@ export function Footer() { the separator below. */}
+
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 ( - +