From c38554013c35a40703619be5f7fbf95c8fb1e134 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Wed, 8 Apr 2026 23:38:30 -0400 Subject: [PATCH 1/3] Add descriptions, props, composition, and examples to component pages Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/app/components/[name]/page.tsx | 3 + .../components/docs/component-page-shell.tsx | 129 +++- .../src/components/docs/demo-loader.tsx | 37 +- .../docs/examples/code-block/with-diff.tsx | 79 +++ .../examples/conversation/with-messages.tsx | 48 ++ .../docs/examples/message/streaming.tsx | 29 + .../docs/examples/message/with-actions.tsx | 42 ++ .../prompt-input/with-attachments.tsx | 31 + packages/ghost-ui/src/lib/component-docs.ts | 659 ++++++++++++++++++ 9 files changed, 1055 insertions(+), 2 deletions(-) create mode 100644 packages/ghost-ui/src/components/docs/examples/code-block/with-diff.tsx create mode 100644 packages/ghost-ui/src/components/docs/examples/conversation/with-messages.tsx create mode 100644 packages/ghost-ui/src/components/docs/examples/message/streaming.tsx create mode 100644 packages/ghost-ui/src/components/docs/examples/message/with-actions.tsx create mode 100644 packages/ghost-ui/src/components/docs/examples/prompt-input/with-attachments.tsx create mode 100644 packages/ghost-ui/src/lib/component-docs.ts diff --git a/packages/ghost-ui/src/app/components/[name]/page.tsx b/packages/ghost-ui/src/app/components/[name]/page.tsx index 0d7f3e3..8781ba2 100644 --- a/packages/ghost-ui/src/app/components/[name]/page.tsx +++ b/packages/ghost-ui/src/app/components/[name]/page.tsx @@ -5,6 +5,7 @@ import { getComponent, getComponentsByCategory, } from "@/lib/component-registry"; +import { getComponentDoc } from "@/lib/component-docs"; import { getComponentSpec } from "@/lib/component-source"; // ── Import demo source files as raw strings at build time ── @@ -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..32a13f6 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", @@ -177,6 +180,14 @@ export function ComponentPageShell({ > {component.name} + {docs?.description && ( +

+ {docs.description} +

