diff --git a/docs/content/docs/chat/artifacts.mdx b/docs/content/docs/chat/artifacts.mdx new file mode 100644 index 000000000..0c0b91741 --- /dev/null +++ b/docs/content/docs/chat/artifacts.mdx @@ -0,0 +1,173 @@ +--- +title: Artifacts +description: Add side-panel content that opens from inline previews in chat. +--- + +Artifacts let a component render a compact inline preview inside the chat message and expand into a full side panel when clicked. Use them for code viewers, document previews, embedded frames, or any content that benefits from a larger canvas. + +```tsx +import { defineComponent } from "@openuidev/react-lang"; +import { Artifact } from "@openuidev/react-ui"; +import { z } from "zod"; + +const ArtifactCodeBlock = defineComponent({ + name: "ArtifactCodeBlock", + props: z.object({ + language: z.string(), + title: z.string(), + codeString: z.string(), + }), + description: "Code block that opens in the artifact side panel", + component: Artifact({ + title: (props) => props.title, + preview: (props, { open, isActive }) => ( + + ), + panel: (props) => ( + {props.codeString} + ), + }), +}); +``` + +## How it works + +An artifact component has two parts: + +- **Preview** — a compact element rendered inline in the chat message. It receives an `open` callback to activate the side panel. +- **Panel** — the full content rendered inside `ArtifactPanel`, portaled into the `ArtifactPortalTarget` in your layout. Only one panel is visible at a time. + +`Artifact()` is a factory function that wires these together. It generates a `ComponentRenderer` that handles ID generation, artifact state, and panel portaling internally. Pass the result as the `component` field of `defineComponent`. + +## `Artifact()` config + +```ts +import { Artifact } from "@openuidev/react-ui"; + +Artifact({ + title, // string | (props) => string + preview, // (props, controls) => ReactNode + panel, // (props, controls) => ReactNode + panelProps, // optional — className, errorFallback, header +}); +``` + +| Option | Type | Description | +|--------|------|-------------| +| `title` | `string \| (props: P) => string` | Panel header title. Static string or derived from props. | +| `preview` | `(props: P, controls: ArtifactControls) => ReactNode` | Inline preview rendered in the chat message. | +| `panel` | `(props: P, controls: ArtifactControls) => ReactNode` | Content rendered inside the side panel. | +| `panelProps` | `{ className?, errorFallback?, header? }` | Optional overrides forwarded to `ArtifactPanel`. | + +Both `preview` and `panel` receive the full Zod-inferred props as the first argument and `ArtifactControls` as the second. + +## `ArtifactControls` + +The controls object passed to `preview` and `panel` render functions. + +```ts +interface ArtifactControls { + isActive: boolean; // whether this artifact's panel is currently open + open: () => void; // activate this artifact + close: () => void; // deactivate this artifact + toggle: () => void; // toggle open/close +} +``` + +The preview typically uses `open` and `isActive` to show a click-to-expand button. The panel can use `close` to render a dismiss button inside the panel body. + +## Layout setup + +Built-in layouts (`FullScreen`, `Copilot`, `BottomTray`) mount `ArtifactPortalTarget` automatically. Artifact panels render into this target with no extra setup. + +If you build a custom layout with the headless hooks, mount one `ArtifactPortalTarget` in your layout where the panel should appear. + +```tsx +import { ArtifactPortalTarget } from "@openuidev/react-ui"; + +function Layout() { + return ( +
+
{/* chat area */}
+ +
+ ); +} +``` + +Only one `ArtifactPortalTarget` should be mounted at a time. All artifact panels portal into this single element. + +## Headless hooks + +For custom layouts or advanced control, use the artifact hooks from `@openuidev/react-headless`. + +### `useArtifact(id)` + +Binds a component to a specific artifact by ID. Returns activation state and actions. + +```ts +import { useArtifact } from "@openuidev/react-headless"; + +const { isActive, open, close, toggle } = useArtifact(artifactId); +``` + +### `useActiveArtifact()` + +Returns global artifact state — whether any artifact is open, and a close action. Use this in layout components that resize or show overlays when any artifact is active. + +```ts +import { useActiveArtifact } from "@openuidev/react-headless"; + +const { isArtifactActive, activeArtifactId, closeArtifact } = useActiveArtifact(); +``` + +Both hooks require a `ChatProvider` ancestor in the component tree. + +## Manual wiring + +If `Artifact()` does not fit your use case, wire the pieces directly. This is the escape hatch for full control. + +```tsx +import { defineComponent } from "@openuidev/react-lang"; +import { ArtifactPanel } from "@openuidev/react-ui"; +import { useArtifact } from "@openuidev/react-headless"; +import { useId } from "react"; + +const CustomArtifact = defineComponent({ + name: "CustomArtifact", + props: CustomSchema, + description: "Artifact with full manual control", + component: ({ props }) => { + const artifactId = useId(); + const { isActive, open, close } = useArtifact(artifactId); + + return ( + <> + + +
{/* panel content */}
+
+ + ); + }, +}); +``` + +`ArtifactPanel` accepts `artifactId`, `title`, `children`, `className`, `errorFallback`, and `header` (boolean or custom ReactNode). It renders nothing when the artifact is inactive. + +## Related guides + + + + Create custom openui-lang components with `defineComponent`. + + + Build a fully custom chat UI with headless hooks. + + + Full reference for all headless hooks. + + + Adjust colors, mode, and theme overrides. + + diff --git a/docs/content/docs/chat/meta.json b/docs/content/docs/chat/meta.json index ff4a9110d..062aa6803 100644 --- a/docs/content/docs/chat/meta.json +++ b/docs/content/docs/chat/meta.json @@ -12,6 +12,7 @@ "copilot", "fullscreen", "bottom-tray", + "artifacts", "---Configurations---", "connecting", "persistence", diff --git a/examples/openui-artifact-demo/.dockerignore b/examples/openui-artifact-demo/.dockerignore new file mode 100644 index 000000000..999ffc10a --- /dev/null +++ b/examples/openui-artifact-demo/.dockerignore @@ -0,0 +1,39 @@ +# Node +node_modules +.pnpm-store +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Next.js +.next +out + +# Git +.git +.gitignore + +# Logs +logs +*.log + +# Env files +.env +.env.* +!.env.example + +# OS files +.DS_Store +Thumbs.db + +# Build / cache +dist +build +.turbo +.cache +coverage + +# Editor +.vscode +.idea \ No newline at end of file diff --git a/examples/openui-artifact-demo/.gitignore b/examples/openui-artifact-demo/.gitignore new file mode 100644 index 000000000..5ef6a5207 --- /dev/null +++ b/examples/openui-artifact-demo/.gitignore @@ -0,0 +1,41 @@ +# 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 + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/openui-artifact-demo/Dockerfile b/examples/openui-artifact-demo/Dockerfile new file mode 100644 index 000000000..b3a36dfe1 --- /dev/null +++ b/examples/openui-artifact-demo/Dockerfile @@ -0,0 +1,73 @@ +# syntax=docker/dockerfile:1.7 +# -------------------------------------------------- +# Build stage +# -------------------------------------------------- +FROM node:20-alpine AS builder + +WORKDIR /app + +RUN apk add --no-cache libc6-compat +ARG PNPM_VERSION=9.12.0 +RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate + +ENV NEXT_TELEMETRY_DISABLED=1 + +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.json ./ + +COPY packages/openui-cli/package.json ./packages/openui-cli/ +COPY packages/react-ui/package.json ./packages/react-ui/ +COPY packages/react-headless/package.json ./packages/react-headless/ +COPY packages/react-lang/package.json ./packages/react-lang/ +COPY examples/openui-chat/package.json ./examples/openui-chat/ + +RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile --ignore-scripts + +COPY packages/openui-cli ./packages/openui-cli +COPY packages/react-ui ./packages/react-ui +COPY packages/react-headless ./packages/react-headless +COPY packages/react-lang ./packages/react-lang +COPY examples/openui-chat ./examples/openui-chat + +RUN pnpm --filter @openuidev/cli build +RUN pnpm --filter @openuidev/react-ui build +RUN pnpm --filter @openuidev/react-headless build +RUN pnpm --filter @openuidev/react-lang build + +WORKDIR /app/examples/openui-chat +RUN node /app/packages/openui-cli/dist/index.js generate src/library.ts --out src/generated/system-prompt.txt \ + && pnpm build + + + +# -------------------------------------------------- +# Runtime stage +# -------------------------------------------------- +FROM node:20-alpine AS runner + +WORKDIR /app + +RUN apk add --no-cache libc6-compat + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 HOSTNAME=0.0.0.0 + +RUN addgroup -S nodejs && adduser -S nextjs -G nodejs +USER nextjs + +# Copy full standalone output to avoid brittle partial-copy assumptions +COPY --from=builder --chown=nextjs:nodejs /app/examples/openui-chat/.next/standalone ./ + +# Static assets expected by Next at runtime +COPY --from=builder --chown=nextjs:nodejs /app/examples/openui-chat/.next/static ./examples/openui-chat/.next/static + +# If your app has a public directory, include this line +# COPY --from=builder --chown=nextjs:nodejs /app/examples/openui-chat/public ./examples/openui-chat/public + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:3000').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +CMD ["node", "examples/openui-chat/server.js"] \ No newline at end of file diff --git a/examples/openui-artifact-demo/README.md b/examples/openui-artifact-demo/README.md new file mode 100644 index 000000000..e2f54d200 --- /dev/null +++ b/examples/openui-artifact-demo/README.md @@ -0,0 +1,46 @@ +# OpenUI Artifact Demo + +A demo application showcasing the OpenUI artifact system for displaying generated code in a resizable side panel. + +## Features + +- **Artifact Code Blocks**: AI-generated code appears as compact previews in chat +- **Side Panel**: Click "View Code" to open the full code in a resizable artifact panel +- **Syntax Highlighting**: Full Prism-based syntax highlighting in the artifact panel +- **Multiple Artifacts**: Multiple code blocks per conversation, one active at a time +- **Copy to Clipboard**: One-click code copying from the artifact panel + +## Getting Started + +```bash +# Install dependencies (from repo root) +pnpm install + +# Generate the system prompt +pnpm --filter openui-artifact-demo generate:prompt + +# Start the development server +pnpm --filter openui-artifact-demo dev +``` + +Set your OpenAI API key: +```bash +export OPENAI_API_KEY=your-key-here +``` + +## How It Works + +This example extends the standard OpenUI chat library with a custom `ArtifactCodeBlock` component that integrates with the OpenUI artifact system: + +1. User asks for code (e.g., "Build me a React login form") +2. AI generates a response using `ArtifactCodeBlock` components +3. Each code block shows an inline preview in the chat +4. Clicking "View Code" opens the full code in the artifact side panel +5. The panel is resizable and supports syntax highlighting + copy + +## Architecture + +- `src/components/ArtifactCodeBlock/` — Custom genui component with inline preview and artifact panel view +- `src/library.ts` — Extended component library with ArtifactCodeBlock +- `src/app/page.tsx` — Main page using FullScreen layout +- `src/app/api/chat/route.ts` — API route for OpenAI streaming diff --git a/examples/openui-artifact-demo/eslint.config.mjs b/examples/openui-artifact-demo/eslint.config.mjs new file mode 100644 index 000000000..05e726d1b --- /dev/null +++ b/examples/openui-artifact-demo/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-artifact-demo/next.config.ts b/examples/openui-artifact-demo/next.config.ts new file mode 100644 index 000000000..d6663987d --- /dev/null +++ b/examples/openui-artifact-demo/next.config.ts @@ -0,0 +1,9 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + output: "standalone", + turbopack: {}, + transpilePackages: ["@openuidev/react-ui", "@openuidev/react-headless", "@openuidev/react-lang"], +}; + +export default nextConfig; diff --git a/examples/openui-artifact-demo/package.json b/examples/openui-artifact-demo/package.json new file mode 100644 index 000000000..567500334 --- /dev/null +++ b/examples/openui-artifact-demo/package.json @@ -0,0 +1,36 @@ +{ + "name": "openui-artifact-demo", + "version": "0.1.0", + "private": true, + "scripts": { + "generate:prompt": "pnpm --filter @openuidev/cli build && pnpm exec openui generate src/library.ts --out src/generated/system-prompt.txt", + "dev": "pnpm generate:prompt && next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "@openuidev/react-headless": "workspace:*", + "@openuidev/react-lang": "workspace:*", + "@openuidev/react-ui": "workspace:*", + "lucide-react": "^0.575.0", + "react-syntax-highlighter": "^15.6.1", + "zod": "^4.0.0", + "next": "16.1.6", + "openai": "^6.22.0", + "react": "19.2.3", + "react-dom": "19.2.3" + }, + "devDependencies": { + "@openuidev/cli": "workspace:*", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/react-syntax-highlighter": "^15.5.13", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/examples/openui-artifact-demo/postcss.config.mjs b/examples/openui-artifact-demo/postcss.config.mjs new file mode 100644 index 000000000..61e36849c --- /dev/null +++ b/examples/openui-artifact-demo/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/examples/openui-artifact-demo/src/app/api/chat/route.ts b/examples/openui-artifact-demo/src/app/api/chat/route.ts new file mode 100644 index 000000000..40ad28c27 --- /dev/null +++ b/examples/openui-artifact-demo/src/app/api/chat/route.ts @@ -0,0 +1,303 @@ +import { readFileSync } from "fs"; +import { NextRequest } from "next/server"; +import OpenAI from "openai"; +import type { ChatCompletionMessageParam } from "openai/resources/chat/completions.mjs"; +import { join } from "path"; + +const systemPrompt = readFileSync(join(process.cwd(), "src/generated/system-prompt.txt"), "utf-8"); + +// ── Tool implementations ── + +function getWeather({ location }: { location: string }): Promise { + return new Promise((resolve) => { + setTimeout(() => { + const knownTemps: Record = { + tokyo: 22, "san francisco": 18, london: 14, "new york": 25, + paris: 19, sydney: 27, mumbai: 33, berlin: 16, + }; + const conditions = ["Sunny", "Partly Cloudy", "Cloudy", "Light Rain", "Clear Skies"]; + const temp = knownTemps[location.toLowerCase()] ?? Math.floor(Math.random() * 30 + 5); + const condition = conditions[Math.floor(Math.random() * conditions.length)]; + resolve(JSON.stringify({ + location, temperature_celsius: temp, + temperature_fahrenheit: Math.round(temp * 1.8 + 32), + condition, + humidity_percent: Math.floor(Math.random() * 40 + 40), + wind_speed_kmh: Math.floor(Math.random() * 25 + 5), + forecast: [ + { day: "Tomorrow", high: temp + 2, low: temp - 4, condition: "Partly Cloudy" }, + { day: "Day After", high: temp + 1, low: temp - 3, condition: "Sunny" }, + ], + })); + }, 800); + }); +} + +function getStockPrice({ symbol }: { symbol: string }): Promise { + return new Promise((resolve) => { + setTimeout(() => { + const s = symbol.toUpperCase(); + const knownPrices: Record = { + AAPL: 189.84, GOOGL: 141.8, TSLA: 248.42, MSFT: 378.91, + AMZN: 178.25, NVDA: 875.28, META: 485.58, + }; + const price = knownPrices[s] ?? Math.floor(Math.random() * 500 + 20); + const change = parseFloat((Math.random() * 8 - 4).toFixed(2)); + resolve(JSON.stringify({ + symbol: s, + price: parseFloat((price + change).toFixed(2)), + change, change_percent: parseFloat(((change / price) * 100).toFixed(2)), + volume: `${(Math.random() * 50 + 10).toFixed(1)}M`, + day_high: parseFloat((price + Math.abs(change) + 1.5).toFixed(2)), + day_low: parseFloat((price - Math.abs(change) - 1.2).toFixed(2)), + })); + }, 600); + }); +} + +function calculate({ expression }: { expression: string }): Promise { + return new Promise((resolve) => { + setTimeout(() => { + try { + const sanitized = expression.replace(/[^0-9+\-*/().%\s,Math.sqrtpowabsceilfloorround]/g, ""); + + const result = new Function(`return (${sanitized})`)(); + resolve(JSON.stringify({ expression, result: Number(result) })); + } catch { + resolve(JSON.stringify({ expression, error: "Invalid expression" })); + } + }, 300); + }); +} + +function searchWeb({ query }: { query: string }): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(JSON.stringify({ + query, + results: [ + { title: `Top result for "${query}"`, snippet: `Comprehensive overview of ${query} with the latest information.` }, + { title: `${query} - Latest News`, snippet: `Recent developments and updates related to ${query}.` }, + { title: `Understanding ${query}`, snippet: `An in-depth guide explaining everything about ${query}.` }, + ], + })); + }, 1000); + }); +} + +// ── Tool definitions ── + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const tools: any[] = [ + { + type: "function", + function: { + name: "get_weather", + description: "Get current weather for a location.", + parameters: { + type: "object", + properties: { location: { type: "string", description: "City name" } }, + required: ["location"], + }, + function: getWeather, + parse: JSON.parse, + }, + }, + { + type: "function", + function: { + name: "get_stock_price", + description: "Get stock price for a ticker symbol.", + parameters: { + type: "object", + properties: { symbol: { type: "string", description: "Ticker symbol, e.g. AAPL" } }, + required: ["symbol"], + }, + function: getStockPrice, + parse: JSON.parse, + }, + }, + { + type: "function", + function: { + name: "calculate", + description: "Evaluate a math expression.", + parameters: { + type: "object", + properties: { expression: { type: "string", description: "Math expression to evaluate" } }, + required: ["expression"], + }, + function: calculate, + parse: JSON.parse, + }, + }, + { + type: "function", + function: { + name: "search_web", + description: "Search the web for information.", + parameters: { + type: "object", + properties: { query: { type: "string", description: "Search query" } }, + required: ["query"], + }, + function: searchWeb, + parse: JSON.parse, + }, + }, +]; + +// ── SSE helpers ── + +function sseToolCallStart( + encoder: TextEncoder, + tc: { id: string; function: { name: string } }, + index: number, +) { + return encoder.encode( + `data: ${JSON.stringify({ + id: `chatcmpl-tc-${tc.id}`, + object: "chat.completion.chunk", + choices: [{ + index: 0, + delta: { + tool_calls: [{ index, id: tc.id, type: "function", function: { name: tc.function.name, arguments: "" } }], + }, + finish_reason: null, + }], + })}\n\n`, + ); +} + +function sseToolCallArgs( + encoder: TextEncoder, + tc: { id: string; function: { arguments: string } }, + result: string, + index: number, +) { + let enrichedArgs: string; + try { + enrichedArgs = JSON.stringify({ _request: JSON.parse(tc.function.arguments), _response: JSON.parse(result) }); + } catch { + enrichedArgs = tc.function.arguments; + } + return encoder.encode( + `data: ${JSON.stringify({ + id: `chatcmpl-tc-${tc.id}-args`, + object: "chat.completion.chunk", + choices: [{ + index: 0, + delta: { tool_calls: [{ index, function: { arguments: enrichedArgs } }] }, + finish_reason: null, + }], + })}\n\n`, + ); +} + +// ── Route handler ── + +export async function POST(req: NextRequest) { + const { messages } = await req.json(); + + const client = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + }); + const MODEL = process.env.OPENAI_MODEL || "gpt-4o"; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cleanMessages = (messages as any[]) + .filter((m) => m.role !== "tool") + .map((m) => { + if (m.role === "assistant" && m.tool_calls?.length) { + // Strip tool_calls (runTools re-runs the agentic loop server-side) + // but preserve content so prior replies remain in context. + const { tool_calls: _tc, ...rest } = m; // eslint-disable-line @typescript-eslint/no-unused-vars + return rest; + } + return m; + }); + + const chatMessages: ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...cleanMessages, + ]; + + const encoder = new TextEncoder(); + let controllerClosed = false; + + const readable = new ReadableStream({ + start(controller) { + const enqueue = (data: Uint8Array) => { + if (controllerClosed) return; + try { controller.enqueue(data); } catch { /* already closed */ } + }; + const close = () => { + if (controllerClosed) return; + controllerClosed = true; + try { controller.close(); } catch { /* already closed */ } + }; + + const pendingCalls: Array<{ id: string; name: string; arguments: string }> = []; + let callIdx = 0; + let resultIdx = 0; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const runner = (client.chat.completions as any).runTools({ + model: MODEL, + messages: chatMessages, + tools, + stream: true + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + runner.on("functionToolCall", (fc: any) => { + const id = `tc-${callIdx}`; + pendingCalls.push({ id, name: fc.name, arguments: fc.arguments }); + enqueue(sseToolCallStart(encoder, { id, function: { name: fc.name } }, callIdx)); + callIdx++; + }); + + runner.on("functionToolCallResult", (result: string) => { + const tc = pendingCalls[resultIdx]; + if (tc) { + enqueue(sseToolCallArgs(encoder, { id: tc.id, function: { arguments: tc.arguments } }, result, resultIdx)); + } + resultIdx++; + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + runner.on("chunk", (chunk: any) => { + const choice = chunk.choices?.[0]; + const delta = choice?.delta; + if (!delta) return; + if (delta.content) { + enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); + } + if (choice?.finish_reason === "stop") { + enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)); + } + }); + + runner.on("end", () => { + enqueue(encoder.encode("data: [DONE]\n\n")); + close(); + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + runner.on("error", (err: any) => { + const msg = err instanceof Error ? err.message : "Stream error"; + console.error("Chat route error:", msg); + enqueue(encoder.encode(`data: ${JSON.stringify({ error: msg })}\n\n`)); + close(); + }); + }, + }); + + return new Response(readable, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, no-transform", + Connection: "keep-alive", + }, + }); +} diff --git a/examples/openui-artifact-demo/src/app/globals.css b/examples/openui-artifact-demo/src/app/globals.css new file mode 100644 index 000000000..3d552a61f --- /dev/null +++ b/examples/openui-artifact-demo/src/app/globals.css @@ -0,0 +1,2 @@ +@import "tailwindcss"; + diff --git a/examples/openui-artifact-demo/src/app/layout.tsx b/examples/openui-artifact-demo/src/app/layout.tsx new file mode 100644 index 000000000..7e44b0451 --- /dev/null +++ b/examples/openui-artifact-demo/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 Chat", + description: "Generative UI Chat with OpenAI SDK", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/examples/openui-artifact-demo/src/app/page.tsx b/examples/openui-artifact-demo/src/app/page.tsx new file mode 100644 index 000000000..a68e6c6f9 --- /dev/null +++ b/examples/openui-artifact-demo/src/app/page.tsx @@ -0,0 +1,57 @@ +"use client"; +import "@openuidev/react-ui/components.css"; + +import { useTheme } from "@/hooks/use-system-theme"; +import { openAIAdapter, openAIMessageFormat } from "@openuidev/react-headless"; +import { FullScreen } from "@openuidev/react-ui"; +import { artifactDemoLibrary } from "@/library"; + +export default function Page() { + const mode = useTheme(); + + return ( +
+ { + return fetch("/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + messages: openAIMessageFormat.toApi(messages), + }), + signal: abortController.signal, + }); + }} + streamProtocol={openAIAdapter()} + componentLibrary={artifactDemoLibrary} + agentName="Artifact Demo" + theme={{ mode }} + conversationStarters={{ + variant: "short", + options: [ + { + displayText: "React login form", + prompt: + "Build me a React login form with email and password validation", + }, + { + displayText: "Python REST API", + prompt: + "Create a FastAPI REST API with CRUD endpoints for a todo app", + }, + { + displayText: "CSS animation", + prompt: + "Write a CSS animation for a bouncing loading indicator", + }, + { + displayText: "SQL schema", + prompt: + "Design a SQL schema for a blog with users, posts, and comments", + }, + ], + }} + /> +
+ ); +} diff --git a/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/ArtifactView.tsx b/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/ArtifactView.tsx new file mode 100644 index 000000000..bd5d85b2a --- /dev/null +++ b/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/ArtifactView.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"; +import { Copy, CheckCheck } from "lucide-react"; + +interface ArtifactViewProps { + language: string; + codeString: string; + title: string; +} + +export function ArtifactView({ language, codeString, title }: ArtifactViewProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(codeString); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // Fallback for environments where clipboard API is unavailable + const textarea = document.createElement("textarea"); + textarea.value = codeString; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }, [codeString]); + + return ( +
+ {/* Toolbar */} +
+ {title} +
+ + {language} + + +
+
+ + {/* Code */} +
+ + {codeString} + +
+
+ ); +} diff --git a/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/InlinePreview.tsx b/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/InlinePreview.tsx new file mode 100644 index 000000000..b75b860e5 --- /dev/null +++ b/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/InlinePreview.tsx @@ -0,0 +1,63 @@ +"use client"; + +interface InlinePreviewProps { + language: string; + title: string; + codeString: string; + open: () => void; + isActive: boolean; +} + +export function InlinePreview({ language, title, codeString, open, isActive }: InlinePreviewProps) { + const truncatedCode = codeString.split("\n").slice(0, 6).join("\n"); + + return ( +
+ {/* Header */} +
+
+ + + + + {title} +
+ + {language} + +
+ + {/* Code preview */} +
+
+          {truncatedCode}
+        
+ {/* Gradient fade */} +
+
+ + {/* Footer */} +
+ +
+
+ ); +} diff --git a/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/index.tsx b/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/index.tsx new file mode 100644 index 000000000..ecf780567 --- /dev/null +++ b/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/index.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Artifact } from "@openuidev/react-ui"; +import { ArtifactView } from "./ArtifactView"; +import { InlinePreview } from "./InlinePreview"; +import { ArtifactCodeBlockSchema } from "./schema"; + +export { ArtifactCodeBlockSchema } from "./schema"; +export type { ArtifactCodeBlockProps } from "./schema"; + +export const ArtifactCodeBlock = defineComponent({ + name: "ArtifactCodeBlock", + props: ArtifactCodeBlockSchema, + description: + "Code block that opens in the artifact side panel for full viewing with syntax highlighting", + component: Artifact({ + title: (props) => props.title as string, + preview: (props, { open, isActive }) => ( + + ), + panel: (props) => ( + + ), + }), +}); diff --git a/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/schema.ts b/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/schema.ts new file mode 100644 index 000000000..02ce83ac7 --- /dev/null +++ b/examples/openui-artifact-demo/src/components/ArtifactCodeBlock/schema.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const ArtifactCodeBlockSchema = z.object({ + language: z.string(), + title: z.string(), + codeString: z.string(), +}); + +export type ArtifactCodeBlockProps = z.infer; diff --git a/examples/openui-artifact-demo/src/generated/system-prompt.txt b/examples/openui-artifact-demo/src/generated/system-prompt.txt new file mode 100644 index 000000000..61e4d87c1 --- /dev/null +++ b/examples/openui-artifact-demo/src/generated/system-prompt.txt @@ -0,0 +1,218 @@ +You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang. + +## Syntax Rules + +1. Each statement is on its own line: `identifier = Expression` +2. `root` is the entry point — every program must define `root = Card(...)` +3. Expressions are: strings ("..."), numbers, booleans (true/false), arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...) +4. Use references for readability: define `name = ...` on one line, then use `name` later +5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array. +6. Arguments are POSITIONAL (order matters, not names) +7. Optional arguments can be omitted from the end +8. No operators, no logic, no variables — only declarations +9. Strings use double quotes with backslash escaping + +## Component Signatures + +Arguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming. +The `action` prop type accepts: ContinueConversation (sends message to LLM), OpenUrl (navigates to URL), or Custom (app-defined). + +### Content +CardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle +TextContent(text: string, size?: "small" | "default" | "large" | "small-heavy" | "large-heavy") — Text block. Supports markdown. Optional size: "small" | "default" | "large" | "small-heavy" | "large-heavy". +MarkDownRenderer(textMarkdown: string, variant?: "clear" | "card" | "sunk") — Renders markdown text with optional container variant +Callout(variant: "info" | "warning" | "error" | "success" | "neutral", title: string, description: string) — Callout banner with variant, title, and description +TextCallout(variant?: "neutral" | "info" | "warning" | "success" | "danger", title?: string, description?: string) — Text callout with variant, title, and description +Image(alt: string, src?: string) — Image with alt text and optional URL +ImageBlock(src: string, alt?: string) — Image block with loading state +ImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview +CodeBlock(language: string, codeString: string) — Syntax-highlighted code block +Separator(orientation?: "horizontal" | "vertical", decorative?: boolean) — Visual divider between content sections +ArtifactCodeBlock(language: string, title: string, codeString: string) — Code block that opens in the artifact side panel for full viewing with syntax highlighting + +### Tables +Table(columns: Col[], rows: (string | number | boolean)[][]) — Data table +Col(label: string, type?: "string" | "number" | "action") — Column definition + +### Charts (2D) +BarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Vertical bars; use for comparing values across categories with one or more series +LineChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Lines over categories; use for trends and continuous data over time +AreaChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Filled area under lines; use for cumulative totals or volume trends over time +RadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities +HorizontalBarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists +Series(category: string, values: number[]) — One data series + +### Charts (1D) +PieChart(slices: Slice[], variant?: "pie" | "donut") — Circular slices showing part-to-whole proportions; supports pie and donut variants +RadialChart(slices: Slice[]) — Radial bars showing proportional distribution across named segments +SingleStackedBarChart(slices: Slice[]) — Single horizontal stacked bar; use for showing part-to-whole proportions in one row +Slice(category: string, value: number) — One slice with label and numeric value + +### Charts (Scatter) +ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering +ScatterSeries(name: string, points: Point[]) — Named dataset +Point(x: number, y: number, z?: number) — Data point with numeric coordinates + +### Forms +Form(name: string, buttons: Buttons, fields) — Form container with fields and explicit action buttons +FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup, hint?: string) — Field with label, input component, and optional hint text +Label(text: string) — Text label +Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) +TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) +Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) +SelectItem(value: string, label: string) — Option for Select +DatePicker(name: string, mode: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) +Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) — Numeric slider input; supports continuous and discrete (stepped) variants +CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) +CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean) +RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}) +RadioItem(label: string, description: string, value: string) +SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk") — Group of switch toggles +SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle +- Define EACH FormControl as its own reference — do NOT inline all controls in one array. +- NEVER nest Form inside Form. +- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument. +- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 } +- The renderer shows error messages automatically — do NOT generate error text in the UI + +### Buttons +Button(label: string, action?: {type: "open_url", url: string} | {type: "continue_conversation", context?: string} | {type: string, params?}, variant?: "primary" | "secondary" | "tertiary", type?: "normal" | "destructive", size?: "extra-small" | "small" | "medium" | "large") — Clickable button +Buttons(buttons: Button[], direction?: "row" | "column") — Group of Button components. direction: "row" (default) | "column". + +### Lists & Follow-ups +ListBlock(items: ListItem[], variant?: "number" | "image") — A list of items with number or image indicators. Each item can optionally have an action. +ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: {type: "open_url", url: string} | {type: "continue_conversation", context?: string} | {type: string, params?}) — Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable. +FollowUpBlock(items: FollowUpItem[]) — List of clickable follow-up suggestions placed at the end of a response +FollowUpItem(text: string) — Clickable follow-up suggestion — when clicked, sends text as user message +- Use ListBlock with ListItem references for numbered, clickable lists. +- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions. +- Clicking a ListItem or FollowUpItem sends its text to the LLM as a user message. +- Example: list = ListBlock([item1, item2]) item1 = ListItem("Option A", "Details about A") + +### Sections +SectionBlock(sections: SectionItem[], isFoldable?: boolean) — Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section. +SectionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | ListBlock | FollowUpBlock)[]) — Section with a label and collapsible content — used inside SectionBlock +- SectionBlock renders collapsible accordion sections that auto-open as they stream. +- Each section needs a unique `value` id, a `trigger` label, and a `content` array. +- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem("intro", "Introduction", [content1]) +- Set isFoldable=false to render sections as flat headers instead of accordion. + +### Layout +Tabs(items: TabItem[]) — Tabbed container +TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is tab label, content is array of components +Accordion(items: AccordionItem[]) — Collapsible sections +AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is section title +Steps(items: StepsItem[]) — Step-by-step guide +StepsItem(title: string, details: string) — title and details text for one step +Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[][], variant?: "card" | "sunk") — Horizontal scrollable carousel +- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array. +- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]]) +- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order. +- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern. +- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs. + +### Data Display +TagBlock(tags: string[]) — tags is an array of strings +Tag(text: string, icon?: string, size?: "sm" | "md" | "lg", variant?: "neutral" | "info" | "success" | "warning" | "danger") — Styled tag/badge with optional icon and variant + +### Ungrouped +Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically. + +## Hoisting & Streaming (CRITICAL) + +openui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed. + +During streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in. + +**Recommended statement order for optimal streaming:** +1. `root = Card(...)` — UI shell appears immediately +2. Component definitions — fill in as they stream +3. Data values — leaf content last + +Always write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in. + +## Examples + +Example 1 — Table with follow-ups: +root = Card([title, tbl, followUps]) +title = TextContent("Top Languages", "large-heavy") +tbl = Table(cols, rows) +cols = [Col("Language", "string"), Col("Users (M)", "number"), Col("Year", "number")] +rows = [["Python", 15.7, 1991], ["JavaScript", 14.2, 1995], ["Java", 12.1, 1995]] +followUps = FollowUpBlock([fu1, fu2]) +fu1 = FollowUpItem("Tell me more about Python") +fu2 = FollowUpItem("Show me a JavaScript comparison") + +Example 2 — Clickable list: +root = Card([title, list]) +title = TextContent("Choose a topic", "large-heavy") +list = ListBlock([item1, item2, item3]) +item1 = ListItem("Getting started", "New to the platform? Start here.") +item2 = ListItem("Advanced features", "Deep dives into powerful capabilities.") +item3 = ListItem("Troubleshooting", "Common issues and how to fix them.") + +Example 3 — Image carousel with consistent slides + follow-ups: +root = Card([header, carousel, followups]) +header = CardHeader("Featured Destinations", "Discover highlights and best time to visit") +carousel = Carousel([[t1, img1, d1, tags1], [t2, img2, d2, tags2], [t3, img3, d3, tags3]], "card") +t1 = TextContent("Paris, France", "large-heavy") +img1 = ImageBlock("https://picsum.photos/seed/paris/800/500", "Eiffel Tower at night") +d1 = TextContent("City of light — best Apr–Jun and Sep–Oct.", "default") +tags1 = TagBlock(["Landmark", "City Break", "Culture"]) +t2 = TextContent("Kyoto, Japan", "large-heavy") +img2 = ImageBlock("https://picsum.photos/seed/kyoto/800/500", "Bamboo grove in Arashiyama") +d2 = TextContent("Temples and bamboo groves — best Mar–Apr and Nov.", "default") +tags2 = TagBlock(["Temples", "Autumn", "Culture"]) +t3 = TextContent("Machu Picchu, Peru", "large-heavy") +img3 = ImageBlock("https://picsum.photos/seed/machupicchu/800/500", "Inca citadel in the clouds") +d3 = TextContent("High-altitude Inca citadel — best May–Sep.", "default") +tags3 = TagBlock(["Andes", "Hike", "UNESCO"]) +followups = FollowUpBlock([fu1, fu2]) +fu1 = FollowUpItem("Show me only beach destinations") +fu2 = FollowUpItem("Turn this into a comparison table") + +Example 4 — Form with validation: +root = Card([title, form]) +title = TextContent("Contact Us", "large-heavy") +form = Form("contact", btns, [nameField, emailField, msgField]) +nameField = FormControl("Name", Input("name", "Your name", "text", { required: true, minLength: 2 })) +emailField = FormControl("Email", Input("email", "you@example.com", "email", { required: true, email: true })) +msgField = FormControl("Message", TextArea("message", "Tell us more...", 4, { required: true, minLength: 10 })) +btns = Buttons([Button("Submit", { type: "continue_conversation" }, "primary")]) + +Example — Code generation with artifacts: +root = Card([intro, code1, explanation, code2, followUps]) +intro = TextContent("Here's a React login form with validation:", "default") +code1 = ArtifactCodeBlock("typescript", "LoginForm.tsx", "import React, { useState } from 'react';\n\nexport function LoginForm() {\n const [email, setEmail] = useState('');\n const [password, setPassword] = useState('');\n\n return (\n
\n setEmail(e.target.value)} />\n setPassword(e.target.value)} />\n \n
\n );\n}") +explanation = TextContent("And the validation helper:", "default") +code2 = ArtifactCodeBlock("typescript", "validate.ts", "export function validateEmail(email: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n}") +followUps = FollowUpBlock([fu1, fu2]) +fu1 = FollowUpItem("Add password strength indicator") +fu2 = FollowUpItem("Add form styling with Tailwind") + +## Important Rules +- ALWAYS start with root = Card(...) +- Write statements in TOP-DOWN order: root → components → data (leverages hoisting for progressive streaming) +- Each statement on its own line +- No trailing text or explanations — output ONLY openui-lang code +- When asked about data, generate realistic/plausible data +- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.) +- NEVER define a variable without referencing it from the tree. Every variable must be reachable from root, otherwise it will not render. + +- Every response is a single Card(children) — children stack vertically automatically. No layout params are needed on Card. +- Card is the only layout container. Do NOT use Stack. Use Tabs to switch between sections, Carousel for horizontal scroll. +- Use FollowUpBlock at the END of a Card to suggest what the user can do or ask next. +- Use ListBlock when presenting a set of options or steps the user can click to select. +- Use SectionBlock to group long responses into collapsible sections — good for reports, FAQs, and structured content. +- Use SectionItem inside SectionBlock: each item needs a unique value id, a trigger (header label), and a content array. +- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]]) +- IMPORTANT: Every slide in a Carousel must use the same component structure in the same order — e.g. all slides: [title, image, description, tags]. +- For image carousels, always use real accessible URLs like https://picsum.photos/seed/KEYWORD/800/500. Never hallucinate or invent image URLs. +- For forms, define one FormControl reference per field so controls can stream progressively. +- For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields). +- Never nest Form inside Form. +- ALWAYS use ArtifactCodeBlock for ANY code output. NEVER use regular CodeBlock. +- Set title to the filename (e.g. 'LoginForm.tsx', 'sort.py', 'schema.sql'). +- Set language to the correct syntax highlighting language (e.g. 'typescript', 'python', 'sql', 'css'). +- You can include multiple ArtifactCodeBlocks in one response — each with a unique title. +- Surround code blocks with TextContent for explanations. diff --git a/examples/openui-artifact-demo/src/hooks/use-system-theme.tsx b/examples/openui-artifact-demo/src/hooks/use-system-theme.tsx new file mode 100644 index 000000000..7c110c21d --- /dev/null +++ b/examples/openui-artifact-demo/src/hooks/use-system-theme.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { createContext, useContext, useLayoutEffect, useState } from "react"; + +type ThemeMode = "light" | "dark"; + +interface ThemeContextType { + mode: ThemeMode; +} + +const ThemeContext = createContext(undefined); + +function getSystemMode(): ThemeMode { + if (typeof window === "undefined") return "light"; + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [mode, setMode] = useState(getSystemMode); + + useLayoutEffect(() => { + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const handler = (e: MediaQueryListEvent) => setMode(e.matches ? "dark" : "light"); + mq.addEventListener("change", handler); + return () => mq.removeEventListener("change", handler); + }, []); + + useLayoutEffect(() => { + document.body.setAttribute("data-theme", mode); + }, [mode]); + + return {children}; +} + +export function useTheme(): ThemeMode { + const ctx = useContext(ThemeContext); + if (!ctx) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return ctx.mode; +} diff --git a/examples/openui-artifact-demo/src/library.ts b/examples/openui-artifact-demo/src/library.ts new file mode 100644 index 000000000..0ab637d04 --- /dev/null +++ b/examples/openui-artifact-demo/src/library.ts @@ -0,0 +1,58 @@ +import type { ComponentGroup, PromptOptions } from "@openuidev/react-lang"; +import { createLibrary } from "@openuidev/react-lang"; +import { + openuiChatComponentGroups, + openuiChatLibrary, + openuiChatPromptOptions, +} from "@openuidev/react-ui/genui-lib"; + +import { ArtifactCodeBlock } from "./components/ArtifactCodeBlock"; + +// ── Component Groups — extend chat groups, add ArtifactCodeBlock to Content ── + +const artifactComponentGroups: ComponentGroup[] = openuiChatComponentGroups.map((group) => { + if (group.name === "Content") { + return { + ...group, + components: [...group.components, "ArtifactCodeBlock"], + }; + } + return group; +}); + +// ── Library — all chat components + ArtifactCodeBlock ── + +export const artifactDemoLibrary = createLibrary({ + root: "Card", + componentGroups: artifactComponentGroups, + components: [...Object.values(openuiChatLibrary.components), ArtifactCodeBlock], +}); + +// ── Prompt Options — extend chat rules with artifact-specific instructions ── + +export const artifactDemoPromptOptions: PromptOptions = { + additionalRules: [ + ...(openuiChatPromptOptions.additionalRules ?? []), + "ALWAYS use ArtifactCodeBlock for ANY code output. NEVER use regular CodeBlock.", + "Set title to the filename (e.g. 'LoginForm.tsx', 'sort.py', 'schema.sql').", + "Set language to the correct syntax highlighting language (e.g. 'typescript', 'python', 'sql', 'css').", + "You can include multiple ArtifactCodeBlocks in one response — each with a unique title.", + "Surround code blocks with TextContent for explanations.", + ], + examples: [ + ...(openuiChatPromptOptions.examples ?? []), + `Example — Code generation with artifacts: +root = Card([intro, code1, explanation, code2, followUps]) +intro = TextContent("Here's a React login form with validation:", "default") +code1 = ArtifactCodeBlock("typescript", "LoginForm.tsx", "import React, { useState } from 'react';\\n\\nexport function LoginForm() {\\n const [email, setEmail] = useState('');\\n const [password, setPassword] = useState('');\\n\\n return (\\n
\\n setEmail(e.target.value)} />\\n setPassword(e.target.value)} />\\n \\n
\\n );\\n}") +explanation = TextContent("And the validation helper:", "default") +code2 = ArtifactCodeBlock("typescript", "validate.ts", "export function validateEmail(email: string): boolean {\\n return /^[^\\\\s@]+@[^\\\\s@]+\\\\.[^\\\\s@]+$/.test(email);\\n}") +followUps = FollowUpBlock([fu1, fu2]) +fu1 = FollowUpItem("Add password strength indicator") +fu2 = FollowUpItem("Add form styling with Tailwind")`, + ], +}; + +// ── CLI exports — the generate:prompt script expects `library` and `promptOptions` ── + +export { artifactDemoLibrary as library, artifactDemoPromptOptions as promptOptions }; diff --git a/examples/openui-artifact-demo/tsconfig.json b/examples/openui-artifact-demo/tsconfig.json new file mode 100644 index 000000000..cf9c65d3e --- /dev/null +++ b/examples/openui-artifact-demo/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/packages/react-ui/src/artifact/Artifact.tsx b/packages/react-ui/src/artifact/Artifact.tsx new file mode 100644 index 000000000..1ef54edd2 --- /dev/null +++ b/packages/react-ui/src/artifact/Artifact.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useArtifact } from "@openuidev/react-headless"; +import type { ComponentRenderer } from "@openuidev/react-lang"; +import { useId, type ReactNode } from "react"; +import { ArtifactPanel, type ArtifactPanelProps } from "../components/_shared/artifact"; + +/** + * Controls injected into `preview` and `panel` render functions. + */ +export interface ArtifactControls { + /** Whether this artifact is the currently active (visible) one. */ + isActive: boolean; + /** Activates this artifact. */ + open: () => void; + /** Deactivates this artifact. */ + close: () => void; + /** Toggles this artifact: opens if closed, closes if open. */ + toggle: () => void; +} + +/** + * Configuration for {@link Artifact}. + */ +export interface ArtifactConfig

