diff --git a/README.md b/README.md index bdcd5cd..3362dab 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ # Ghost -**Design drift detection and fingerprinting for design systems.** +**Autonomous perception of organic drift across decentralized design consumers.** -Ghost detects unintentional divergence between a parent design language and its consumer implementations. It scans for drift across values, structure, and visual dimensions, generates design fingerprints for comparison, and tracks how systems evolve over time. +Ghost makes design systems legible. It continuously detects divergence between a parent design language and its consumers, generates quantitative fingerprints for comparison, tracks how systems evolve over time, and ships a reference design language as a shadcn-compatible component registry. ## Why Ghost? -Design languages drift. Teams override tokens, hardcode colors, restructure components, and make visual changes that silently diverge from the source of truth. Drift can be neutral. Sometimes organic. Sometimes a mistake. Sometimes intentional. Ghost catches this drift. +Design languages drift — and drift degrades trust. When interfaces lose coherence, the experience suffers regardless of how good the underlying capabilities are. Ghost perceives this drift across an ecosystem so teams can reason about it and act with intent. -- **Multi-dimensional scanning** - Detect token overrides, hardcoded values, structural divergence, and pixel-level visual regressions -- **Design fingerprinting** - Generate a 64-dimensional numeric profile of any design system for quantitative comparison -- **Evolution tracking** - Acknowledge, adopt, or intentionally diverge from a parent system with full lineage history -- **Fleet analysis** - Compare fingerprints across an ecosystem to identify clusters and outliers -- **LLM-powered interpretation** - Optionally use Claude or OpenAI for richer fingerprint generation -- **3D visualization** - Explore fingerprint similarity space in an interactive Three.js viewer +- **Continuous scanning** — Detect token overrides, hardcoded values, structural divergence, and pixel-level visual regressions across every consumer +- **Design fingerprinting** — Generate a 64-dimensional profile of any design system — a continuous signal, not a binary check +- **Intent tracking** — Acknowledge, adopt, or intentionally diverge from a parent system. Every stance is published with reasoning and full lineage +- **Fleet observability** — Compare fingerprints across an ecosystem to see the full picture: clusters, outliers, and how consumers relate to each other and the source +- **LLM-aided interpretation** — Optionally use Claude or OpenAI for richer fingerprint generation and drift analysis +- **3D visualization** — Explore fingerprint similarity space in an interactive Three.js viewer +- **Composable design language** — A full shadcn-compatible registry of atomic components, design tokens, and a live catalogue — building blocks that interfaces compose from ## Getting Started @@ -57,18 +58,25 @@ ghost compare system-a.json system-b.json ghost viz system-a.json system-b.json system-c.json ``` +**Run the ghost-ui catalogue:** + +```bash +just dev +# or: cd packages/ghost-ui && pnpm dev +``` + ## CLI Commands | Command | Description | | --------------- | ---------------------------------------------------------------------------- | -| `ghost scan` | Detect design drift against a registry | -| `ghost profile` | Generate a design fingerprint from a registry, codebase, or via LLM | -| `ghost compare` | Compare two fingerprints with optional temporal analysis | -| `ghost ack` | Acknowledge current drift and record a stance (aligned, accepted, diverging) | -| `ghost adopt` | Shift parent baseline to a new fingerprint | -| `ghost diverge` | Mark a fingerprint dimension as intentionally diverging | -| `ghost fleet` | Compare N fingerprints across an ecosystem | -| `ghost viz` | Launch interactive 3D fingerprint visualization | +| `ghost scan` | Detect design drift against a registry | +| `ghost profile` | Generate a design fingerprint from a registry, codebase, or via LLM | +| `ghost compare` | Compare two fingerprints with optional temporal analysis | +| `ghost ack` | Acknowledge current drift and publish a stance (aligned, accepted, diverging) | +| `ghost adopt` | Shift parent baseline to a new fingerprint | +| `ghost diverge` | Mark a fingerprint dimension as intentionally diverging with reasoning | +| `ghost fleet` | Compare N fingerprints for ecosystem-wide observability | +| `ghost viz` | Launch interactive 3D fingerprint visualization | ## Configuration @@ -104,17 +112,17 @@ export default defineConfig({ ## How It Works -### Drift Scanning +### Scanning -Ghost scans at three levels: +Ghost perceives drift at three levels: -1. **Values** - Detects hardcoded colors, token overrides, and missing tokens by comparing your styles against the registry -2. **Structure** - Diffs component files between your implementation and the registry source -3. **Visual** - Renders components with Playwright and performs pixel-level comparison using pixelmatch +1. **Values** — Detects hardcoded colors, token overrides, and missing tokens by comparing styles against the registry +2. **Structure** — Diffs component files between a consumer implementation and the registry source +3. **Visual** — Renders components with Playwright and performs pixel-level comparison using pixelmatch -### Design Fingerprinting +### Fingerprinting -A fingerprint is a 64-dimensional vector capturing a system's design characteristics: +A fingerprint is a 64-dimensional vector — a continuous representation of a system's design characteristics: | Dimensions | Category | What it captures | | ---------- | ------------ | -------------------------------------------------------------- | @@ -126,17 +134,58 @@ A fingerprint is a 64-dimensional vector capturing a system's design characteris Fingerprints can be generated deterministically from extracted material, from a shadcn-compatible registry, or with LLM assistance for richer interpretation. -### Evolution Tracking +### Intent Tracking + +Ghost tracks design lineage and published intent through: + +- **`.ghost-sync.json`** — Per-dimension stances toward the parent: aligned, accepted, or diverging — each with recorded reasoning +- **`.ghost/history.jsonl`** — Append-only fingerprint history for temporal analysis +- **Temporal comparison** — Velocity and trajectory classification to understand where a system is heading, not just where it is + +### Fleet Observability -Ghost tracks design lineage through: +Compare fingerprints across multiple systems to make an ecosystem legible. Ghost calculates pairwise distances, identifies a centroid, and clusters systems by similarity — surfacing which consumers are coherent, which are drifting, and where gaps exist. -- **`.ghost-sync.json`** - A manifest recording per-dimension stances toward the parent (aligned, accepted, diverging) -- **`.ghost/history.jsonl`** - Append-only fingerprint history for temporal analysis -- **Temporal comparison** - Velocity and trajectory classification to understand drift trends +## Ghost UI -### Fleet Analysis +Ghost UI (`@ghost/ui`) is the project's reference design language — atomic, composable interface primitives published as a shadcn-compatible registry. It serves as both a living design system and the concrete baseline Ghost scans consumers against. + +### What's included + +- **49 primitive components** — Foundational building blocks (accordion, button, card, dialog, form, table, tabs, etc.) built on Radix UI and styled with Tailwind CSS +- **48 AI-native elements** — Components for conversational and agentic interfaces: prompt input, message, code block, chain of thought, file tree, terminal, tool, and more — the pieces intelligent interfaces compose from +- **Design tokens** — A full token system (colors, spacing, typography, radii, shadows) defined as CSS custom properties with light and dark mode support +- **Theme system** — Runtime theme switching with presets, a live theme panel for editing tokens, and CSS variable export +- **HK Grotesk typeface** — Self-hosted display font (300–900 weights) paired with system sans-serif for body text +- **Live catalogue** — An interactive documentation site (React + Vite) with component demos, foundations pages, and a bento showcase + +### Registry + +Ghost UI publishes a `registry.json` conforming to the [shadcn registry schema](https://ui.shadcn.com/docs/registry). Consumers can install individual components directly: + +```bash +npx shadcn@latest add --registry https://your-ghost-ui-host/registry.json button card dialog +``` -Compare fingerprints across multiple systems to get an ecosystem-wide view. Ghost calculates pairwise distances, identifies a centroid, and optionally clusters systems by similarity. +Ghost itself can profile the registry to generate a fingerprint, then scan downstream consumers against it to detect drift: + +```bash +ghost profile --registry ./packages/ghost-ui/registry.json +ghost scan --config ghost.config.ts +``` + +### Catalogue development + +```bash +# dev server with hot reload +just dev + +# production build +just build-ui + +# rebuild the shadcn registry +just build-registry +``` ## Project Structure @@ -154,6 +203,20 @@ packages/ ghost-cli/ CLI interface src/ viz/ 3D visualization (Three.js, PCA projection) + ghost-ui/ Reference design language (@ghost/ui) + src/ + components/ + ui/ Primitive components (Radix + Tailwind) + ai-elements/ AI-native components (chat, code, agents) + theme/ ThemeProvider and theme toggle + theme-panel/ Live token editor panel + docs/ Catalogue pages, demos, and bento showcase + contexts/ Theme and theme-panel context providers + hooks/ Shared React hooks + lib/ Utilities, registry helpers, theme presets + styles/ Design tokens and global CSS + fonts/ HK Grotesk woff2 files + registry.json shadcn-compatible component registry ``` ## Development @@ -170,8 +233,13 @@ pnpm test # lint and format pnpm check + +# run ghost-ui dev server +just dev ``` +A `justfile` is included for common workflows — run `just` to see all available recipes. + ## Project Resources | Resource | Description | diff --git a/biome.json b/biome.json index 70f4aec..165530e 100644 --- a/biome.json +++ b/biome.json @@ -22,9 +22,23 @@ } } }, + "css": { + "parser": { + "cssModules": false, + "tailwindDirectives": true + } + }, "javascript": { "formatter": { "quoteStyle": "double" } - } + }, + "overrides": [ + { + "includes": ["packages/ghost-ui/**"], + "linter": { + "enabled": false + } + } + ] } diff --git a/justfile b/justfile index d223cd3..2ce8d9b 100644 --- a/justfile +++ b/justfile @@ -39,6 +39,20 @@ test: test-watch: pnpm test:watch +# ── Run ────────────────────────────────────────────────────── + +# Run ghost-ui catalogue dev server +dev: + cd packages/ghost-ui && pnpm dev + +# Build ghost-ui catalogue (static export) +build-ui: + cd packages/ghost-ui && pnpm build + +# Build ghost-ui shadcn registry +build-registry: + cd packages/ghost-ui && pnpm build:registry + # ── Utilities ──────────────────────────────────────────────── # Clean build artifacts diff --git a/package.json b/package.json index 948fa12..699d1c6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --build", + "build:ui": "pnpm --filter @ghost/ui build", "check": "biome check . && pnpm typecheck && pnpm check:file-sizes", "check:file-sizes": "node scripts/check-file-sizes.mjs", "fmt": "biome format --write .", diff --git a/packages/ghost-cli/src/bin.ts b/packages/ghost-cli/src/bin.ts index 9c58d45..9104769 100644 --- a/packages/ghost-cli/src/bin.ts +++ b/packages/ghost-cli/src/bin.ts @@ -5,9 +5,12 @@ import type { DesignFingerprint } from "@ghost/core"; import { compareFingerprints, computeTemporalComparison, + diff, formatCLIReport, formatComparison, formatComparisonJSON, + formatDiffCLI, + formatDiffJSON, formatFingerprint, formatFingerprintJSON, formatJSONReport, @@ -239,6 +242,54 @@ const compareCommand = defineCommand({ }, }); +const diffCommand = defineCommand({ + meta: { + name: "diff", + description: + "Compare local components against registry with drift analysis", + }, + args: { + component: { + type: "positional", + description: "Component name (optional, all if omitted)", + required: false, + }, + config: { + type: "string", + description: "Path to ghost config file", + alias: "c", + }, + format: { + type: "string", + description: "Output format: cli or json", + default: "cli", + }, + }, + async run({ args }) { + try { + const config = await loadConfig(args.config); + const results = await diff(config, args.component || undefined); + + const output = + args.format === "json" + ? formatDiffJSON(results) + : formatDiffCLI(results); + + process.stdout.write(output); + + const hasBreaking = results.some((r) => + r.components.some((c) => c.severity === "error"), + ); + process.exit(hasBreaking ? 1 : 0); + } catch (err) { + console.error( + `Error: ${err instanceof Error ? err.message : String(err)}`, + ); + process.exit(2); + } + }, +}); + const main = defineCommand({ meta: { name: "ghost", @@ -249,6 +300,7 @@ const main = defineCommand({ scan: scanCommand, profile: profileCommand, compare: compareCommand, + diff: diffCommand, fleet: fleetCommand, ack: ackCommand, adopt: adoptCommand, diff --git a/packages/ghost-core/src/diff.ts b/packages/ghost-core/src/diff.ts new file mode 100644 index 0000000..260b4ca --- /dev/null +++ b/packages/ghost-core/src/diff.ts @@ -0,0 +1,168 @@ +import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { parseCSS } from "./resolvers/css.js"; +import { resolveRegistry } from "./resolvers/registry.js"; +import { scanStructure } from "./scanners/structure.js"; +import { scanValues } from "./scanners/values.js"; +import type { GhostConfig, StructureDrift, ValueDrift } from "./types.js"; + +export type DiffSeverity = "info" | "warn" | "error"; + +export interface ComponentDiff { + component: string; + severity: DiffSeverity; + structureDrift?: StructureDrift; + valueDrifts: ValueDrift[]; + classification: "cosmetic" | "additive" | "breaking" | "missing"; +} + +export interface DiffResult { + designSystem: string; + components: ComponentDiff[]; + summary: { + total: number; + missing: number; + diverged: number; + clean: number; + tokenDrifts: number; + }; +} + +function classifyStructureDrift( + drift: StructureDrift, +): "cosmetic" | "additive" | "breaking" { + if (!drift.diff) return "cosmetic"; + + const lines = drift.diff.split("\n"); + let hasRemovedExport = false; + let hasRemovedProp = false; + let onlyWhitespace = true; + + for (const line of lines) { + if (line.startsWith("-") && !line.startsWith("---")) { + const content = line.slice(1).trim(); + if (content.length > 0) onlyWhitespace = false; + if (content.startsWith("export ")) hasRemovedExport = true; + if (content.includes("Props") || content.includes("interface ")) + hasRemovedProp = true; + } + if (line.startsWith("+") && !line.startsWith("+++")) { + const content = line.slice(1).trim(); + if (content.length > 0) onlyWhitespace = false; + } + } + + if (onlyWhitespace) return "cosmetic"; + if (hasRemovedExport || hasRemovedProp) return "breaking"; + return "additive"; +} + +function classificationToSeverity( + classification: ComponentDiff["classification"], +): DiffSeverity { + switch (classification) { + case "cosmetic": + return "info"; + case "additive": + return "warn"; + case "breaking": + case "missing": + return "error"; + } +} + +export async function diff( + config: GhostConfig, + componentFilter?: string, +): Promise { + const results: DiffResult[] = []; + + for (const ds of config.designSystems ?? []) { + const registry = await resolveRegistry(ds.registry); + const consumerDir = process.cwd(); + + // Structure scan + const structureDrifts = await scanStructure({ + registryItems: registry.items, + consumerDir, + componentDir: ds.componentDir, + rules: config.rules, + ignore: config.ignore, + }); + + // Value scan + let valueDrifts: ValueDrift[] = []; + const styleEntryPath = resolve(consumerDir, ds.styleEntry); + if (existsSync(styleEntryPath)) { + const consumerCSS = await readFile(styleEntryPath, "utf-8"); + const consumerTokens = parseCSS(consumerCSS); + valueDrifts = scanValues({ + registryTokens: registry.tokens, + consumerTokens, + consumerCSS, + rules: config.rules, + styleFile: ds.styleEntry, + }); + } + + // Group by component + const componentMap = new Map(); + + // Process structure drifts + for (const sd of structureDrifts) { + if (componentFilter && sd.component !== componentFilter) continue; + + const classification = + sd.rule === "missing-component" + ? "missing" + : classifyStructureDrift(sd); + + componentMap.set(sd.component, { + component: sd.component, + severity: classificationToSeverity(classification), + structureDrift: sd, + valueDrifts: [], + classification, + }); + } + + // Attach value drifts (these are token-level, not per-component) + // Group them under a synthetic "_tokens" component + if (valueDrifts.length > 0) { + const tokenDiff: ComponentDiff = { + component: "_tokens", + severity: valueDrifts.some((v) => v.severity === "error") + ? "error" + : "warn", + valueDrifts, + classification: "additive", + }; + componentMap.set("_tokens", tokenDiff); + } + + // Count clean components + const uiItems = registry.items.filter((i) => i.type === "registry:ui"); + const divergedComponents = new Set(structureDrifts.map((d) => d.component)); + const clean = componentFilter + ? 0 + : uiItems.filter((i) => !divergedComponents.has(i.name)).length; + + results.push({ + designSystem: ds.name, + components: Array.from(componentMap.values()), + summary: { + total: componentFilter ? 1 : uiItems.length, + missing: structureDrifts.filter((d) => d.rule === "missing-component") + .length, + diverged: structureDrifts.filter( + (d) => d.rule === "structural-divergence", + ).length, + clean, + tokenDrifts: valueDrifts.length, + }, + }); + } + + return results; +} diff --git a/packages/ghost-core/src/index.ts b/packages/ghost-core/src/index.ts index bed92cc..9124c77 100644 --- a/packages/ghost-core/src/index.ts +++ b/packages/ghost-core/src/index.ts @@ -1,4 +1,6 @@ export { defineConfig, loadConfig } from "./config.js"; +export type { ComponentDiff, DiffResult, DiffSeverity } from "./diff.js"; +export { diff } from "./diff.js"; export { acknowledge, appendHistory, @@ -29,6 +31,7 @@ export { createProvider } from "./llm/index.js"; export type { ProfileOptions } from "./profile.js"; export { profile, profileRegistry } from "./profile.js"; export { formatReport as formatCLIReport } from "./reporters/cli.js"; +export { formatDiffCLI, formatDiffJSON } from "./reporters/diff.js"; export { formatComparison, formatComparisonJSON, @@ -50,7 +53,9 @@ export { scan } from "./scan.js"; export { scanVisual } from "./scanners/visual.js"; export type { ColorRamp, + ComponentMeta, CSSToken, + CSSVarsMap, DesignFingerprint, DesignSystemConfig, DesignSystemReport, @@ -72,6 +77,7 @@ export type { FleetComparison, FleetMember, FleetPair, + FontDescriptor, GhostConfig, LLMConfig, LLMProvider, @@ -79,6 +85,7 @@ export type { Registry, RegistryFile, RegistryItem, + RegistryItemType, ResolvedRegistry, RuleSeverity, ScanOptions, diff --git a/packages/ghost-core/src/reporters/diff.ts b/packages/ghost-core/src/reporters/diff.ts new file mode 100644 index 0000000..40848a1 --- /dev/null +++ b/packages/ghost-core/src/reporters/diff.ts @@ -0,0 +1,134 @@ +import type { ComponentDiff, DiffResult, DiffSeverity } from "../diff.js"; + +const useColor = + !process.env.NO_COLOR && + !process.argv.includes("--no-color") && + process.stdout.isTTY; + +const c = { + red: (s: string) => (useColor ? `\x1b[31m${s}\x1b[0m` : s), + yellow: (s: string) => (useColor ? `\x1b[33m${s}\x1b[0m` : s), + green: (s: string) => (useColor ? `\x1b[32m${s}\x1b[0m` : s), + cyan: (s: string) => (useColor ? `\x1b[36m${s}\x1b[0m` : s), + dim: (s: string) => (useColor ? `\x1b[2m${s}\x1b[0m` : s), + bold: (s: string) => (useColor ? `\x1b[1m${s}\x1b[0m` : s), + magenta: (s: string) => (useColor ? `\x1b[35m${s}\x1b[0m` : s), +}; + +function _severityBadge(severity: DiffSeverity): string { + switch (severity) { + case "error": + return c.red("BREAKING"); + case "warn": + return c.yellow("CHANGED"); + case "info": + return c.cyan("COSMETIC"); + } +} + +function classificationLabel( + classification: ComponentDiff["classification"], +): string { + switch (classification) { + case "missing": + return c.red("MISSING"); + case "breaking": + return c.red("BREAKING"); + case "additive": + return c.yellow("ADDITIVE"); + case "cosmetic": + return c.cyan("COSMETIC"); + } +} + +function formatComponentDiff(cd: ComponentDiff): string { + const lines: string[] = []; + + if (cd.component === "_tokens") { + lines.push(`\n${c.bold("Token Drift")}`); + for (const vd of cd.valueDrifts) { + const tag = vd.severity === "error" ? c.red("ERROR") : c.yellow(" WARN"); + lines.push(` ${tag} ${vd.message}`); + if (vd.registryValue && vd.consumerValue) { + lines.push(` ${c.dim("registry:")} ${vd.registryValue}`); + lines.push(` ${c.dim("local:")} ${vd.consumerValue}`); + } + if (vd.suggestion) { + lines.push(` ${c.green("suggestion:")} ${vd.suggestion}`); + } + } + return lines.join("\n"); + } + + const badge = classificationLabel(cd.classification); + lines.push(`\n${c.bold(cd.component)} ${badge}`); + + if (cd.structureDrift) { + const sd = cd.structureDrift; + if (sd.rule === "missing-component") { + lines.push(` Not found at ${c.dim(sd.consumerFile ?? "")}`); + } else { + lines.push( + ` ${c.green(`+${sd.linesAdded}`)} ${c.red(`-${sd.linesRemoved}`)} lines`, + ); + if (sd.diff) { + // Show a condensed diff (first 20 meaningful lines) + const diffLines = sd.diff.split("\n"); + let shown = 0; + for (const line of diffLines) { + if (shown >= 20) { + lines.push(c.dim(` ... (${diffLines.length - shown} more lines)`)); + break; + } + if (line.startsWith("+") && !line.startsWith("+++")) { + lines.push(` ${c.green(line)}`); + shown++; + } else if (line.startsWith("-") && !line.startsWith("---")) { + lines.push(` ${c.red(line)}`); + shown++; + } else if (line.startsWith("@@")) { + lines.push(` ${c.magenta(line)}`); + shown++; + } + } + } + } + } + + return lines.join("\n"); +} + +export function formatDiffCLI(results: DiffResult[]): string { + const lines: string[] = []; + + for (const result of results) { + lines.push(c.bold(`\n${result.designSystem}`)); + lines.push( + c.dim( + `${result.summary.total} components | ${result.summary.clean} clean | ${result.summary.diverged} diverged | ${result.summary.missing} missing | ${result.summary.tokenDrifts} token drifts`, + ), + ); + + if (result.components.length === 0) { + lines.push(c.green("\n All components match registry.")); + continue; + } + + // Sort: breaking first, then additive, then cosmetic + const order = { missing: 0, breaking: 1, additive: 2, cosmetic: 3 }; + const sorted = [...result.components].sort( + (a, b) => order[a.classification] - order[b.classification], + ); + + for (const cd of sorted) { + lines.push(formatComponentDiff(cd)); + } + } + + lines.push(""); + return lines.join("\n"); +} + +export function formatDiffJSON(results: DiffResult[]): string { + return JSON.stringify(results, null, 2); +} diff --git a/packages/ghost-core/src/resolvers/css.ts b/packages/ghost-core/src/resolvers/css.ts index 8f93525..9d34607 100644 --- a/packages/ghost-core/src/resolvers/css.ts +++ b/packages/ghost-core/src/resolvers/css.ts @@ -22,6 +22,7 @@ const CATEGORY_PREFIXES: [string, TokenCategory][] = [ ["--duration-", "animation"], ["--ease-", "animation"], ["--color-", "color"], + ["--font-face-", "font-face"], ["--font-", "font"], ["--chart-", "chart"], ["--sidebar-", "sidebar"], diff --git a/packages/ghost-core/src/resolvers/registry.ts b/packages/ghost-core/src/resolvers/registry.ts index f58d062..92865c3 100644 --- a/packages/ghost-core/src/resolvers/registry.ts +++ b/packages/ghost-core/src/resolvers/registry.ts @@ -6,6 +6,7 @@ import type { Registry, RegistryItem, ResolvedRegistry, + TokenCategory, } from "../types.js"; import { parseCSS } from "./css.js"; @@ -25,10 +26,20 @@ async function resolveItemContent( item: RegistryItem, registryDir: string, ): Promise { + // registry:font items carry structured metadata, not file content + if (item.type === "registry:font" && item.files.length === 0) { + return item; + } + const resolvedFiles = await Promise.all( item.files.map(async (file) => { if (file.content) return file; + // Skip binary files (e.g., woff2 font files) + if (file.path.match(/\.(woff2?|ttf|otf|eot)$/)) { + return file; + } + // Try built output first: out/r/[name].json const builtPath = join(registryDir, "out", "r", `${item.name}.json`); if (existsSync(builtPath)) { @@ -81,7 +92,44 @@ async function resolveItemContentFromURL( } } +function extractCSSVarsTokens( + vars: Record, + selector: string, +): CSSToken[] { + // Build a synthetic CSS string and parse it to reuse categorization logic + const lines = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`); + const css = `${selector} {\n${lines.join("\n")}\n}`; + return parseCSS(css); +} + +function extractFontTokens(items: RegistryItem[]): CSSToken[] { + const tokens: CSSToken[] = []; + for (const item of items) { + if (item.type !== "registry:font" || !item.font) continue; + tokens.push({ + name: item.font.variable, + value: item.font.family, + resolvedValue: item.font.family, + selector: ":root", + category: "font" as TokenCategory, + }); + if (item.font.weight) { + tokens.push({ + name: `${item.font.variable}-weights`, + value: item.font.weight.join(", "), + resolvedValue: item.font.weight.join(", "), + selector: ":root", + category: "font-face" as TokenCategory, + }); + } + } + return tokens; +} + function extractStyleTokens(items: RegistryItem[]): CSSToken[] { + const tokens: CSSToken[] = []; + + // Extract from registry:style items (CSS file content) for (const item of items) { if (item.type !== "registry:style") continue; for (const file of item.files) { @@ -89,11 +137,29 @@ function extractStyleTokens(items: RegistryItem[]): CSSToken[] { file.content && (file.path.endsWith(".css") || file.type === "registry:theme") ) { - return parseCSS(file.content); + tokens.push(...parseCSS(file.content)); } } } - return []; + + // Extract from registry:base items (cssVars maps) + for (const item of items) { + if (item.type !== "registry:base" || !item.cssVars) continue; + if (item.cssVars.theme) { + tokens.push(...extractCSSVarsTokens(item.cssVars.theme, "@theme")); + } + if (item.cssVars.light) { + tokens.push(...extractCSSVarsTokens(item.cssVars.light, ":root")); + } + if (item.cssVars.dark) { + tokens.push(...extractCSSVarsTokens(item.cssVars.dark, ".dark")); + } + } + + // Extract from registry:font items + tokens.push(...extractFontTokens(items)); + + return tokens; } export async function resolveRegistry( diff --git a/packages/ghost-core/src/types.ts b/packages/ghost-core/src/types.ts index 2f5a823..abda207 100644 --- a/packages/ghost-core/src/types.ts +++ b/packages/ghost-core/src/types.ts @@ -1,5 +1,36 @@ // --- Registry types (mirrors shadcn registry schema) --- +export type RegistryItemType = + | "registry:ui" + | "registry:style" + | "registry:lib" + | "registry:base" + | "registry:font" + | "registry:block" + | "registry:component" + | "registry:hook" + | "registry:theme" + | "registry:file" + | "registry:page" + | "registry:item"; + +export interface FontDescriptor { + family: string; + provider: string; + import: string; + variable: string; + weight?: string[]; + subsets?: string[]; + selector?: string; + dependency?: string; +} + +export interface CSSVarsMap { + theme?: Record; + light?: Record; + dark?: Record; +} + export interface Registry { $schema?: string; name: string; @@ -9,12 +40,31 @@ export interface Registry { export interface RegistryItem { name: string; - type: "registry:ui" | "registry:style" | "registry:lib"; + type: RegistryItemType; dependencies?: string[]; devDependencies?: string[]; registryDependencies?: string[]; files: RegistryFile[]; categories?: string[]; + // v4 fields + font?: FontDescriptor; + cssVars?: CSSVarsMap; + css?: string; + meta?: Record; + title?: string; + description?: string; + author?: string; +} + +export interface ComponentMeta { + name: string; + description?: string; + categories: string[]; + exports: string[]; + variants: { name: string; options: string[] }[]; + dataSlots: string[]; + dependencies: string[]; + registryDependencies: string[]; } export interface RegistryFile { @@ -44,6 +94,7 @@ export type TokenCategory = | "animation" | "color" | "font" + | "font-face" | "chart" | "sidebar" | "other"; diff --git a/packages/ghost-ui/.shadcn/skills.md b/packages/ghost-ui/.shadcn/skills.md new file mode 100644 index 0000000..f0a8fbc --- /dev/null +++ b/packages/ghost-ui/.shadcn/skills.md @@ -0,0 +1,176 @@ +# Ghost UI — Agent Skills + +## Overview + +Ghost UI is a magazine-inspired design system built on shadcn/ui with: +- **Pill geometry**: 999px border-radius on buttons, inputs, and pills +- **HK Grotesk typography**: Self-hosted, 7 weights (300-900), magazine-scale heading hierarchy +- **4-tier shadow hierarchy**: mini, card, elevated, modal +- **97 components** across 9 categories +- **323+ CSS custom properties** with full light/dark mode support + +## Style: ghost + +This registry uses the `ghost` style. Components use CVA (class-variance-authority) for variants and `data-slot` attributes for runtime introspection. + +## Token System + +Semantic layers: +- **Backgrounds**: `--background-default`, `--background-alt`, `--background-muted`, `--background-inverse`, `--background-accent`, `--background-danger/success/info/warning` +- **Borders**: `--border-default`, `--border-input`, `--border-strong`, `--border-card`, `--border-accent` +- **Text**: `--text-default`, `--text-muted`, `--text-alt`, `--text-inverse`, `--text-accent` +- **Shadows**: `--shadow-mini`, `--shadow-card`, `--shadow-elevated`, `--shadow-modal` +- **Radii**: `--radius-pill` (999px), `--radius-button` (999px), `--radius-card` (20px), `--radius-modal` (16px) + +Typography scale (magazine rhythm): +- Display: clamp(64px, 8vw, 96px), weight 900 +- Section: clamp(44px, 5vw, 64px), weight 700 +- Sub: clamp(28px, 3vw, 40px), weight 700 +- Card: clamp(20px, 2vw, 28px), weight 600 +- Body reading: clamp(1rem, 1.3vw, 1.25rem), line-height 1.65 +- Label: 11px, uppercase, 0.12em letter-spacing + +## Theme Presets + +- **Default** (default): Monochromatic grayscale +- **Warm Sand** (warm-sand): Earthy terracotta warmth +- **Ocean** (ocean): Cool blues and teals +- **Midnight Luxe** (midnight-luxe): Deep purples and golds +- **Neon Brutalist** (neon-brutalist): High contrast, sharp edges +- **Soft Pastel** (soft-pastel): Gentle, muted pastels + +## Categories + +- **layout**: accordion, aspect-ratio, collapsible, resizable, scroll-area, separator, sidebar, canvas, connection, controls, edge, node, panel, toolbar +- **feedback**: alert-dialog, alert, dialog, drawer, popover, progress, sheet, sonner, spinner, tooltip, confirmation, shimmer +- **display**: avatar, badge, calendar, card, carousel, chart, hover-card, skeleton, table, agent, artifact, chain-of-thought, checkpoint, context, inline-citation, plan, queue, sources, task, tool +- **navigation**: breadcrumb, command, context-menu, dropdown-menu, menubar, navigation-menu, pagination, sidebar, tabs +- **input**: button-group, button, calendar, checkbox, command, form, input-group, input-otp, input, label, radio-group, select, slider, switch, textarea, toggle-group, toggle, attachments, mic-selector, model-selector, prompt-input, speech-input, voice-selector +- **ai**: agent, artifact, attachments, audio-player, canvas, chain-of-thought, checkpoint, code-block, commit, confirmation, connection, context, controls, conversation, edge, environment-variables, file-tree, image, inline-citation, jsx-preview, message, mic-selector, model-selector, node, open-in-chat, package-info, panel, persona, plan, prompt-input, queue, reasoning, sandbox, schema-display, shimmer, snippet, sources, speech-input, stack-trace, suggestion, task, terminal, test-results, tool, toolbar, transcription, voice-selector, web-preview +- **chat**: attachments, conversation, message, open-in-chat, prompt-input, reasoning, suggestion, transcription +- **media**: audio-player, image, mic-selector, persona, speech-input, transcription, voice-selector, web-preview +- **code**: code-block, commit, environment-variables, file-tree, jsx-preview, package-info, sandbox, schema-display, snippet, stack-trace, terminal, test-results + +## Component Reference + +| Component | Categories | Exports | Variants | Data Slots | +|-----------|-----------|---------|----------|------------| +| accordion | layout | | - | accordion, accordion-item, accordion-trigger, accordion-content | +| alert-dialog | feedback | | - | alert-dialog, alert-dialog-trigger, alert-dialog-portal, alert-dialog-overlay, alert-dialog-content, alert-dialog-header, alert-dialog-footer, alert-dialog-title, alert-dialog-description | +| alert | feedback | | - | alert, alert-title, alert-description | +| aspect-ratio | layout | | - | aspect-ratio | +| avatar | display | | - | avatar, avatar-image, avatar-fallback | +| badge | display | | - | badge | +| breadcrumb | navigation | | - | breadcrumb, breadcrumb-list, breadcrumb-item, breadcrumb-link, breadcrumb-page, breadcrumb-separator, breadcrumb-ellipsis | +| button-group | input | | - | button-group, button-group-separator | +| button | input | | - | button | +| calendar | display, input | | - | - | +| card | display | | - | card, card-header, card-title, card-description, card-action, card-content, card-footer | +| carousel | display | | - | carousel, carousel-content, carousel-item, carousel-previous, carousel-next | +| chart | display | | - | chart | +| checkbox | input | | - | checkbox, checkbox-indicator | +| collapsible | layout | | - | collapsible, collapsible-trigger, collapsible-content | +| command | input, navigation | | - | command, command-input-wrapper, command-input, command-list, command-empty, command-group, command-separator, command-item, command-shortcut | +| context-menu | navigation | | - | context-menu, context-menu-trigger, context-menu-group, context-menu-portal, context-menu-sub, context-menu-radio-group, context-menu-sub-trigger, context-menu-sub-content, context-menu-content, context-menu-item, context-menu-checkbox-item, context-menu-radio-item, context-menu-label, context-menu-separator, context-menu-shortcut | +| dialog | feedback | | - | dialog, dialog-trigger, dialog-portal, dialog-close, dialog-overlay, dialog-content, dialog-header, dialog-footer, dialog-title, dialog-description | +| drawer | feedback | | - | drawer, drawer-trigger, drawer-portal, drawer-close, drawer-overlay, drawer-content, drawer-header, drawer-footer, drawer-title, drawer-description | +| dropdown-menu | navigation | | - | dropdown-menu, dropdown-menu-portal, dropdown-menu-trigger, dropdown-menu-content, dropdown-menu-group, dropdown-menu-item, dropdown-menu-checkbox-item, dropdown-menu-radio-group, dropdown-menu-radio-item, dropdown-menu-label, dropdown-menu-separator, dropdown-menu-shortcut, dropdown-menu-sub, dropdown-menu-sub-trigger, dropdown-menu-sub-content | +| form | input | | - | form-item, form-label, form-control, form-description, form-message | +| hover-card | display | | - | hover-card, hover-card-trigger, hover-card-portal, hover-card-content | +| input-group | input | | - | input-group, input-group-addon, input-group-control | +| input-otp | input | | - | input-otp, input-otp-group, input-otp-slot, input-otp-separator | +| input | input | | - | input | +| label | input | | - | label | +| menubar | navigation | | - | menubar, menubar-menu, menubar-group, menubar-portal, menubar-radio-group, menubar-trigger, menubar-content, menubar-item, menubar-checkbox-item, menubar-radio-item, menubar-label, menubar-separator, menubar-shortcut, menubar-sub, menubar-sub-trigger, menubar-sub-content | +| navigation-menu | navigation | | - | navigation-menu, navigation-menu-list, navigation-menu-item, navigation-menu-trigger, navigation-menu-content, navigation-menu-viewport, navigation-menu-link, navigation-menu-indicator | +| pagination | navigation | | - | pagination, pagination-content, pagination-item, pagination-link, pagination-ellipsis | +| popover | feedback | | - | popover, popover-trigger, popover-content, popover-anchor | +| progress | feedback | | - | progress, progress-indicator | +| radio-group | input | | - | radio-group, radio-group-item, radio-group-indicator | +| resizable | layout | | - | resizable-panel-group, resizable-panel, resizable-handle | +| scroll-area | layout | | - | scroll-area, scroll-area-viewport, scroll-area-scrollbar, scroll-area-thumb | +| select | input | | - | select, select-group, select-value, select-trigger, select-content, select-label, select-item, select-separator, select-scroll-up-button, select-scroll-down-button | +| separator | layout | | - | separator-root | +| sheet | feedback | | - | sheet, sheet-trigger, sheet-close, sheet-portal, sheet-overlay, sheet-content, sheet-header, sheet-footer, sheet-title, sheet-description | +| sidebar | navigation, layout | | - | sidebar-wrapper, sidebar, sidebar-gap, sidebar-container, sidebar-inner, sidebar-trigger, sidebar-rail, sidebar-inset, sidebar-input, sidebar-header, sidebar-footer, sidebar-separator, sidebar-content, sidebar-group, sidebar-group-label, sidebar-group-action, sidebar-group-content, sidebar-menu, sidebar-menu-item, sidebar-menu-button, sidebar-menu-action, sidebar-menu-badge, sidebar-menu-skeleton, sidebar-menu-sub, sidebar-menu-sub-item, sidebar-menu-sub-button | +| skeleton | display | | - | skeleton | +| slider | input | | - | slider, slider-track, slider-range, slider-thumb | +| sonner | feedback | | - | - | +| spinner | feedback | | - | - | +| switch | input | | - | switch, switch-thumb | +| table | display | | - | table-container, table, table-header, table-body, table-footer, table-row, table-head, table-cell, table-caption | +| tabs | navigation | | - | tabs, tabs-list, tabs-trigger, tabs-content | +| textarea | input | | - | textarea | +| toggle-group | input | | - | toggle-group, toggle-group-item | +| toggle | input | | - | toggle | +| tooltip | feedback | | - | tooltip-provider, tooltip, tooltip-trigger, tooltip-content | +| agent | ai, display | Agent, AgentHeader, AgentContent, AgentInstructions, AgentTools, AgentTool, AgentOutput | - | - | +| artifact | ai, display | Artifact, ArtifactHeader, ArtifactClose, ArtifactTitle, ArtifactDescription, ArtifactActions, ArtifactAction, ArtifactContent | - | - | +| attachments | ai, input, chat | getMediaCategory, getAttachmentLabel, useAttachmentsContext, useAttachmentContext, Attachments, Attachment, AttachmentPreview, AttachmentInfo, AttachmentRemove, AttachmentHoverCard, AttachmentHoverCardTrigger, AttachmentHoverCardContent, AttachmentEmpty | - | - | +| audio-player | ai, media | AudioPlayer, AudioPlayerElement, AudioPlayerControlBar, AudioPlayerPlayButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, AudioPlayerTimeRange, AudioPlayerDurationDisplay, AudioPlayerMuteButton, AudioPlayerVolumeRange | - | audio-player, audio-player-element, audio-player-control-bar, audio-player-play-button, audio-player-seek-backward-button, audio-player-seek-forward-button, audio-player-time-display, audio-player-time-range, audio-player-duration-display, audio-player-mute-button, audio-player-volume-range | +| canvas | ai, layout | Canvas | - | - | +| chain-of-thought | ai, display | ChainOfThought, ChainOfThoughtHeader, ChainOfThoughtStep, ChainOfThoughtSearchResults, ChainOfThoughtSearchResult, ChainOfThoughtContent, ChainOfThoughtImage | - | - | +| checkpoint | ai, display | Checkpoint, CheckpointIcon, CheckpointTrigger | - | - | +| code-block | ai, code | highlightCode, CodeBlockContainer, CodeBlockHeader, CodeBlockTitle, CodeBlockFilename, CodeBlockActions, CodeBlockContent, CodeBlock, CodeBlockCopyButton, CodeBlockLanguageSelector, CodeBlockLanguageSelectorTrigger, CodeBlockLanguageSelectorValue, CodeBlockLanguageSelectorContent, CodeBlockLanguageSelectorItem | - | - | +| commit | ai, code | Commit, CommitHeader, CommitHash, CommitMessage, CommitMetadata, CommitSeparator, CommitInfo, CommitAuthor, CommitAuthorAvatar, CommitTimestamp, CommitActions, CommitCopyButton, CommitContent, CommitFiles, CommitFile, CommitFileInfo, CommitFileStatus, CommitFileIcon, CommitFilePath, CommitFileChanges, CommitFileAdditions, CommitFileDeletions | - | - | +| confirmation | ai, feedback | Confirmation, ConfirmationTitle, ConfirmationRequest, ConfirmationAccepted, ConfirmationRejected, ConfirmationActions, ConfirmationAction | - | - | +| connection | ai, layout | Connection | - | - | +| context | ai, display | Context, ContextTrigger, ContextContent, ContextContentHeader, ContextContentBody, ContextContentFooter, ContextInputUsage, ContextOutputUsage, ContextReasoningUsage, ContextCacheUsage | - | - | +| controls | ai, layout | Controls | - | - | +| conversation | ai, chat | Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton, messagesToMarkdown, ConversationDownload | - | - | +| edge | ai, layout | Edge | - | - | +| environment-variables | ai, code | EnvironmentVariables, EnvironmentVariablesHeader, EnvironmentVariablesTitle, EnvironmentVariablesToggle, EnvironmentVariablesContent, EnvironmentVariableGroup, EnvironmentVariableName, EnvironmentVariableValue, EnvironmentVariable, EnvironmentVariableCopyButton, EnvironmentVariableRequired | - | - | +| file-tree | ai, code | FileTree, FileTreeIcon, FileTreeName, FileTreeFolder, FileTreeFile, FileTreeActions | - | - | +| image | ai, media | Image | - | - | +| inline-citation | ai, display | InlineCitation, InlineCitationText, InlineCitationCard, InlineCitationCardTrigger, InlineCitationCardBody, InlineCitationCarousel, InlineCitationCarouselContent, InlineCitationCarouselItem, InlineCitationCarouselHeader, InlineCitationCarouselIndex, InlineCitationCarouselPrev, InlineCitationCarouselNext, InlineCitationSource, InlineCitationQuote | - | - | +| jsx-preview | ai, code | useJSXPreview, JSXPreview, JSXPreviewContent, JSXPreviewError | - | - | +| message | ai, chat | Message, MessageContent, MessageActions, MessageAction, MessageBranch, MessageBranchContent, MessageBranchSelector, MessageBranchPrevious, MessageBranchNext, MessageBranchPage, MessageResponse, MessageToolbar | - | - | +| mic-selector | ai, input, media | useAudioDevices, MicSelector, MicSelectorTrigger, MicSelectorContent, MicSelectorInput, MicSelectorList, MicSelectorEmpty, MicSelectorItem, MicSelectorLabel, MicSelectorValue | - | - | +| model-selector | ai, input | ModelSelector, ModelSelectorTrigger, ModelSelectorContent, ModelSelectorDialog, ModelSelectorInput, ModelSelectorList, ModelSelectorEmpty, ModelSelectorGroup, ModelSelectorItem, ModelSelectorShortcut, ModelSelectorSeparator, ModelSelectorLogo, ModelSelectorLogoGroup, ModelSelectorName | - | - | +| node | ai, layout | Node, NodeHeader, NodeTitle, NodeDescription, NodeAction, NodeContent, NodeFooter | - | - | +| open-in-chat | ai, chat | OpenIn, OpenInContent, OpenInItem, OpenInLabel, OpenInSeparator, OpenInTrigger, OpenInChatGPT, OpenInClaude, OpenInT3, OpenInScira, OpenInv0, OpenInCursor | - | - | +| package-info | ai, code | PackageInfoHeader, PackageInfoName, PackageInfoChangeType, PackageInfoVersion, PackageInfo, PackageInfoDescription, PackageInfoContent, PackageInfoDependencies, PackageInfoDependency | - | - | +| panel | ai, layout | Panel | - | - | +| persona | ai, media | Persona | - | - | +| plan | ai, display | Plan, PlanHeader, PlanTitle, PlanDescription, PlanAction, PlanContent, PlanFooter, PlanTrigger | - | plan, plan-header, plan-title, plan-description, plan-action, plan-content, plan-footer, plan-trigger | +| prompt-input | ai, input, chat | usePromptInputController, useProviderAttachments, PromptInputProvider, usePromptInputAttachments, LocalReferencedSourcesContext, usePromptInputReferencedSources, PromptInputActionAddAttachments, PromptInputActionAddScreenshot, PromptInput, PromptInputBody, PromptInputTextarea, PromptInputHeader, PromptInputFooter, PromptInputTools, PromptInputButton, PromptInputActionMenu, PromptInputActionMenuTrigger, PromptInputActionMenuContent, PromptInputActionMenuItem, PromptInputSubmit, PromptInputSelect, PromptInputSelectTrigger, PromptInputSelectContent, PromptInputSelectItem, PromptInputSelectValue, PromptInputHoverCard, PromptInputHoverCardTrigger, PromptInputHoverCardContent, PromptInputTabsList, PromptInputTab, PromptInputTabLabel, PromptInputTabBody, PromptInputTabItem, PromptInputCommand, PromptInputCommandInput, PromptInputCommandList, PromptInputCommandEmpty, PromptInputCommandGroup, PromptInputCommandItem, PromptInputCommandSeparator | - | - | +| queue | ai, display | QueueItem, QueueItemIndicator, QueueItemContent, QueueItemDescription, QueueItemActions, QueueItemAction, QueueItemAttachment, QueueItemImage, QueueItemFile, QueueList, QueueSection, QueueSectionTrigger, QueueSectionLabel, QueueSectionContent, Queue | - | - | +| reasoning | ai, chat | useReasoning, Reasoning, ReasoningTrigger, ReasoningContent | - | - | +| sandbox | ai, code | Sandbox, SandboxHeader, SandboxContent, SandboxTabs, SandboxTabsBar, SandboxTabsList, SandboxTabsTrigger, SandboxTabContent | - | - | +| schema-display | ai, code | SchemaDisplayHeader, SchemaDisplayMethod, SchemaDisplayPath, SchemaDisplayDescription, SchemaDisplayContent, SchemaDisplayParameter, SchemaDisplayParameters, SchemaDisplayProperty, SchemaDisplayRequest, SchemaDisplayResponse, SchemaDisplay, SchemaDisplayBody, SchemaDisplayExample | - | - | +| shimmer | ai, feedback | Shimmer | - | - | +| snippet | ai, code | Snippet, SnippetAddon, SnippetText, SnippetInput, SnippetCopyButton | - | - | +| sources | ai, display | Sources, SourcesTrigger, SourcesContent, Source | - | - | +| speech-input | ai, input, media | SpeechInput | - | - | +| stack-trace | ai, code | StackTrace, StackTraceHeader, StackTraceError, StackTraceErrorType, StackTraceErrorMessage, StackTraceActions, StackTraceCopyButton, StackTraceExpandButton, StackTraceContent, StackTraceFrames | - | - | +| suggestion | ai, chat | Suggestions, Suggestion | - | - | +| task | ai, display | TaskItemFile, TaskItem, Task, TaskTrigger, TaskContent | - | - | +| terminal | ai, code | TerminalHeader, TerminalTitle, TerminalStatus, TerminalActions, TerminalCopyButton, TerminalClearButton, TerminalContent, Terminal | - | - | +| test-results | ai, code | TestResultsHeader, TestResultsDuration, TestResultsSummary, TestResults, TestResultsProgress, TestResultsContent, TestSuite, TestSuiteName, TestSuiteStats, TestSuiteContent, TestName, TestDuration, TestStatus, Test, TestError, TestErrorMessage, TestErrorStack | - | - | +| tool | ai, display | Tool, getStatusBadge, ToolHeader, ToolContent, ToolInput, ToolOutput | - | - | +| toolbar | ai, layout | Toolbar | - | - | +| transcription | ai, chat, media | Transcription, TranscriptionSegment | - | transcription, transcription-segment | +| voice-selector | ai, input, media | useVoiceSelector, VoiceSelector, VoiceSelectorTrigger, VoiceSelectorContent, VoiceSelectorDialog, VoiceSelectorInput, VoiceSelectorList, VoiceSelectorEmpty, VoiceSelectorGroup, VoiceSelectorItem, VoiceSelectorShortcut, VoiceSelectorSeparator, VoiceSelectorGender, VoiceSelectorAccent, VoiceSelectorAge, VoiceSelectorName, VoiceSelectorDescription, VoiceSelectorAttributes, VoiceSelectorBullet, VoiceSelectorPreview | - | - | +| web-preview | ai, media | WebPreview, WebPreviewNavigation, WebPreviewNavigationButton, WebPreviewUrl, WebPreviewBody, WebPreviewConsole | - | - | + +## Usage Patterns + +### ThemeProvider +```tsx +import { ThemeProvider } from "@/lib/theme-provider" + + {children} + +``` + +### Import Aliases +- `@/components/ui/*` — UI primitives +- `@/components/ai-elements/*` — AI-native components +- `@/lib/utils` — `cn()` utility (clsx + tailwind-merge) +- `@/lib/theme-provider` — ThemeProvider context + +### Icon Library +Uses `lucide-react` for all icons. + +### Dark Mode +Toggle via `.dark` class on `document.documentElement`. ThemeProvider handles this automatically. diff --git a/packages/ghost-ui/components.json b/packages/ghost-ui/components.json new file mode 100644 index 0000000..afe38a0 --- /dev/null +++ b/packages/ghost-ui/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "ghost", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "styles/main.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/packages/ghost-ui/index.html b/packages/ghost-ui/index.html new file mode 100644 index 0000000..3c9dc3b --- /dev/null +++ b/packages/ghost-ui/index.html @@ -0,0 +1,13 @@ + + + + + + Ghost UI + + + +
+ + + diff --git a/packages/ghost-ui/package.json b/packages/ghost-ui/package.json new file mode 100644 index 0000000..645c134 --- /dev/null +++ b/packages/ghost-ui/package.json @@ -0,0 +1,102 @@ +{ + "name": "@ghost/ui", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "build:base": "node scripts/build-base-vars.mjs", + "build:presets": "node scripts/build-presets.mjs", + "generate:skills": "node scripts/generate-skills.mjs", + "build:registry": "npm run build:base && npm run build:presets && npm run generate:skills && shadcn build", + "typecheck": "tsc --noEmit", + "generate:icons": "npx tsx scripts/generate-icons.ts" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-aspect-ratio": "^1.1.8", + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-hover-card": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-radio-group": "^1.3.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slider": "^1.3.6", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@radix-ui/react-use-controllable-state": "^1.2.2", + "@rive-app/react-webgl2": "^4.27.3", + "@streamdown/cjk": "^1.0.3", + "@streamdown/code": "^1.1.1", + "@streamdown/math": "^1.0.2", + "@streamdown/mermaid": "^1.0.2", + "@tanstack/react-table": "^8.21.3", + "@xyflow/react": "^12.10.2", + "ai": "^6.0.141", + "ansi-to-react": "^6.2.6", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "embla-carousel-react": "^8.6.0", + "gsap": "^3.14.2", + "input-otp": "^1.4.2", + "lucide-react": "^1.7.0", + "media-chrome": "^4.18.3", + "motion": "^12.38.0", + "nanoid": "^5.1.7", + "react-day-picker": "9.14.0", + "react-hook-form": "^7.72.0", + "react-jsx-parser": "^2.4.1", + "react-resizable-panels": "^4.8.0", + "react-router": "^7.5.0", + "recharts": "^3.8.1", + "shiki": "^4.0.2", + "smart-registry": "^1.16.0", + "sonner": "^2.0.7", + "split-type": "^0.3.4", + "streamdown": "^2.5.0", + "tailwind-merge": "^3.5.0", + "tokenlens": "^1.3.1", + "tw-animate-css": "^1.4.0", + "use-stick-to-bottom": "^1.1.3", + "vaul": "^1.1.2", + "zod": "^4.3.6", + "zustand": "^5.0.12" + }, + "peerDependencies": { + "react": ">=19.0.0", + "react-dom": ">=19.0.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.2.2", + "@types/node": "^22.0.0", + "@types/react": "19.1.2", + "@types/react-dom": "19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "react": "19.1.0", + "react-dom": "19.1.0", + "shadcn": "4.1.1", + "tailwindcss": "^4.2.2", + "typescript": "^5.7.0", + "vite": "^6.3.0" + } +} diff --git a/packages/ghost-ui/public/placeholder.svg b/packages/ghost-ui/public/placeholder.svg new file mode 100644 index 0000000..95e9d5a --- /dev/null +++ b/packages/ghost-ui/public/placeholder.svg @@ -0,0 +1,4 @@ + + + Placeholder + diff --git a/packages/ghost-ui/public/r/accordion.json b/packages/ghost-ui/public/r/accordion.json new file mode 100644 index 0000000..b9337a2 --- /dev/null +++ b/packages/ghost-ui/public/r/accordion.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "accordion", + "dependencies": ["@radix-ui/react-accordion", "lucide-react"], + "registryDependencies": ["utils"], + "files": [ + { + "path": "src/components/ui/accordion.tsx", + "content": "\"use client\";\n\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport { ChevronDownIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Accordion({\n ...props\n}: React.ComponentProps) {\n return ;\n}\n\nfunction AccordionItem({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction AccordionTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps) {\n return (\n \n svg]:rotate-180\",\n className,\n )}\n {...props}\n >\n {children}\n \n \n \n );\n}\n\nfunction AccordionContent({\n className,\n children,\n ...props\n}: React.ComponentProps) {\n return (\n \n
{children}
\n \n );\n}\n\nexport { Accordion, AccordionContent, AccordionItem, AccordionTrigger };\n", + "type": "registry:ui", + "target": "components/ui/accordion.tsx" + } + ], + "categories": ["layout"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/agent.json b/packages/ghost-ui/public/r/agent.json new file mode 100644 index 0000000..87986b6 --- /dev/null +++ b/packages/ghost-ui/public/r/agent.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "agent", + "dependencies": ["ai", "lucide-react"], + "registryDependencies": ["utils", "accordion", "badge"], + "files": [ + { + "path": "src/components/ai-elements/agent.tsx", + "content": "\"use client\";\n\nimport type { Tool } from \"ai\";\nimport { BotIcon } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\nimport { memo } from \"react\";\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"@/components/ui/accordion\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/utils\";\n\nimport { CodeBlock } from \"./code-block\";\n\nexport type AgentProps = ComponentProps<\"div\">;\n\nexport const Agent = memo(({ className, ...props }: AgentProps) => (\n \n));\n\nexport type AgentHeaderProps = ComponentProps<\"div\"> & {\n name: string;\n model?: string;\n};\n\nexport const AgentHeader = memo(\n ({ className, name, model, ...props }: AgentHeaderProps) => (\n \n
\n \n {name}\n {model && (\n \n {model}\n \n )}\n
\n \n ),\n);\n\nexport type AgentContentProps = ComponentProps<\"div\">;\n\nexport const AgentContent = memo(\n ({ className, ...props }: AgentContentProps) => (\n
\n ),\n);\n\nexport type AgentInstructionsProps = ComponentProps<\"div\"> & {\n children: string;\n};\n\nexport const AgentInstructions = memo(\n ({ className, children, ...props }: AgentInstructionsProps) => (\n
\n \n Instructions\n \n
\n