+ )}
@@ -208,6 +219,21 @@ export function ComponentPageShell({
+ {docs?.usage && ( +
+ + + + Usage + + + + + + +
+ )} + {/* ── Tabs: Preview / Source / Demo Code ── */}
@@ -372,6 +398,107 @@ export function ComponentPageShell({
+ {docs && docs.props.length > 0 && ( +
+
+

+ Props +

+
+
+ {docs.props.map((prop) => ( + +
+ + {prop.type} + + {prop.default && ( + + Default: {prop.default} + + )} + {prop.description} +
+
+ ))} +
+
+ )} + + {docs && docs.composedWith.length > 0 && ( +
+
+

+ Works With +

+
+
+
+ {docs.composedWith.map((slug) => ( + + {slug} + + ))} +
+
+
+ )} + + {docs && docs.examples.length > 0 && ( +
+

+ Examples +

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

{example.title}

+ {example.description && ( +

+ {example.description} +

+ )} +
+
+ + + +
+
+ ))} +
+ )} + {/* ── Prev / Next ── */}
{prev ? ( 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..d929c56 --- /dev/null +++ b/packages/ghost-ui/src/components/docs/examples/code-block/with-diff.tsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import { + CodeBlock, + CodeBlockActions, + CodeBlockCopyButton, + CodeBlockHeader, + CodeBlockLanguageSelector, + CodeBlockLanguageSelectorContent, + CodeBlockLanguageSelectorItem, + CodeBlockLanguageSelectorTrigger, + CodeBlockLanguageSelectorValue, +} from "@/components/ai-elements/code-block"; + +const snippets: Record = { + 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..796e192 --- /dev/null +++ b/packages/ghost-ui/src/components/docs/examples/message/with-actions.tsx @@ -0,0 +1,42 @@ +import { CopyIcon, RefreshCwIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react"; +import { + Message, + MessageActions, + MessageAction, + 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..e5c51f8 --- /dev/null +++ b/packages/ghost-ui/src/components/docs/examples/prompt-input/with-attachments.tsx @@ -0,0 +1,31 @@ +import { + PromptInput, + PromptInputTextarea, + PromptInputFooter, + PromptInputSubmit, + PromptInputActionAddAttachments, + PromptInputTools, +} from "@/components/ai-elements/prompt-input"; + +export default function PromptInputWithAttachments() { + return ( +
+ { + console.log("Submitted:", msg); + }} + > + + + + + + + + +
+ ); +} 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..8819328 --- /dev/null +++ b/packages/ghost-ui/src/lib/component-docs.ts @@ -0,0 +1,659 @@ +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]; +} From d687396665f38e3db1f376851232ec17e24c4fc6 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 9 Apr 2026 08:30:00 -0400 Subject: [PATCH 2/3] Polish component pages: registry URL, centered layout, floating nav, fix demos - Install command uses full Ghost registry URL - Content centered at max-w-3xl - Prev/next floats on sides at xl+, falls back to bottom nav - Remove AI badge from component headers - Fix carousel and calendar demos hidden by container query Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/docs/component-page-shell.tsx | 45 +++++++++++++++---- .../docs/primitives/calendar-demo.tsx | 2 +- .../docs/primitives/carousel-demo.tsx | 2 +- 3 files changed, 38 insertions(+), 11 deletions(-) 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 32a13f6..3272655 100644 --- a/packages/ghost-ui/src/components/docs/component-page-shell.tsx +++ b/packages/ghost-ui/src/components/docs/component-page-shell.tsx @@ -135,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 = @@ -144,8 +144,36 @@ export function ComponentPageShell({ ) ?? []; return ( + <> + {/* ── Prev / Next (floating sides, xl+) ── */} + {prev && ( + + + + + + {prev.name} + + + )} + {next && ( + + + + + + {next.name} + + + )} -
+
{/* ── Header ── */}
@@ -166,11 +194,6 @@ export function ComponentPageShell({ > {categoryName} - {component.isAI && ( - - AI - - )}
@@ -499,8 +522,8 @@ export function ComponentPageShell({
)} - {/* ── Prev / Next ── */} -
+ {/* ── Prev / Next (bottom, below xl) ── */} +
{prev ? ( )}
+ + {/* spacer for bottom nav hidden on xl */} +
+ ); } 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) => ( From 297cfe3488f4970b932c5fa59ee5084a0037d670 Mon Sep 17 00:00:00 2001 From: Nahiyan Khan Date: Thu, 9 Apr 2026 08:35:57 -0400 Subject: [PATCH 3/3] Fix Biome lint: import ordering and line wrapping Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/app/components/[name]/page.tsx | 2 +- .../components/docs/component-page-shell.tsx | 683 +++++++++--------- .../docs/examples/code-block/with-diff.tsx | 11 +- .../docs/examples/message/with-actions.tsx | 9 +- .../prompt-input/with-attachments.tsx | 4 +- packages/ghost-ui/src/lib/component-docs.ts | 42 +- 6 files changed, 392 insertions(+), 359 deletions(-) diff --git a/packages/ghost-ui/src/app/components/[name]/page.tsx b/packages/ghost-ui/src/app/components/[name]/page.tsx index 8781ba2..4291697 100644 --- a/packages/ghost-ui/src/app/components/[name]/page.tsx +++ b/packages/ghost-ui/src/app/components/[name]/page.tsx @@ -1,11 +1,11 @@ import { Navigate, useParams } from "react-router"; import { ComponentPageShell } from "@/components/docs/component-page-shell"; +import { getComponentDoc } from "@/lib/component-docs"; import { getCategory, getComponent, getComponentsByCategory, } from "@/lib/component-registry"; -import { getComponentDoc } from "@/lib/component-docs"; import { getComponentSpec } from "@/lib/component-source"; // ── Import demo source files as raw strings at build time ── 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 3272655..ddb76eb 100644 --- a/packages/ghost-ui/src/components/docs/component-page-shell.tsx +++ b/packages/ghost-ui/src/components/docs/component-page-shell.tsx @@ -172,256 +172,163 @@ export function ComponentPageShell({ )} - -
- {/* ── Header ── */} -
-
- - - -
- +
+ {/* ── Header ── */} +
+
+ - {categoryName} - + + +
+ + {categoryName} + +
-
- -

- {component.name} -

- {docs?.description && ( -

- {docs.description} -

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

+ {docs.description} +

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

- Specification -

+ /> +
-
- {/* Variants */} - {spec && spec.variants.length > 0 && ( - - - - )} + {docs?.usage && ( +
+ + + + Usage + + + + + + +
+ )} - {/* Sub-components */} - {componentExports.length > 1 && ( - -
- {componentExports.map((exp) => ( - - {exp} - - ))} -
-
- )} + {/* ── Tabs: Preview / Source / Demo Code ── */} +
+
+ {( + [ + ["preview", "Preview"], + ...(spec?.source ? [["source", "Source"]] : []), + ...(demoSource ? [["demo", "Demo"]] : []), + ] as [string, string][] + ).map(([key, label]) => ( + + ))} +
- {/* Data slots */} - {spec && spec.dataSlots.length > 0 && ( - -
- {spec.dataSlots.map((slot) => ( - - [data-slot="{slot}"] - - ))} -
-
- )} +
+ {activeTab === "preview" && ( + + + + )} - {/* Registry dependencies */} - {component.registryDependencies.length > 0 && ( - -
- {component.registryDependencies.map((dep) => ( - - {dep} - - ))} + {activeTab === "source" && spec?.source && ( +
+ + + + {spec.filePath} + + + + + +
- - )} + )} - {/* npm Dependencies */} - {component.dependencies.length > 0 && ( - -
- {component.dependencies.map((dep) => ( - - {dep} - - ))} + {activeTab === "demo" && demoSource && ( +
+ + + + {component.slug}-demo.tsx + + + + + +
- - )} - - {/* File path */} - {spec?.filePath && ( - - {spec.filePath} - - )} + )} +
-
- {docs && docs.props.length > 0 && ( + {/* ── Spec Sheet ── */}

- Props + Specification

+
- {docs.props.map((prop) => ( - -
- - {prop.type} - - {prop.default && ( - - Default: {prop.default} + {/* Variants */} + {spec && spec.variants.length > 0 && ( + + + + )} + + {/* Sub-components */} + {componentExports.length > 1 && ( + +
+ {componentExports.map((exp) => ( + + {exp} - )} - {prop.description} + ))}
- ))} + )} + + {/* 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.composedWith.length > 0 && ( -
-
+ {docs && docs.props.length > 0 && ( +
+
+

+ Props +

+
+
+ {docs.props.map((prop) => ( + +
+ + {prop.type} + + {prop.default && ( + + Default:{" "} + {prop.default} + + )} + {prop.description} +
+
+ ))} +
+
+ )} + + {docs && docs.composedWith.length > 0 && ( +
+
+

+ Works With +

+
+
+
+ {docs.composedWith.map((slug) => ( + + {slug} + + ))} +
+
+
+ )} + + {docs && docs.examples.length > 0 && ( +

- Works With + Examples

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

{example.title}

+ {example.description && ( +

+ {example.description} +

+ )} +
+
+ + + +
+
+ ))}
-
-
- {docs.composedWith.map((slug) => ( - - {slug} - - ))} -
-
-
- )} + )} - {docs && docs.examples.length > 0 && ( -
-

- Examples -

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

{example.title}

- {example.description && ( -

- {example.description} -

- )} + {/* ── Prev / Next (bottom, below xl) ── */} +
+ {prev ? ( + + +
+ + Previous + + + {prev.name} +
-
- - - + + ) : ( +
+ )} + {next ? ( + +
+ Next + + {next.name} +
-
- ))} + + + ) : ( +
+ )}
- )} - {/* ── Prev / Next (bottom, below xl) ── */} -
- {prev ? ( - - -
- - Previous - - - {prev.name} - -
- - ) : ( -
- )} - {next ? ( - -
- Next - - {next.name} - -
- - - ) : ( -
- )} + {/* spacer for bottom nav hidden on xl */} +
- - {/* spacer for bottom nav hidden on xl */} -
-
- + ); } 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 index d929c56..2f4b633 100644 --- 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 @@ -11,7 +11,10 @@ import { CodeBlockLanguageSelectorValue, } from "@/components/ai-elements/code-block"; -const snippets: Record = { +const snippets: Record< + string, + { code: string; language: "typescript" | "python" | "rust" } +> = { typescript: { code: `function fibonacci(n: number): number { if (n <= 1) return n; @@ -51,7 +54,11 @@ export default function CodeBlockWithDiff() { return (
- + 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 index 796e192..115c468 100644 --- a/packages/ghost-ui/src/components/docs/examples/message/with-actions.tsx +++ b/packages/ghost-ui/src/components/docs/examples/message/with-actions.tsx @@ -1,8 +1,13 @@ -import { CopyIcon, RefreshCwIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react"; +import { + CopyIcon, + RefreshCwIcon, + ThumbsDownIcon, + ThumbsUpIcon, +} from "lucide-react"; import { Message, - MessageActions, MessageAction, + MessageActions, MessageContent, MessageResponse, } from "@/components/ai-elements/message"; 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 index e5c51f8..2d67b79 100644 --- 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 @@ -1,9 +1,9 @@ import { PromptInput, - PromptInputTextarea, + PromptInputActionAddAttachments, PromptInputFooter, PromptInputSubmit, - PromptInputActionAddAttachments, + PromptInputTextarea, PromptInputTools, } from "@/components/ai-elements/prompt-input"; diff --git a/packages/ghost-ui/src/lib/component-docs.ts b/packages/ghost-ui/src/lib/component-docs.ts index 8819328..5b16fd7 100644 --- a/packages/ghost-ui/src/lib/component-docs.ts +++ b/packages/ghost-ui/src/lib/component-docs.ts @@ -45,12 +45,14 @@ const docs: Record = { { name: "from", type: '"user" | "assistant" | "system"', - description: "The role of the message sender, controls alignment and styling.", + description: + "The role of the message sender, controls alignment and styling.", }, { name: "children", type: "ReactNode", - description: "Message sub-components (MessageContent, MessageActions, etc.).", + description: + "Message sub-components (MessageContent, MessageActions, etc.).", }, ], composedWith: [ @@ -181,12 +183,14 @@ const docs: Record = { name: "isStreaming", type: "boolean", default: "false", - description: "Whether the model is currently generating reasoning tokens.", + description: + "Whether the model is currently generating reasoning tokens.", }, { name: "duration", type: "number", - description: "Elapsed thinking time in seconds, shown in the trigger label.", + description: + "Elapsed thinking time in seconds, shown in the trigger label.", }, { name: "open", @@ -267,7 +271,8 @@ const docs: Record = { { name: "language", type: "BundledLanguage", - description: 'The programming language for syntax highlighting (e.g. "tsx", "python").', + description: + 'The programming language for syntax highlighting (e.g. "tsx", "python").', }, { name: "showLineNumbers", @@ -281,7 +286,8 @@ const docs: Record = { { name: "with-diff", title: "Multi-Language", - description: "CodeBlock with a language selector for multiple snippets.", + description: + "CodeBlock with a language selector for multiple snippets.", }, ], }, @@ -305,7 +311,8 @@ const docs: Record = { { name: "children", type: "ReactNode", - description: "Agent sub-components (AgentHeader, AgentContent, AgentTools, etc.).", + description: + "Agent sub-components (AgentHeader, AgentContent, AgentTools, etc.).", }, ], composedWith: ["code-block", "message"], @@ -343,7 +350,8 @@ const docs: Record = { { name: "onClear", type: "() => void", - description: "Callback to clear the terminal; enables the clear button when provided.", + description: + "Callback to clear the terminal; enables the clear button when provided.", }, ], composedWith: ["code-block", "agent", "message"], @@ -410,7 +418,8 @@ const docs: Record = { { name: "children", type: "ReactNode", - description: "Artifact sub-components (ArtifactHeader, ArtifactContent, etc.).", + description: + "Artifact sub-components (ArtifactHeader, ArtifactContent, etc.).", }, ], composedWith: ["code-block", "file-tree", "message"], @@ -446,7 +455,8 @@ const docs: Record = { { name: "usage", type: "LanguageModelUsage", - description: "Detailed token usage breakdown (input, output, reasoning, cache).", + description: + "Detailed token usage breakdown (input, output, reasoning, cache).", }, { name: "modelId", @@ -480,7 +490,8 @@ const docs: Record = { name: "isStreaming", type: "boolean", default: "false", - description: "Enables shimmer animation on title and description while streaming.", + description: + "Enables shimmer animation on title and description while streaming.", }, { name: "open", @@ -519,13 +530,15 @@ const docs: Record = { name: "appearance", type: '"default" | "icon"', default: '"default"', - description: 'Set to "icon" for square icon-only buttons that scale with size.', + 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