> { + /** Panel title — static string or derived from props. */ + title: string | ((props: P) => string); + /** Renders the inline preview shown in the chat message. */ + preview: (props: P, controls: ArtifactControls) => ReactNode; + /** Renders the content inside the artifact side panel. */ + panel: (props: P, controls: ArtifactControls) => ReactNode; + /** Optional props forwarded to the underlying ``. */ + panelProps?: Pick; +} + +/** + * Factory that returns a `ComponentRenderer

` wiring up `useId`, `useArtifact`, + * and `` internally. Pass the result as `defineComponent`'s `component`. + * + * @example + * ```tsx + * export const ArtifactCodeBlock = defineComponent({ + * name: "ArtifactCodeBlock", + * props: ArtifactCodeBlockSchema, + * description: "Code block that opens in the artifact side panel", + * component: Artifact({ + * title: (props) => props.title, + * preview: (props, { open, isActive }) => ( + * + * ), + * panel: (props) => ( + * + * ), + * }), + * }); + * ``` + */ +export function Artifact

>( + config: ArtifactConfig

, +): ComponentRenderer

{ + const { title, preview, panel, panelProps } = config; + + const ArtifactComponent: ComponentRenderer

= ({ props }) => { + const artifactId = useId(); + const { isActive, open, close, toggle } = useArtifact(artifactId); + + const controls: ArtifactControls = { isActive, open, close, toggle }; + const resolvedTitle = typeof title === "function" ? title(props) : title; + + return ( + <> + {preview(props, controls)} + + {panel(props, controls)} + + + ); + }; + + ArtifactComponent.displayName = `Artifact(${typeof title === "string" ? title : "dynamic"})`; + + return ArtifactComponent; +} diff --git a/packages/react-ui/src/artifact/index.ts b/packages/react-ui/src/artifact/index.ts new file mode 100644 index 000000000..199e769a9 --- /dev/null +++ b/packages/react-ui/src/artifact/index.ts @@ -0,0 +1,2 @@ +export { Artifact } from "./Artifact"; +export type { ArtifactConfig, ArtifactControls } from "./Artifact"; diff --git a/packages/react-ui/src/components/_shared/artifact/artifactPanel.scss b/packages/react-ui/src/components/_shared/artifact/artifactPanel.scss index cbc0bb181..634ce066d 100644 --- a/packages/react-ui/src/components/_shared/artifact/artifactPanel.scss +++ b/packages/react-ui/src/components/_shared/artifact/artifactPanel.scss @@ -7,6 +7,10 @@ gap: cssUtils.$space-s; padding: cssUtils.$space-s cssUtils.$space-m; border-bottom: 1px solid cssUtils.$border-default; + background-color: cssUtils.$foreground; + position: sticky; + top: 0; + z-index: 1; } .openui-artifact-panel__title { diff --git a/packages/react-ui/src/index.ts b/packages/react-ui/src/index.ts index fac42b290..34d9cf8ed 100644 --- a/packages/react-ui/src/index.ts +++ b/packages/react-ui/src/index.ts @@ -2,6 +2,10 @@ export * from "./components/Accordion"; +// Artifact() factory — generates a ComponentRenderer with artifact wiring +export { Artifact } from "./artifact"; +export type { ArtifactConfig, ArtifactControls } from "./artifact"; + // Artifact exports (ArtifactPanel/ArtifactPortalTarget also available as Shell.*) export { useActiveArtifact, useArtifact } from "@openuidev/react-headless"; export { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c9ac0652..72162f65c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,6 +191,70 @@ importers: specifier: ^5 version: 5.9.3 + examples/openui-artifact-demo: + dependencies: + '@openuidev/react-headless': + specifier: workspace:* + version: link:../../packages/react-headless + '@openuidev/react-lang': + specifier: workspace:* + version: link:../../packages/react-lang + '@openuidev/react-ui': + specifier: workspace:* + version: link:../../packages/react-ui + lucide-react: + specifier: ^0.575.0 + version: 0.575.0(react@19.2.3) + next: + specifier: 16.1.6 + version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) + openai: + specifier: ^6.22.0 + version: 6.22.0(ws@8.20.0)(zod@4.3.6) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + react-syntax-highlighter: + specifier: ^15.6.1 + version: 15.6.1(react@19.2.3) + zod: + specifier: ^4.0.0 + version: 4.3.6 + devDependencies: + '@openuidev/cli': + specifier: workspace:* + version: link:../../packages/openui-cli + '@tailwindcss/postcss': + specifier: ^4 + version: 4.2.1 + '@types/node': + specifier: ^20 + version: 20.19.35 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + '@types/react-syntax-highlighter': + specifier: ^15.5.13 + version: 15.5.13 + eslint: + specifier: ^9 + version: 9.29.0(jiti@2.6.1) + eslint-config-next: + specifier: 16.1.6 + version: 16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) + tailwindcss: + specifier: ^4 + version: 4.2.1 + typescript: + specifier: ^5 + version: 5.9.3 + examples/openui-chat: dependencies: '@openuidev/react-headless': @@ -2748,105 +2812,89 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -3076,10 +3124,6 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} - '@jridgewell/remapping@2.3.5': resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} @@ -3087,10 +3131,6 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.6': resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} @@ -3100,9 +3140,6 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -3176,56 +3213,48 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-gnu@16.1.6': resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.5.12': resolution: {integrity: sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-arm64-musl@16.1.6': resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.5.12': resolution: {integrity: sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-gnu@16.1.6': resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.5.12': resolution: {integrity: sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-linux-x64-musl@16.1.6': resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.5.12': resolution: {integrity: sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==} @@ -3469,56 +3498,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-arm64-musl@0.117.0': resolution: {integrity: sha512-C3zapJconWpl2Y7LR3GkRkH6jxpuV2iVUfkFcHT5Ffn4Zu7l88mZa2dhcfdULZDybN1Phka/P34YUzuskUUrXw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-minify/binding-linux-ppc64-gnu@0.117.0': resolution: {integrity: sha512-2T/Bm+3/qTfuNS4gKSzL8qbiYk+ErHW2122CtDx+ilZAzvWcJ8IbqdZIbEWOlwwe03lESTxPwTBLFqVgQU2OeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-riscv64-gnu@0.117.0': resolution: {integrity: sha512-MKLjpldYkeoB4T+yAi4aIAb0waifxUjLcKkCUDmYAY3RqBJTvWK34KtfaKZL0IBMIXfD92CbKkcxQirDUS9Xcg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-riscv64-musl@0.117.0': resolution: {integrity: sha512-UFVcbPvKUStry6JffriobBp8BHtjmLLPl4bCY+JMxIn/Q3pykCpZzRwFTcDurG/kY8tm+uSNfKKdRNa5Nh9A7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-minify/binding-linux-s390x-gnu@0.117.0': resolution: {integrity: sha512-B9GyPQ1NKbvpETVAMyJMfRlD3c6UJ7kiuFUAlx9LTYiQL+YIyT6vpuRlq1zgsXxavZluVrfeJv6x0owV4KDx4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-x64-gnu@0.117.0': resolution: {integrity: sha512-fXfhtr+WWBGNy4M5GjAF5vu/lpulR4Me34FjTyaK9nDrTZs7LM595UDsP1wliksqp4hD/KdoqHGmbCrC+6d4vA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-minify/binding-linux-x64-musl@0.117.0': resolution: {integrity: sha512-jFBgGbx1oLadb83ntJmy1dWlAHSQanXTS21G4PgkxyONmxZdZ/UMKr7KsADzMuoPsd2YhJHxzRpwJd9U+4BFBw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxc-minify/binding-openharmony-arm64@0.117.0': resolution: {integrity: sha512-nxPd9vx1vYz8IlIMdl9HFdOK/ood1H5hzbSFsyO8JU55tkcJoBL8TLCbuFf9pHpOy27l2gcPyV6z3p4eAcTH5Q==} @@ -3596,56 +3617,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.117.0': resolution: {integrity: sha512-QagKTDF4lrz8bCXbUi39Uq5xs7C7itAseKm51f33U+Dyar9eJY/zGKqfME9mKLOiahX7Fc1J3xMWVS0AdDXLPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.117.0': resolution: {integrity: sha512-RPddpcE/0xxWaommWy0c5i/JdrXcXAkxBS2GOrAUh5LKmyCh03hpJedOAWszG4ADsKQwoUQQ1/tZVGRhZIWtKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.117.0': resolution: {integrity: sha512-ur/WVZF9FSOiZGxyP+nfxZzuv6r5OJDYoVxJnUR7fM/hhXLh4V/be6rjbzm9KLCDBRwYCEKJtt+XXNccwd06IA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.117.0': resolution: {integrity: sha512-ujGcAx8xAMvhy7X5sBFi3GXML1EtyORuJZ5z2T6UV3U416WgDX/4OCi3GnoteeenvxIf6JgP45B+YTHpt71vpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.117.0': resolution: {integrity: sha512-hbsfKjUwRjcMZZvvmpZSc+qS0bHcHRu8aV/I3Ikn9BzOA0ZAgUE7ctPtce5zCU7bM8dnTLi4sJ1Pi9YHdx6Urw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.117.0': resolution: {integrity: sha512-1QrTrf8rige7UPJrYuDKJLQOuJlgkt+nRSJLBMHWNm9TdivzP48HaK3f4q18EjNlglKtn03lgjMu4fryDm8X4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.117.0': resolution: {integrity: sha512-gRvK6HPzF5ITRL68fqb2WYYs/hGviPIbkV84HWCgiJX+LkaOpp+HIHQl3zVZdyKHwopXToTbXbtx/oFjDjl8pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.117.0': resolution: {integrity: sha512-QPJvFbnnDZZY7xc+xpbIBWLThcGBakwaYA9vKV8b3+oS5MGfAZUoTFJcix5+Zg2Ri46sOfrUim6Y6jsKNcssAQ==} @@ -3726,56 +3739,48 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-arm64-musl@0.117.0': resolution: {integrity: sha512-ykxpPQp0eAcSmhy0Y3qKvdanHY4d8THPonDfmCoktUXb6r0X6qnjpJB3V+taN1wevW55bOEZd97kxtjTKjqhmg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-transform/binding-linux-ppc64-gnu@0.117.0': resolution: {integrity: sha512-Rvspti4Kr7eq6zSrURK5WjscfWQPvmy/KjJZV45neRKW8RLonE3r9+NgrwSLGoHvQ3F24fbqlkplox1RtlhH5A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-riscv64-gnu@0.117.0': resolution: {integrity: sha512-Dr2ZW9ZZ4l1eQ5JUEUY3smBh4JFPCPuybWaDZTLn3ADZjyd8ZtNXEjeMT8rQbbhbgSL9hEgbwaqraole3FNThQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-riscv64-musl@0.117.0': resolution: {integrity: sha512-oD1Bnes1bIC3LVBSrWEoSUBj6fvatESPwAVWfJVGVQlqWuOs/ZBn1e4Nmbipo3KGPHK7DJY75r/j7CQCxhrOFQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-transform/binding-linux-s390x-gnu@0.117.0': resolution: {integrity: sha512-qT//IAPLvse844t99Kff5j055qEbXfwzWgvCMb0FyjisnB8foy25iHZxZIocNBe6qwrCYWUP1M8rNrB/WyfS1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-x64-gnu@0.117.0': resolution: {integrity: sha512-2YEO5X+KgNzFqRVO5dAkhjcI5gwxus4NSWVl/+cs2sI6P0MNPjqE3VWPawl4RTC11LvetiiZdHcujUCPM8aaUw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-transform/binding-linux-x64-musl@0.117.0': resolution: {integrity: sha512-3wqWbTSaIFZvDr1aqmTul4cg8PRWYh6VC52E8bLI7ytgS/BwJLW+sDUU2YaGIds4sAf/1yKeJRmudRCDPW9INg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxc-transform/binding-openharmony-arm64@0.117.0': resolution: {integrity: sha512-Ebxx6NPqhzlrjvx4+PdSqbOq+li0f7X59XtJljDghkbJsbnkHvhLmPR09ifHt5X32UlZN63ekjwcg/nbmHLLlA==} @@ -3835,42 +3840,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-wasm@2.5.6': resolution: {integrity: sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA==} @@ -5882,145 +5881,121 @@ packages: resolution: {integrity: sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.60.1': resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.43.0': resolution: {integrity: sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.43.0': resolution: {integrity: sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.43.0': resolution: {integrity: sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.43.0': resolution: {integrity: sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.43.0': resolution: {integrity: sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.43.0': resolution: {integrity: sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.43.0': resolution: {integrity: sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.43.0': resolution: {integrity: sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.43.0': resolution: {integrity: sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.43.0': resolution: {integrity: sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -6434,56 +6409,48 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -6566,28 +6533,24 @@ packages: engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@takumi-rs/core-linux-arm64-musl@0.68.17': resolution: {integrity: sha512-4CiEF518wDnujF0fjql2XN6uO+OXl0svy0WgAF2656dCx2gJtWscaHytT2rsQ0ZmoFWE0dyWcDW1g/FBVPvuvA==} engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@takumi-rs/core-linux-x64-gnu@0.68.17': resolution: {integrity: sha512-jm8lTe2E6Tfq2b97GJC31TWK1JAEv+MsVbvL9DCLlYcafgYFlMXDUnOkZFMjlrmh0HcFAYDaBkniNDgIQfXqzg==} engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@takumi-rs/core-linux-x64-musl@0.68.17': resolution: {integrity: sha512-nbdzQgC4ywzltDDV1fer1cKswwGE+xXZHdDiacdd7RM5XBng209Bmo3j1iv9dsX+4xXhByzCCGbxdWhhHqVXmw==} engines: {node: '>= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@takumi-rs/core-win32-arm64-msvc@0.68.17': resolution: {integrity: sha512-kE4F0LRmuhSwiNkFG7dTY9ID8+B7zb97QedyN/IO2fBJmRQDkqCGcip2gloh8YPPhCuKGjCqqqh2L+Tg9PKW7w==} @@ -6937,49 +6900,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -10160,56 +10115,48 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -13931,8 +13878,8 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@asamuzakjp/css-color@3.2.0': dependencies: @@ -13948,7 +13895,7 @@ snapshots: '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - lru-cache: 11.2.6 + lru-cache: 11.2.7 optional: true '@asamuzakjp/dom-selector@6.8.1': @@ -13957,7 +13904,7 @@ snapshots: bidi-js: 1.0.3 css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.6 + lru-cache: 11.2.7 optional: true '@asamuzakjp/nwsapi@2.3.9': @@ -14027,8 +13974,8 @@ snapshots: dependencies: '@babel/parser': 7.27.5 '@babel/types': 7.27.6 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/generator@7.29.1': @@ -15857,12 +15804,6 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/gen-mapping@0.3.8': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/remapping@2.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -15870,8 +15811,6 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.6': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -15881,11 +15820,6 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -22379,7 +22313,7 @@ snapshots: '@asamuzakjp/css-color': 5.0.1 '@csstools/css-syntax-patches-for-csstree': 1.1.0 css-tree: 3.2.1 - lru-cache: 11.2.6 + lru-cache: 11.2.7 optional: true csstype@3.2.3: {} @@ -24868,7 +24802,7 @@ snapshots: magic-string@0.27.0: dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 magic-string@0.30.17: dependencies: @@ -27448,6 +27382,16 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + react-syntax-highlighter@15.6.1(react@19.2.3): + dependencies: + '@babel/runtime': 7.27.6 + highlight.js: 10.7.3 + highlightjs-vue: 1.0.0 + lowlight: 1.20.0 + prismjs: 1.30.0 + react: 19.2.3 + refractor: 3.6.0 + react-syntax-highlighter@15.6.1(react@19.2.4): dependencies: '@babel/runtime': 7.27.6 @@ -28398,7 +28342,7 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4