{children}

\n
\n
\n ),\n);\n\nexport type AgentToolsProps = ComponentProps;\n\nexport const AgentTools = memo(({ className, ...props }: AgentToolsProps) => (\n
\n Tools\n \n
\n));\n\nexport type AgentToolProps = ComponentProps & {\n tool: Tool;\n};\n\nexport const AgentTool = memo(\n ({ className, tool, value, ...props }: AgentToolProps) => {\n const schema =\n \"jsonSchema\" in tool && tool.jsonSchema\n ? tool.jsonSchema\n : tool.inputSchema;\n\n return (\n \n \n {tool.description ?? \"No description\"}\n \n \n
\n \n
\n
\n \n );\n },\n);\n\nexport type AgentOutputProps = ComponentProps<\"div\"> & {\n schema: string;\n};\n\nexport const AgentOutput = memo(\n ({ className, schema, ...props }: AgentOutputProps) => (\n
\n \n Output Schema\n \n
\n \n
\n
\n ),\n);\n\nAgent.displayName = \"Agent\";\nAgentHeader.displayName = \"AgentHeader\";\nAgentContent.displayName = \"AgentContent\";\nAgentInstructions.displayName = \"AgentInstructions\";\nAgentTools.displayName = \"AgentTools\";\nAgentTool.displayName = \"AgentTool\";\nAgentOutput.displayName = \"AgentOutput\";\n", + "type": "registry:ui", + "target": "components/ai-elements/agent.tsx" + } + ], + "categories": ["ai", "display"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/alert-dialog.json b/packages/ghost-ui/public/r/alert-dialog.json new file mode 100644 index 0000000..3497075 --- /dev/null +++ b/packages/ghost-ui/public/r/alert-dialog.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "alert-dialog", + "dependencies": ["@radix-ui/react-alert-dialog"], + "registryDependencies": ["utils", "button"], + "files": [ + { + "path": "src/components/ui/alert-dialog.tsx", + "content": "\"use client\";\n\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\";\nimport type * as React from \"react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nfunction AlertDialog({\n ...props\n}: React.ComponentProps) {\n return ;\n}\n\nfunction AlertDialogTrigger({\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction AlertDialogPortal({\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction AlertDialogOverlay({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction AlertDialogContent({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n \n );\n}\n\nfunction AlertDialogHeader({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction AlertDialogFooter({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction AlertDialogTitle({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction AlertDialogDescription({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction AlertDialogAction({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction AlertDialogCancel({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nexport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogOverlay,\n AlertDialogPortal,\n AlertDialogTitle,\n AlertDialogTrigger,\n};\n", + "type": "registry:ui", + "target": "components/ui/alert-dialog.tsx" + } + ], + "categories": ["feedback"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/alert.json b/packages/ghost-ui/public/r/alert.json new file mode 100644 index 0000000..0b638c3 --- /dev/null +++ b/packages/ghost-ui/public/r/alert.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "alert", + "dependencies": ["class-variance-authority"], + "registryDependencies": ["utils"], + "files": [ + { + "path": "src/components/ui/alert.tsx", + "content": "import { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst alertVariants = cva(\n \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n {\n variants: {\n variant: {\n default: \"bg-background text-text\",\n destructive:\n \"text-destructive bg-background [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n },\n);\n\nfunction Alert({\n className,\n variant,\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps) {\n return (\n \n );\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nfunction AlertDescription({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n \n );\n}\n\nexport { Alert, AlertDescription, AlertTitle };\n", + "type": "registry:ui", + "target": "components/ui/alert.tsx" + } + ], + "categories": ["feedback"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/artifact.json b/packages/ghost-ui/public/r/artifact.json new file mode 100644 index 0000000..6d0b4e8 --- /dev/null +++ b/packages/ghost-ui/public/r/artifact.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "artifact", + "dependencies": ["lucide-react"], + "registryDependencies": ["utils", "button", "tooltip"], + "files": [ + { + "path": "src/components/ai-elements/artifact.tsx", + "content": "\"use client\";\n\nimport type { LucideIcon } from \"lucide-react\";\nimport { XIcon } from \"lucide-react\";\nimport type { ComponentProps, HTMLAttributes } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ArtifactProps = HTMLAttributes;\n\nexport const Artifact = ({ className, ...props }: ArtifactProps) => (\n \n);\n\nexport type ArtifactHeaderProps = HTMLAttributes;\n\nexport const ArtifactHeader = ({\n className,\n ...props\n}: ArtifactHeaderProps) => (\n \n);\n\nexport type ArtifactCloseProps = ComponentProps;\n\nexport const ArtifactClose = ({\n className,\n children,\n size = \"sm\",\n variant = \"ghost\",\n ...props\n}: ArtifactCloseProps) => (\n \n {children ?? }\n Close\n \n);\n\nexport type ArtifactTitleProps = HTMLAttributes;\n\nexport const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (\n \n);\n\nexport type ArtifactDescriptionProps = HTMLAttributes;\n\nexport const ArtifactDescription = ({\n className,\n ...props\n}: ArtifactDescriptionProps) => (\n

\n);\n\nexport type ArtifactActionsProps = HTMLAttributes;\n\nexport const ArtifactActions = ({\n className,\n ...props\n}: ArtifactActionsProps) => (\n

\n);\n\nexport type ArtifactActionProps = ComponentProps & {\n tooltip?: string;\n label?: string;\n icon?: LucideIcon;\n};\n\nexport const ArtifactAction = ({\n tooltip,\n label,\n icon: Icon,\n children,\n className,\n size = \"sm\",\n variant = \"ghost\",\n ...props\n}: ArtifactActionProps) => {\n const button = (\n \n {Icon ? : children}\n {label || tooltip}\n \n );\n\n if (tooltip) {\n return (\n \n \n {button}\n \n

{tooltip}

\n
\n
\n
\n );\n }\n\n return button;\n};\n\nexport type ArtifactContentProps = HTMLAttributes;\n\nexport const ArtifactContent = ({\n className,\n ...props\n}: ArtifactContentProps) => (\n
\n);\n", + "type": "registry:ui", + "target": "components/ai-elements/artifact.tsx" + } + ], + "categories": ["ai", "display"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/aspect-ratio.json b/packages/ghost-ui/public/r/aspect-ratio.json new file mode 100644 index 0000000..21df4d3 --- /dev/null +++ b/packages/ghost-ui/public/r/aspect-ratio.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "aspect-ratio", + "dependencies": ["@radix-ui/react-aspect-ratio"], + "files": [ + { + "path": "src/components/ui/aspect-ratio.tsx", + "content": "\"use client\";\n\nimport * as AspectRatioPrimitive from \"@radix-ui/react-aspect-ratio\";\n\nfunction AspectRatio({\n ...props\n}: React.ComponentProps) {\n return ;\n}\n\nexport { AspectRatio };\n", + "type": "registry:ui", + "target": "components/ui/aspect-ratio.tsx" + } + ], + "categories": ["layout"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/attachments.json b/packages/ghost-ui/public/r/attachments.json new file mode 100644 index 0000000..e8d1ef9 --- /dev/null +++ b/packages/ghost-ui/public/r/attachments.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "attachments", + "dependencies": ["ai", "lucide-react"], + "registryDependencies": ["utils", "button", "hover-card"], + "files": [ + { + "path": "src/components/ai-elements/attachments.tsx", + "content": "\"use client\";\n\nimport type { FileUIPart, SourceDocumentUIPart } from \"ai\";\nimport {\n FileTextIcon,\n GlobeIcon,\n ImageIcon,\n Music2Icon,\n PaperclipIcon,\n VideoIcon,\n XIcon,\n} from \"lucide-react\";\nimport type { ComponentProps, HTMLAttributes, ReactNode } from \"react\";\nimport { createContext, useCallback, useContext, useMemo } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n HoverCard,\n HoverCardContent,\n HoverCardTrigger,\n} from \"@/components/ui/hover-card\";\nimport { cn } from \"@/lib/utils\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type AttachmentData =\n | (FileUIPart & { id: string })\n | (SourceDocumentUIPart & { id: string });\n\nexport type AttachmentMediaCategory =\n | \"image\"\n | \"video\"\n | \"audio\"\n | \"document\"\n | \"source\"\n | \"unknown\";\n\nexport type AttachmentVariant = \"grid\" | \"inline\" | \"list\";\n\nconst mediaCategoryIcons: Record = {\n audio: Music2Icon,\n document: FileTextIcon,\n image: ImageIcon,\n source: GlobeIcon,\n unknown: PaperclipIcon,\n video: VideoIcon,\n};\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\nexport const getMediaCategory = (\n data: AttachmentData,\n): AttachmentMediaCategory => {\n if (data.type === \"source-document\") {\n return \"source\";\n }\n\n const mediaType = data.mediaType ?? \"\";\n\n if (mediaType.startsWith(\"image/\")) {\n return \"image\";\n }\n if (mediaType.startsWith(\"video/\")) {\n return \"video\";\n }\n if (mediaType.startsWith(\"audio/\")) {\n return \"audio\";\n }\n if (mediaType.startsWith(\"application/\") || mediaType.startsWith(\"text/\")) {\n return \"document\";\n }\n\n return \"unknown\";\n};\n\nexport const getAttachmentLabel = (data: AttachmentData): string => {\n if (data.type === \"source-document\") {\n return data.title || data.filename || \"Source\";\n }\n\n const category = getMediaCategory(data);\n return data.filename || (category === \"image\" ? \"Image\" : \"Attachment\");\n};\n\nconst renderAttachmentImage = (\n url: string,\n filename: string | undefined,\n isGrid: boolean,\n) =>\n isGrid ? (\n \n ) : (\n \n );\n\n// ============================================================================\n// Contexts\n// ============================================================================\n\ninterface AttachmentsContextValue {\n variant: AttachmentVariant;\n}\n\nconst AttachmentsContext = createContext(null);\n\ninterface AttachmentContextValue {\n data: AttachmentData;\n mediaCategory: AttachmentMediaCategory;\n onRemove?: () => void;\n variant: AttachmentVariant;\n}\n\nconst AttachmentContext = createContext(null);\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nexport const useAttachmentsContext = () =>\n useContext(AttachmentsContext) ?? { variant: \"grid\" as const };\n\nexport const useAttachmentContext = () => {\n const ctx = useContext(AttachmentContext);\n if (!ctx) {\n throw new Error(\"Attachment components must be used within \");\n }\n return ctx;\n};\n\n// ============================================================================\n// Attachments - Container\n// ============================================================================\n\nexport type AttachmentsProps = HTMLAttributes & {\n variant?: AttachmentVariant;\n};\n\nexport const Attachments = ({\n variant = \"grid\",\n className,\n children,\n ...props\n}: AttachmentsProps) => {\n const contextValue = useMemo(() => ({ variant }), [variant]);\n\n return (\n \n \n {children}\n
\n \n );\n};\n\n// ============================================================================\n// Attachment - Item\n// ============================================================================\n\nexport type AttachmentProps = HTMLAttributes & {\n data: AttachmentData;\n onRemove?: () => void;\n};\n\nexport const Attachment = ({\n data,\n onRemove,\n className,\n children,\n ...props\n}: AttachmentProps) => {\n const { variant } = useAttachmentsContext();\n const mediaCategory = getMediaCategory(data);\n\n const contextValue = useMemo(\n () => ({ data, mediaCategory, onRemove, variant }),\n [data, mediaCategory, onRemove, variant],\n );\n\n return (\n \n \n {children}\n
\n \n );\n};\n\n// ============================================================================\n// AttachmentPreview - Media preview\n// ============================================================================\n\nexport type AttachmentPreviewProps = HTMLAttributes & {\n fallbackIcon?: ReactNode;\n};\n\nexport const AttachmentPreview = ({\n fallbackIcon,\n className,\n ...props\n}: AttachmentPreviewProps) => {\n const { data, mediaCategory, variant } = useAttachmentContext();\n\n const iconSize = variant === \"inline\" ? \"size-3\" : \"size-4\";\n\n const renderIcon = (Icon: typeof ImageIcon) => (\n \n );\n\n const renderContent = () => {\n if (mediaCategory === \"image\" && data.type === \"file\" && data.url) {\n return renderAttachmentImage(data.url, data.filename, variant === \"grid\");\n }\n\n if (mediaCategory === \"video\" && data.type === \"file\" && data.url) {\n return
\n );\n};\n\n// ============================================================================\n// AttachmentInfo - Name and type display\n// ============================================================================\n\nexport type AttachmentInfoProps = HTMLAttributes & {\n showMediaType?: boolean;\n};\n\nexport const AttachmentInfo = ({\n showMediaType = false,\n className,\n ...props\n}: AttachmentInfoProps) => {\n const { data, variant } = useAttachmentContext();\n const label = getAttachmentLabel(data);\n\n if (variant === \"grid\") {\n return null;\n }\n\n return (\n
\n {label}\n {showMediaType && data.mediaType && (\n \n {data.mediaType}\n \n )}\n
\n );\n};\n\n// ============================================================================\n// AttachmentRemove - Remove button\n// ============================================================================\n\nexport type AttachmentRemoveProps = ComponentProps & {\n label?: string;\n};\n\nexport const AttachmentRemove = ({\n label = \"Remove\",\n className,\n children,\n ...props\n}: AttachmentRemoveProps) => {\n const { onRemove, variant } = useAttachmentContext();\n\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation();\n onRemove?.();\n },\n [onRemove],\n );\n\n if (!onRemove) {\n return null;\n }\n\n return (\n svg]:size-3\",\n ],\n variant === \"inline\" && [\n \"size-5 rounded p-0\",\n \"opacity-0 transition-opacity group-hover:opacity-100\",\n \"[&>svg]:size-2.5\",\n ],\n variant === \"list\" && [\"size-8 shrink-0 rounded p-0\", \"[&>svg]:size-4\"],\n className,\n )}\n onClick={handleClick}\n type=\"button\"\n variant=\"ghost\"\n {...props}\n >\n {children ?? }\n {label}\n \n );\n};\n\n// ============================================================================\n// AttachmentHoverCard - Hover preview\n// ============================================================================\n\nexport type AttachmentHoverCardProps = ComponentProps;\n\nexport const AttachmentHoverCard = ({\n openDelay = 0,\n closeDelay = 0,\n ...props\n}: AttachmentHoverCardProps) => (\n \n);\n\nexport type AttachmentHoverCardTriggerProps = ComponentProps<\n typeof HoverCardTrigger\n>;\n\nexport const AttachmentHoverCardTrigger = (\n props: AttachmentHoverCardTriggerProps,\n) => ;\n\nexport type AttachmentHoverCardContentProps = ComponentProps<\n typeof HoverCardContent\n>;\n\nexport const AttachmentHoverCardContent = ({\n align = \"start\",\n className,\n ...props\n}: AttachmentHoverCardContentProps) => (\n \n);\n\n// ============================================================================\n// AttachmentEmpty - Empty state\n// ============================================================================\n\nexport type AttachmentEmptyProps = HTMLAttributes;\n\nexport const AttachmentEmpty = ({\n className,\n children,\n ...props\n}: AttachmentEmptyProps) => (\n \n {children ?? \"No attachments\"}\n \n);\n", + "type": "registry:ui", + "target": "components/ai-elements/attachments.tsx" + } + ], + "categories": ["ai", "input", "chat"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/audio-player.json b/packages/ghost-ui/public/r/audio-player.json new file mode 100644 index 0000000..f9b7095 --- /dev/null +++ b/packages/ghost-ui/public/r/audio-player.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "audio-player", + "dependencies": ["ai", "media-chrome"], + "registryDependencies": ["utils", "button", "button-group"], + "files": [ + { + "path": "src/components/ai-elements/audio-player.tsx", + "content": "\"use client\";\n\nimport type { Experimental_SpeechResult as SpeechResult } from \"ai\";\nimport {\n MediaControlBar,\n MediaController,\n MediaDurationDisplay,\n MediaMuteButton,\n MediaPlayButton,\n MediaSeekBackwardButton,\n MediaSeekForwardButton,\n MediaTimeDisplay,\n MediaTimeRange,\n MediaVolumeRange,\n} from \"media-chrome/react\";\nimport type { ComponentProps, CSSProperties } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { ButtonGroup, ButtonGroupText } from \"@/components/ui/button-group\";\nimport { cn } from \"@/lib/utils\";\n\nexport type AudioPlayerProps = Omit<\n ComponentProps,\n \"audio\"\n>;\n\nexport const AudioPlayer = ({\n children,\n style,\n ...props\n}: AudioPlayerProps) => (\n \n {children}\n \n);\n\nexport type AudioPlayerElementProps = Omit, \"src\"> &\n (\n | {\n data: SpeechResult[\"audio\"];\n }\n | {\n src: string;\n }\n );\n\nexport const AudioPlayerElement = ({ ...props }: AudioPlayerElementProps) => (\n // oxlint-disable-next-line eslint-plugin-jsx-a11y(media-has-caption) -- audio player captions are provided by consumer\n \n);\n\nexport type AudioPlayerControlBarProps = ComponentProps;\n\nexport const AudioPlayerControlBar = ({\n children,\n ...props\n}: AudioPlayerControlBarProps) => (\n \n {children}\n \n);\n\nexport type AudioPlayerPlayButtonProps = ComponentProps;\n\nexport const AudioPlayerPlayButton = ({\n className,\n ...props\n}: AudioPlayerPlayButtonProps) => (\n \n);\n\nexport type AudioPlayerSeekBackwardButtonProps = ComponentProps<\n typeof MediaSeekBackwardButton\n>;\n\nexport const AudioPlayerSeekBackwardButton = ({\n seekOffset = 10,\n ...props\n}: AudioPlayerSeekBackwardButtonProps) => (\n \n);\n\nexport type AudioPlayerSeekForwardButtonProps = ComponentProps<\n typeof MediaSeekForwardButton\n>;\n\nexport const AudioPlayerSeekForwardButton = ({\n seekOffset = 10,\n ...props\n}: AudioPlayerSeekForwardButtonProps) => (\n \n);\n\nexport type AudioPlayerTimeDisplayProps = ComponentProps<\n typeof MediaTimeDisplay\n>;\n\nexport const AudioPlayerTimeDisplay = ({\n className,\n ...props\n}: AudioPlayerTimeDisplayProps) => (\n \n \n \n);\n\nexport type AudioPlayerTimeRangeProps = ComponentProps;\n\nexport const AudioPlayerTimeRange = ({\n className,\n ...props\n}: AudioPlayerTimeRangeProps) => (\n \n \n \n);\n\nexport type AudioPlayerDurationDisplayProps = ComponentProps<\n typeof MediaDurationDisplay\n>;\n\nexport const AudioPlayerDurationDisplay = ({\n className,\n ...props\n}: AudioPlayerDurationDisplayProps) => (\n \n \n \n);\n\nexport type AudioPlayerMuteButtonProps = ComponentProps;\n\nexport const AudioPlayerMuteButton = ({\n className,\n ...props\n}: AudioPlayerMuteButtonProps) => (\n \n \n \n);\n\nexport type AudioPlayerVolumeRangeProps = ComponentProps<\n typeof MediaVolumeRange\n>;\n\nexport const AudioPlayerVolumeRange = ({\n className,\n ...props\n}: AudioPlayerVolumeRangeProps) => (\n \n \n \n);\n", + "type": "registry:ui", + "target": "components/ai-elements/audio-player.tsx" + } + ], + "categories": ["ai", "media"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/avatar.json b/packages/ghost-ui/public/r/avatar.json new file mode 100644 index 0000000..5f5a437 --- /dev/null +++ b/packages/ghost-ui/public/r/avatar.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "avatar", + "dependencies": ["@radix-ui/react-avatar"], + "registryDependencies": ["utils"], + "files": [ + { + "path": "src/components/ui/avatar.tsx", + "content": "\"use client\";\n\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Avatar({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction AvatarImage({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction AvatarFallback({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nexport { Avatar, AvatarFallback, AvatarImage };\n", + "type": "registry:ui", + "target": "components/ui/avatar.tsx" + } + ], + "categories": ["display"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/badge.json b/packages/ghost-ui/public/r/badge.json new file mode 100644 index 0000000..a00865f --- /dev/null +++ b/packages/ghost-ui/public/r/badge.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "badge", + "dependencies": ["@radix-ui/react-slot", "class-variance-authority"], + "registryDependencies": ["utils"], + "files": [ + { + "path": "src/components/ui/badge.tsx", + "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n \"inline-flex items-center justify-center rounded-pill border px-2 py-0.5 text-xs w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n {\n variants: {\n variant: {\n default:\n \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"border-transparent bg-muted text-foreground [a&]:hover:bg-muted/90\",\n destructive:\n \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n outline:\n \"text-foreground [a&]:hover:bg-muted [a&]:hover:text-muted-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n },\n);\n\nfunction Badge({\n className,\n variant,\n asChild = false,\n ...props\n}: React.ComponentProps<\"span\"> &\n VariantProps & { asChild?: boolean }) {\n const Comp = asChild ? Slot : \"span\";\n\n return (\n \n );\n}\n\nexport { Badge, badgeVariants };\n", + "type": "registry:ui", + "target": "components/ui/badge.tsx" + } + ], + "categories": ["display"], + "type": "registry:ui" +} diff --git a/packages/ghost-ui/public/r/breadcrumb.json b/packages/ghost-ui/public/r/breadcrumb.json new file mode 100644 index 0000000..053b770 --- /dev/null +++ b/packages/ghost-ui/public/r/breadcrumb.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "breadcrumb", + "dependencies": ["@radix-ui/react-slot", "lucide-react"], + "registryDependencies": ["utils"], + "files": [ + { + "path": "src/components/ui/breadcrumb.tsx", + "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Breadcrumb({ ...props }: React.ComponentProps<\"nav\">) {\n return