diff --git a/apps/docs/src/App.tsx b/apps/docs/src/App.tsx index 25e2f11..53002c4 100644 --- a/apps/docs/src/App.tsx +++ b/apps/docs/src/App.tsx @@ -1,12 +1,23 @@ import { ThemeProvider } from "ghost-ui"; -import { Navigate, Route, Routes } from "react-router"; +import { Navigate, Route, Routes, useParams } from "react-router"; import DriftEngineIndex from "@/app/docs/page"; import WorkflowPage from "@/app/docs/workflow/page"; import HomePage from "@/app/page"; import ToolsIndex from "@/app/tools/page"; +import ComponentPage from "@/app/ui/components/[name]/page"; +import ComponentsIndex from "@/app/ui/components/page"; +import ColorsPage from "@/app/ui/foundations/colors/page"; +import FoundationsIndex from "@/app/ui/foundations/page"; +import TypographyPage from "@/app/ui/foundations/typography/page"; +import DesignLanguageIndex from "@/app/ui/page"; import { Dock } from "@/components/docs/dock"; import { mdxDocsRoutes } from "@/routes/docs-routes"; +function ComponentRedirect() { + const { name } = useParams<{ name: string }>(); + return ; +} + export function App() { return ( } /> + } /> + } /> + } + /> + } /> + } /> + {/* Redirects from old /docs/* URLs */} } /> } /> + + {/* Redirects from legacy root /foundations and /components URLs */} + } + /> + } + /> + } + /> + } + /> + } /> diff --git a/apps/docs/src/app/ui/components/[name]/page.tsx b/apps/docs/src/app/ui/components/[name]/page.tsx new file mode 100644 index 0000000..4291697 --- /dev/null +++ b/apps/docs/src/app/ui/components/[name]/page.tsx @@ -0,0 +1,59 @@ +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 { getComponentSpec } from "@/lib/component-source"; + +// ── Import demo source files as raw strings at build time ── + +const demoSourceModules = import.meta.glob( + [ + "/src/components/docs/primitives/*-demo.tsx", + "/src/components/docs/ai-elements/*-demo.tsx", + ], + { query: "?raw", eager: true }, +) as Record; + +function getDemoSource( + slug: string, + source: "primitives" | "ai-elements", +): string | null { + const key = `/src/components/docs/${source}/${slug}-demo.tsx`; + return demoSourceModules[key]?.default ?? null; +} + +export default function ComponentPage() { + const { name } = useParams<{ name: string }>(); + + if (!name) return ; + + const component = getComponent(name); + if (!component) return ; + + const category = getCategory(component.primaryCategory); + const siblings = getComponentsByCategory(component.primaryCategory); + const currentIndex = siblings.findIndex((c) => c.slug === name); + const prev = currentIndex > 0 ? siblings[currentIndex - 1] : null; + const next = + currentIndex < siblings.length - 1 ? siblings[currentIndex + 1] : null; + + const demoSource = getDemoSource(component.slug, component.demoSource); + const spec = getComponentSpec(component.slug); + const docs = getComponentDoc(name); + + return ( + + ); +} diff --git a/apps/docs/src/app/ui/components/page.tsx b/apps/docs/src/app/ui/components/page.tsx new file mode 100644 index 0000000..5be5744 --- /dev/null +++ b/apps/docs/src/app/ui/components/page.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { useStaggerReveal } from "ghost-ui"; +import { useMemo, useState } from "react"; +import { Link } from "react-router"; +import { AnimatedPageHeader } from "@/components/docs/animated-page-header"; +import { SectionWrapper } from "@/components/docs/wrappers"; +import { + categories, + getAllComponents, + getComponentsByCategory, +} from "@/lib/component-registry"; + +/* ── Fuzzy match ─────────────────────────────────────────────────────── */ + +function fuzzyMatch(query: string, target: string): number { + const q = query.toLowerCase(); + const t = target.toLowerCase(); + + // exact substring match scores highest + if (t.includes(q)) return 1; + + // character-by-character fuzzy: every query char must appear in order + let qi = 0; + let score = 0; + let lastIdx = -1; + + for (let ti = 0; ti < t.length && qi < q.length; ti++) { + if (t[ti] === q[qi]) { + // bonus for consecutive matches + score += ti === lastIdx + 1 ? 2 : 1; + lastIdx = ti; + qi++; + } + } + + // all query characters must be found + if (qi < q.length) return 0; + + // normalise to 0–1 range (below 1 so substring match always wins) + return (score / (q.length * 2)) * 0.9; +} + +/* ── Page ─────────────────────────────────────────────────────────────── */ + +export default function ComponentsIndex() { + const [query, setQuery] = useState(""); + const allComponents = useMemo(() => getAllComponents(), []); + + const filtered = useMemo(() => { + if (!query.trim()) return null; + return allComponents + .map((c) => ({ ...c, score: fuzzyMatch(query, c.name) })) + .filter((c) => c.score > 0) + .sort((a, b) => b.score - a.score); + }, [query, allComponents]); + + const isSearching = query.trim().length > 0; + + return ( + + + + {/* Search */} +
+ setQuery(e.target.value)} + placeholder="Search components…" + className="w-full max-w-md rounded-full border border-border-card bg-card px-5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/50 outline-none focus:border-foreground/25 transition-colors duration-200" + /> +
+ + {/* Search results */} + {isSearching && ( +
+ {filtered && filtered.length > 0 ? ( +
+ {filtered.map((item) => ( + + ))} +
+ ) : ( +

+ No components match "{query}" +

+ )} +
+ )} + + {/* Category sections */} + {!isSearching && ( +
+ {categories.map((cat) => { + const items = getComponentsByCategory(cat.slug); + if (items.length === 0) return null; + return ( + + ); + })} +
+ )} +
+ ); +} + +/* ── Pill ─────────────────────────────────────────────────────────────── */ + +function ComponentPill({ slug, name }: { slug: string; name: string }) { + return ( + + + + {name} + + + ); +} + +/* ── Category section ─────────────────────────────────────────────────── */ + +function CategorySection({ + name, + description, + items, +}: { + name: string; + description: string; + items: { slug: string; name: string }[]; +}) { + const ref = useStaggerReveal(".component-card", { + stagger: 0.04, + y: 24, + duration: 0.6, + }); + + return ( +
+

+ {name} +

+

{description}

+
+ {items.map((item) => ( + + ))} +
+
+ ); +} diff --git a/apps/docs/src/app/ui/foundations/colors/page.tsx b/apps/docs/src/app/ui/foundations/colors/page.tsx new file mode 100644 index 0000000..ddaa335 --- /dev/null +++ b/apps/docs/src/app/ui/foundations/colors/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useScrollReveal } from "ghost-ui"; +import { AnimatedPageHeader } from "@/components/docs/animated-page-header"; +import { ColorsDemos } from "@/components/docs/foundations/colors"; +import { SectionWrapper } from "@/components/docs/wrappers"; + +export default function ColorsPage() { + const contentRef = useScrollReveal({ + y: 50, + duration: 0.9, + ease: "expo.out", + }); + + return ( + <> + + + + + +
+ +
+
+ + ); +} diff --git a/apps/docs/src/app/ui/foundations/page.tsx b/apps/docs/src/app/ui/foundations/page.tsx new file mode 100644 index 0000000..1f64834 --- /dev/null +++ b/apps/docs/src/app/ui/foundations/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useStaggerReveal } from "ghost-ui"; +import { type ReactNode } from "react"; +import { Link } from "react-router"; +import { AnimatedPageHeader } from "@/components/docs/animated-page-header"; +import { SectionWrapper } from "@/components/docs/wrappers"; + +function ColorsVisual() { + return ( +
+ {[ + "bg-foreground", + "bg-foreground/80", + "bg-foreground/60", + "bg-foreground/40", + "bg-foreground/20", + "bg-foreground/10", + ].map((bg, i) => ( +
+ ))} +
+ ); +} + +function TypographyVisual() { + return ( +
+
+
+
+
+
+ ); +} + +const foundations: { + name: string; + href: string; + description: string; + visual: ReactNode; +}[] = [ + { + name: "Colors", + href: "/ui/foundations/colors", + description: + "A pure monochromatic scale with selective semantic color for status and utility.", + visual: , + }, + { + name: "Typography", + href: "/ui/foundations/typography", + description: + "Magazine-grade hierarchy. Display for headers, Regular for body, Mono for data.", + visual: , + }, +]; + +export default function FoundationsIndex() { + const ref = useStaggerReveal(".foundation-card", { + stagger: 0.06, + y: 30, + duration: 0.7, + }); + + return ( + + + +
+ {foundations.map((item) => ( + +
{item.visual}
+ + + {item.name} + + + +

+ {item.description} +

+ + ))} +
+
+ ); +} diff --git a/apps/docs/src/app/ui/foundations/typography/page.tsx b/apps/docs/src/app/ui/foundations/typography/page.tsx new file mode 100644 index 0000000..d6a8f53 --- /dev/null +++ b/apps/docs/src/app/ui/foundations/typography/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { useScrollReveal } from "ghost-ui"; +import { AnimatedPageHeader } from "@/components/docs/animated-page-header"; +import { TypographyDemos } from "@/components/docs/foundations/typography"; +import { SectionWrapper } from "@/components/docs/wrappers"; + +export default function TypographyPage() { + const contentRef = useScrollReveal({ + y: 50, + duration: 0.9, + ease: "expo.out", + }); + + return ( + <> + + + + + +
+ +
+
+ + ); +} diff --git a/apps/docs/src/app/ui/page.tsx b/apps/docs/src/app/ui/page.tsx new file mode 100644 index 0000000..686405e --- /dev/null +++ b/apps/docs/src/app/ui/page.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useStaggerReveal } from "ghost-ui"; +import { type ReactNode } from "react"; +import { Link } from "react-router"; +import { AnimatedPageHeader } from "@/components/docs/animated-page-header"; +import { SectionWrapper } from "@/components/docs/wrappers"; +import { getAllComponents } from "@/lib/component-registry"; + +function ColorsVisual() { + return ( +
+ {[ + "bg-foreground", + "bg-foreground/80", + "bg-foreground/60", + "bg-foreground/40", + "bg-foreground/20", + "bg-foreground/10", + ].map((bg, i) => ( +
+ ))} +
+ ); +} + +function TypographyVisual() { + return ( +
+
+
+
+
+
+ ); +} + +function ComponentsVisual() { + const count = getAllComponents().length; + return ( +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} + + {count} components + +
+ ); +} + +const sections: { + name: string; + href: string; + description: string; + visual: ReactNode; +}[] = [ + { + name: "Foundations", + href: "/ui/foundations", + description: + "Color, typography, and the design tokens that underpin every Ghost UI component.", + visual: ( +
+
+ +
+
+ +
+
+ ), + }, + { + name: "Components", + href: "/ui/components", + description: + "Production-ready building blocks. Every component follows Ghost UI — pill-first, monochromatic, accessible.", + visual: , + }, +]; + +export default function DesignLanguageIndex() { + const ref = useStaggerReveal(".dl-card", { + stagger: 0.06, + y: 30, + duration: 0.7, + }); + + return ( + + + +
+ {sections.map((item) => ( + +
{item.visual}
+ + + {item.name} + + + +

+ {item.description} +

+ + ))} +
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/agent-demo.tsx b/apps/docs/src/components/docs/ai-elements/agent-demo.tsx new file mode 100644 index 0000000..0b34086 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/agent-demo.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { + Agent, + AgentContent, + AgentHeader, + AgentInstructions, + AgentOutput, + AgentTool, + AgentTools, +} from "ghost-ui"; + +export function AgentDemo() { + return ( +
+ + + + + You are a research assistant that helps users find and summarize + academic papers. Use the provided tools to search databases and + retrieve relevant publications. Always cite your sources. + + + + + + + + + + + + + + Review code for best practices, potential bugs, and performance + issues. Provide actionable feedback with specific line references. + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/artifact-demo.tsx b/apps/docs/src/components/docs/ai-elements/artifact-demo.tsx new file mode 100644 index 0000000..ab5105a --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/artifact-demo.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { + Artifact, + ArtifactAction, + ArtifactActions, + ArtifactClose, + ArtifactContent, + ArtifactDescription, + ArtifactHeader, + ArtifactTitle, +} from "ghost-ui"; +import { CopyIcon, DownloadIcon, ShareIcon } from "lucide-react"; + +export function ArtifactDemo() { + return ( +
+ + +
+ React Component + + A reusable button component with variants + +
+ + + + + + +
+ +
+            {`export function Button({ variant = "primary", children }) {
+  return (
+    
+  );
+}`}
+          
+
+
+ + + +
+ SVG Illustration + + Generated logo design concept + +
+ + + + +
+ +
+ AI +
+
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/attachments-demo.tsx b/apps/docs/src/components/docs/ai-elements/attachments-demo.tsx new file mode 100644 index 0000000..a5caa72 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/attachments-demo.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { + Attachment, + AttachmentEmpty, + AttachmentInfo, + AttachmentPreview, + AttachmentRemove, + Attachments, +} from "ghost-ui"; + +const mockAttachments = [ + { + id: "1", + type: "file" as const, + mediaType: "image/png", + filename: "screenshot.png", + url: "https://picsum.photos/seed/attach1/200/200", + }, + { + id: "2", + type: "file" as const, + mediaType: "application/pdf", + filename: "quarterly-report.pdf", + url: "", + }, + { + id: "3", + type: "file" as const, + mediaType: "audio/mp3", + filename: "recording.mp3", + url: "", + }, +]; + +export function AttachmentsDemo() { + return ( +
+
+

Grid variant

+ + {mockAttachments.map((file) => ( + {}}> + + + + ))} + +
+ +
+

Inline variant

+ + {mockAttachments.map((file) => ( + {}}> + + + + + ))} + +
+ +
+

List variant

+ + {mockAttachments.map((file) => ( + {}}> + + + + + ))} + +
+ +
+

Empty state

+ +
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/audio-player-demo.tsx b/apps/docs/src/components/docs/ai-elements/audio-player-demo.tsx new file mode 100644 index 0000000..eb65a58 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/audio-player-demo.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { + AudioPlayer, + AudioPlayerControlBar, + AudioPlayerDurationDisplay, + AudioPlayerElement, + AudioPlayerMuteButton, + AudioPlayerPlayButton, + AudioPlayerSeekBackwardButton, + AudioPlayerSeekForwardButton, + AudioPlayerTimeDisplay, + AudioPlayerTimeRange, + AudioPlayerVolumeRange, +} from "ghost-ui"; + +export function AudioPlayerDemo() { + return ( +
+
+

Full audio player

+ + + + + + + + + + + + + +
+ +
+

+ Minimal player (play, time, scrub) +

+ + + + + + + + + +
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/canvas-demo.tsx b/apps/docs/src/components/docs/ai-elements/canvas-demo.tsx new file mode 100644 index 0000000..27fc23c --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/canvas-demo.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; +import { + Canvas, + Controls, + Node, + NodeContent, + NodeDescription, + NodeHeader, + NodeTitle, +} from "ghost-ui"; +import { useMemo } from "react"; + +const InputNode = () => ( + + + User Input + Text prompt + + +

+ Accepts a natural language query from the user. +

+
+
+); + +const ProcessNode = () => ( + + + LLM Processing + GPT-4o + + +

+ Processes the input and generates a response. +

+
+
+); + +const OutputNode = () => ( + + + Response + Markdown output + + +

+ Displays the generated response to the user. +

+
+
+); + +const initialNodes = [ + { id: "1", type: "input", position: { x: 0, y: 100 }, data: {} }, + { id: "2", type: "process", position: { x: 500, y: 100 }, data: {} }, + { id: "3", type: "output", position: { x: 1000, y: 100 }, data: {} }, +]; + +const initialEdges = [ + { id: "e1-2", source: "1", target: "2" }, + { id: "e2-3", source: "2", target: "3" }, +]; + +export function CanvasDemo() { + const nodeTypes: NodeTypes = useMemo( + () => ({ + input: InputNode, + output: OutputNode, + process: ProcessNode, + }), + [], + ); + + return ( + +
+ + + +
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/chain-of-thought-demo.tsx b/apps/docs/src/components/docs/ai-elements/chain-of-thought-demo.tsx new file mode 100644 index 0000000..5238cc1 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/chain-of-thought-demo.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { + ChainOfThought, + ChainOfThoughtContent, + ChainOfThoughtHeader, + ChainOfThoughtSearchResult, + ChainOfThoughtSearchResults, + ChainOfThoughtStep, +} from "ghost-ui"; +import { DatabaseIcon, FileTextIcon, SearchIcon } from "lucide-react"; + +export function ChainOfThoughtDemo() { + return ( + + Researching climate data + + + + IPCC 2024 + + NASA Climate + + NOAA Data + + + + + + + + + + + ); +} diff --git a/apps/docs/src/components/docs/ai-elements/checkpoint-demo.tsx b/apps/docs/src/components/docs/ai-elements/checkpoint-demo.tsx new file mode 100644 index 0000000..5073b3d --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/checkpoint-demo.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Checkpoint, CheckpointIcon, CheckpointTrigger } from "ghost-ui"; + +export function CheckpointDemo() { + return ( +
+ + + + Checkpoint 1 — Initial draft + + + +
+ Some conversation content between checkpoints... +
+ + + + + Checkpoint 2 — After revisions + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/code-block-demo.tsx b/apps/docs/src/components/docs/ai-elements/code-block-demo.tsx new file mode 100644 index 0000000..9fa3d01 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/code-block-demo.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { + CodeBlock, + CodeBlockActions, + CodeBlockCopyButton, + CodeBlockFilename, + CodeBlockHeader, + CodeBlockTitle, +} from "ghost-ui"; + +const typescriptCode = `interface User { + id: string; + name: string; + email: string; + role: "admin" | "user" | "guest"; +} + +async function getUser(id: string): Promise { + const response = await fetch(\`/api/users/\${id}\`); + + if (!response.ok) { + throw new Error(\`Failed to fetch user: \${response.statusText}\`); + } + + return response.json(); +}`; + +const cssCode = `.container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; + padding: 2rem; +} + +.card { + border-radius: 0.75rem; + background: var(--card-bg); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +}`; + +const jsonCode = `{ + "name": "@acme/design-system", + "version": "2.4.0", + "dependencies": { + "react": "^19.0.0", + "tailwindcss": "^4.0.0" + } +}`; + +export function CodeBlockDemo() { + return ( +
+ + + + lib/api/users.ts + + + + + + + + + + + styles/layout.css + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/commit-demo.tsx b/apps/docs/src/components/docs/ai-elements/commit-demo.tsx new file mode 100644 index 0000000..73cb521 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/commit-demo.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { + Commit, + CommitActions, + CommitAuthor, + CommitAuthorAvatar, + CommitContent, + CommitCopyButton, + CommitFile, + CommitFileAdditions, + CommitFileChanges, + CommitFileDeletions, + CommitFileIcon, + CommitFileInfo, + CommitFilePath, + CommitFileStatus, + CommitFiles, + CommitHash, + CommitHeader, + CommitInfo, + CommitMessage, + CommitMetadata, + CommitSeparator, + CommitTimestamp, +} from "ghost-ui"; + +export function CommitDemo() { + return ( +
+ + + + + + + + feat: add user authentication with OAuth2 support + + + a1b2c3d + + + + + + + + + + + + + + + src/lib/auth/oauth.ts + + + + + + + + + + + src/middleware.ts + + + + + + + + + + + src/lib/auth/legacy.ts + + + + + + + + + + + src/config/auth.config.ts + + + + + + + + + + + + + + + + + + fix: resolve race condition in data fetching + + + f8e9d0c + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/confirmation-demo.tsx b/apps/docs/src/components/docs/ai-elements/confirmation-demo.tsx new file mode 100644 index 0000000..772fc14 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/confirmation-demo.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { + Confirmation, + ConfirmationAccepted, + ConfirmationAction, + ConfirmationActions, + ConfirmationRejected, + ConfirmationRequest, + ConfirmationTitle, +} from "ghost-ui"; + +export function ConfirmationDemo() { + return ( +
+
+

Approval requested

+ + + The assistant wants to execute rm -rf ./build + + +

+ This action will delete the build directory. Do you want to + proceed? +

+
+ + Deny + Approve + +
+
+ +
+

Accepted

+ + + Executed rm -rf ./build + + +

+ Action was approved and completed successfully. +

+
+
+
+ +
+

Rejected

+ + + Blocked rm -rf ./build + + +

+ Action was denied by the user. +

+
+
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/connection-demo.tsx b/apps/docs/src/components/docs/ai-elements/connection-demo.tsx new file mode 100644 index 0000000..629c0c5 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/connection-demo.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; +import { + Canvas, + Connection, + Node, + NodeContent, + NodeHeader, + NodeTitle, +} from "ghost-ui"; +import { useMemo } from "react"; + +const SourceNode = () => ( + + + Source + + +

+ Drag from the handle to see the custom connection line. +

+
+
+); + +const TargetNode = () => ( + + + Target + + +

Drop a connection here.

+
+
+); + +const initialNodes = [ + { id: "1", type: "source", position: { x: 0, y: 80 }, data: {} }, + { id: "2", type: "target", position: { x: 500, y: 80 }, data: {} }, +]; + +export function ConnectionDemo() { + const nodeTypes: NodeTypes = useMemo( + () => ({ + source: SourceNode, + target: TargetNode, + }), + [], + ); + + return ( +
+

+ Drag from the source handle to see the animated bezier connection line + with a circular endpoint indicator. +

+ +
+ +
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/context-demo.tsx b/apps/docs/src/components/docs/ai-elements/context-demo.tsx new file mode 100644 index 0000000..6b11729 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/context-demo.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { + Context, + ContextContent, + ContextContentBody, + ContextContentFooter, + ContextContentHeader, + ContextInputUsage, + ContextOutputUsage, + ContextTrigger, +} from "ghost-ui"; + +export function ContextDemo() { + return ( + + + + + +
+ + +
+
+ +
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/controls-demo.tsx b/apps/docs/src/components/docs/ai-elements/controls-demo.tsx new file mode 100644 index 0000000..2d845c1 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/controls-demo.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; +import { + Canvas, + Controls, + Node, + NodeContent, + NodeHeader, + NodeTitle, +} from "ghost-ui"; +import { useMemo } from "react"; + +const SampleNode = () => ( + + + Sample Node + + +

+ Use the controls in the bottom-left to zoom, fit view, and lock + interactions. +

+
+
+); + +const initialNodes = [ + { id: "1", type: "sample", position: { x: 0, y: 0 }, data: {} }, + { id: "2", type: "sample", position: { x: 400, y: 150 }, data: {} }, +]; + +export function ControlsDemo() { + const nodeTypes: NodeTypes = useMemo( + () => ({ + sample: SampleNode, + }), + [], + ); + + return ( + +
+ + + +
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/conversation-demo.tsx b/apps/docs/src/components/docs/ai-elements/conversation-demo.tsx new file mode 100644 index 0000000..32a994a --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/conversation-demo.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { + Conversation, + ConversationContent, + ConversationEmptyState, +} from "ghost-ui"; +import { MessageSquareIcon } from "lucide-react"; + +export function ConversationDemo() { + return ( +
+ + + } + /> + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/edge-demo.tsx b/apps/docs/src/components/docs/ai-elements/edge-demo.tsx new file mode 100644 index 0000000..ec29297 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/edge-demo.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { + type EdgeTypes, + type NodeTypes, + ReactFlowProvider, +} from "@xyflow/react"; +import { Canvas, Edge, Node, NodeHeader, NodeTitle } from "ghost-ui"; +import { useMemo } from "react"; + +const SimpleNode = ({ data }: { data: { label: string } }) => ( + + + {data.label} + + +); + +const initialNodes = [ + { + id: "a1", + type: "simple", + position: { x: 0, y: 0 }, + data: { label: "Start" }, + }, + { + id: "a2", + type: "simple", + position: { x: 450, y: 0 }, + data: { label: "Animated" }, + }, + { + id: "b1", + type: "simple", + position: { x: 0, y: 150 }, + data: { label: "Draft" }, + }, + { + id: "b2", + type: "simple", + position: { x: 450, y: 150 }, + data: { label: "Temporary" }, + }, +]; + +const initialEdges = [ + { id: "e-animated", source: "a1", target: "a2", type: "animated" }, + { id: "e-temporary", source: "b1", target: "b2", type: "temporary" }, +]; + +export function EdgeDemo() { + const nodeTypes: NodeTypes = useMemo( + () => ({ + simple: SimpleNode, + }), + [], + ); + + const edgeTypes: EdgeTypes = useMemo( + () => ({ + animated: Edge.Animated, + temporary: Edge.Temporary, + }), + [], + ); + + return ( +
+

+ Two edge variants: Animated (top, with a traveling dot) + and Temporary (bottom, dashed line). +

+ +
+ +
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/environment-variables-demo.tsx b/apps/docs/src/components/docs/ai-elements/environment-variables-demo.tsx new file mode 100644 index 0000000..8c0d8ce --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/environment-variables-demo.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { + EnvironmentVariable, + EnvironmentVariableCopyButton, + EnvironmentVariableGroup, + EnvironmentVariableName, + EnvironmentVariableRequired, + EnvironmentVariables, + EnvironmentVariablesContent, + EnvironmentVariablesHeader, + EnvironmentVariablesTitle, + EnvironmentVariablesToggle, + EnvironmentVariableValue, +} from "ghost-ui"; + +export function EnvironmentVariablesDemo() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/file-tree-demo.tsx b/apps/docs/src/components/docs/ai-elements/file-tree-demo.tsx new file mode 100644 index 0000000..c2ce9e4 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/file-tree-demo.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { FileTree, FileTreeFile, FileTreeFolder } from "ghost-ui"; + +export function FileTreeDemo() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/image-demo.tsx b/apps/docs/src/components/docs/ai-elements/image-demo.tsx new file mode 100644 index 0000000..6511c17 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/image-demo.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { Image } from "ghost-ui"; + +// A tiny 1x1 transparent PNG placeholder +const PLACEHOLDER_BASE64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; + +export function ImageDemo() { + return ( +
+

+ Renders an AI-generated image from base64 data. The component + automatically constructs a data URI from the provided media type and + base64 string. +

+ +
+
+ AI-generated landscape placeholder + + PNG, landscape aspect ratio + +
+ +
+ AI-generated portrait placeholder + + PNG, square aspect ratio + +
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/index.tsx b/apps/docs/src/components/docs/ai-elements/index.tsx new file mode 100644 index 0000000..46e71e0 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/index.tsx @@ -0,0 +1,238 @@ +"use client"; + +// Code +import { AgentDemo } from "@/components/docs/ai-elements/agent-demo"; +import { ArtifactDemo } from "@/components/docs/ai-elements/artifact-demo"; +// Chatbot +import { AttachmentsDemo } from "@/components/docs/ai-elements/attachments-demo"; +// Voice +import { AudioPlayerDemo } from "@/components/docs/ai-elements/audio-player-demo"; +// Workflow +import { CanvasDemo } from "@/components/docs/ai-elements/canvas-demo"; +import { ChainOfThoughtDemo } from "@/components/docs/ai-elements/chain-of-thought-demo"; +import { CheckpointDemo } from "@/components/docs/ai-elements/checkpoint-demo"; +import { CodeBlockDemo } from "@/components/docs/ai-elements/code-block-demo"; +import { CommitDemo } from "@/components/docs/ai-elements/commit-demo"; +import { ConfirmationDemo } from "@/components/docs/ai-elements/confirmation-demo"; +import { ConnectionDemo } from "@/components/docs/ai-elements/connection-demo"; +import { ContextDemo } from "@/components/docs/ai-elements/context-demo"; +import { ControlsDemo } from "@/components/docs/ai-elements/controls-demo"; +import { ConversationDemo } from "@/components/docs/ai-elements/conversation-demo"; +import { EdgeDemo } from "@/components/docs/ai-elements/edge-demo"; +import { EnvironmentVariablesDemo } from "@/components/docs/ai-elements/environment-variables-demo"; +import { FileTreeDemo } from "@/components/docs/ai-elements/file-tree-demo"; +// Utilities +import { ImageDemo } from "@/components/docs/ai-elements/image-demo"; +import { InlineCitationDemo } from "@/components/docs/ai-elements/inline-citation-demo"; +import { JsxPreviewDemo } from "@/components/docs/ai-elements/jsx-preview-demo"; +import { MessageDemo } from "@/components/docs/ai-elements/message-demo"; +import { MicSelectorDemo } from "@/components/docs/ai-elements/mic-selector-demo"; +import { ModelSelectorDemo } from "@/components/docs/ai-elements/model-selector-demo"; +import { NodeDemo } from "@/components/docs/ai-elements/node-demo"; +import { OpenInChatDemo } from "@/components/docs/ai-elements/open-in-chat-demo"; +import { PackageInfoDemo } from "@/components/docs/ai-elements/package-info-demo"; +import { PanelDemo } from "@/components/docs/ai-elements/panel-demo"; +import { PersonaDemo } from "@/components/docs/ai-elements/persona-demo"; +import { PlanDemo } from "@/components/docs/ai-elements/plan-demo"; +import { PromptInputDemo } from "@/components/docs/ai-elements/prompt-input-demo"; +import { QueueDemo } from "@/components/docs/ai-elements/queue-demo"; +import { ReasoningDemo } from "@/components/docs/ai-elements/reasoning-demo"; +import { SandboxDemo } from "@/components/docs/ai-elements/sandbox-demo"; +import { SchemaDisplayDemo } from "@/components/docs/ai-elements/schema-display-demo"; +import { ShimmerDemo } from "@/components/docs/ai-elements/shimmer-demo"; +import { SnippetDemo } from "@/components/docs/ai-elements/snippet-demo"; +import { SourcesDemo } from "@/components/docs/ai-elements/sources-demo"; +import { SpeechInputDemo } from "@/components/docs/ai-elements/speech-input-demo"; +import { StackTraceDemo } from "@/components/docs/ai-elements/stack-trace-demo"; +import { SuggestionDemo } from "@/components/docs/ai-elements/suggestion-demo"; +import { TaskDemo } from "@/components/docs/ai-elements/task-demo"; +import { TerminalDemo } from "@/components/docs/ai-elements/terminal-demo"; +import { TestResultsDemo } from "@/components/docs/ai-elements/test-results-demo"; +import { ToolDemo } from "@/components/docs/ai-elements/tool-demo"; +import { ToolbarDemo } from "@/components/docs/ai-elements/toolbar-demo"; +import { TranscriptionDemo } from "@/components/docs/ai-elements/transcription-demo"; +import { VoiceSelectorDemo } from "@/components/docs/ai-elements/voice-selector-demo"; +import { WebPreviewDemo } from "@/components/docs/ai-elements/web-preview-demo"; +import { ComponentWrapper } from "@/components/docs/primitives/component-wrapper"; + +function CategoryLabel({ children }: { children: React.ReactNode }) { + return ( +
+

+ {children} +

+
+ ); +} + +export function AIElementDemos() { + return ( +
+ {/* Chatbot */} + Chatbot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Code */} + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Voice */} + Voice + + + + + + + + + + + + + + + + + + + + {/* Workflow */} + Workflow + + + + + + + + + + + + + + + + + + + + + + + {/* Utilities */} + Utilities + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/inline-citation-demo.tsx b/apps/docs/src/components/docs/ai-elements/inline-citation-demo.tsx new file mode 100644 index 0000000..3221903 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/inline-citation-demo.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { + InlineCitation, + InlineCitationCard, + InlineCitationCardBody, + InlineCitationCardTrigger, + InlineCitationCarousel, + InlineCitationCarouselContent, + InlineCitationCarouselHeader, + InlineCitationCarouselIndex, + InlineCitationCarouselItem, + InlineCitationCarouselNext, + InlineCitationCarouselPrev, + InlineCitationSource, + InlineCitationText, +} from "ghost-ui"; + +const sources = [ + "https://en.wikipedia.org/wiki/Large_language_model", + "https://arxiv.org/abs/2303.08774", +]; + +export function InlineCitationDemo() { + return ( +

+ Large language models are neural networks trained on vast amounts of text + data.{" "} + + + They use transformer architectures to generate coherent text + + + + + + + + + + + + + + + + + + + + + + {" "} + and have become a cornerstone of modern AI applications. +

+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/jsx-preview-demo.tsx b/apps/docs/src/components/docs/ai-elements/jsx-preview-demo.tsx new file mode 100644 index 0000000..ffa7d6f --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/jsx-preview-demo.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { JSXPreview, JSXPreviewContent, JSXPreviewError } from "ghost-ui"; + +const validJsx = `
+

Welcome Back

+

Your dashboard is ready to explore.

+
+ + +
+
`; + +const errorJsx = `
+ +
`; + +export function JsxPreviewDemo() { + return ( +
+ + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/message-demo.tsx b/apps/docs/src/components/docs/ai-elements/message-demo.tsx new file mode 100644 index 0000000..40d6673 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/message-demo.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { + Message, + MessageAction, + MessageActions, + MessageContent, + MessageResponse, +} from "ghost-ui"; +import { + CopyIcon, + RefreshCwIcon, + ThumbsDownIcon, + ThumbsUpIcon, +} from "lucide-react"; + +export function MessageDemo() { + return ( +
+ + + Can you explain how React Server Components work? + + + + + + + {`**React Server Components** (RSC) allow you to render components on the server, reducing the amount of JavaScript sent to the client.\n\n### Key Benefits\n\n- **Zero bundle size** — Server Components are not included in the client bundle\n- **Direct backend access** — You can query databases directly\n- **Automatic code splitting** — Client components are lazy-loaded\n\n\`\`\`tsx\n// This runs on the server\nasync function UserProfile({ id }: { id: string }) {\n const user = await db.user.findUnique({ where: { id } });\n return
{user.name}
;\n}\n\`\`\``} +
+
+ + + + + + + + + + + + + + +
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/mic-selector-demo.tsx b/apps/docs/src/components/docs/ai-elements/mic-selector-demo.tsx new file mode 100644 index 0000000..06786dd --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/mic-selector-demo.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { + MicSelector, + MicSelectorContent, + MicSelectorEmpty, + MicSelectorInput, + MicSelectorItem, + MicSelectorLabel, + MicSelectorList, + MicSelectorTrigger, + MicSelectorValue, +} from "ghost-ui"; + +export function MicSelectorDemo() { + return ( +
+

+ Opens a popover listing available audio input devices. Requires + microphone permission to show device names. +

+ + + + + + + + {(devices) => + devices.length === 0 ? ( + + ) : ( + devices.map((device) => ( + + + + )) + ) + } + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/model-selector-demo.tsx b/apps/docs/src/components/docs/ai-elements/model-selector-demo.tsx new file mode 100644 index 0000000..df2e74b --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/model-selector-demo.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { + Button, + ModelSelector, + ModelSelectorContent, + ModelSelectorEmpty, + ModelSelectorGroup, + ModelSelectorInput, + ModelSelectorItem, + ModelSelectorList, + ModelSelectorLogo, + ModelSelectorLogoGroup, + ModelSelectorName, + ModelSelectorTrigger, +} from "ghost-ui"; + +export function ModelSelectorDemo() { + return ( + + + + + + + + No models found. + + + + + + GPT-4o + + + + + + GPT-4o Mini + + + + + + + + Claude Sonnet 4 + + + + + + Claude Opus 4 + + + + + + + + Gemini 2.5 Pro + + + + + + ); +} diff --git a/apps/docs/src/components/docs/ai-elements/node-demo.tsx b/apps/docs/src/components/docs/ai-elements/node-demo.tsx new file mode 100644 index 0000000..922f932 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/node-demo.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { ReactFlowProvider } from "@xyflow/react"; +import { + Badge, + Button, + Node, + NodeContent, + NodeDescription, + NodeFooter, + NodeHeader, + NodeTitle, +} from "ghost-ui"; + +export function NodeDemo() { + return ( + +
+
+ + + Full Node + + A node with header, content, and footer + + + +

+ This node demonstrates all available sub-components arranged + together. It has both target (left) and source (right) handles. +

+
+ + Ready + + +
+ + + + Source Only + Starting node in a workflow + + +

+ This node only has a source handle on the right side. +

+
+
+ + + + Target Only + Terminal node in a workflow + + +

+ This node only has a target handle on the left side. +

+
+
+ + + + Standalone + No handles + + +

+ A standalone card-style node with no connection handles. +

+
+ + Idle + +
+
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/open-in-chat-demo.tsx b/apps/docs/src/components/docs/ai-elements/open-in-chat-demo.tsx new file mode 100644 index 0000000..f5e1c69 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/open-in-chat-demo.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { + OpenIn, + OpenInChatGPT, + OpenInClaude, + OpenInContent, + OpenInCursor, + OpenInLabel, + OpenInScira, + OpenInSeparator, + OpenInT3, + OpenInTrigger, + OpenInv0, +} from "ghost-ui"; + +export function OpenInChatDemo() { + return ( +
+

+ A dropdown menu that lets users open a query in various AI chat + providers. Each item generates a provider-specific URL and opens it in a + new tab. +

+ + + + + Open in a chat provider + + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/package-info-demo.tsx b/apps/docs/src/components/docs/ai-elements/package-info-demo.tsx new file mode 100644 index 0000000..d6d393d --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/package-info-demo.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { + PackageInfo, + PackageInfoContent, + PackageInfoDependencies, + PackageInfoDependency, + PackageInfoDescription, +} from "ghost-ui"; + +export function PackageInfoDemo() { + return ( +
+ + + A JavaScript library for building user interfaces + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/panel-demo.tsx b/apps/docs/src/components/docs/ai-elements/panel-demo.tsx new file mode 100644 index 0000000..ec11091 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/panel-demo.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; +import { + Canvas, + Node, + NodeContent, + NodeHeader, + NodeTitle, + Panel, +} from "ghost-ui"; +import { useMemo } from "react"; + +const PlaceholderNode = () => ( + + + Workflow Node + + +

+ Panels float above the canvas at fixed positions. +

+
+
+); + +const initialNodes = [ + { id: "1", type: "placeholder", position: { x: 100, y: 80 }, data: {} }, +]; + +export function PanelDemo() { + const nodeTypes: NodeTypes = useMemo( + () => ({ + placeholder: PlaceholderNode, + }), + [], + ); + + return ( +
+

+ Panels are floating overlays positioned at the edges of the canvas. +

+ +
+ + + Top Left Panel + + + Top Right Panel + + + Bottom Center Panel + + +
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/persona-demo.tsx b/apps/docs/src/components/docs/ai-elements/persona-demo.tsx new file mode 100644 index 0000000..f5e4857 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/persona-demo.tsx @@ -0,0 +1,55 @@ +"use client"; + +import type { PersonaState } from "ghost-ui"; +import { Button, Persona } from "ghost-ui"; +import { useState } from "react"; + +const variants = [ + "obsidian", + "glint", + "halo", + "command", + "mana", + "opal", +] as const; +const states: PersonaState[] = [ + "idle", + "listening", + "thinking", + "speaking", + "asleep", +]; + +export function PersonaDemo() { + const [currentState, setCurrentState] = useState("idle"); + + return ( +
+
+ {states.map((s) => ( + + ))} +
+ +
+ {variants.map((variant) => ( +
+ + {variant} +
+ ))} +
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/plan-demo.tsx b/apps/docs/src/components/docs/ai-elements/plan-demo.tsx new file mode 100644 index 0000000..ce00bc8 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/plan-demo.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { + Plan, + PlanAction, + PlanContent, + PlanDescription, + PlanFooter, + PlanHeader, + PlanTitle, + PlanTrigger, +} from "ghost-ui"; +import { CheckCircleIcon, CircleIcon } from "lucide-react"; + +export function PlanDemo() { + return ( +
+ + +
+ Build a Landing Page + + Create a responsive landing page with hero section, features, and + footer. + +
+ + + +
+ +
    +
  • + + + Set up project structure + +
  • +
  • + + + Design hero section + +
  • +
  • + + Build features grid +
  • +
  • + + Add footer and navigation +
  • +
+
+ +

+ 2 of 4 steps completed +

+
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/prompt-input-demo.tsx b/apps/docs/src/components/docs/ai-elements/prompt-input-demo.tsx new file mode 100644 index 0000000..47132fc --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/prompt-input-demo.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { + PromptInput, + PromptInputButton, + PromptInputFooter, + PromptInputSubmit, + PromptInputTextarea, + PromptInputTools, +} from "ghost-ui"; +import { PaperclipIcon } from "lucide-react"; + +export function PromptInputDemo() { + return ( +
+ {}}> + + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/queue-demo.tsx b/apps/docs/src/components/docs/ai-elements/queue-demo.tsx new file mode 100644 index 0000000..b6f9b37 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/queue-demo.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { + Queue, + QueueItem, + QueueItemContent, + QueueItemDescription, + QueueItemIndicator, + QueueList, + QueueSection, + QueueSectionContent, + QueueSectionLabel, + QueueSectionTrigger, +} from "ghost-ui"; +import { CheckCircleIcon, ListTodoIcon } from "lucide-react"; + +export function QueueDemo() { + return ( +
+ + + + } + /> + + + + +
+ + + Refactor authentication module + +
+ + Extract shared logic into a reusable hook + +
+ +
+ + + Write unit tests for API routes + +
+
+
+
+
+ + + + } + /> + + + + +
+ + + Set up project scaffolding + +
+
+ +
+ + + Configure database schema + +
+
+
+
+
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/reasoning-demo.tsx b/apps/docs/src/components/docs/ai-elements/reasoning-demo.tsx new file mode 100644 index 0000000..0ca165b --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/reasoning-demo.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Reasoning, ReasoningContent, ReasoningTrigger } from "ghost-ui"; + +export function ReasoningDemo() { + return ( +
+
+

Completed reasoning

+ + + + {`The user is asking about the performance implications of using React Server Components. Let me think through the key factors:\n\n1. **Bundle size reduction** - Since server components don't ship JavaScript to the client, the initial bundle can be significantly smaller.\n\n2. **Data fetching** - Server components can fetch data directly during rendering, eliminating client-side waterfalls.\n\n3. **Streaming** - The server can stream HTML progressively, improving Time to First Byte.`} + + +
+ +
+

Streaming reasoning

+ + + + {`Analyzing the query about database optimization strategies. I should consider indexing, query planning, and caching...`} + + +
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/sandbox-demo.tsx b/apps/docs/src/components/docs/ai-elements/sandbox-demo.tsx new file mode 100644 index 0000000..5560444 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/sandbox-demo.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { + CodeBlock, + CodeBlockActions, + CodeBlockCopyButton, + CodeBlockFilename, + CodeBlockHeader, + CodeBlockTitle, + Sandbox, + SandboxContent, + SandboxHeader, + SandboxTabContent, + SandboxTabs, + SandboxTabsBar, + SandboxTabsList, + SandboxTabsTrigger, +} from "ghost-ui"; + +const codeSnippet = `import { useState } from "react"; + +export default function Counter() { + const [count, setCount] = useState(0); + + return ( +
+

{count}

+ +
+ ); +}`; + +const outputText = `> next dev --turbo + Ready in 1.2s + Local: http://localhost:3000 + Network: http://192.168.1.100:3000 + + Compiled /page in 340ms`; + +export function SandboxDemo() { + return ( +
+ + + + + + + Code + Output + + + + + + + counter.tsx + + + + + + + + +
{outputText}
+
+
+
+
+ + + + +
+ Executing test suite... +
+
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/schema-display-demo.tsx b/apps/docs/src/components/docs/ai-elements/schema-display-demo.tsx new file mode 100644 index 0000000..12c4c54 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/schema-display-demo.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { SchemaDisplay } from "ghost-ui"; + +export function SchemaDisplayDemo() { + return ( +
+ + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/shimmer-demo.tsx b/apps/docs/src/components/docs/ai-elements/shimmer-demo.tsx new file mode 100644 index 0000000..2eb428f --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/shimmer-demo.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { Shimmer } from "ghost-ui"; + +export function ShimmerDemo() { + return ( +
+ + Generating response... + + + + Analyzing your code and preparing suggestions + + + + Thinking... + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/snippet-demo.tsx b/apps/docs/src/components/docs/ai-elements/snippet-demo.tsx new file mode 100644 index 0000000..56d45b7 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/snippet-demo.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { + Snippet, + SnippetAddon, + SnippetCopyButton, + SnippetInput, + SnippetText, +} from "ghost-ui"; + +export function SnippetDemo() { + return ( +
+ + + $ + + + + + + + + + + $ + + + + + + + + + + + + + + + + + $ + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/sources-demo.tsx b/apps/docs/src/components/docs/ai-elements/sources-demo.tsx new file mode 100644 index 0000000..598afb1 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/sources-demo.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { Source, Sources, SourcesContent, SourcesTrigger } from "ghost-ui"; + +export function SourcesDemo() { + return ( + + + + + + + + + ); +} diff --git a/apps/docs/src/components/docs/ai-elements/speech-input-demo.tsx b/apps/docs/src/components/docs/ai-elements/speech-input-demo.tsx new file mode 100644 index 0000000..9d75873 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/speech-input-demo.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { SpeechInput } from "ghost-ui"; +import { useState } from "react"; + +export function SpeechInputDemo() { + const [transcript, setTranscript] = useState(""); + + return ( +
+

+ Click the microphone button to begin recording. Uses the Web Speech API + when available, with a MediaRecorder fallback. +

+ + + setTranscript((prev) => (prev ? `${prev} ${text}` : text)) + } + /> + +
+

Transcription output

+

+ {transcript || "Transcribed text will appear here..."} +

+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/stack-trace-demo.tsx b/apps/docs/src/components/docs/ai-elements/stack-trace-demo.tsx new file mode 100644 index 0000000..b8863d1 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/stack-trace-demo.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { + StackTrace, + StackTraceActions, + StackTraceContent, + StackTraceCopyButton, + StackTraceError, + StackTraceErrorMessage, + StackTraceErrorType, + StackTraceExpandButton, + StackTraceFrames, + StackTraceHeader, +} from "ghost-ui"; + +const typeErrorTrace = `TypeError: Cannot read properties of undefined (reading 'map') + at UserList (/src/components/UserList.tsx:24:18) + at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14985:18) + at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17811:13) + at beginWork (node_modules/react-dom/cjs/react-dom.development.js:19049:16) + at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:3945:14)`; + +const referenceErrorTrace = `ReferenceError: fetchData is not defined + at loadDashboard (/src/pages/dashboard.ts:15:3) + at async handleRequest (/src/server/router.ts:42:12) + at async processMiddleware (/src/server/middleware.ts:28:5) + at node:internal/process/task_queues:95:5`; + +export function StackTraceDemo() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/suggestion-demo.tsx b/apps/docs/src/components/docs/ai-elements/suggestion-demo.tsx new file mode 100644 index 0000000..feea651 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/suggestion-demo.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { Suggestion, Suggestions } from "ghost-ui"; + +export function SuggestionDemo() { + return ( +
+

Suggested prompts

+ + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/task-demo.tsx b/apps/docs/src/components/docs/ai-elements/task-demo.tsx new file mode 100644 index 0000000..fd75243 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/task-demo.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { + Task, + TaskContent, + TaskItem, + TaskItemFile, + TaskTrigger, +} from "ghost-ui"; + +export function TaskDemo() { + return ( +
+ + + + + Found src/auth/login.ts — contains the + main login handler with JWT token generation. + + + Found src/middleware/auth.ts — + validates tokens on protected routes. + + + Found src/lib/session.ts — manages + session creation and expiration. + + + + + + + + + Reviewed prisma/schema.prisma for user + model relationships. + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/terminal-demo.tsx b/apps/docs/src/components/docs/ai-elements/terminal-demo.tsx new file mode 100644 index 0000000..6142dca --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/terminal-demo.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { + Terminal, + TerminalActions, + TerminalContent, + TerminalCopyButton, + TerminalHeader, + TerminalTitle, +} from "ghost-ui"; + +const buildOutput = `\x1b[32m$\x1b[0m next build +\x1b[36minfo\x1b[0m - Linting and checking validity of types... +\x1b[36minfo\x1b[0m - Creating an optimized production build... +\x1b[36minfo\x1b[0m - Compiled successfully +\x1b[36minfo\x1b[0m - Collecting page data... +\x1b[36minfo\x1b[0m - Generating static pages (8/8) +\x1b[36minfo\x1b[0m - Finalizing page optimization... + +Route (app) Size First Load JS +\x1b[37m+\x1b[0m / 5.2 kB 89.3 kB +\x1b[37m+\x1b[0m /about 1.8 kB 85.9 kB +\x1b[37m+\x1b[0m /dashboard 12.4 kB 96.5 kB +\x1b[37m+\x1b[0m /api/health 0 B 0 B + +\x1b[32m\u2713\x1b[0m Build completed in 14.2s`; + +const gitOutput = `\x1b[33mOn branch main\x1b[0m +Your branch is up to date with 'origin/main'. + +Changes to be committed: + \x1b[32mnew file: src/components/avatar.tsx\x1b[0m + \x1b[32mmodified: src/lib/utils.ts\x1b[0m + \x1b[31mdeleted: src/old-component.tsx\x1b[0m + +Untracked files: + \x1b[31m.env.local\x1b[0m`; + +export function TerminalDemo() { + return ( +
+ + + Build Output + + + + + + + + + + git status + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/test-results-demo.tsx b/apps/docs/src/components/docs/ai-elements/test-results-demo.tsx new file mode 100644 index 0000000..e381143 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/test-results-demo.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { + Test, + TestError, + TestErrorMessage, + TestErrorStack, + TestResults, + TestResultsContent, + TestResultsDuration, + TestResultsHeader, + TestResultsProgress, + TestResultsSummary, + TestSuite, + TestSuiteContent, + TestSuiteName, +} from "ghost-ui"; + +export function TestResultsDemo() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + Expected status 200 but received 500 + + + {`at Object. (tests/api/user.test.ts:45:10) + at processTicksAndRejections (node:internal/process/task_queues:95:5)`} + + + + + + + AssertionError: expected 'invalid' to match + /^[^@]+@[^@]+$/ + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/tool-demo.tsx b/apps/docs/src/components/docs/ai-elements/tool-demo.tsx new file mode 100644 index 0000000..db1d9e3 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/tool-demo.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { Tool, ToolContent, ToolHeader, ToolInput, ToolOutput } from "ghost-ui"; + +export function ToolDemo() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/toolbar-demo.tsx b/apps/docs/src/components/docs/ai-elements/toolbar-demo.tsx new file mode 100644 index 0000000..0d6a2cf --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/toolbar-demo.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { type NodeTypes, ReactFlowProvider } from "@xyflow/react"; +import { + Button, + Canvas, + Node, + NodeContent, + NodeHeader, + NodeTitle, + Toolbar, +} from "ghost-ui"; +import { CopyIcon, PencilIcon, Trash2Icon } from "lucide-react"; +import { useMemo } from "react"; + +const ToolbarNode = () => ( + + + Select this node + + +

+ Click to select and reveal the toolbar below. +

+
+ + + + + +
+); + +const initialNodes = [ + { + id: "1", + type: "toolbar", + position: { x: 100, y: 60 }, + data: {}, + selected: true, + }, +]; + +export function ToolbarDemo() { + const nodeTypes: NodeTypes = useMemo( + () => ({ + toolbar: ToolbarNode, + }), + [], + ); + + return ( +
+

+ A floating toolbar that appears below a selected node, providing + contextual actions. +

+ +
+ +
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/transcription-demo.tsx b/apps/docs/src/components/docs/ai-elements/transcription-demo.tsx new file mode 100644 index 0000000..43c17fa --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/transcription-demo.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { Button, Transcription, TranscriptionSegment } from "ghost-ui"; +import { useState } from "react"; + +const mockSegments = [ + { text: "Welcome to the demo.", startSecond: 0, endSecond: 2 }, + { text: " Today we are looking at", startSecond: 2, endSecond: 4 }, + { text: " the transcription component,", startSecond: 4, endSecond: 6 }, + { text: " which highlights words", startSecond: 6, endSecond: 8 }, + { text: " as audio plays.", startSecond: 8, endSecond: 10 }, + { text: " Each segment is clickable", startSecond: 10, endSecond: 12 }, + { text: " and can seek to", startSecond: 12, endSecond: 14 }, + { text: " the corresponding position", startSecond: 14, endSecond: 16 }, + { text: " in the audio track.", startSecond: 16, endSecond: 18 }, +]; + +export function TranscriptionDemo() { + const [currentTime, setCurrentTime] = useState(0); + + return ( +
+
+ + + {currentTime.toFixed(1)}s + + +
+ + + {(segment, index) => ( + + )} + +
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/voice-selector-demo.tsx b/apps/docs/src/components/docs/ai-elements/voice-selector-demo.tsx new file mode 100644 index 0000000..a5fe8b0 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/voice-selector-demo.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { + Button, + VoiceSelector, + VoiceSelectorAccent, + VoiceSelectorAge, + VoiceSelectorAttributes, + VoiceSelectorBullet, + VoiceSelectorContent, + VoiceSelectorDescription, + VoiceSelectorEmpty, + VoiceSelectorGender, + VoiceSelectorGroup, + VoiceSelectorInput, + VoiceSelectorItem, + VoiceSelectorList, + VoiceSelectorName, + VoiceSelectorPreview, + VoiceSelectorSeparator, + VoiceSelectorTrigger, +} from "ghost-ui"; +import { useState } from "react"; + +const voices = [ + { + id: "alloy", + name: "Alloy", + gender: "non-binary" as const, + accent: "american" as const, + age: "Young Adult", + description: "Versatile, balanced tone", + }, + { + id: "echo", + name: "Echo", + gender: "male" as const, + accent: "american" as const, + age: "Adult", + description: "Warm, resonant baritone", + }, + { + id: "fable", + name: "Fable", + gender: "female" as const, + accent: "british" as const, + age: "Adult", + description: "Expressive storyteller", + }, + { + id: "onyx", + name: "Onyx", + gender: "male" as const, + accent: "american" as const, + age: "Mature", + description: "Deep, authoritative", + }, + { + id: "nova", + name: "Nova", + gender: "female" as const, + accent: "australian" as const, + age: "Young Adult", + description: "Bright, energetic", + }, + { + id: "shimmer", + name: "Shimmer", + gender: "female" as const, + accent: "irish" as const, + age: "Adult", + description: "Soft, calming presence", + }, +]; + +export function VoiceSelectorDemo() { + const [selected, setSelected] = useState(undefined); + + return ( +
+

+ A dialog-based voice picker with search, gender, accent, and preview + controls. +

+ + + + + + + + + + No voices found. + + {voices.map((voice) => ( + setSelected(voice.id)} + > +
+
+
+ {voice.name} + + + + + + {voice.age} + +
+ + {voice.description} + +
+ { + /* no-op in demo */ + }} + /> +
+
+ ))} +
+ +
+
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/ai-elements/web-preview-demo.tsx b/apps/docs/src/components/docs/ai-elements/web-preview-demo.tsx new file mode 100644 index 0000000..76c8253 --- /dev/null +++ b/apps/docs/src/components/docs/ai-elements/web-preview-demo.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { + WebPreview, + WebPreviewBody, + WebPreviewConsole, + WebPreviewNavigation, + WebPreviewNavigationButton, + WebPreviewUrl, +} from "ghost-ui"; +import { ArrowLeftIcon, ArrowRightIcon, RefreshCwIcon } from "lucide-react"; + +export function WebPreviewDemo() { + return ( +
+ + + + + + + + + + + + + + + + +
+ ); +} diff --git a/apps/docs/src/components/docs/bento/activity-goal.tsx b/apps/docs/src/components/docs/bento/activity-goal.tsx new file mode 100644 index 0000000..f7f1c2d --- /dev/null +++ b/apps/docs/src/components/docs/bento/activity-goal.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "ghost-ui"; +import { Minus, Plus } from "lucide-react"; +import * as React from "react"; + +export function CardsActivityGoal() { + const [amount, setAmount] = React.useState(350); + + function onClick(adjustment: number) { + setAmount(Math.max(200, Math.min(400, amount + adjustment))); + } + + return ( + + + Payment Amount + Set your payment amount. + + +
+ +
+
${amount}
+
+ USD +
+
+ +
+
+ + + +
+ ); +} diff --git a/apps/docs/src/components/docs/bento/calendar.tsx b/apps/docs/src/components/docs/bento/calendar.tsx new file mode 100644 index 0000000..e1971ca --- /dev/null +++ b/apps/docs/src/components/docs/bento/calendar.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { addDays } from "date-fns"; +import { Calendar, Card, CardContent } from "ghost-ui"; + +const start = new Date(2023, 5, 5); + +export function CardsCalendar() { + return ( + + + + + + ); +} diff --git a/apps/docs/src/components/docs/bento/chat.tsx b/apps/docs/src/components/docs/bento/chat.tsx new file mode 100644 index 0000000..8566347 --- /dev/null +++ b/apps/docs/src/components/docs/bento/chat.tsx @@ -0,0 +1,249 @@ +"use client"; + +import { + Avatar, + AvatarFallback, + AvatarImage, + Button, + Card, + CardContent, + CardFooter, + CardHeader, + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + cn, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "ghost-ui"; +import { Check, Plus, Send } from "lucide-react"; +import * as React from "react"; + +const users = [ + { + name: "Olivia Martin", + email: "m@example.com", + }, + { + name: "Isabella Nguyen", + email: "isabella.nguyen@email.com", + }, + { + name: "Emma Wilson", + email: "emma@example.com", + }, + { + name: "Jackson Lee", + email: "lee@example.com", + }, + { + name: "William Kim", + email: "will@email.com", + }, +] as const; + +type User = (typeof users)[number]; + +export function CardsChat() { + const [open, setOpen] = React.useState(false); + const [selectedUsers, setSelectedUsers] = React.useState([]); + + const [messages, setMessages] = React.useState([ + { + role: "agent", + content: "Hi, how can I help you today?", + }, + { + role: "user", + content: "Hey, I'm having trouble with my account.", + }, + { + role: "agent", + content: "What seems to be the problem?", + }, + { + role: "user", + content: "I can't log in.", + }, + ]); + const [input, setInput] = React.useState(""); + const inputLength = input.trim().length; + + return ( + <> + + +
+ + OM + +
+

Sofia Davis

+

m@example.com

+
+
+ + + + + + New message + + +
+ +
+ {messages.map((message, index) => ( +
+ {message.content} +
+ ))} +
+
+ +
{ + event.preventDefault(); + if (inputLength === 0) return; + setMessages([ + ...messages, + { + role: "user", + content: input, + }, + ]); + setInput(""); + }} + className="flex w-full items-center space-x-2" + > + setInput(event.target.value)} + /> + +
+
+
+ + + + New message + + Invite a user to this thread. This will create a new group + message. + + + + + + No users found. + + {users.map((user) => ( + { + if (selectedUsers.includes(user)) { + return setSelectedUsers( + selectedUsers.filter( + (selectedUser) => selectedUser !== user, + ), + ); + } + + return setSelectedUsers( + [...users].filter((u) => + [...selectedUsers, user].includes(u), + ), + ); + }} + > + + {user.name[0]} + +
+

+ {user.name} +

+

+ {user.email} +

+
+ {selectedUsers.includes(user) ? ( + + ) : null} +
+ ))} +
+
+
+ + {selectedUsers.length > 0 ? ( +
+ {selectedUsers.map((user) => ( + + {user.name[0]} + + ))} +
+ ) : ( +

+ Select users to add to this thread. +

+ )} + +
+
+
+ + ); +} diff --git a/apps/docs/src/components/docs/bento/cookie-settings.tsx b/apps/docs/src/components/docs/bento/cookie-settings.tsx new file mode 100644 index 0000000..515542a --- /dev/null +++ b/apps/docs/src/components/docs/bento/cookie-settings.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + Label, + Switch, +} from "ghost-ui"; + +export function CardsCookieSettings() { + return ( + + + Cookie Settings + Manage your cookie settings here. + + +
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ ); +} diff --git a/apps/docs/src/components/docs/bento/create-account.tsx b/apps/docs/src/components/docs/bento/create-account.tsx new file mode 100644 index 0000000..236868c --- /dev/null +++ b/apps/docs/src/components/docs/bento/create-account.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + Input, + Label, +} from "ghost-ui"; +import { Icons } from "@/components/docs/icons"; + +export function CardsCreateAccount() { + return ( + + + Create an account + + Enter your email below to create your account + + + +
+ + +
+
+
+ +
+
+ + Or continue with + +
+
+
+ + +
+
+ + +
+
+ + + +
+ ); +} diff --git a/apps/docs/src/components/docs/bento/data-table.tsx b/apps/docs/src/components/docs/bento/data-table.tsx new file mode 100644 index 0000000..239bd1c --- /dev/null +++ b/apps/docs/src/components/docs/bento/data-table.tsx @@ -0,0 +1,322 @@ +"use client"; + +import { + ColumnDef, + ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + SortingState, + useReactTable, + VisibilityState, +} from "@tanstack/react-table"; +import { + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Checkbox, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + Input, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "ghost-ui"; +import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +const data: Payment[] = [ + { + id: "m5gr84i9", + amount: 316, + status: "success", + email: "ken99@example.com", + }, + { + id: "3u1reuv4", + amount: 242, + status: "success", + email: "Abe45@example.com", + }, + { + id: "derv1ws0", + amount: 837, + status: "processing", + email: "Monserrat44@example.com", + }, + { + id: "bhqecj4p", + amount: 721, + status: "failed", + email: "carmella@example.com", + }, +]; + +export type Payment = { + id: string; + amount: number; + status: "pending" | "processing" | "success" | "failed"; + email: string; +}; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "status", + header: "Status", + cell: ({ row }) => ( +
{row.getValue("status")}
+ ), + }, + { + accessorKey: "email", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) =>
{row.getValue("email")}
, + }, + { + accessorKey: "amount", + header: () =>
Amount
, + cell: ({ row }) => { + const amount = parseFloat(row.getValue("amount")); + + // Format the amount as a dollar amount + const formatted = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); + + return ( +
{formatted}
+ ); + }, + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const payment = row.original; + + return ( + + + + + + Actions + navigator.clipboard.writeText(payment.id)} + > + Copy payment ID + + + View customer + View payment details + + + ); + }, + }, +]; + +export function CardsDataTable() { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( + + + Payments + Manage your payments. + + +
+ + table.getColumn("email")?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+ + +
+
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/bento/index.tsx b/apps/docs/src/components/docs/bento/index.tsx new file mode 100644 index 0000000..bf01514 --- /dev/null +++ b/apps/docs/src/components/docs/bento/index.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useIsMobile } from "ghost-ui"; +import { lazy, Suspense } from "react"; +import { CardsChat } from "@/components/docs/bento/chat"; +import { CardsCookieSettings } from "@/components/docs/bento/cookie-settings"; +import { CardsCreateAccount } from "@/components/docs/bento/create-account"; +import { CardsPaymentMethod } from "@/components/docs/bento/payment-method"; +import { CardsReportIssue } from "@/components/docs/bento/report-issue"; +import { CardsShare } from "@/components/docs/bento/share"; +import { CardsTeamMembers } from "@/components/docs/bento/team-members"; + +const CardsStats = lazy(() => + import("@/components/docs/bento/stats").then((m) => ({ + default: m.CardsStats, + })), +); +const CardsCalendar = lazy(() => + import("@/components/docs/bento/calendar").then((m) => ({ + default: m.CardsCalendar, + })), +); +const CardsActivityGoal = lazy(() => + import("@/components/docs/bento/activity-goal").then((m) => ({ + default: m.CardsActivityGoal, + })), +); +const CardsMetric = lazy(() => + import("@/components/docs/bento/metric").then((m) => ({ + default: m.CardsMetric, + })), +); +const CardsDataTable = lazy(() => + import("@/components/docs/bento/data-table").then((m) => ({ + default: m.CardsDataTable, + })), +); + +function CalendarMetricGroup() { + return ( +
+ + + +
+ + + +
+
+ + + +
+
+ ); +} + +export function BentoDemo() { + const isMobile = useIsMobile(); + + return ( +
+
+ + + + {isMobile && } +
+
+ + + +
+
+ + +
+ +
+
+
+
+
+ {!isMobile && ( + <> + + + + + + )} + +
+ +
+
+
+ ); +} diff --git a/apps/docs/src/components/docs/bento/metric.tsx b/apps/docs/src/components/docs/bento/metric.tsx new file mode 100644 index 0000000..17d3c52 --- /dev/null +++ b/apps/docs/src/components/docs/bento/metric.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "ghost-ui"; +import { Line, LineChart } from "recharts"; + +const data = [ + { + average: 400, + today: 240, + }, + { + average: 300, + today: 139, + }, + { + average: 200, + today: 980, + }, + { + average: 278, + today: 390, + }, + { + average: 189, + today: 480, + }, + { + average: 239, + today: 380, + }, + { + average: 349, + today: 430, + }, +]; + +const chartConfig = { + today: { + label: "Current Value", + color: "hsl(var(--primary))", + }, + average: { + label: "Average Value", + color: "hsl(var(--primary))", + }, +} satisfies ChartConfig; + +export function CardsMetric() { + return ( + + + Portfolio Value + + Your portfolio is performing above its 7-day average. + + + + + + + + } /> + + + + + ); +} diff --git a/apps/docs/src/components/docs/bento/payment-amount.tsx b/apps/docs/src/components/docs/bento/payment-amount.tsx new file mode 100644 index 0000000..d2b8477 --- /dev/null +++ b/apps/docs/src/components/docs/bento/payment-amount.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "ghost-ui"; +import { Minus, Plus } from "lucide-react"; +import * as React from "react"; + +export function PaymentAmount() { + const [amount, setAmount] = React.useState(350); + + function onClick(adjustment: number) { + setAmount(Math.max(200, Math.min(400, amount + adjustment))); + } + + return ( + + + Payment Amount + Set your payment amount. + + +
+ +
+
${amount}
+
+ USD +
+
+ +
+
+ + + +
+ ); +} diff --git a/apps/docs/src/components/docs/bento/payment-method.tsx b/apps/docs/src/components/docs/bento/payment-method.tsx new file mode 100644 index 0000000..7df3c97 --- /dev/null +++ b/apps/docs/src/components/docs/bento/payment-method.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + Input, + Label, + RadioGroup, + RadioGroupItem, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "ghost-ui"; +import { Icons } from "@/components/docs/icons"; + +export function CardsPaymentMethod() { + return ( + + + Payment Method + + Add a new payment method to your account. + + + + +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+ ); +} diff --git a/apps/docs/src/components/docs/bento/report-issue.tsx b/apps/docs/src/components/docs/bento/report-issue.tsx new file mode 100644 index 0000000..4cdf6b2 --- /dev/null +++ b/apps/docs/src/components/docs/bento/report-issue.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Textarea, +} from "ghost-ui"; +import * as React from "react"; + +export function CardsReportIssue() { + const id = React.useId(); + + return ( + + + Report an issue + + What area are you having problems with? + + + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ +