From 53b9eb3f41db7cb9a9dafe82188a3307a0780bb5 Mon Sep 17 00:00:00 2001 From: Abhin Rustagi Date: Tue, 7 Jul 2026 17:58:52 +0530 Subject: [PATCH 01/10] fix: add shadcn components --- examples/openui-cloud-shadcn/.gitignore | 48 + examples/openui-cloud-shadcn/README.md | 72 + examples/openui-cloud-shadcn/components.json | 25 + .../openui-cloud-shadcn/eslint.config.mjs | 18 + examples/openui-cloud-shadcn/next.config.ts | 7 + examples/openui-cloud-shadcn/package.json | 57 + .../openui-cloud-shadcn/postcss.config.mjs | 7 + .../src/app/api/chat/route.ts | 100 + .../src/app/api/frontend-token/route.ts | 23 + .../openui-cloud-shadcn/src/app/globals.css | 12 + .../openui-cloud-shadcn/src/app/layout.tsx | 22 + examples/openui-cloud-shadcn/src/app/page.tsx | 79 + .../src/components/ui/badge.tsx | 52 + .../src/components/ui/bubble.tsx | 128 + .../src/components/ui/button-group.tsx | 87 + .../src/components/ui/button.tsx | 58 + .../src/components/ui/card.tsx | 103 + .../src/components/ui/checkbox.tsx | 29 + .../src/components/ui/dialog.tsx | 157 ++ .../src/components/ui/field.tsx | 238 ++ .../src/components/ui/input-group.tsx | 158 ++ .../src/components/ui/input.tsx | 20 + .../src/components/ui/item.tsx | 201 ++ .../src/components/ui/label.tsx | 20 + .../src/components/ui/select.tsx | 201 ++ .../src/components/ui/separator.tsx | 25 + .../src/components/ui/skeleton.tsx | 13 + .../src/components/ui/switch.tsx | 32 + .../src/components/ui/tabs.tsx | 82 + .../src/components/ui/textarea.tsx | 18 + .../src/components/ui/toggle-group.tsx | 89 + .../src/components/ui/toggle.tsx | 45 + .../src/components/ui/tooltip.tsx | 66 + .../src/hooks/use-system-theme.tsx | 41 + examples/openui-cloud-shadcn/src/lib/env.ts | 9 + examples/openui-cloud-shadcn/tsconfig.json | 34 + pnpm-lock.yaml | 2054 +++++++---------- 37 files changed, 3230 insertions(+), 1200 deletions(-) create mode 100644 examples/openui-cloud-shadcn/.gitignore create mode 100644 examples/openui-cloud-shadcn/README.md create mode 100644 examples/openui-cloud-shadcn/components.json create mode 100644 examples/openui-cloud-shadcn/eslint.config.mjs create mode 100644 examples/openui-cloud-shadcn/next.config.ts create mode 100644 examples/openui-cloud-shadcn/package.json create mode 100644 examples/openui-cloud-shadcn/postcss.config.mjs create mode 100644 examples/openui-cloud-shadcn/src/app/api/chat/route.ts create mode 100644 examples/openui-cloud-shadcn/src/app/api/frontend-token/route.ts create mode 100644 examples/openui-cloud-shadcn/src/app/globals.css create mode 100644 examples/openui-cloud-shadcn/src/app/layout.tsx create mode 100644 examples/openui-cloud-shadcn/src/app/page.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/badge.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/bubble.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/button-group.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/button.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/card.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/checkbox.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/dialog.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/field.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/input-group.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/input.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/item.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/label.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/select.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/separator.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/skeleton.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/switch.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/tabs.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/textarea.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/toggle-group.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/toggle.tsx create mode 100644 examples/openui-cloud-shadcn/src/components/ui/tooltip.tsx create mode 100644 examples/openui-cloud-shadcn/src/hooks/use-system-theme.tsx create mode 100644 examples/openui-cloud-shadcn/src/lib/env.ts create mode 100644 examples/openui-cloud-shadcn/tsconfig.json diff --git a/examples/openui-cloud-shadcn/.gitignore b/examples/openui-cloud-shadcn/.gitignore new file mode 100644 index 000000000..3cceb2b4c --- /dev/null +++ b/examples/openui-cloud-shadcn/.gitignore @@ -0,0 +1,48 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# local thread index (created at runtime) +/.data/ + +# typescript +*.tsbuildinfo +next-env.d.ts + +# vendored @openuidev/thesys tarball — obtain separately; never commit (the +# package is distributed via the registry, not this repo). +/vendor/ diff --git a/examples/openui-cloud-shadcn/README.md b/examples/openui-cloud-shadcn/README.md new file mode 100644 index 000000000..01acedbdc --- /dev/null +++ b/examples/openui-cloud-shadcn/README.md @@ -0,0 +1,72 @@ +This is an [OpenUI](https://openui.com) Cloud project bootstrapped with [`openui-cli`](https://openui.com/docs/chat/quick-start). + +## Setup + +```bash +cp .env.example .env.local # fill THESYS_API_KEY and point the base URLs at your API +``` + +Required env: `THESYS_API_KEY`, `OPENUI_MODEL` (bare `provider/model`, e.g. `openai/gpt-5`), `DEMO_USER_ID`. + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `src/app/api/route.ts` and improving your agent +by adding system prompts or tools. + +## Switching Models + +To switch the chat model used for the app, add an `OPENUI_MODEL` env var in your .env file. Thesys Server accepts a value in the `provider/model` format compliant with [models.dev](models.dev). Some of the models supported include: + +| Provider | Model | Model String | +|----------|-----------|--------------| +| Google | Gemini 3.1 Flash Lite | google/gemini-3.1-flash-lite-free | +| Google | Gemini 3.1 Pro | google/gemini-3.1-pro-free | +| Google | Gemini 3.5 Flash | google/gemini-3.5-flash-free | +| OpenAI | GPT 5.2 | openai/gpt-5.2 | +| OpenAI | GPT 5.5 | openai/gpt-5.5 | +| OpenAI | GPT 5 | openai/gpt-5.4| +| OpenAI | GPT 5 | openai/gpt-5.4-mini| +| OpenAI | GPT 5 | openai/gpt-5.1| +| OpenAI | GPT 5 | openai/gpt-5| +| OpenAI | GPT 5 | openai/gpt-5-mini| +| OpenAI | GPT 5 | openai/gpt-5-nano| +| OpenAI | GPT 5 | openai/gpt-4.1| +| OpenAI | GPT 5 | openai/gpt-4.1-mini| +| OpenAI | GPT 5 | openai/gpt-4o| +| Anthropic | Claude Sonnet | anthropic/claude-sonnet-5| +| Anthropic | Claude Opus | anthropic/claude-opus-4.7| +| Anthropic | Claude Sonnet | anthropic/claude-sonnet-4.6| +| Anthropic | Claude Sonnet | anthropic/claude-sonnet-4| +| Anthropic | Claude Haiku | anthropic/claude-haiku-4.5| + + + +## SDK packages + +- `@openuidev/thesys-server` — the server SDK (`artifactTool`, + `createResponsesInstructions`) used by the `/api/chat` route. +- `@openuidev/thesys` — the React component library (`chatLibrary`, `Presentation`, + `Report`) used by the client page and artifact renderers. +- `@openuidev/react-headless` / `@openuidev/react-ui` — the chat UI runtime + (`AgentInterface`, storage/stream contracts, `defineArtifactRenderer`). + +## Learn More + +To learn more about OpenUI, take a look at the following resources: + +- [OpenUI Documentation](https://openui.com/docs) - learn about OpenUI features and API. +- [OpenUI GitHub repository](https://github.com/thesysdev/openui) - your feedback and contributions are welcome! diff --git a/examples/openui-cloud-shadcn/components.json b/examples/openui-cloud-shadcn/components.json new file mode 100644 index 000000000..857a178a1 --- /dev/null +++ b/examples/openui-cloud-shadcn/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-vega", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/examples/openui-cloud-shadcn/eslint.config.mjs b/examples/openui-cloud-shadcn/eslint.config.mjs new file mode 100644 index 000000000..05e726d1b --- /dev/null +++ b/examples/openui-cloud-shadcn/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/examples/openui-cloud-shadcn/next.config.ts b/examples/openui-cloud-shadcn/next.config.ts new file mode 100644 index 000000000..68a6c64d2 --- /dev/null +++ b/examples/openui-cloud-shadcn/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone", +}; + +export default nextConfig; diff --git a/examples/openui-cloud-shadcn/package.json b/examples/openui-cloud-shadcn/package.json new file mode 100644 index 000000000..687d57410 --- /dev/null +++ b/examples/openui-cloud-shadcn/package.json @@ -0,0 +1,57 @@ +{ + "name": "openui-cloud-shadcn", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 3300", + "build": "next build", + "start": "next start -p 3300", + "lint": "eslint", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@floating-ui/react-dom": "2.1.3", + "@openuidev/lang-core": "latest", + "@openuidev/react-headless": "latest", + "@openuidev/react-lang": "latest", + "@openuidev/react-ui": "latest", + "@openuidev/thesys": "latest", + "@openuidev/thesys-server": "file:~/Desktop/Repositories/genui-sdk/packages/c1-server", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-tooltip": "^1.2.0", + "@tanstack/react-table": "8.21.3", + "@tiptap/extension-placeholder": "2.27.2", + "@tiptap/react": "2.27.2", + "@tiptap/starter-kit": "2.27.2", + "clsx": "2.1.1", + "katex": "0.16.44", + "lodash": "4.17.21", + "lucide-react": "^0.575.0", + "mermaid": "11.15.0", + "next": "16.1.6", + "react": "19.2.3", + "react-dom": "19.2.3", + "recharts": "2.15.4", + "rehype-katex": "7.0.1", + "remark-breaks": "4.0.0", + "remark-gfm": "4.0.1", + "remark-math": "6.0.0", + "tiny-invariant": "1.3.3", + "unist-util-visit": "5.1.0", + "zod": "^4.0.0", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "openai": "^6.22.0", + "tailwindcss": "^4", + "typescript": "^5", + "vitest": "^4.1.0" + } +} diff --git a/examples/openui-cloud-shadcn/postcss.config.mjs b/examples/openui-cloud-shadcn/postcss.config.mjs new file mode 100644 index 000000000..61e36849c --- /dev/null +++ b/examples/openui-cloud-shadcn/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/examples/openui-cloud-shadcn/src/app/api/chat/route.ts b/examples/openui-cloud-shadcn/src/app/api/chat/route.ts new file mode 100644 index 000000000..1728ccab5 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/app/api/chat/route.ts @@ -0,0 +1,100 @@ +import { envOr, requiredEnv } from "@/lib/env"; +import { artifactTool, createResponsesInstructions } from "@openuidev/thesys-server"; +import OpenAI from "openai"; +import type { ResponseInputItem } from "openai/resources/responses/responses"; + +/** + * Generation plane: browser → THIS route → OpenUI Cloud. + * + * Calls the hosted Responses API (`POST /v1/embed/responses`) with the stock + * OpenAI SDK — the endpoint speaks the Responses protocol — and proxies the SSE + * stream straight to the browser, where `openAIResponsesAdapter` parses it + * (including the custom `response.artifact_call.delta` events). + * + * The artifact tool runs **server-side** inside OpenUI Cloud, so this route is a + * pure pipe: there is no client-side tool loop. Reads/edits go browser → /v1/* + * with the fct_ token (see /api/frontend-token + the storage adapter). + */ +export async function POST(req: Request) { + const { threadId, input } = (await req.json()) as { + threadId?: string; + input?: ResponseInputItem[]; + }; + + if (!threadId) { + return Response.json( + { error: { message: "threadId is required — create the conversation first" } }, + { status: 400 }, + ); + } + if (!Array.isArray(input) || input.length === 0) { + return Response.json( + { error: { message: "input must be a non-empty ResponseInputItem[]" } }, + { status: 400 }, + ); + } + + const client = new OpenAI({ + baseURL: "http://localhost:3102/v1/embed", + apiKey: requiredEnv("THESYS_API_KEY"), // sent as Authorization: Bearer … + }); + + let stream: AsyncIterable>; + try { + stream = (await client.responses.create( + { + model: envOr("OPENUI_MODEL", "google/gemini-3.1-pro-free"), + conversation: threadId, // store:true persists to the conversation + input, + stream: true, + store: true, + tools: [ + artifactTool({ artifacts: ["slides", "report"] }), + { + type: "web_search", + }, + { + type: "image_search", + }, + ], + instructions: createResponsesInstructions(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + { signal: req.signal }, // propagate browser aborts (stop button / tab close) + )) as unknown as AsyncIterable>; + } catch (err) { + // The SDK surfaces upstream HTTP errors (e.g. 403) as APIError. + const e = err as { status?: number; error?: unknown; message?: string }; + return Response.json( + { error: e.error ?? { message: e.message ?? "upstream error" } }, + { status: e.status ?? 502 }, + ); + } + + // Re-emit each SDK event as SSE for the browser adapter. + const encoder = new TextEncoder(); + const body = new ReadableStream({ + async start(controller) { + try { + for await (const event of stream) { + controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ type: "error", message })}\n\n`), + ); + } finally { + controller.close(); + } + }, + }); + + return new Response(body, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, no-transform", + Connection: "keep-alive", + }, + }); +} diff --git a/examples/openui-cloud-shadcn/src/app/api/frontend-token/route.ts b/examples/openui-cloud-shadcn/src/app/api/frontend-token/route.ts new file mode 100644 index 000000000..52c0a25e2 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/app/api/frontend-token/route.ts @@ -0,0 +1,23 @@ +import { envOr, requiredEnv } from "@/lib/env"; + +export async function POST() { + const upstream = await fetch(`http://localhost:3102/v1/frontend-tokens`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${requiredEnv("THESYS_API_KEY")}`, + }, + body: JSON.stringify({ user_id: envOr("DEMO_USER_ID", "demo-user") }), + }); + + if (!upstream.ok) { + const errText = await upstream + .text() + .catch(() => "There was an error in the response from the upstream service."); + console.error("[frontend-token] mint failed:", upstream.status, errText); + return Response.json({ error: { message: errText } }, { status: 502 }); + } + + const { token, expires_at } = (await upstream.json()) as { token: string; expires_at: number }; + return Response.json({ token, expires_at }); +} diff --git a/examples/openui-cloud-shadcn/src/app/globals.css b/examples/openui-cloud-shadcn/src/app/globals.css new file mode 100644 index 000000000..b60243d5e --- /dev/null +++ b/examples/openui-cloud-shadcn/src/app/globals.css @@ -0,0 +1,12 @@ +@import "tailwindcss"; + +.openui-detailed-view-panel { + flex: 1 1 0% !important; + min-height: 0 !important; + display: flex !important; + flex-direction: column !important; +} +.openui-detailed-view-panel > .thesys-artifact-standalone { + flex: 1 1 0% !important; + min-height: 0 !important; +} diff --git a/examples/openui-cloud-shadcn/src/app/layout.tsx b/examples/openui-cloud-shadcn/src/app/layout.tsx new file mode 100644 index 000000000..d0d67c899 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import { ThemeProvider } from "@/hooks/use-system-theme"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "OpenUI Cloud", + description: "Managed OpenUI Cloud Chat with web, image & artifact tools", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/examples/openui-cloud-shadcn/src/app/page.tsx b/examples/openui-cloud-shadcn/src/app/page.tsx new file mode 100644 index 000000000..adea51445 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/app/page.tsx @@ -0,0 +1,79 @@ +"use client"; +import "@openuidev/react-ui/components.css"; +import "@openuidev/thesys/styles.css"; + +import { useTheme } from "@/hooks/use-system-theme"; +import { + defineArtifactCategories, + openAIConversationMessageFormat, + openAIResponsesAdapter, + type ChatLLM, +} from "@openuidev/react-headless"; +import { AgentInterface } from "@openuidev/react-ui"; +import { + chatLibrary, + presentationArtifactRenderer, + reportArtifactRenderer, + useOpenuiCloudStorage, +} from "@openuidev/thesys"; + +const { artifactRenderers, artifactCategories } = defineArtifactCategories([ + { name: "Presentations", renderers: [presentationArtifactRenderer] }, + { name: "Reports", renderers: [reportArtifactRenderer] }, +]); + +const llm: ChatLLM = { + send: async ({ threadId, messages, signal }) => { + const latest = messages.slice(-1); + return fetch("/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ threadId, input: openAIConversationMessageFormat.toApi(latest) }), + signal, + }); + }, + streamProtocol: openAIResponsesAdapter(), +}; + +export default function Page() { + const mode = useTheme(); + const storage = useOpenuiCloudStorage({ + token: "/api/frontend-token", + apiBaseUrl: "http://localhost:3102", + features: { artifact: true }, + }); + + return ( +
+ +
+ ); +} diff --git a/examples/openui-cloud-shadcn/src/components/ui/badge.tsx b/examples/openui-cloud-shadcn/src/components/ui/badge.tsx new file mode 100644 index 000000000..b20959dd7 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/components/ui/badge.tsx @@ -0,0 +1,52 @@ +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + render, + ...props +}: useRender.ComponentProps<"span"> & VariantProps) { + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">( + { + className: cn(badgeVariants({ variant }), className), + }, + props + ), + render, + state: { + slot: "badge", + variant, + }, + }) +} + +export { Badge, badgeVariants } diff --git a/examples/openui-cloud-shadcn/src/components/ui/bubble.tsx b/examples/openui-cloud-shadcn/src/components/ui/bubble.tsx new file mode 100644 index 000000000..0b4bfe2ff --- /dev/null +++ b/examples/openui-cloud-shadcn/src/components/ui/bubble.tsx @@ -0,0 +1,128 @@ +import * as React from "react" +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +function BubbleGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +const bubbleVariants = cva( + "group/bubble relative flex w-fit max-w-[80%] min-w-0 flex-col gap-1 group-data-[align=end]/message:self-end data-[align=end]:self-end data-[variant=ghost]:max-w-full", + { + variants: { + variant: { + default: + "*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80", + secondary: + "*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]", + muted: + "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]", + tinted: + "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]", + outline: + "*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30", + ghost: + "border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50", + destructive: + "*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive dark:*:data-[slot=bubble-content]:bg-destructive/20 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Bubble({ + variant = "default", + align = "start", + className, + ...props +}: React.ComponentProps<"div"> & + VariantProps & { + align?: "start" | "end" + }) { + return ( +
+ ) +} + +function BubbleContent({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">( + { + className: cn( + "w-fit max-w-full min-w-0 overflow-hidden rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed wrap-break-word group-data-[align=end]/bubble:self-end [button]:text-left [button,a]:transition-colors [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50", + className + ), + }, + props + ), + render, + state: { + slot: "bubble-content", + }, + }) +} + +const bubbleReactionsVariants = cva( + "absolute z-10 flex w-fit shrink-0 items-center justify-center gap-1 rounded-full bg-muted px-1.5 py-0.5 text-sm ring-3 ring-card has-[button]:p-0", + { + variants: { + side: { + top: "top-0 -translate-y-3/4", + bottom: "bottom-0 translate-y-3/4", + }, + align: { + start: "left-3", + end: "right-3", + }, + }, + defaultVariants: { + side: "bottom", + align: "end", + }, + } +) + +function BubbleReactions({ + side = "bottom", + align = "end", + className, + ...props +}: React.ComponentProps<"div"> & { + align?: "start" | "end" + side?: "top" | "bottom" +}) { + return ( +
+ ) +} + +export { BubbleGroup, Bubble, BubbleContent, BubbleReactions } diff --git a/examples/openui-cloud-shadcn/src/components/ui/button-group.tsx b/examples/openui-cloud-shadcn/src/components/ui/button-group.tsx new file mode 100644 index 000000000..5266c6d37 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/components/ui/button-group.tsx @@ -0,0 +1,87 @@ +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Separator } from "@/components/ui/separator" + +const buttonGroupVariants = cva( + "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", + { + variants: { + orientation: { + horizontal: + "*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0", + vertical: + "flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0", + }, + }, + defaultVariants: { + orientation: "horizontal", + }, + } +) + +function ButtonGroup({ + className, + orientation, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function ButtonGroupText({ + className, + render, + ...props +}: useRender.ComponentProps<"div">) { + return useRender({ + defaultTagName: "div", + props: mergeProps<"div">( + { + className: cn( + "flex items-center gap-2 rounded-md border bg-muted px-2.5 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", + className + ), + }, + props + ), + render, + state: { + slot: "button-group-text", + }, + }) +} + +function ButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + ButtonGroup, + ButtonGroupSeparator, + ButtonGroupText, + buttonGroupVariants, +} diff --git a/examples/openui-cloud-shadcn/src/components/ui/button.tsx b/examples/openui-cloud-shadcn/src/components/ui/button.tsx new file mode 100644 index 000000000..76a87dd74 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import { Button as ButtonPrimitive } from "@base-ui/react/button" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + outline: + "border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5", + lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-9", + "icon-xs": + "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": + "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + ...props +}: ButtonPrimitive.Props & VariantProps) { + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/examples/openui-cloud-shadcn/src/components/ui/card.tsx b/examples/openui-cloud-shadcn/src/components/ui/card.tsx new file mode 100644 index 000000000..0ac4beb56 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/components/ui/card.tsx @@ -0,0 +1,103 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/examples/openui-cloud-shadcn/src/components/ui/checkbox.tsx b/examples/openui-cloud-shadcn/src/components/ui/checkbox.tsx new file mode 100644 index 000000000..7fa62dbc9 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/components/ui/checkbox.tsx @@ -0,0 +1,29 @@ +"use client" + +import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox" + +import { cn } from "@/lib/utils" +import { CheckIcon } from "lucide-react" + +function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/examples/openui-cloud-shadcn/src/components/ui/dialog.tsx b/examples/openui-cloud-shadcn/src/components/ui/dialog.tsx new file mode 100644 index 000000000..7e5914fb5 --- /dev/null +++ b/examples/openui-cloud-shadcn/src/components/ui/dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +import * as React from "react" +import { Dialog as DialogPrimitive } from "@base-ui/react/dialog" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { XIcon } from "lucide-react" + +function Dialog({ ...props }: DialogPrimitive.Root.Props) { + return +} + +function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { + return +} + +function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { + return +} + +function DialogClose({ ...props }: DialogPrimitive.Close.Props) { + return +} + +function DialogOverlay({ + className, + ...props +}: DialogPrimitive.Backdrop.Props) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: DialogPrimitive.Popup.Props & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + } + > + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ + className, + showCloseButton = false, + children, + ...props +}: React.ComponentProps<"div"> & { + showCloseButton?: boolean +}) { + return ( +
+ {children} + {showCloseButton && ( + }> + Close + + )} +
+ ) +} + +function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: DialogPrimitive.Description.Props) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/examples/openui-cloud-shadcn/src/components/ui/field.tsx b/examples/openui-cloud-shadcn/src/components/ui/field.tsx new file mode 100644 index 000000000..383e7c83e --- /dev/null +++ b/examples/openui-cloud-shadcn/src/components/ui/field.tsx @@ -0,0 +1,238 @@ +"use client" + +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +const fieldVariants = cva( + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +