diff --git a/packages/ghost-ui/src/app/components/[name]/page.tsx b/packages/ghost-ui/src/app/components/[name]/page.tsx index 0d7f3e3..4291697 100644 --- a/packages/ghost-ui/src/app/components/[name]/page.tsx +++ b/packages/ghost-ui/src/app/components/[name]/page.tsx @@ -1,5 +1,6 @@ import { Navigate, useParams } from "react-router"; import { ComponentPageShell } from "@/components/docs/component-page-shell"; +import { getComponentDoc } from "@/lib/component-docs"; import { getCategory, getComponent, @@ -42,6 +43,7 @@ export default function ComponentPage() { const demoSource = getDemoSource(component.slug, component.demoSource); const spec = getComponentSpec(component.slug); + const docs = getComponentDoc(name); return ( ); } diff --git a/packages/ghost-ui/src/components/docs/component-page-shell.tsx b/packages/ghost-ui/src/components/docs/component-page-shell.tsx index c6a846b..ddb76eb 100644 --- a/packages/ghost-ui/src/components/docs/component-page-shell.tsx +++ b/packages/ghost-ui/src/components/docs/component-page-shell.tsx @@ -15,11 +15,12 @@ import { CodeBlockCopyButton, CodeBlockHeader, } from "@/components/ai-elements/code-block"; -import { DemoLoader } from "@/components/docs/demo-loader"; +import { DemoLoader, ExampleLoader } from "@/components/docs/demo-loader"; import { ComponentErrorBoundary } from "@/components/docs/error-boundary"; import { SectionWrapper } from "@/components/docs/wrappers"; import { Button } from "@/components/ui/button"; import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; +import type { ComponentDoc } from "@/lib/component-docs"; import { ComponentEntry } from "@/lib/component-registry"; import type { ComponentSpec } from "@/lib/component-source"; import { cn } from "@/lib/utils"; @@ -101,6 +102,7 @@ export function ComponentPageShell({ spec, prev, next, + docs, }: { component: ComponentEntry; categoryName: string; @@ -108,6 +110,7 @@ export function ComponentPageShell({ spec: ComponentSpec | null; prev: { slug: string; name: string } | null; next: { slug: string; name: string } | null; + docs?: ComponentDoc; }) { const [activeTab, setActiveTab] = useState<"preview" | "source" | "demo">( "preview", @@ -132,7 +135,7 @@ export function ComponentPageShell({ return () => ctx.revert(); }, [component.slug]); - const installCommand = `npx shadcn@latest add ${component.slug}`; + const installCommand = `npx shadcn@latest add --registry https://block.github.io/ghost/r/registry.json ${component.slug}`; // Filter exports to only component names (capitalized), exclude variant exports const componentExports = @@ -141,276 +144,434 @@ export function ComponentPageShell({ ) ?? []; return ( - -
- {/* ── Header ── */} -
-
- - - -
- + {/* ── Prev / Next (floating sides, xl+) ── */} + {prev && ( + + + + + + {prev.name} + + + )} + {next && ( + + + + + + {next.name} + + + )} + +
+ {/* ── Header ── */} +
+
+ - {categoryName} - - {component.isAI && ( - - AI + + +
+ + {categoryName} - )} +
-
-

- {component.name} -

-
- -
- {/* ── Install ── */} -
- - - {installCommand} - - + {component.name} + + {docs?.description && ( +

+ {docs.description} +

+ )}
- {/* ── Tabs: Preview / Source / Demo Code ── */} -
-
- {( - [ - ["preview", "Preview"], - ...(spec?.source ? [["source", "Source"]] : []), - ...(demoSource ? [["demo", "Demo"]] : []), - ] as [string, string][] - ).map(([key, label]) => ( -
-
- {activeTab === "preview" && ( - - - - )} + {docs?.usage && ( +
+ + + + Usage + + + + + + +
+ )} - {activeTab === "source" && spec?.source && ( -
- - - - {spec.filePath} - - - - - - -
- )} + {/* ── Tabs: Preview / Source / Demo Code ── */} +
+
+ {( + [ + ["preview", "Preview"], + ...(spec?.source ? [["source", "Source"]] : []), + ...(demoSource ? [["demo", "Demo"]] : []), + ] as [string, string][] + ).map(([key, label]) => ( + + ))} +
- {activeTab === "demo" && demoSource && ( -
- - - - {component.slug}-demo.tsx - - - - - - -
- )} -
-
+
+ {activeTab === "preview" && ( + + + + )} - {/* ── Spec Sheet ── */} -
-
-

- Specification -

+ {activeTab === "source" && spec?.source && ( +
+ + + + {spec.filePath} + + + + + + +
+ )} + + {activeTab === "demo" && demoSource && ( +
+ + + + {component.slug}-demo.tsx + + + + + + +
+ )} +
-
- {/* Variants */} - {spec && spec.variants.length > 0 && ( - - - - )} + {/* ── Spec Sheet ── */} +
+
+

+ Specification +

+
- {/* Sub-components */} - {componentExports.length > 1 && ( - -
- {componentExports.map((exp) => ( - - {exp} - - ))} -
-
- )} +
+ {/* Variants */} + {spec && spec.variants.length > 0 && ( + + + + )} - {/* Data slots */} - {spec && spec.dataSlots.length > 0 && ( - -
- {spec.dataSlots.map((slot) => ( - - [data-slot="{slot}"] - - ))} -
-
- )} + {/* Sub-components */} + {componentExports.length > 1 && ( + +
+ {componentExports.map((exp) => ( + + {exp} + + ))} +
+
+ )} + + {/* Data slots */} + {spec && spec.dataSlots.length > 0 && ( + +
+ {spec.dataSlots.map((slot) => ( + + [data-slot="{slot}"] + + ))} +
+
+ )} + + {/* Registry dependencies */} + {component.registryDependencies.length > 0 && ( + +
+ {component.registryDependencies.map((dep) => ( + + {dep} + + ))} +
+
+ )} + + {/* npm Dependencies */} + {component.dependencies.length > 0 && ( + +
+ {component.dependencies.map((dep) => ( + + {dep} + + ))} +
+
+ )} + + {/* File path */} + {spec?.filePath && ( + + + {spec.filePath} + + + )} +
+
+ + {docs && docs.props.length > 0 && ( +
+
+

+ Props +

+
+
+ {docs.props.map((prop) => ( + +
+ + {prop.type} + + {prop.default && ( + + Default:{" "} + {prop.default} + + )} + {prop.description} +
+
+ ))} +
+
+ )} - {/* Registry dependencies */} - {component.registryDependencies.length > 0 && ( - + {docs && docs.composedWith.length > 0 && ( +
+
+

+ Works With +

+
+
- {component.registryDependencies.map((dep) => ( + {docs.composedWith.map((slug) => ( - {dep} + {slug} ))}
- - )} +
+
+ )} - {/* npm Dependencies */} - {component.dependencies.length > 0 && ( - -
- {component.dependencies.map((dep) => ( - 0 && ( +
+

+ Examples +

+ {docs.examples.map((example) => ( +
+
+

{example.title}

+ {example.description && ( +

+ {example.description} +

+ )} +
+
+ - {dep} - - ))} + + +
- - )} + ))} +
+ )} - {/* File path */} - {spec?.filePath && ( - - {spec.filePath} - + {/* ── Prev / Next (bottom, below xl) ── */} +
+ {prev ? ( + + +
+ + Previous + + + {prev.name} + +
+ + ) : ( +
+ )} + {next ? ( + +
+ Next + + {next.name} + +
+ + + ) : ( +
)}
-
- {/* ── Prev / Next ── */} -
- {prev ? ( - - -
- - Previous - - - {prev.name} - -
- - ) : ( -
- )} - {next ? ( - -
- Next - - {next.name} - -
- - - ) : ( -
- )} + {/* spacer for bottom nav hidden on xl */} +
-
- + + ); } diff --git a/packages/ghost-ui/src/components/docs/demo-loader.tsx b/packages/ghost-ui/src/components/docs/demo-loader.tsx index 8efe902..3e4a7fb 100644 --- a/packages/ghost-ui/src/components/docs/demo-loader.tsx +++ b/packages/ghost-ui/src/components/docs/demo-loader.tsx @@ -1,8 +1,43 @@ "use client"; -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useMemo } from "react"; import { Skeleton } from "@/components/ui/skeleton"; +const exampleModules = import.meta.glob<{ default: React.ComponentType }>( + "/src/components/docs/examples/**/*.tsx", +); + +const LoadingSkeleton = () => ( +
+ + +
+); + +export function ExampleLoader({ + componentSlug, + exampleName, +}: { + componentSlug: string; + exampleName: string; +}) { + const path = `/src/components/docs/examples/${componentSlug}/${exampleName}.tsx`; + const loader = exampleModules[path]; + + const LazyComponent = useMemo(() => { + if (!loader) return null; + return lazy(loader); + }, [loader]); + + if (!LazyComponent) return null; + + return ( + }> + + + ); +} + /* eslint-disable @typescript-eslint/no-explicit-any */ const wrap = (imp: Promise, name: string) => imp.then((m: any) => ({ default: m[name] })); diff --git a/packages/ghost-ui/src/components/docs/examples/code-block/with-diff.tsx b/packages/ghost-ui/src/components/docs/examples/code-block/with-diff.tsx new file mode 100644 index 0000000..2f4b633 --- /dev/null +++ b/packages/ghost-ui/src/components/docs/examples/code-block/with-diff.tsx @@ -0,0 +1,86 @@ +import { useState } from "react"; +import { + CodeBlock, + CodeBlockActions, + CodeBlockCopyButton, + CodeBlockHeader, + CodeBlockLanguageSelector, + CodeBlockLanguageSelectorContent, + CodeBlockLanguageSelectorItem, + CodeBlockLanguageSelectorTrigger, + CodeBlockLanguageSelectorValue, +} from "@/components/ai-elements/code-block"; + +const snippets: Record< + string, + { code: string; language: "typescript" | "python" | "rust" } +> = { + typescript: { + code: `function fibonacci(n: number): number { + if (n <= 1) return n; + return fibonacci(n - 1) + fibonacci(n - 2); +} + +console.log(fibonacci(10)); // 55`, + language: "typescript", + }, + python: { + code: `def fibonacci(n: int) -> int: + if n <= 1: + return n + return fibonacci(n - 1) + fibonacci(n - 2) + +print(fibonacci(10)) # 55`, + language: "python", + }, + rust: { + code: `fn fibonacci(n: u32) -> u32 { + if n <= 1 { + return n; + } + fibonacci(n - 1) + fibonacci(n - 2) +} + +fn main() { + println!("{}", fibonacci(10)); // 55 +}`, + language: "rust", + }, +}; + +export default function CodeBlockWithDiff() { + const [lang, setLang] = useState("typescript"); + const snippet = snippets[lang]; + + return ( +
+ + + + + + + + + TypeScript + + + Python + + + Rust + + + + + + + + +
+ ); +} diff --git a/packages/ghost-ui/src/components/docs/examples/conversation/with-messages.tsx b/packages/ghost-ui/src/components/docs/examples/conversation/with-messages.tsx new file mode 100644 index 0000000..dbb8fde --- /dev/null +++ b/packages/ghost-ui/src/components/docs/examples/conversation/with-messages.tsx @@ -0,0 +1,48 @@ +import { + Conversation, + ConversationContent, + ConversationScrollButton, +} from "@/components/ai-elements/conversation"; +import { + Message, + MessageContent, + MessageResponse, +} from "@/components/ai-elements/message"; + +const messages = [ + { id: "1", role: "user" as const, text: "What is TypeScript?" }, + { + id: "2", + role: "assistant" as const, + text: "TypeScript is a strongly-typed superset of JavaScript developed by Microsoft. It adds optional static type checking, interfaces, enums, and other features that help catch errors at compile time rather than at runtime.", + }, + { id: "3", role: "user" as const, text: "How does it compare to Flow?" }, + { + id: "4", + role: "assistant" as const, + text: "Both TypeScript and Flow add static types to JavaScript, but they differ in key ways:\n\n- **Adoption**: TypeScript has much wider community adoption and tooling support.\n- **Type system**: TypeScript uses a structural type system; Flow also uses structural typing but with some nominal typing features.\n- **Tooling**: TypeScript ships its own compiler (`tsc`), while Flow relies on Babel for compilation.\n- **Ecosystem**: TypeScript has DefinitelyTyped with type definitions for thousands of packages.", + }, +]; + +export default function ConversationWithMessages() { + return ( +
+ + + {messages.map((msg) => ( + + + {msg.role === "assistant" ? ( + {msg.text} + ) : ( +

{msg.text}

+ )} +
+
+ ))} +
+ +
+
+ ); +} diff --git a/packages/ghost-ui/src/components/docs/examples/message/streaming.tsx b/packages/ghost-ui/src/components/docs/examples/message/streaming.tsx new file mode 100644 index 0000000..3529d16 --- /dev/null +++ b/packages/ghost-ui/src/components/docs/examples/message/streaming.tsx @@ -0,0 +1,29 @@ +import { + Message, + MessageContent, + MessageResponse, +} from "@/components/ai-elements/message"; +import { Shimmer } from "@/components/ai-elements/shimmer"; + +export default function MessageStreaming() { + return ( +
+ + +

Explain quantum entanglement in simple terms.

+
+
+ + + + + {`Quantum entanglement is when two particles become linked so that measuring one instantly affects the other, no matter how far apart they are. Einstein called it "spooky action at a distance."`} + + + Generating... + + + +
+ ); +} diff --git a/packages/ghost-ui/src/components/docs/examples/message/with-actions.tsx b/packages/ghost-ui/src/components/docs/examples/message/with-actions.tsx new file mode 100644 index 0000000..115c468 --- /dev/null +++ b/packages/ghost-ui/src/components/docs/examples/message/with-actions.tsx @@ -0,0 +1,47 @@ +import { + CopyIcon, + RefreshCwIcon, + ThumbsDownIcon, + ThumbsUpIcon, +} from "lucide-react"; +import { + Message, + MessageAction, + MessageActions, + MessageContent, + MessageResponse, +} from "@/components/ai-elements/message"; + +export default function MessageWithActions() { + return ( +
+ + +

How do I center a div?

+
+
+ + + + + {`You can center a div using **flexbox**:\n\n\`\`\`css\n.parent {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\`\`\`\n\nOr use **grid**:\n\n\`\`\`css\n.parent {\n display: grid;\n place-items: center;\n}\n\`\`\``} + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/packages/ghost-ui/src/components/docs/examples/prompt-input/with-attachments.tsx b/packages/ghost-ui/src/components/docs/examples/prompt-input/with-attachments.tsx new file mode 100644 index 0000000..2d67b79 --- /dev/null +++ b/packages/ghost-ui/src/components/docs/examples/prompt-input/with-attachments.tsx @@ -0,0 +1,31 @@ +import { + PromptInput, + PromptInputActionAddAttachments, + PromptInputFooter, + PromptInputSubmit, + PromptInputTextarea, + PromptInputTools, +} from "@/components/ai-elements/prompt-input"; + +export default function PromptInputWithAttachments() { + return ( +
+ { + console.log("Submitted:", msg); + }} + > + + + + + + + + +
+ ); +} diff --git a/packages/ghost-ui/src/components/docs/primitives/calendar-demo.tsx b/packages/ghost-ui/src/components/docs/primitives/calendar-demo.tsx index 5f57901..0e331b0 100644 --- a/packages/ghost-ui/src/components/docs/primitives/calendar-demo.tsx +++ b/packages/ghost-ui/src/components/docs/primitives/calendar-demo.tsx @@ -40,7 +40,7 @@ export function CalendarDemo() { selected={range} onSelect={setRange} numberOfMonths={3} - className="hidden rounded-md border @4xl:flex [&>div]:gap-5" + className="hidden rounded-md border @2xl:flex [&>div]:gap-5" />
); diff --git a/packages/ghost-ui/src/components/docs/primitives/carousel-demo.tsx b/packages/ghost-ui/src/components/docs/primitives/carousel-demo.tsx index 4d7ad7e..f1846bb 100644 --- a/packages/ghost-ui/src/components/docs/primitives/carousel-demo.tsx +++ b/packages/ghost-ui/src/components/docs/primitives/carousel-demo.tsx @@ -11,7 +11,7 @@ import { export function CarouselDemo() { return ( -
+
{Array.from({ length: 5 }).map((_, index) => ( diff --git a/packages/ghost-ui/src/lib/component-docs.ts b/packages/ghost-ui/src/lib/component-docs.ts new file mode 100644 index 0000000..5b16fd7 --- /dev/null +++ b/packages/ghost-ui/src/lib/component-docs.ts @@ -0,0 +1,673 @@ +export type PropDef = { + name: string; + type: string; + default?: string; + description: string; +}; + +export type ExampleMeta = { + name: string; + title: string; + description?: string; +}; + +export type ComponentDoc = { + description: string; + usage: string; + props: PropDef[]; + composedWith: string[]; + examples: ExampleMeta[]; +}; + +const docs: Record = { + message: { + description: + "Renders a single chat message with support for markdown streaming, actions, and branching.", + usage: `import { + Message, + MessageContent, + MessageResponse, + MessageActions, + MessageAction, +} from "@/components/ai-elements/message"; + + + + Hello, how can I help? + + + + + + +`, + props: [ + { + name: "from", + type: '"user" | "assistant" | "system"', + description: + "The role of the message sender, controls alignment and styling.", + }, + { + name: "children", + type: "ReactNode", + description: + "Message sub-components (MessageContent, MessageActions, etc.).", + }, + ], + composedWith: [ + "conversation", + "prompt-input", + "reasoning", + "chain-of-thought", + "code-block", + ], + examples: [ + { + name: "with-actions", + title: "With Actions", + description: "Message with copy and regenerate action buttons.", + }, + { + name: "streaming", + title: "Streaming Response", + description: "Message with an animated streaming indicator.", + }, + ], + }, + conversation: { + description: + "A scrollable container that auto-sticks to the bottom as new messages arrive.", + usage: `import { + Conversation, + ConversationContent, + ConversationScrollButton, +} from "@/components/ai-elements/conversation"; + + + + {messages.map((msg) => ( + ... + ))} + + +`, + props: [ + { + name: "initial", + type: '"smooth" | "instant" | "auto"', + default: '"smooth"', + description: "Scroll behavior when the component first mounts.", + }, + { + name: "resize", + type: '"smooth" | "instant" | "auto"', + default: '"smooth"', + description: "Scroll behavior when content resizes.", + }, + ], + composedWith: ["message", "prompt-input", "reasoning"], + examples: [ + { + name: "with-messages", + title: "With Messages", + description: "Conversation with multiple user and assistant messages.", + }, + ], + }, + "prompt-input": { + description: + "A composable prompt input form with file attachments, commands, screenshots, and submit handling.", + usage: `import { + PromptInput, + PromptInputTextarea, + PromptInputActions, +} from "@/components/ai-elements/prompt-input"; + + console.log(msg)}> + + +`, + props: [ + { + name: "onSubmit", + type: "(message: PromptInputMessage, event: FormEvent) => void | Promise", + description: "Called when the user submits the prompt.", + }, + { + name: "accept", + type: "string", + description: 'MIME filter for file attachments, e.g. "image/*".', + }, + { + name: "maxFiles", + type: "number", + description: "Maximum number of attachable files.", + }, + { + name: "maxFileSize", + type: "number", + description: "Maximum file size in bytes.", + }, + { + name: "globalDrop", + type: "boolean", + default: "false", + description: "Accept file drops anywhere on the document.", + }, + ], + composedWith: ["conversation", "message", "attachments"], + examples: [ + { + name: "with-attachments", + title: "With Attachments", + description: "PromptInput showing file attachment controls.", + }, + ], + }, + reasoning: { + description: + "A collapsible thinking indicator that auto-opens during streaming and auto-closes when done.", + usage: `import { + Reasoning, + ReasoningTrigger, + ReasoningContent, +} from "@/components/ai-elements/reasoning"; + + + + The model's internal reasoning text... +`, + props: [ + { + name: "isStreaming", + type: "boolean", + default: "false", + description: + "Whether the model is currently generating reasoning tokens.", + }, + { + name: "duration", + type: "number", + description: + "Elapsed thinking time in seconds, shown in the trigger label.", + }, + { + name: "open", + type: "boolean", + description: "Controlled open state.", + }, + { + name: "defaultOpen", + type: "boolean", + description: "Initial open state when uncontrolled.", + }, + ], + composedWith: ["message", "conversation", "chain-of-thought"], + examples: [], + }, + "chain-of-thought": { + description: + "Displays a step-by-step breakdown of an AI model's reasoning process with collapsible detail.", + usage: `import { + ChainOfThought, + ChainOfThoughtHeader, + ChainOfThoughtContent, + ChainOfThoughtStep, +} from "@/components/ai-elements/chain-of-thought"; + + + Reasoning steps + + + + +`, + props: [ + { + name: "open", + type: "boolean", + description: "Controlled open state of the collapsible.", + }, + { + name: "defaultOpen", + type: "boolean", + default: "false", + description: "Initial open state when uncontrolled.", + }, + { + name: "onOpenChange", + type: "(open: boolean) => void", + description: "Called when the open state changes.", + }, + ], + composedWith: ["message", "reasoning", "conversation"], + examples: [], + }, + "code-block": { + description: + "Syntax-highlighted code viewer powered by Shiki with copy-to-clipboard, line numbers, and language selection.", + usage: `import { + CodeBlock, + CodeBlockHeader, + CodeBlockActions, + CodeBlockCopyButton, +} from "@/components/ai-elements/code-block"; + + + + example.ts + + + + +`, + props: [ + { + name: "code", + type: "string", + description: "The source code string to highlight and display.", + }, + { + name: "language", + type: "BundledLanguage", + description: + 'The programming language for syntax highlighting (e.g. "tsx", "python").', + }, + { + name: "showLineNumbers", + type: "boolean", + default: "false", + description: "Whether to display line numbers in the gutter.", + }, + ], + composedWith: ["message", "artifact", "terminal"], + examples: [ + { + name: "with-diff", + title: "Multi-Language", + description: + "CodeBlock with a language selector for multiple snippets.", + }, + ], + }, + agent: { + description: + "Displays an AI agent configuration card with name, model, instructions, tools, and output schema.", + usage: `import { + Agent, + AgentHeader, + AgentContent, + AgentInstructions, +} from "@/components/ai-elements/agent"; + + + + + Find relevant papers on the topic. + +`, + props: [ + { + name: "children", + type: "ReactNode", + description: + "Agent sub-components (AgentHeader, AgentContent, AgentTools, etc.).", + }, + ], + composedWith: ["code-block", "message"], + examples: [], + }, + terminal: { + description: + "A terminal emulator view with ANSI color support, auto-scroll, copy, and clear actions.", + usage: `import { + Terminal, + TerminalHeader, + TerminalTitle, + TerminalContent, +} from "@/components/ai-elements/terminal"; + +`, + props: [ + { + name: "output", + type: "string", + description: "The terminal output string, supports ANSI escape codes.", + }, + { + name: "isStreaming", + type: "boolean", + default: "false", + description: "Shows a blinking cursor when true.", + }, + { + name: "autoScroll", + type: "boolean", + default: "true", + description: "Automatically scroll to the bottom when output changes.", + }, + { + name: "onClear", + type: "() => void", + description: + "Callback to clear the terminal; enables the clear button when provided.", + }, + ], + composedWith: ["code-block", "agent", "message"], + examples: [], + }, + "file-tree": { + description: + "An interactive file system tree with expandable folders, file selection, and custom icons.", + usage: `import { + FileTree, + FileTreeFolder, + FileTreeFile, +} from "@/components/ai-elements/file-tree"; + + + + + +`, + props: [ + { + name: "expanded", + type: "Set", + description: "Controlled set of expanded folder paths.", + }, + { + name: "defaultExpanded", + type: "Set", + description: "Initial set of expanded folder paths when uncontrolled.", + }, + { + name: "selectedPath", + type: "string", + description: "The currently selected file or folder path.", + }, + { + name: "onSelect", + type: "(path: string) => void", + description: "Called when a file or folder is selected.", + }, + ], + composedWith: ["artifact", "code-block"], + examples: [], + }, + artifact: { + description: + "A panel container for generated content with a header, close button, actions, and scrollable body.", + usage: `import { + Artifact, + ArtifactHeader, + ArtifactTitle, + ArtifactContent, + ArtifactClose, +} from "@/components/ai-elements/artifact"; + + + + Generated Code + + + ... +`, + props: [ + { + name: "children", + type: "ReactNode", + description: + "Artifact sub-components (ArtifactHeader, ArtifactContent, etc.).", + }, + ], + composedWith: ["code-block", "file-tree", "message"], + examples: [], + }, + context: { + description: + "A hover card displaying model context window usage, token breakdown, and cost estimation.", + usage: `import { + Context, + ContextTrigger, + ContextContent, + ContextContentHeader, +} from "@/components/ai-elements/context"; + + + + + + +`, + props: [ + { + name: "usedTokens", + type: "number", + description: "Number of tokens used in the current context window.", + }, + { + name: "maxTokens", + type: "number", + description: "Maximum token capacity of the model.", + }, + { + name: "usage", + type: "LanguageModelUsage", + description: + "Detailed token usage breakdown (input, output, reasoning, cache).", + }, + { + name: "modelId", + type: "string", + description: "Model identifier used for cost estimation via tokenlens.", + }, + ], + composedWith: ["prompt-input", "conversation"], + examples: [], + }, + plan: { + description: + "A collapsible card showing an AI-generated plan with streaming shimmer support.", + usage: `import { + Plan, + PlanHeader, + PlanTitle, + PlanDescription, + PlanContent, +} from "@/components/ai-elements/plan"; + + + + Implementation Plan + 3 steps to complete the task + + ... +`, + props: [ + { + name: "isStreaming", + type: "boolean", + default: "false", + description: + "Enables shimmer animation on title and description while streaming.", + }, + { + name: "open", + type: "boolean", + description: "Controlled collapsed/expanded state.", + }, + { + name: "defaultOpen", + type: "boolean", + description: "Initial open state when uncontrolled.", + }, + ], + composedWith: ["message", "conversation", "chain-of-thought"], + examples: [], + }, + button: { + description: + "A versatile button with multiple visual variants, sizes, and an icon-only appearance mode.", + usage: `import { Button } from "@/components/ui/button"; + +`, + props: [ + { + name: "variant", + type: '"default" | "destructive" | "outline" | "secondary" | "ghost" | "link"', + default: '"default"', + description: "Visual style of the button.", + }, + { + name: "size", + type: '"default" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm"', + default: '"default"', + description: "Size preset controlling height and padding.", + }, + { + name: "appearance", + type: '"default" | "icon"', + default: '"default"', + description: + 'Set to "icon" for square icon-only buttons that scale with size.', + }, + { + name: "asChild", + type: "boolean", + default: "false", + description: + "Merge props onto child element instead of rendering a + + + + Dialog Title + Description text. + + +`, + props: [ + { + name: "open", + type: "boolean", + description: "Controlled open state of the dialog.", + }, + { + name: "onOpenChange", + type: "(open: boolean) => void", + description: "Called when the dialog open state changes.", + }, + ], + composedWith: ["button", "card"], + examples: [], + }, + input: { + description: + "A styled text input with focus ring, validation states, and file input support.", + usage: `import { Input } from "@/components/ui/input"; + +`, + props: [ + { + name: "type", + type: "string", + default: '"text"', + description: "HTML input type attribute.", + }, + { + name: "placeholder", + type: "string", + description: "Placeholder text shown when empty.", + }, + ], + composedWith: ["button", "label"], + examples: [], + }, + tabs: { + description: + "A tabbed interface for switching between panels of related content.", + usage: `import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; + + + + Tab 1 + Tab 2 + + Content 1 + Content 2 +`, + props: [ + { + name: "defaultValue", + type: "string", + description: "The value of the tab that should be active by default.", + }, + { + name: "value", + type: "string", + description: "Controlled active tab value.", + }, + { + name: "onValueChange", + type: "(value: string) => void", + description: "Called when the active tab changes.", + }, + ], + composedWith: ["card"], + examples: [], + }, +}; + +export function getComponentDoc(slug: string): ComponentDoc | undefined { + return docs[slug]; +}