diff --git a/.changeset/corridor-gather.md b/.changeset/corridor-gather.md
new file mode 100644
index 00000000..624cb891
--- /dev/null
+++ b/.changeset/corridor-gather.md
@@ -0,0 +1,14 @@
+---
+"@anarchitecture/ghost": minor
+---
+
+Recompose `gather` on a corridor + hub-and-spoke model and fix a sibling-surface
+context leak. A surface's slice is now: a **spine** of full-body nodes from every
+file on the corridor (the package root down to the surface's own folder — folders
+are walls, so sibling folders never leak in), the **edges** reachable in one hop
+from any spine node's `relates` (so a broad rule authored once high in the tree —
+e.g. `relates: { to: arcade }` on `features/` — reaches every descendant), and a
+set of **spokes**: pointer entries (id + description) for the surface's own
+descendants and any edge hub's subtree, which the agent pulls on demand. The
+`GraphSlice` JSON gains a `spokes` array; graph nodes carry their file `folder`.
+Grounding for `checks`/`review` remains the full-body spine + edges.
diff --git a/.changeset/cross-package-extends.md b/.changeset/cross-package-extends.md
index 0c891863..4f06d699 100644
--- a/.changeset/cross-package-extends.md
+++ b/.changeset/cross-package-extends.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-Add cross-package inheritance via `extends`. A package's `manifest.yml` can declare `extends: { : }`, mapping another contract's identity to where it lives. Node refs then reference inherited context by identity, never path — `under: brand:core` or `relates: [{ to: brand:core-trust }]` (the `:` form replaces the earlier npm-style `#` ref grammar). Inherited nodes load read-only and flow into gather and validate like local ones. `ghost validate` resolves cross-package refs and reports unresolved refs, packages not declared in `extends`, identity mismatches, and cross-package cycles. This delivers the shared-brand story: one brand contract extended by many products, without copy-paste or merge. One level of `extends` in v1 (no transitive); location is an explicit relative dir (identity-based discovery is a future upgrade that keeps refs unchanged).
+Add cross-package inheritance via `extends`. A package's `manifest.yml` can declare `extends: { : }`, mapping another contract's identity to where it lives. Node refs then reference inherited context by identity, never path — `relates: [{ to: brand:core/trust }]` (the `:` form replaces the earlier npm-style `#` ref grammar). Inherited nodes load read-only and flow into gather and validate like local ones. `ghost validate` resolves cross-package refs and reports unresolved refs, packages not declared in `extends`, identity mismatches, and cross-package cycles. This delivers the shared-brand story: one brand contract extended by many products, without copy-paste or merge. One level of `extends` in v1 (no transitive); location is an explicit relative dir (identity-based discovery is a future upgrade that keeps refs unchanged).
diff --git a/.changeset/described-nodes.md b/.changeset/described-nodes.md
index 60fbe52a..7b695163 100644
--- a/.changeset/described-nodes.md
+++ b/.changeset/described-nodes.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-Make `description` a first-class node field — the retrieval payload an agent matches a task against, the way a tool is selected by name + description. `ghost gather` with no argument now lists nodes by id + description (the catalog), built from the graph rather than a separate surface menu. Node frontmatter is now passthrough: free-form descriptive keys (`audience`, `stage`, …) are allowed and ride along untouched. The surface composition-edge vocabulary (`composes`/`governed-by`) is removed — lateral composition lives on node `relates`; `surfaces.yml` is now an optional terse spine file (id + parent + optional description) that folds into the node id space, not a distinct content concept.
+Make `description` a first-class node field — the retrieval payload an agent matches a task against, the way a tool is selected by name + description. `ghost gather` with no argument now lists nodes by id + description (the catalog), built from the graph rather than a separate surface menu. Node frontmatter is now passthrough: free-form descriptive keys (`audience`, `stage`, …) are allowed and ride along untouched. The surface composition-edge vocabulary (`composes`/`governed-by`) is removed — lateral composition lives on node `relates`.
diff --git a/.changeset/directory-tree-nodes.md b/.changeset/directory-tree-nodes.md
new file mode 100644
index 00000000..2b05ddc8
--- /dev/null
+++ b/.changeset/directory-tree-nodes.md
@@ -0,0 +1,18 @@
+---
+"@anarchitecture/ghost": minor
+---
+
+Collapse the on-disk node model into the directory tree: the layout *is* the
+graph. A node's id is its file path (`marketing/email.md` → `marketing/email`)
+and its parent is its containing directory; a surface is just a directory, and a
+directory's own prose lives in its `index.md` (the package-root `index.md` is
+the implicit `core` node). The `surfaces.yml` spine file and the `nodes/`
+directory are removed, along with the node frontmatter `id` and `under` fields —
+identity and containment now come from where a file sits, never from frontmatter
+or a declared spine. Node frontmatter carries descriptive properties only
+(`description`, `relates`, `incarnation`, plus passthrough keys); `relates` and
+cross-package `extends` refs are path ids (`core/trust`, `brand:core/trust`).
+`ghost init` scaffolds `manifest.yml` + a core `index.md`; `ghost migrate`
+writes a directory tree; any `*.md` outside the reserved `checks/` subtree lints
+as a node. Moving a node is a rename — `ghost validate` reports `relates` that no
+longer resolve.
diff --git a/.changeset/docs-node-model-refresh.md b/.changeset/docs-node-model-refresh.md
new file mode 100644
index 00000000..b884f37d
--- /dev/null
+++ b/.changeset/docs-node-model-refresh.md
@@ -0,0 +1,6 @@
+---
+"@anarchitecture/ghost": patch
+---
+
+Refresh the README and docs site onto the current command set (drop the removed
+`lint`/`verify`/`relay`/`describe`/`survey`/`emit` commands).
diff --git a/.changeset/facet-removal.md b/.changeset/facet-removal.md
index 30ccabff..56b0a693 100644
--- a/.changeset/facet-removal.md
+++ b/.changeset/facet-removal.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-Remove the facet model — the graph is now the only fingerprint model. The `intent.yml`/`inventory.yml`/`composition.yml` schemas, the `GhostFingerprintDocument`, the facet→node load-time projection, and the dormant facet slice/grounding are deleted; the loader folds `nodes/*.md` + `surfaces.yml` directly into the graph. `ghost lint` and `ghost verify` are replaced by one `ghost validate` verb (artifact shape pass + node-graph pass: links resolve, one root, acyclic); `ghost emit` is removed. `ghost scan` now reports node/surface contribution instead of facet contribution. Legacy facet packages no longer load directly — `ghost validate`/load fail with guidance to run `ghost migrate`. Structured exemplar-path and evidence verification is dropped (evidence lives in node prose, per the prose-node model).
+Remove the facet model — the graph is now the only fingerprint model. The `intent.yml`/`inventory.yml`/`composition.yml` schemas, the `GhostFingerprintDocument`, the facet→node load-time projection, and the dormant facet slice/grounding are deleted; the loader folds the package's directory tree of prose nodes directly into the graph. `ghost lint` and `ghost verify` are replaced by one `ghost validate` verb (artifact shape pass + node-graph pass: links resolve, one root, acyclic); `ghost emit` is removed. `ghost scan` now reports node/surface contribution instead of facet contribution. Legacy facet packages no longer load directly — `ghost validate`/load fail with guidance to run `ghost migrate`. Structured exemplar-path and evidence verification is dropped (evidence lives in node prose, per the prose-node model).
diff --git a/.changeset/migrate-command.md b/.changeset/migrate-command.md
index a0c7180d..79f18982 100644
--- a/.changeset/migrate-command.md
+++ b/.changeset/migrate-command.md
@@ -2,6 +2,7 @@
"@anarchitecture/ghost": minor
---
-Add `ghost migrate`: transform a legacy `.ghost/` package onto the surface model
-— derive `surfaces.yml` from old `topology.scopes`, place single-scope nodes via
-`surface:`, and report (never guess) any node it cannot place unambiguously.
+Add `ghost migrate`: transform a legacy `.ghost/` package onto the directory-tree
+node model — derive surface directories from old `topology.scopes`, place
+single-scope nodes inside them, and report (never guess) any node it cannot place
+unambiguously.
diff --git a/.changeset/node-authoring.md b/.changeset/node-authoring.md
index d7fa23e9..31f0d4d0 100644
--- a/.changeset/node-authoring.md
+++ b/.changeset/node-authoring.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-`ghost init` now scaffolds a node package (`manifest.yml` + `surfaces.yml` spine + a seed `nodes/*.md`) via a template registry (`--template `, `default` for now) instead of emitting `intent.yml`/`inventory.yml`/`composition.yml`; the `--reference` flag is removed. `ghost migrate` now performs a one-way conversion of legacy/facet packages into `surfaces.yml` + `nodes/*.md` (the facet→node projection becomes the writer) and removes the old facet files. The authoring skill (`capture.md`, `SKILL.md`) teaches node authoring with intent/inventory/composition as authoring lenses rather than facet files.
+`ghost init` now scaffolds a node package (`manifest.yml` + a core `index.md` node) via a template registry (`--template `, `default` for now) instead of emitting `intent.yml`/`inventory.yml`/`composition.yml`; the `--reference` flag is removed. `ghost migrate` now performs a one-way conversion of legacy/facet packages into a directory tree of nodes (the facet→node projection becomes the writer) and removes the old facet files. The authoring skill (`capture.md`, `SKILL.md`) teaches node authoring with intent/inventory/composition as authoring lenses rather than facet files.
diff --git a/.changeset/remove-compare-drift-fleet.md b/.changeset/remove-compare-drift-fleet.md
index d1b61dad..64c47989 100644
--- a/.changeset/remove-compare-drift-fleet.md
+++ b/.changeset/remove-compare-drift-fleet.md
@@ -2,4 +2,4 @@
"@anarchitecture/ghost": minor
---
-Remove `compare`, `drift`, `ack`, `track`, and `diverge` commands and the direct `fingerprint.md` machinery (parser, writer, semantic diff, decisions/dimensions, embeddings, perceptual prior). These rested on a quantified visual-design-system model (fixed dimensions + decision embeddings) that the context-graph reframe abandoned; the concepts are parked for a graph-native rethink (see docs/ideas/compare-drift-fleet-rethink.md). The `./compare` and `./drift` package subpaths and the root `compare`/`drift` exports are removed. `ghost lint` now validates `.ghost/` packages and node/surface/check artifacts only (direct `fingerprint.md` is no longer linted); a `nodes/*.md` file lints as a `ghost.node/v1` node.
+Remove `compare`, `drift`, `ack`, `track`, and `diverge` commands and the direct `fingerprint.md` machinery (parser, writer, semantic diff, decisions/dimensions, embeddings, perceptual prior). These rested on a quantified visual-design-system model (fixed dimensions + decision embeddings) that the context-graph reframe abandoned; the concepts are parked for a graph-native rethink (see docs/ideas/compare-drift-fleet-rethink.md). The `./compare` and `./drift` package subpaths and the root `compare`/`drift` exports are removed. `ghost lint` now validates `.ghost/` packages and node/surface/check artifacts only (direct `fingerprint.md` is no longer linted); a `*.md` node file lints as a `ghost.node/v1` node.
diff --git a/.changeset/skill-bundle-prose.md b/.changeset/skill-bundle-prose.md
new file mode 100644
index 00000000..9404422e
--- /dev/null
+++ b/.changeset/skill-bundle-prose.md
@@ -0,0 +1,6 @@
+---
+"@anarchitecture/ghost": patch
+---
+
+Clean em dashes out of the shipped skill bundle and package README prose,
+rewriting them as plain sentences, colons, or parentheticals.
diff --git a/.changeset/surface-coordinate-space.md b/.changeset/surface-coordinate-space.md
index efbd27a1..582b5fc8 100644
--- a/.changeset/surface-coordinate-space.md
+++ b/.changeset/surface-coordinate-space.md
@@ -2,7 +2,7 @@
"@anarchitecture/ghost": minor
---
-Replace topology/applies_to/surface_type/scope coordinates with a surfaces.yml
-coordinate space and a single `surface:` placement per node. Remove the
+Replace topology/applies_to/surface_type/scope coordinates with a surface
+coordinate space and a single surface placement per node. Remove the
`ghost.map/v1` (`map.md`) coordinate and routing system; checks now route by
`applies_to.paths`.
diff --git a/CLAUDE.md b/CLAUDE.md
index 89dd8174..1e18f898 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,10 +4,12 @@ Agents can assemble UI. What they cannot reliably preserve is the product
surface composition behind that UI: hierarchy, density, restraint, repetition,
trust, flow, and the choices that make a surface feel intentional.
-Ghost keeps that surface composition in a repo-local `.ghost/` fingerprint package. The public npm shape
-is one package, `@anarchitecture/ghost`, with one user-facing bin, `ghost`.
-The CLI validates, computes, compares, and emits deterministic packets. The
-host agent does the interpretive BYOA work through the installed `ghost` skill.
+Ghost keeps that surface composition in a repo-local `.ghost/` fingerprint
+package, a graph of prose nodes. The public npm shape is one package,
+`@anarchitecture/ghost`, with one user-facing bin, `ghost`. The CLI validates
+the node graph, composes context, routes checks, and emits deterministic
+packets. The host agent does the interpretive BYOA work through the installed
+`ghost` skill.
## Build & Run
@@ -29,74 +31,85 @@ pnpm --filter @anarchitecture/ghost exec ghost
## Architecture
Ghost is **BYOA (bring your own agent)**. Claude Code, Codex, Cursor, Goose, or
-another host agent reads, decides, and writes. Ghost is the deterministic
-calculator the agent reaches for: schema validation, repo-signal helpers,
-structural diffs, drift checks, comparison math, and handoff packets.
+another host agent reads, decides, and writes. Ghost grounds that work with two
+things. A **deterministic CLI** does the repeatable parts with no LLM: schema
+and graph validation, repo-signal helpers, context composition, check routing,
+and advisory review packets. An **interpretive skill bundle** teaches the agent
+how to author and use the fingerprint.
-The canonical root `.ghost/` package follows:
+The canonical root `.ghost/` package is a directory tree of prose nodes:
```text
-manifest.yml
-intent.yml
-inventory.yml
-composition.yml
-validate.yml
+manifest.yml # schema + id
+index.md # the core node, true everywhere (optional)
+/index.md # a surface's own prose (the directory is the surface)
+/.md # a prose node placed in that surface
+checks/*.md # optional ghost.check/v1 checks
```
-The three root facet files are the core model:
-
-- `intent.yml` for surface intent.
-- `inventory.yml` for curated material, exemplars, and source links.
-- `composition.yml` for experience patterns.
-
-`validate.yml` validates output through deterministic checks; it is not
-generation input.
-Ordinary Git review is the approval boundary for fingerprint edits.
-
-Legacy `resources.yml`, `map.md`, `survey.json`, and `patterns.yml` may still
-appear in older repos or as migration source material. They are not canonical
-fingerprint input for new Ghost work.
+The **directory tree is the graph**. A node is a markdown file: descriptive
+frontmatter (`description`, `relates`, `incarnation`) plus a prose body. A
+node's identity is its path (`marketing/email.md` → `marketing/email`) and its
+parent is its containing directory. A surface is just a directory, and a
+directory's own prose lives in its `index.md`. The package-root `index.md` is
+the implicit `core` node. The body is written through three authoring lenses
+(they guide what to capture, they are not fields):
+
+- **intent**: the why and the stance.
+- **inventory**: the materials, and pointers to code the agent can inspect.
+- **composition**: the patterns that make the surface feel intentional.
+
+`description` is the retrieval payload; `relates` links nodes laterally;
+`incarnation` tags a medium-bound expression. Reserved at the package root:
+`manifest.yml` and the `checks/` subtree; every other `*.md` is a node. Moving a
+node is a rename. `checks/*.md` validate output, routed by surface; they are not
+generation input. Ordinary Git review is the approval boundary for fingerprint
+edits.
+
+A package may `extend` another by identity (the shared-brand pattern): the
+manifest's `extends` maps a package id to where it lives, and nodes reference
+inherited context by identity (`relates: [{ to: brand:core/trust }]`), never by
+path.
## Packages
| Package | Published? | Description |
| --- | --- | --- |
-| `packages/ghost` | yes: `@anarchitecture/ghost` | Unified public package. Ships the `ghost` CLI, fingerprint package authoring, checks, advisory review packets, comparison, drift stance verbs, and the unified skill bundle. |
+| `packages/ghost` | yes: `@anarchitecture/ghost` | Unified public package. Ships the `ghost` CLI, node authoring, graph validation, check routing, advisory review packets, and the unified skill bundle. |
| `packages/ghost-core` | no | Private historical shared package. Runtime code needed by npm is folded into `packages/ghost/src/ghost-core`. |
| `packages/ghost-fleet` | no | Private fleet view across many Ghost bundles. Consumes workspace exports from `@anarchitecture/ghost`. |
-| `packages/ghost-ui` | no | Reference design system: shadcn registry plus `ghost-mcp` MCP server. |
+| `packages/ghost-ui` | no | Parked. A standalone shadcn component registry plus `ghost-mcp` MCP server, developed in this monorepo. Not coupled to Ghost and not referenced by the docs site or public surfaces. |
| `apps/docs` | no | Docs site. |
## CLI Commands
+Core workflow:
+
+| Command | Description |
+| --- | --- |
+| `ghost init` | Scaffold `.ghost/` with a manifest and a core `index.md` node. |
+| `ghost scan` | Report node and surface contribution. |
+| `ghost validate` | Validate the package: artifact shape and the node graph (links resolve, one root, acyclic). |
+| `ghost gather` | List nodes by id + description, or compose a surface's context slice (own + inherited + edges). |
+| `ghost checks` | Select and ground the markdown checks governing the named surfaces. |
+| `ghost review` | Emit an advisory review packet: touched surfaces, routed checks, fingerprint grounding, and the diff. |
+| `ghost skill install` | Install the unified `ghost` skill bundle. |
+
+Advanced/maintenance:
+
| Command | Description |
| --- | --- |
-| `ghost init` | Create `.ghost/` with manifest, facets, and deterministic checks. |
-| `ghost scan` | Report fingerprint contribution facets and BYOA next-step guidance. |
| `ghost signals` | Emit raw repo signals as JSON for fingerprint authoring. |
-| `ghost lint` | Validate a fingerprint package or single artifact. |
-| `ghost verify` | Validate fingerprint evidence and exemplar paths, and typed check refs. |
-| `ghost describe` | Print direct markdown section ranges. |
-| `ghost diff` | Structural direct-fingerprint prose diff between direct fingerprints. |
-| `ghost survey ` | Legacy survey helpers for optional `ghost.survey/v1` workflows. |
-| `ghost check` | Run active `ghost.validate/v1` deterministic gates against a diff. |
-| `ghost review` | Emit an evidence-routed advisory review packet grounded in fingerprint facets, inventory exemplars, checks, and the diff. |
-| `ghost compare` | Pairwise or composite comparison over packages or direct fingerprints. |
-| `ghost ack` | Record stance toward the tracked fingerprint in `.ghost-sync.json`. |
-| `ghost track` | Shift the tracked fingerprint. |
-| `ghost diverge` | Declare intentional divergence on a dimension. |
-| `ghost emit ` | Emit `review-command`. |
-| `ghost skill install` | Install the unified `ghost` agentskills.io bundle. |
-
-`ghost scan --format json` is deterministic contribution and source-signal state.
-It does not run an LLM.
+| `ghost migrate` | Migrate a legacy `.ghost/` package onto the node-graph surface model. |
+
+`ghost scan --format json` is deterministic contribution state. It does not run
+an LLM.
## Public Exports
- `@anarchitecture/ghost` for the combined surface.
-- `@anarchitecture/ghost/scan` for scan contribution, source signals, and stack discovery.
-- `@anarchitecture/ghost/fingerprint` for fingerprint package authoring, linting, verification, parsing, and serialization.
-- `@anarchitecture/ghost/drift` for check/review/compare/stance helpers.
+- `@anarchitecture/ghost/scan` for scan contribution and source signals.
+- `@anarchitecture/ghost/fingerprint` for node package authoring, validation, parsing, and serialization.
- `@anarchitecture/ghost/core` for shared schemas, types, and loaders.
- `@anarchitecture/ghost/cli` for `buildCli()`.
@@ -132,8 +145,10 @@ Use `patch` for fixes and docs, `minor` for new commands/flags/exports, and
- Keep publishable runtime code self-contained in `packages/ghost`; no
`workspace:*` runtime dependencies in the packed public artifact.
-- The canonical on-disk form is a flat `.ghost/` package.
-- Direct `fingerprint.md` remains only for legacy/direct compare workflows.
+- The canonical on-disk form is a `.ghost/` directory tree: `manifest.yml` plus
+ prose nodes (`index.md` and `/.md`) and optional `checks/*.md`.
+ The directory layout is the graph; ids and parents come from paths, never a
+ spine file.
- Skill recipes live in `packages/ghost/src/skill-bundle/references/`; install
them with `ghost skill install`.
- The CLI manifest at `apps/docs/src/generated/cli-manifest.json` is generated
diff --git a/README.md b/README.md
index 3aa464c0..04bcca46 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,50 @@
# Ghost
-**Ghost captures the composition of a product surface: the intent behind it,
-the materials it draws from, and the patterns that make it feel intentional.**
+**Agents can assemble UI. They can't reliably preserve the _composition_ behind
+it: the hierarchy, density, restraint, copy, trust, and flow that make a
+surface feel intentional.**
-Ghost gives AI agents a checked-in product fingerprint they can read before
-they generate UI and validate after they change it. The public package is
-`@anarchitecture/ghost`, and it installs one CLI: `ghost`.
+Ghost is a checked-in product-surface fingerprint your agent reads before it
+builds and checks after it changes. One package, `@anarchitecture/ghost`. One
+CLI, `ghost`.
-Agents can assemble components. What they need help preserving is the product
-surface behind those components: hierarchy, density, restraint, behavior, copy,
-accessibility, trust, and flow. Ghost keeps that surface composition in a
-portable `.ghost/` package that ordinary Git review can approve.
+[Documentation](https://block.github.io/ghost/) · [npm](https://www.npmjs.com/package/@anarchitecture/ghost) · [Skill](#skill)
## The Shape
-The canonical package is intentionally small:
+A fingerprint is a small folder of prose. The CLI computes; your agent reads,
+writes, and decides.
```text
.ghost/
- manifest.yml # ghost.fingerprint-package/v1 anchor
- intent.yml # surface intent
- inventory.yml # curated material and exemplars
- composition.yml # patterns, flows, states, and arrangements
- validate.yml # optional deterministic gates
+ manifest.yml # ghost.fingerprint-package/v1 anchor: schema + id
+ index.md # the core node, true everywhere (optional)
+ /index.md # a surface's own prose (the directory is the surface)
+ /.md # a prose node placed in that surface
+ checks/*.md # optional ghost.check/v1 checks
```
-A package can be sparse: it contributes whichever facets are locally true. Generation usually uses intent + inventory + composition:
-
-- `intent.yml` says what the surface is trying to do and for whom.
-- `inventory.yml` points agents at materials they can inspect or reuse.
-- `composition.yml` captures the patterns that make those materials feel like
- one intentional product.
-
-`validate.yml`, nested packages, custom host-wrapper package locations, and raw repo
-signals are supporting features. They do not replace curated fingerprint
-facets. `ghost signals` answers what exists; curated fingerprint facets answer
+The fingerprint is a graph of **nodes**, and the **directory tree is the graph**.
+A node is a markdown file: descriptive frontmatter (`description`, `relates`,
+`incarnation`) plus a prose body. A node's identity is its path
+(`marketing/email.md` → `marketing/email`) and its parent is its containing
+directory. A surface is just a directory, and a directory's own prose lives in
+its `index.md`. The package-root `index.md` is the implicit `core` node, true
+everywhere.
+
+The body is written through three authoring lenses. They guide what to capture;
+they are not fields:
+
+- **intent**: what the surface is trying to do and for whom.
+- **inventory**: the materials, and pointers to code the agent can inspect.
+- **composition**: the patterns that make the surface feel intentional.
+
+`description` is the retrieval payload; `relates` links nodes laterally;
+`incarnation` tags a medium-bound expression (essence is untagged). Reserved at
+the package root: `manifest.yml` and the `checks/` subtree; every other `*.md`
+is a node. `ghost signals` answers what exists; the curated node graph answers
what the surface is trying to preserve.
-Older `resources.yml`, `map.md`, `survey.json`, `patterns.yml`, and direct
-`fingerprint.yml` artifacts can still inform migration workflows, but new Ghost
-work should target `.ghost/`.
-
## Project Status: Beta
> [!WARNING]
@@ -58,132 +62,99 @@ work should target `.ghost/`.
```bash
npm install -D @anarchitecture/ghost
npx ghost --help
-npx ghost --help --all
```
-`ghost --help` shows the core workflow. `ghost --help --all` shows the complete
-command index, and each command supports `ghost --help`.
-
-Install the BYOA skill bundle so Codex, Claude, Cursor, Goose, or another host
-agent knows how to author and use the fingerprint:
+## Quick Start
```bash
-npx ghost skill install
-# or choose an explicit destination
-npx ghost skill install --dest ~/.codex/skills/ghost
+ghost init # scaffold .ghost/ with a manifest + a core index.md node
+ghost validate # links resolve, one root, acyclic
+ghost gather # list nodes; ghost gather composes a context slice
```
-Then ask your agent in plain English:
+A node is a markdown file; its id is its path (`checkout/trust.md` →
+`checkout/trust`) and its parent is its directory:
-```text
-Set up the Ghost fingerprint for this repo.
-Brief this work from the Ghost fingerprint.
-Review this PR against the Ghost fingerprint.
-Compare these two Ghost bundles.
-```
-
-## Author A Fingerprint
+```markdown
+---
+description: Trust at the payment moment.
+relates:
+ - to: core/trust
+ as: reinforces
+---
-Ghost authoring is a human-plus-agent workflow. The CLI creates, inspects, and
-validates the package; the host agent interviews, reads the repo, drafts facet
-edits, and asks you to curate the claims.
-
-```bash
-ghost init
-ghost init --package product-surface
-ghost scan --format json
-ghost signals .
-ghost lint .ghost
-ghost lint product-surface
-ghost verify .ghost --root .
+Near the moment of payment, reduce felt risk. Proximity of reassurance to the
+action beats completeness. Never introduce a new visual system here.
```
-Use `--reference` when a reference library should seed inventory, `--scope`
-for nested product areas, or `--package ` when initializing an exact
-package directory such as `product-surface/`.
-For monorepos, `ghost init --monorepo` creates or preserves the root package,
-detects workspace child roots, and prints proposed `ghost init --scope ...`
-commands by default. Run `ghost init --monorepo --apply` to create the detected
-child packages. Host wrappers that need Ghost files somewhere other than
-`.ghost` may set `GHOST_PACKAGE_DIR=` on the child `ghost`
-process. Exact `--package ` values win over the environment default.
-
-Drafted fingerprint edits are just ordinary file changes until Git review
-accepts them. Checked-in Ghost facet files are the Ghost source of truth.
+## Skill
-## Generate From Ghost
-
-Before generating or revising UI, gather the Relay brief for the target path:
+Ghost is **bring-your-own-agent**. Install the skill bundle so Claude Code,
+Codex, Cursor, Goose, or another host agent knows how to author and use the
+fingerprint:
```bash
-ghost relay gather apps/checkout/review/page.tsx
+npx ghost skill install
```
-Relay compiles selected context from the resolved stack as context hits:
-fingerprint refs, why they matched, suggested reads, omissions, and gaps.
-The important shift is timing: Ghost gives agents surface-composition context
-before they build, not only after a review finds drift.
+Then ask in plain English:
-After implementation, run the deterministic and advisory workflows against the
-same fingerprint:
-
-```bash
-ghost check --base main
-ghost review --base main
+```text
+Set up the Ghost fingerprint for this repo.
+Brief this work from the Ghost fingerprint.
+Review this change against the Ghost fingerprint.
```
-`ghost check` runs active `ghost.validate/v1` gates and can fail. `ghost review`
-emits an evidence-routed advisory packet for a human or host adapter to use.
+The skill tells the agent what to read, what to write, and which CLI checks to
+run. The CLI does the deterministic work; the agent does the interpretation.
-## Compare And Govern
-
-Advanced workflows remain available when a repo needs package stacks,
-comparison, or explicit drift stance:
+## The Loop
```bash
-ghost stack apps/checkout/review/page.tsx
-ghost compare market/.ghost dashboard/.ghost
-ghost ack --stance aligned --reason "Initial baseline"
-ghost track new-tracked.fingerprint.md
-ghost diverge typography --reason "Editorial product uses a different type scale"
-ghost emit review-command --path apps/checkout/review/page.tsx
+ghost gather # before: compose the context slice for the work
+ghost checks --surface # route the markdown checks the change touches
+ghost review --surface # after: an advisory packet grounded in the diff
```
-`ghost scan --format json` emits deterministic contribution state for `intent`,
-`inventory`, `composition`, and `validate`. A sparse package can be useful with
-only one contributing facet; absent facets may be inherited from broader stack
-context. It does not call an LLM.
+The shift is timing: Ghost gives agents surface-composition context **before**
+they build, not only after a review finds drift. Checked-in nodes are the source
+of truth; ordinary Git review is the approval boundary for fingerprint edits.
## CLI Commands
| Command | Description |
| --- | --- |
-| `ghost init` | Create `.ghost/` package facet files. |
-| `ghost scan` | Report sparse fingerprint contribution facets. |
-| `ghost lint` | Validate a fingerprint package or individual artifact. |
-| `ghost verify` | Validate evidence paths, exemplar paths, and typed check refs. |
-| `ghost check` | Run active deterministic gates against a diff. |
-| `ghost review` | Emit an evidence-routed advisory packet from fingerprint facets and a diff. |
-| `ghost relay gather` | Gather fingerprint-grounded context for an agent target. |
-| `ghost emit ` | Emit `review-command` artifacts. |
-| `ghost skill install` | Install the unified Ghost skill bundle. |
-| `ghost stack` | Inspect resolved root-to-leaf fingerprint stacks. |
-| `ghost signals` | Emit raw repo signals as JSON for fingerprint authoring. |
-| `ghost describe` | Print markdown section ranges. |
-| `ghost compare` | Compare fingerprint packages. |
-| `ghost ack` / `track` / `diverge` | Record stance toward tracked drift. |
-| `ghost diff` / `survey` | Maintain direct markdown fingerprints or survey/cache files for compatibility workflows. |
+| `ghost init` | Scaffold `.ghost/` with a manifest and a core `index.md` node. |
+| `ghost scan` | Report node and surface contribution. |
+| `ghost validate` | Validate the package: artifact shape and the node graph. |
+| `ghost gather` | List nodes, or compose a surface's context slice. |
+| `ghost checks` | Select and ground the checks a change touches, by surface. |
+| `ghost review` | Emit an advisory review packet grounded in fingerprint + diff. |
+| `ghost skill install` | Install the BYOA skill bundle. |
+| `ghost signals` | Emit raw repo signals as authoring evidence _(advanced)_. |
+| `ghost migrate` | Migrate a legacy `.ghost/` package onto the node model _(maintenance)_. |
+
+Run `ghost --help` for the core workflow, `ghost --help --all` for everything,
+and `ghost --help` for flags.
+
+## Status: Beta
+
+> [!WARNING]
+> Ghost is pre-1.0 and under active development. The CLI, node schema, on-disk
+> `.ghost/` shape, and public exports may change in breaking ways before 1.0.
+> Breaking changes may ship in minor versions while pre-1.0; patch versions are
+> reserved for fixes that should not require migration. Pin the version you
+> depend on and review release notes before upgrading.
## Repo Layout
Ghost is a pnpm monorepo. The public package is self-contained for npm; private
-workspace packages remain development context.
+workspace packages are development context.
| Path | Role | Published? |
| ---- | ---- | --- |
-| [`packages/ghost`](./packages/ghost) | Public package. Ships the `ghost` CLI, folded core runtime, fingerprint package helpers, deterministic checks, advisory review packets, comparison/stance helpers, and the unified skill bundle. | yes: `@anarchitecture/ghost` |
+| [`packages/ghost`](./packages/ghost) | Public package: the `ghost` CLI, folded core runtime, node authoring, checks, advisory review, and the skill bundle. | yes: `@anarchitecture/ghost` |
| [`packages/ghost-fleet`](./packages/ghost-fleet) | Private fleet view across many Ghost bundles. | no |
-| [`packages/ghost-ui`](./packages/ghost-ui) | Reference design system: shadcn registry plus `ghost-mcp` MCP server. | no |
| [`apps/docs`](./apps/docs) | Docs site. | no |
## Development
@@ -194,17 +165,11 @@ pnpm build
pnpm test
pnpm check
pnpm dump:cli-help
-pnpm --filter @anarchitecture/ghost pack
```
No API key is required to run Ghost. `OPENAI_API_KEY` / `VOYAGE_API_KEY` are
optional and only used by semantic embedding helpers when a host opts in.
-## Resources
+## License
-| Resource | Description |
-| --- | --- |
-| [docs/purposes.md](./docs/purposes.md) | What fingerprints are for: one model, many projections. |
-| [docs/ideas/](./docs/ideas) | Live design notes, anchored by `fingerprint-first-architecture.md`. |
-| [GOVERNANCE.md](./GOVERNANCE.md) | Project governance. |
-| [LICENSE](./LICENSE) | Apache License, Version 2.0. |
+[Apache License 2.0](./LICENSE) · [Governance](./GOVERNANCE.md)
diff --git a/apps/docs/src/App.tsx b/apps/docs/src/App.tsx
index cb3515a5..91dfb7cd 100644
--- a/apps/docs/src/App.tsx
+++ b/apps/docs/src/App.tsx
@@ -1,27 +1,15 @@
import { ThemeProvider } from "ghost-ui";
import { useEffect } from "react";
-import { Navigate, Route, Routes, useLocation, useParams } from "react-router";
+import { Navigate, Route, Routes, useLocation } from "react-router";
import DocsIndex from "@/app/docs/page";
import HomePage from "@/app/page";
import GhostDriftLanding from "@/app/tools/drift/page";
import GhostFleetLanding from "@/app/tools/fleet/page";
import ToolsIndex from "@/app/tools/page";
import GhostScanLanding from "@/app/tools/scan/page";
-import GhostUiLanding from "@/app/tools/ui/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 ;
-}
-
function ScrollToHash() {
const { hash, pathname } = useLocation();
@@ -51,7 +39,7 @@ export function App() {
} />
- {/* Tools — four-card index plus per-tool landings */}
+ {/* Tools: four-card index plus per-tool landings */}
} />
} />
} />
} />
- } />
{/* Cross-tool docs hub */}
} />
@@ -74,17 +61,6 @@ export function App() {
{/* MDX-authored doc pages under /docs/* */}
{mdxDocsRoutes()}
- {/* Design Language (ghost-ui catalogue) */}
- } />
- } />
- } />
- }
- />
- } />
- } />
-
{/* Redirects from the previous /tools/drift/{getting-started,cli} URLs */}
}
/>
-
- {/* Redirects from legacy root /foundations and /components URLs */}
- }
- />
- }
- />
- }
- />
- }
- />
- } />
diff --git a/apps/docs/src/app/docs/page.tsx b/apps/docs/src/app/docs/page.tsx
index 2bf96ca0..541383bb 100644
--- a/apps/docs/src/app/docs/page.tsx
+++ b/apps/docs/src/app/docs/page.tsx
@@ -1,7 +1,7 @@
"use client";
import { useStaggerReveal } from "ghost-ui";
-import { BookOpen, Rocket } from "lucide-react";
+import { BookOpen, FileText, Rocket } from "lucide-react";
import type { ReactNode } from "react";
import { Link } from "react-router";
import { AnimatedPageHeader } from "@/components/docs/animated-page-header";
@@ -20,11 +20,18 @@ const sections: {
"Install Ghost, set up the repo fingerprint, and learn the loop around .ghost.",
icon: ,
},
+ {
+ name: "Fingerprint Authoring",
+ href: "/docs/fingerprint-authoring",
+ description:
+ "Co-author nodes through the intent, inventory, and composition lenses, and place them for inheritance.",
+ icon: ,
+ },
{
name: "CLI Reference",
href: "/docs/cli",
description:
- "Commands for checks and comparison, plus the skill recipes your agent runs.",
+ "Every command around the node-graph fingerprint: init, validate, gather, checks, and review.",
icon: ,
},
];
diff --git a/apps/docs/src/app/page.tsx b/apps/docs/src/app/page.tsx
index 12e77918..8783c2a6 100644
--- a/apps/docs/src/app/page.tsx
+++ b/apps/docs/src/app/page.tsx
@@ -29,120 +29,85 @@ export default function Home() {
- Agents can assemble UI. What they cannot reliably preserve is the
- surface composition that UI belongs to.
+ Agents can assemble UI. What they can't reliably preserve is the
+ composition behind it: the hierarchy, density, restraint, copy,
+ trust, and flow that make a surface feel intentional.
- For years, design systems solved a human assembly problem. They
- gave teams shared tokens, components, examples, and usage rules so
- new surfaces could be composed from known parts.
+ Design systems solved a human assembly problem: shared tokens,
+ components, and usage rules so teams could build from known parts.
+ That layer still matters. But agents already recombine those
+ parts. The scarce layer now is the composition that tells them
+ when and how the parts belong.
- That layer still matters, but agents change the scarce layer.
- Models can copy local patterns and recombine components. They do
- not consistently preserve the composition that makes a product
- surface feel intentional: hierarchy, density, restraint, behavior,
- copy, accessibility, trust, and flow.
+ Ghost captures that composition and checks it into the repo, where
+ generation happens. It is a{" "}
+ graph of prose nodes , one
+ markdown file each, that your agent reads before it builds and
+ checks after it changes.
-
- Ghost captures the composition of a product surface: the intent
- behind it, the materials it draws from, and the patterns that make
- it feel intentional.
-
-
- It stores that composition as checked-in fingerprint facets: which
- intent shapes the surface, which materials agents can draw from,
- which situations change the obligation, which patterns hold the
- surface together, and which examples show it at its best.
-
-
- Components, tokens, and libraries become implementation material.
- Ghost does not replace them. It gives agents the surface context
- that tells them when and how those materials belong.
-
-
Ghost keeps that model compact:
- .ghost/ is the default portable fingerprint package
+ .ghost/ is the portable fingerprint package
- intent.yml, inventory.yml, and{" "}
- composition.yml store the three facets
+ the directory tree is the graph: a node's id is
+ its file path, and its parent is its containing directory
- validate.yml stores optional deterministic gates
- grounded in fingerprint refs
+ a surface is just a directory; its own prose lives in that
+ directory's index.md, and the root{" "}
+ index.md is the implicit core node
- ordinary Git review separates draft fingerprint edits from
- checked-in truth
+ each node is written through intent,{" "}
+ inventory, and composition: the why,
+ the materials, the patterns
+
+ checks/*.md validate output; they are never
+ generation input
+
+ ordinary Git review is the approval boundary for edits
- The split is deliberate. intent.yml captures the
- intent behind the surface. inventory.yml captures the
- materials it draws from. composition.yml captures the
- patterns that make it feel intentional. Checks validate output;
- they are not generation input.
+ A node inherits everything in the directories above it. The brand
+ soul lives in the root index.md (the{" "}
+ core node) and reaches every surface;
+ surface-specific nodes refine it; relates links them
+ laterally. Asking for context becomes a graph traversal:{" "}
+ ghost gather <surface> composes the slice that
+ applies.
-
A typical loop becomes:
+
The loop is small:
- Brief from the fingerprint facets and exemplars
- Generate or edit with the host agent
- Run active deterministic checks and advisory review
- Fix code, explain intentional divergence, or update the Ghost
- package through Git
+ Gather the composed context for the surface you're touching
+
+ Generate or edit with your agent
+ Route checks and emit an advisory review against the diff
+
+ Fix code, explain intentional divergence, or update the
+ fingerprint through Git
Ghost stays bring-your-own-agent. The agent reads, decides, and
- writes. Ghost does the repeatable work: initialization, schema
- validation, inventory, evidence verification, checks, advisory
- review packets, comparison, and upstream handoff packets.
-
-
- This is critical because surface composition that cannot be
- recalled or evaluated cannot be delegated. A product surface that
- only its original author can assess is not transferable: to
- agents, to new engineers, or to forks of the product.
-
-
- Drift becomes measurable within this system. When generated or
- modified UI diverges from checked-in fingerprint facets, the
- failure is not just error; it is signal. Drift can originate from:
-
-
- incorrect generation: agent failure
- missing-fingerprint: under-specified surface context
- intentional product evolution
-
-
- Ghost does not eliminate drift; it surfaces and localizes it. The
- system's boundary becomes visible where composition fails.
-
-
- The fingerprint package must live where generation happens: in the
- repository, versioned alongside the code it governs. As the
- product changes, fingerprint edits move through the same ordinary
- Git review that introduces new UI.
-
-
- This leads to a practical governance model. Each repository owns
- its product-surface fingerprint. Advanced workflows can add nested
- packages for product areas, custom fingerprint directories for
- host wrappers, comparison across systems, and declared drift
- stances.
+ writes. Ghost does the repeatable work: scaffolding, schema and
+ graph validation, context composition, check routing, and advisory
+ review packets.
- Across an organization, the collection of Ghost packages forms a
- higher-order map: a distributed model of product-surface
- composition as it is actually practiced, not as it is only
- described.
+ Composition that can't be recalled or evaluated can't be
+ delegated. A surface only its author can assess won't transfer to
+ an agent, a new engineer, or a fork. Ghost makes it transferable,
+ and makes drift measurable: where generated UI diverges from the
+ fingerprint, the gap is signal, and it is localized.
Design systems were libraries for humans. Ghost is composition
- context for agents: every surface can carry the fingerprint it
+ context for agents: every surface carries the fingerprint it
extends, and every deviation can carry evidence.
diff --git a/apps/docs/src/app/tools/page.tsx b/apps/docs/src/app/tools/page.tsx
index ffaefd8f..6fd44c69 100644
--- a/apps/docs/src/app/tools/page.tsx
+++ b/apps/docs/src/app/tools/page.tsx
@@ -1,7 +1,7 @@
"use client";
import { useStaggerReveal } from "ghost-ui";
-import { FileText, Network, Orbit, Palette } from "lucide-react";
+import { FileText, Network, Orbit } from "lucide-react";
import type { ReactNode } from "react";
import { Link } from "react-router";
import { AnimatedPageHeader } from "@/components/docs/animated-page-header";
@@ -31,12 +31,6 @@ const tools: {
blurb: "Compare projects",
icon: ,
},
- {
- name: "ghost-ui",
- href: "/tools/ui",
- blurb: "Reference UI library",
- icon: ,
- },
];
function ToolStrip() {
@@ -49,7 +43,7 @@ function ToolStrip() {
return (
{tools.map((tool) => (
diff --git a/apps/docs/src/app/tools/scan/page.tsx b/apps/docs/src/app/tools/scan/page.tsx
index c4a97f49..6c1dbeb4 100644
--- a/apps/docs/src/app/tools/scan/page.tsx
+++ b/apps/docs/src/app/tools/scan/page.tsx
@@ -22,16 +22,16 @@ const cards: {
},
{
name: "CLI reference",
- href: "/docs/cli#ghost--fingerprint-layers-and-package-checks",
+ href: "/docs/cli",
description:
- "Check fingerprint contribution facets, validate packages, and emit context.",
+ "Report node and surface contribution, validate the graph, and compose context.",
icon:
,
},
{
- name: "Format spec",
- href: "https://github.com/block/ghost/blob/main/docs/fingerprint-format.md",
+ name: "Authoring",
+ href: "/docs/fingerprint-authoring",
description:
- "The full package format for fingerprint intent, inventory, composition, and validation.",
+ "How to write nodes through the intent, inventory, and composition lenses.",
icon:
,
},
];
@@ -48,7 +48,7 @@ export default function GhostScanLanding() {
,
- },
- {
- name: `Components (${componentCount})`,
- href: "/ui/components",
- description:
- "Production-ready primitives + AI elements. Distributed via the shadcn registry.json — installed component-by-component, never wholesale.",
- icon:
,
- },
- {
- name: "MCP server",
- href: "https://github.com/block/ghost/tree/main/packages/ghost-ui#mcp-server",
- description:
- "ghost-mcp re-exposes the registry to AI assistants — five tools, two resources, so an agent can search components and pull source.",
- icon:
,
- },
-];
-
-export default function GhostUiLanding() {
- const ref = useStaggerReveal
(".tool-card", {
- stagger: 0.06,
- y: 30,
- duration: 0.7,
- });
-
- return (
-
-
-
-
- {cards.map((item) => (
-
-
- {item.icon}
-
-
-
- {item.name}
-
-
-
-
- {item.description}
-
-
- ))}
-
-
- );
-}
diff --git a/apps/docs/src/app/ui/components/[name]/page.tsx b/apps/docs/src/app/ui/components/[name]/page.tsx
deleted file mode 100644
index 4291697f..00000000
--- a/apps/docs/src/app/ui/components/[name]/page.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-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
deleted file mode 100644
index 5be57442..00000000
--- a/apps/docs/src/app/ui/components/page.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-"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
deleted file mode 100644
index ddaa3355..00000000
--- a/apps/docs/src/app/ui/foundations/colors/page.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-"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
deleted file mode 100644
index 1f648342..00000000
--- a/apps/docs/src/app/ui/foundations/page.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-"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
deleted file mode 100644
index d6a8f53c..00000000
--- a/apps/docs/src/app/ui/foundations/typography/page.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-"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
deleted file mode 100644
index 686405ef..00000000
--- a/apps/docs/src/app/ui/page.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-"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
deleted file mode 100644
index 0b340865..00000000
--- a/apps/docs/src/components/docs/ai-elements/agent-demo.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-"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
deleted file mode 100644
index ab5105a5..00000000
--- a/apps/docs/src/components/docs/ai-elements/artifact-demo.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-"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 (
-
- {children}
-
- );
-}`}
-
-
-
-
-
-
-
-
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
deleted file mode 100644
index a5caa72c..00000000
--- a/apps/docs/src/components/docs/ai-elements/attachments-demo.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-"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) => (
- {}}>
-
-
-
-
- ))}
-
-
-
-
-
- );
-}
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
deleted file mode 100644
index eb65a58e..00000000
--- a/apps/docs/src/components/docs/ai-elements/audio-player-demo.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-"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
deleted file mode 100644
index 27fc23c1..00000000
--- a/apps/docs/src/components/docs/ai-elements/canvas-demo.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-"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
deleted file mode 100644
index 5238cc15..00000000
--- a/apps/docs/src/components/docs/ai-elements/chain-of-thought-demo.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-"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
deleted file mode 100644
index 5073b3d3..00000000
--- a/apps/docs/src/components/docs/ai-elements/checkpoint-demo.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-"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
deleted file mode 100644
index 9fa3d01f..00000000
--- a/apps/docs/src/components/docs/ai-elements/code-block-demo.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-"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
deleted file mode 100644
index 73cb521c..00000000
--- a/apps/docs/src/components/docs/ai-elements/commit-demo.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-"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
deleted file mode 100644
index 772fc14f..00000000
--- a/apps/docs/src/components/docs/ai-elements/confirmation-demo.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-"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
deleted file mode 100644
index 629c0c58..00000000
--- a/apps/docs/src/components/docs/ai-elements/connection-demo.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-"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
deleted file mode 100644
index 6b11729b..00000000
--- a/apps/docs/src/components/docs/ai-elements/context-demo.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-"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
deleted file mode 100644
index 2d845c1f..00000000
--- a/apps/docs/src/components/docs/ai-elements/controls-demo.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-"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
deleted file mode 100644
index 32a994ac..00000000
--- a/apps/docs/src/components/docs/ai-elements/conversation-demo.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-"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
deleted file mode 100644
index ec292976..00000000
--- a/apps/docs/src/components/docs/ai-elements/edge-demo.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-"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
deleted file mode 100644
index 8c0d8ce4..00000000
--- a/apps/docs/src/components/docs/ai-elements/environment-variables-demo.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-"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
deleted file mode 100644
index c2ce9e49..00000000
--- a/apps/docs/src/components/docs/ai-elements/file-tree-demo.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-"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
deleted file mode 100644
index 6511c174..00000000
--- a/apps/docs/src/components/docs/ai-elements/image-demo.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-"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.
-
-
-
-
-
-
- PNG, landscape aspect ratio
-
-
-
-
-
-
- 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
deleted file mode 100644
index 46e71e02..00000000
--- a/apps/docs/src/components/docs/ai-elements/index.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-"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 (
-
- );
-}
-
-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
deleted file mode 100644
index 32219032..00000000
--- a/apps/docs/src/components/docs/ai-elements/inline-citation-demo.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-"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
deleted file mode 100644
index ffa7d6f9..00000000
--- a/apps/docs/src/components/docs/ai-elements/jsx-preview-demo.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-"use client";
-
-import { JSXPreview, JSXPreviewContent, JSXPreviewError } from "ghost-ui";
-
-const validJsx = `
-
Welcome Back
-
Your dashboard is ready to explore.
-
- View Reports
- Get Started
-
-
`;
-
-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
deleted file mode 100644
index 40d6673e..00000000
--- a/apps/docs/src/components/docs/ai-elements/message-demo.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-"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
deleted file mode 100644
index 06786dd3..00000000
--- a/apps/docs/src/components/docs/ai-elements/mic-selector-demo.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-"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
deleted file mode 100644
index df2e74b1..00000000
--- a/apps/docs/src/components/docs/ai-elements/model-selector-demo.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-"use client";
-
-import {
- Button,
- ModelSelector,
- ModelSelectorContent,
- ModelSelectorEmpty,
- ModelSelectorGroup,
- ModelSelectorInput,
- ModelSelectorItem,
- ModelSelectorList,
- ModelSelectorLogo,
- ModelSelectorLogoGroup,
- ModelSelectorName,
- ModelSelectorTrigger,
-} from "ghost-ui";
-
-export function ModelSelectorDemo() {
- return (
-
-
- Select a model
-
-
-
-
- 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
deleted file mode 100644
index 922f9323..00000000
--- a/apps/docs/src/components/docs/ai-elements/node-demo.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-"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
-
- Run
-
-
-
-
-
-
- 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
deleted file mode 100644
index f5e1c694..00000000
--- a/apps/docs/src/components/docs/ai-elements/open-in-chat-demo.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-"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
deleted file mode 100644
index d6d393dd..00000000
--- a/apps/docs/src/components/docs/ai-elements/package-info-demo.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-"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
deleted file mode 100644
index ec110916..00000000
--- a/apps/docs/src/components/docs/ai-elements/panel-demo.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-"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
deleted file mode 100644
index f5e48571..00000000
--- a/apps/docs/src/components/docs/ai-elements/persona-demo.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-"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) => (
- setCurrentState(s)}
- >
- {s}
-
- ))}
-
-
-
- {variants.map((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
deleted file mode 100644
index ce00bc8d..00000000
--- a/apps/docs/src/components/docs/ai-elements/plan-demo.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-"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
deleted file mode 100644
index 47132fcf..00000000
--- a/apps/docs/src/components/docs/ai-elements/prompt-input-demo.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-"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
deleted file mode 100644
index b6f9b370..00000000
--- a/apps/docs/src/components/docs/ai-elements/queue-demo.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-"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
deleted file mode 100644
index 0ca165bd..00000000
--- a/apps/docs/src/components/docs/ai-elements/reasoning-demo.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-"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
deleted file mode 100644
index 55604449..00000000
--- a/apps/docs/src/components/docs/ai-elements/sandbox-demo.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-"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}
- setCount(c => c + 1)}>
- Increment
-
-
- );
-}`;
-
-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
deleted file mode 100644
index 12c4c54b..00000000
--- a/apps/docs/src/components/docs/ai-elements/schema-display-demo.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-"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
deleted file mode 100644
index 2eb428f5..00000000
--- a/apps/docs/src/components/docs/ai-elements/shimmer-demo.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-"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
deleted file mode 100644
index 56d45b7b..00000000
--- a/apps/docs/src/components/docs/ai-elements/snippet-demo.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-"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
deleted file mode 100644
index 598afb1c..00000000
--- a/apps/docs/src/components/docs/ai-elements/sources-demo.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-"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
deleted file mode 100644
index 9d75873d..00000000
--- a/apps/docs/src/components/docs/ai-elements/speech-input-demo.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-"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
deleted file mode 100644
index b8863d15..00000000
--- a/apps/docs/src/components/docs/ai-elements/stack-trace-demo.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-"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
deleted file mode 100644
index feea6511..00000000
--- a/apps/docs/src/components/docs/ai-elements/suggestion-demo.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-"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
deleted file mode 100644
index fd752437..00000000
--- a/apps/docs/src/components/docs/ai-elements/task-demo.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-"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
deleted file mode 100644
index 6142dca7..00000000
--- a/apps/docs/src/components/docs/ai-elements/terminal-demo.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-"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
deleted file mode 100644
index e3811432..00000000
--- a/apps/docs/src/components/docs/ai-elements/test-results-demo.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-"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
deleted file mode 100644
index db1d9e3d..00000000
--- a/apps/docs/src/components/docs/ai-elements/tool-demo.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-"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
deleted file mode 100644
index 0d6a2cf5..00000000
--- a/apps/docs/src/components/docs/ai-elements/toolbar-demo.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-"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
deleted file mode 100644
index 43c17faf..00000000
--- a/apps/docs/src/components/docs/ai-elements/transcription-demo.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-"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 (
-
-
- setCurrentTime((t) => Math.max(0, t - 2))}
- >
- -2s
-
-
- {currentTime.toFixed(1)}s
-
- setCurrentTime((t) => Math.min(18, t + 2))}
- >
- +2s
-
-
-
-
- {(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
deleted file mode 100644
index a5fe8b04..00000000
--- a/apps/docs/src/components/docs/ai-elements/voice-selector-demo.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-"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.
-
-
-
-
-
- {selected
- ? (voices.find((v) => v.id === selected)?.name ?? "Select voice")
- : "Select a voice..."}
-
-
-
-
-
-
- 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
deleted file mode 100644
index 76c8253b..00000000
--- a/apps/docs/src/components/docs/ai-elements/web-preview-demo.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-"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
deleted file mode 100644
index f7f1c2d4..00000000
--- a/apps/docs/src/components/docs/bento/activity-goal.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-"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.
-
-
-
-
onClick(-10)}
- disabled={amount <= 200}
- >
-
- Decrease
-
-
-
onClick(10)}
- disabled={amount >= 400}
- >
-
- Increase
-
-
-
-
- Pay Now
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/bento/calendar.tsx b/apps/docs/src/components/docs/bento/calendar.tsx
deleted file mode 100644
index e1971caf..00000000
--- a/apps/docs/src/components/docs/bento/calendar.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-"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
deleted file mode 100644
index 85663470..00000000
--- a/apps/docs/src/components/docs/bento/chat.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-"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
-
-
-
-
-
- setOpen(true)}
- >
-
- New message
-
-
- New message
-
-
-
-
-
- {messages.map((message, index) => (
-
- {message.content}
-
- ))}
-
-
-
-
-
-
-
-
-
- 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.
-
- )}
- {
- setOpen(false);
- }}
- >
- Continue
-
-
-
-
- >
- );
-}
diff --git a/apps/docs/src/components/docs/bento/cookie-settings.tsx b/apps/docs/src/components/docs/bento/cookie-settings.tsx
deleted file mode 100644
index 515542a8..00000000
--- a/apps/docs/src/components/docs/bento/cookie-settings.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-"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.
-
-
-
-
- Strictly Necessary
-
- These cookies are essential in order to use the website and use
- its features.
-
-
-
-
-
-
- Functional Cookies
-
- These cookies allow the website to provide personalized
- functionality.
-
-
-
-
-
-
- Performance Cookies
-
- These cookies help to improve the performance of the website.
-
-
-
-
-
-
-
- Save preferences
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/bento/create-account.tsx b/apps/docs/src/components/docs/bento/create-account.tsx
deleted file mode 100644
index 236868cd..00000000
--- a/apps/docs/src/components/docs/bento/create-account.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-"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
-
-
-
-
-
-
- GitHub
-
-
-
- Google
-
-
-
-
-
-
-
-
- Or continue with
-
-
-
-
- Email
-
-
-
- Password
-
-
-
-
- Create account
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/bento/data-table.tsx b/apps/docs/src/components/docs/bento/data-table.tsx
deleted file mode 100644
index 239bd1c7..00000000
--- a/apps/docs/src/components/docs/bento/data-table.tsx
+++ /dev/null
@@ -1,322 +0,0 @@
-"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 (
- column.toggleSorting(column.getIsSorted() === "asc")}
- >
- Email
-
-
- );
- },
- 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 (
-
-
-
- Open menu
-
-
-
-
- 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"
- />
-
-
-
- Columns
-
-
-
- {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.
-
-
- table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- >
- Previous
-
- table.nextPage()}
- disabled={!table.getCanNextPage()}
- >
- Next
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/bento/index.tsx b/apps/docs/src/components/docs/bento/index.tsx
deleted file mode 100644
index bf01514f..00000000
--- a/apps/docs/src/components/docs/bento/index.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-"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
deleted file mode 100644
index 17d3c52f..00000000
--- a/apps/docs/src/components/docs/bento/metric.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-"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
deleted file mode 100644
index d2b84774..00000000
--- a/apps/docs/src/components/docs/bento/payment-amount.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-"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.
-
-
-
-
onClick(-10)}
- disabled={amount <= 200}
- >
-
- Decrease
-
-
-
onClick(10)}
- disabled={amount >= 400}
- >
-
- Increase
-
-
-
-
- Pay Now
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/bento/payment-method.tsx b/apps/docs/src/components/docs/bento/payment-method.tsx
deleted file mode 100644
index 7df3c97e..00000000
--- a/apps/docs/src/components/docs/bento/payment-method.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-"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.
-
-
-
-
-
-
-
-
-
- CashApp
-
-
-
-
- Name
-
-
-
- City
-
-
-
- Card number
-
-
-
-
- Expires
-
-
-
-
-
- January
- February
- March
- April
- May
- June
- July
- August
- September
- October
- November
- December
-
-
-
-
- Year
-
-
-
-
-
- {Array.from({ length: 10 }, (_, i) => (
-
- {new Date().getFullYear() + i}
-
- ))}
-
-
-
-
- CVC
-
-
-
-
-
- Continue
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/bento/report-issue.tsx b/apps/docs/src/components/docs/bento/report-issue.tsx
deleted file mode 100644
index 4cdf6b29..00000000
--- a/apps/docs/src/components/docs/bento/report-issue.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-"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?
-
-
-
-
-
- Area
-
-
-
-
-
- Team
- Billing
- Account
- Deployments
- Support
-
-
-
-
- Security Level
-
-
-
-
-
- Severity 1 (Highest)
- Severity 2
- Severity 3
- Severity 4 (Lowest)
-
-
-
-
-
- Subject
-
-
-
- Description
-
-
-
-
-
- Cancel
-
- Submit
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/bento/share.tsx b/apps/docs/src/components/docs/bento/share.tsx
deleted file mode 100644
index 58569ab6..00000000
--- a/apps/docs/src/components/docs/bento/share.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-"use client";
-
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
- Button,
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
- Input,
- Label,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- Separator,
-} from "ghost-ui";
-
-export function CardsShare() {
- return (
-
-
- Share this document
-
- Anyone with the link can view this document.
-
-
-
-
-
- Link
-
-
- Copy Link
-
-
-
-
- People with access
-
-
-
-
-
- OM
-
-
-
- Olivia Martin
-
-
m@example.com
-
-
-
-
-
-
-
- Can edit
- Can view
-
-
-
-
-
-
- IN
-
-
-
- Isabella Nguyen
-
-
b@example.com
-
-
-
-
-
-
-
- Can edit
- Can view
-
-
-
-
-
-
- SD
-
-
-
- Sofia Davis
-
-
p@example.com
-
-
-
-
-
-
-
- Can edit
- Can view
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/bento/stats.tsx b/apps/docs/src/components/docs/bento/stats.tsx
deleted file mode 100644
index 261808ac..00000000
--- a/apps/docs/src/components/docs/bento/stats.tsx
+++ /dev/null
@@ -1,215 +0,0 @@
-"use client";
-
-import {
- Card,
- CardContent,
- CardHeader,
- CardTitle,
- ChartConfig,
- ChartContainer,
-} from "ghost-ui";
-import { useState } from "react";
-import { Bar, BarChart, Line, LineChart, Tooltip } from "recharts";
-
-const data = [
- {
- revenue: 12500,
- subscription: 320,
- },
- {
- revenue: 15800,
- subscription: 380,
- },
- {
- revenue: 14200,
- subscription: 350,
- },
- {
- revenue: 16800,
- subscription: 420,
- },
- {
- revenue: 19200,
- subscription: 450,
- },
- {
- revenue: 17500,
- subscription: 410,
- },
- {
- revenue: 20300,
- subscription: 480,
- },
- {
- revenue: 22800,
- subscription: 520,
- },
-];
-
-const chartConfig = {
- revenue: {
- label: "Revenue",
- color: "hsl(var(--primary))",
- },
- subscription: {
- label: "Subscriptions",
- color: "hsl(var(--primary))",
- },
-} satisfies ChartConfig;
-
-interface CustomTooltipProps {
- active?: boolean;
- payload?: any[];
- label?: string;
- type?: "revenue" | "subscription";
-}
-
-const CustomTooltip = ({
- active,
- payload,
- label,
- type,
-}: CustomTooltipProps) => {
- if (active && payload && payload.length) {
- return (
-
-
- {type === "revenue" ? "$" : ""}
- {payload[0].value.toLocaleString()}
-
-
- );
- }
- return null;
-};
-
-export function CardsStats() {
- const [activeIndex, setActiveIndex] = useState(null);
-
- const handleMouseEnter = (props: any) => {
- if (props && typeof props.index === "number") {
- setActiveIndex(props.index);
- }
- };
-
- const handleMouseLeave = () => {
- setActiveIndex(null);
- };
-
- return (
-
-
-
- Total Revenue
-
-
-
-
-
- {
- const isActive = activeIndex === props.index;
- return (
-
- );
- }}
- onMouseEnter={handleMouseEnter}
- />
- }
- cursor={false}
- />
-
-
-
-
-
$15,231.89
-
- +20.1% from last month
-
-
-
-
-
-
- Subscriptions
-
-
-
-
-
- handleMouseEnter(data)}
- className={`transition-opacity duration-200 ${
- activeIndex !== null ? "opacity-60" : ""
- }`}
- />
- }
- cursor={{
- fill: "var(--border)",
- opacity: 0.1,
- }}
- />
-
-
-
-
-
+2350
-
- +180.1% from last month
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/bento/team-members.tsx b/apps/docs/src/components/docs/bento/team-members.tsx
deleted file mode 100644
index d2e2a2b9..00000000
--- a/apps/docs/src/components/docs/bento/team-members.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-"use client";
-
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
- Button,
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "ghost-ui";
-import { ChevronDown } from "lucide-react";
-
-export function CardsTeamMembers() {
- return (
-
-
- Team Members
-
- Invite your team members to collaborate.
-
-
-
-
-
-
- OM
-
-
-
Sofia Davis
-
m@example.com
-
-
-
-
-
- Owner
-
-
-
-
-
-
- No roles found.
-
-
- Viewer
-
- Can view and comment.
-
-
-
- Developer
-
- Can view, comment and edit.
-
-
-
- Billing
-
- Can view, comment and manage billing.
-
-
-
- Owner
-
- Admin-level access to all resources.
-
-
-
-
-
-
-
-
-
-
-
- JL
-
-
-
Jackson Lee
-
p@example.com
-
-
-
-
-
- Member
-
-
-
-
-
-
- No roles found.
-
-
- Viewer
-
- Can view and comment.
-
-
-
- Developer
-
- Can view, comment and edit.
-
-
-
- Billing
-
- Can view, comment and manage billing.
-
-
-
- Owner
-
- Admin-level access to all resources.
-
-
-
-
-
-
-
-
-
-
-
- IN
-
-
-
- Isabella Nguyen
-
-
i@example.com
-
-
-
-
-
- Member
-
-
-
-
-
-
- No roles found.
-
-
- Viewer
-
- Can view and comment.
-
-
-
- Developer
-
- Can view, comment and edit.
-
-
-
- Billing
-
- Can view, comment and manage billing.
-
-
-
- Owner
-
- Admin-level access to all resources.
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/component-page-shell.tsx b/apps/docs/src/components/docs/component-page-shell.tsx
deleted file mode 100644
index 73ceaa55..00000000
--- a/apps/docs/src/components/docs/component-page-shell.tsx
+++ /dev/null
@@ -1,577 +0,0 @@
-"use client";
-
-import {
- Button,
- CodeBlock,
- CodeBlockActions,
- CodeBlockCopyButton,
- CodeBlockHeader,
- cn,
- useCopyToClipboard,
-} from "ghost-ui";
-import gsap from "gsap";
-import {
- Check as CheckIcon,
- Copy as CopyIcon,
- Download as DownloadIcon,
- ArrowLeft as NavigationBackIcon,
-} from "lucide-react";
-import { useEffect, useRef, useState } from "react";
-import { Link } from "react-router";
-import { DemoLoader, ExampleLoader } from "@/components/docs/demo-loader";
-import { ComponentErrorBoundary } from "@/components/docs/error-boundary";
-import { SectionWrapper } from "@/components/docs/wrappers";
-import type { ComponentDoc } from "@/lib/component-docs";
-import { ComponentEntry } from "@/lib/component-registry";
-import type { ComponentSpec } from "@/lib/component-source";
-
-// ── Spec Row ──
-
-function SpecRow({
- label,
- children,
- mono = false,
-}: {
- label: string;
- children: React.ReactNode;
- mono?: boolean;
-}) {
- return (
-
-
- {label}
-
-
- {children}
-
-
- );
-}
-
-// ── Variant Table ──
-
-function VariantTable({ variants }: { variants: ComponentSpec["variants"] }) {
- if (variants.length === 0) return null;
-
- return (
-
- {variants.map((v) => (
-
-
- {v.name}
- {v.defaultValue && (
-
- default: {v.defaultValue}
-
- )}
-
-
- {v.values.map((val) => (
-
- {val}
-
- ))}
-
-
- ))}
-
- );
-}
-
-// ── Main Shell ──
-
-export function ComponentPageShell({
- component,
- categoryName,
- demoSource,
- spec,
- prev,
- next,
- docs,
-}: {
- component: ComponentEntry;
- categoryName: string;
- demoSource: string | null;
- spec: ComponentSpec | null;
- prev: { slug: string; name: string } | null;
- next: { slug: string; name: string } | null;
- docs?: ComponentDoc;
-}) {
- const [activeTab, setActiveTab] = useState<"preview" | "source" | "demo">(
- "preview",
- );
- const { copyToClipboard, isCopied } = useCopyToClipboard();
- const shellRef = useRef(null);
-
- useEffect(() => {
- const ctx = gsap.context(() => {
- const tl = gsap.timeline({ defaults: { ease: "expo.out" } });
- const header = shellRef.current?.querySelector(".shell-header");
- const content = shellRef.current?.querySelector(".shell-content");
- if (header) {
- gsap.set(header, { y: 24, opacity: 0 });
- tl.to(header, { y: 0, opacity: 1, duration: 0.6 });
- }
- if (content) {
- gsap.set(content, { y: 16, opacity: 0 });
- tl.to(content, { y: 0, opacity: 1, duration: 0.5 }, "-=0.25");
- }
- }, shellRef);
- return () => ctx.revert();
- }, [component.slug]);
-
- const installCommand = `npx shadcn@latest add --registry https://block.github.io/ghost/r/registry.json ${component.slug}`;
-
- // Filter exports to only component names (capitalized), exclude variant exports
- const componentExports =
- spec?.exports.filter(
- (e) => /^[A-Z]/.test(e) && !e.toLowerCase().includes("variant"),
- ) ?? [];
-
- return (
- <>
- {/* ── Prev / Next (floating sides, xl+) ── */}
- {prev && (
-
-
-
-
-
- {prev.name}
-
-
- )}
- {next && (
-
-
-
-
-
- {next.name}
-
-
- )}
-
-
- {/* ── Header ── */}
-
-
-
-
-
-
-
- {categoryName}
-
-
-
-
-
- {component.name}
-
- {docs?.description && (
-
- {docs.description}
-
- )}
-
-
-
- {/* ── Install ── */}
-
-
-
- {installCommand}
-
- copyToClipboard(installCommand)}
- >
-
-
-
-
-
- {docs?.usage && (
-
-
-
-
- Usage
-
-
-
-
-
-
-
- )}
-
- {/* ── Tabs: Preview / Source / Demo Code ── */}
-
-
- {(
- [
- ["preview", "Preview"],
- ...(spec?.source ? [["source", "Source"]] : []),
- ...(demoSource ? [["demo", "Demo"]] : []),
- ] as [string, string][]
- ).map(([key, label]) => (
- setActiveTab(key as typeof activeTab)}
- className={cn(
- "px-3 py-2 text-sm font-medium transition-colors relative cursor-pointer",
- activeTab === key
- ? "text-foreground"
- : "text-muted-foreground hover:text-foreground",
- )}
- >
- {label}
- {activeTab === key && (
-
- )}
-
- ))}
-
-
-
- {activeTab === "preview" && (
-
-
-
- )}
-
- {activeTab === "source" && spec?.source && (
-
-
-
-
- {spec.filePath}
-
-
-
-
-
-
-
- )}
-
- {activeTab === "demo" && demoSource && (
-
-
-
-
- {component.slug}-demo.tsx
-
-
-
-
-
-
-
- )}
-
-
-
- {/* ── Spec Sheet ── */}
-
-
-
- Specification
-
-
-
-
- {/* Variants */}
- {spec && spec.variants.length > 0 && (
-
-
-
- )}
-
- {/* Sub-components */}
- {componentExports.length > 1 && (
-
-
- {componentExports.map((exp) => (
-
- {exp}
-
- ))}
-
-
- )}
-
- {/* Data slots */}
- {spec && spec.dataSlots.length > 0 && (
-
-
- {spec.dataSlots.map((slot) => (
-
- [data-slot="{slot}"]
-
- ))}
-
-
- )}
-
- {/* Registry dependencies */}
- {component.registryDependencies.length > 0 && (
-
-
- {component.registryDependencies.map((dep) => (
-
- {dep}
-
- ))}
-
-
- )}
-
- {/* npm Dependencies */}
- {component.dependencies.length > 0 && (
-
-
- {component.dependencies.map((dep) => (
-
- {dep}
-
- ))}
-
-
- )}
-
- {/* File path */}
- {spec?.filePath && (
-
-
- {spec.filePath}
-
-
- )}
-
-
-
- {docs && docs.props.length > 0 && (
-
-
-
- Props
-
-
-
- {docs.props.map((prop) => (
-
-
-
- {prop.type}
-
- {prop.default && (
-
- Default:{" "}
- {prop.default}
-
- )}
- {prop.description}
-
-
- ))}
-
-
- )}
-
- {docs && docs.composedWith.length > 0 && (
-
-
-
- Works With
-
-
-
-
- {docs.composedWith.map((slug) => (
-
- {slug}
-
- ))}
-
-
-
- )}
-
- {docs && docs.examples.length > 0 && (
-
-
- Examples
-
- {docs.examples.map((example) => (
-
-
-
{example.title}
- {example.description && (
-
- {example.description}
-
- )}
-
-
-
-
-
-
-
- ))}
-
- )}
-
- {/* ── Prev / Next (bottom, below xl) ── */}
-
- {prev ? (
-
-
-
-
- Previous
-
-
- {prev.name}
-
-
-
- ) : (
-
- )}
- {next ? (
-
-
- Next
-
- {next.name}
-
-
-
-
- ) : (
-
- )}
-
-
- {/* spacer for bottom nav hidden on xl */}
-
-
-
-
- >
- );
-}
diff --git a/apps/docs/src/components/docs/demo-loader.tsx b/apps/docs/src/components/docs/demo-loader.tsx
deleted file mode 100644
index b95c5bc1..00000000
--- a/apps/docs/src/components/docs/demo-loader.tsx
+++ /dev/null
@@ -1,536 +0,0 @@
-"use client";
-
-import { Skeleton } from "ghost-ui";
-import { lazy, Suspense, useMemo } from "react";
-
-const exampleModules = import.meta.glob<{ default: React.ComponentType }>(
- "/src/components/docs/examples/**/*.tsx",
-);
-
-const LoadingSkeleton = () => (
-
-
-
-
-);
-
-export function ExampleLoader({
- componentSlug,
- exampleName,
-}: {
- componentSlug: string;
- exampleName: string;
-}) {
- const path = `/src/components/docs/examples/${componentSlug}/${exampleName}.tsx`;
- const loader = exampleModules[path];
-
- const LazyComponent = useMemo(() => {
- if (!loader) return null;
- return lazy(loader);
- }, [loader]);
-
- if (!LazyComponent) return null;
-
- return (
- }>
-
-
- );
-}
-
-/* eslint-disable @typescript-eslint/no-explicit-any */
-const wrap = (imp: Promise, name: string) =>
- imp.then((m: any) => ({ default: m[name] }));
-
-const demos: Record> = {
- // ── Inputs ──
- button: lazy(() =>
- wrap(import("@/components/docs/primitives/button-demo"), "ButtonDemo"),
- ),
- input: lazy(() =>
- wrap(import("@/components/docs/primitives/input-demo"), "InputDemo"),
- ),
- textarea: lazy(() =>
- wrap(import("@/components/docs/primitives/textarea-demo"), "TextareaDemo"),
- ),
- select: lazy(() =>
- wrap(import("@/components/docs/primitives/select-demo"), "SelectDemo"),
- ),
- checkbox: lazy(() =>
- wrap(import("@/components/docs/primitives/checkbox-demo"), "CheckboxDemo"),
- ),
- "radio-group": lazy(() =>
- wrap(
- import("@/components/docs/primitives/radio-group-demo"),
- "RadioGroupDemo",
- ),
- ),
- slider: lazy(() =>
- wrap(import("@/components/docs/primitives/slider-demo"), "SliderDemo"),
- ),
- switch: lazy(() =>
- wrap(import("@/components/docs/primitives/switch-demo"), "SwitchDemo"),
- ),
- toggle: lazy(() =>
- wrap(import("@/components/docs/primitives/toggle-demo"), "ToggleDemo"),
- ),
- "toggle-group": lazy(() =>
- wrap(
- import("@/components/docs/primitives/toggle-group-demo"),
- "ToggleGroupDemo",
- ),
- ),
- combobox: lazy(() =>
- wrap(import("@/components/docs/primitives/combobox-demo"), "ComboboxDemo"),
- ),
- command: lazy(() =>
- wrap(import("@/components/docs/primitives/command-demo"), "CommandDemo"),
- ),
- "date-picker": lazy(() =>
- wrap(
- import("@/components/docs/primitives/date-picker-demo"),
- "DatePickerDemo",
- ),
- ),
- "input-otp": lazy(() =>
- wrap(import("@/components/docs/primitives/input-otp-demo"), "InputOTPDemo"),
- ),
- form: lazy(() =>
- wrap(import("@/components/docs/primitives/form-demo"), "FormDemo"),
- ),
- forms: lazy(() =>
- wrap(import("@/components/docs/primitives/forms-demo"), "FormsDemo"),
- ),
- label: lazy(() =>
- wrap(import("@/components/docs/primitives/label-demo"), "LabelDemo"),
- ),
-
- // ── Display ──
- card: lazy(() =>
- wrap(import("@/components/docs/primitives/card-demo"), "CardDemo"),
- ),
- badge: lazy(() =>
- wrap(import("@/components/docs/primitives/badge-demo"), "BadgeDemo"),
- ),
- avatar: lazy(() =>
- wrap(import("@/components/docs/primitives/avatar-demo"), "AvatarDemo"),
- ),
- carousel: lazy(() =>
- wrap(import("@/components/docs/primitives/carousel-demo"), "CarouselDemo"),
- ),
- "aspect-ratio": lazy(() =>
- wrap(
- import("@/components/docs/primitives/aspect-ratio-demo"),
- "AspectRatioDemo",
- ),
- ),
- "hover-card": lazy(() =>
- wrap(
- import("@/components/docs/primitives/hover-card-demo"),
- "HoverCardDemo",
- ),
- ),
- skeleton: lazy(() =>
- wrap(import("@/components/docs/primitives/skeleton-demo"), "SkeletonDemo"),
- ),
- separator: lazy(() =>
- wrap(
- import("@/components/docs/primitives/separator-demo"),
- "SeparatorDemo",
- ),
- ),
- image: lazy(() =>
- wrap(import("@/components/docs/ai-elements/image-demo"), "ImageDemo"),
- ),
-
- // ── Feedback ──
- alert: lazy(() =>
- wrap(import("@/components/docs/primitives/alert-demo"), "AlertDemo"),
- ),
- "alert-dialog": lazy(() =>
- wrap(
- import("@/components/docs/primitives/alert-dialog-demo"),
- "AlertDialogDemo",
- ),
- ),
- progress: lazy(() =>
- wrap(import("@/components/docs/primitives/progress-demo"), "ProgressDemo"),
- ),
- sonner: lazy(() =>
- wrap(import("@/components/docs/primitives/sonner-demo"), "SonnerDemo"),
- ),
-
- // ── Data ──
- table: lazy(() =>
- wrap(import("@/components/docs/primitives/table-demo"), "TableDemo"),
- ),
- chart: lazy(() =>
- wrap(import("@/components/docs/primitives/chart-demo"), "ChartDemo"),
- ),
- "chart-area": lazy(() =>
- wrap(
- import("@/components/docs/primitives/chart-area-demo"),
- "ChartAreaDemo",
- ),
- ),
- "chart-banded": lazy(() =>
- wrap(
- import("@/components/docs/primitives/chart-banded-demo"),
- "ChartBandedDemo",
- ),
- ),
- "chart-bar": lazy(() =>
- wrap(import("@/components/docs/primitives/chart-bar-demo"), "ChartBarDemo"),
- ),
- "chart-line": lazy(() =>
- wrap(
- import("@/components/docs/primitives/chart-line-demo"),
- "ChartLineDemo",
- ),
- ),
- "chart-pie": lazy(() =>
- wrap(import("@/components/docs/primitives/chart-pie-demo"), "ChartPieDemo"),
- ),
- "chart-posneg-bar": lazy(() =>
- wrap(
- import("@/components/docs/primitives/chart-posneg-bar-demo"),
- "ChartPosNegBarDemo",
- ),
- ),
- calendar: lazy(() =>
- wrap(import("@/components/docs/primitives/calendar-demo"), "CalendarDemo"),
- ),
-
- // ── Overlay ──
- dialog: lazy(() =>
- wrap(import("@/components/docs/primitives/dialog-demo"), "DialogDemo"),
- ),
- drawer: lazy(() =>
- wrap(import("@/components/docs/primitives/drawer-demo"), "DrawerDemo"),
- ),
- sheet: lazy(() =>
- wrap(import("@/components/docs/primitives/sheet-demo"), "SheetDemo"),
- ),
- popover: lazy(() =>
- wrap(import("@/components/docs/primitives/popover-demo"), "PopoverDemo"),
- ),
- tooltip: lazy(() =>
- wrap(import("@/components/docs/primitives/tooltip-demo"), "TooltipDemo"),
- ),
- "context-menu": lazy(() =>
- wrap(
- import("@/components/docs/primitives/context-menu-demo"),
- "ContextMenuDemo",
- ),
- ),
- "dropdown-menu": lazy(() =>
- wrap(
- import("@/components/docs/primitives/dropdown-menu-demo"),
- "DropdownMenuDemo",
- ),
- ),
- menubar: lazy(() =>
- wrap(import("@/components/docs/primitives/menubar-demo"), "MenubarDemo"),
- ),
-
- // ── Navigation ──
- breadcrumb: lazy(() =>
- wrap(
- import("@/components/docs/primitives/breadcrumb-demo"),
- "BreadcrumbDemo",
- ),
- ),
- tabs: lazy(() =>
- wrap(import("@/components/docs/primitives/tabs-demo"), "TabsDemo"),
- ),
- "navigation-menu": lazy(() =>
- wrap(
- import("@/components/docs/primitives/navigation-menu-demo"),
- "NavigationMenuDemo",
- ),
- ),
- pagination: lazy(() =>
- wrap(
- import("@/components/docs/primitives/pagination-demo"),
- "PaginationDemo",
- ),
- ),
- "scroll-area": lazy(() =>
- wrap(
- import("@/components/docs/primitives/scroll-area-demo"),
- "ScrollAreaDemo",
- ),
- ),
-
- // ── Layout ──
- accordion: lazy(() =>
- wrap(
- import("@/components/docs/primitives/accordion-demo"),
- "AccordionDemo",
- ),
- ),
- collapsible: lazy(() =>
- wrap(
- import("@/components/docs/primitives/collapsible-demo"),
- "CollapsibleDemo",
- ),
- ),
- resizable: lazy(() =>
- wrap(
- import("@/components/docs/primitives/resizable-demo"),
- "ResizableDemo",
- ),
- ),
-
- // ── Chat ──
- attachments: lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/attachments-demo"),
- "AttachmentsDemo",
- ),
- ),
- "chain-of-thought": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/chain-of-thought-demo"),
- "ChainOfThoughtDemo",
- ),
- ),
- checkpoint: lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/checkpoint-demo"),
- "CheckpointDemo",
- ),
- ),
- confirmation: lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/confirmation-demo"),
- "ConfirmationDemo",
- ),
- ),
- context: lazy(() =>
- wrap(import("@/components/docs/ai-elements/context-demo"), "ContextDemo"),
- ),
- conversation: lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/conversation-demo"),
- "ConversationDemo",
- ),
- ),
- "inline-citation": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/inline-citation-demo"),
- "InlineCitationDemo",
- ),
- ),
- message: lazy(() =>
- wrap(import("@/components/docs/ai-elements/message-demo"), "MessageDemo"),
- ),
- "model-selector": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/model-selector-demo"),
- "ModelSelectorDemo",
- ),
- ),
- "open-in-chat": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/open-in-chat-demo"),
- "OpenInChatDemo",
- ),
- ),
- plan: lazy(() =>
- wrap(import("@/components/docs/ai-elements/plan-demo"), "PlanDemo"),
- ),
- "prompt-input": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/prompt-input-demo"),
- "PromptInputDemo",
- ),
- ),
- queue: lazy(() =>
- wrap(import("@/components/docs/ai-elements/queue-demo"), "QueueDemo"),
- ),
- reasoning: lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/reasoning-demo"),
- "ReasoningDemo",
- ),
- ),
- shimmer: lazy(() =>
- wrap(import("@/components/docs/ai-elements/shimmer-demo"), "ShimmerDemo"),
- ),
- sources: lazy(() =>
- wrap(import("@/components/docs/ai-elements/sources-demo"), "SourcesDemo"),
- ),
- suggestion: lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/suggestion-demo"),
- "SuggestionDemo",
- ),
- ),
- task: lazy(() =>
- wrap(import("@/components/docs/ai-elements/task-demo"), "TaskDemo"),
- ),
- tool: lazy(() =>
- wrap(import("@/components/docs/ai-elements/tool-demo"), "ToolDemo"),
- ),
-
- // ── Code ──
- agent: lazy(() =>
- wrap(import("@/components/docs/ai-elements/agent-demo"), "AgentDemo"),
- ),
- artifact: lazy(() =>
- wrap(import("@/components/docs/ai-elements/artifact-demo"), "ArtifactDemo"),
- ),
- "code-block": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/code-block-demo"),
- "CodeBlockDemo",
- ),
- ),
- commit: lazy(() =>
- wrap(import("@/components/docs/ai-elements/commit-demo"), "CommitDemo"),
- ),
- "environment-variables": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/environment-variables-demo"),
- "EnvironmentVariablesDemo",
- ),
- ),
- "file-tree": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/file-tree-demo"),
- "FileTreeDemo",
- ),
- ),
- "jsx-preview": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/jsx-preview-demo"),
- "JsxPreviewDemo",
- ),
- ),
- "package-info": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/package-info-demo"),
- "PackageInfoDemo",
- ),
- ),
- sandbox: lazy(() =>
- wrap(import("@/components/docs/ai-elements/sandbox-demo"), "SandboxDemo"),
- ),
- "schema-display": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/schema-display-demo"),
- "SchemaDisplayDemo",
- ),
- ),
- snippet: lazy(() =>
- wrap(import("@/components/docs/ai-elements/snippet-demo"), "SnippetDemo"),
- ),
- "stack-trace": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/stack-trace-demo"),
- "StackTraceDemo",
- ),
- ),
- terminal: lazy(() =>
- wrap(import("@/components/docs/ai-elements/terminal-demo"), "TerminalDemo"),
- ),
- "test-results": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/test-results-demo"),
- "TestResultsDemo",
- ),
- ),
- "web-preview": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/web-preview-demo"),
- "WebPreviewDemo",
- ),
- ),
-
- // ── Voice ──
- "audio-player": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/audio-player-demo"),
- "AudioPlayerDemo",
- ),
- ),
- "mic-selector": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/mic-selector-demo"),
- "MicSelectorDemo",
- ),
- ),
- persona: lazy(() =>
- wrap(import("@/components/docs/ai-elements/persona-demo"), "PersonaDemo"),
- ),
- "speech-input": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/speech-input-demo"),
- "SpeechInputDemo",
- ),
- ),
- transcription: lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/transcription-demo"),
- "TranscriptionDemo",
- ),
- ),
- "voice-selector": lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/voice-selector-demo"),
- "VoiceSelectorDemo",
- ),
- ),
-
- // ── Workflow ──
- canvas: lazy(() =>
- wrap(import("@/components/docs/ai-elements/canvas-demo"), "CanvasDemo"),
- ),
- connection: lazy(() =>
- wrap(
- import("@/components/docs/ai-elements/connection-demo"),
- "ConnectionDemo",
- ),
- ),
- controls: lazy(() =>
- wrap(import("@/components/docs/ai-elements/controls-demo"), "ControlsDemo"),
- ),
- edge: lazy(() =>
- wrap(import("@/components/docs/ai-elements/edge-demo"), "EdgeDemo"),
- ),
- node: lazy(() =>
- wrap(import("@/components/docs/ai-elements/node-demo"), "NodeDemo"),
- ),
- panel: lazy(() =>
- wrap(import("@/components/docs/ai-elements/panel-demo"), "PanelDemo"),
- ),
- toolbar: lazy(() =>
- wrap(import("@/components/docs/ai-elements/toolbar-demo"), "ToolbarDemo"),
- ),
-};
-
-export function DemoLoader({ name }: { name: string }) {
- const Demo = demos[name];
-
- if (!Demo) {
- return (
-
- Component “{name}” not found
-
- );
- }
-
- return (
-
-
-
-
- }
- >
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/dock.tsx b/apps/docs/src/components/docs/dock.tsx
index a6cae6ee..65138780 100644
--- a/apps/docs/src/components/docs/dock.tsx
+++ b/apps/docs/src/components/docs/dock.tsx
@@ -254,15 +254,6 @@ export function Dock() {
ghost-fleet
- {
- navigate("/tools/ui");
- setSearchOpen(false);
- }}
- >
-
- ghost-ui
-
diff --git a/apps/docs/src/components/docs/examples/code-block/with-diff.tsx b/apps/docs/src/components/docs/examples/code-block/with-diff.tsx
deleted file mode 100644
index c59240e0..00000000
--- a/apps/docs/src/components/docs/examples/code-block/with-diff.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import {
- CodeBlock,
- CodeBlockActions,
- CodeBlockCopyButton,
- CodeBlockHeader,
- CodeBlockLanguageSelector,
- CodeBlockLanguageSelectorContent,
- CodeBlockLanguageSelectorItem,
- CodeBlockLanguageSelectorTrigger,
- CodeBlockLanguageSelectorValue,
-} from "ghost-ui";
-import { useState } from "react";
-
-const snippets: Record<
- string,
- { code: string; language: "typescript" | "python" | "rust" }
-> = {
- typescript: {
- code: `function fibonacci(n: number): number {
- if (n <= 1) return n;
- return fibonacci(n - 1) + fibonacci(n - 2);
-}
-
-console.log(fibonacci(10)); // 55`,
- language: "typescript",
- },
- python: {
- code: `def fibonacci(n: int) -> int:
- if n <= 1:
- return n
- return fibonacci(n - 1) + fibonacci(n - 2)
-
-print(fibonacci(10)) # 55`,
- language: "python",
- },
- rust: {
- code: `fn fibonacci(n: u32) -> u32 {
- if n <= 1 {
- return n;
- }
- fibonacci(n - 1) + fibonacci(n - 2)
-}
-
-fn main() {
- println!("{}", fibonacci(10)); // 55
-}`,
- language: "rust",
- },
-};
-
-export default function CodeBlockWithDiff() {
- const [lang, setLang] = useState("typescript");
- const snippet = snippets[lang];
-
- return (
-
-
-
-
-
-
-
-
-
- TypeScript
-
-
- Python
-
-
- Rust
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/examples/conversation/with-messages.tsx b/apps/docs/src/components/docs/examples/conversation/with-messages.tsx
deleted file mode 100644
index a7f1f2c9..00000000
--- a/apps/docs/src/components/docs/examples/conversation/with-messages.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import {
- Conversation,
- ConversationContent,
- ConversationScrollButton,
- Message,
- MessageContent,
- MessageResponse,
-} from "ghost-ui";
-
-const messages = [
- { id: "1", role: "user" as const, text: "What is TypeScript?" },
- {
- id: "2",
- role: "assistant" as const,
- text: "TypeScript is a strongly-typed superset of JavaScript developed by Microsoft. It adds optional static type checking, interfaces, enums, and other features that help catch errors at compile time rather than at runtime.",
- },
- { id: "3", role: "user" as const, text: "How does it compare to Flow?" },
- {
- id: "4",
- role: "assistant" as const,
- text: "Both TypeScript and Flow add static types to JavaScript, but they differ in key ways:\n\n- **Adoption**: TypeScript has much wider community adoption and tooling support.\n- **Type system**: TypeScript uses a structural type system; Flow also uses structural typing but with some nominal typing features.\n- **Tooling**: TypeScript ships its own compiler (`tsc`), while Flow relies on Babel for compilation.\n- **Ecosystem**: TypeScript has DefinitelyTyped with type definitions for thousands of packages.",
- },
-];
-
-export default function ConversationWithMessages() {
- return (
-
-
-
- {messages.map((msg) => (
-
-
- {msg.role === "assistant" ? (
- {msg.text}
- ) : (
- {msg.text}
- )}
-
-
- ))}
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/examples/message/streaming.tsx b/apps/docs/src/components/docs/examples/message/streaming.tsx
deleted file mode 100644
index eb2f82d8..00000000
--- a/apps/docs/src/components/docs/examples/message/streaming.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Message, MessageContent, MessageResponse, Shimmer } from "ghost-ui";
-
-export default function MessageStreaming() {
- return (
-
-
-
- Explain quantum entanglement in simple terms.
-
-
-
-
-
-
- {`Quantum entanglement is when two particles become linked so that measuring one instantly affects the other, no matter how far apart they are. Einstein called it "spooky action at a distance."`}
-
-
- Generating...
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/examples/message/with-actions.tsx b/apps/docs/src/components/docs/examples/message/with-actions.tsx
deleted file mode 100644
index b6169642..00000000
--- a/apps/docs/src/components/docs/examples/message/with-actions.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import {
- Message,
- MessageAction,
- MessageActions,
- MessageContent,
- MessageResponse,
-} from "ghost-ui";
-import {
- CopyIcon,
- RefreshCwIcon,
- ThumbsDownIcon,
- ThumbsUpIcon,
-} from "lucide-react";
-
-export default function MessageWithActions() {
- return (
-
-
-
- How do I center a div?
-
-
-
-
-
-
- {`You can center a div using **flexbox**:\n\n\`\`\`css\n.parent {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\`\`\`\n\nOr use **grid**:\n\n\`\`\`css\n.parent {\n display: grid;\n place-items: center;\n}\n\`\`\``}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/examples/prompt-input/with-attachments.tsx b/apps/docs/src/components/docs/examples/prompt-input/with-attachments.tsx
deleted file mode 100644
index 549ee0ee..00000000
--- a/apps/docs/src/components/docs/examples/prompt-input/with-attachments.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import {
- PromptInput,
- PromptInputActionAddAttachments,
- PromptInputFooter,
- PromptInputSubmit,
- PromptInputTextarea,
- PromptInputTools,
-} from "ghost-ui";
-
-export default function PromptInputWithAttachments() {
- return (
-
-
{
- console.log("Submitted:", msg);
- }}
- >
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/foundations/colors.tsx b/apps/docs/src/components/docs/foundations/colors.tsx
deleted file mode 100644
index b2e63f06..00000000
--- a/apps/docs/src/components/docs/foundations/colors.tsx
+++ /dev/null
@@ -1,444 +0,0 @@
-export function ColorsDemos() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Semantic Colors */}
-
-
- {/* Border Colors */}
-
-
- {/* Text Colors */}
-
-
- text
-
-
-
-
hello world
-
text default
-
-
-
hello world
-
text alt
-
-
-
hello world
-
text muted
-
-
-
- hello world
-
-
text inverse
-
-
-
hello world
-
text danger
-
-
-
hello world
-
text success
-
-
-
hello world
-
text warning
-
-
-
hello world
-
text info
-
-
-
hello world
-
text accent
-
-
-
-
- {/* Dark Surfaces */}
-
-
- dark surface
-
-
-
-
-
-
- surface dark muted
-
-
-
-
-
-
- surface dark border
-
-
-
-
-
-
- {/* Chart Colors */}
-
-
- {/* Shadows */}
-
-
- {/* Radii */}
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/foundations/typography.tsx b/apps/docs/src/components/docs/foundations/typography.tsx
deleted file mode 100644
index 17c8b743..00000000
--- a/apps/docs/src/components/docs/foundations/typography.tsx
+++ /dev/null
@@ -1,297 +0,0 @@
-export function TypographyDemos() {
- return (
-
- {/* Heading Hierarchy */}
-
-
- Headings
-
-
-
-
- Display
-
-
- Display / 64–96px / Black (900) /-0.05em / 0.88 lh
-
-
-
-
- Section heading
-
-
- Section / 44–64px / Bold (700) /-0.035em / 0.95 lh
-
-
-
-
- Subsection heading
-
-
- Sub / 28–40px / Bold (700) /-0.02em / 1.0 lh
-
-
-
-
- Card heading
-
-
- Card / 20–28px / Semibold (600) /-0.01em / 1.1 lh
-
-
-
-
-
- {/* Labels */}
-
-
- Labels
-
-
-
-
-
- Culture
-
-
- The Future of Open Design
-
-
-
-
- Design · March 2026
-
-
- Systems That Scale
-
-
-
- 11px / Semibold (600) / uppercase / +0.12em tracking
-
-
-
-
-
- {/* Pull Quotes */}
-
-
- Pull Quotes
-
-
-
-
- The magic that happens when you work in the open — co-creation,
- co-celebration.
-
-
- Pull quote / 1.5–2.5rem / Light (300) / -0.02em / 1.3 lh
-
-
-
-
-
- {/* Type Scale */}
-
-
- Type Scale
-
-
-
-
- Open source design: all for one, one for all.
-
-
9XL / Light
-
-
-
- AI will not replace your job. Humans that use AI will.
-
-
8XL / Light
-
-
-
- Leave it cleaner than you found it.
-
-
7XL / Light
-
-
-
- The magic that happens when you work in the open.
-
-
6XL / Light
-
-
-
- Actual open AI. Co-creation, co-celebration.
-
-
5XL / Light
-
-
-
- Work hard smarter, play hard in the grass.
-
-
4XL / Light
-
-
-
- The first rule of open source design is you talk about open source
- design
-
-
3XL / Light
-
-
-
- Open source design: all for one, one for all.
-
-
2XL / Light
-
-
-
- The magic that happens when you work in the open.
-
-
XL / Light
-
-
-
- Leave it cleaner than you found it.
-
-
LG / Light
-
-
-
- Work hard smarter, play hard in the grass.
-
-
BASE / Light
-
-
-
- Actual open AI. Co-creation, co-celebration.
-
-
SM / Light
-
-
-
- AI will not replace your job. Humans that use AI will.
-
-
XS / Light
-
-
-
-
- {/* Font Weights */}
-
-
- Weights
-
-
-
-
The quick brown fox
-
300 / Light
-
-
-
The quick brown fox
-
400 / Regular
-
-
-
The quick brown fox
-
500 / Medium
-
-
-
The quick brown fox
-
600 / Semibold
-
-
-
The quick brown fox
-
700 / Bold
-
-
-
The quick brown fox
-
900 / Black
-
-
-
-
- {/* Font Families */}
-
-
- Families
-
-
-
-
- The quick brown fox jumps over the lazy dog
-
-
font-sans
-
-
-
- The quick brown fox jumps over the lazy dog
-
-
font-display
-
-
-
- Geist Mono — The quick brown fox
-
-
font-mono
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/hero.tsx b/apps/docs/src/components/docs/hero.tsx
index 93bf3a78..4f61766d 100644
--- a/apps/docs/src/components/docs/hero.tsx
+++ b/apps/docs/src/components/docs/hero.tsx
@@ -28,7 +28,7 @@ export function Hero() {
return (
<>
- {/* Concentric circles — fixed backdrop, persists through page scroll */}
+ {/* Concentric circles: fixed backdrop, persists through page scroll */}
{[3, 4, 5].map((i) => {
const size = Math.pow(i, 1.6) * 12;
@@ -61,6 +61,10 @@ export function Hero() {
>
Ghost
+
+ The product-surface fingerprint your agent reads before it builds
+ and checks after it changes.
+
>
diff --git a/apps/docs/src/components/docs/open-in-v0-button.tsx b/apps/docs/src/components/docs/open-in-v0-button.tsx
deleted file mode 100644
index 5f411973..00000000
--- a/apps/docs/src/components/docs/open-in-v0-button.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Button, cn } from "ghost-ui";
-
-export function OpenInV0Button({
- name,
- className,
-}: { name: string } & React.ComponentProps) {
- return (
-
-
- Open in{" "}
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/accordion-demo.tsx b/apps/docs/src/components/docs/primitives/accordion-demo.tsx
deleted file mode 100644
index 3d1a4d93..00000000
--- a/apps/docs/src/components/docs/primitives/accordion-demo.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
-} from "ghost-ui";
-
-export function AccordionDemo() {
- return (
-
-
-
- Is it accessible?
-
- Yes. It adheres to the WAI-ARIA design pattern.
-
-
-
- Is it styled?
-
- Yes. It comes with default styles that matches the other
- components' aesthetic.
-
-
-
- Is it animated?
-
- Yes. It's animated by default, but you can disable it if you
- prefer.
-
-
-
-
-
-
- What are the key considerations when implementing a comprehensive
- enterprise-level authentication system?
-
-
- Implementing a robust enterprise authentication system requires
- careful consideration of multiple factors. This includes secure
- password hashing and storage, multi-factor authentication (MFA)
- implementation, session management, OAuth2 and SSO integration,
- regular security audits, rate limiting to prevent brute force
- attacks, and maintaining detailed audit logs. Additionally,
- you'll need to consider scalability, performance impact, and
- compliance with relevant data protection regulations such as GDPR or
- HIPAA.
-
-
-
-
- How does modern distributed system architecture handle eventual
- consistency and data synchronization across multiple regions?
-
-
- Modern distributed systems employ various strategies to maintain
- data consistency across regions. This often involves using
- techniques like CRDT (Conflict-Free Replicated Data Types), vector
- clocks, and gossip protocols. Systems might implement event sourcing
- patterns, utilize message queues for asynchronous updates, and
- employ sophisticated conflict resolution strategies. Popular
- solutions like Amazon's DynamoDB and Google's Spanner
- demonstrate different approaches to solving these challenges,
- balancing between consistency, availability, and partition tolerance
- as described in the CAP theorem.
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/alert-demo.tsx b/apps/docs/src/components/docs/primitives/alert-demo.tsx
deleted file mode 100644
index 4690fd05..00000000
--- a/apps/docs/src/components/docs/primitives/alert-demo.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { Alert, AlertDescription, AlertTitle, Button } from "ghost-ui";
-import {
- AlertCircleIcon,
- BookmarkCheckIcon,
- CheckCircle2Icon,
- GiftIcon,
- PopcornIcon,
- ShieldAlertIcon,
-} from "lucide-react";
-
-export function AlertDemo() {
- return (
-
-
-
- Success! Your changes have been saved
-
- This is an alert with icon, title and description.
-
-
-
- Heads up!
-
- This one has an icon and a description only. No title.
-
-
-
-
- This one has a description only. No title. No icon.
-
-
-
-
- Let's try one with icon and title.
-
-
-
-
- This is a very long alert title that demonstrates how the component
- handles extended text content and potentially wraps across multiple
- lines
-
-
-
-
-
- This is a very long alert description that demonstrates how the
- component handles extended text content and potentially wraps across
- multiple lines
-
-
-
-
-
- This is an extremely long alert title that spans multiple lines to
- demonstrate how the component handles very lengthy headings while
- maintaining readability and proper text wrapping behavior
-
-
- This is an equally long description that contains detailed information
- about the alert. It shows how the component can accommodate extensive
- content while preserving proper spacing, alignment, and readability
- across different screen sizes and viewport widths. This helps ensure
- the user experience remains consistent regardless of the content
- length.
-
-
-
-
- Something went wrong!
-
- Your session has expired. Please log in again.
-
-
-
-
- Unable to process your payment.
-
- Please verify your billing information and try again.
-
- Check your card details
- Ensure sufficient funds
- Verify billing address
-
-
-
-
-
-
- The selected emails have been marked as spam.
-
-
- Undo
-
-
-
-
- Plot Twist: This Alert is Actually Amber!
-
- This one has custom colors for light and dark mode.
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/alert-dialog-demo.tsx b/apps/docs/src/components/docs/primitives/alert-dialog-demo.tsx
deleted file mode 100644
index a37afac5..00000000
--- a/apps/docs/src/components/docs/primitives/alert-dialog-demo.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
- Button,
-} from "ghost-ui";
-
-export function AlertDialogDemo() {
- return (
-
-
- Show Dialog
-
-
-
- Are you absolutely sure?
-
- This action cannot be undone. This will permanently delete your
- account and remove your data from our servers.
-
-
-
- Cancel
- Continue
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/aspect-ratio-demo.tsx b/apps/docs/src/components/docs/primitives/aspect-ratio-demo.tsx
deleted file mode 100644
index 3d9b838d..00000000
--- a/apps/docs/src/components/docs/primitives/aspect-ratio-demo.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { AspectRatio } from "ghost-ui";
-
-export function AspectRatioDemo() {
- return (
-
-
-
- 16:9
-
-
-
-
- 1:1
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/avatar-demo.tsx b/apps/docs/src/components/docs/primitives/avatar-demo.tsx
deleted file mode 100644
index bd40cc94..00000000
--- a/apps/docs/src/components/docs/primitives/avatar-demo.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { Avatar, AvatarFallback, AvatarImage } from "ghost-ui";
-
-export function AvatarDemo() {
- return (
-
-
-
- NK
-
-
- NK
-
-
-
- NK
-
-
-
- SM
-
-
-
-
- NK
-
-
-
- BA
-
-
-
- SM
-
-
-
-
-
- NK
-
-
-
- BA
-
-
-
- SM
-
-
-
-
-
- NK
-
-
-
- BA
-
-
-
- SM
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/badge-demo.tsx b/apps/docs/src/components/docs/primitives/badge-demo.tsx
deleted file mode 100644
index d4b55b26..00000000
--- a/apps/docs/src/components/docs/primitives/badge-demo.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Badge } from "ghost-ui";
-import { AlertCircleIcon, ArrowRightIcon, CheckIcon } from "lucide-react";
-
-export function BadgeDemo() {
- return (
-
-
-
Badge
-
Secondary
-
Destructive
-
Outline
-
-
- Badge
-
-
-
- Alert
-
-
- 8
-
-
- 99
-
-
- 20+
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/breadcrumb-demo.tsx b/apps/docs/src/components/docs/primitives/breadcrumb-demo.tsx
deleted file mode 100644
index 4ca09c32..00000000
--- a/apps/docs/src/components/docs/primitives/breadcrumb-demo.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import {
- Breadcrumb,
- BreadcrumbEllipsis,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "ghost-ui";
-
-export function BreadcrumbDemo() {
- return (
-
-
-
- Home
-
-
-
-
-
-
- Toggle menu
-
-
- Documentation
- Themes
- GitHub
-
-
-
-
-
- Components
-
-
-
- Breadcrumb
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/button-demo.tsx b/apps/docs/src/components/docs/primitives/button-demo.tsx
deleted file mode 100644
index 2e21d1fa..00000000
--- a/apps/docs/src/components/docs/primitives/button-demo.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { Button } from "ghost-ui";
-import { ArrowRightIcon, Loader2Icon, SendIcon } from "lucide-react";
-
-export function ButtonDemo() {
- return (
-
-
-
Button
-
-
-
-
Outline
-
Ghost
-
-
-
-
Destructive
-
Secondary
-
Link
-
- Send
-
-
-
-
-
- Learn More
-
-
-
- Please wait
-
-
-
-
Small
-
-
-
-
- Outline
-
-
- Ghost
-
-
-
-
-
- Destructive
-
-
- Secondary
-
-
- Link
-
-
- Send
-
-
-
-
-
- Learn More
-
-
-
- Please wait
-
-
-
-
Large
-
-
-
-
- Outline
-
-
- Ghost
-
-
-
-
-
- Destructive
-
-
- Secondary
-
-
- Link
-
-
- Send
-
-
-
-
-
- Learn More
-
-
-
- Please wait
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/calendar-demo.tsx b/apps/docs/src/components/docs/primitives/calendar-demo.tsx
deleted file mode 100644
index 9d4d03d0..00000000
--- a/apps/docs/src/components/docs/primitives/calendar-demo.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-"use client";
-
-import { addDays } from "date-fns";
-import { Calendar } from "ghost-ui";
-import * as React from "react";
-import { type DateRange } from "react-day-picker";
-
-export function CalendarDemo() {
- const [date, setDate] = React.useState(new Date());
- const [dateRange, setDateRange] = React.useState({
- from: new Date(new Date().getFullYear(), 0, 12),
- to: addDays(new Date(new Date().getFullYear(), 0, 12), 30),
- });
- const [range, setRange] = React.useState({
- from: new Date(new Date().getFullYear(), 0, 12),
- to: addDays(new Date(new Date().getFullYear(), 0, 12), 50),
- });
-
- return (
-
-
- date > new Date() || date < new Date("1900-01-01")}
- className="rounded-md border"
- />
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/card-demo.tsx b/apps/docs/src/components/docs/primitives/card-demo.tsx
deleted file mode 100644
index 05974cb0..00000000
--- a/apps/docs/src/components/docs/primitives/card-demo.tsx
+++ /dev/null
@@ -1,187 +0,0 @@
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
- Badge,
- Button,
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- Input,
- Label,
-} from "ghost-ui";
-import { BathIcon, BedIcon, LandPlotIcon } from "lucide-react";
-export function CardDemo() {
- return (
-
-
-
- Login to your account
-
- Enter your email below to login to your account
-
-
-
-
-
-
-
- Login
-
-
- Login with Google
-
-
-
-
-
-
- Meeting Notes
-
- Transcript from the meeting with the client.
-
-
-
-
- Client requested dashboard redesign with focus on mobile
- responsiveness.
-
-
- New analytics widgets for daily/weekly metrics
- Simplified navigation menu
- Dark mode support
- Timeline: 6 weeks
- Follow-up meeting scheduled for next Tuesday
-
-
-
-
-
-
- NK
-
-
-
- BA
-
-
-
- SM
-
-
-
-
-
-
- Is this an image?
- This is a card with an image.
-
-
-
-
-
-
- 4
-
-
- 2
-
-
- 350m²
-
- $135,000
-
-
-
-
- Content Only
-
-
-
- Header Only
-
- This is a card with a header and a description.
-
-
-
-
-
- Header and Content
-
- This is a card with a header and a content.
-
-
- Content
-
-
- Footer Only
-
-
-
- Header + Footer
-
- This is a card with a header and a footer.
-
-
- Footer
-
-
- Content
- Footer
-
-
-
- Header + Footer
-
- This is a card with a header and a footer.
-
-
- Content
- Footer
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/carousel-demo.tsx b/apps/docs/src/components/docs/primitives/carousel-demo.tsx
deleted file mode 100644
index 6402d64e..00000000
--- a/apps/docs/src/components/docs/primitives/carousel-demo.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import {
- Card,
- CardContent,
- Carousel,
- CarouselContent,
- CarouselItem,
- CarouselNext,
- CarouselPrevious,
-} from "ghost-ui";
-import * as React from "react";
-
-export function CarouselDemo() {
- return (
-
-
-
- {Array.from({ length: 5 }).map((_, index) => (
-
-
-
-
-
- {index + 1}
-
-
-
-
-
- ))}
-
-
-
-
-
-
- {Array.from({ length: 5 }).map((_, index) => (
-
-
-
-
-
- {index + 1}
-
-
-
-
-
- ))}
-
-
-
-
-
-
- {Array.from({ length: 5 }).map((_, index) => (
-
-
-
-
-
- {index + 1}
-
-
-
-
-
- ))}
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/chart-area-demo.tsx b/apps/docs/src/components/docs/primitives/chart-area-demo.tsx
deleted file mode 100644
index 45bf4aed..00000000
--- a/apps/docs/src/components/docs/primitives/chart-area-demo.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-"use client";
-
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- ChartConfig,
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
-} from "ghost-ui";
-import { TrendingUp } from "lucide-react";
-import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
-
-export const description = "A simple area chart";
-
-const chartData = [
- { month: "January", desktop: 186 },
- { month: "February", desktop: 305 },
- { month: "March", desktop: 237 },
- { month: "April", desktop: 73 },
- { month: "May", desktop: 209 },
- { month: "June", desktop: 214 },
-];
-
-const chartConfig = {
- desktop: {
- label: "Desktop",
- color: "var(--chart-1)",
- },
-} satisfies ChartConfig;
-
-export function ChartAreaDemo() {
- return (
-
-
- Area Chart
-
- Showing total visitors for the last 6 months
-
-
-
-
-
-
- value.slice(0, 3)}
- />
- }
- />
-
-
-
-
-
-
-
-
- Trending up by 5.2% this month
-
-
- January - June 2024
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/chart-banded-demo.tsx b/apps/docs/src/components/docs/primitives/chart-banded-demo.tsx
deleted file mode 100644
index 47ff8ed4..00000000
--- a/apps/docs/src/components/docs/primitives/chart-banded-demo.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-"use client";
-
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- ChartConfig,
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
-} from "ghost-ui";
-import { TrendingUp } from "lucide-react";
-import { ComposedChart, Line, XAxis, YAxis } from "recharts";
-
-export const description = "A banded chart showing range";
-
-const chartData = [
- { month: "Jan", upper: 86, lower: 23 },
- { month: "Feb", upper: 105, lower: 45 },
- { month: "Mar", upper: 137, lower: 58 },
- { month: "Apr", upper: 173, lower: 68 },
- { month: "May", upper: 109, lower: 45 },
- { month: "Jun", upper: 144, lower: 67 },
-];
-
-const chartConfig = {
- upper: {
- label: "Upper Band",
- color: "var(--chart-1)",
- },
- lower: {
- label: "Lower Band",
- color: "var(--chart-2)",
- },
-} satisfies ChartConfig;
-
-export function ChartBandedDemo() {
- return (
-
-
- Banded Chart
- Showing range variations over time
-
-
-
-
-
-
-
-
- }
- />
-
-
-
-
-
-
-
- Range increased by 12%
-
-
- January - June 2024
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/chart-bar-demo.tsx b/apps/docs/src/components/docs/primitives/chart-bar-demo.tsx
deleted file mode 100644
index 1212bd01..00000000
--- a/apps/docs/src/components/docs/primitives/chart-bar-demo.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-"use client";
-
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- ChartConfig,
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
-} from "ghost-ui";
-import { TrendingUp } from "lucide-react";
-import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
-
-export const description = "A multiple bar chart";
-
-const chartData = [
- { month: "January", desktop: 186, mobile: 80 },
- { month: "February", desktop: 305, mobile: 200 },
- { month: "March", desktop: 237, mobile: 120 },
- { month: "April", desktop: 73, mobile: 190 },
- { month: "May", desktop: 209, mobile: 130 },
- { month: "June", desktop: 214, mobile: 140 },
-];
-
-const chartConfig = {
- desktop: {
- label: "Desktop",
- color: "var(--chart-1)",
- },
- mobile: {
- label: "Mobile",
- color: "var(--chart-2)",
- },
-} satisfies ChartConfig;
-
-export function ChartBarDemo() {
- return (
-
-
- Bar Chart - Multiple
- January - June 2024
-
-
-
-
-
- value.slice(0, 3)}
- />
- }
- />
-
-
-
-
-
-
-
- Trending up by 5.2% this month
-
-
- Showing total visitors for the last 6 months
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/chart-bar-mixed.tsx b/apps/docs/src/components/docs/primitives/chart-bar-mixed.tsx
deleted file mode 100644
index 05f23595..00000000
--- a/apps/docs/src/components/docs/primitives/chart-bar-mixed.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-"use client";
-
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- ChartConfig,
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
-} from "ghost-ui";
-import { TrendingUp } from "lucide-react";
-import { Bar, BarChart, XAxis, YAxis } from "recharts";
-
-export const description = "A mixed bar chart";
-
-const chartData = [
- { browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
- { browser: "safari", visitors: 200, fill: "var(--color-safari)" },
- { browser: "firefox", visitors: 187, fill: "var(--color-firefox)" },
- { browser: "edge", visitors: 173, fill: "var(--color-edge)" },
- { browser: "other", visitors: 90, fill: "var(--color-other)" },
-];
-
-const chartConfig = {
- visitors: {
- label: "Visitors",
- },
- chrome: {
- label: "Chrome",
- color: "var(--chart-1)",
- },
- safari: {
- label: "Safari",
- color: "var(--chart-2)",
- },
- firefox: {
- label: "Firefox",
- color: "var(--chart-3)",
- },
- edge: {
- label: "Edge",
- color: "var(--chart-4)",
- },
- other: {
- label: "Other",
- color: "var(--chart-5)",
- },
-} satisfies ChartConfig;
-
-export function ChartBarMixed() {
- return (
-
-
- Bar Chart - Mixed
- January - June 2024
-
-
-
-
-
- chartConfig[value as keyof typeof chartConfig]?.label
- }
- />
-
- }
- />
-
-
-
-
-
-
- Trending up by 5.2% this month
-
-
- Showing total visitors for the last 6 months
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/chart-demo.tsx b/apps/docs/src/components/docs/primitives/chart-demo.tsx
deleted file mode 100644
index 97dfeec3..00000000
--- a/apps/docs/src/components/docs/primitives/chart-demo.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ChartAreaDemo } from "@/components/docs/primitives/chart-area-demo";
-import { ChartBandedDemo } from "@/components/docs/primitives/chart-banded-demo";
-import { ChartBarDemo } from "@/components/docs/primitives/chart-bar-demo";
-import { ChartBarMixed } from "@/components/docs/primitives/chart-bar-mixed";
-import { ChartLineDemo } from "@/components/docs/primitives/chart-line-demo";
-import { ChartPieDemo } from "@/components/docs/primitives/chart-pie-demo";
-import { ChartPosNegBarDemo } from "@/components/docs/primitives/chart-posneg-bar-demo";
-
-export function ChartDemo() {
- return (
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/chart-line-demo.tsx b/apps/docs/src/components/docs/primitives/chart-line-demo.tsx
deleted file mode 100644
index 8c48a194..00000000
--- a/apps/docs/src/components/docs/primitives/chart-line-demo.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-"use client";
-
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- ChartConfig,
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
-} from "ghost-ui";
-import { TrendingUp } from "lucide-react";
-import { CartesianGrid, Line, LineChart, XAxis } from "recharts";
-
-export const description = "A multiple line chart";
-
-const chartData = [
- { month: "January", desktop: 186, mobile: 80 },
- { month: "February", desktop: 305, mobile: 200 },
- { month: "March", desktop: 237, mobile: 120 },
- { month: "April", desktop: 73, mobile: 190 },
- { month: "May", desktop: 209, mobile: 130 },
- { month: "June", desktop: 214, mobile: 140 },
-];
-
-const chartConfig = {
- desktop: {
- label: "Desktop",
- color: "var(--chart-1)",
- },
- mobile: {
- label: "Mobile",
- color: "var(--chart-2)",
- },
-} satisfies ChartConfig;
-
-export function ChartLineDemo() {
- return (
-
-
- Line Chart - Multiple
- January - June 2024
-
-
-
-
-
- value.slice(0, 3)}
- />
- } />
-
-
-
-
-
-
-
-
-
- Trending up by 5.2% this month
-
-
- Showing total visitors for the last 6 months
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/chart-pie-demo.tsx b/apps/docs/src/components/docs/primitives/chart-pie-demo.tsx
deleted file mode 100644
index da29b969..00000000
--- a/apps/docs/src/components/docs/primitives/chart-pie-demo.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-"use client";
-
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- ChartConfig,
- ChartContainer,
-} from "ghost-ui";
-import { TrendingUp } from "lucide-react";
-import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
-
-export const description = "A pie chart with padding angles and hover effects";
-
-const chartData = [
- { name: "Desktop", value: 45, color: "var(--color-desktop)" },
- { name: "Mobile", value: 35, color: "var(--color-mobile)" },
- { name: "Tablet", value: 20, color: "var(--color-tablet)" },
-];
-
-const chartConfig = {
- desktop: {
- label: "Desktop",
- color: "var(--chart-1)",
- },
- mobile: {
- label: "Mobile",
- color: "var(--chart-2)",
- },
- tablet: {
- label: "Tablet",
- color: "var(--chart-3)",
- },
-} satisfies ChartConfig;
-
-const renderCustomizedLabel = ({
- cx,
- cy,
- midAngle,
- outerRadius,
- payload,
- fill,
- value,
-}: any) => {
- const RADIAN = Math.PI / 180;
- const sin = Math.sin(-RADIAN * midAngle);
- const cos = Math.cos(-RADIAN * midAngle);
-
- // Extend line beyond the outer radius
- const mx = cx + (outerRadius + 15) * cos;
- const my = cy + (outerRadius + 15) * sin;
- const ex = mx + (cos >= 0 ? 1 : -1) * 22;
- const ey = my;
-
- // Text anchor based on which side of the pie we're on
- const textAnchor = cos >= 0 ? "start" : "end";
-
- return (
-
- {/* Label line */}
-
- {/* Label text */}
- = 0 ? 10 : -10)}
- y={ey}
- textAnchor={textAnchor}
- fill="var(--foreground)"
- className="text-xs"
- >
- {payload.name} ({value}%)
-
-
- );
-};
-
-const renderActiveShape = (props: any) => {
- const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } =
- props;
-
- return (
-
- );
-};
-
-export function ChartPieDemo() {
- return (
-
-
- Device Distribution
- Traffic sources by device type
-
-
-
-
-
-
- {chartData.map((entry, index) => (
- |
- ))}
-
-
-
-
-
-
-
- Desktop usage up 5.2%
-
-
- Based on total visits in last 6 months
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/chart-posneg-bar-demo.tsx b/apps/docs/src/components/docs/primitives/chart-posneg-bar-demo.tsx
deleted file mode 100644
index 4686107b..00000000
--- a/apps/docs/src/components/docs/primitives/chart-posneg-bar-demo.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-"use client";
-
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- ChartConfig,
- ChartContainer,
- ChartTooltip,
- ChartTooltipContent,
-} from "ghost-ui";
-import { TrendingUp } from "lucide-react";
-import {
- Bar,
- BarChart,
- CartesianGrid,
- ReferenceLine,
- XAxis,
- YAxis,
-} from "recharts";
-
-export const description = "A bar chart showing positive and negative values";
-
-const chartData = [
- { month: "Jan", value: 8.2 },
- { month: "Feb", value: -4.5 },
- { month: "Mar", value: 6.8 },
- { month: "Apr", value: -3.2 },
- { month: "May", value: 9.4 },
- { month: "Jun", value: -5.1 },
-];
-
-const chartConfig = {
- value: {
- label: "Growth Rate",
- color: "var(--chart-1)",
- },
-} satisfies ChartConfig;
-
-const CustomBar = (props: any) => {
- const { fill, x, y, width, height, value } = props;
- const isNegative = value < 0;
- const barFill = isNegative
- ? "var(--background-danger)"
- : "var(--background-success)";
-
- // For negative values, we want to start from the zero line and go up
- const barHeight = Math.abs(height);
- const barY = isNegative ? y - barHeight : y;
-
- return (
-
-
-
- );
-};
-
-export function ChartPosNegBarDemo() {
- return (
-
-
- Growth Rate
- Monthly growth rate variations
-
-
-
-
-
-
- `${value}%`}
- />
-
- }
- />
- } />
-
-
-
-
-
- Net positive trend
-
-
- Showing monthly growth rate changes
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/checkbox-demo.tsx b/apps/docs/src/components/docs/primitives/checkbox-demo.tsx
deleted file mode 100644
index 489edee1..00000000
--- a/apps/docs/src/components/docs/primitives/checkbox-demo.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-"use client";
-
-import { Checkbox, Label } from "ghost-ui";
-
-export function CheckboxDemo() {
- return (
-
-
-
- Accept terms and conditions
-
-
-
-
-
Accept terms and conditions
-
- By clicking this checkbox, you agree to the terms and conditions.
-
-
-
-
-
- Enable notifications
-
-
-
-
-
Enable notifications
-
- You can enable or disable notifications at any time.
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/collapsible-demo.tsx b/apps/docs/src/components/docs/primitives/collapsible-demo.tsx
deleted file mode 100644
index 74a3c3f2..00000000
--- a/apps/docs/src/components/docs/primitives/collapsible-demo.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-"use client";
-
-import {
- Button,
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from "ghost-ui";
-import { ChevronsUpDown } from "lucide-react";
-import * as React from "react";
-
-export function CollapsibleDemo() {
- const [isOpen, setIsOpen] = React.useState(false);
-
- return (
-
-
-
- @peduarte starred 3 repositories
-
-
-
-
- Toggle
-
-
-
-
- @radix-ui/primitives
-
-
-
- @radix-ui/colors
-
-
- @stitches/react
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/combobox-demo.tsx b/apps/docs/src/components/docs/primitives/combobox-demo.tsx
deleted file mode 100644
index bc28ca0f..00000000
--- a/apps/docs/src/components/docs/primitives/combobox-demo.tsx
+++ /dev/null
@@ -1,400 +0,0 @@
-"use client";
-
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
- Button,
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- CommandSeparator,
- cn,
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "ghost-ui";
-import {
- CheckIcon,
- ChevronDownIcon,
- ChevronsUpDown,
- PlusCircleIcon,
-} from "lucide-react";
-import * as React from "react";
-
-const frameworks = [
- {
- value: "next.js",
- label: "Next.js",
- },
- {
- value: "sveltekit",
- label: "SvelteKit",
- },
- {
- value: "nuxt.js",
- label: "Nuxt.js",
- },
- {
- value: "remix",
- label: "Remix",
- },
- {
- value: "astro",
- label: "Astro",
- },
-];
-
-type Framework = (typeof frameworks)[number];
-
-const users = [
- {
- id: "1",
- username: "nahiyankhan",
- },
- {
- id: "2",
- username: "baxen",
- },
- {
- id: "3",
- username: "spencrmartin",
- },
-] as const;
-
-type User = (typeof users)[number];
-
-const timezones = [
- {
- label: "Americas",
- timezones: [
- { value: "America/New_York", label: "(GMT-5) New York" },
- { value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
- { value: "America/Chicago", label: "(GMT-6) Chicago" },
- { value: "America/Toronto", label: "(GMT-5) Toronto" },
- { value: "America/Vancouver", label: "(GMT-8) Vancouver" },
- { value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" },
- ],
- },
- {
- label: "Europe",
- timezones: [
- { value: "Europe/London", label: "(GMT+0) London" },
- { value: "Europe/Paris", label: "(GMT+1) Paris" },
- { value: "Europe/Berlin", label: "(GMT+1) Berlin" },
- { value: "Europe/Rome", label: "(GMT+1) Rome" },
- { value: "Europe/Madrid", label: "(GMT+1) Madrid" },
- { value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" },
- ],
- },
- {
- label: "Asia/Pacific",
- timezones: [
- { value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
- { value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
- { value: "Asia/Singapore", label: "(GMT+8) Singapore" },
- { value: "Asia/Dubai", label: "(GMT+4) Dubai" },
- { value: "Australia/Sydney", label: "(GMT+11) Sydney" },
- { value: "Asia/Seoul", label: "(GMT+9) Seoul" },
- ],
- },
-] as const;
-
-type Timezone = (typeof timezones)[number];
-
-export function ComboboxDemo() {
- return (
-
-
-
-
-
-
- );
-}
-
-function FrameworkCombobox({ frameworks }: { frameworks: Framework[] }) {
- const [open, setOpen] = React.useState(false);
- const [value, setValue] = React.useState("");
-
- return (
-
-
-
- {value
- ? frameworks.find((framework) => framework.value === value)?.label
- : "Select framework..."}
-
-
-
-
-
-
-
- No framework found.
-
- {frameworks.map((framework) => (
- {
- setValue(currentValue === value ? "" : currentValue);
- setOpen(false);
- }}
- >
- {framework.label}
-
-
- ))}
-
-
-
-
-
- );
-}
-
-function UserCombobox({
- users,
- selectedUserId,
-}: {
- users: User[];
- selectedUserId: string;
-}) {
- const [open, setOpen] = React.useState(false);
- const [value, setValue] = React.useState(selectedUserId);
-
- const selectedUser = React.useMemo(
- () => users.find((user) => user.id === value),
- [value, users],
- );
-
- return (
-
-
-
- {selectedUser ? (
-
-
-
- {selectedUser.username[0]}
-
- {selectedUser.username}
-
- ) : (
- "Select user..."
- )}
-
-
-
-
-
-
-
- No user found.
-
- {users.map((user) => (
- {
- setValue(currentValue === value ? "" : currentValue);
- setOpen(false);
- }}
- >
-
-
- {user.username[0]}
-
- {user.username}
-
-
- ))}
-
-
-
-
-
- Create user
-
-
-
-
-
-
- );
-}
-
-function TimezoneCombobox({
- timezones,
- selectedTimezone,
-}: {
- timezones: Timezone[];
- selectedTimezone: Timezone["timezones"][number];
-}) {
- const [open, setOpen] = React.useState(false);
- const [value, setValue] = React.useState(selectedTimezone.value);
-
- const selectedGroup = React.useMemo(
- () =>
- timezones.find((group) =>
- group.timezones.find((tz) => tz.value === value),
- ),
- [value, timezones],
- );
-
- const selectedTimezoneLabel = React.useMemo(
- () => selectedGroup?.timezones.find((tz) => tz.value === value)?.label,
- [value, selectedGroup],
- );
-
- return (
-
-
-
- {selectedTimezone ? (
-
-
- {selectedGroup?.label}
-
- {selectedTimezoneLabel}
-
- ) : (
- "Select timezone"
- )}
-
-
-
-
-
-
-
- No timezone found.
- {timezones.map((region) => (
-
- {region.timezones.map((timezone) => (
- {
- setValue(
- currentValue as Timezone["timezones"][number]["value"],
- );
- setOpen(false);
- }}
- >
- {timezone.label}
-
-
- ))}
-
- ))}
-
-
-
-
- Create timezone
-
-
-
-
-
-
- );
-}
-
-function ComboboxWithCheckbox({ frameworks }: { frameworks: Framework[] }) {
- const [open, setOpen] = React.useState(false);
- const [selectedFrameworks, setSelectedFrameworks] = React.useState<
- Framework[]
- >([]);
-
- return (
-
-
-
- {selectedFrameworks.length > 0
- ? selectedFrameworks.map((framework) => framework.label).join(", ")
- : "Select frameworks (multi-select)..."}
-
-
-
-
-
-
-
- No framework found.
-
- {frameworks.map((framework) => (
- {
- setSelectedFrameworks(
- selectedFrameworks.some((f) => f.value === currentValue)
- ? selectedFrameworks.filter(
- (f) => f.value !== currentValue,
- )
- : [...selectedFrameworks, framework],
- );
- }}
- >
- f.value === framework.value,
- )}
- >
-
-
- {framework.label}
-
- ))}
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/command-demo.tsx b/apps/docs/src/components/docs/primitives/command-demo.tsx
deleted file mode 100644
index 34021ae0..00000000
--- a/apps/docs/src/components/docs/primitives/command-demo.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-"use client";
-
-import {
- CommandDialog,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- CommandSeparator,
- CommandShortcut,
-} from "ghost-ui";
-import {
- Calculator,
- Calendar,
- CreditCard,
- Settings,
- Smile,
- User,
-} from "lucide-react";
-import * as React from "react";
-
-export function CommandDemo() {
- const [open, setOpen] = React.useState(false);
-
- React.useEffect(() => {
- const down = (e: KeyboardEvent) => {
- if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
- e.preventDefault();
- setOpen((open) => !open);
- }
- };
-
- document.addEventListener("keydown", down);
- return () => document.removeEventListener("keydown", down);
- }, []);
-
- return (
- <>
-
- Press{" "}
-
- ⌘ J
-
-
-
-
-
- No results found.
-
-
-
- Calendar
-
-
-
- Search Emoji
-
-
-
- Calculator
-
-
-
-
-
-
- Profile
- ⌘P
-
-
-
- Billing
- ⌘B
-
-
-
- Settings
- ⌘S
-
-
-
-
- >
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/component-wrapper.tsx b/apps/docs/src/components/docs/primitives/component-wrapper.tsx
deleted file mode 100644
index f6193379..00000000
--- a/apps/docs/src/components/docs/primitives/component-wrapper.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-// pulled from https://github.com/shadcn-ui/ui/blob/main/apps/v4/components/component-wrapper.tsx#L7
-"use client";
-
-import {
- Alert,
- AlertTitle,
- Button,
- cn,
- getComponentName,
- useCopyToClipboard,
-} from "ghost-ui";
-import { Check, Clipboard, Expand, Globe, Hash, Terminal } from "lucide-react";
-import * as React from "react";
-import { ComponentErrorBoundary } from "@/components/docs/error-boundary";
-import { ExternalLink } from "@/components/docs/external-link";
-import { TooltipWrapper } from "@/components/docs/tooltip-wrapper";
-
-export function ComponentWrapper({
- className,
- name,
- children,
- internalUrl,
- showUrl = false,
- ...props
-}: React.ComponentPropsWithoutRef<"div"> & {
- name: string;
- internalUrl?: string;
- showUrl?: boolean;
-}) {
- const { copyToClipboard, isCopied } = useCopyToClipboard();
- const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
- const [isHashCopied, setIsHashCopied] = React.useState(false);
-
- const handleHashCopy = () => {
- const url = `${window.location.origin}${
- window.location.pathname
- }#${name.toLowerCase()}`;
- copyToClipboard(url);
- setIsHashCopied(true);
- setTimeout(() => setIsHashCopied(false), 2000);
- };
-
- React.useEffect(() => {
- // Enable smooth scrolling for anchor links
- document.documentElement.style.scrollBehavior = "smooth";
- return () => {
- document.documentElement.style.scrollBehavior = "";
- };
- }, []);
-
- return (
-
-
-
-
-
-
-
-
- {getComponentName(name)}
-
-
-
-
-
-
-
-
-
- {/*
-
-
-
-
-
- {`npx shadcn@latest add `}
- {name}
-
-
-
-
-
- copyToClipboard(`npx shadcn@latest add ${name}`)
- }
- >
-
-
-
-
-
- {showUrl && (
-
-
- {internalUrl ? (
-
- ) : (
-
- )}
-
-
- )}
-
- */}
-
-
-
- {children}
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/context-menu-demo.tsx b/apps/docs/src/components/docs/primitives/context-menu-demo.tsx
deleted file mode 100644
index f97a1a9f..00000000
--- a/apps/docs/src/components/docs/primitives/context-menu-demo.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import {
- ContextMenu,
- ContextMenuCheckboxItem,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuLabel,
- ContextMenuRadioGroup,
- ContextMenuRadioItem,
- ContextMenuSeparator,
- ContextMenuShortcut,
- ContextMenuSub,
- ContextMenuSubContent,
- ContextMenuSubTrigger,
- ContextMenuTrigger,
-} from "ghost-ui";
-import { Code2Icon, PlusIcon, TrashIcon } from "lucide-react";
-
-export function ContextMenuDemo() {
- return (
-
-
- Right click here
-
-
-
- Back
- ⌘[
-
-
- Forward
- ⌘]
-
-
- Reload
- ⌘R
-
-
- More Tools
-
-
- Save Page...
- ⇧⌘S
-
-
-
- Create Shortcut...
-
- Name Window...
-
-
-
- Developer Tools
-
-
-
-
- Delete
-
-
-
-
-
- Show Bookmarks Bar
- ⌘⇧B
-
- Show Full URLs
-
-
- People
-
- Pedro Duarte
-
- Colm Tuite
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/date-picker-demo.tsx b/apps/docs/src/components/docs/primitives/date-picker-demo.tsx
deleted file mode 100644
index 13066b65..00000000
--- a/apps/docs/src/components/docs/primitives/date-picker-demo.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-"use client";
-
-import { addDays, format } from "date-fns";
-import {
- Button,
- Calendar,
- cn,
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "ghost-ui";
-import { CalendarIcon } from "lucide-react";
-import * as React from "react";
-import { DateRange } from "react-day-picker";
-
-export function DatePickerDemo() {
- return (
-
-
-
-
- );
-}
-
-function DatePickerSimple() {
- const [date, setDate] = React.useState();
-
- return (
-
-
-
-
- {date ? format(date, "PPP") : Pick a date }
-
-
-
-
-
-
- );
-}
-
-function DatePickerWithRange() {
- const [date, setDate] = React.useState({
- from: new Date(new Date().getFullYear(), 0, 20),
- to: addDays(new Date(new Date().getFullYear(), 0, 20), 20),
- });
-
- return (
-
-
-
-
- {date?.from ? (
- date.to ? (
- <>
- {format(date.from, "LLL dd, y")} -{" "}
- {format(date.to, "LLL dd, y")}
- >
- ) : (
- format(date.from, "LLL dd, y")
- )
- ) : (
- Pick a date
- )}
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/dialog-demo.tsx b/apps/docs/src/components/docs/primitives/dialog-demo.tsx
deleted file mode 100644
index d3bc7a4b..00000000
--- a/apps/docs/src/components/docs/primitives/dialog-demo.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import {
- Button,
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
- Input,
- Label,
-} from "ghost-ui";
-
-export function DialogDemo() {
- return (
-
-
-
-
-
- );
-}
-
-function DialogWithForm() {
- return (
-
-
-
- );
-}
-
-function DialogScrollableContent() {
- return (
-
-
- Scrollable Content
-
-
-
- Scrollable Content
-
- This is a dialog with scrollable content.
-
-
-
-
- Lorem Ipsum
-
- {Array.from({ length: 10 }).map((_, index) => (
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
- eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
- enim ad minim veniam, quis nostrud exercitation ullamco laboris
- nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
- reprehenderit in voluptate velit esse cillum dolore eu fugiat
- nulla pariatur. Excepteur sint occaecat cupidatat non proident,
- sunt in culpa qui officia deserunt mollit anim id est laborum.
-
- ))}
-
-
-
- );
-}
-
-function DialogWithStickyFooter() {
- return (
-
-
- Sticky Footer
-
-
-
- Scrollable Content
-
- This is a dialog with scrollable content.
-
-
-
-
- Lorem Ipsum
-
- {Array.from({ length: 10 }).map((_, index) => (
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
- eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
- enim ad minim veniam, quis nostrud exercitation ullamco laboris
- nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
- reprehenderit in voluptate velit esse cillum dolore eu fugiat
- nulla pariatur. Excepteur sint occaecat cupidatat non proident,
- sunt in culpa qui officia deserunt mollit anim id est laborum.
-
- ))}
-
-
-
- Close
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/drawer-demo.tsx b/apps/docs/src/components/docs/primitives/drawer-demo.tsx
deleted file mode 100644
index 530deefd..00000000
--- a/apps/docs/src/components/docs/primitives/drawer-demo.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-"use client";
-
-import {
- Button,
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from "ghost-ui";
-import { Minus, Plus } from "lucide-react";
-import * as React from "react";
-import { Bar, BarChart, ResponsiveContainer } from "recharts";
-
-const data = [
- {
- goal: 400,
- },
- {
- goal: 300,
- },
- {
- goal: 200,
- },
- {
- goal: 300,
- },
- {
- goal: 200,
- },
- {
- goal: 278,
- },
- {
- goal: 189,
- },
- {
- goal: 239,
- },
- {
- goal: 300,
- },
- {
- goal: 200,
- },
- {
- goal: 278,
- },
- {
- goal: 189,
- },
- {
- goal: 349,
- },
-];
-
-export function DrawerDemo() {
- return (
-
-
-
-
-
- );
-}
-
-function DrawerBottom() {
- const [goal, setGoal] = React.useState(350);
-
- const onClick = React.useCallback((adjustment: number) => {
- setGoal((prevGoal) => Math.max(200, Math.min(400, prevGoal + adjustment)));
- }, []);
-
- return (
-
-
- Open Drawer
-
-
-
-
- Payment Amount
- Set your payment amount.
-
-
-
-
onClick(-10)}
- disabled={goal <= 200}
- >
-
- Decrease
-
-
-
onClick(10)}
- disabled={goal >= 400}
- >
-
- Increase
-
-
-
-
- Pay
-
- Cancel
-
-
-
-
-
- );
-}
-
-function DrawerScrollableContent() {
- return (
-
-
- Scrollable Content
-
-
-
- Payment Amount
- Set your payment amount.
-
-
-
- Lorem Ipsum
-
- {Array.from({ length: 10 }).map((_, index) => (
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do
- eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
- enim ad minim veniam, quis nostrud exercitation ullamco laboris
- nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
- reprehenderit in voluptate velit esse cillum dolore eu fugiat
- nulla pariatur. Excepteur sint occaecat cupidatat non proident,
- sunt in culpa qui officia deserunt mollit anim id est laborum.
-
- ))}
-
-
- Submit
-
- Cancel
-
-
-
-
- );
-}
-
-const directions = ["top", "right", "bottom", "left"] as const;
-
-function DrawerDirections() {
- return (
- <>
- {directions.map((direction) => (
-
-
-
- {direction}
-
-
-
-
- Move Goal
-
- Set your daily activity goal.
-
-
-
- {Array.from({ length: 10 }).map((_, index) => (
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
- do eiusmod tempor incididunt ut labore et dolore magna aliqua.
- Ut enim ad minim veniam, quis nostrud exercitation ullamco
- laboris nisi ut aliquip ex ea commodo consequat. Duis aute
- irure dolor in reprehenderit in voluptate velit esse cillum
- dolore eu fugiat nulla pariatur. Excepteur sint occaecat
- cupidatat non proident, sunt in culpa qui officia deserunt
- mollit anim id est laborum.
-
- ))}
-
-
- Submit
-
- Cancel
-
-
-
-
- ))}
- >
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/dropdown-menu-demo.tsx b/apps/docs/src/components/docs/primitives/dropdown-menu-demo.tsx
deleted file mode 100644
index 97b56b4f..00000000
--- a/apps/docs/src/components/docs/primitives/dropdown-menu-demo.tsx
+++ /dev/null
@@ -1,369 +0,0 @@
-"use client";
-
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
- Button,
- DropdownMenu,
- DropdownMenuCheckboxItem,
- DropdownMenuContent,
- DropdownMenuGroup,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuPortal,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
-} from "ghost-ui";
-import {
- BadgeCheckIcon,
- BellIcon,
- ChevronsUpDownIcon,
- CreditCardIcon,
- LogOut,
- LogOutIcon,
- MoreHorizontalIcon,
- PencilIcon,
- Settings2Icon,
- ShareIcon,
- SparklesIcon,
- TrashIcon,
- UserIcon,
-} from "lucide-react";
-import * as React from "react";
-
-export function DropdownMenuDemo() {
- return (
-
-
-
-
-
-
-
-
- );
-}
-
-function DropdownMenuSimple() {
- return (
-
-
- Open
-
-
- My Account
-
-
- Profile
- ⇧⌘P
-
-
- Billing
- ⌘B
-
-
- Settings
- ⌘S
-
-
- Keyboard shortcuts
- ⌘K
-
-
-
-
- Team
-
- Invite users
-
-
- Email
- Message
-
- More...
-
-
-
-
- New Team
- ⌘+T
-
-
-
- GitHub
- Support
- API
-
-
- Log out
- ⇧⌘Q
-
-
-
- );
-}
-
-function DropdownMenuCheckboxes() {
- const [showStatusBar, setShowStatusBar] = React.useState(true);
- const [showActivityBar, setShowActivityBar] = React.useState(false);
- const [showPanel, setShowPanel] = React.useState(false);
-
- return (
-
-
- Checkboxes
-
-
-
- Account
-
- Profile
-
-
- Billing
-
-
- Settings
-
-
-
-
- Appearance
-
- Status Bar
-
-
- Activity Bar
-
-
- Panel
-
-
-
-
-
- Sign Out
-
-
-
-
- );
-}
-
-function DropdownMenuRadioGroupDemo() {
- const [position, setPosition] = React.useState("bottom");
-
- return (
-
-
- Radio Group
-
-
- Panel Position
-
-
- Top
- Bottom
-
- Right
-
-
-
-
-
- );
-}
-
-function DropdownMenuWithAvatar() {
- return (
-
-
-
-
-
- NK
-
-
- nahiyankhan
-
- nahiyankhan@example.com
-
-
-
-
-
-
-
-
-
-
- NK
-
-
- nahiyankhan
-
- nahiyankhan@example.com
-
-
-
-
-
-
-
-
- Upgrade to Pro
-
-
-
-
-
-
- Account
-
-
-
- Billing
-
-
-
- Notifications
-
-
-
-
-
- Sign Out
-
-
-
- );
-}
-
-function DropdownMenuAvatarOnly() {
- return (
-
-
-
-
-
- NK
-
-
-
-
-
-
-
-
- NK
-
-
- nahiyankhan
-
- nahiyankhan@example.com
-
-
-
-
-
-
-
-
- Upgrade to Pro
-
-
-
-
-
-
- Account
-
-
-
- Billing
-
-
-
- Notifications
-
-
-
-
-
- Sign Out
-
-
-
- );
-}
-
-function DropdownMenuIconColor() {
- return (
-
-
-
-
- Toggle menu
-
-
-
-
-
-
- Edit
-
-
-
- Share
-
-
-
-
- Delete
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/form-demo.tsx b/apps/docs/src/components/docs/primitives/form-demo.tsx
deleted file mode 100644
index 56b5895c..00000000
--- a/apps/docs/src/components/docs/primitives/form-demo.tsx
+++ /dev/null
@@ -1,419 +0,0 @@
-"use client";
-
-import { zodResolver } from "@hookform/resolvers/zod";
-import { format } from "date-fns";
-import {
- Button,
- Calendar,
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
- Checkbox,
- cn,
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- Input,
- Popover,
- PopoverContent,
- PopoverTrigger,
- RadioGroup,
- RadioGroupItem,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- Switch,
- Textarea,
-} from "ghost-ui";
-import { CalendarIcon } from "lucide-react";
-import { useForm } from "react-hook-form";
-import { toast } from "sonner";
-import { z } from "zod";
-
-const items = [
- {
- id: "recents",
- label: "Recents",
- },
- {
- id: "home",
- label: "Home",
- },
- {
- id: "applications",
- label: "Applications",
- },
- {
- id: "desktop",
- label: "Desktop",
- },
- {
- id: "downloads",
- label: "Downloads",
- },
- {
- id: "documents",
- label: "Documents",
- },
-] as const;
-
-const FormSchema = z.object({
- username: z.string().min(2, {
- message: "Username must be at least 2 characters.",
- }),
- bio: z
- .string()
- .min(10, {
- message: "Bio must be at least 10 characters.",
- })
- .max(160, {
- message: "Bio must not be longer than 30 characters.",
- }),
- email: z
- .string({
- error: "Please select an email to display.",
- })
- .email(),
- type: z.enum(["all", "mentions", "none"], {
- error: "You need to select a notification type.",
- }),
- mobile: z.boolean().default(false).optional(),
- items: z.array(z.string()).refine((value) => value.some((item) => item), {
- message: "You have to select at least one item.",
- }),
- dob: z.date({
- error: "A date of birth is required.",
- }),
- marketing_emails: z.boolean().default(false).optional(),
- security_emails: z.boolean(),
-});
-
-export function FormDemo() {
- const form = useForm>({
- resolver: zodResolver(FormSchema),
- defaultValues: {
- username: "",
- items: ["recents", "home"],
- },
- });
-
- function onSubmit(data: z.infer) {
- toast("You submitted the following values:", {
- description: (
-
- {JSON.stringify(data, null, 2)}
-
- ),
- });
- }
-
- return (
-
-
- Example
-
- See some of the form building blocks.
-
-
-
-
- {" "}
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/forms-demo.tsx b/apps/docs/src/components/docs/primitives/forms-demo.tsx
deleted file mode 100644
index 723dbb32..00000000
--- a/apps/docs/src/components/docs/primitives/forms-demo.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-"use client";
-
-import {
- Button,
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- Checkbox,
- Input,
- Label,
- RadioGroup,
- RadioGroupItem,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- Textarea,
- useTheme,
-} from "ghost-ui";
-import * as React from "react";
-
-const plans = [
- {
- id: "starter",
- name: "Starter Plan",
- description: "Perfect for small businesses.",
- price: "$10",
- },
- {
- id: "pro",
- name: "Pro Plan",
- description: "Advanced features with more storage.",
- price: "$20",
- },
-] as const;
-
-const themes = {
- neutral: {
- light: {
- "--primary": "oklch(0.205 0 0)",
- "--primary-foreground": "oklch(0.985 0 0)",
- "--ring": "oklch(0.708 0 0)",
- },
- dark: {
- "--primary": "oklch(0.922 0 0)",
- "--primary-foreground": "oklch(0.205 0 0)",
- "--ring": "oklch(0.556 0 0)",
- },
- },
- blue: {
- light: {
- "--primary": "oklch(0.546 0.245 262.881)",
- "--primary-foreground": "oklch(0.985 0.001 106.423)",
- "--ring": "oklch(0.546 0.245 262.881)",
- },
- dark: {
- "--primary": "oklch(0.623 0.214 259.815)",
- "--primary-foreground": "oklch(0.985 0.001 106.423)",
- "--ring": "oklch(0.623 0.214 259.815)",
- },
- },
- amber: {
- light: {
- "--primary": "oklch(0.769 0.188 70.08)",
- "--primary-foreground": "oklch(0.985 0.001 106.423)",
- "--ring": "oklch(0.82 0.13 92.25)",
- },
- dark: {
- "--primary": "oklch(0.769 0.188 70.08)",
- "--primary-foreground": "oklch(0.216 0.006 56.043)",
- "--ring": "oklch(0.666 0.179 58.318)",
- },
- },
- teal: {
- light: {
- "--primary": "oklch(0.627 0.194 149.214)",
- "--primary-foreground": "oklch(0.985 0.001 106.423)",
- "--ring": "oklch(0.79 0.19 153.13)",
- },
- dark: {
- "--primary": "oklch(0.704 0.14 182.503)",
- "--primary-foreground": "oklch(0.216 0.006 56.043)",
- "--ring": "oklch(0.704 0.14 182.503)",
- },
- },
-} as const;
-
-export function FormsDemo() {
- const { theme: mode = "light" } = useTheme();
- const [theme, setTheme] = React.useState(
- undefined,
- );
-
- const themeStyles = React.useMemo(() => {
- if (!theme) return undefined;
- return themes[theme][mode as keyof (typeof themes)[typeof theme]];
- }, [theme, mode]);
-
- return (
-
-
-
- Upgrade your subscription
-
- You are currently on the free plan. Upgrade to the pro plan to get
- access to all features.
-
-
-
-
-
-
-
-
Color
-
- setTheme(value as keyof typeof themes)
- }
- >
-
-
-
-
- {Object.keys(themes).map((theme) => (
-
-
- {theme}
-
- ))}
-
-
-
-
- Plan
-
- Select the plan that best fits your needs.
-
-
- {plans.map((plan) => (
-
-
-
-
{plan.name}
-
- {plan.description}
-
-
-
- ))}
-
-
-
- Notes
-
-
-
-
-
-
- I agree to the terms and conditions
-
-
-
-
-
- Allow us to send you emails
-
-
-
-
-
-
-
- Cancel
-
- Upgrade Plan
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/hover-card-demo.tsx b/apps/docs/src/components/docs/primitives/hover-card-demo.tsx
deleted file mode 100644
index d2104e43..00000000
--- a/apps/docs/src/components/docs/primitives/hover-card-demo.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import {
- Avatar,
- AvatarFallback,
- AvatarImage,
- Button,
- HoverCard,
- HoverCardContent,
- HoverCardTrigger,
-} from "ghost-ui";
-import { CalendarIcon } from "lucide-react";
-
-export function HoverCardDemo() {
- return (
-
-
- @nextjs
-
-
-
-
-
- VC
-
-
-
@nextjs
-
- The React Framework – created and maintained by @vercel.
-
-
- {" "}
-
- Joined December 2021
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/index.tsx b/apps/docs/src/components/docs/primitives/index.tsx
deleted file mode 100644
index 5b0a85b1..00000000
--- a/apps/docs/src/components/docs/primitives/index.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-"use client";
-
-import { AccordionDemo } from "@/components/docs/primitives/accordion-demo";
-import { AlertDemo } from "@/components/docs/primitives/alert-demo";
-import { AlertDialogDemo } from "@/components/docs/primitives/alert-dialog-demo";
-import { AspectRatioDemo } from "@/components/docs/primitives/aspect-ratio-demo";
-import { AvatarDemo } from "@/components/docs/primitives/avatar-demo";
-import { BadgeDemo } from "@/components/docs/primitives/badge-demo";
-import { BreadcrumbDemo } from "@/components/docs/primitives/breadcrumb-demo";
-import { ButtonDemo } from "@/components/docs/primitives/button-demo";
-import { CalendarDemo } from "@/components/docs/primitives/calendar-demo";
-import { CardDemo } from "@/components/docs/primitives/card-demo";
-import { CarouselDemo } from "@/components/docs/primitives/carousel-demo";
-import { ChartDemo } from "@/components/docs/primitives/chart-demo";
-import { CheckboxDemo } from "@/components/docs/primitives/checkbox-demo";
-import { CollapsibleDemo } from "@/components/docs/primitives/collapsible-demo";
-import { ComboboxDemo } from "@/components/docs/primitives/combobox-demo";
-import { CommandDemo } from "@/components/docs/primitives/command-demo";
-import { ComponentWrapper } from "@/components/docs/primitives/component-wrapper";
-import { ContextMenuDemo } from "@/components/docs/primitives/context-menu-demo";
-import { DatePickerDemo } from "@/components/docs/primitives/date-picker-demo";
-import { DialogDemo } from "@/components/docs/primitives/dialog-demo";
-import { DrawerDemo } from "@/components/docs/primitives/drawer-demo";
-import { DropdownMenuDemo } from "@/components/docs/primitives/dropdown-menu-demo";
-import { FormDemo } from "@/components/docs/primitives/form-demo";
-import { FormsDemo } from "@/components/docs/primitives/forms-demo";
-import { HoverCardDemo } from "@/components/docs/primitives/hover-card-demo";
-import { InputDemo } from "@/components/docs/primitives/input-demo";
-import { InputOTPDemo } from "@/components/docs/primitives/input-otp-demo";
-import { LabelDemo } from "@/components/docs/primitives/label-demo";
-import { MenubarDemo } from "@/components/docs/primitives/menubar-demo";
-import { NavigationMenuDemo } from "@/components/docs/primitives/navigation-menu-demo";
-import { PaginationDemo } from "@/components/docs/primitives/pagination-demo";
-import { PopoverDemo } from "@/components/docs/primitives/popover-demo";
-import { ProgressDemo } from "@/components/docs/primitives/progress-demo";
-import { RadioGroupDemo } from "@/components/docs/primitives/radio-group-demo";
-import { ResizableDemo } from "@/components/docs/primitives/resizable-demo";
-import { ScrollAreaDemo } from "@/components/docs/primitives/scroll-area-demo";
-import { SelectDemo } from "@/components/docs/primitives/select-demo";
-import { SeparatorDemo } from "@/components/docs/primitives/separator-demo";
-import { SheetDemo } from "@/components/docs/primitives/sheet-demo";
-import { SkeletonDemo } from "@/components/docs/primitives/skeleton-demo";
-import { SliderDemo } from "@/components/docs/primitives/slider-demo";
-import { SonnerDemo } from "@/components/docs/primitives/sonner-demo";
-import { SwitchDemo } from "@/components/docs/primitives/switch-demo";
-import { TableDemo } from "@/components/docs/primitives/table-demo";
-import { TabsDemo } from "@/components/docs/primitives/tabs-demo";
-import { TextareaDemo } from "@/components/docs/primitives/textarea-demo";
-import { ToggleDemo } from "@/components/docs/primitives/toggle-demo";
-import { ToggleGroupDemo } from "@/components/docs/primitives/toggle-group-demo";
-import { TooltipDemo } from "@/components/docs/primitives/tooltip-demo";
-
-export function ComponentDemos() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/input-demo.tsx b/apps/docs/src/components/docs/primitives/input-demo.tsx
deleted file mode 100644
index 381a35f2..00000000
--- a/apps/docs/src/components/docs/primitives/input-demo.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Input } from "ghost-ui";
-
-export function InputDemo() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/input-otp-demo.tsx b/apps/docs/src/components/docs/primitives/input-otp-demo.tsx
deleted file mode 100644
index e1aa37d9..00000000
--- a/apps/docs/src/components/docs/primitives/input-otp-demo.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-"use client";
-
-import {
- InputOTP,
- InputOTPGroup,
- InputOTPSeparator,
- InputOTPSlot,
- Label,
-} from "ghost-ui";
-import { REGEXP_ONLY_DIGITS } from "input-otp";
-import * as React from "react";
-
-export function InputOTPDemo() {
- return (
-
-
-
-
-
-
- );
-}
-
-function InputOTPSimple() {
- return (
-
- Simple
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function InputOTPPattern() {
- return (
-
- Digits Only
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function InputOTPWithSeparator() {
- const [value, setValue] = React.useState("123456");
-
- return (
-
- With Separator
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function InputOTPWithSpacing() {
- return (
-
- With Spacing
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/label-demo.tsx b/apps/docs/src/components/docs/primitives/label-demo.tsx
deleted file mode 100644
index 28a83581..00000000
--- a/apps/docs/src/components/docs/primitives/label-demo.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Checkbox, Input, Label, Textarea } from "ghost-ui";
-
-export function LabelDemo() {
- return (
-
-
-
- Accept terms and conditions
-
-
- Username
-
-
-
- Disabled
-
-
-
- Message
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/menubar-demo.tsx b/apps/docs/src/components/docs/primitives/menubar-demo.tsx
deleted file mode 100644
index 78f67072..00000000
--- a/apps/docs/src/components/docs/primitives/menubar-demo.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import {
- Menubar,
- MenubarCheckboxItem,
- MenubarContent,
- MenubarGroup,
- MenubarItem,
- MenubarMenu,
- MenubarRadioGroup,
- MenubarRadioItem,
- MenubarSeparator,
- MenubarShortcut,
- MenubarSub,
- MenubarSubContent,
- MenubarSubTrigger,
- MenubarTrigger,
-} from "ghost-ui";
-import { HelpCircleIcon, SettingsIcon, Trash2Icon } from "lucide-react";
-
-export function MenubarDemo() {
- return (
-
-
- File
-
-
- New Tab ⌘T
-
-
- New Window ⌘N
-
- New Incognito Window
-
-
- Share
-
- Email link
- Messages
- Notes
-
-
-
-
- Print... ⌘P
-
-
-
-
- Edit
-
-
- Undo ⌘Z
-
-
- Redo ⇧⌘Z
-
-
-
- Find
-
- Search the web
-
- Find...
- Find Next
- Find Previous
-
-
-
- Cut
- Copy
- Paste
-
-
-
- View
-
- Always Show Bookmarks Bar
-
- Always Show Full URLs
-
-
-
- Reload ⌘R
-
-
- Force Reload ⇧⌘R
-
-
- Toggle Fullscreen
-
- Hide Sidebar
-
-
-
- Profiles
-
-
- Andy
- Benoit
- Luis
-
-
- Edit...
-
- Add Profile...
-
-
-
- More
-
-
-
-
- Settings
-
-
-
- Help
-
-
-
-
- Delete
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/navigation-menu-demo.tsx b/apps/docs/src/components/docs/primitives/navigation-menu-demo.tsx
deleted file mode 100644
index 08684696..00000000
--- a/apps/docs/src/components/docs/primitives/navigation-menu-demo.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-import {
- NavigationMenu,
- NavigationMenuContent,
- NavigationMenuItem,
- NavigationMenuLink,
- NavigationMenuList,
- NavigationMenuTrigger,
- navigationMenuTriggerStyle,
-} from "ghost-ui";
-import { CircleCheckIcon, CircleHelpIcon, CircleIcon } from "lucide-react";
-import * as React from "react";
-import { Link } from "react-router";
-
-const components: { title: string; href: string; description: string }[] = [
- {
- title: "Alert Dialog",
- href: "/docs/primitives/alert-dialog",
- description:
- "A modal dialog that interrupts the user with important content and expects a response.",
- },
- {
- title: "Hover Card",
- href: "/docs/primitives/hover-card",
- description:
- "For sighted users to preview content available behind a link.",
- },
- {
- title: "Progress",
- href: "/docs/primitives/progress",
- description:
- "Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.",
- },
- {
- title: "Scroll-area",
- href: "/docs/primitives/scroll-area",
- description: "Visually or semantically separates content.",
- },
- {
- title: "Tabs",
- href: "/docs/primitives/tabs",
- description:
- "A set of layered sections of content—known as tab panels—that are displayed one at a time.",
- },
- {
- title: "Tooltip",
- href: "/docs/primitives/tooltip",
- description:
- "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.",
- },
-];
-
-export function NavigationMenuDemo() {
- return (
-
-
-
-
- Getting started
-
-
-
-
-
- Components
-
-
- {components.map((component) => (
-
- {component.description}
-
- ))}
-
-
-
-
-
- Documentation
-
-
-
-
-
-
-
-
- Documentation
-
-
-
- List
-
-
-
-
-
- Components
-
- Browse all components in the library.
-
-
-
-
-
- Documentation
-
- Learn how to use the library.
-
-
-
-
-
- Blog
-
- Read our latest blog posts.
-
-
-
-
-
-
-
-
- Simple List
-
-
-
-
- Components
-
-
- Documentation
-
-
- Blocks
-
-
-
-
-
-
- With Icon
-
-
-
-
-
-
- Backlog
-
-
-
-
-
- To Do
-
-
-
-
-
- Done
-
-
-
-
-
-
-
-
-
- );
-}
-
-function ListItem({
- title,
- children,
- href,
- ...props
-}: React.ComponentPropsWithoutRef<"li"> & { href: string }) {
- return (
-
-
-
- {title}
-
- {children}
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/pagination-demo.tsx b/apps/docs/src/components/docs/primitives/pagination-demo.tsx
deleted file mode 100644
index f76b352e..00000000
--- a/apps/docs/src/components/docs/primitives/pagination-demo.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import {
- Pagination,
- PaginationContent,
- PaginationEllipsis,
- PaginationItem,
- PaginationLink,
- PaginationNext,
- PaginationPrevious,
-} from "ghost-ui";
-
-export function PaginationDemo() {
- return (
-
-
-
-
-
-
-
- 1
-
-
-
- 2
-
-
-
- 3
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/popover-demo.tsx b/apps/docs/src/components/docs/primitives/popover-demo.tsx
deleted file mode 100644
index fc32da75..00000000
--- a/apps/docs/src/components/docs/primitives/popover-demo.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import {
- Button,
- Input,
- Label,
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "ghost-ui";
-
-export function PopoverDemo() {
- return (
-
-
- Open popover
-
-
-
-
-
- Dimensions
-
-
- Set the dimensions for the layer.
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/progress-demo.tsx b/apps/docs/src/components/docs/primitives/progress-demo.tsx
deleted file mode 100644
index 7d9404e6..00000000
--- a/apps/docs/src/components/docs/primitives/progress-demo.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-"use client";
-
-import { Progress } from "ghost-ui";
-import * as React from "react";
-
-export function ProgressDemo() {
- const [progress, setProgress] = React.useState(13);
-
- React.useEffect(() => {
- const timer = setTimeout(() => setProgress(66), 500);
- return () => clearTimeout(timer);
- }, []);
-
- return ;
-}
diff --git a/apps/docs/src/components/docs/primitives/radio-group-demo.tsx b/apps/docs/src/components/docs/primitives/radio-group-demo.tsx
deleted file mode 100644
index e9acb8e7..00000000
--- a/apps/docs/src/components/docs/primitives/radio-group-demo.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Label, RadioGroup, RadioGroupItem } from "ghost-ui";
-
-const plans = [
- {
- id: "starter",
- name: "Starter Plan",
- description:
- "Perfect for small businesses getting started with our platform",
- price: "$10",
- },
- {
- id: "pro",
- name: "Pro Plan",
- description: "Advanced features for growing businesses with higher demands",
- price: "$20",
- },
-] as const;
-
-export function RadioGroupDemo() {
- return (
-
-
-
-
- Default
-
-
-
- Comfortable
-
-
-
- Compact
-
-
-
- {plans.map((plan) => (
-
-
-
-
{plan.name}
-
- {plan.description}
-
-
-
- ))}
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/resizable-demo.tsx b/apps/docs/src/components/docs/primitives/resizable-demo.tsx
deleted file mode 100644
index 835194d5..00000000
--- a/apps/docs/src/components/docs/primitives/resizable-demo.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "ghost-ui";
-
-export function ResizableDemo() {
- return (
-
-
-
-
- One
-
-
-
-
-
-
-
- Two
-
-
-
-
-
- Three
-
-
-
-
-
-
-
-
- Sidebar
-
-
-
-
-
- Content
-
-
-
-
-
-
- Header
-
-
-
-
-
- Content
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/scroll-area-demo.tsx b/apps/docs/src/components/docs/primitives/scroll-area-demo.tsx
deleted file mode 100644
index f21ec8a7..00000000
--- a/apps/docs/src/components/docs/primitives/scroll-area-demo.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { ScrollArea, ScrollBar, Separator } from "ghost-ui";
-import * as React from "react";
-export function ScrollAreaDemo() {
- return (
-
-
-
-
- );
-}
-
-const tags = Array.from({ length: 50 }).map(
- (_, i, a) => `v1.2.0-beta.${a.length - i}`,
-);
-
-function ScrollAreaVertical() {
- return (
-
-
-
-
- Tags
-
- {tags.map((tag) => (
-
- {tag}
-
-
- ))}
-
-
-
- );
-}
-
-export const works = [
- {
- artist: "Placeholder Name",
- },
- {
- artist: "Placeholder Name",
- },
- {
- artist: "Placeholder Name",
- },
-] as const;
-
-function ScrollAreaHorizontalDemo() {
- return (
-
-
- {works.map((artwork, index) => (
-
-
-
-
-
- Photo by{" "}
-
- {artwork.artist}
-
-
-
- ))}
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/select-demo.tsx b/apps/docs/src/components/docs/primitives/select-demo.tsx
deleted file mode 100644
index b32e4e07..00000000
--- a/apps/docs/src/components/docs/primitives/select-demo.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectTrigger,
- SelectValue,
-} from "ghost-ui";
-import {
- ChartBarIcon,
- ChartLineIcon,
- ChartPieIcon,
- CircleDashed,
-} from "lucide-react";
-import * as React from "react";
-
-export function SelectDemo() {
- return (
-
-
-
-
-
-
-
- Fruits
- Apple
- Banana
- Blueberry
-
- Grapes
-
- Pineapple
-
-
-
-
-
-
-
-
- {Array.from({ length: 100 }).map((_, i) => (
-
- Item {i}
-
- ))}
-
-
-
-
-
-
-
- Apple
- Banana
- Blueberry
-
- Grapes
-
- Pineapple
-
-
-
-
-
-
- With Icon
- >
- }
- />
-
-
-
-
- Line
-
-
-
- Bar
-
-
-
- Pie
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/separator-demo.tsx b/apps/docs/src/components/docs/primitives/separator-demo.tsx
deleted file mode 100644
index 57ca0763..00000000
--- a/apps/docs/src/components/docs/primitives/separator-demo.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Separator } from "ghost-ui";
-
-export function SeparatorDemo() {
- return (
-
-
-
Tailwind CSS
-
- A utility-first CSS framework.
-
-
-
-
-
Blog
-
-
Docs
-
-
Source
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/sheet-demo.tsx b/apps/docs/src/components/docs/primitives/sheet-demo.tsx
deleted file mode 100644
index bef1b5d2..00000000
--- a/apps/docs/src/components/docs/primitives/sheet-demo.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import {
- Button,
- Input,
- Label,
- Sheet,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
- SheetTrigger,
-} from "ghost-ui";
-
-const SHEET_SIDES = ["top", "right", "bottom", "left"] as const;
-
-export function SheetDemo() {
- return (
-
-
-
- Open
-
-
-
- Edit profile
-
- Make changes to your profile here. Click save when you're
- done.
-
-
-
-
- Save changes
-
- Close
-
-
-
-
-
- {SHEET_SIDES.map((side) => (
-
-
-
- {side}
-
-
-
-
- Edit profile
-
- Make changes to your profile here. Click save when you're
- done.
-
-
-
-
- Lorem Ipsum
-
- {Array.from({ length: 10 }).map((_, index) => (
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
- do eiusmod tempor incididunt ut labore et dolore magna
- aliqua. Ut enim ad minim veniam, quis nostrud exercitation
- ullamco laboris nisi ut aliquip ex ea commodo consequat.
- Duis aute irure dolor in reprehenderit in voluptate velit
- esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
- occaecat cupidatat non proident, sunt in culpa qui officia
- deserunt mollit anim id est laborum.
-
- ))}
-
-
- Save changes
-
- Cancel
-
-
-
-
- ))}
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/skeleton-demo.tsx b/apps/docs/src/components/docs/primitives/skeleton-demo.tsx
deleted file mode 100644
index 29f92922..00000000
--- a/apps/docs/src/components/docs/primitives/skeleton-demo.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Card, CardContent, CardHeader, Skeleton } from "ghost-ui";
-
-export function SkeletonDemo() {
- return (
-
-
-
- {Array.from({ length: 3 }).map((_, index) => (
-
-
-
-
-
-
-
-
-
- ))}
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/slider-demo.tsx b/apps/docs/src/components/docs/primitives/slider-demo.tsx
deleted file mode 100644
index 3ed8978e..00000000
--- a/apps/docs/src/components/docs/primitives/slider-demo.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-"use client";
-
-import { Label, Slider } from "ghost-ui";
-import * as React from "react";
-
-export function SliderDemo() {
- return (
-
- );
-}
-
-function SliderControlled() {
- const [value, setValue] = React.useState([0.3, 0.7]);
-
- return (
-
-
- Temperature
-
- {value.join(", ")}
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/sonner-demo.tsx b/apps/docs/src/components/docs/primitives/sonner-demo.tsx
deleted file mode 100644
index c2554f29..00000000
--- a/apps/docs/src/components/docs/primitives/sonner-demo.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-"use client";
-
-import { Button } from "ghost-ui";
-import * as React from "react";
-import { toast } from "sonner";
-
-const promiseCode = "`${data.name} toast has been added`";
-
-const allTypes = [
- {
- name: "Default",
- snippet: `toast('Event has been created')`,
- action: () => toast("Event has been created"),
- },
- {
- name: "Description",
- snippet: `toast.message('Event has been created', {
- description: 'Monday, January 3rd at 6:00pm',
-})`,
- action: () =>
- toast("Event has been created", {
- description: "Monday, January 3rd at 6:00pm",
- }),
- },
- {
- name: "Success",
- snippet: `toast.success('Event has been created')`,
- action: () => toast.success("Event has been created"),
- },
- {
- name: "Info",
- snippet: `toast.info('Be at the area 10 minutes before the event time')`,
- action: () => toast.info("Be at the area 10 minutes before the event time"),
- },
- {
- name: "Warning",
- snippet: `toast.warning('Event start time cannot be earlier than 8am')`,
- action: () => toast.warning("Event start time cannot be earlier than 8am"),
- },
- {
- name: "Error",
- snippet: `toast.error('Event has not been created')`,
- action: () => toast.error("Event has not been created"),
- },
- {
- name: "Action",
- action: () =>
- toast.message("Event has been created", {
- action: {
- label: "Undo",
- onClick: () => console.log("Undo"),
- },
- }),
- },
- {
- name: "Cancel",
- action: () =>
- toast.message("Event has been created", {
- cancel: {
- label: "Cancel",
- onClick: () => console.log("Cancel"),
- },
- }),
- },
- {
- name: "Promise",
- snippet: `const promise = () => new Promise((resolve) => setTimeout(() => resolve({ name: 'Sonner' }), 2000));
-
-toast.promise(promise, {
- loading: 'Loading...',
- success: (data) => {
- return ${promiseCode};
- },
- error: 'Error',
-});`,
- action: () =>
- toast.promise<{ name: string }>(
- () =>
- new Promise((resolve) => {
- setTimeout(() => {
- resolve({ name: "Sonner" });
- }, 2000);
- }),
- {
- loading: "Loading...",
- success: (data) => {
- return `${data.name} toast has been added`;
- },
- error: "Error",
- },
- ),
- },
-];
-
-export function SonnerDemo() {
- const [activeType, setActiveType] = React.useState(allTypes[0]);
- return (
-
- toast("My first toast")} variant="outline">
- Give me a toast
-
-
- toast("Event has been created", {
- description: "Sunday, December 03, 2023 at 9:00 AM",
- action: {
- label: "Undo",
- onClick: () => console.log("Undo"),
- },
- })
- }
- >
- Show Toast
-
- {allTypes.map((type) => (
- {
- type.action();
- setActiveType(type);
- }}
- key={type.name}
- >
- {type.name}
-
- ))}
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/switch-demo.tsx b/apps/docs/src/components/docs/primitives/switch-demo.tsx
deleted file mode 100644
index 33470e81..00000000
--- a/apps/docs/src/components/docs/primitives/switch-demo.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Label, Switch } from "ghost-ui";
-
-export function SwitchDemo() {
- return (
-
-
-
- Airplane Mode
-
-
-
- Bluetooth
-
-
-
-
Share across devices
-
- Focus is shared across devices, and turns off when you leave the
- app.
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/table-demo.tsx b/apps/docs/src/components/docs/primitives/table-demo.tsx
deleted file mode 100644
index 59e18af6..00000000
--- a/apps/docs/src/components/docs/primitives/table-demo.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import {
- Table,
- TableBody,
- TableCaption,
- TableCell,
- TableFooter,
- TableHead,
- TableHeader,
- TableRow,
-} from "ghost-ui";
-
-const invoices = [
- {
- invoice: "INV001",
- paymentStatus: "Paid",
- totalAmount: "$250.00",
- paymentMethod: "Credit Card",
- },
- {
- invoice: "INV002",
- paymentStatus: "Pending",
- totalAmount: "$150.00",
- paymentMethod: "PayPal",
- },
- {
- invoice: "INV003",
- paymentStatus: "Unpaid",
- totalAmount: "$350.00",
- paymentMethod: "Bank Transfer",
- },
- {
- invoice: "INV004",
- paymentStatus: "Paid",
- totalAmount: "$450.00",
- paymentMethod: "Credit Card",
- },
- {
- invoice: "INV005",
- paymentStatus: "Paid",
- totalAmount: "$550.00",
- paymentMethod: "PayPal",
- },
- {
- invoice: "INV006",
- paymentStatus: "Pending",
- totalAmount: "$200.00",
- paymentMethod: "Bank Transfer",
- },
- {
- invoice: "INV007",
- paymentStatus: "Unpaid",
- totalAmount: "$300.00",
- paymentMethod: "Credit Card",
- },
-];
-
-export function TableDemo() {
- return (
-
- A list of your recent invoices.
-
-
- Invoice
- Status
- Method
- Amount
-
-
-
- {invoices.map((invoice) => (
-
- {invoice.invoice}
- {invoice.paymentStatus}
- {invoice.paymentMethod}
- {invoice.totalAmount}
-
- ))}
-
-
-
- Total
- $2,500.00
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/tabs-demo.tsx b/apps/docs/src/components/docs/primitives/tabs-demo.tsx
deleted file mode 100644
index ec679cbd..00000000
--- a/apps/docs/src/components/docs/primitives/tabs-demo.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import {
- Button,
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
- Input,
- Label,
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
-} from "ghost-ui";
-import { AppWindowIcon, CodeIcon } from "lucide-react";
-
-export function TabsDemo() {
- return (
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/textarea-demo.tsx b/apps/docs/src/components/docs/primitives/textarea-demo.tsx
deleted file mode 100644
index 12259ccf..00000000
--- a/apps/docs/src/components/docs/primitives/textarea-demo.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Label, Textarea } from "ghost-ui";
-
-export function TextareaDemo() {
- return (
-
-
-
-
- Label
-
-
-
-
- With label and description
-
-
-
- Type your message and press enter to send.
-
-
-
- Disabled
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/toggle-demo.tsx b/apps/docs/src/components/docs/primitives/toggle-demo.tsx
deleted file mode 100644
index 4ef1c3b0..00000000
--- a/apps/docs/src/components/docs/primitives/toggle-demo.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Toggle } from "ghost-ui";
-import {
- BoldIcon,
- BookmarkIcon,
- ItalicIcon,
- UnderlineIcon,
-} from "lucide-react";
-
-export function ToggleDemo() {
- return (
-
-
-
-
-
-
-
-
- Disabled
-
-
-
- Italic
-
-
-
-
-
- Small
-
-
- Large
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/toggle-group-demo.tsx b/apps/docs/src/components/docs/primitives/toggle-group-demo.tsx
deleted file mode 100644
index 1ac43fe0..00000000
--- a/apps/docs/src/components/docs/primitives/toggle-group-demo.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { ToggleGroup, ToggleGroupItem } from "ghost-ui";
-import { BoldIcon, ItalicIcon, UnderlineIcon } from "lucide-react";
-
-export function ToggleGroupDemo() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- All
-
-
- Missed
-
-
-
-
-
- Last 24 hours
-
-
- Last 7 days
-
-
-
-
-
- Last 24 hours
-
-
- Last 7 days
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/primitives/tooltip-demo.tsx b/apps/docs/src/components/docs/primitives/tooltip-demo.tsx
deleted file mode 100644
index 8be1855a..00000000
--- a/apps/docs/src/components/docs/primitives/tooltip-demo.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Button, Tooltip, TooltipContent, TooltipTrigger } from "ghost-ui";
-import { InfoIcon } from "lucide-react";
-
-export function TooltipDemo() {
- return (
-
-
-
- Hover
-
-
- Add to library
-
-
-
- {["top", "right", "bottom", "left"].map((side) => (
-
-
-
- {side}
-
-
-
- Add to library
-
-
- ))}
-
-
-
-
-
- Info
-
-
-
- To learn more about how this works, check out the docs. If you have
- any questions, please reach out to us.
-
-
-
- );
-}
diff --git a/apps/docs/src/components/docs/registry-sidebar.tsx b/apps/docs/src/components/docs/registry-sidebar.tsx
deleted file mode 100644
index f83837b7..00000000
--- a/apps/docs/src/components/docs/registry-sidebar.tsx
+++ /dev/null
@@ -1,187 +0,0 @@
-"use client";
-
-import {
- Button,
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
- Input,
- Sidebar,
- SidebarContent,
- SidebarGroup,
- SidebarGroupContent,
- SidebarGroupLabel,
- SidebarHeader,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- useSidebar,
-} from "ghost-ui";
-import { ChevronDown, Menu, Search } from "lucide-react";
-import { useEffect, useMemo, useState } from "react";
-import { Link, useLocation } from "react-router";
-import {
- type ComponentEntry,
- categories,
- getComponentsByCategory,
-} from "@/lib/component-registry";
-
-const topNav = [
- { name: "home", path: "/" },
- { name: "colors", path: "/ui/foundations/colors" },
- { name: "typography", path: "/ui/foundations/typography" },
- { name: "all components", path: "/ui/components" },
-];
-
-export function RegistrySidebar() {
- const { pathname } = useLocation();
- const { setOpenMobile } = useSidebar();
-
- const [searchTerm, setSearchTerm] = useState("");
-
- const filteredCategories = useMemo(() => {
- const term = searchTerm.toLowerCase();
- return categories
- .map((cat) => {
- const items = getComponentsByCategory(cat.slug);
- const filtered = term
- ? items.filter((item) => item.name.toLowerCase().includes(term))
- : items;
- return { ...cat, items: filtered };
- })
- .filter((cat) => cat.items.length > 0);
- }, [searchTerm]);
-
- const hasSearch = searchTerm.length > 0;
-
- return (
- <>
- setOpenMobile(true)}
- appearance="icon"
- size="lg"
- className="hover:scale-105 sticky z-50 top-4 ml-6 md:hidden"
- >
-
-
-
-
-
-
-
-
- setSearchTerm(e.target.value)}
- />
-
-
-
-
-
- {!hasSearch && (
-
-
-
-
-
- get started
-
-
-
-
-
-
-
-
- {topNav.map((item) => (
-
-
- setOpenMobile(false)}
- >
-
- {item.name}
-
-
-
-
- ))}
-
-
-
-
-
- )}
-
- {filteredCategories.map((cat) => (
- setOpenMobile(false)}
- />
- ))}
-
-
- >
- );
-}
-
-function CategorySection({
- label,
- items,
- pathname,
- defaultOpen,
- onNavigate,
-}: {
- label: string;
- items: ComponentEntry[];
- pathname: string;
- defaultOpen: boolean;
- onNavigate: () => void;
-}) {
- return (
-
-
-
-
- {label}
-
-
-
-
-
-
- {items.map((item) => (
-
-
-
- {item.name}
-
-
-
- ))}
-
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/theme-panel/ColorControls.tsx b/apps/docs/src/components/theme-panel/ColorControls.tsx
deleted file mode 100644
index 3add09de..00000000
--- a/apps/docs/src/components/theme-panel/ColorControls.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-"use client";
-
-import {
- DEFAULT_COLORS_DARK,
- DEFAULT_COLORS_LIGHT,
- getResolvedVariable,
- useTheme,
-} from "ghost-ui";
-import { useEffect, useState } from "react";
-import { useThemePanel } from "@/contexts/ThemePanelContext";
-import { ColorSwatch } from "./ColorSwatch";
-
-interface ColorGroup {
- label: string;
- variables: { key: string; label: string }[];
-}
-
-const COLOR_GROUPS: ColorGroup[] = [
- {
- label: "Accent",
- variables: [{ key: "--background-accent", label: "Accent" }],
- },
- {
- label: "Surfaces",
- variables: [
- { key: "--background-default", label: "Default" },
- { key: "--background-alt", label: "Alt" },
- { key: "--background-muted", label: "Muted" },
- { key: "--background-medium", label: "Medium" },
- { key: "--background-inverse", label: "Inverse" },
- ],
- },
- {
- label: "Text",
- variables: [
- { key: "--text-default", label: "Default" },
- { key: "--text-muted", label: "Muted" },
- { key: "--text-alt", label: "Alt" },
- { key: "--text-inverse", label: "Inverse" },
- ],
- },
- {
- label: "Borders",
- variables: [
- { key: "--border-default", label: "Default" },
- { key: "--border-input", label: "Input" },
- { key: "--border-card", label: "Card" },
- { key: "--border-strong", label: "Strong" },
- ],
- },
- {
- label: "Feedback",
- variables: [
- { key: "--background-danger", label: "Danger" },
- { key: "--background-success", label: "Success" },
- { key: "--background-warning", label: "Warning" },
- { key: "--background-info", label: "Info" },
- ],
- },
- {
- label: "Charts",
- variables: [
- { key: "--chart-1", label: "Chart 1" },
- { key: "--chart-2", label: "Chart 2" },
- { key: "--chart-3", label: "Chart 3" },
- { key: "--chart-4", label: "Chart 4" },
- { key: "--chart-5", label: "Chart 5" },
- ],
- },
-];
-
-// Linked variables: when accent changes, also update these
-const ACCENT_LINKED = ["--border-accent", "--text-accent"];
-
-export function ColorControls() {
- const { overrides, setVariable, setVariables } = useThemePanel();
- const { resolvedTheme } = useTheme();
- const isDark = resolvedTheme === "dark";
- const defaults = isDark ? DEFAULT_COLORS_DARK : DEFAULT_COLORS_LIGHT;
-
- // Track resolved values for initial display
- const [resolved, setResolved] = useState>({});
-
- useEffect(() => {
- // Read current computed values for all color variables
- const vals: Record = {};
- for (const group of COLOR_GROUPS) {
- for (const v of group.variables) {
- const computed = getResolvedVariable(v.key);
- if (computed) {
- vals[v.key] = rgbToHex(computed) || defaults[v.key] || "#000000";
- }
- }
- }
- setResolved(vals);
- }, [overrides, isDark]); // eslint-disable-line react-hooks/exhaustive-deps
-
- function getColor(key: string): string {
- return overrides[key] || resolved[key] || defaults[key] || "#000000";
- }
-
- function handleChange(key: string, value: string) {
- if (key === "--background-accent") {
- // Also update linked accent variables
- const linked: Record = { [key]: value };
- for (const k of ACCENT_LINKED) {
- linked[k] = value;
- }
- setVariables(linked);
- } else {
- setVariable(key, value);
- }
- }
-
- return (
-
- {COLOR_GROUPS.map((group) => (
-
-
- {group.label}
-
-
- {group.variables.map((v) => (
- handleChange(v.key, val)}
- label={v.label}
- />
- ))}
-
-
- ))}
-
- );
-}
-
-function rgbToHex(rgb: string): string | null {
- // Handle hex passthrough
- if (rgb.startsWith("#")) return rgb;
-
- // Handle "rgb(r, g, b)" or "r g b" format
- const match = rgb.match(/(\d+)[,\s]+(\d+)[,\s]+(\d+)/);
- if (!match) return null;
-
- const r = parseInt(match[1]);
- const g = parseInt(match[2]);
- const b = parseInt(match[3]);
- return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
-}
diff --git a/apps/docs/src/components/theme-panel/ColorSwatch.tsx b/apps/docs/src/components/theme-panel/ColorSwatch.tsx
deleted file mode 100644
index 22acfeb5..00000000
--- a/apps/docs/src/components/theme-panel/ColorSwatch.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-"use client";
-
-interface ColorSwatchProps {
- value: string;
- onChange: (value: string) => void;
- label: string;
-}
-
-export function ColorSwatch({ value, onChange, label }: ColorSwatchProps) {
- return (
-
-
-
onChange(e.target.value)}
- className="absolute inset-0 w-[200%] h-[200%] -top-1/2 -left-1/2 cursor-pointer opacity-0"
- />
-
-
-
- {label}
-
- {value}
-
-
-
- );
-}
diff --git a/apps/docs/src/components/theme-panel/ExportReset.tsx b/apps/docs/src/components/theme-panel/ExportReset.tsx
deleted file mode 100644
index 27c40d16..00000000
--- a/apps/docs/src/components/theme-panel/ExportReset.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-"use client";
-
-import { Button } from "ghost-ui";
-import { Check, Copy, RotateCcw } from "lucide-react";
-import { useCallback, useState } from "react";
-import { useThemePanel } from "@/contexts/ThemePanelContext";
-
-export function ExportReset() {
- const { exportCSS, reset, overrides } = useThemePanel();
- const [copied, setCopied] = useState(false);
-
- const handleCopy = useCallback(async () => {
- const css = exportCSS();
- if (!css) return;
-
- await navigator.clipboard.writeText(css);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- }, [exportCSS]);
-
- const hasOverrides = Object.keys(overrides).length > 0;
-
- return (
-
-
- {copied ? (
- <>
-
- Copied
- >
- ) : (
- <>
-
- Copy CSS
- >
- )}
-
-
-
- Reset
-
-
- );
-}
diff --git a/apps/docs/src/components/theme-panel/PresetSelector.tsx b/apps/docs/src/components/theme-panel/PresetSelector.tsx
deleted file mode 100644
index adaed674..00000000
--- a/apps/docs/src/components/theme-panel/PresetSelector.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-"use client";
-
-import { cn, PRESETS } from "ghost-ui";
-import { useThemePanel } from "@/contexts/ThemePanelContext";
-
-export function PresetSelector() {
- const { activePresetId, applyPreset } = useThemePanel();
-
- return (
-
- {PRESETS.map((preset) => {
- const isActive = activePresetId === preset.id;
- return (
-
applyPreset(preset.id)}
- className={cn(
- "flex flex-col items-center gap-2 rounded-card-sm border p-3 transition-all cursor-pointer",
- isActive
- ? "border-border-accent shadow-mini bg-background-alt"
- : "border-border hover:border-border-input-hover hover:bg-background-alt",
- )}
- >
-
- {[
- preset.preview.accent,
- preset.preview.background,
- preset.preview.text,
- preset.preview.muted,
- ].map((color, i) => (
-
- ))}
-
-
- {preset.name}
-
-
- );
- })}
- {/* Custom indicator */}
- {activePresetId === null && (
-
- )}
-
- );
-}
diff --git a/apps/docs/src/components/theme-panel/RadiusControls.tsx b/apps/docs/src/components/theme-panel/RadiusControls.tsx
deleted file mode 100644
index 63f2c3fb..00000000
--- a/apps/docs/src/components/theme-panel/RadiusControls.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-"use client";
-
-import { Slider } from "ghost-ui";
-import { useThemePanel } from "@/contexts/ThemePanelContext";
-
-export function RadiusControls() {
- const { radiusScale, setRadiusScale } = useThemePanel();
-
- const label =
- radiusScale < 10
- ? "Sharp"
- : radiusScale < 40
- ? "Subtle"
- : radiusScale < 60
- ? "Default"
- : radiusScale < 85
- ? "Rounded"
- : "Pill";
-
- return (
-
-
-
-
- Corner Radius
-
-
- {label}
-
-
-
setRadiusScale(v)}
- min={0}
- max={100}
- step={1}
- />
-
-
- {/* Live preview */}
-
-
Preview
-
-
- Card
-
-
- Button
-
-
- Input
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/theme-panel/ShadowControls.tsx b/apps/docs/src/components/theme-panel/ShadowControls.tsx
deleted file mode 100644
index bed7961d..00000000
--- a/apps/docs/src/components/theme-panel/ShadowControls.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-"use client";
-
-import { Slider } from "ghost-ui";
-import { useThemePanel } from "@/contexts/ThemePanelContext";
-
-export function ShadowControls() {
- const { shadowScale, setShadowScale } = useThemePanel();
-
- const label =
- shadowScale < 5
- ? "None"
- : shadowScale < 30
- ? "Subtle"
- : shadowScale < 60
- ? "Default"
- : shadowScale < 85
- ? "Prominent"
- : "Heavy";
-
- return (
-
-
-
-
- Shadow Intensity
-
-
- {label}
-
-
-
setShadowScale(v)}
- min={0}
- max={100}
- step={1}
- />
-
-
- {/* Live preview */}
-
-
Preview
-
-
- Mini
-
-
- Card
-
-
- Elevated
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/theme-panel/ThemePanel.tsx b/apps/docs/src/components/theme-panel/ThemePanel.tsx
deleted file mode 100644
index 8a8571e3..00000000
--- a/apps/docs/src/components/theme-panel/ThemePanel.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-"use client";
-
-import {
- cn,
- Separator,
- Tabs,
- TabsContent,
- TabsList,
- TabsTrigger,
-} from "ghost-ui";
-import { XIcon } from "lucide-react";
-import { useThemePanel } from "@/contexts/ThemePanelContext";
-import { ColorControls } from "./ColorControls";
-import { ExportReset } from "./ExportReset";
-import { PresetSelector } from "./PresetSelector";
-import { RadiusControls } from "./RadiusControls";
-import { ShadowControls } from "./ShadowControls";
-import { TypographyControls } from "./TypographyControls";
-
-export function ThemePanel() {
- const { isOpen, setOpen } = useThemePanel();
-
- return (
-
- {/* Header */}
-
-
-
- Theme
-
- setOpen(false)}
- className="rounded-xs opacity-70 transition-opacity hover:opacity-100"
- >
-
- Close
-
-
-
- Customize the design language in real time.
-
-
-
- {/* Scrollable content */}
-
- {/* Presets */}
-
-
-
-
- {/* Token Controls */}
-
-
-
- Colors
-
-
- Radius
-
-
- Shadows
-
-
- Type
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Footer */}
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/theme-panel/ThemePanelTrigger.tsx b/apps/docs/src/components/theme-panel/ThemePanelTrigger.tsx
deleted file mode 100644
index b075f1f6..00000000
--- a/apps/docs/src/components/theme-panel/ThemePanelTrigger.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-"use client";
-
-import { Button } from "ghost-ui";
-import { Palette } from "lucide-react";
-import { useThemePanel } from "@/contexts/ThemePanelContext";
-
-export function ThemePanelTrigger() {
- const { setOpen } = useThemePanel();
-
- return (
- setOpen(true)}
- className="bg-background-inverse text-primary-foreground hover:scale-105"
- >
-
-
- );
-}
diff --git a/apps/docs/src/components/theme-panel/TypographyControls.tsx b/apps/docs/src/components/theme-panel/TypographyControls.tsx
deleted file mode 100644
index c2d2467c..00000000
--- a/apps/docs/src/components/theme-panel/TypographyControls.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-"use client";
-
-import { Slider } from "ghost-ui";
-import { useCallback, useState } from "react";
-import { useThemePanel } from "@/contexts/ThemePanelContext";
-
-// Default values for scaling (50 = default)
-const WEIGHT_MAP: Record> = {
- 0: {
- "--heading-display-font-weight": "300",
- "--heading-section-font-weight": "400",
- "--heading-sub-font-weight": "400",
- "--heading-card-font-weight": "400",
- },
- 25: {
- "--heading-display-font-weight": "600",
- "--heading-section-font-weight": "500",
- "--heading-sub-font-weight": "500",
- "--heading-card-font-weight": "500",
- },
- 50: {
- "--heading-display-font-weight": "900",
- "--heading-section-font-weight": "700",
- "--heading-sub-font-weight": "700",
- "--heading-card-font-weight": "600",
- },
- 75: {
- "--heading-display-font-weight": "900",
- "--heading-section-font-weight": "900",
- "--heading-sub-font-weight": "700",
- "--heading-card-font-weight": "700",
- },
- 100: {
- "--heading-display-font-weight": "900",
- "--heading-section-font-weight": "900",
- "--heading-sub-font-weight": "900",
- "--heading-card-font-weight": "900",
- },
-};
-
-function interpolateWeight(factor: number): Record {
- const keys = Object.keys(WEIGHT_MAP)
- .map(Number)
- .sort((a, b) => a - b);
- const lower = keys.filter((k) => k <= factor).pop() ?? 0;
- const upper = keys.find((k) => k >= factor) ?? 100;
-
- if (lower === upper) return WEIGHT_MAP[lower];
-
- // Snap to nearest weight bracket
- const t = (factor - lower) / (upper - lower);
- return t < 0.5 ? WEIGHT_MAP[lower] : WEIGHT_MAP[upper];
-}
-
-const TRACKING_VARS = [
- "--heading-display-letter-spacing",
- "--heading-section-letter-spacing",
- "--heading-sub-letter-spacing",
- "--heading-card-letter-spacing",
-];
-
-const DEFAULT_TRACKING = [-0.05, -0.035, -0.02, -0.01];
-
-export function TypographyControls() {
- const { setVariables } = useThemePanel();
- const [weightScale, setWeightScale] = useState(50);
- const [trackingScale, setTrackingScale] = useState(50);
-
- const handleWeightChange = useCallback(
- (value: number) => {
- setWeightScale(value);
- setVariables(interpolateWeight(value));
- },
- [setVariables],
- );
-
- const handleTrackingChange = useCallback(
- (value: number) => {
- setTrackingScale(value);
- // 0 = loose (0em), 50 = default, 100 = very tight (2x default)
- const multiplier = value / 50;
- const vars: Record = {};
- TRACKING_VARS.forEach((key, i) => {
- vars[key] = `${(DEFAULT_TRACKING[i] * multiplier).toFixed(3)}em`;
- });
- setVariables(vars);
- },
- [setVariables],
- );
-
- const weightLabel =
- weightScale < 20
- ? "Light"
- : weightScale < 45
- ? "Medium"
- : weightScale < 65
- ? "Default"
- : weightScale < 85
- ? "Bold"
- : "Black";
-
- const trackingLabel =
- trackingScale < 15
- ? "Loose"
- : trackingScale < 40
- ? "Normal"
- : trackingScale < 60
- ? "Default"
- : trackingScale < 85
- ? "Tight"
- : "Very Tight";
-
- return (
-
-
-
-
- Weight
-
-
- {weightLabel}
-
-
-
handleWeightChange(v)}
- min={0}
- max={100}
- step={1}
- />
-
-
-
-
-
- Letter Spacing
-
-
- {trackingLabel}
-
-
-
handleTrackingChange(v)}
- min={0}
- max={100}
- step={1}
- />
-
-
- {/* Live preview */}
-
-
Preview
-
-
- Heading
-
-
- Body text sample for preview.
-
-
-
-
- );
-}
diff --git a/apps/docs/src/components/theme/ThemeControls.tsx b/apps/docs/src/components/theme/ThemeControls.tsx
deleted file mode 100644
index cba10256..00000000
--- a/apps/docs/src/components/theme/ThemeControls.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-"use client";
-
-import { ThemeToggle } from "ghost-ui";
-import { ThemePanel } from "@/components/theme-panel/ThemePanel";
-import { ThemePanelTrigger } from "@/components/theme-panel/ThemePanelTrigger";
-import { ThemePanelProvider } from "@/contexts/ThemePanelContext";
-
-export function ThemeControls() {
- return (
-
-
- {/* */}
-
-
- {/* */}
-
- );
-}
diff --git a/apps/docs/src/content/docs/cli-reference.mdx b/apps/docs/src/content/docs/cli-reference.mdx
index 2fed608b..844f02d2 100644
--- a/apps/docs/src/content/docs/cli-reference.mdx
+++ b/apps/docs/src/content/docs/cli-reference.mdx
@@ -1,6 +1,6 @@
---
title: CLI Reference
-description: Commands around the portable fingerprint lifecycle. Your agent handles the composition work.
+description: The deterministic commands around the node-graph fingerprint lifecycle. Your agent does the reading, writing, and reviewing.
kicker: Docs
section: guide
order: 30
@@ -9,25 +9,23 @@ slug: cli
-The CLI does the repeatable parts around the fingerprint lifecycle: create
-packages, report contribution state, validate files, gather optional source material,
-emit handoff packets, govern diffs, compare packages, and record intent. Your
-agent does the reading, writing, and reviewing.
+The CLI does the repeatable parts around the fingerprint lifecycle: scaffold a
+package, validate the node graph, compose context for a surface, route checks,
+and emit advisory review packets. Your agent does the interpretation.
-`ghost --help` intentionally shows the short core workflow for new adopters.
-Run `ghost --help --all` for the complete command index; command-specific help
-remains available with `ghost --help`.
+`ghost --help` shows the short core workflow. `ghost --help --all` shows the
+complete command index, and `ghost --help` shows flags for one
+command.
-Canonical Ghost fingerprints start here, with optional child packages for scoped
-product areas:
+The canonical fingerprint is a `.ghost/` directory tree of prose nodes:
```text
-.ghost/manifest.yml
-.ghost/intent.yml
-.ghost/inventory.yml
-.ghost/composition.yml
-.ghost/validate.yml
-apps/checkout/.ghost/manifest.yml
+.ghost/
+ manifest.yml # schema + id
+ index.md # the core node, true everywhere (optional)
+ /index.md # a surface's own prose (the directory is the surface)
+ /.md # a prose node placed in that surface
+ checks/*.md # optional ghost.check/v1 checks
```
The command tables below are generated from the CLI source. Run
@@ -37,59 +35,40 @@ The command tables below are generated from the CLI source. Run
-### Initialize - `init`
+### Initialize: `init`
-Create a `.ghost/` package with a manifest, raw facet files,
-and deterministic checks. Use `--scope ` for nested package roots. Use
-`--monorepo` to create or preserve the root package, detect workspace child
-roots, and print scoped init commands; add `--apply` to create the detected
-child packages. Use
-`GHOST_PACKAGE_DIR=` only when a host wrapper stores Ghost package
-roots under a different safe relative directory; raw `ghost` defaults to
-`.ghost`. Exact `--package ` values win over the environment default.
+Scaffold a `.ghost/` package: a manifest and a core `index.md` node. Add
+surfaces by adding directories (`checkout/index.md` is the `checkout` surface).
+Use `--template ` to pick a starter, `--package ` for an exact
+directory, or set `GHOST_PACKAGE_DIR` when a host wrapper stores Ghost files
+outside the default `.ghost`.
```bash
ghost init
-ghost init --monorepo
-ghost init --monorepo --apply
-ghost init --scope apps/checkout
+ghost init --template default
ghost init --package product-surface
-ghost init --package .design/custom-ghost
GHOST_PACKAGE_DIR=.agents/ghost ghost init
-GHOST_PACKAGE_DIR=.design/memory ghost init --scope apps/checkout
```
-### Contribution facets - `scan`
+### Contribution: `scan`
-Report whether `manifest.yml` is present and which sparse facets
-this package contributes: `intent`, `inventory`, `composition`, and `validate`.
-Raw repo signals do not count toward inventory contribution.
+Report what the package contributes: presence of the manifest, and the nodes
+and surfaces (directories) it carries.
```bash
ghost scan
ghost scan --format json
-GHOST_PACKAGE_DIR=.agents/ghost ghost scan --format json
-GHOST_PACKAGE_DIR=.design/memory ghost scan --include-nested --format json
```
-### Stack inspection - `stack`
-
-Inspect the root-to-leaf fingerprint stack for one or more paths.
-
-
-
-```bash
-ghost stack apps/checkout/review/page.tsx --format json
-```
-
-### Inspect repo signals - `signals`
+### Repo signals: `signals`
Emit raw signals about a frontend repo as JSON. Use this as scratch evidence
-while authoring curated fingerprint facets.
+while authoring curated nodes. It does not contribute to the fingerprint by
+itself.
@@ -99,108 +78,119 @@ ghost signals .
-
+
-### Validation - `lint`
+### Validation: `validate`
-Validate a root `.ghost` fingerprint package or an individual split artifact.
-`--all` validates every nested package and merged stack.
+Validate the package: artifact shape plus the node graph. Every `relates` link
+resolves, there is exactly one root, and the graph is acyclic. Defaults to
+`.ghost`; pass a file to validate a single node.
-
+
```bash
-ghost lint
-ghost lint .ghost/intent.yml
-ghost lint .ghost/validate.yml --format json
-ghost lint --all
+ghost validate
+ghost validate .ghost/checkout/trust.md
+ghost validate --format json
```
-### Package fidelity - `verify`
+
-Validate fingerprint evidence and exemplar paths, typed check refs, and
-optional rationale files.
+
-
+### Compose a surface slice: `gather`
+
+With no argument, list every node by id and description so an agent can match a
+task to one. With a surface, compose its context slice. Folders are walls,
+files fill the corridor:
+
+- **spine** (full bodies): every file from the package root down to the
+ surface's own folder. A sibling folder is a wall; its nodes never appear.
+- **edges** (full bodies, one hop): each spine node's `relates` targets, so a
+ rule authored once high in the corridor reaches every descendant. A link to a
+ hub also unfolds the hub's subtree as spokes.
+- **spokes** (pointers): the surface's own descendants and any edge hub's
+ subtree, offered as `id` + `description` for the agent to pull on demand.
+
+Use `--as` to filter full-body nodes to a single incarnation; untagged essence
+nodes always pass.
+
+
```bash
-ghost verify .ghost --root .
-GHOST_PACKAGE_DIR=.design/memory ghost verify --all
+ghost gather
+ghost gather checkout
+ghost gather checkout --as email
+ghost gather checkout --format json
```
-### Reusable Review Command - `emit`
+This is the pre-generation step: Ghost gives agents surface-composition context
+before they build, not only after a review finds drift.
-Emit `review-command` from split fingerprint facets when a host wants a
-reusable review prompt.
+
-
+
-### Agent Context - `relay gather`
+### Route checks: `checks`
-Gather Relay context for a target path or structured Relay request. Relay loads
-config first; omitted `base` means `base.kind: fingerprint`, while
-`base.kind: none` lets agent-framework repos gather declared request context
-without a `.ghost` package. For agents and host adapters, use JSON: the full
-`ghost.relay.gather/v2` result is the stable contract, and its nested `context`
-is `ghost.relay-context/v1`. Plain markdown output remains a compact human
-preview.
+Select and ground the markdown checks governing the named surfaces. The agent
+names the surfaces the change touches, then evaluates the returned checks. Use
+`--no-grounding` to omit the grounded nodes and return only the relevant checks.
-
+
```bash
-ghost relay gather apps/checkout/review/page.tsx --format json
-ghost relay gather apps/checkout/review/page.tsx --package product-surface --format json
-ghost relay gather apps/checkout/review/page.tsx --config .ghost/relay.yml --format json
-GHOST_RELAY_CONFIG=.agents/ghost/relay.yml ghost relay gather --request-stdin --format json
-ghost relay gather stacks/portal.renewal-reminder.email.yml --config .agents/ghost/relay.yml --format json
-ghost relay gather --request request.yml --format json
-ghost relay gather --request-stdin --format json
-ghost relay gather apps/checkout/review/page.tsx # human preview
+ghost checks --surface checkout
+ghost checks --surface checkout,billing
+ghost checks --surface checkout --format json
```
-### Inspection - `describe`
-
-Print a markdown section map.
+### Advisory review packet: `review`
-
+Emit an advisory packet for a diff: touched surfaces, routed checks, and
+fingerprint grounding, with the diff embedded verbatim. Diff against a git ref
+with `--base`, or pass a patch with `--diff` (use `-` for stdin).
-### Survey/cache ops - `survey `
+
-Operate on `ghost.survey/v1` files as compatibility cache source material.
+```bash
+ghost review --surface checkout --base main
+ghost review --surface checkout --diff change.patch
+git diff | ghost review --surface checkout --diff -
+ghost review --surface checkout --format json
+```
-
+Wrappers should consume `--format json` and map Ghost severities (`critical`,
+`serious`, `nit`) into their own review format. Advisory review is never a CI
+gate on its own.
-
-
-### Deterministic gates - `check`
-
-Run active `ghost.validate/v1` gates against a git diff. Without `--package`,
-Ghost groups changed files by resolved fingerprint stack and runs merged checks
-per group.
-
-
+
-### Advisory governance packet - `review`
+### Install the skill: `skill`
-Emit an evidence-routed advisory review packet grounded in selected context,
-validation checks, and the diff.
+Install the unified Ghost skill bundle so a host agent knows how to author and
+use the fingerprint.
-
-
-### Comparison - `compare`
+
-Pairwise distance or composite analysis over fingerprint packages.
+```bash
+ghost skill install
+ghost skill install --agent claude
+ghost skill install --dest ~/.codex/skills/ghost
+```
-
+### Migrate a legacy package: `migrate`
-### Drift stance - `ack` / `track` / `diverge`
+Migrate a legacy `.ghost/` package onto the node-graph surface model. Use
+`--dry-run` to print the plan without writing.
-Record how the repo should treat tracked fingerprint drift. These compatibility
-governance verbs still operate on tracked direct fingerprint markdown files.
+
-
-
-
+```bash
+ghost migrate
+ghost migrate .ghost --dry-run
+```
diff --git a/apps/docs/src/content/docs/fingerprint-authoring.mdx b/apps/docs/src/content/docs/fingerprint-authoring.mdx
index 6f285361..591df926 100644
--- a/apps/docs/src/content/docs/fingerprint-authoring.mdx
+++ b/apps/docs/src/content/docs/fingerprint-authoring.mdx
@@ -34,8 +34,8 @@ weight to give human intent, existing code, and library evidence.
| Design system or UI library | Grammar-led. Describe primitives, tokens, component behavior, accessibility, and composition constraints. |
| Rebrand, redesign, or migration | Human-led transition. Capture current, target, and migration cautions. |
| Prototype becoming product | Ratification-led. Preserve only the emergent patterns humans want to keep. |
-| Fork, white label, or tenant variant | Shared base + local divergence. Keep common surface composition broad and local differences scoped. |
-| Monorepo or nested surfaces | Stack-aware. Use root guidance for broad composition and nested packages for surfaces assessed differently. |
+| Fork, white label, or tenant variant | Shared base + local divergence. Put the common surface composition in a base package and `extends` it from each variant. |
+| Monorepo or shared brand | Keep one package per product surface. A shared brand lives in its own package that the others `extends` by identity. |
@@ -43,7 +43,7 @@ weight to give human intent, existing code, and library evidence.
Ghost supports two agent authoring modes:
-- **Default** - interview first, scan as needed, draft facet edits, then
+- **Default** - interview first, scan as needed, draft node prose, then
curate.
- **Auto-draft** - scan first, draft a small starter fingerprint, then curate
the claims with a human.
@@ -61,8 +61,8 @@ Set up the Ghost fingerprint for this repo with auto-draft.
claims.
2. **Scan** - inspect routes, components, stories, tests, docs, screenshots,
copy, tokens, assets, and UI library references.
-3. **Draft** - write the smallest useful `intent.yml`, `inventory.yml`, and
- `composition.yml` entries.
+3. **Draft** - write the smallest useful node prose, reading each node through
+ the intent, inventory, and composition lenses.
4. **Curate** - have the human keep, soften, reject, scope, or record important
claims before treating them as durable surface context.
5. **Validate** - run Ghost validation and use Git review as the approval
@@ -71,8 +71,7 @@ Set up the Ghost fingerprint for this repo with auto-draft.
```bash
ghost scan --format json
ghost signals .
-ghost lint .ghost
-ghost verify .ghost --root .
+ghost validate
```
Raw repo signals are source evidence only. They can support curated inventory,
@@ -81,16 +80,45 @@ frequency may seed a draft, but it does not decide what the surface should do.
-
+
-Keep each claim in the file that will make it useful later:
+The fingerprint is a directory tree of prose nodes. The tree _is_ the graph: a
+node's identity is its file path with `.md` dropped (`marketing/email.md` is the
+node `marketing/email`), and its parent is the directory that contains it. A
+surface is just a directory; its own prose lives in that directory's
+`index.md` (`checkout/index.md` is the `checkout` surface), and the
+package-root `index.md` is the implicit `core` node that is true everywhere.
-| Facet | What belongs there |
+There is no spine file. A surface exists when its directory exists. Reserved at
+the package root are `manifest.yml` and the `checks/` subtree; every other
+`*.md` is a node. Moving a node to another directory is a rename: its id and
+parent change, and `ghost validate` reports any `relates` that no longer
+resolve.
+
+Node frontmatter carries only descriptive properties:
+
+| Property | What it does |
+| --- | --- |
+| `description` | A short summary of the node. |
+| `relates` | Lateral links to other nodes by path id (`to: core/trust`); cross-package refs use `:`, e.g. `brand:core/trust`. |
+| `incarnation` | Tags a medium-bound expression (`email`, `billboard`, `voice`, `web`); untagged nodes are essence. |
+| _passthrough_ | Free-form keys are preserved for host tooling. |
+
+
+
+
+
+You write and read a node's prose body through three lenses. They shape how you
+write; they are never frontmatter fields:
+
+| Lens | What belongs there |
| --- | --- |
-| `intent.yml` | Audience, goals, anti-goals, situations, principles, and experience contracts. |
-| `inventory.yml` | Scopes, surface types, files, routes, libraries, assets, building blocks, exemplars, and source links. |
-| `composition.yml` | Repeatable rules, layouts, structures, flows, states, content patterns, behavior, and visual arrangements. |
-| `validate.yml` | Deterministic gates that can be checked from a diff. |
+| Intent | Audience, goals, anti-goals, situations, principles, and experience contracts. |
+| Inventory | Scopes, surface types, files, routes, libraries, assets, building blocks, exemplars, and source links. |
+| Composition | Repeatable rules, layouts, structures, flows, states, content patterns, behavior, and visual arrangements. |
+
+Deterministic `checks/` gates that can be evaluated from a diff live alongside
+the tree in the `checks/` subtree.
@@ -100,50 +128,59 @@ A useful fingerprint should help future agents choose, restrain, route, anchor,
and review. It should not only describe what exists or collect every available
style detail.
-Write facet content so generation decisions become explicit:
-
-- `goals` name what generated work should preserve.
-- `anti_goals` block plausible defaults that would make the surface feel
- generic or wrong.
-- `tradeoffs` say which value wins when choices conflict.
-- `situations` route guidance by task, surface type, state, or audience need.
-- `principles` capture broad product intent.
-- `experience_contracts` turn taste, trust, recovery, or disclosure into
- obligations.
-- `composition.patterns` give repeatable layout, flow, state, content,
- behavior, or visual rules.
-- `inventory.exemplars` anchor the guidance in concrete material an agent can
- inspect.
+Write node prose so generation decisions become explicit:
+
+- Name what generated work should preserve.
+- Block plausible defaults that would make the surface feel generic or wrong.
+- Say which value wins when choices conflict.
+- Route guidance by task, surface type, state, or audience need.
+- Capture broad product intent.
+- Turn taste, trust, recovery, or disclosure into obligations.
+- Give repeatable layout, flow, state, content, behavior, or visual rules.
+- Anchor the guidance in concrete exemplars an agent can inspect.
Write less like a brand book and more like a decision engine.
-
+
-Nested fingerprints are opt-in. Create a local `.ghost/` only when a surface
-should be assessed differently from the root product fingerprint.
+One contract per package: a repo's `.ghost/` is the whole fingerprint, and
+surfaces are directories within it. Reach for a second package only when a
+distinct product owns its own fingerprint: a separate app, a shared brand, a
+tenant variant.
-Use a nested package when a surface has distinct users, information density,
-trust or recovery posture, interaction rhythm, component grammar, UI library
-usage, or review criteria for the same UI decision.
+When several packages share a brand, put the common composition in its own
+package and `extends` it by identity:
-Keep broad product-family guidance at the root. Put local obligations in the
-nearest package that owns the surface.
+```yaml
+# product/.ghost/manifest.yml
+schema: ghost.fingerprint-package/v1
+id: acme-product
+extends:
+ brand: ../brand/.ghost # map the brand contract's id to where it lives
+```
-```bash
-ghost init --scope apps/checkout
-ghost stack apps/checkout
-ghost lint --all
-ghost verify --all
+Nodes then reference inherited context by identity, never by path:
+
+```yaml
+# product/.ghost/checkout/trust.md
+---
+relates:
+ - to: brand:core/trust
+ as: reinforces
+---
```
+Inherited nodes load read-only and flow into `gather` and `validate` like local
+ones. A variant inherits the brand without seeing its siblings.
+
Uncommitted or unmerged fingerprint edits are drafts. Checked-in
-Ghost package facet files are canonical.
+Ghost package node prose is canonical.
Add deterministic checks sparingly, and only when a rule can be enforced
deterministically.
diff --git a/apps/docs/src/content/docs/getting-started.mdx b/apps/docs/src/content/docs/getting-started.mdx
index ebc88699..32077df3 100644
--- a/apps/docs/src/content/docs/getting-started.mdx
+++ b/apps/docs/src/content/docs/getting-started.mdx
@@ -1,35 +1,48 @@
---
title: Getting Started
-description: Install Ghost, author a product-surface composition fingerprint, and use it to generate, validate, compare, and govern product surfaces.
+description: Install Ghost, author a product-surface composition fingerprint, and use it to brief, validate, and review product surfaces.
kicker: Docs
section: guide
order: 10
slug: getting-started
---
-
+
Ghost captures the composition of a product surface: the intent behind it, the
materials it draws from, and the patterns that make it feel intentional. The
public package is `@anarchitecture/ghost`, and it installs one CLI: `ghost`.
-The canonical portable fingerprint is a folder:
+The canonical portable fingerprint is a directory tree of prose nodes:
```text
.ghost/
- manifest.yml
- intent.yml
- inventory.yml
- composition.yml
- validate.yml
+ manifest.yml # schema + package id
+ index.md # the core node, true everywhere
+ trust.md # another root node, also true everywhere
+ marketing/index.md # the `marketing` surface's own prose
+ marketing/email.md # a node in the marketing surface
+ checks/*.md # optional ghost.check/v1 deterministic checks
```
-Generation starts from `intent.yml`, `inventory.yml`, and `composition.yml`.
-`validate.yml` checks validate the result afterward; they are not generation input.
+The directory tree _is_ the graph. A node's identity is its file path with
+`.md` dropped (`marketing/email.md` is the node `marketing/email`), and its
+parent is the directory that contains it. A surface is a directory: its own
+prose lives in that directory's `index.md`, and the package-root `index.md` is
+the implicit `core` node. No spine file to maintain. A surface exists when its
+directory exists.
-Nested product areas can add child package roots such as
-`apps/checkout/.ghost/`. Ghost resolves fingerprint stacks
-root-to-leaf for the file or diff being reviewed.
+Folders are walls; files fill the corridor. A node inherits every file from the
+package root down to its own folder, so root nodes (`index.md`, `trust.md`)
+reach every surface while a sibling surface stays invisible. `relates` links
+nodes laterally across that boundary when a relationship carries rationale.
+
+Write and read every prose node through three lenses: intent, inventory, and
+composition. Deterministic `checks/` validate the result afterward; they are not
+generation input.
+
+One contract per package: a repo's `.ghost/` is the whole fingerprint, and
+surfaces (directories) are the only locality.
@@ -78,18 +91,19 @@ run.
The CLI handles the deterministic package work. Your agent handles the
-composition work: interviewing, reading repo evidence, drafting facet edits, and
-asking you to curate the claims.
+composition work: interviewing, reading repo evidence, drafting node prose, and
+asking you to curate the claims. `ghost init` scaffolds `manifest.yml` and a
+core `index.md` node.
```bash
ghost init
ghost scan --format json
ghost signals .
-ghost lint .ghost
-ghost verify .ghost --root .
+ghost validate
```
-The fingerprint records durable surface-composition guidance:
+Each node's prose records durable surface-composition guidance through three
+lenses:
1. **Intent** - what must remain true: what product this is, who it
serves, which situations matter, and which principles or contracts apply.
@@ -99,71 +113,51 @@ The fingerprint records durable surface-composition guidance:
3. **Composition** - the patterns that make it intentional: rules, layouts,
structures, flows, states, content, behavior, and visual arrangements.
-Raw repo signals are optional authoring evidence. Curate durable intent,
-inventory, and composition into the facet files, then use normal Git
-review for approval. For a fuller human-agent workflow, read
-[Fingerprint Authoring](/docs/fingerprint-authoring).
+These lenses shape how you write the prose body; they are never frontmatter
+fields. Node frontmatter carries only descriptive properties: `description`,
+`relates`, `incarnation`, plus free-form passthrough keys. Raw repo signals are optional
+authoring evidence. Curate durable intent, inventory, and composition into the
+node prose, then use normal Git review for approval. For a fuller human-agent
+workflow, read [Fingerprint Authoring](/docs/fingerprint-authoring).
-
+
-Before generating or revising UI, gather Relay JSON for the target path:
+Before generating or revising UI, gather the composed slice for the surface the
+work touches. The agent names the surface; Ghost composes its corridor of prose:
```bash
-ghost relay gather apps/checkout/review/page.tsx --format json
+ghost gather marketing
+ghost gather marketing/email --as email
+ghost gather marketing --format json
```
-`ghost.relay.gather/v2` is the agent contract. Agents should read `context`,
-`selected_context`, `targetPaths`, `source`, `stackDirs`, gaps, and trace fields
-from JSON. Plain `ghost relay gather ` remains a compact human preview.
-For prompt-shaped work where there is no clear path, host agents can create a
-`ghost.relay-request/v1` and run
-`ghost relay gather --request-stdin --format json`.
-Relay config controls the runtime. Omitted `base` uses the resolved fingerprint
-stack; `base.kind: none` lets frameworks provide declared request context
-without a `.ghost` package:
-
-```bash
-GHOST_RELAY_CONFIG=.agents/ghost/relay.yml ghost relay gather --request-stdin --format json
-ghost relay gather stacks/portal.renewal-reminder.email.yml --config .agents/ghost/relay.yml --format json
-```
-
-The package remains the approved product-surface context; review and check
-commands apply it after implementation.
+`gather` returns the **spine** (every node on the corridor from the root down to
+the surface, as full bodies), the **edges** reachable in one hop from any spine
+node's `relates`, and **spokes**, pointers to nearby nodes the agent can pull on
+demand. Run it before generation, so the agent builds with surface composition
+in hand rather than discovering the gaps in review.
-After implementation, run Ghost against the same fingerprint:
+After implementation, run Ghost against the same fingerprint. The agent names
+the surfaces the change touches:
```bash
-ghost check --base main
-ghost review --base main
+ghost checks --surface marketing
+ghost review --surface marketing --base main
+git diff | ghost review --surface marketing --diff -
```
-`ghost check` applies active deterministic gates from the resolved fingerprint
-stack for each changed file. `ghost review` emits advisory context grounded in
-the same selected context as Relay, selected validation checks, and the diff.
-
-Wrappers should consume `ghost check --format json` and map Ghost severities
-outside Ghost. Ghost severities remain `critical`, `serious`, and `nit`.
-
-
-
-
-
-```bash
-ghost compare market/.ghost dashboard/.ghost
-ghost stack apps/checkout/review/page.tsx
-ghost ack --stance aligned --reason "Initial baseline"
-ghost track new-tracked.fingerprint.md
-ghost diverge typography --reason "Editorial product uses a different type scale"
-```
+`ghost checks` selects and grounds the markdown checks governing the named
+surfaces. `ghost review` emits an advisory packet: touched surfaces, routed
+checks, fingerprint grounding, and the diff embedded verbatim.
-Package comparison uses canonical `.ghost/` packages. `ack`,
-`track`, and `diverge` record stance for compatibility drift workflows that
-track direct fingerprint markdown references.
+Wrappers should consume `--format json` and map Ghost severities outside Ghost.
+Ghost severities remain `critical`, `serious`, and `nit`. Advisory review is
+never a CI gate on its own.
diff --git a/apps/docs/src/contexts/ThemePanelContext.tsx b/apps/docs/src/contexts/ThemePanelContext.tsx
deleted file mode 100644
index 0b01302f..00000000
--- a/apps/docs/src/contexts/ThemePanelContext.tsx
+++ /dev/null
@@ -1,248 +0,0 @@
-"use client";
-
-import {
- ALL_THEMEABLE_KEYS,
- DEFAULT_COLORS_DARK,
- DEFAULT_COLORS_LIGHT,
- DEFAULT_SHADOWS_DARK,
- DEFAULT_SHADOWS_LIGHT,
- generateCSSExport,
- PRESETS,
- scaleRadius,
- scaleShadows,
- type ThemePreset,
- useTheme as useNextTheme,
-} from "ghost-ui";
-import type React from "react";
-import {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useRef,
- useState,
-} from "react";
-
-const STORAGE_KEY = "ghost-ui-theme";
-
-interface StoredTheme {
- presetId: string | null;
- overrides: Record;
- radiusScale: number;
- shadowScale: number;
-}
-
-interface ThemePanelContextType {
- isOpen: boolean;
- setOpen: (open: boolean) => void;
- activePresetId: string | null;
- overrides: Record;
- radiusScale: number;
- shadowScale: number;
- applyPreset: (id: string) => void;
- setVariable: (key: string, value: string) => void;
- setVariables: (vars: Record) => void;
- setRadiusScale: (factor: number) => void;
- setShadowScale: (factor: number) => void;
- reset: () => void;
- exportCSS: () => string;
-}
-
-const ThemePanelContext = createContext(
- undefined,
-);
-
-export function ThemePanelProvider({
- children,
-}: {
- children: React.ReactNode;
-}) {
- const { resolvedTheme } = useNextTheme();
- const isDark = resolvedTheme === "dark";
-
- const [isOpen, setOpen] = useState(false);
- const [activePresetId, setActivePresetId] = useState(
- "default",
- );
- const [overrides, setOverrides] = useState>({});
- const [radiusScale, setRadiusScaleState] = useState(50);
- const [shadowScale, setShadowScaleState] = useState(50);
- const [mounted, setMounted] = useState(false);
- const saveTimeoutRef = useRef>(undefined);
- const prevKeysRef = useRef>(new Set());
-
- // Load from localStorage on mount
- useEffect(() => {
- try {
- const stored = localStorage.getItem(STORAGE_KEY);
- if (stored) {
- const data: StoredTheme = JSON.parse(stored);
- setActivePresetId(data.presetId);
- setOverrides(data.overrides);
- setRadiusScaleState(data.radiusScale ?? 50);
- setShadowScaleState(data.shadowScale ?? 50);
- }
- } catch {
- // ignore corrupted data
- }
- setMounted(true);
- }, []);
-
- // Apply overrides to document whenever they change
- useEffect(() => {
- if (!mounted) return;
-
- const currentKeys = new Set(Object.keys(overrides));
-
- // Remove properties that were in previous overrides but not in current
- for (const key of prevKeysRef.current) {
- if (!currentKeys.has(key)) {
- document.documentElement.style.removeProperty(key);
- }
- }
-
- // Apply current overrides
- requestAnimationFrame(() => {
- for (const [key, value] of Object.entries(overrides)) {
- document.documentElement.style.setProperty(key, value);
- }
- });
-
- prevKeysRef.current = currentKeys;
- }, [overrides, mounted]);
-
- // Debounced save to localStorage
- useEffect(() => {
- if (!mounted) return;
-
- if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
- saveTimeoutRef.current = setTimeout(() => {
- const data: StoredTheme = {
- presetId: activePresetId,
- overrides,
- radiusScale,
- shadowScale,
- };
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
- }, 300);
-
- return () => {
- if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
- };
- }, [overrides, activePresetId, radiusScale, shadowScale, mounted]);
-
- // Re-apply preset when dark/light mode toggles
- useEffect(() => {
- if (!mounted) return;
- if (!activePresetId || activePresetId === "default") return;
-
- const preset = PRESETS.find((p) => p.id === activePresetId);
- if (!preset) return;
-
- const modeVars = isDark ? preset.variables.dark : preset.variables.light;
- if (Object.keys(modeVars).length > 0) {
- setOverrides(modeVars);
- }
- }, [isDark, mounted]); // eslint-disable-line react-hooks/exhaustive-deps
-
- const applyPreset = useCallback(
- (id: string) => {
- setActivePresetId(id);
-
- if (id === "default") {
- // Clear all overrides to restore CSS defaults
- setOverrides({});
- setRadiusScaleState(50);
- setShadowScaleState(50);
- return;
- }
-
- const preset = PRESETS.find((p) => p.id === id);
- if (!preset) return;
-
- const modeVars = isDark ? preset.variables.dark : preset.variables.light;
- setOverrides(modeVars);
- setRadiusScaleState(50);
- setShadowScaleState(50);
- },
- [isDark],
- );
-
- const setVariable = useCallback((key: string, value: string) => {
- setActivePresetId(null); // mark as custom
- setOverrides((prev) => ({ ...prev, [key]: value }));
- }, []);
-
- const setVariables = useCallback((vars: Record) => {
- setActivePresetId(null);
- setOverrides((prev) => ({ ...prev, ...vars }));
- }, []);
-
- const setRadiusScale = useCallback((factor: number) => {
- setRadiusScaleState(factor);
- setActivePresetId(null);
- const radiusVars = scaleRadius(factor);
- setOverrides((prev) => ({ ...prev, ...radiusVars }));
- }, []);
-
- const setShadowScale = useCallback(
- (factor: number) => {
- setShadowScaleState(factor);
- setActivePresetId(null);
- const shadowVars = scaleShadows(factor, isDark);
- setOverrides((prev) => ({ ...prev, ...shadowVars }));
- },
- [isDark],
- );
-
- const reset = useCallback(() => {
- // Remove all overrides from document
- for (const key of ALL_THEMEABLE_KEYS) {
- document.documentElement.style.removeProperty(key);
- }
- // Also remove any current override keys not in ALL_THEMEABLE_KEYS
- for (const key of Object.keys(overrides)) {
- document.documentElement.style.removeProperty(key);
- }
-
- setOverrides({});
- setActivePresetId("default");
- setRadiusScaleState(50);
- setShadowScaleState(50);
- localStorage.removeItem(STORAGE_KEY);
- }, [overrides]);
-
- const exportCSS = useCallback(() => {
- return generateCSSExport(overrides);
- }, [overrides]);
-
- return (
-
- {children}
-
- );
-}
-
-export function useThemePanel() {
- const context = useContext(ThemePanelContext);
- if (context === undefined) {
- throw new Error("useThemePanel must be used within a ThemePanelProvider");
- }
- return context;
-}
diff --git a/apps/docs/src/generated/cli-manifest.json b/apps/docs/src/generated/cli-manifest.json
index 3bc070d8..5c74ff8e 100644
--- a/apps/docs/src/generated/cli-manifest.json
+++ b/apps/docs/src/generated/cli-manifest.json
@@ -1,5 +1,5 @@
{
- "generatedAt": "2026-06-28T13:42:06.396Z",
+ "generatedAt": "2026-06-28T21:25:38.799Z",
"tools": [
{
"tool": "ghost",
@@ -191,7 +191,7 @@
"tool": "ghost",
"name": "migrate",
"rawName": "migrate [dir]",
- "description": "Migrate a legacy .ghost/ package onto the surface model (surfaces.yml + surface: placement).",
+ "description": "Migrate a legacy .ghost/ package onto the directory-tree node model.",
"group": "maintenance",
"defaultHelp": false,
"compactName": "migrate",
diff --git a/apps/docs/src/lib/component-docs.ts b/apps/docs/src/lib/component-docs.ts
deleted file mode 100644
index 36f760b3..00000000
--- a/apps/docs/src/lib/component-docs.ts
+++ /dev/null
@@ -1,673 +0,0 @@
-export type PropDef = {
- name: string;
- type: string;
- default?: string;
- description: string;
-};
-
-export type ExampleMeta = {
- name: string;
- title: string;
- description?: string;
-};
-
-export type ComponentDoc = {
- description: string;
- usage: string;
- props: PropDef[];
- composedWith: string[];
- examples: ExampleMeta[];
-};
-
-const docs: Record = {
- message: {
- description:
- "Renders a single chat message with support for markdown streaming, actions, and branching.",
- usage: `import {
- Message,
- MessageContent,
- MessageResponse,
- MessageActions,
- MessageAction,
-} from "ghost-ui";
-
-
-
- Hello, how can I help?
-
-
-
-
-
-
- `,
- props: [
- {
- name: "from",
- type: '"user" | "assistant" | "system"',
- description:
- "The role of the message sender, controls alignment and styling.",
- },
- {
- name: "children",
- type: "ReactNode",
- description:
- "Message sub-components (MessageContent, MessageActions, etc.).",
- },
- ],
- composedWith: [
- "conversation",
- "prompt-input",
- "reasoning",
- "chain-of-thought",
- "code-block",
- ],
- examples: [
- {
- name: "with-actions",
- title: "With Actions",
- description: "Message with copy and regenerate action buttons.",
- },
- {
- name: "streaming",
- title: "Streaming Response",
- description: "Message with an animated streaming indicator.",
- },
- ],
- },
- conversation: {
- description:
- "A scrollable container that auto-sticks to the bottom as new messages arrive.",
- usage: `import {
- Conversation,
- ConversationContent,
- ConversationScrollButton,
-} from "ghost-ui";
-
-
-
- {messages.map((msg) => (
- ...
- ))}
-
-
- `,
- props: [
- {
- name: "initial",
- type: '"smooth" | "instant" | "auto"',
- default: '"smooth"',
- description: "Scroll behavior when the component first mounts.",
- },
- {
- name: "resize",
- type: '"smooth" | "instant" | "auto"',
- default: '"smooth"',
- description: "Scroll behavior when content resizes.",
- },
- ],
- composedWith: ["message", "prompt-input", "reasoning"],
- examples: [
- {
- name: "with-messages",
- title: "With Messages",
- description: "Conversation with multiple user and assistant messages.",
- },
- ],
- },
- "prompt-input": {
- description:
- "A composable prompt input form with file attachments, commands, screenshots, and submit handling.",
- usage: `import {
- PromptInput,
- PromptInputTextarea,
- PromptInputActions,
-} from "ghost-ui";
-
- console.log(msg)}>
-
-
- `,
- props: [
- {
- name: "onSubmit",
- type: "(message: PromptInputMessage, event: FormEvent) => void | Promise",
- description: "Called when the user submits the prompt.",
- },
- {
- name: "accept",
- type: "string",
- description: 'MIME filter for file attachments, e.g. "image/*".',
- },
- {
- name: "maxFiles",
- type: "number",
- description: "Maximum number of attachable files.",
- },
- {
- name: "maxFileSize",
- type: "number",
- description: "Maximum file size in bytes.",
- },
- {
- name: "globalDrop",
- type: "boolean",
- default: "false",
- description: "Accept file drops anywhere on the document.",
- },
- ],
- composedWith: ["conversation", "message", "attachments"],
- examples: [
- {
- name: "with-attachments",
- title: "With Attachments",
- description: "PromptInput showing file attachment controls.",
- },
- ],
- },
- reasoning: {
- description:
- "A collapsible thinking indicator that auto-opens during streaming and auto-closes when done.",
- usage: `import {
- Reasoning,
- ReasoningTrigger,
- ReasoningContent,
-} from "ghost-ui";
-
-
-
- The model's internal reasoning text...
- `,
- props: [
- {
- name: "isStreaming",
- type: "boolean",
- default: "false",
- description:
- "Whether the model is currently generating reasoning tokens.",
- },
- {
- name: "duration",
- type: "number",
- description:
- "Elapsed thinking time in seconds, shown in the trigger label.",
- },
- {
- name: "open",
- type: "boolean",
- description: "Controlled open state.",
- },
- {
- name: "defaultOpen",
- type: "boolean",
- description: "Initial open state when uncontrolled.",
- },
- ],
- composedWith: ["message", "conversation", "chain-of-thought"],
- examples: [],
- },
- "chain-of-thought": {
- description:
- "Displays a step-by-step breakdown of an AI model's reasoning process with collapsible detail.",
- usage: `import {
- ChainOfThought,
- ChainOfThoughtHeader,
- ChainOfThoughtContent,
- ChainOfThoughtStep,
-} from "ghost-ui";
-
-
- Reasoning steps
-
-
-
-
- `,
- props: [
- {
- name: "open",
- type: "boolean",
- description: "Controlled open state of the collapsible.",
- },
- {
- name: "defaultOpen",
- type: "boolean",
- default: "false",
- description: "Initial open state when uncontrolled.",
- },
- {
- name: "onOpenChange",
- type: "(open: boolean) => void",
- description: "Called when the open state changes.",
- },
- ],
- composedWith: ["message", "reasoning", "conversation"],
- examples: [],
- },
- "code-block": {
- description:
- "Syntax-highlighted code viewer powered by Shiki with copy-to-clipboard, line numbers, and language selection.",
- usage: `import {
- CodeBlock,
- CodeBlockHeader,
- CodeBlockActions,
- CodeBlockCopyButton,
-} from "ghost-ui";
-
-
-
- example.ts
-
-
-
-
- `,
- props: [
- {
- name: "code",
- type: "string",
- description: "The source code string to highlight and display.",
- },
- {
- name: "language",
- type: "BundledLanguage",
- description:
- 'The programming language for syntax highlighting (e.g. "tsx", "python").',
- },
- {
- name: "showLineNumbers",
- type: "boolean",
- default: "false",
- description: "Whether to display line numbers in the gutter.",
- },
- ],
- composedWith: ["message", "artifact", "terminal"],
- examples: [
- {
- name: "with-diff",
- title: "Multi-Language",
- description:
- "CodeBlock with a language selector for multiple snippets.",
- },
- ],
- },
- agent: {
- description:
- "Displays an AI agent configuration card with name, model, instructions, tools, and output schema.",
- usage: `import {
- Agent,
- AgentHeader,
- AgentContent,
- AgentInstructions,
-} from "ghost-ui";
-
-
-
-
- Find relevant papers on the topic.
-
- `,
- props: [
- {
- name: "children",
- type: "ReactNode",
- description:
- "Agent sub-components (AgentHeader, AgentContent, AgentTools, etc.).",
- },
- ],
- composedWith: ["code-block", "message"],
- examples: [],
- },
- terminal: {
- description:
- "A terminal emulator view with ANSI color support, auto-scroll, copy, and clear actions.",
- usage: `import {
- Terminal,
- TerminalHeader,
- TerminalTitle,
- TerminalContent,
-} from "ghost-ui";
-
- `,
- props: [
- {
- name: "output",
- type: "string",
- description: "The terminal output string, supports ANSI escape codes.",
- },
- {
- name: "isStreaming",
- type: "boolean",
- default: "false",
- description: "Shows a blinking cursor when true.",
- },
- {
- name: "autoScroll",
- type: "boolean",
- default: "true",
- description: "Automatically scroll to the bottom when output changes.",
- },
- {
- name: "onClear",
- type: "() => void",
- description:
- "Callback to clear the terminal; enables the clear button when provided.",
- },
- ],
- composedWith: ["code-block", "agent", "message"],
- examples: [],
- },
- "file-tree": {
- description:
- "An interactive file system tree with expandable folders, file selection, and custom icons.",
- usage: `import {
- FileTree,
- FileTreeFolder,
- FileTreeFile,
-} from "ghost-ui";
-
-
-
-
-
- `,
- props: [
- {
- name: "expanded",
- type: "Set",
- description: "Controlled set of expanded folder paths.",
- },
- {
- name: "defaultExpanded",
- type: "Set",
- description: "Initial set of expanded folder paths when uncontrolled.",
- },
- {
- name: "selectedPath",
- type: "string",
- description: "The currently selected file or folder path.",
- },
- {
- name: "onSelect",
- type: "(path: string) => void",
- description: "Called when a file or folder is selected.",
- },
- ],
- composedWith: ["artifact", "code-block"],
- examples: [],
- },
- artifact: {
- description:
- "A panel container for generated content with a header, close button, actions, and scrollable body.",
- usage: `import {
- Artifact,
- ArtifactHeader,
- ArtifactTitle,
- ArtifactContent,
- ArtifactClose,
-} from "ghost-ui";
-
-
-
- Generated Code
-
-
- ...
- `,
- props: [
- {
- name: "children",
- type: "ReactNode",
- description:
- "Artifact sub-components (ArtifactHeader, ArtifactContent, etc.).",
- },
- ],
- composedWith: ["code-block", "file-tree", "message"],
- examples: [],
- },
- context: {
- description:
- "A hover card displaying model context window usage, token breakdown, and cost estimation.",
- usage: `import {
- Context,
- ContextTrigger,
- ContextContent,
- ContextContentHeader,
-} from "ghost-ui";
-
-
-
-
-
-
- `,
- props: [
- {
- name: "usedTokens",
- type: "number",
- description: "Number of tokens used in the current context window.",
- },
- {
- name: "maxTokens",
- type: "number",
- description: "Maximum token capacity of the model.",
- },
- {
- name: "usage",
- type: "LanguageModelUsage",
- description:
- "Detailed token usage breakdown (input, output, reasoning, cache).",
- },
- {
- name: "modelId",
- type: "string",
- description: "Model identifier used for cost estimation via tokenlens.",
- },
- ],
- composedWith: ["prompt-input", "conversation"],
- examples: [],
- },
- plan: {
- description:
- "A collapsible card showing an AI-generated plan with streaming shimmer support.",
- usage: `import {
- Plan,
- PlanHeader,
- PlanTitle,
- PlanDescription,
- PlanContent,
-} from "ghost-ui";
-
-
-
- Implementation Plan
- 3 steps to complete the task
-
- ...
- `,
- props: [
- {
- name: "isStreaming",
- type: "boolean",
- default: "false",
- description:
- "Enables shimmer animation on title and description while streaming.",
- },
- {
- name: "open",
- type: "boolean",
- description: "Controlled collapsed/expanded state.",
- },
- {
- name: "defaultOpen",
- type: "boolean",
- description: "Initial open state when uncontrolled.",
- },
- ],
- composedWith: ["message", "conversation", "chain-of-thought"],
- examples: [],
- },
- button: {
- description:
- "A versatile button with multiple visual variants, sizes, and an icon-only appearance mode.",
- usage: `import { Button } from "ghost-ui";
-
-Click me `,
- props: [
- {
- name: "variant",
- type: '"default" | "destructive" | "outline" | "secondary" | "ghost" | "link"',
- default: '"default"',
- description: "Visual style of the button.",
- },
- {
- name: "size",
- type: '"default" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm"',
- default: '"default"',
- description: "Size preset controlling height and padding.",
- },
- {
- name: "appearance",
- type: '"default" | "icon"',
- default: '"default"',
- description:
- 'Set to "icon" for square icon-only buttons that scale with size.',
- },
- {
- name: "asChild",
- type: "boolean",
- default: "false",
- description:
- "Merge props onto child element instead of rendering a .",
- },
- ],
- composedWith: ["dialog", "card"],
- examples: [],
- },
- card: {
- description:
- "A content container with header, title, description, body, action slot, and footer sections.",
- usage: `import {
- Card,
- CardHeader,
- CardTitle,
- CardDescription,
- CardContent,
-} from "ghost-ui";
-
-
-
- Card Title
- Card description text.
-
- Body content here.
- `,
- props: [
- {
- name: "children",
- type: "ReactNode",
- description:
- "Card sub-components (CardHeader, CardContent, CardFooter, etc.).",
- },
- ],
- composedWith: ["button", "dialog"],
- examples: [],
- },
- dialog: {
- description:
- "A modal dialog overlay with title, description, header/footer sections, and close button.",
- usage: `import {
- Dialog,
- DialogTrigger,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogDescription,
-} from "ghost-ui";
-
-
-
- Open
-
-
-
- Dialog Title
- Description text.
-
-
- `,
- props: [
- {
- name: "open",
- type: "boolean",
- description: "Controlled open state of the dialog.",
- },
- {
- name: "onOpenChange",
- type: "(open: boolean) => void",
- description: "Called when the dialog open state changes.",
- },
- ],
- composedWith: ["button", "card"],
- examples: [],
- },
- input: {
- description:
- "A styled text input with focus ring, validation states, and file input support.",
- usage: `import { Input } from "ghost-ui";
-
- `,
- props: [
- {
- name: "type",
- type: "string",
- default: '"text"',
- description: "HTML input type attribute.",
- },
- {
- name: "placeholder",
- type: "string",
- description: "Placeholder text shown when empty.",
- },
- ],
- composedWith: ["button", "label"],
- examples: [],
- },
- tabs: {
- description:
- "A tabbed interface for switching between panels of related content.",
- usage: `import { Tabs, TabsList, TabsTrigger, TabsContent } from "ghost-ui";
-
-
-
- Tab 1
- Tab 2
-
- Content 1
- Content 2
- `,
- props: [
- {
- name: "defaultValue",
- type: "string",
- description: "The value of the tab that should be active by default.",
- },
- {
- name: "value",
- type: "string",
- description: "Controlled active tab value.",
- },
- {
- name: "onValueChange",
- type: "(value: string) => void",
- description: "Called when the active tab changes.",
- },
- ],
- composedWith: ["card"],
- examples: [],
- },
-};
-
-export function getComponentDoc(slug: string): ComponentDoc | undefined {
- return docs[slug];
-}
diff --git a/apps/docs/src/lib/component-registry.ts b/apps/docs/src/lib/component-registry.ts
deleted file mode 100644
index 554a609a..00000000
--- a/apps/docs/src/lib/component-registry.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import registryData from "ghost-ui/registry.json";
-
-// ── Category metadata ──
-// Display order and names for the functional categories in registry.json.
-// "ai" is a tag, not a navigation category — it's used as a badge instead.
-
-const categoryMeta: Record = {
- input: {
- name: "Inputs",
- description: "Form controls and interactive inputs",
- },
- display: {
- name: "Display",
- description: "Visual content and presentation",
- },
- feedback: {
- name: "Feedback",
- description: "Alerts, notifications, and status indicators",
- },
- navigation: {
- name: "Navigation",
- description: "Menus, tabs, and wayfinding",
- },
- layout: {
- name: "Layout",
- description: "Content organization and structure",
- },
- chat: { name: "Chat", description: "Conversational AI interfaces" },
- code: {
- name: "Code",
- description: "Code editing, display, and development tools",
- },
- media: { name: "Media", description: "Audio, video, and rich media" },
-};
-
-const categoryOrder = Object.keys(categoryMeta);
-
-// ── Types ──
-
-export type Category = {
- slug: string;
- name: string;
- description: string;
-};
-
-export type ComponentEntry = {
- slug: string;
- name: string;
- categories: string[];
- primaryCategory: string;
- dependencies: string[];
- registryDependencies: string[];
- isAI: boolean;
- /** Where the demo file lives, if it exists */
- demoSource: "primitives" | "ai-elements";
-};
-
-// ── Helpers ──
-
-function slugToDisplayName(slug: string): string {
- return slug
- .split("-")
- .map((w) => {
- // Special casing
- if (w === "otp") return "OTP";
- if (w === "jsx") return "JSX";
- if (w === "api") return "API";
- return w.charAt(0).toUpperCase() + w.slice(1);
- })
- .join(" ");
-}
-
-function getPrimaryCategory(cats: string[]): string {
- const functional = cats.filter((c) => c !== "ai");
- if (functional.length === 0) return "display";
- // Pick the one that appears first in our defined order
- for (const cat of categoryOrder) {
- if (functional.includes(cat)) return cat;
- }
- return functional[0];
-}
-
-// ── Derived data ──
-
-type RegistryItem = {
- name: string;
- type: string;
- dependencies?: string[];
- registryDependencies?: string[];
- files?: { type: string; target: string; path: string }[];
- categories?: string[];
-};
-
-const uiItems = (registryData.items as RegistryItem[]).filter(
- (item) => item.type === "registry:ui",
-);
-
-const componentMap = new Map();
-
-for (const item of uiItems) {
- const cats = item.categories ?? [];
- const isAI = cats.includes("ai");
-
- // Determine demo directory from the component's file path
- const filePath = item.files?.[0]?.path ?? "";
- const demoSource: "primitives" | "ai-elements" = filePath.includes(
- "ai-elements",
- )
- ? "ai-elements"
- : "primitives";
-
- const entry: ComponentEntry = {
- slug: item.name,
- name: slugToDisplayName(item.name),
- categories: cats,
- primaryCategory: getPrimaryCategory(cats),
- dependencies: item.dependencies ?? [],
- registryDependencies: (item.registryDependencies ?? []).filter(
- (d) => d !== "utils",
- ),
- isAI,
- demoSource,
- };
-
- componentMap.set(item.name, entry);
-}
-
-// Sort components alphabetically within each category
-const allComponents = Array.from(componentMap.values()).sort((a, b) =>
- a.slug.localeCompare(b.slug),
-);
-
-// ── Public API ──
-
-export const categories: Category[] = categoryOrder
- .filter((slug) => allComponents.some((c) => c.primaryCategory === slug))
- .map((slug) => ({
- slug,
- ...categoryMeta[slug],
- }));
-
-export function getAllComponents(): ComponentEntry[] {
- return allComponents;
-}
-
-export function getComponent(slug: string): ComponentEntry | undefined {
- return componentMap.get(slug);
-}
-
-export function getCategory(slug: string): Category | undefined {
- return categories.find((c) => c.slug === slug);
-}
-
-export function getComponentsByCategory(
- categorySlug: string,
-): ComponentEntry[] {
- return allComponents.filter((c) => c.primaryCategory === categorySlug);
-}
diff --git a/apps/docs/src/lib/component-source.ts b/apps/docs/src/lib/component-source.ts
deleted file mode 100644
index 7795cfab..00000000
--- a/apps/docs/src/lib/component-source.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import registryData from "ghost-ui/registry.json";
-
-// ── Types ──
-
-export type VariantDef = {
- name: string;
- values: string[];
- defaultValue?: string;
-};
-
-export type ComponentSpec = {
- /** All exported component/function names */
- exports: string[];
- /** CVA variant definitions, if any */
- variants: VariantDef[];
- /** data-slot values used in this component */
- dataSlots: string[];
- /** The raw source code */
- source: string;
- /** File path relative to project root */
- filePath: string;
-};
-
-type RegistryItem = {
- name: string;
- type: string;
- files?: { path: string }[];
-};
-
-// ── Raw source imports via Vite glob ──
-
-const rawSources: Record = {};
-
-const uiModules = import.meta.glob("/src/components/ui/**/*.tsx", {
- query: "?raw",
- eager: true,
-}) as Record;
-
-const aiModules = import.meta.glob("/src/components/ai-elements/**/*.tsx", {
- query: "?raw",
- eager: true,
-}) as Record;
-
-for (const [key, mod] of Object.entries({ ...uiModules, ...aiModules })) {
- // Glob keys are like "/src/components/ui/button.tsx"
- // Registry paths are like "src/components/ui/button.tsx"
- const normalized = key.startsWith("/") ? key.slice(1) : key;
- rawSources[normalized] = mod.default;
-}
-
-// ── Extraction helpers ──
-
-function extractExports(source: string): string[] {
- // Match named exports at the end of file: export { Foo, Bar, baz }
- const exportBlockRe = /export\s*\{([^}]+)\}/g;
- const exports: string[] = [];
-
- let match;
- while ((match = exportBlockRe.exec(source)) !== null) {
- const names = match[1].split(",").map((s) =>
- s
- .trim()
- .split(/\s+as\s+/)
- .pop()!
- .trim(),
- );
- exports.push(...names.filter(Boolean));
- }
-
- // Match: export function Foo / export const Foo
- const exportDeclRe =
- /export\s+(?:function|const|class)\s+([A-Z][A-Za-z0-9]*)/g;
- while ((match = exportDeclRe.exec(source)) !== null) {
- if (!exports.includes(match[1])) {
- exports.push(match[1]);
- }
- }
-
- return exports;
-}
-
-function extractVariants(source: string): VariantDef[] {
- // Find the CVA variants block
- const variantsBlockRe =
- /variants:\s*\{([\s\S]*?)\},\s*(?:compoundVariants|defaultVariants)/;
- const variantsMatch = variantsBlockRe.exec(source);
- if (!variantsMatch) return [];
-
- const variantsBlock = variantsMatch[1];
-
- // Find default variants
- const defaultsRe = /defaultVariants:\s*\{([^}]*)\}/;
- const defaultsMatch = defaultsRe.exec(source);
- const defaults: Record = {};
-
- if (defaultsMatch) {
- const defaultEntries = defaultsMatch[1].matchAll(
- /(\w[\w-]*)\s*:\s*"([^"]*)"/g,
- );
- for (const entry of defaultEntries) {
- defaults[entry[1]] = entry[2];
- }
- }
-
- // Parse each variant group: variantName: { value1: "...", value2: "..." }
- const variantDefs: VariantDef[] = [];
- const variantGroupRe = /(\w[\w-]*)\s*:\s*\{/g;
- let groupMatch;
-
- while ((groupMatch = variantGroupRe.exec(variantsBlock)) !== null) {
- const variantName = groupMatch[1];
- // Find the matching closing brace
- let depth = 1;
- let i = groupMatch.index + groupMatch[0].length;
- while (i < variantsBlock.length && depth > 0) {
- if (variantsBlock[i] === "{") depth++;
- if (variantsBlock[i] === "}") depth--;
- i++;
- }
- const groupContent = variantsBlock.slice(
- groupMatch.index + groupMatch[0].length,
- i - 1,
- );
-
- // Extract value names (keys of the object, anchored to line start to skip Tailwind prefixes like hover:)
- const valueRe = /^\s*["']?([\w-]+)["']?\s*:/gm;
- const values: string[] = [];
- let valueMatch;
- while ((valueMatch = valueRe.exec(groupContent)) !== null) {
- if (!values.includes(valueMatch[1])) {
- values.push(valueMatch[1]);
- }
- }
-
- if (values.length > 0) {
- variantDefs.push({
- name: variantName,
- values,
- defaultValue: defaults[variantName],
- });
- }
- }
-
- return variantDefs;
-}
-
-function extractDataSlots(source: string): string[] {
- const slotRe = /data-slot="([^"]+)"/g;
- const slots: string[] = [];
- let match;
- while ((match = slotRe.exec(source)) !== null) {
- if (!slots.includes(match[1])) {
- slots.push(match[1]);
- }
- }
- return slots;
-}
-
-// ── Public API ──
-
-export function getComponentSpec(slug: string): ComponentSpec | null {
- const items = registryData.items as RegistryItem[];
- const item = items.find((i) => i.name === slug && i.type === "registry:ui");
- if (!item?.files?.[0]?.path) return null;
-
- const filePath = item.files[0].path;
- const source = rawSources[filePath];
- if (!source) return null;
-
- return {
- exports: extractExports(source),
- variants: extractVariants(source),
- dataSlots: extractDataSlots(source),
- source,
- filePath,
- };
-}
diff --git a/packages/ghost/README.md b/packages/ghost/README.md
index bdef9c74..093171c5 100644
--- a/packages/ghost/README.md
+++ b/packages/ghost/README.md
@@ -2,24 +2,24 @@
**A unified Ghost CLI for product-surface composition fingerprints.**
-Ghost captures the composition of a product surface: the intent behind it, the
-materials it draws from, and the patterns that make it feel intentional. It
-stores that composition in a repo-local `.ghost/` package that host
-agents can read before generation and validate after changes.
+Agents can assemble UI. They can't reliably preserve the _composition_ behind
+it: the hierarchy, density, restraint, copy, trust, and flow that make a surface
+feel intentional. Ghost captures that composition in a repo-local `.ghost/`
+package that a host agent reads before it builds and checks after it changes.
This package ships one CLI: `ghost`.
## Project Status: Beta
> [!WARNING]
-> Ghost is pre-1.0 and under active development. The CLI, fingerprint schema,
-> on-disk `.ghost/` package shape, and public JavaScript exports may
-> change in breaking ways before a stable 1.0 release.
+> Ghost is pre-1.0 and under active development. The CLI, node schema, on-disk
+> `.ghost/` package shape, and public JavaScript exports may change in breaking
+> ways before a stable 1.0 release.
>
> Breaking changes may ship in minor versions while Ghost is pre-1.0. Patch
> versions are reserved for fixes that should not require migration. If you adopt
-> Ghost today, expect some churn, pin the version you depend on, and review
-> release notes before upgrading.
+> Ghost today, pin the version you depend on and review release notes before
+> upgrading.
## Install
@@ -30,7 +30,27 @@ npx ghost --help --all
```
`ghost --help` shows the core workflow. `ghost --help --all` shows the complete
-command index.
+command index, and `ghost --help` shows flags for one command.
+
+## The Shape
+
+A fingerprint is a directory tree of prose, a **graph of nodes**:
+
+```text
+.ghost/
+ manifest.yml # schema + id
+ index.md # the core node, true everywhere (optional)
+ /index.md # a surface's own prose (the directory is the surface)
+ /.md # a prose node placed in that surface
+ checks/*.md # optional ghost.check/v1 checks
+```
+
+The **directory tree is the graph**. A node is one markdown file: descriptive
+frontmatter (`description`, `relates`, `incarnation`) plus a prose body written
+through three lenses: **intent** (the why), **inventory** (the materials), and
+**composition** (the patterns). A node's id is its path and its parent is its
+directory; a surface is just a directory, and the package-root `index.md` is the
+implicit `core` node that reaches every surface.
## Use
@@ -38,65 +58,63 @@ Create and validate the fingerprint package:
```bash
ghost init
-ghost scan --format json
-ghost lint .ghost
-ghost verify .ghost --root .
+ghost validate
+ghost scan
```
Gather context before generation:
```bash
-ghost relay gather apps/checkout/review/page.tsx
+ghost gather # list nodes by id + description
+ghost gather checkout # compose a surface's context slice
```
Govern changes afterward:
```bash
-ghost check --base main
-ghost review --base main
+ghost checks --surface checkout
+ghost review --surface checkout --base main
```
-Install the BYOA skill bundle so your host agent can author, brief, review,
-verify, remediate, and update fingerprints:
+Install the BYOA skill bundle so your host agent can author, brief, review, and
+verify fingerprints:
```bash
ghost skill install
```
-Advanced commands such as `signals`, `stack`, `compare`, `ack`, `track`, and
-`diverge` remain available in the full command index.
+Advanced and maintenance commands (`signals` and `migrate`) remain available
+in the full command index.
-Zero config for every verb. No API key is required. `OPENAI_API_KEY` /
-`VOYAGE_API_KEY` are optional and only used by semantic embedding helpers when a
-host opts in.
+No API key is required. `OPENAI_API_KEY` / `VOYAGE_API_KEY` are optional and
+only used by semantic embedding helpers when a host opts in.
## Library
```ts
-import { compare } from "@anarchitecture/ghost/compare";
-import { runGhostCheck } from "@anarchitecture/ghost/govern";
-import { gatherRelayContext } from "@anarchitecture/ghost/relay";
import {
initFingerprintPackage,
lintFingerprintPackage,
- verifyFingerprintPackage,
} from "@anarchitecture/ghost/fingerprint";
+import { buildCli } from "@anarchitecture/ghost/cli";
```
+Available subpath exports: `@anarchitecture/ghost`,
+`@anarchitecture/ghost/scan`, `@anarchitecture/ghost/fingerprint`,
+`@anarchitecture/ghost/core`, and `@anarchitecture/ghost/cli`.
+
## BYOA
Ghost is bring-your-own-agent. The CLI performs deterministic work: repo
-signals, readiness reporting, linting, verification, comparison, checks, and
-advisory review packet generation. The installed `ghost` skill teaches a host
-agent how to capture canonical `.ghost/` surface-composition
-context, brief and generate work from it, review changes against it, verify
-generated UI, remediate issues, and suggest fingerprint edits when the user
-asks.
+signals, contribution reporting, graph validation, context composition, check
+routing, and advisory review packets. The installed `ghost` skill teaches a host
+agent how to capture canonical `.ghost/` surface-composition context, brief and
+generate work from it, review changes against it, and verify generated UI.
```text
Set up the Ghost fingerprint for this repo.
Brief this work from the Ghost fingerprint.
-Review this PR against the Ghost fingerprint.
+Review this change against the Ghost fingerprint.
```
## Maintainers
diff --git a/packages/ghost/src/commands/gather-command.ts b/packages/ghost/src/commands/gather-command.ts
index 38efbeab..5a2a9267 100644
--- a/packages/ghost/src/commands/gather-command.ts
+++ b/packages/ghost/src/commands/gather-command.ts
@@ -158,5 +158,21 @@ function formatSliceMarkdown(slice: GraphSlice): string {
}
}
+ // Spokes: pointers the agent may pull on demand (descendants + edge hubs).
+ if (slice.spokes.length > 0) {
+ lines.push(
+ "",
+ "## Available to pull",
+ "",
+ "Pointers to nearby context — run `ghost gather ` to expand.",
+ "",
+ );
+ for (const spoke of slice.spokes) {
+ const via = spoke.kind === "edge-hub" ? ` _(via \`${spoke.hub}\`)_` : "";
+ lines.push(`- \`${spoke.id}\`${via}`);
+ if (spoke.description) lines.push(` - ${spoke.description}`);
+ }
+ }
+
return `${lines.join("\n")}\n`;
}
diff --git a/packages/ghost/src/commands/migrate-command.ts b/packages/ghost/src/commands/migrate-command.ts
index d761cd0e..23f0673e 100644
--- a/packages/ghost/src/commands/migrate-command.ts
+++ b/packages/ghost/src/commands/migrate-command.ts
@@ -1,7 +1,7 @@
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import type { CAC } from "cac";
-import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
+import { parse as parseYaml } from "yaml";
import { resolveFingerprintPackage } from "../fingerprint.js";
import {
looksLegacy,
@@ -15,7 +15,7 @@ export function registerMigrateCommand(cli: CAC): void {
cli
.command(
"migrate [dir]",
- "Migrate a legacy .ghost/ package onto the surface model (surfaces.yml + surface: placement).",
+ "Migrate a legacy .ghost/ package onto the directory-tree node model.",
)
.option("--dry-run", "Print the migration plan and report; write nothing")
.option("--force", "Overwrite existing facet files with the migrated form")
@@ -50,7 +50,7 @@ export function registerMigrateCommand(cli: CAC): void {
`${JSON.stringify(reportJson(result), null, 2)}\n`,
);
} else {
- process.stdout.write(formatReport(result, paths.surfaces));
+ process.stdout.write(formatReport(result, paths.packageDir));
}
if (opts.dryRun) {
@@ -61,7 +61,6 @@ export function registerMigrateCommand(cli: CAC): void {
await writeMigrated(
{
packageDir: paths.packageDir,
- surfaces: paths.surfaces,
facetFiles: [paths.intent, paths.inventory, paths.composition],
},
result,
@@ -96,24 +95,22 @@ async function readYaml(
async function writeMigrated(
paths: {
packageDir: string;
- surfaces: string;
facetFiles: string[];
},
result: MigrationResult,
force: boolean,
): Promise {
- // One-way conversion to the node form: surfaces.yml (spine) + nodes/*.md.
- // Facet files are removed; Git history preserves the old form.
+ // One-way conversion to the directory-tree node form. Facet files are
+ // removed; Git history preserves the old form.
const nodeFiles = migratedNodeFiles(result);
- const writes: Array<[string, string]> = [
- [paths.surfaces, stringifyYaml(result.surfaces)],
- ...nodeFiles.map((file): [string, string] => [
+ const writes: Array<[string, string]> = nodeFiles.map(
+ (file): [string, string] => [
join(paths.packageDir, file.relativePath),
file.content,
- ]),
- ];
+ ],
+ );
- // Ensure nested dirs (nodes/) exist.
+ // Ensure nested surface directories exist.
const dirs = new Set(writes.map(([path]) => dirname(path)));
await Promise.all([...dirs].map((dir) => mkdir(dir, { recursive: true })));
@@ -147,18 +144,18 @@ function isExisting(err: unknown): boolean {
function reportJson(result: MigrationResult): Record {
return {
- surfaces: (result.surfaces.surfaces as unknown[]) ?? [],
+ surfaces: result.surfaceIds,
notes: result.notes,
};
}
-function formatReport(result: MigrationResult, surfacesPath: string): string {
- const surfaces = (result.surfaces.surfaces as Array<{ id: string }>) ?? [];
+function formatReport(result: MigrationResult, packageDir: string): string {
+ const surfaceIds = result.surfaceIds;
const lines: string[] = ["# Ghost Migration"];
lines.push(
"",
- `Derived ${surfaces.length} surface(s) → ${surfacesPath}`,
- ...surfaces.map((surface) => ` - \`${surface.id}\``),
+ `Derived ${surfaceIds.length} surface director(ies) under ${packageDir}/`,
+ ...surfaceIds.map((id) => ` - \`${id}/\``),
);
lines.push("", `## Review (${result.notes.length})`);
if (result.notes.length === 0) {
diff --git a/packages/ghost/src/ghost-core/graph/assemble.ts b/packages/ghost/src/ghost-core/graph/assemble.ts
index 20e2c2b0..3cda91be 100644
--- a/packages/ghost/src/ghost-core/graph/assemble.ts
+++ b/packages/ghost/src/ghost-core/graph/assemble.ts
@@ -1,16 +1,30 @@
import type { GhostNodeDocument } from "../node/types.js";
-import type { GhostSurfacesDocument } from "../surfaces/types.js";
import {
GHOST_GRAPH_ROOT_ID,
type GhostGraph,
type GhostGraphNode,
} from "./types.js";
+/**
+ * One local node located in the package directory tree: its computed path id,
+ * the id of its containing directory (absent ⇒ the node *is* the `core` root,
+ * i.e. a package-root `index.md`), and the parsed document.
+ */
+export interface PlacedNode {
+ id: string;
+ parent?: string;
+ /**
+ * The node's file folder — the directory its source file sits in. For an
+ * index node this is its own id (`a/b/index.md` → `a/b`); for a leaf it is
+ * the parent (`a/b.md` → `a`); for the root `index.md` it is `""`.
+ */
+ folder: string;
+ doc: GhostNodeDocument;
+}
+
export interface AssembleGraphInput {
- /** Authored on-disk node files (parsed `ghost.node/v1` documents). */
- nodeFiles?: GhostNodeDocument[];
- /** The explicit surface tree, which seeds tree nodes even when empty. */
- surfaces?: GhostSurfacesDocument;
+ /** Local nodes located in the package's directory tree. */
+ placedNodes?: PlacedNode[];
/**
* Read-only nodes inherited from extended packages. Their ids are already
* qualified (`:`). Local nodes never override these and
@@ -22,10 +36,11 @@ export interface AssembleGraphInput {
/**
* Fold the package's sources into one in-memory prose-node graph.
*
- * Authored node files are unioned with the surface tree (`surfaces.yml`), which
- * seeds containment so a surface with no node still exists as a tree position,
- * plus any read-only nodes inherited from extended packages. The implicit
- * `core` root is never required to be declared.
+ * Local nodes are the package's directory tree: each node's id is its path and
+ * its parent is its containing directory. Intermediate directories that hold no
+ * `index.md` are still materialized as bare tree positions so children resolve.
+ * Inherited nodes from extended packages join as read-only context. The
+ * implicit `core` root is never required to be declared.
*/
export function assembleGraph(input: AssembleGraphInput): GhostGraph {
const nodes = new Map();
@@ -35,21 +50,25 @@ export function assembleGraph(input: AssembleGraphInput): GhostGraph {
nodes.set(node.id, node);
}
- for (const doc of input.nodeFiles ?? []) {
- const fm = doc.frontmatter;
- nodes.set(fm.id, {
- id: fm.id,
+ for (const placed of input.placedNodes ?? []) {
+ const fm = placed.doc.frontmatter;
+ // A node whose parent is absent is the package-root index — the core node.
+ const id = placed.parent === undefined ? GHOST_GRAPH_ROOT_ID : placed.id;
+ nodes.set(id, {
+ id,
...(fm.description !== undefined ? { description: fm.description } : {}),
- ...(fm.under !== undefined ? { under: fm.under } : {}),
+ ...(placed.parent !== undefined ? { parent: placed.parent } : {}),
+ folder: placed.folder,
relates: fm.relates ?? [],
...(fm.incarnation !== undefined ? { incarnation: fm.incarnation } : {}),
- body: doc.body,
+ body: placed.doc.body,
origin: "node-file",
});
}
- // Build the containment tree. Surfaces seed positions; node `under` edges and
- // surface `parent` edges both contribute. The root (`core`) has no parent.
+ // Build the containment tree from each node's parent (its directory). The
+ // root (`core`) has no parent. Intermediate directories with no index node
+ // are seeded as bare positions so the chain resolves to the root.
const parents = new Map();
const children = new Map();
@@ -64,25 +83,30 @@ export function assembleGraph(input: AssembleGraphInput): GhostGraph {
}
};
- // Surface tree edges (the authoritative spine in Phase 2).
- for (const surface of input.surfaces?.surfaces ?? []) {
- if (surface.id === GHOST_GRAPH_ROOT_ID) continue;
- link(surface.id, surface.parent ?? GHOST_GRAPH_ROOT_ID);
- }
-
- // Node containment: a node `under` X is a child of X. A placed node whose
- // `under` is itself a node id nests under that node; otherwise it attaches to
- // the named surface (or core).
for (const node of nodes.values()) {
if (node.id === GHOST_GRAPH_ROOT_ID) continue;
- if (node.under !== undefined) {
- link(node.id, node.under);
+ if (node.origin === "inherited") continue;
+ link(node.id, node.parent ?? GHOST_GRAPH_ROOT_ID);
+ // Seed any ancestor directories that have no index node of their own, so a
+ // deep node (a/b/c) still has a/b → a → core links even when a, a/b are
+ // empty directories.
+ let current = node.parent;
+ while (current !== undefined && current !== GHOST_GRAPH_ROOT_ID) {
+ const grandparent = parentIdOf(current);
+ link(current, grandparent ?? GHOST_GRAPH_ROOT_ID);
+ current = grandparent;
}
}
return { nodes, parents, children };
}
+/** The id of the directory containing `id`, or undefined when `id` is top-level. */
+function parentIdOf(id: string): string | undefined {
+ const slash = id.lastIndexOf("/");
+ return slash === -1 ? undefined : id.slice(0, slash);
+}
+
/** The ancestor chain for a node id, nearest parent first, ending at the root. */
export function ancestorChain(graph: GhostGraph, id: string): string[] {
const chain: string[] = [];
diff --git a/packages/ghost/src/ghost-core/graph/index.ts b/packages/ghost/src/ghost-core/graph/index.ts
index 426a83ce..053b3498 100644
--- a/packages/ghost/src/ghost-core/graph/index.ts
+++ b/packages/ghost/src/ghost-core/graph/index.ts
@@ -1,13 +1,14 @@
/**
* Public surface for the in-memory fingerprint graph — the only fingerprint
- * model. The graph is folded from authored node files + the surface tree, and
- * is what every consumer traverses (gather, checks, validate).
+ * model. The graph is folded from the package's directory tree of prose nodes,
+ * and is what every consumer traverses (gather, checks, validate).
*/
export {
type AssembleGraphInput,
ancestorChain,
assembleGraph,
+ type PlacedNode,
} from "./assemble.js";
export {
type GraphLintIssue,
diff --git a/packages/ghost/src/ghost-core/graph/lint.ts b/packages/ghost/src/ghost-core/graph/lint.ts
index 1b5df230..9664a02b 100644
--- a/packages/ghost/src/ghost-core/graph/lint.ts
+++ b/packages/ghost/src/ghost-core/graph/lint.ts
@@ -1,4 +1,4 @@
-import { GHOST_GRAPH_ROOT_ID, type GhostGraph } from "./types.js";
+import type { GhostGraph } from "./types.js";
export type GraphLintSeverity = "error" | "warning" | "info";
@@ -20,39 +20,23 @@ export interface GraphLintReport {
/**
* The graph pass of `validate`: the ghost-specific network is correct.
*
- * - every `under` parent resolves to a node or a declared surface tree position;
- * - every local `relates` target resolves (cross-package `pkg#id` refs are
- * skipped here — they are resolved in the cross-package phase);
- * - exactly one root (no `under`) — the implicit `core`;
- * - the containment graph is acyclic.
+ * Containment comes from the directory tree (a node's parent is its directory),
+ * so parent edges resolve by construction — there is no "unresolved parent" to
+ * check. What remains is the network correctness the layout cannot guarantee:
+ *
+ * - every local `relates` target resolves (cross-package `pkg:id` refs resolve
+ * against inherited nodes; an unknown one is reported);
+ * - the containment graph reaches the single implicit `core` root (it always
+ * does by construction; verified defensively);
+ * - the containment graph is acyclic (a directory tree is, defensively checked).
*
* Pure: operates on the assembled in-memory graph, no I/O.
*/
export function lintGraph(graph: GhostGraph): GraphLintReport {
const issues: GraphLintIssue[] = [];
const ids = new Set(graph.nodes.keys());
- // Valid containment targets: nodes, declared surface tree positions, and the
- // implicit root. Surfaces are tree positions (in parents/children), not nodes.
- const treePositions = new Set([
- GHOST_GRAPH_ROOT_ID,
- ...graph.parents.keys(),
- ...graph.children.keys(),
- ]);
for (const node of graph.nodes.values()) {
- // under must resolve to a known node or surface tree position
- if (
- node.under !== undefined &&
- !ids.has(node.under) &&
- !treePositions.has(node.under)
- ) {
- issues.push({
- severity: "error",
- rule: "unresolved-parent",
- message: `node '${node.id}' is under '${node.under}', which is not a known node or surface.`,
- node: node.id,
- });
- }
// relates targets must resolve. A `:` ref resolves to an
// inherited node (id-keyed the same way) — same lookup, no special case.
for (const relation of node.relates) {
@@ -67,25 +51,8 @@ export function lintGraph(graph: GhostGraph): GraphLintReport {
}
}
- // Exactly one root: the implicit core. Nodes with no `under` are roots.
- // Inherited (extended-package) nodes are read-only context, not part of this
- // package's tree — they are exempt from the single-root rule.
- const roots = [...graph.nodes.values()].filter(
- (node) =>
- node.under === undefined &&
- node.id !== GHOST_GRAPH_ROOT_ID &&
- node.origin !== "inherited",
- );
- for (const root of roots) {
- issues.push({
- severity: "error",
- rule: "multiple-roots",
- message: `node '${root.id}' has no 'under'; every node must descend from the implicit '${GHOST_GRAPH_ROOT_ID}' root (give it an 'under').`,
- node: root.id,
- });
- }
-
- // Cycle detection over containment.
+ // Cycle detection over containment (defensive — a directory tree cannot cycle,
+ // but inherited/seeded positions are checked for safety).
for (const node of graph.nodes.values()) {
const seen = new Set();
let cursor: string | undefined = node.id;
@@ -94,13 +61,13 @@ export function lintGraph(graph: GhostGraph): GraphLintReport {
issues.push({
severity: "error",
rule: "containment-cycle",
- message: `node '${node.id}' is part of an 'under' cycle.`,
+ message: `node '${node.id}' is part of a containment cycle.`,
node: node.id,
});
break;
}
seen.add(cursor);
- cursor = graph.nodes.get(cursor)?.under;
+ cursor = graph.parents.get(cursor);
}
}
diff --git a/packages/ghost/src/ghost-core/graph/menu.ts b/packages/ghost/src/ghost-core/graph/menu.ts
index bed23a64..efbe1f49 100644
--- a/packages/ghost/src/ghost-core/graph/menu.ts
+++ b/packages/ghost/src/ghost-core/graph/menu.ts
@@ -38,12 +38,12 @@ export function buildGraphMenu(graph: GhostGraph): GraphMenuEntry[] {
entries.push({
id: node.id,
...(node.description ? { description: node.description } : {}),
- parent: node.under ?? GHOST_GRAPH_ROOT_ID,
+ parent: node.parent ?? GHOST_GRAPH_ROOT_ID,
});
}
- // Tree positions declared only in the spine file (surfaces.yml) — no node of
- // their own yet — are still anchorable. Include them as bare entries.
+ // Intermediate directories with no index node of their own are still
+ // anchorable tree positions. Include them as bare entries.
for (const [id, parent] of graph.parents) {
if (seen.has(id)) continue;
seen.add(id);
diff --git a/packages/ghost/src/ghost-core/graph/slice.ts b/packages/ghost/src/ghost-core/graph/slice.ts
index d28464ef..60be1caa 100644
--- a/packages/ghost/src/ghost-core/graph/slice.ts
+++ b/packages/ghost/src/ghost-core/graph/slice.ts
@@ -3,10 +3,10 @@ import { ancestorChain } from "./assemble.js";
import { GHOST_GRAPH_ROOT_ID, type GhostGraph } from "./types.js";
/**
- * Why a node is present in a resolved slice.
- * - `own`: placed directly on the requested surface.
- * - `ancestor`: placed on an ancestor and cascaded down the tree.
- * - `edge`: contributed by a typed `relates` link from a slice node (one hop).
+ * Why a full-body node is present in a resolved slice.
+ * - `own`: a file in the requested surface's own folder.
+ * - `ancestor`: a file in a folder higher on the corridor (root → surface).
+ * - `edge`: contributed by a typed `relates` link from a spine node (one hop).
*/
export type GraphSliceProvenance =
| { kind: "own" }
@@ -20,6 +20,24 @@ export interface GraphSliceNode {
provenance: GraphSliceProvenance;
}
+/**
+ * A spoke: a node offered as a pointer (id + description, no body) for the agent
+ * to pull on demand. Spokes are navigable optionality, never authoritative
+ * context.
+ * - `descendant`: a node within or below the requested surface's own subtree.
+ * - `edge-hub`: a node within or below an `edge` target's subtree (a hub the
+ * surface `relates` to unfolds its menu).
+ */
+export type GraphSpokeKind = "descendant" | "edge-hub";
+
+export interface GraphSlicePointer {
+ id: string;
+ description?: string;
+ kind: GraphSpokeKind;
+ /** For an `edge-hub` spoke, the hub id it belongs to. */
+ hub?: string;
+}
+
export interface GraphSlice {
/** The requested node/surface id. */
surface: string;
@@ -27,7 +45,10 @@ export interface GraphSlice {
ancestors: string[];
/** The `--as` incarnation filter applied, if any. */
incarnation?: string;
+ /** Full-body context: the corridor spine plus one-hop `relates` edges. */
nodes: GraphSliceNode[];
+ /** Pointers (id + description) the agent may pull: descendants + edge hubs. */
+ spokes: GraphSlicePointer[];
}
export interface ResolveGraphSliceOptions {
@@ -36,17 +57,23 @@ export interface ResolveGraphSliceOptions {
}
/**
- * Compose a context slice for a surface by traversing the graph, deterministic
- * and with no I/O or LLM:
+ * Compose a context slice for a surface by traversing the graph — deterministic,
+ * no I/O, no LLM. The model is "folders are walls; files fill the corridor":
*
- * - own: nodes placed directly on the requested id;
- * - ancestor: nodes on each `under` ancestor up to `core` cascade down;
- * - edge: for each slice node's `relates`, the target node's body is included
- * once (one hop, no recursion), tagged by the relation qualifier.
+ * - **spine** (`own`/`ancestor`): every node whose **file folder** is on the
+ * surface's corridor — the chain of folders from the package root down to the
+ * surface's own folder. A sibling folder is a wall: its nodes never appear.
+ * Spine nodes are full bodies.
+ * - **edge**: for each spine node's `relates`, the target node's body is
+ * included once (one hop, no recursion), tagged by the relation qualifier.
+ * This is how a broad rule placed high in the corridor (e.g. "all feature UI
+ * draws on Arcade", authored once on `features`) reaches every descendant.
+ * - **spokes**: descendants of the surface's own folder, plus descendants of
+ * each edge target (a hub the surface draws on unfolds its menu), as pointers.
*
- * The `incarnation` option filters: a node with no incarnation (essence) is
- * always included; a tagged node is included only when it matches; absent
- * option means no filtering.
+ * The `incarnation` option filters full-body nodes: essence (untagged) always
+ * passes; a tagged node passes only when it matches. Spokes are unfiltered
+ * pointers.
*/
export function resolveGraphSlice(
graph: GhostGraph,
@@ -54,16 +81,13 @@ export function resolveGraphSlice(
options: ResolveGraphSliceOptions = {},
): GraphSlice {
const ancestorsFull = ancestorChain(graph, surfaceId);
- // Exclude the implicit root from the reported chain (parity with the old
- // resolver, which reported up to but not labeling core specially); keep it in
- // the cascade set so root/essence nodes still cascade.
const ancestors = ancestorsFull.filter((id) => id !== GHOST_GRAPH_ROOT_ID);
- const cascadeIds = new Set([
- surfaceId,
- ...ancestorsFull,
- GHOST_GRAPH_ROOT_ID,
- ]);
+ const surfaceNode = graph.nodes.get(surfaceId);
+ // The surface's own file folder anchors the corridor. For a bare tree
+ // position (a directory with no index node) the folder is the id itself.
+ const surfaceFolder = surfaceNode?.folder ?? surfaceId;
+ const corridor = corridorFolders(surfaceFolder);
const passesIncarnation = (incarnation?: string): boolean => {
if (options.incarnation === undefined) return true;
@@ -78,15 +102,16 @@ export function resolveGraphSlice(
? { incarnation: options.incarnation }
: {}),
nodes: [],
+ spokes: [],
};
- const seen = new Set();
- const add = (id: string, provenance: GraphSliceProvenance) => {
- if (seen.has(id)) return;
+ const seenBody = new Set();
+ const addBody = (id: string, provenance: GraphSliceProvenance): boolean => {
+ if (seenBody.has(id)) return false;
const node = graph.nodes.get(id);
- if (!node) return;
- if (!passesIncarnation(node.incarnation)) return;
- seen.add(id);
+ if (!node) return false;
+ if (!passesIncarnation(node.incarnation)) return false;
+ seenBody.add(id);
slice.nodes.push({
id: node.id,
body: node.body,
@@ -95,42 +120,89 @@ export function resolveGraphSlice(
: {}),
provenance,
});
+ return true;
};
- // Placement of a node: nodes attach to a surface via `under`; nodes whose id
- // *is* a surface in the cascade are themselves placed there. We resolve
- // placement as: a node belongs to surface S if its containment parent chain
- // reaches S directly (its `under` is S), or the node id equals S.
- const placementOf = (nodeUnder?: string): string =>
- nodeUnder ?? GHOST_GRAPH_ROOT_ID;
-
- // Own + ancestor: walk every node, place it, decide provenance by cascade.
+ // Spine: every node whose file folder is on the corridor. `own` when the
+ // node sits in the surface's own folder; `ancestor` when higher up.
for (const node of graph.nodes.values()) {
- const placement =
- node.id === surfaceId ? surfaceId : placementOf(node.under);
- if (placement === surfaceId || node.id === surfaceId) {
- add(node.id, { kind: "own" });
- } else if (cascadeIds.has(placement)) {
- add(node.id, { kind: "ancestor", from: placement });
- }
+ if (node.origin === "inherited") continue;
+ if (!corridor.has(node.folder)) continue;
+ const provenance: GraphSliceProvenance =
+ node.folder === surfaceFolder
+ ? { kind: "own" }
+ : { kind: "ancestor", from: node.parent ?? GHOST_GRAPH_ROOT_ID };
+ addBody(node.id, provenance);
}
- // Edge contributions: one hop along `relates` from the nodes already in the
- // slice. The target's body is included, tagged by qualifier.
- const ownAndAncestor = [...slice.nodes];
- for (const sliceNode of ownAndAncestor) {
- const source = graph.nodes.get(sliceNode.id);
+ // Edges: one hop along `relates` from every spine node. The target's body is
+ // included; if the target is a hub, its subtree is offered as spokes.
+ const spineIds = slice.nodes.map((n) => n.id);
+ const edgeTargets: string[] = [];
+ for (const sourceId of spineIds) {
+ const source = graph.nodes.get(sourceId);
if (!source) continue;
for (const relation of source.relates) {
- // A `:` ref resolves to an inherited node, keyed the
- // same way in graph.nodes — `add` no-ops if it isn't present.
- add(relation.to, {
+ const added = addBody(relation.to, {
kind: "edge",
...(relation.as !== undefined ? { via: relation.as } : {}),
- from: sliceNode.id,
+ from: sourceId,
});
+ if (added) edgeTargets.push(relation.to);
+ }
+ }
+
+ // Spokes: descendants of the surface, plus descendants of each edge hub.
+ const seenSpoke = new Set(seenBody);
+ const addSpoke = (id: string, kind: GraphSpokeKind, hub?: string) => {
+ if (seenSpoke.has(id)) return;
+ const node = graph.nodes.get(id);
+ if (!node) return;
+ seenSpoke.add(id);
+ slice.spokes.push({
+ id: node.id,
+ ...(node.description !== undefined
+ ? { description: node.description }
+ : {}),
+ kind,
+ ...(hub !== undefined ? { hub } : {}),
+ });
+ };
+
+ for (const node of graph.nodes.values()) {
+ if (node.origin === "inherited") continue;
+ if (isWithinOrBelow(node.folder, surfaceFolder)) {
+ addSpoke(node.id, "descendant");
+ }
+ }
+ for (const hubId of edgeTargets) {
+ const hub = graph.nodes.get(hubId);
+ if (!hub) continue;
+ for (const node of graph.nodes.values()) {
+ if (isWithinOrBelow(node.folder, hub.folder)) {
+ addSpoke(node.id, "edge-hub", hubId);
+ }
}
}
return slice;
}
+
+/** The set of folders on the corridor from the package root down to `folder`. */
+function corridorFolders(folder: string): Set {
+ const set = new Set([""]); // root files reach everywhere
+ let current = folder;
+ while (current !== "") {
+ set.add(current);
+ const slash = current.lastIndexOf("/");
+ current = slash === -1 ? "" : current.slice(0, slash);
+ }
+ return set;
+}
+
+/** True when `folder` is `base` or nested below it (a descendant position). */
+function isWithinOrBelow(folder: string, base: string): boolean {
+ if (folder === base) return true;
+ if (base === "") return folder !== "";
+ return folder.startsWith(`${base}/`);
+}
diff --git a/packages/ghost/src/ghost-core/graph/types.ts b/packages/ghost/src/ghost-core/graph/types.ts
index 76d9e23b..d59e24bb 100644
--- a/packages/ghost/src/ghost-core/graph/types.ts
+++ b/packages/ghost/src/ghost-core/graph/types.ts
@@ -1,28 +1,43 @@
import type { GhostNodeRelation } from "../node/types.js";
-import { GHOST_SURFACE_ROOT_ID } from "../surfaces/types.js";
-/** The implicit root every node ultimately descends from (shared with surfaces). */
-export const GHOST_GRAPH_ROOT_ID = GHOST_SURFACE_ROOT_ID;
+/**
+ * The implicit root every node ultimately descends from. A package-root
+ * `index.md` *is* this node's prose; otherwise it exists implicitly and never
+ * needs to be declared.
+ */
+export const GHOST_GRAPH_ROOT_ID = "core";
/**
- * Where a node in the resolved graph came from. The fold unions authored
- * on-disk node files with a transition projection of the legacy facet model;
- * `origin` records which, so later phases and lint can treat them differently
- * (and so the projection can be deleted cleanly in the facet-removal phase).
+ * Where a node in the resolved graph came from. A local node is read from the
+ * package's own directory tree (its path is its id, its directory its parent);
+ * an inherited node is read-only context pulled in by `extends`. `origin`
+ * records which, so later phases and lint can treat them differently.
*/
export type GhostGraphNodeOrigin = "node-file" | "inherited";
/**
* A resolved graph node — pure prose (Option A). The body is the design
- * expression; there are no structured node fields. `under` is the single
- * containment parent (absent ⇒ child of the implicit `core` root); `relates`
- * are the typed lateral links; `incarnation` is the optional projection tag.
+ * expression; there are no structured node fields. `id` is the node's path in
+ * the package; `parent` is its containing directory — the single containment
+ * parent (absent ⇒ the implicit `core` root itself); `relates` are the typed
+ * lateral links; `incarnation` is the optional projection tag.
*/
export interface GhostGraphNode {
id: string;
/** One-line "what this is / when to gather it" — the retrieval payload. */
description?: string;
- under?: string;
+ /** The containing directory's id; absent ⇒ this node is the `core` root. */
+ parent?: string;
+ /**
+ * The node's **file folder** — the directory the source file physically sits
+ * in, which is the unit of containment for corridor composition. This differs
+ * from `parent` for index nodes: `features/bitcoin/index.md` has folder
+ * `features/bitcoin` but parent `features`. A plain leaf shares its parent's
+ * value (`features/bitcoin/buy.md` → folder `features/bitcoin`). The root
+ * `core` node has folder `""`. Folders are walls: a node only cascades into
+ * surfaces whose folder chain includes this folder.
+ */
+ folder: string;
relates: GhostNodeRelation[];
incarnation?: string;
body: string;
@@ -31,9 +46,9 @@ export interface GhostGraphNode {
/**
* The in-memory fingerprint graph: prose nodes indexed by id, plus the
- * containment tree (`under` parent edges, root = `core`) that is the traversal
- * spine. This is the shape later phases (gather, checks, compare) traverse;
- * disk layout is just one serialization of it.
+ * containment tree (parent edges from the directory layout, root = `core`) that
+ * is the traversal spine. This is the shape later phases (gather, checks,
+ * review) traverse; the directory layout is just one serialization of it.
*/
export interface GhostGraph {
/** Every node, indexed by id. */
diff --git a/packages/ghost/src/ghost-core/index.ts b/packages/ghost/src/ghost-core/index.ts
index 3daa70b3..237e6133 100644
--- a/packages/ghost/src/ghost-core/index.ts
+++ b/packages/ghost/src/ghost-core/index.ts
@@ -37,6 +37,7 @@ export {
type GraphSliceNode,
type GraphSliceProvenance,
lintGraph,
+ type PlacedNode,
type ResolveGraphSliceOptions,
resolveGraphSlice,
} from "./graph/index.js";
@@ -75,16 +76,3 @@ export type {
// --- Skill bundle loader ---
export type { SkillBundleFile } from "./skill-bundle-loader.js";
export { loadSkillBundle } from "./skill-bundle-loader.js";
-// --- Surfaces (ghost.surfaces/v1) — the optional terse spine file ---
-export {
- GHOST_SURFACE_ROOT_ID,
- GHOST_SURFACES_SCHEMA,
- GHOST_SURFACES_YML_FILENAME,
- type GhostSurface,
- type GhostSurfacesDocument,
- type GhostSurfacesLintIssue,
- type GhostSurfacesLintReport,
- type GhostSurfacesLintSeverity,
- GhostSurfacesSchema,
- lintGhostSurfaces,
-} from "./surfaces/index.js";
diff --git a/packages/ghost/src/ghost-core/node/schema.ts b/packages/ghost/src/ghost-core/node/schema.ts
index 60e3cbdc..e23a7357 100644
--- a/packages/ghost/src/ghost-core/node/schema.ts
+++ b/packages/ghost/src/ghost-core/node/schema.ts
@@ -2,33 +2,38 @@ import { z } from "zod";
import { GHOST_NODE_RELATION_KINDS } from "./types.js";
/**
- * A node id is a permissive lowercase slug, unique within the package. The
- * charset is liberal on purpose (lowercase alphanumeric plus `.` `_` `-`): the
- * schema enforces machine-tractability, not a separator style. Dashes are the
- * emitted convention (skill / init / agent authoring), nudged in guidance — not
- * a lint rule. The tree lives only in `under`; an id never encodes hierarchy.
+ * A node id is its path within the package, `.md` dropped (`marketing/email`).
+ * The directory tree is the containment spine: the containing directory is the
+ * parent, so the id *does* encode hierarchy by design. A segment is a permissive
+ * lowercase slug (alphanumeric plus `.` `_` `-`); segments join with `/`. No
+ * leading, trailing, or doubled slash. Ids are computed by the loader from the
+ * file path, never authored in frontmatter.
*/
-const NodeIdSchema = z
- .string()
- .min(1)
- .regex(/^[a-z0-9][a-z0-9._-]*$/, {
- message:
- "node id must be a lowercase slug (alphanumeric plus . _ -, leading alphanumeric)",
- });
+const NODE_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/;
+
+const NodeIdSchema = z.string().min(1).regex(NODE_ID_PATTERN, {
+ message:
+ "node id must be a path of lowercase slug segments joined by '/' (alphanumeric plus . _ -, no leading/trailing/doubled slash)",
+});
/**
- * A node ref points at another node: a local id (``), or a cross-package
- * ref `:` where `` is a key declared in the
- * package manifest's `extends` map. Reference is by identity, never by path —
- * `:` is Ghost's qualifier lineage (e.g. the old `intent.principle:foo` refs).
+ * A node ref points at another node by its path id (`marketing/email`), or a
+ * cross-package ref `:` where `` is a key declared
+ * in the package manifest's `extends` map. The local part is a path id; `:` is
+ * Ghost's cross-package qualifier lineage (e.g. the old `intent.principle:foo`).
*/
const NodeRefSchema = z
.string()
.min(1)
- .regex(/^(?:[a-z0-9][a-z0-9._-]*:)?[a-z0-9][a-z0-9._-]*$/, {
- message:
- "node ref must be a local id '' or a cross-package ref ':'",
- });
+ .regex(
+ new RegExp(
+ `^(?:[a-z0-9][a-z0-9._-]*:)?${NODE_ID_PATTERN.source.slice(1, -1)}$`,
+ ),
+ {
+ message:
+ "node ref must be a path id 'marketing/email' or a cross-package ref ':'",
+ },
+ );
const NodeRelationSchema = z
.object({
@@ -47,9 +52,7 @@ const NodeRelationSchema = z
*/
export const GhostNodeFrontmatterSchema = z
.object({
- id: NodeIdSchema,
description: z.string().min(1).optional(),
- under: NodeRefSchema.optional(),
relates: z.array(NodeRelationSchema).optional(),
incarnation: z.string().min(1).optional(),
})
diff --git a/packages/ghost/src/ghost-core/node/serialize.ts b/packages/ghost/src/ghost-core/node/serialize.ts
index fdeb2643..0bae0ea6 100644
--- a/packages/ghost/src/ghost-core/node/serialize.ts
+++ b/packages/ghost/src/ghost-core/node/serialize.ts
@@ -3,14 +3,15 @@ import type { GhostNodeDocument, GhostNodeFrontmatter } from "./types.js";
/**
* Serialize a node back to its `---\n\n---\n` markdown form. Keys
- * are emitted in a stable order (id, under, relates, incarnation) so round-trips and
- * diffs are deterministic. Undefined fields are omitted.
+ * are emitted in a stable order (description, relates, incarnation) so
+ * round-trips and diffs are deterministic. Identity and containment are not
+ * serialized — they are the node's path in the directory tree. Undefined fields
+ * are omitted; a node with no frontmatter fields emits an empty block.
*/
export function serializeNode(node: GhostNodeDocument): string {
const fm = node.frontmatter;
- const ordered: Record = { id: fm.id };
+ const ordered: Record = {};
if (fm.description !== undefined) ordered.description = fm.description;
- if (fm.under !== undefined) ordered.under = fm.under;
if (fm.relates !== undefined) {
ordered.relates = fm.relates.map((relation) => {
const entry: Record = { to: relation.to };
@@ -20,9 +21,13 @@ export function serializeNode(node: GhostNodeDocument): string {
}
if (fm.incarnation !== undefined) ordered.incarnation = fm.incarnation;
- const yaml = stringifyYaml(ordered).trimEnd();
+ // An empty frontmatter object stringifies to "{}"; emit a bare block instead.
+ const yaml =
+ Object.keys(ordered).length === 0
+ ? ""
+ : `${stringifyYaml(ordered).trimEnd()}\n`;
const body = node.body.replace(/^\n+/, "");
- return `---\n${yaml}\n---\n${body.length ? `\n${body}\n` : "\n"}`;
+ return `---\n${yaml}---\n${body.length ? `\n${body}\n` : "\n"}`;
}
export type { GhostNodeFrontmatter };
diff --git a/packages/ghost/src/ghost-core/node/types.ts b/packages/ghost/src/ghost-core/node/types.ts
index 89bf6a22..326d7e0d 100644
--- a/packages/ghost/src/ghost-core/node/types.ts
+++ b/packages/ghost/src/ghost-core/node/types.ts
@@ -25,28 +25,21 @@ export interface GhostNodeRelation {
}
/**
- * A node's frontmatter: the machinery's handle (identity, tree, links,
- * incarnation).
- * The prose body carries the design expression; intent / inventory /
- * composition are authorship lenses, never fields.
+ * A node's frontmatter: descriptive properties only. Identity and containment
+ * are not here — they are the node's location in the directory tree (the file
+ * path is the id; the containing directory is the parent). The prose body
+ * carries the design expression; intent / inventory / composition are
+ * authorship lenses, never fields.
*/
export interface GhostNodeFrontmatter {
- /** Unique, addressable id within the package. */
- id: string;
/**
* One-line statement of what this node is and when to gather it — the
- * retrieval payload. Together with `id` it is how an agent selects a node,
- * exactly like a tool's name + description. The body is the node's
- * "implementation"; the description is what makes it discoverable. Optional,
- * but strongly encouraged on any node worth anchoring a task at.
+ * retrieval payload. Together with the node's id (its path) it is how an
+ * agent selects a node, exactly like a tool's name + description. The body is
+ * the node's "implementation"; the description is what makes it discoverable.
+ * Optional, but strongly encouraged on any node worth anchoring a task at.
*/
description?: string;
- /**
- * The single containment parent (the tree + the cascade). Absent means a
- * top-level node under the implicit `core` root. The tree lives only here;
- * the id never encodes hierarchy.
- */
- under?: string;
/** Typed lateral links to other nodes (composition graph). */
relates?: GhostNodeRelation[];
/**
diff --git a/packages/ghost/src/ghost-core/surfaces/index.ts b/packages/ghost/src/ghost-core/surfaces/index.ts
deleted file mode 100644
index cf79d30c..00000000
--- a/packages/ghost/src/ghost-core/surfaces/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Public surface for `ghost.surfaces/v1` schema and types.
- *
- * Phase 1 ships schema + types only. Lint (graph validation) is Phase 2; the
- * disk loader and CLI wiring come later. See docs/ideas/phase-1-plan.md.
- */
-
-export { lintGhostSurfaces } from "./lint.js";
-export { GhostSurfacesSchema } from "./schema.js";
-export {
- GHOST_SURFACE_ROOT_ID,
- GHOST_SURFACES_SCHEMA,
- GHOST_SURFACES_YML_FILENAME,
- type GhostSurface,
- type GhostSurfacesDocument,
- type GhostSurfacesLintIssue,
- type GhostSurfacesLintReport,
- type GhostSurfacesLintSeverity,
-} from "./types.js";
diff --git a/packages/ghost/src/ghost-core/surfaces/lint.ts b/packages/ghost/src/ghost-core/surfaces/lint.ts
deleted file mode 100644
index 940e5329..00000000
--- a/packages/ghost/src/ghost-core/surfaces/lint.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-import type { ZodIssue } from "zod";
-import { GhostSurfacesSchema } from "./schema.js";
-import {
- GHOST_SURFACE_ROOT_ID,
- type GhostSurfacesDocument,
- type GhostSurfacesLintIssue,
- type GhostSurfacesLintReport,
-} from "./types.js";
-
-/**
- * Lint a `ghost.surfaces/v1` document for document-level correctness that the
- * schema cannot express in isolation: the containment tree (parent refs, no
- * cycles), the composition graph (edge refs), the reserved root, duplicate ids,
- * and teaching warnings for near-miss references.
- *
- * Containment (`parent`) is tree-constrained: cycles and self-parents are
- * errors. Composition (`edges`) may form a graph, including cycles among edges;
- * only dangling edge targets are errors.
- */
-export function lintGhostSurfaces(input: unknown): GhostSurfacesLintReport {
- const result = GhostSurfacesSchema.safeParse(input);
- if (!result.success) return finalize(zodIssues(result.error.issues));
-
- const doc = result.data as GhostSurfacesDocument;
- const issues: GhostSurfacesLintIssue[] = [];
-
- const ids = new Set();
- for (const surface of doc.surfaces) ids.add(surface.id);
- // `core` is always a resolvable target (implicit root) even if not declared.
- const knownIds = new Set(ids);
- knownIds.add(GHOST_SURFACE_ROOT_ID);
-
- checkDuplicateIds(doc, issues);
- checkReservedCore(doc, issues);
- checkParentRefs(doc, knownIds, issues);
- checkParentCycles(doc, issues);
- checkNearMissIds(doc, ids, issues);
-
- return finalize(issues);
-}
-
-function checkDuplicateIds(
- doc: GhostSurfacesDocument,
- issues: GhostSurfacesLintIssue[],
-): void {
- const seen = new Map();
- doc.surfaces.forEach((surface, index) => {
- const previous = seen.get(surface.id);
- if (previous !== undefined) {
- issues.push({
- severity: "error",
- rule: "duplicate-id",
- message: `surface id '${surface.id}' is duplicated (also at surfaces[${previous}])`,
- path: `surfaces[${index}].id`,
- });
- } else {
- seen.set(surface.id, index);
- }
- });
-}
-
-function checkReservedCore(
- doc: GhostSurfacesDocument,
- issues: GhostSurfacesLintIssue[],
-): void {
- // `core` is the implicit root: it may be declared (to describe it) but may
- // never have a parent.
- doc.surfaces.forEach((surface, index) => {
- if (surface.id === GHOST_SURFACE_ROOT_ID && surface.parent !== undefined) {
- issues.push({
- severity: "error",
- rule: "surface-core-reserved",
- message: `'${GHOST_SURFACE_ROOT_ID}' is the reserved implicit root and cannot declare a parent`,
- path: `surfaces[${index}].parent`,
- });
- }
- });
-}
-
-function checkParentRefs(
- doc: GhostSurfacesDocument,
- knownIds: Set,
- issues: GhostSurfacesLintIssue[],
-): void {
- doc.surfaces.forEach((surface, index) => {
- if (surface.parent === undefined) return;
- if (!knownIds.has(surface.parent)) {
- issues.push({
- severity: "error",
- rule: "surface-parent-unknown",
- message: `parent '${surface.parent}' does not match any surface id`,
- path: `surfaces[${index}].parent`,
- });
- }
- });
-}
-
-function checkParentCycles(
- doc: GhostSurfacesDocument,
- issues: GhostSurfacesLintIssue[],
-): void {
- const parentOf = new Map();
- for (const surface of doc.surfaces) parentOf.set(surface.id, surface.parent);
-
- doc.surfaces.forEach((surface, index) => {
- const visited = new Set([surface.id]);
- let current = surface.parent;
- while (current !== undefined && current !== GHOST_SURFACE_ROOT_ID) {
- if (visited.has(current)) {
- issues.push({
- severity: "error",
- rule: "surface-parent-cycle",
- message: `surface '${surface.id}' is part of a parent cycle (revisits '${current}')`,
- path: `surfaces[${index}].parent`,
- });
- return;
- }
- visited.add(current);
- // Only walk ids that exist; an unknown parent is reported separately.
- if (!parentOf.has(current)) return;
- current = parentOf.get(current);
- }
- });
-}
-
-function checkNearMissIds(
- doc: GhostSurfacesDocument,
- ids: Set,
- issues: GhostSurfacesLintIssue[],
-): void {
- const candidates = [...ids];
-
- doc.surfaces.forEach((surface, index) => {
- if (surface.parent !== undefined && !ids.has(surface.parent)) {
- const near = nearest(surface.parent, candidates);
- if (near) {
- issues.push({
- severity: "warning",
- rule: "surface-id-near-miss",
- message: `parent '${surface.parent}' is unknown; did you mean '${near}'?`,
- path: `surfaces[${index}].parent`,
- });
- }
- }
- });
-}
-
-/** Nearest candidate within edit distance 2, or null. */
-function nearest(value: string, candidates: string[]): string | null {
- let best: string | null = null;
- let bestDistance = 3;
- for (const candidate of candidates) {
- const distance = levenshtein(value, candidate);
- if (distance < bestDistance) {
- bestDistance = distance;
- best = candidate;
- }
- }
- return bestDistance <= 2 ? best : null;
-}
-
-function levenshtein(a: string, b: string): number {
- const rows = a.length + 1;
- const cols = b.length + 1;
- const dist: number[][] = Array.from({ length: rows }, () =>
- new Array(cols).fill(0),
- );
- for (let i = 0; i < rows; i++) dist[i][0] = i;
- for (let j = 0; j < cols; j++) dist[0][j] = j;
- for (let i = 1; i < rows; i++) {
- for (let j = 1; j < cols; j++) {
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
- dist[i][j] = Math.min(
- dist[i - 1][j] + 1,
- dist[i][j - 1] + 1,
- dist[i - 1][j - 1] + cost,
- );
- }
- }
- return dist[a.length][b.length];
-}
-
-function zodIssues(issues: ZodIssue[]): GhostSurfacesLintIssue[] {
- return issues.map((issue) => ({
- severity: "error" as const,
- rule: `schema/${issue.code}`,
- message: issue.message,
- path: formatZodPath(issue.path),
- }));
-}
-
-function formatZodPath(path: ZodIssue["path"]): string | undefined {
- if (path.length === 0) return undefined;
- return path.reduce((formatted, segment) => {
- if (typeof segment === "number") return `${formatted}[${segment}]`;
- const key = String(segment);
- return formatted ? `${formatted}.${key}` : key;
- }, "");
-}
-
-function finalize(issues: GhostSurfacesLintIssue[]): GhostSurfacesLintReport {
- return {
- issues,
- errors: issues.filter((issue) => issue.severity === "error").length,
- warnings: issues.filter((issue) => issue.severity === "warning").length,
- info: issues.filter((issue) => issue.severity === "info").length,
- };
-}
diff --git a/packages/ghost/src/ghost-core/surfaces/schema.ts b/packages/ghost/src/ghost-core/surfaces/schema.ts
deleted file mode 100644
index a03d370b..00000000
--- a/packages/ghost/src/ghost-core/surfaces/schema.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { z } from "zod";
-import { GHOST_SURFACES_SCHEMA } from "./types.js";
-
-/**
- * Flat slug for surface ids. The dot is excluded: a dotted id (`email.marketing`)
- * would pretend to be a `parent` link, creating a second source of truth for the
- * tree. Containment lives only in `parent`.
- */
-const SurfaceIdSchema = z
- .string()
- .min(1)
- .regex(/^[a-z0-9][a-z0-9_-]*$/, {
- message:
- "surface id must be a flat slug (lowercase alphanumeric plus _ -, no dots; the tree lives in parent)",
- });
-
-const SurfaceSchema = z
- .object({
- id: SurfaceIdSchema,
- description: z.string().min(1).optional(),
- parent: SurfaceIdSchema.optional(),
- })
- .strict();
-
-/**
- * Zod schema for `surfaces.yml` (`ghost.surfaces/v1`) — the optional terse spine
- * file. Validates each position in isolation; graph-level rules (parent exists,
- * no cycles) are covered by the node-graph lint after the fold.
- */
-export const GhostSurfacesSchema = z
- .object({
- schema: z.literal(GHOST_SURFACES_SCHEMA),
- surfaces: z.array(SurfaceSchema).optional().default([]),
- })
- .strict();
diff --git a/packages/ghost/src/ghost-core/surfaces/types.ts b/packages/ghost/src/ghost-core/surfaces/types.ts
deleted file mode 100644
index 2a31aebc..00000000
--- a/packages/ghost/src/ghost-core/surfaces/types.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-export const GHOST_SURFACES_SCHEMA = "ghost.surfaces/v1" as const;
-export const GHOST_SURFACES_YML_FILENAME = "surfaces.yml" as const;
-
-/** The implicit root every node ultimately descends from. */
-export const GHOST_SURFACE_ROOT_ID = "core" as const;
-
-/**
- * `surfaces.yml` is an optional terse spine file: a place to declare bare tree
- * positions (id + parent) in one file rather than as bodyless node files. It
- * folds into the same node id space at load time — a position that needs
- * guidance is simply a node with that id. Lateral composition lives on node
- * `relates`, never here (the old surface edge vocabulary is gone).
- */
-export interface GhostSurface {
- id: string;
- description?: string;
- /**
- * The single containment parent. Absent means a top-level position under the
- * implicit `core` root. Containment lives only here; the id never encodes
- * hierarchy (see GhostSurfacesSchema id rules).
- */
- parent?: string;
-}
-
-export interface GhostSurfacesDocument {
- schema: typeof GHOST_SURFACES_SCHEMA;
- surfaces: GhostSurface[];
-}
-
-/**
- * Lint report types reuse the fingerprint facet shape verbatim so Phase 2 and
- * the CLI can treat all facet lint reports uniformly.
- */
-export type GhostSurfacesLintSeverity = "error" | "warning" | "info";
-
-export interface GhostSurfacesLintIssue {
- severity: GhostSurfacesLintSeverity;
- rule: string;
- message: string;
- path?: string;
-}
-
-export interface GhostSurfacesLintReport {
- issues: GhostSurfacesLintIssue[];
- errors: number;
- warnings: number;
- info: number;
-}
diff --git a/packages/ghost/src/scan/file-kind.ts b/packages/ghost/src/scan/file-kind.ts
index 539f58bc..21199173 100644
--- a/packages/ghost/src/scan/file-kind.ts
+++ b/packages/ghost/src/scan/file-kind.ts
@@ -3,22 +3,20 @@ import {
GhostFingerprintPackageManifestSchema,
lintGhostCheck,
lintGhostNode,
- lintGhostSurfaces,
} from "#ghost-core";
import type { LintReport } from "./lint.js";
export type DetectedFileKind =
| "fingerprint-manifest"
- | "surfaces"
| "check"
| "node"
| "unsupported";
/**
- * Decide whether a file is a bundle artifact. Canonical filenames and YAML
- * `schema:` markers route to their artifact linters; markdown under `nodes/`
- * or `checks/` routes to the node / check linter. Unknown files remain
- * unsupported instead of being guessed at.
+ * Decide whether a file is a bundle artifact. The manifest routes to its
+ * artifact linter; markdown under `checks/` is a check; any other markdown is a
+ * node (its path is its id — containment is the directory tree). Unknown files
+ * remain unsupported instead of being guessed at.
*/
export function detectFileKind(path: string, raw: string): DetectedFileKind {
const lowerPath = path.toLowerCase();
@@ -29,21 +27,19 @@ export function detectFileKind(path: string, raw: string): DetectedFileKind {
if (filename === "manifest.yaml") {
return "fingerprint-manifest";
}
- if (filename === "surfaces.yml") return "surfaces";
- if (filename === "surfaces.yaml") return "surfaces";
// A markdown check lives under a `checks/` directory. Detected by location so
// the established agent-check format (no `schema:` field) is recognized.
if (filename.endsWith(".md") && /(^|[\\/])checks[\\/]/.test(lowerPath)) {
return "check";
}
- // A markdown node lives under a `nodes/` directory (ghost.node/v1).
- if (filename.endsWith(".md") && /(^|[\\/])nodes[\\/]/.test(lowerPath)) {
+ // Any other markdown file is a node (ghost.node/v1). Its id is its path; the
+ // containing directory is its parent.
+ if (filename.endsWith(".md")) {
return "node";
}
if (/^\s*schema:\s*ghost\.fingerprint-package\/v1\b/m.test(raw)) {
return "fingerprint-manifest";
}
- if (/^\s*schema:\s*ghost\.surfaces\/v1\b/m.test(raw)) return "surfaces";
return "unsupported";
}
@@ -53,13 +49,11 @@ export function lintDetectedFileKind(
): LintReport {
return kind === "fingerprint-manifest"
? lintFingerprintManifestFile(raw)
- : kind === "surfaces"
- ? lintSurfacesFile(raw)
- : kind === "check"
- ? lintGhostCheck(raw)
- : kind === "node"
- ? lintGhostNode(raw)
- : lintUnsupportedFile();
+ : kind === "check"
+ ? lintGhostCheck(raw)
+ : kind === "node"
+ ? lintGhostNode(raw)
+ : lintUnsupportedFile();
}
function lintFingerprintManifestFile(raw: string): LintReport {
@@ -98,14 +92,6 @@ function zodLintReport(result: {
};
}
-function lintSurfacesFile(raw: string): LintReport {
- try {
- return lintGhostSurfaces(parseYaml(raw));
- } catch (err) {
- return yamlErrorReport("surfaces-not-yaml", "surfaces file", err);
- }
-}
-
function lintUnsupportedFile(): LintReport {
return {
issues: [
@@ -113,7 +99,7 @@ function lintUnsupportedFile(): LintReport {
severity: "error",
rule: "unsupported-artifact",
message:
- "File is not a recognized Ghost artifact. Use manifest.yml, surfaces.yml, a checks/*.md check, or a nodes/*.md node.",
+ "File is not a recognized Ghost artifact. Use manifest.yml, a checks/*.md check, or a *.md node.",
},
],
errors: 1,
diff --git a/packages/ghost/src/scan/fingerprint-contribution.ts b/packages/ghost/src/scan/fingerprint-contribution.ts
index 6ac1037c..b9abd1d5 100644
--- a/packages/ghost/src/scan/fingerprint-contribution.ts
+++ b/packages/ghost/src/scan/fingerprint-contribution.ts
@@ -35,8 +35,6 @@ export interface ScanContributionReport {
*/
export function summarizeFingerprintContribution(input: {
graph?: GhostGraph;
- /** Declared surface ids from surfaces.yml (excluding the implicit root). */
- surfaceIds?: string[];
missing?: boolean;
invalidReason?: string;
}): ScanContributionReport {
@@ -52,20 +50,25 @@ export function summarizeFingerprintContribution(input: {
}
const graph = input.graph;
+ // Authored local nodes contribute. The root `index.md` is a real authored
+ // node (origin node-file) and counts; the implicit root (when undeclared) and
+ // inherited nodes do not.
const nodes = [...graph.nodes.values()].filter(
- (node) => node.id !== GHOST_GRAPH_ROOT_ID,
+ (node) => node.origin === "node-file",
);
const essence = nodes.filter((node) => node.incarnation === undefined);
const tagged = nodes.filter((node) => node.incarnation !== undefined);
- // Surface coverage: count nodes whose `under` is each declared surface.
+ // Surface coverage: count nodes whose parent is each declared surface.
const placement = new Map();
for (const node of nodes) {
- const under = node.under ?? GHOST_GRAPH_ROOT_ID;
- placement.set(under, (placement.get(under) ?? 0) + 1);
+ const parent = node.parent ?? GHOST_GRAPH_ROOT_ID;
+ placement.set(parent, (placement.get(parent) ?? 0) + 1);
}
- const surfaceIds = (input.surfaceIds ?? []).filter(
- (id) => id !== GHOST_GRAPH_ROOT_ID,
+ // Surfaces are the tree's interior positions: any id that is a parent of at
+ // least one node (a directory), excluding the implicit root.
+ const surfaceIds = [...graph.parents.values()].filter(
+ (id, index, all) => id !== GHOST_GRAPH_ROOT_ID && all.indexOf(id) === index,
);
const surfaces: ScanSurfaceCoverage[] = surfaceIds
.map((id) => ({ id, node_count: placement.get(id) ?? 0 }))
@@ -88,7 +91,7 @@ export function summarizeFingerprintContribution(input: {
? [`Add nodes for sparse surfaces: ${sparse.join(", ")}.`]
: ["Package contributes nodes across its declared surfaces."]
: [
- "Package is valid but has no nodes yet. Add nodes/*.md to contribute.",
+ "Package is valid but has no nodes yet. Add an index.md or /.md to contribute.",
],
};
}
diff --git a/packages/ghost/src/scan/fingerprint-package-layers.ts b/packages/ghost/src/scan/fingerprint-package-layers.ts
index 860051a9..16baf9f5 100644
--- a/packages/ghost/src/scan/fingerprint-package-layers.ts
+++ b/packages/ghost/src/scan/fingerprint-package-layers.ts
@@ -6,37 +6,31 @@ import {
type GhostFingerprintPackageManifest,
GhostFingerprintPackageManifestSchema,
type GhostGraphNode,
- type GhostSurfacesDocument,
- GhostSurfacesSchema,
lintGraph,
} from "#ghost-core";
-import { isMissingPathError, readOptionalUtf8 } from "../internal/fs.js";
+import { isMissingPathError } from "../internal/fs.js";
import {
type FingerprintPackagePaths,
type LoadedFingerprintPackage,
resolveFingerprintPackage,
} from "./fingerprint-package.js";
import type { LintIssue } from "./lint.js";
-import { loadNodesDir } from "./nodes-dir.js";
+import { loadNodeTree } from "./node-tree.js";
const LEGACY_FACET_FILES = ["intent.yml", "inventory.yml", "composition.yml"];
export async function loadFingerprintPackage(
paths: FingerprintPackagePaths,
): Promise {
- const [manifestRaw, surfacesRaw] = await Promise.all([
- readFile(paths.manifest, "utf-8"),
- readOptional(paths.surfaces),
- ]);
+ const manifestRaw = await readFile(paths.manifest, "utf-8");
const manifest = parseManifest(manifestRaw, "manifest.yml");
- const surfaces = parseSurfaces(surfacesRaw);
// Legacy facet packages no longer load directly — guide to `ghost migrate`.
await assertNotLegacyFacetPackage(paths);
- const { nodes: nodeFiles } = await loadNodesDir(paths.dir);
+ const { nodes: placedNodes } = await loadNodeTree(paths.packageDir);
const inheritedNodes = await loadInheritedNodes(manifest, paths);
- const graph = assembleGraph({ nodeFiles, surfaces, inheritedNodes });
+ const graph = assembleGraph({ placedNodes, inheritedNodes });
const report = lintGraph(graph);
if (report.errors > 0) {
@@ -51,7 +45,6 @@ export async function loadFingerprintPackage(
manifest,
manifestRaw,
graph,
- ...(surfaces ? { surfaces } : {}),
};
}
@@ -94,6 +87,10 @@ async function loadInheritedNodes(
...(node.description !== undefined
? { description: node.description }
: {}),
+ // Inherited nodes carry a package-qualified folder so they never sit on
+ // a local corridor (folders are walls per package); they enter a slice
+ // only via an explicit cross-package `relates` edge.
+ folder: `${id}:${node.folder}`,
relates: [],
...(node.incarnation !== undefined
? { incarnation: node.incarnation }
@@ -107,18 +104,16 @@ async function loadInheritedNodes(
}
/**
- * If a package still ships the legacy facet files and has no `nodes/`, fail
- * with migrate guidance rather than a confusing graph error.
+ * If a package still ships the legacy facet files, fail with migrate guidance
+ * rather than a confusing graph error.
*/
async function assertNotLegacyFacetPackage(
paths: FingerprintPackagePaths,
): Promise {
- const hasNodes = await pathExists(paths.nodes);
- if (hasNodes) return;
for (const facet of LEGACY_FACET_FILES) {
if (await pathExists(`${paths.packageDir}/${facet}`)) {
throw new Error(
- `This is a legacy facet package (found ${facet}, no nodes/). Run \`ghost migrate\` to convert it to the node model.`,
+ `This is a legacy facet package (found ${facet}). Run \`ghost migrate\` to convert it to the directory-tree node model.`,
);
}
}
@@ -134,20 +129,6 @@ async function pathExists(path: string): Promise {
}
}
-function parseSurfaces(
- raw: string | undefined,
-): GhostSurfacesDocument | undefined {
- if (raw === undefined) return undefined;
- const result = GhostSurfacesSchema.safeParse(parseYaml(raw));
- if (!result.success) {
- const first = result.error.issues[0];
- throw new Error(
- `surfaces.yml failed schema validation: ${first?.message ?? "invalid surfaces"}`,
- );
- }
- return result.data as GhostSurfacesDocument;
-}
-
export function lintFingerprintPackageManifest(
raw: string,
issues: LintIssue[],
@@ -211,5 +192,3 @@ function parseYamlSafe(
return undefined;
}
}
-
-const readOptional = readOptionalUtf8;
diff --git a/packages/ghost/src/scan/fingerprint-package.ts b/packages/ghost/src/scan/fingerprint-package.ts
index 2d4a58cf..006eb6b2 100644
--- a/packages/ghost/src/scan/fingerprint-package.ts
+++ b/packages/ghost/src/scan/fingerprint-package.ts
@@ -1,10 +1,8 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import {
- GHOST_SURFACES_YML_FILENAME,
type GhostFingerprintPackageManifest,
type GhostGraph,
- type GhostSurfacesDocument,
lintGraph,
} from "#ghost-core";
import { isExistingPathError, isMissingPathError } from "../internal/fs.js";
@@ -32,9 +30,6 @@ export interface FingerprintPackagePaths {
dir: string;
packageDir: string;
manifest: string;
- surfaces: string;
- /** The `nodes/` directory holding `ghost.node/v1` markdown nodes. */
- nodes: string;
/** Legacy facet paths — used only to detect legacy packages for migration. */
intent: string;
inventory: string;
@@ -44,8 +39,6 @@ export interface FingerprintPackagePaths {
export interface LoadedFingerprintPackage {
manifest: GhostFingerprintPackageManifest;
manifestRaw: string;
- /** Parsed `surfaces.yml`, or `undefined` when the package has no surfaces file. */
- surfaces?: GhostSurfacesDocument;
/** The in-memory node graph — the only fingerprint model. */
graph: GhostGraph;
}
@@ -72,8 +65,6 @@ export function resolveFingerprintPackage(
dir,
packageDir,
manifest: join(packageDir, FINGERPRINT_MANIFEST_FILENAME),
- surfaces: join(packageDir, GHOST_SURFACES_YML_FILENAME),
- nodes: join(packageDir, "nodes"),
intent: join(packageDir, FINGERPRINT_INTENT_FILENAME),
inventory: join(packageDir, FINGERPRINT_INVENTORY_FILENAME),
composition: join(packageDir, FINGERPRINT_COMPOSITION_FILENAME),
@@ -187,7 +178,7 @@ export async function lintFingerprintPackage(
severity: issue.severity,
rule: issue.rule,
message: issue.message,
- ...(issue.node ? { path: `nodes/${issue.node}` } : {}),
+ ...(issue.node ? { path: `${issue.node}.md` } : {}),
})),
);
} catch (err) {
diff --git a/packages/ghost/src/scan/migrate-legacy.ts b/packages/ghost/src/scan/migrate-legacy.ts
index f9604e95..177fef87 100644
--- a/packages/ghost/src/scan/migrate-legacy.ts
+++ b/packages/ghost/src/scan/migrate-legacy.ts
@@ -1,19 +1,15 @@
-import {
- GHOST_SURFACE_ROOT_ID,
- GHOST_SURFACES_SCHEMA,
- type GhostNodeDocument,
- serializeNode,
-} from "#ghost-core";
+import { type GhostNodeDocument, serializeNode } from "#ghost-core";
/**
* One-shot migration of a legacy `.ghost/` package (pre-surface coordinates)
- * onto the surface model. Operates on raw parsed YAML, because the current
- * schema rejects the legacy fields (`topology`, `applies_to`, `surface_type`,
- * `scope`) and a legacy package no longer parses through the loader.
+ * onto the directory-tree node model. Operates on raw parsed YAML, because the
+ * current schema rejects the legacy fields (`topology`, `applies_to`,
+ * `surface_type`, `scope`) and a legacy package no longer parses through the
+ * loader.
*
* Core discipline: report, don't guess. A node whose home cannot be derived
- * unambiguously is left unplaced and recorded for human review, never
- * auto-placed.
+ * unambiguously is left unplaced (at the package root, cascading from core) and
+ * recorded for human review, never auto-placed into a surface.
*/
type Yaml = Record;
@@ -31,7 +27,8 @@ export interface MigrationNote {
}
export interface MigrationResult {
- surfaces: Yaml;
+ /** Derived surface ids (directories), each a child of the implicit `core`. */
+ surfaceIds: string[];
intent: Yaml | undefined;
inventory: Yaml | undefined;
composition: Yaml | undefined;
@@ -61,17 +58,10 @@ export function migrateLegacyPackage(
? structuredClone(input.composition)
: undefined;
- // --- surfaces.yml from inventory.topology.scopes ---
- const scopeIds = collectScopeIds(inventory);
- const surfaces: Yaml = {
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: scopeIds.map((id) => ({
- id,
- parent: GHOST_SURFACE_ROOT_ID,
- })),
- };
+ // --- surface ids (directories) from inventory.topology.scopes ---
+ const surfaceIds = collectScopeIds(inventory);
- // Drop topology from inventory (its data is now surfaces.yml).
+ // Drop topology from inventory (its data is now the directory layout).
if (inventory && "topology" in inventory) delete inventory.topology;
// --- place + clean nodes ---
@@ -86,7 +76,7 @@ export function migrateLegacyPackage(
placeArray(composition, "patterns", "composition.patterns", notes);
placeArray(inventory, "exemplars", "inventory.exemplars", notes);
- return { surfaces, intent, inventory, composition, notes };
+ return { surfaceIds, intent, inventory, composition, notes };
}
function collectScopeIds(inventory: Yaml | undefined): string[] {
@@ -224,28 +214,45 @@ export interface MigratedNodeFile {
}
/**
- * Convert the migrated facet docs into `nodes/*.md` files — the persistent form
- * of the Phase 2 facet→node projection. Each facet entry becomes one prose node
- * whose body is the entry's primary text and whose `under` is its placement
- * (`surface`, omitted when unplaced ⇒ cascades from core). Lossy by design:
- * structured affordances (evidence, check_refs, exemplar paths) are dropped, in
- * line with Option A. Returns one file per node (`nodes/.md`).
+ * Convert the migrated facet docs into a directory tree of `*.md` nodes — the
+ * persistent form of the facet→node projection. Each facet entry becomes one
+ * prose node placed by the directory layout: a placed node lands at
+ * `/.md` (its directory is its parent), an unplaced node at
+ * `.md` (the package root, cascading from core). Each derived surface also
+ * gets a bare `/index.md` so the directory survives even when no node
+ * lands in it. Lossy by design: structured affordances (evidence, check_refs,
+ * exemplar paths) are dropped, in line with Option A.
*/
export function migratedNodeFiles(result: MigrationResult): MigratedNodeFile[] {
const files: MigratedNodeFile[] = [];
const seen = new Set();
+ // Seed each derived surface as a directory with an index node, so an empty
+ // surface still exists as a tree position.
+ for (const surfaceId of result.surfaceIds) {
+ files.push({
+ relativePath: `${surfaceId}/index.md`,
+ content: serializeNode({
+ frontmatter: {},
+ body: `The \`${surfaceId}\` surface.`,
+ }),
+ });
+ }
+
const emit = (entry: Yaml, body: string) => {
const id = typeof entry.id === "string" ? entry.id : undefined;
if (!id || seen.has(id)) return;
seen.add(id);
- const under = typeof entry.surface === "string" ? entry.surface : undefined;
+ const surface =
+ typeof entry.surface === "string" ? entry.surface : undefined;
const doc: GhostNodeDocument = {
- frontmatter: { id, ...(under !== undefined ? { under } : {}) },
+ frontmatter: {},
body: body.trim(),
};
+ const relativePath =
+ surface !== undefined ? `${surface}/${id}.md` : `${id}.md`;
files.push({
- relativePath: `nodes/${id}.md`,
+ relativePath,
content: serializeNode(doc),
});
};
diff --git a/packages/ghost/src/scan/node-tree.ts b/packages/ghost/src/scan/node-tree.ts
new file mode 100644
index 00000000..ab991b92
--- /dev/null
+++ b/packages/ghost/src/scan/node-tree.ts
@@ -0,0 +1,139 @@
+import { readdir, readFile } from "node:fs/promises";
+import { join } from "node:path";
+import { type PlacedNode, parseNode } from "#ghost-core";
+import { GHOST_CHECKS_DIRNAME } from "./checks-dir.js";
+import { FINGERPRINT_MANIFEST_FILENAME } from "./constants.js";
+
+/** A directory `index.md` denotes the prose for the directory itself. */
+const INDEX_FILENAME = "index.md";
+
+/**
+ * Reserved package-root entries that are never nodes. `checks/` is a reserved
+ * top-level subtree (the markdown checks that govern surfaces). The manifest is
+ * the package anchor.
+ *
+ * NOTE: `checks/` is reserved at the package root only. Internal/nested reuse
+ * (e.g. teams that compose nested `.agents`-style trees) will want this set to
+ * be configurable per package — a planned follow-up, deliberately not built yet.
+ */
+const RESERVED_ROOT_ENTRIES = new Set([
+ FINGERPRINT_MANIFEST_FILENAME,
+ "manifest.yaml",
+ GHOST_CHECKS_DIRNAME,
+]);
+
+export interface LoadedNodeTree {
+ nodes: PlacedNode[];
+ /** Files that failed lint, with their first error message (path-relative id). */
+ invalid: Array<{ file: string; message: string }>;
+}
+
+/**
+ * Load authored prose nodes from the package's directory tree.
+ *
+ * Every `*.md` file under the package directory is a node. Its id is its path
+ * with `.md` dropped (`marketing/email.md` → `marketing/email`); its parent is
+ * its containing directory (`marketing`), or the implicit `core` root at the
+ * top level. A directory's own prose lives in its `index.md`: the root
+ * `index.md` is the `core` node (parent absent); `marketing/index.md` is the
+ * `marketing` node (id `marketing`, parent `core`). The `checks/` subtree and
+ * `manifest.yml` are reserved and skipped.
+ *
+ * A file that fails per-node lint is collected in `invalid` (with its first
+ * error) and skipped rather than throwing, so one bad node does not block
+ * folding the rest. Absent or empty tree → no nodes.
+ */
+export async function loadNodeTree(
+ packageDir: string,
+): Promise {
+ const nodes: PlacedNode[] = [];
+ const invalid: LoadedNodeTree["invalid"] = [];
+
+ await walk(packageDir, "", true, nodes, invalid);
+
+ // Deterministic order by id, mirroring the old sorted readdir.
+ nodes.sort((a, b) => a.id.localeCompare(b.id));
+ invalid.sort((a, b) => a.file.localeCompare(b.file));
+ return { nodes, invalid };
+}
+
+async function walk(
+ packageDir: string,
+ relDir: string,
+ isRoot: boolean,
+ nodes: PlacedNode[],
+ invalid: LoadedNodeTree["invalid"],
+): Promise {
+ const absDir = relDir === "" ? packageDir : join(packageDir, relDir);
+ let entries: Array<{ name: string; isDir: boolean }>;
+ try {
+ const dirents = await readdir(absDir, { withFileTypes: true });
+ entries = dirents.map((d) => ({ name: d.name, isDir: d.isDirectory() }));
+ } catch {
+ return;
+ }
+
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
+ if (isRoot && RESERVED_ROOT_ENTRIES.has(entry.name)) continue;
+ if (entry.name.startsWith(".")) continue;
+
+ const relPath = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
+
+ if (entry.isDir) {
+ await walk(packageDir, relPath, false, nodes, invalid);
+ continue;
+ }
+ if (!entry.name.endsWith(".md")) continue;
+
+ const raw = await readFile(join(packageDir, relPath), "utf-8");
+ const { node, report } = parseNode(raw);
+ if (node === null || report.errors > 0) {
+ const first = report.issues.find((issue) => issue.severity === "error");
+ invalid.push({
+ file: relPath,
+ message: first?.message ?? "invalid node",
+ });
+ continue;
+ }
+
+ const { id, parent, folder } = locate(relPath);
+ nodes.push({
+ id,
+ ...(parent !== undefined ? { parent } : {}),
+ folder,
+ doc: node,
+ });
+ }
+}
+
+/**
+ * Compute a node's id, parent, and file folder from its package-relative path.
+ * The folder is the directory the file sits in — the unit of corridor
+ * containment, which differs from `parent` for index nodes.
+ * - `index.md` → id `core`, parent absent, folder ``.
+ * - `a/index.md` → id `a`, parent `core`, folder `a`.
+ * - `a/b/index.md` → id `a/b`, parent `a`, folder `a/b`.
+ * - `a.md` → id `a`, parent `core`, folder ``.
+ * - `a/b.md` → id `a/b`, parent `a`, folder `a`.
+ */
+function locate(relPath: string): {
+ id: string;
+ parent?: string;
+ folder: string;
+} {
+ const withoutExt = relPath.replace(/\.md$/, "");
+ const segments = withoutExt.split("/");
+ const isIndex = segments[segments.length - 1] === "index";
+ const idSegments = isIndex ? segments.slice(0, -1) : segments;
+ // The file folder: drop the filename segment (`index` or the leaf name).
+ const folder = segments.slice(0, -1).join("/");
+
+ if (idSegments.length === 0) {
+ // Root index.md → the core node, folder is the package root ("").
+ return { id: "core", folder };
+ }
+ const id = idSegments.join("/");
+ const parent =
+ idSegments.length === 1 ? "core" : idSegments.slice(0, -1).join("/");
+ return { id, parent: parent === "core" ? "core" : parent, folder };
+}
diff --git a/packages/ghost/src/scan/nodes-dir.ts b/packages/ghost/src/scan/nodes-dir.ts
deleted file mode 100644
index 652fe848..00000000
--- a/packages/ghost/src/scan/nodes-dir.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { readdir, readFile } from "node:fs/promises";
-import { join } from "node:path";
-import { type GhostNodeDocument, parseNode } from "#ghost-core";
-
-export const GHOST_NODES_DIRNAME = "nodes";
-
-export interface LoadedNodesDir {
- nodes: GhostNodeDocument[];
- /** Files that failed lint, with their first error message. */
- invalid: Array<{ file: string; message: string }>;
-}
-
-/**
- * Load authored prose nodes from `/nodes/*.md`. Each file is parsed
- * and validated per-node; a file with errors is collected in `invalid` (with
- * its first error) and skipped rather than throwing, so one bad node does not
- * block folding the rest. Absent directory → no nodes.
- *
- * Phase 2 keeps discovery deliberately minimal (one default `nodes/` directory,
- * mirroring `checks/`). Loose-anywhere and custom layouts are a later
- * refinement.
- */
-export async function loadNodesDir(
- packageDir: string,
-): Promise {
- const dir = join(packageDir, GHOST_NODES_DIRNAME);
- let entries: string[];
- try {
- entries = await readdir(dir);
- } catch {
- return { nodes: [], invalid: [] };
- }
-
- const nodes: GhostNodeDocument[] = [];
- const invalid: LoadedNodesDir["invalid"] = [];
-
- for (const name of entries.sort()) {
- if (!name.endsWith(".md")) continue;
- const raw = await readFile(join(dir, name), "utf-8");
- const { node, report } = parseNode(raw);
- if (node === null || report.errors > 0) {
- const first = report.issues.find((issue) => issue.severity === "error");
- invalid.push({ file: name, message: first?.message ?? "invalid node" });
- continue;
- }
- nodes.push(node);
- }
-
- return { nodes, invalid };
-}
diff --git a/packages/ghost/src/scan/scan-status.ts b/packages/ghost/src/scan/scan-status.ts
index 27d2cf93..6019fcd1 100644
--- a/packages/ghost/src/scan/scan-status.ts
+++ b/packages/ghost/src/scan/scan-status.ts
@@ -69,7 +69,6 @@ async function scanContribution(
const loaded = await loadFingerprintPackage(paths);
return summarizeFingerprintContribution({
graph: loaded.graph,
- surfaceIds: (loaded.surfaces?.surfaces ?? []).map((s) => s.id),
});
} catch (err) {
return summarizeFingerprintContribution({
diff --git a/packages/ghost/src/scan/templates.ts b/packages/ghost/src/scan/templates.ts
index 5cf56b66..972210e2 100644
--- a/packages/ghost/src/scan/templates.ts
+++ b/packages/ghost/src/scan/templates.ts
@@ -29,45 +29,34 @@ function manifestFile(): TemplateFile {
}
/**
- * The default starter: the surfaces spine (the implicit `core` root needs no
- * declaration, so the file starts empty) plus one `core`-placed intent node
- * that demonstrates the shape — frontmatter handles + prose body written
- * through the intent/inventory/composition lenses.
+ * The default starter: a manifest plus the package-root `index.md` — the `core`
+ * node whose prose cascades to every surface. The directory tree is the spine:
+ * add a surface by adding a directory, give it prose with its own `index.md`,
+ * and place nodes as `/.md`.
*/
const DEFAULT_TEMPLATE: GhostInitTemplate = {
name: "default",
- description: "Minimal node package: surfaces spine + one core intent node.",
+ description: "Minimal node package: manifest + a core index node.",
files() {
return [
manifestFile(),
{
- relativePath: "surfaces.yml",
- content: `schema: ghost.surfaces/v1
-# The implicit \`core\` root needs no declaration. Add surfaces as you author,
-# e.g.:
-# surfaces:
-# - id: checkout
-# parent: core
-surfaces: []
-`,
- },
- {
- relativePath: "nodes/core-voice.md",
+ relativePath: "index.md",
content: `---
-id: core-voice
-under: core
+description: The product-wide root; true everywhere.
---
-Replace this with your product's voice. A node is prose written through the
-intent / inventory / composition lenses — they guide what to capture, they are
-not fields:
+Replace this with your product's voice — the \`core\` node. A node is prose
+written through the intent / inventory / composition lenses; they guide what to
+capture, they are not fields:
- intent — the why and the stance (e.g. "calm, direct, never breathless").
- inventory — the material you have (tokens, components, pointers to code).
- composition — how it is assembled (the patterns that make it feel intentional).
-This node sits at \`core\`, so it cascades to every surface. Place
-surface-specific nodes with \`under: \`, link related nodes with
+This file is the package-root \`index.md\`, so it cascades to every surface. Add
+a surface by adding a directory: \`checkout/index.md\` is the \`checkout\` surface,
+and \`checkout/payment.md\` is a node under it. Link related nodes with
\`relates\`, and tag medium-bound expressions with \`incarnation\` (e.g. email,
billboard, voice). Leave essence untagged.
`,
diff --git a/packages/ghost/src/skill-bundle/SKILL.md b/packages/ghost/src/skill-bundle/SKILL.md
index 3f97a616..3ee5ae28 100644
--- a/packages/ghost/src/skill-bundle/SKILL.md
+++ b/packages/ghost/src/skill-bundle/SKILL.md
@@ -14,10 +14,11 @@ materials it draws from, and the patterns that make it feel intentional.
```text
.ghost/
- manifest.yml # schema + id
- surfaces.yml # the spine: surfaces + their parent (core is implicit)
- nodes/*.md # prose nodes — the design expression
- checks/*.md # optional ghost.check/v1 checks
+ manifest.yml # schema + id
+ index.md # the core node, true everywhere (optional)
+ /index.md # a surface's own prose
+ /.md # a node placed in that surface
+ checks/*.md # optional ghost.check/v1 checks
```
The checked-in `.ghost/` package is the source of truth. Ordinary Git
@@ -26,27 +27,49 @@ are drafts, and committed fingerprint changes are canonical for Ghost. Checks ar
markdown rules an agent evaluates. Ghost is not a lifecycle manager, proposal system,
design-system registry, or screenshot archive.
-The fingerprint is a graph of **nodes**. A node is a markdown file:
-frontmatter (`id`, `description`, `under`, `relates`, `incarnation`) + a prose
-body. **Intent + inventory + composition** are the authoring lenses the body is
-written through — they guide what to capture, they are not fields or node types:
-
-- intent — the why and the stance.
-- inventory — the materials and pointers to implementation the agent can inspect.
-- composition — the patterns that make the surface feel intentional.
-
-`description` is the retrieval payload — a one-line "what this is / when to
+The fingerprint is a graph of **nodes**, and the **directory tree is the graph**.
+A node is a markdown file: descriptive frontmatter (`description`, `relates`,
+`incarnation`) + a prose body. A node's **identity is its path** (`marketing/email.md`
+→ `marketing/email`) and its **parent is its containing directory**. A surface
+is just a directory, and a directory's own prose lives in its `index.md`
+(`marketing/index.md` is the `marketing` surface; the package-root `index.md` is
+the implicit `core` node, true everywhere). You write the body through three
+authoring lenses, **intent + inventory + composition**. They guide what to
+capture; they are not fields or node types:
+
+- intent: the why and the stance.
+- inventory: the materials and pointers to implementation the agent can inspect.
+- composition: the patterns that make the surface feel intentional.
+
+`description` is the retrieval payload, a one-line "what this is / when to
gather it" (like a tool's name + description); `ghost gather` with no argument
-lists nodes by id + description for the agent to match against. `under` places a
-node so it is inherited downward (`core` is the implicit root that reaches every
-surface); `relates` links nodes laterally; `incarnation` tags a medium-bound
-expression (essence is untagged). Free-form keys (`audience`, …) pass through.
-See [references/capture.md](references/capture.md) for the full node shape.
+lists nodes by id + description for the agent to match against. The directory
+places a node so it is inherited downward (`core` is the implicit root that
+reaches every surface); `relates` links nodes laterally; `incarnation` tags a
+medium-bound expression (essence is untagged). Free-form keys (`audience`, …)
+pass through. See [references/capture.md](references/capture.md) for the full
+node shape.
+
+**How `gather` composes** (folders are walls; files fill the corridor):
+
+- **spine** (full bodies): the package inherits every file from the root down to
+ the surface's own folder, so a feature's `invariants.md` reaches every screen
+ in that feature, and root files reach everywhere. A **sibling folder is a
+ wall**: its nodes never appear, not even as a pointer.
+- **edges** (full bodies, one hop): each spine node's `relates` targets. Author
+ a broad rule once at the level it is true (say `relates: { to: arcade }` on
+ `features/`) and every descendant inherits it. A link to a hub also unfolds
+ the hub's subtree as spokes.
+- **spokes** (pointers: id + description): the surface's own descendants and any
+ edge hub's subtree. The agent reads the descriptions and pulls what it needs
+ with a follow-up `gather`.
Checks and review validate output; they are not generation input.
`manifest.yml` anchors the package with `schema: ghost.fingerprint-package/v1`.
-The tree is declared in `surfaces.yml`, never inferred from filenames or paths.
+The tree is the layout itself: ids and parents come from where files sit, so
+moving a node is a rename. Reserved at the package root: `manifest.yml` and the
+`checks/` subtree; every other `*.md` is a node.
Optional `ghost.check/v1` markdown checks live in `checks/*.md`, routed by surface.
Use `ghost signals` as a stdout-only reconnaissance helper when an agent needs
@@ -59,22 +82,22 @@ the child `ghost` process when they need repo-local Ghost files outside raw
one product in a monorepo). Ghost stays adapter-neutral: wrappers consume JSON
and map severities into their own review or check format.
-A package can **extend** another by identity — the shared-brand pattern. The
+A package can **extend** another by identity (the shared-brand pattern). The
manifest's `extends` maps a package id to where it lives:
`extends: { brand: ../brand/.ghost }`. Then nodes reference inherited context by
-identity, never path: `under: brand:core` or `relates: [{ to: brand:core-trust }]`.
-Inherited nodes are read-only and flow into gather/validate like local ones.
+identity, never path: `relates: [{ to: brand:core/trust }]` (a `:`
+ref). Inherited nodes are read-only and flow into gather/validate like local ones.
## Core CLI Verbs
| Verb | Purpose |
|---|---|
-| `ghost init [--template ]` | Scaffold `.ghost/` with manifest, surfaces spine, and a seed node. |
+| `ghost init [--template ]` | Scaffold `.ghost/` with a manifest and a core `index.md` node. |
| `ghost scan [dir] [--format json]` | Report node/surface contribution. |
-| `ghost validate [file-or-dir]` | Validate the package — artifact shape and the node graph (links resolve, one root, acyclic). |
+| `ghost validate [file-or-dir]` | Validate the package: artifact shape and the node graph (links resolve, one root, acyclic). |
| `ghost checks --surface ` | Select and ground the markdown checks governing the named surfaces. |
| `ghost review --surface [--diff ]` | Emit an advisory review packet: touched surfaces, routed checks, and fingerprint grounding (diff embedded verbatim). |
-| `ghost gather [surface] [--as ]` | Compose a surface's context slice (own + inherited + edge), or list the surface menu. |
+| `ghost gather [surface] [--as ]` | Compose a surface's context slice (corridor spine + relates edges, plus spoke pointers), or list the surface menu. |
| `ghost skill install` | Install this unified skill bundle. |
## Advanced CLI Verbs
@@ -83,7 +106,7 @@ Inherited nodes are read-only and flow into gather/validate like local ones.
|---|---|
| `GHOST_PACKAGE_DIR= ghost init` / `ghost init --package ` | Create or resolve a custom fingerprint package directory for host wrappers or a monorepo package. |
| `ghost signals [path]` | Emit raw repo signals for fingerprint authoring. |
-| `ghost migrate [dir]` | Migrate a legacy `.ghost/` package onto the surface model. |
+| `ghost migrate [dir]` | Migrate a legacy `.ghost/` package onto the directory-tree node model. |
## Workflows
diff --git a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
index 0fc69c19..6f89a708 100644
--- a/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
+++ b/packages/ghost/src/skill-bundle/references/authoring-scenarios.md
@@ -83,11 +83,12 @@ content; scan frequency and raw signals do not establish guidance.
## 4. Draft The Nodes
-Write the smallest useful set of `nodes/*.md`, each a purpose-coherent prose
-body with a one-line `description`, placed with `under` and linked with
-`relates` where a relationship carries meaning. Write each body through the
-intent / inventory / composition lenses — the why, the material (with pointers
-to implementation), and how it is assembled. These are lenses, not fields.
+Write the smallest useful set of nodes, each a purpose-coherent prose body with
+a one-line `description`, placed by putting its file in the right surface
+directory and linked with `relates` where a relationship carries meaning. Write
+each body through the intent / inventory / composition lenses: the why, the
+material (with pointers to implementation), and how it is assembled. These are
+lenses, not fields.
Label uncertain reasoning as provisional. Prefer a few high-confidence nodes
with evidence over a broad catalog. In auto-draft mode, write nodes directly
diff --git a/packages/ghost/src/skill-bundle/references/brief.md b/packages/ghost/src/skill-bundle/references/brief.md
index 69a62f0a..042c2917 100644
--- a/packages/ghost/src/skill-bundle/references/brief.md
+++ b/packages/ghost/src/skill-bundle/references/brief.md
@@ -27,10 +27,10 @@ json` as the agent interface.
The host agent owns natural-language matching: read the surface menu (each
surface's authored description) and pick the surface the ask belongs to. Ghost
-never infers a surface from a repo path — the agent names it.
+never infers a surface from a repo path; the agent names it.
When no surface is selected (or an unknown one is named), `gather` returns the
-surface menu, never the whole tree — choose a surface from it rather than
+surface menu, never the whole tree; choose a surface from it rather than
guessing.
Return a short human-facing brief synthesized from the slice: the relevant
diff --git a/packages/ghost/src/skill-bundle/references/capture.md b/packages/ghost/src/skill-bundle/references/capture.md
index 0827298e..a34d9fa6 100644
--- a/packages/ghost/src/skill-bundle/references/capture.md
+++ b/packages/ghost/src/skill-bundle/references/capture.md
@@ -18,27 +18,29 @@ is checked in, Ghost treats the fingerprint package as canonical.
```text
.ghost/
- manifest.yml # schema + id
- surfaces.yml # the spine: surfaces + their `parent` (core is implicit)
- nodes/ # one prose node per file
- core-voice.md
- checkout-trust.md
- checks/ # optional ghost.check/v1 markdown checks
+ manifest.yml # schema + id
+ index.md # the core node, true everywhere
+ checkout/ # a surface is a directory
+ index.md # the checkout surface's own prose
+ trust.md # a node placed under checkout
+ checks/ # optional ghost.check/v1 markdown checks
```
-A **node** is a markdown file: YAML frontmatter (the machine handles) + a prose
-body (the design expression). The fingerprint is the graph of nodes the loader
-folds together; `ghost gather ` traverses it.
+A **node** is a markdown file: YAML frontmatter (descriptive properties) + a
+prose body (the design expression). The **directory tree is the graph**: a
+node's id is its path, its parent is its containing directory, and a surface is
+just a directory whose own prose lives in its `index.md`. `ghost gather
+` traverses it.
## The node shape
+A node at `checkout/trust.md` (id `checkout/trust`, parent `checkout`):
+
```markdown
---
-id: checkout-trust # required: unique, stable
description: Trust at the payment moment. # the retrieval payload (see below)
-under: checkout # optional: parent — inherited downward
relates: # optional: lateral links
- - to: core-trust
+ - to: core/trust
as: reinforces # reinforces | contrasts | variant
incarnation: web # optional: email | billboard | voice | … (omit = essence)
# free-form keys (audience, stage, …) are allowed and pass through untouched
@@ -48,15 +50,18 @@ Near the moment of payment, reduce felt risk. Proximity of reassurance to the
action beats completeness…
```
-- **`description`** is how an agent finds the node — a one-line "what this is and
+- **`description`** is how an agent finds the node: a one-line "what this is and
when to gather it," exactly like a tool's name + description. `ghost gather`
with no argument lists nodes by id + description; the agent matches the ask
against those and names one. The body is the node's "implementation"; the
description is what makes it discoverable. Write one on any node worth
anchoring a task at.
-- **`under`** places the node — a node inherits everything it sits under. The
- brand soul lives at `core` (implicit root), so `core`-placed nodes reach every
- surface.
+- **The directory places the node.** Folders are walls; files fill the
+ corridor. A node inherits every file in the folders above it, up to the root;
+ a sibling folder is invisible. The brand soul lives in the package-root files
+ (the `core` node and other root files), so it reaches every surface. Author a
+ broad rule at the broadest folder where it is true: a feature's
+ `invariants.md` reaches every screen in that feature and nowhere else.
- **`relates`** links laterally when a relationship carries rationale. When the
rationale is rich (e.g. "checkout and item-detail disagree on density on
purpose"), write a **relationship node** whose body explains the tension.
@@ -67,16 +72,17 @@ action beats completeness…
Intent / inventory / composition are **authoring lenses**, not fields and not
node types. They are the things worth thinking through as you write a node's
-prose — a node may lean entirely on one:
+prose, and a node may lean entirely on one:
-- **intent** — the why and the stance.
-- **inventory** — the material you have (tokens, components, and pointers to the
+- **intent**: the why and the stance.
+- **inventory**: the material you have (tokens, components, and pointers to the
actual implementation in code).
-- **composition** — how it is assembled (the patterns that make it intentional).
+- **composition**: how it is assembled (the patterns that make it intentional).
A finding cites a node by id, so keep a node **purpose-coherent**: one purpose,
-any length. Split into a second node only when a handle diverges — a different
-`under`, a different `incarnation`, or a genuinely different `relates` role.
+any length. Split into a second node only when a handle diverges, say a different
+directory (parent), a different `incarnation`, or a genuinely different
+`relates` role.
## Steps
@@ -94,31 +100,34 @@ a single contract organizes locality.
### 2. Initialize
```bash
-ghost init # scaffolds manifest + surfaces.yml + a seed node
+ghost init # scaffolds manifest + a core index.md node
ghost scan
```
`ghost init` is template-driven (`--template ` selects a starter). The
-default template seeds the spine plus one `core` node demonstrating the shape.
+default template seeds the package-root `index.md` (the `core` node),
+demonstrating the shape.
-### 3. Shape the spine
+### 3. Shape the tree
-Edit `surfaces.yml` to declare the surfaces this product has and their `parent`
-(containment). `core` is implicit. The tree is always declared here — never
-inferred from node filenames or repo paths.
+Add a surface by adding a directory: `checkout/` is the `checkout` surface, and
+`checkout/index.md` holds its prose. Nest surfaces by nesting directories. The
+tree is the layout itself; a node's id and parent come from where its file
+sits, never from a declared spine.
### 4. Orient
Read the product, not just the component library. Look for surfaces, docs,
tests, stories, routes, screenshots, or examples that reveal hierarchy,
behavior, copy, accessibility, trust, and flow. `ghost signals .` emits raw
-scratch observations — curate, never copy verbatim into a node.
+scratch observations; curate, never copy verbatim into a node.
### 5. Write sparse nodes
-Add the smallest useful set of `nodes/*.md`, each a purpose-coherent prose body
-written through the lenses, placed with `under` and linked with `relates` where
-a relationship carries meaning. Prefer a few high-confidence nodes over a noisy
+Add the smallest useful set of nodes, each a purpose-coherent prose body
+written through the lenses, placed by putting its file in the right directory
+and linked with `relates` where a relationship carries meaning. Prefer a few
+high-confidence nodes over a noisy
catalog. Ask the human to keep, soften, reject, or re-place important claims
before treating draft nodes as durable.
@@ -139,8 +148,6 @@ ghost check --base HEAD
- Never describe any file outside `.ghost/` as canonical package input.
- Never treat raw `ghost signals` output as a node without curation.
-- Never infer the surface tree from filenames or repo paths — declare it in
- `surfaces.yml`.
- Never invent surface-composition obligations absent from evidence or human
direction.
- Never promote subjective taste directly into checks; make it deterministic or
diff --git a/packages/ghost/src/skill-bundle/references/review.md b/packages/ghost/src/skill-bundle/references/review.md
index 668c7f29..91b65cff 100644
--- a/packages/ghost/src/skill-bundle/references/review.md
+++ b/packages/ghost/src/skill-bundle/references/review.md
@@ -22,7 +22,7 @@ in the surface's fingerprint slice. Use JSON as the agent contract. It includes:
- `touched_surfaces`: the surfaces the diff resolved to
- `checks`: the relevant checks per surface, with `relevance` (own or inherited)
- `grounding`: per surface, the slice's prose `nodes`, each with `provenance`
- (own / ancestor / edge). The why and the what live in each node's prose — read
+ (own / ancestor / edge). The why and the what live in each node's prose; read
the grounded nodes, own first, then inherited, then related.
Ghost selects and grounds the checks; it does not run them. Evaluate each
@@ -48,7 +48,7 @@ refs (principles/contracts as the why, exemplars as what good looks like), and a
repair or intentional-divergence rationale.
When a surface's grounding is silent, local evidence can still support advisory
-critique — label those findings provisional and non-Ghost-backed.
+critique; label those findings provisional and non-Ghost-backed.
Fingerprint edits are ordinary Git-reviewed edits to the split fingerprint
package. Do not silently rewrite the Ghost package during review unless the user
diff --git a/packages/ghost/src/skill-bundle/references/schema.md b/packages/ghost/src/skill-bundle/references/schema.md
index a5c1cca1..5cdb0fb3 100644
--- a/packages/ghost/src/skill-bundle/references/schema.md
+++ b/packages/ghost/src/skill-bundle/references/schema.md
@@ -1,6 +1,6 @@
---
name: schema
-description: The Ghost fingerprint package shape — nodes, the spine, checks, and extends.
+description: The Ghost fingerprint package shape: nodes, the directory tree, checks, and extends.
---
# Ghost Fingerprint Package Reference
@@ -9,27 +9,33 @@ Canonical package:
```text
.ghost/
- manifest.yml ghost.fingerprint-package/v1 — id + optional extends
- nodes/*.md ghost.node/v1 — the design expression (the unit)
- surfaces.yml optional ghost.surfaces/v1 — a terse spine (id + parent)
- checks/*.md optional ghost.check/v1 — agent-evaluated output checks
+ manifest.yml ghost.fingerprint-package/v1: id + optional extends
+ index.md the core node, true everywhere (optional)
+ /index.md a surface's own prose (the directory is the surface)
+ /.md ghost.node/v1: a node placed in that surface
+ checks/*.md optional ghost.check/v1: agent-evaluated output checks
```
+The **directory tree is the graph**: a node's id is its path and its parent is
+its containing directory. A surface is a directory; its own prose is its
+`index.md`. The package-root `index.md` is the implicit `core` node. Reserved at
+the root: `manifest.yml` and `checks/`; every other `*.md` is a node.
+
Git is the approval boundary: checked-in files are canonical; uncommitted or
unmerged edits are draft work. One contract per package; the contract carries no
paths and infers nothing from repo location.
## Nodes
-A node is the unit — a markdown file with frontmatter + a prose body:
+A node is the unit: a markdown file with descriptive frontmatter + a prose
+body. Identity and containment are not in the frontmatter; they are where the
+file sits. A node at `checkout/trust.md`:
```yaml
---
-id: checkout-trust # required, unique
description: Trust at the payment moment. # the retrieval payload
-under: checkout # optional parent (inherited downward)
relates: # optional lateral links
- - to: core-trust
+ - to: core/trust
as: reinforces # reinforces | contrasts | variant
incarnation: web # optional: email | billboard | voice | … (omit = essence)
# free-form keys (audience, stage, …) pass through untouched
@@ -39,25 +45,18 @@ lenses, not fields.
```
`description` is how an agent selects a node (like a tool's name + description).
-`under` places the node so it is inherited downward (`core` is the implicit root that
-reaches everywhere). `relates` links nodes laterally. `incarnation` tags a
-medium-bound expression. The tree lives only in `under`/`surfaces.yml`, never in
-the id and never inferred from a path.
-
-## The spine (optional)
+The file's location places it: `checkout/trust.md` has id `checkout/trust` and
+is inherited downward from `checkout` (`core` is the implicit root that reaches
+everywhere). `relates` links nodes laterally. `incarnation` tags a medium-bound
+expression. The tree is the layout; ids encode hierarchy because they *are* paths.
-`surfaces.yml` is a terse place to declare bare tree positions (id + parent +
-optional description) in one file instead of as bodyless node files. It folds
-into the same node id space — a position that needs guidance is just a node with
-that id.
+## Surfaces are directories
-```yaml
-schema: ghost.surfaces/v1
-surfaces:
- - id: checkout
- parent: core
- description: The purchase flow.
-```
+There is no spine file. A surface exists when its directory exists; give it prose
+with an `index.md`, place nodes inside it, and nest surfaces by nesting
+directories. A surface that needs no prose of its own is simply a directory that
+holds nodes. Moving a node to another directory changes its id (a rename) and
+its parent; `ghost validate` reports any `relates` that no longer resolve.
## Manifest + extends
@@ -68,16 +67,25 @@ extends:
brand: ../brand/.ghost # inherit another contract's nodes, by identity
```
-A `brand:core-trust` ref in `under`/`relates` resolves into the extended
-package's nodes (read-only). Reference is by identity (the `extends` key), never
-by path.
+A `brand:core/trust` ref in `relates` resolves into the extended package's nodes
+(read-only), a `:` ref. Reference is by identity (the `extends`
+key), never by repo path.
## Gather
-`ghost gather ` composes a node's slice: its own body + inherited
-ancestors + one-hop `relates`, filtered by `--as `. With no
-argument, `gather` lists nodes by id + description for the agent to match the ask
-against. The agent names the node; Ghost never infers it from a path.
+`ghost gather ` composes a node's slice, filtered by `--as `:
+
+- **spine** (full bodies): every file on the corridor from the package root
+ down to the node's own folder. Folders are walls; sibling folders never
+ appear.
+- **edges** (full bodies, one hop): each spine node's `relates` targets. A rule
+ authored high in the corridor (e.g. `relates: { to: arcade }` on `features/`)
+ reaches every descendant.
+- **spokes** (pointers): the node's own descendants and any edge hub's subtree,
+ offered as id + description for the agent to pull with a follow-up `gather`.
+
+With no argument, `gather` lists nodes by id + description for the agent to match
+the ask against. The agent names the node; Ghost never infers it from a path.
## Checks
diff --git a/packages/ghost/test/cli.test.ts b/packages/ghost/test/cli.test.ts
index bc1a4463..2a49ebb6 100644
--- a/packages/ghost/test/cli.test.ts
+++ b/packages/ghost/test/cli.test.ts
@@ -221,12 +221,9 @@ describe("ghost CLI", () => {
expect(init.code).toBe(0);
const initOutput = JSON.parse(init.stdout);
expect(Object.keys(initOutput).sort()).toEqual(["dir", "written"]);
- // Node package: manifest + surfaces spine + a seed node, no facet files.
+ // Node package: manifest + the package-root core index node, no facet files.
expect(initOutput.written).toContain("manifest.yml");
- expect(initOutput.written).toContain("surfaces.yml");
- expect(initOutput.written.some((p: string) => p.startsWith("nodes/"))).toBe(
- true,
- );
+ expect(initOutput.written).toContain("index.md");
await expect(
readFile(join(dir, ".ghost", "manifest.yml"), "utf-8"),
).resolves.toContain("schema: ghost.fingerprint-package/v1");
@@ -324,8 +321,8 @@ describe("ghost CLI", () => {
it("refuses to overwrite existing fingerprint files unless forced", async () => {
await runCli(["init"], dir);
await writeFile(
- join(dir, ".ghost", "nodes", "core-voice.md"),
- "---\nid: core-voice\nunder: core\n---\n\nCurated Surface voice.\n",
+ join(dir, ".ghost", "index.md"),
+ "---\n---\n\nCurated Surface voice.\n",
);
const refused = await runCli(["init"], dir);
@@ -335,14 +332,14 @@ describe("ghost CLI", () => {
"Refusing to overwrite existing Ghost fingerprint file(s)",
);
await expect(
- readFile(join(dir, ".ghost", "nodes", "core-voice.md"), "utf-8"),
+ readFile(join(dir, ".ghost", "index.md"), "utf-8"),
).resolves.toContain("Curated Surface");
const forced = await runCli(["init", "--force"], dir);
expect(forced.code).toBe(0);
await expect(
- readFile(join(dir, ".ghost", "nodes", "core-voice.md"), "utf-8"),
+ readFile(join(dir, ".ghost", "index.md"), "utf-8"),
).resolves.toContain("intent / inventory / composition");
});
@@ -383,8 +380,7 @@ describe("ghost CLI", () => {
expect(init.code).toBe(0);
expect(init.stdout).toContain("manifest.yml");
- expect(init.stdout).toContain("surfaces.yml");
- expect(init.stdout).toContain("nodes/");
+ expect(init.stdout).toContain("index.md");
expect(init.stdout).not.toContain("cache/:");
expect(init.stdout).not.toContain("memory/intent.md:");
expect(
@@ -425,13 +421,11 @@ describe("ghost CLI", () => {
const lint = await runCli(["validate"], dir);
expect(lint.code).toBe(0);
- // The seed node lives at core, so it cascades to a gather of any surface.
+ // The seed node is the package-root index — the core node itself.
const gather = await runCli(["gather", "core", "--format", "json"], dir);
expect(gather.code).toBe(0);
const slice = JSON.parse(gather.stdout);
- expect(slice.nodes.some((n: { id: string }) => n.id === "core-voice")).toBe(
- true,
- );
+ expect(slice.nodes.some((n: { id: string }) => n.id === "core")).toBe(true);
});
it("runs signals and validate from the unified cli", async () => {
@@ -804,13 +798,13 @@ composition:
await writeGatherPackage(dir);
const result = await runCli(
- ["gather", "email-marketing", "--package", ".ghost", "--format", "json"],
+ ["gather", "email/marketing", "--package", ".ghost", "--format", "json"],
dir,
);
expect(result.code).toBe(0);
const slice = JSON.parse(result.stdout);
- expect(slice.surface).toBe("email-marketing");
+ expect(slice.surface).toBe("email/marketing");
const byId = Object.fromEntries(
slice.nodes.map((node: { id: string; provenance: unknown }) => [
node.id,
@@ -818,12 +812,12 @@ composition:
]),
);
// Graph slice (Option A, prose nodes): own + cascaded ancestors.
- expect(byId["brand-voice"]).toEqual({ kind: "ancestor", from: "core" });
- expect(byId["marketing-urgency"]).toEqual({ kind: "own" });
- // Phase 3 decision: edge contributions come from node `relates`, not from
- // legacy `composes` surface edges. checkout-clarity sits on a sibling
- // surface with no `relates` link in, so it is no longer pulled in.
- expect(byId["checkout-clarity"]).toBeUndefined();
+ // The root index (`core`) cascades; the marketing index node is own.
+ expect(byId["core"]).toEqual({ kind: "ancestor", from: "core" });
+ expect(byId["email/marketing"]).toEqual({ kind: "own" });
+ // checkout/clarity sits on a sibling surface with no `relates` link in, so
+ // it is not pulled in.
+ expect(byId["checkout/clarity"]).toBeUndefined();
});
it("filters the gather slice by incarnation via --as", async () => {
@@ -848,34 +842,31 @@ composition:
const ids = slice.nodes.map((n: { id: string }) => n.id).sort();
// essence (untagged) + matching web; the email node is filtered out.
expect(ids).toContain("launch");
- expect(ids).toContain("launch-web");
- expect(ids).not.toContain("launch-email");
+ expect(ids).toContain("launch/web");
+ expect(ids).not.toContain("launch/email");
});
it("inherits nodes from an extended package via extends", async () => {
- // Brand contract.
- await mkdir(join(dir, "brand", "nodes"), { recursive: true });
+ // Brand contract: a node at brand/core-trust.md → id `core-trust`.
+ await mkdir(join(dir, "brand"), { recursive: true });
await writeFile(
join(dir, "brand", "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: brand\n",
);
await writeFile(
- join(dir, "brand", "nodes", "core-trust.md"),
- "---\nid: core-trust\nunder: core\n---\n\nReduce felt risk.\n",
+ join(dir, "brand", "core-trust.md"),
+ "---\n---\n\nReduce felt risk.\n",
);
- // Product contract extends the brand.
- await mkdir(join(dir, "product", "nodes"), { recursive: true });
+ // Product contract extends the brand. The checkout surface is the
+ // directory `product/checkout/`, with a node at checkout/trust.md.
+ await mkdir(join(dir, "product", "checkout"), { recursive: true });
await writeFile(
join(dir, "product", "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: acme-checkout\nextends:\n brand: ../brand\n",
);
await writeFile(
- join(dir, "product", "surfaces.yml"),
- "schema: ghost.surfaces/v1\nsurfaces:\n - id: checkout\n parent: core\n",
- );
- await writeFile(
- join(dir, "product", "nodes", "checkout-trust.md"),
- "---\nid: checkout-trust\nunder: checkout\nrelates:\n - to: brand:core-trust\n as: reinforces\n---\n\nReassure at payment.\n",
+ join(dir, "product", "checkout", "trust.md"),
+ "---\nrelates:\n - to: brand:core-trust\n as: reinforces\n---\n\nReassure at payment.\n",
);
const validate = await runCli(
@@ -899,14 +890,13 @@ composition:
});
it("fails validate when a cross-package ref is not in extends", async () => {
- await mkdir(join(dir, "nodes"), { recursive: true });
await writeFile(
join(dir, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: solo\n",
);
await writeFile(
- join(dir, "nodes", "n.md"),
- "---\nid: n\nunder: core\nrelates:\n - to: brand:core-trust\n---\n\nBody.\n",
+ join(dir, "n.md"),
+ "---\nrelates:\n - to: brand:core-trust\n---\n\nBody.\n",
);
const validate = await runCli(["validate", "."], dir);
@@ -926,7 +916,7 @@ composition:
const payload = JSON.parse(result.stdout);
expect(payload.kind).toBe("menu");
expect(payload.surfaces.map((entry: { id: string }) => entry.id)).toContain(
- "email-marketing",
+ "email/marketing",
);
});
@@ -980,12 +970,10 @@ experience_contracts: []
expect(result.code).toBe(0);
const report = JSON.parse(result.stdout);
- expect(report.surfaces.map((s: { id: string }) => s.id)).toEqual([
- "lending",
- ]);
+ expect(report.surfaces).toEqual(["lending"]);
// The migrated package must lint clean and gather correctly.
- const lint = await runCli(["validate", ".ghost/surfaces.yml"], dir, {
+ const lint = await runCli(["validate", ".ghost"], dir, {
allowNoExit: true,
});
expect(lint.stdout).toContain("0 error(s)");
@@ -995,8 +983,9 @@ experience_contracts: []
dir,
);
const slice = JSON.parse(gather.stdout);
+ // The single-scope node landed at lending/scoped.md → id `lending/scoped`.
expect(
- slice.nodes.find((node: { id: string }) => node.id === "scoped")
+ slice.nodes.find((node: { id: string }) => node.id === "lending/scoped")
?.provenance,
).toEqual({ kind: "own" });
});
@@ -1019,16 +1008,14 @@ experience_contracts: []
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: c3\n",
);
+ // Surfaces are directories: checkout/ and email/ each with an index node.
+ await mkdir(join(ghost, "checkout"), { recursive: true });
+ await mkdir(join(ghost, "email"), { recursive: true });
await writeFile(
- join(ghost, "surfaces.yml"),
- `schema: ghost.surfaces/v1
-surfaces:
- - id: checkout
- parent: core
- - id: email
- parent: core
-`,
+ join(ghost, "checkout", "index.md"),
+ "---\n---\n\nCheckout.\n",
);
+ await writeFile(join(ghost, "email", "index.md"), "---\n---\n\nEmail.\n");
await writeFile(
join(ghost, "checks", "brand.md"),
"---\nname: brand\ndescription: Brand voice.\nseverity: medium\nsurface: core\n---\n## Instructions\nVoice.\n",
@@ -1071,17 +1058,12 @@ surfaces:
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: c4\n",
);
+ // core prose at the package root; checkout surface as a directory node.
+ await mkdir(join(ghost, "checkout"), { recursive: true });
+ await writeFile(join(ghost, "index.md"), "---\n---\n\nWarm everywhere.\n");
await writeFile(
- join(ghost, "surfaces.yml"),
- "schema: ghost.surfaces/v1\nsurfaces:\n - id: checkout\n parent: core\n",
- );
- await writeFile(
- join(ghost, "nodes", "brand-voice.md"),
- "---\nid: brand-voice\nunder: core\n---\n\nWarm everywhere.\n",
- );
- await writeFile(
- join(ghost, "nodes", "checkout-clarity.md"),
- "---\nid: checkout-clarity\nunder: checkout\n---\n\nCheckout copy is plain.\n",
+ join(ghost, "checkout", "clarity.md"),
+ "---\n---\n\nCheckout copy is plain.\n",
);
await writeFile(
join(ghost, "checks", "checkout.md"),
@@ -1108,10 +1090,10 @@ surfaces:
);
// Grounding is the gather slice: prose nodes by provenance (Phase 4).
const ids = checkout.nodes.map((n: { id: string }) => n.id);
- expect(ids).toContain("checkout-clarity"); // own
- expect(ids).toContain("brand-voice"); // inherited from core
+ expect(ids).toContain("checkout/clarity"); // own
+ expect(ids).toContain("core"); // cascades from the root index
const own = checkout.nodes.find(
- (n: { id: string }) => n.id === "checkout-clarity",
+ (n: { id: string }) => n.id === "checkout/clarity",
);
expect(own.provenance).toEqual({ kind: "own" });
});
@@ -1123,9 +1105,10 @@ surfaces:
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: c4b\n",
);
+ await mkdir(join(ghost, "checkout"), { recursive: true });
await writeFile(
- join(ghost, "surfaces.yml"),
- "schema: ghost.surfaces/v1\nsurfaces:\n - id: checkout\n parent: core\n",
+ join(ghost, "checkout", "index.md"),
+ "---\n---\n\nCheckout.\n",
);
const result = await runCli(
@@ -1155,62 +1138,47 @@ async function writeIncarnationPackage(dir: string): Promise {
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: incarnation-demo\n",
);
+ // launch surface as a directory; web/email incarnations as nodes under it.
+ await mkdir(join(ghost, "launch"), { recursive: true });
await writeFile(
- join(ghost, "surfaces.yml"),
- `schema: ghost.surfaces/v1
-surfaces:
- - id: launch
- description: Launch announcement.
- parent: core
-`,
+ join(ghost, "launch", "index.md"),
+ "---\n---\n\nOne idea, stated with confidence.\n",
);
await writeFile(
- join(ghost, "nodes", "launch.md"),
- "---\nid: launch\nunder: core\n---\n\nOne idea, stated with confidence.\n",
+ join(ghost, "launch", "web.md"),
+ "---\nincarnation: web\n---\n\nHero with one CTA.\n",
);
await writeFile(
- join(ghost, "nodes", "launch-web.md"),
- "---\nid: launch-web\nunder: launch\nincarnation: web\n---\n\nHero with one CTA.\n",
- );
- await writeFile(
- join(ghost, "nodes", "launch-email.md"),
- "---\nid: launch-email\nunder: launch\nincarnation: email\n---\n\nSubject is the headline.\n",
+ join(ghost, "launch", "email.md"),
+ "---\nincarnation: email\n---\n\nSubject is the headline.\n",
);
}
async function writeGatherPackage(dir: string): Promise {
const ghost = join(dir, ".ghost");
- await mkdir(join(ghost, "nodes"), { recursive: true });
+ await mkdir(join(ghost, "email", "marketing"), { recursive: true });
+ await mkdir(join(ghost, "checkout"), { recursive: true });
await writeFile(
join(ghost, "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: gather-demo\n",
);
+ // Surfaces are directories: email/ (with marketing/ nested) and checkout/.
+ // The root index is the brand voice that cascades everywhere.
await writeFile(
- join(ghost, "surfaces.yml"),
- `schema: ghost.surfaces/v1
-surfaces:
- - id: email
- description: Email surface.
- parent: core
- - id: email-marketing
- description: Marketing email.
- parent: email
- - id: checkout
- description: Checkout.
- parent: core
-`,
+ join(ghost, "index.md"),
+ "---\ndescription: Brand voice.\n---\n\nWarm and concise.\n",
);
await writeFile(
- join(ghost, "nodes", "brand-voice.md"),
- "---\nid: brand-voice\nunder: core\n---\n\nWarm and concise.\n",
+ join(ghost, "email", "index.md"),
+ "---\ndescription: Email surface.\n---\n\nEmail.\n",
);
await writeFile(
- join(ghost, "nodes", "marketing-urgency.md"),
- "---\nid: marketing-urgency\nunder: email-marketing\n---\n\nMarketing may use urgency.\n",
+ join(ghost, "email", "marketing", "index.md"),
+ "---\ndescription: Marketing email.\n---\n\nMarketing may use urgency.\n",
);
await writeFile(
- join(ghost, "nodes", "checkout-clarity.md"),
- "---\nid: checkout-clarity\nunder: checkout\n---\n\nCheckout copy is plain.\n",
+ join(ghost, "checkout", "clarity.md"),
+ "---\n---\n\nCheckout copy is plain.\n",
);
}
diff --git a/packages/ghost/test/fingerprint-package.test.ts b/packages/ghost/test/fingerprint-package.test.ts
index 74f170e6..91836449 100644
--- a/packages/ghost/test/fingerprint-package.test.ts
+++ b/packages/ghost/test/fingerprint-package.test.ts
@@ -36,20 +36,24 @@ describe("split fingerprint package", () => {
expect([...loaded.graph.nodes.keys()]).toEqual([]);
});
- it("folds authored nodes/*.md into the graph", async () => {
+ it("folds the directory tree of *.md nodes into the graph", async () => {
await writeManifest(dir);
- await mkdir(join(dir, "nodes"), { recursive: true });
+ await mkdir(join(dir, "checkout"), { recursive: true });
await writeFile(
- join(dir, "nodes", "checkout-trust.md"),
- "---\nid: checkout-trust\nunder: core\nincarnation: web\n---\n\nReduce felt risk near payment.\n",
+ join(dir, "checkout", "trust.md"),
+ "---\nincarnation: web\n---\n\nReduce felt risk near payment.\n",
);
const loaded = await loadFingerprintPackage(resolveFingerprintPackage(dir));
- const authored = loaded.graph.nodes.get("checkout-trust");
+ // id is the path; parent is the containing directory.
+ const authored = loaded.graph.nodes.get("checkout/trust");
expect(authored?.origin).toBe("node-file");
+ expect(authored?.parent).toBe("checkout");
expect(authored?.body).toBe("Reduce felt risk near payment.");
expect(authored?.incarnation).toBe("web");
+ // The containing directory resolves up to the implicit core root.
+ expect(loaded.graph.parents.get("checkout")).toBe("core");
});
it("guides legacy facet packages to migrate", async () => {
@@ -62,10 +66,7 @@ describe("split fingerprint package", () => {
});
it("reports a missing manifest", async () => {
- await writeFile(
- join(dir, "surfaces.yml"),
- "schema: ghost.surfaces/v1\nsurfaces: []\n",
- );
+ await writeFile(join(dir, "index.md"), "---\n---\n\nRoot prose.\n");
const report = await lintFingerprintPackage(dir);
diff --git a/packages/ghost/test/ghost-core/check-route.test.ts b/packages/ghost/test/ghost-core/check-route.test.ts
index a28e3f5d..fd2d66a9 100644
--- a/packages/ghost/test/ghost-core/check-route.test.ts
+++ b/packages/ghost/test/ghost-core/check-route.test.ts
@@ -1,9 +1,8 @@
import { describe, expect, it } from "vitest";
import {
assembleGraph,
- GHOST_SURFACES_SCHEMA,
type GhostCheckDocument,
- type GhostSurfacesDocument,
+ type PlacedNode,
selectChecksForSurfaces,
} from "../../src/ghost-core/index.js";
@@ -19,22 +18,26 @@ function check(name: string, surface?: string): GhostCheckDocument {
};
}
-const SURFACES: GhostSurfacesDocument = {
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [
- { id: "checkout", parent: "core" },
- { id: "email", parent: "core" },
- { id: "email-marketing", parent: "email" },
- ],
-};
+function placed(id: string, parent: string): PlacedNode {
+ // Directory/index node: its file folder is its own id.
+ return { id, parent, folder: id, doc: { frontmatter: {}, body: "Prose." } };
+}
-const GRAPH = assembleGraph({ surfaces: SURFACES });
+// The directory tree that establishes the surfaces:
+// checkout/ email/ email/marketing/
+const GRAPH = assembleGraph({
+ placedNodes: [
+ placed("checkout", "core"),
+ placed("email", "core"),
+ placed("email/marketing", "email"),
+ ],
+});
const CHECKS = [
check("brand", "core"),
check("checkout-color", "checkout"),
check("email-links", "email"),
- check("marketing-unsub", "email-marketing"),
+ check("marketing-unsub", "email/marketing"),
check("unplaced"), // governs core
];
@@ -55,7 +58,7 @@ describe("selectChecksForSurfaces", () => {
});
it("cascades multiple ancestor levels", () => {
- const routed = selectChecksForSurfaces(CHECKS, GRAPH, ["email-marketing"]);
+ const routed = selectChecksForSurfaces(CHECKS, GRAPH, ["email/marketing"]);
// own marketing + ancestor email + ancestor core (brand, unplaced)
expect(names(routed)).toEqual([
"brand",
@@ -66,18 +69,18 @@ describe("selectChecksForSurfaces", () => {
});
it("tags provenance own vs. ancestor", () => {
- const routed = selectChecksForSurfaces(CHECKS, GRAPH, ["email-marketing"]);
+ const routed = selectChecksForSurfaces(CHECKS, GRAPH, ["email/marketing"]);
const byName = Object.fromEntries(
routed.map((r) => [r.check.frontmatter.name, r.relevance]),
);
expect(byName["marketing-unsub"]).toEqual({
kind: "own",
- surface: "email-marketing",
+ surface: "email/marketing",
});
expect(byName["email-links"]).toMatchObject({
kind: "ancestor",
surface: "email",
- via: "email-marketing",
+ via: "email/marketing",
});
});
diff --git a/packages/ghost/test/ghost-core/graph-fold.test.ts b/packages/ghost/test/ghost-core/graph-fold.test.ts
index ac6f87a3..e04536f9 100644
--- a/packages/ghost/test/ghost-core/graph-fold.test.ts
+++ b/packages/ghost/test/ghost-core/graph-fold.test.ts
@@ -3,72 +3,86 @@ import {
ancestorChain,
assembleGraph,
GHOST_GRAPH_ROOT_ID,
- type GhostNodeDocument,
+ type PlacedNode,
} from "../../src/ghost-core/index.js";
-function nodeDoc(
- frontmatter: GhostNodeDocument["frontmatter"],
+// Model an index/directory node: its folder is its own id (`a/b/index.md`).
+// A parentless node is the root `core` (folder ``).
+function placed(
+ id: string,
+ parent: string | undefined,
+ frontmatter: PlacedNode["doc"]["frontmatter"] = {},
body = "Prose.",
-): GhostNodeDocument {
- return { frontmatter, body };
+): PlacedNode {
+ const folder = parent === undefined ? "" : id;
+ return {
+ id,
+ ...(parent !== undefined ? { parent } : {}),
+ folder,
+ doc: { frontmatter, body },
+ };
}
-describe("assembleGraph (node + surfaces fold)", () => {
- it("folds authored node files into the graph", () => {
+describe("assembleGraph (directory-tree fold)", () => {
+ it("folds placed nodes into the graph", () => {
const graph = assembleGraph({
- nodeFiles: [
- nodeDoc(
+ placedNodes: [
+ placed(
+ "checkout/trust",
+ "checkout",
{
- id: "checkout-trust",
- under: "checkout",
- relates: [{ to: "core-trust", as: "reinforces" }],
+ relates: [{ to: "core/trust", as: "reinforces" }],
incarnation: "web",
},
"Reduce felt risk near payment.",
),
],
});
- const node = graph.nodes.get("checkout-trust");
+ const node = graph.nodes.get("checkout/trust");
expect(node?.origin).toBe("node-file");
expect(node?.body).toBe("Reduce felt risk near payment.");
expect(node?.incarnation).toBe("web");
- expect(node?.relates).toEqual([{ to: "core-trust", as: "reinforces" }]);
+ expect(node?.relates).toEqual([{ to: "core/trust", as: "reinforces" }]);
+ expect(node?.parent).toBe("checkout");
});
- it("seeds the containment tree from surfaces and resolves ancestors", () => {
+ it("seeds the containment tree from directory parents and resolves ancestors", () => {
const graph = assembleGraph({
- surfaces: {
- schema: "ghost.surfaces/v1",
- surfaces: [
- { id: "checkout", parent: "core" },
- { id: "payment", parent: "checkout" },
- ],
- },
+ placedNodes: [
+ placed("checkout", "core"),
+ placed("checkout/payment", "checkout"),
+ ],
});
- expect(graph.parents.get("payment")).toBe("checkout");
+ expect(graph.parents.get("checkout/payment")).toBe("checkout");
expect(graph.parents.get("checkout")).toBe(GHOST_GRAPH_ROOT_ID);
- expect(ancestorChain(graph, "payment")).toEqual([
+ expect(ancestorChain(graph, "checkout/payment")).toEqual([
"checkout",
GHOST_GRAPH_ROOT_ID,
]);
});
- it("attaches an under-less node to the implicit core root", () => {
+ it("seeds intermediate directories that have no index node", () => {
+ // Only the deep leaf is placed; a/b and a are empty directories.
+ const graph = assembleGraph({
+ placedNodes: [placed("a/b/c", "a/b")],
+ });
+ expect(ancestorChain(graph, "a/b/c")).toEqual([
+ "a/b",
+ "a",
+ GHOST_GRAPH_ROOT_ID,
+ ]);
+ });
+
+ it("treats a parentless node as the implicit core root", () => {
const graph = assembleGraph({
- nodeFiles: [nodeDoc({ id: "top-level" })],
+ placedNodes: [placed("core", undefined, {}, "Root prose.")],
});
- expect(ancestorChain(graph, "top-level")).toEqual([GHOST_GRAPH_ROOT_ID]);
+ expect(graph.nodes.get(GHOST_GRAPH_ROOT_ID)?.body).toBe("Root prose.");
});
it("records children for downward traversal", () => {
const graph = assembleGraph({
- surfaces: {
- schema: "ghost.surfaces/v1",
- surfaces: [
- { id: "checkout", parent: "core" },
- { id: "email", parent: "core" },
- ],
- },
+ placedNodes: [placed("checkout", "core"), placed("email", "core")],
});
expect(graph.children.get(GHOST_GRAPH_ROOT_ID)?.sort()).toEqual([
"checkout",
diff --git a/packages/ghost/test/ghost-core/graph-slice.test.ts b/packages/ghost/test/ghost-core/graph-slice.test.ts
index 6caff93c..3b1650e9 100644
--- a/packages/ghost/test/ghost-core/graph-slice.test.ts
+++ b/packages/ghost/test/ghost-core/graph-slice.test.ts
@@ -1,141 +1,215 @@
import { describe, expect, it } from "vitest";
import {
assembleGraph,
- type GhostNodeDocument,
+ type PlacedNode,
resolveGraphSlice,
} from "../../src/ghost-core/index.js";
-function nodeDoc(
- frontmatter: GhostNodeDocument["frontmatter"],
- body = "Prose.",
-): GhostNodeDocument {
- return { frontmatter, body };
+/**
+ * Model a node the way the loader does. `folder` is the file's directory — the
+ * unit of corridor containment:
+ * - a root file (`voice.md`) → parent `core`, folder ``.
+ * - a directory index (`a/index.md`)→ parent of `a`, folder `a`.
+ * - a leaf (`a/b.md`) → parent `a`, folder `a`.
+ */
+function root(
+ id: string,
+ fm: PlacedNode["doc"]["frontmatter"] = {},
+): PlacedNode {
+ return { id, parent: "core", folder: "", doc: { frontmatter: fm, body: id } };
+}
+function dir(
+ id: string,
+ fm: PlacedNode["doc"]["frontmatter"] = {},
+): PlacedNode {
+ const slash = id.lastIndexOf("/");
+ const parent = slash === -1 ? "core" : id.slice(0, slash);
+ return { id, parent, folder: id, doc: { frontmatter: fm, body: id } };
+}
+function leaf(
+ id: string,
+ fm: PlacedNode["doc"]["frontmatter"] = {},
+): PlacedNode {
+ const slash = id.lastIndexOf("/");
+ const folder = slash === -1 ? "" : id.slice(0, slash);
+ const parent = folder === "" ? "core" : folder;
+ return { id, parent, folder, doc: { frontmatter: fm, body: id } };
}
-
-const surfaces = {
- schema: "ghost.surfaces/v1" as const,
- surfaces: [
- { id: "checkout", parent: "core" },
- { id: "payment", parent: "checkout" },
- ],
-};
function provenanceOf(slice: ReturnType, id: string) {
return slice.nodes.find((n) => n.id === id)?.provenance;
}
+const bodyIds = (slice: ReturnType) =>
+ slice.nodes.map((n) => n.id).sort();
+const spokeIds = (slice: ReturnType) =>
+ slice.spokes.map((s) => s.id).sort();
-describe("resolveGraphSlice", () => {
- it("tags own, ancestor, and edge provenance", () => {
- const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc({ id: "brand-voice", under: "core" }, "Calm everywhere."),
- nodeDoc(
- {
- id: "checkout-trust",
- under: "checkout",
- relates: [{ to: "density", as: "contrasts" }],
- },
- "Reduce felt risk.",
- ),
- nodeDoc({ id: "density", under: "dashboard" }, "Pack it in."),
+describe("resolveGraphSlice — corridor + hub-and-spoke", () => {
+ // A cash-ios-shaped fixture: globals at root, a design-system hub (arcade),
+ // and two walled feature subtrees. The `features` module declares the Arcade
+ // dependency once, for all its children.
+ function cashGraph() {
+ return assembleGraph({
+ placedNodes: [
+ root("core"), // root index.md
+ root("trust"),
+ root("accessibility"),
+ dir("arcade", { description: "Design system." }),
+ leaf("arcade/color", { description: "Color tokens." }),
+ leaf("arcade/motion", { description: "Motion." }),
+ dir("arcade/components", { description: "Components." }),
+ leaf("arcade/components/button", { description: "Button." }),
+ dir("features", { relates: [{ to: "arcade", as: "reinforces" }] }),
+ dir("features/bitcoin"),
+ leaf("features/bitcoin/invariants", {
+ description: "Non-negotiables.",
+ }),
+ dir("features/bitcoin/buy"),
+ leaf("features/bitcoin/buy/confirm"),
+ leaf("features/bitcoin/buy/review"),
+ dir("features/bitcoin/education"),
+ dir("features/lending"),
+ leaf("features/lending/invariants"),
+ dir("features/banking"),
+ leaf("features/banking/paychecks"),
],
});
- const slice = resolveGraphSlice(graph, "checkout");
+ }
- expect(provenanceOf(slice, "checkout-trust")).toEqual({ kind: "own" });
- expect(provenanceOf(slice, "brand-voice")).toEqual({
- kind: "ancestor",
- from: "core",
- });
- expect(provenanceOf(slice, "density")).toEqual({
+ it("1. a sibling folder is a wall — its nodes never leak in", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ const ids = bodyIds(slice);
+ // Walled-off siblings: other features, the design system, sibling sub-areas.
+ expect(ids).not.toContain("features/banking");
+ expect(ids).not.toContain("features/banking/paychecks");
+ expect(ids).not.toContain("features/lending");
+ expect(ids).not.toContain("features/bitcoin/education");
+ // And not even as spokes — a wall is total.
+ expect(spokeIds(slice)).not.toContain("features/banking");
+ expect(spokeIds(slice)).not.toContain("features/lending");
+ });
+
+ it("2. an ancestor's relates propagates down to a deep leaf", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ // `features` declares the Arcade dependency; a screen 3 levels deeper
+ // inherits it via the corridor → edge path.
+ expect(provenanceOf(slice, "arcade")).toEqual({
kind: "edge",
- via: "contrasts",
- from: "checkout-trust",
+ via: "reinforces",
+ from: "features",
});
});
- it("cascades through multiple ancestor levels", () => {
- const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc({ id: "brand-voice", under: "core" }, "Calm."),
- nodeDoc({ id: "checkout-clarity", under: "checkout" }, "Plain."),
- nodeDoc({ id: "pay-now", under: "payment" }, "One tap."),
- ],
+ it("3. an edge to a hub unfolds the hub's subtree as spokes", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ const hubSpokes = slice.spokes
+ .filter((s) => s.kind === "edge-hub")
+ .map((s) => s.id)
+ .sort();
+ expect(hubSpokes).toEqual([
+ "arcade/color",
+ "arcade/components",
+ "arcade/components/button",
+ "arcade/motion",
+ ]);
+ // The hub body itself is a full-body edge node, not a spoke.
+ expect(spokeIds(slice)).not.toContain("arcade");
+ });
+
+ it("4. a loose file in a corridor folder cascades full-body (invariants)", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ // `features/bitcoin/invariants` is a plain file in folder features/bitcoin,
+ // which is on the corridor — so it is inherited as a full body.
+ expect(provenanceOf(slice, "features/bitcoin/invariants")).toEqual({
+ kind: "ancestor",
+ from: "features/bitcoin",
});
- const slice = resolveGraphSlice(graph, "payment");
- expect(provenanceOf(slice, "pay-now")).toEqual({ kind: "own" });
- expect(provenanceOf(slice, "checkout-clarity")).toEqual({
+ // Globals (root files) reach everywhere.
+ expect(provenanceOf(slice, "trust")).toEqual({
kind: "ancestor",
- from: "checkout",
+ from: "core",
});
- expect(provenanceOf(slice, "brand-voice")).toEqual({
+ expect(provenanceOf(slice, "accessibility")).toEqual({
kind: "ancestor",
from: "core",
});
- expect(slice.ancestors).toEqual(["checkout"]);
});
- it("filters by incarnation: essence always in, matching in, mismatched out", () => {
- const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc({ id: "brand-voice", under: "core" }, "Calm."), // essence
- nodeDoc(
- { id: "checkout-web", under: "checkout", incarnation: "web" },
- "Inline.",
- ),
- nodeDoc(
- { id: "checkout-mail", under: "checkout", incarnation: "email" },
- "Subject.",
- ),
- ],
+ it("5. descendants appear as spokes (pointers), not spine", () => {
+ const slice = resolveGraphSlice(cashGraph(), "features/bitcoin");
+ const descendants = slice.spokes
+ .filter((s) => s.kind === "descendant")
+ .map((s) => s.id)
+ .sort();
+ expect(descendants).toEqual([
+ "features/bitcoin/buy",
+ "features/bitcoin/buy/confirm",
+ "features/bitcoin/buy/review",
+ "features/bitcoin/education",
+ ]);
+ // A descendant is a pointer, never a full body.
+ expect(bodyIds(slice)).not.toContain("features/bitcoin/buy/confirm");
+ // A descendant spoke carries its description for agent selection.
+ const buy = slice.spokes.find((s) => s.id === "features/bitcoin/buy");
+ expect(buy?.kind).toBe("descendant");
+ });
+
+ it("6. same-folder files co-occur as own (a folder is one surface)", () => {
+ const slice = resolveGraphSlice(
+ cashGraph(),
+ "features/bitcoin/buy/confirm",
+ );
+ // confirm.md and review.md share folder features/bitcoin/buy — both `own`.
+ expect(provenanceOf(slice, "features/bitcoin/buy/confirm")).toEqual({
+ kind: "own",
+ });
+ expect(provenanceOf(slice, "features/bitcoin/buy/review")).toEqual({
+ kind: "own",
});
- const slice = resolveGraphSlice(graph, "checkout", { incarnation: "web" });
- const ids = slice.nodes.map((n) => n.id).sort();
- expect(ids).toContain("brand-voice"); // essence
- expect(ids).toContain("checkout-web"); // matches
- expect(ids).not.toContain("checkout-mail"); // mismatched
- expect(slice.incarnation).toBe("web");
});
- it("includes every node when no incarnation filter is given", () => {
+ it("edges follow one hop only — no recursion", () => {
const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc(
- { id: "checkout-web", under: "checkout", incarnation: "web" },
- "x",
- ),
- nodeDoc(
- { id: "checkout-mail", under: "checkout", incarnation: "email" },
- "y",
- ),
+ placedNodes: [
+ leaf("checkout/a", { relates: [{ to: "dashboard/b" }] }),
+ leaf("dashboard/b", { relates: [{ to: "dashboard/c" }] }),
+ leaf("dashboard/c"),
],
});
- const slice = resolveGraphSlice(graph, "checkout");
- const ids = slice.nodes.map((n) => n.id).sort();
- expect(ids).toEqual(["checkout-mail", "checkout-web"]);
- expect(slice.incarnation).toBeUndefined();
+ const slice = resolveGraphSlice(graph, "checkout/a");
+ const ids = bodyIds(slice);
+ expect(ids).toContain("checkout/a"); // own
+ expect(ids).toContain("dashboard/b"); // one hop
+ expect(ids).not.toContain("dashboard/c"); // two hops — excluded
});
- it("follows relates edges one hop only (no recursion)", () => {
+ it("filters full-body nodes by incarnation; essence always passes", () => {
const graph = assembleGraph({
- surfaces,
- nodeFiles: [
- nodeDoc(
- { id: "a", under: "checkout", relates: [{ to: "b" }] },
- "node a",
- ),
- nodeDoc({ id: "b", under: "dashboard", relates: [{ to: "c" }] }, "b"),
- nodeDoc({ id: "c", under: "dashboard" }, "c"),
+ placedNodes: [
+ root("voice"), // essence
+ leaf("checkout/web", { incarnation: "web" }),
+ leaf("checkout/mail", { incarnation: "email" }),
],
});
- const slice = resolveGraphSlice(graph, "checkout");
- const ids = slice.nodes.map((n) => n.id);
- expect(ids).toContain("a"); // own
- expect(ids).toContain("b"); // one hop from a
- expect(ids).not.toContain("c"); // two hops — excluded
+ const slice = resolveGraphSlice(graph, "checkout/web", {
+ incarnation: "web",
+ });
+ const ids = bodyIds(slice);
+ expect(ids).toContain("voice"); // essence
+ expect(ids).toContain("checkout/web"); // matches
+ expect(ids).not.toContain("checkout/mail"); // mismatched
+ expect(slice.incarnation).toBe("web");
});
});
diff --git a/packages/ghost/test/ghost-core/node-schema.test.ts b/packages/ghost/test/ghost-core/node-schema.test.ts
index bbf5f7b3..72b295cb 100644
--- a/packages/ghost/test/ghost-core/node-schema.test.ts
+++ b/packages/ghost/test/ghost-core/node-schema.test.ts
@@ -3,6 +3,8 @@ import {
GHOST_NODE_RELATION_KINDS,
type GhostNodeDocument,
lintGhostNode,
+ NodeIdSchema,
+ NodeRefSchema,
parseNode,
serializeNode,
} from "../../src/ghost-core/node/index.js";
@@ -12,25 +14,12 @@ function node(frontmatter: string, body = "Prose body."): string {
}
describe("ghost.node/v1 schema", () => {
- it("parses and validates a minimal node (id only)", () => {
- const { node: doc, report } = parseNode(node("id: checkout"));
+ it("parses and validates a minimal node (empty frontmatter)", () => {
+ const { node: doc, report } = parseNode(node(""));
expect(report.errors).toBe(0);
- expect(doc?.frontmatter.id).toBe("checkout");
expect(doc?.body).toBe("Prose body.");
});
- it("accepts dashed and dotted ids (permissive charset)", () => {
- for (const id of ["core", "checkout-trust-signals", "email.marketing"]) {
- expect(lintGhostNode(node(`id: ${id}`)).errors).toBe(0);
- }
- });
-
- it("rejects only genuinely malformed ids", () => {
- for (const id of ["Checkout", "-leading", "_leading"]) {
- expect(lintGhostNode(node(`id: ${id}`)).errors).toBeGreaterThan(0);
- }
- });
-
it("errors when frontmatter is missing", () => {
const report = lintGhostNode("# just a heading\n\nno frontmatter");
expect(report.errors).toBe(1);
@@ -40,62 +29,40 @@ describe("ghost.node/v1 schema", () => {
it("accepts the closed relates qualifier set and rejects unknowns", () => {
for (const as of GHOST_NODE_RELATION_KINDS) {
const report = lintGhostNode(
- node(`id: a\nrelates:\n - to: core\n as: ${as}`),
+ node(`relates:\n - to: core\n as: ${as}`),
);
expect(report.errors).toBe(0);
}
- const bad = lintGhostNode(
- node("id: a\nrelates:\n - to: core\n as: governs"),
- );
+ const bad = lintGhostNode(node("relates:\n - to: core\n as: governs"));
expect(bad.errors).toBeGreaterThan(0);
});
it("allows untyped relations (qualifier omitted)", () => {
- const report = lintGhostNode(node("id: a\nrelates:\n - to: core"));
- expect(report.errors).toBe(0);
- });
-
- it("accepts local and cross-package refs in under/relates", () => {
- const report = lintGhostNode(
- node(
- "id: checkout-trust\nunder: checkout\nrelates:\n - to: 'brand:core-trust'\n as: reinforces",
- ),
- );
+ const report = lintGhostNode(node("relates:\n - to: core"));
expect(report.errors).toBe(0);
});
- it("rejects malformed refs", () => {
- expect(
- lintGhostNode(node("id: a\nunder: 'Bad Ref'")).errors,
- ).toBeGreaterThan(0);
- });
-
it("accepts an arbitrary incarnation string", () => {
- expect(lintGhostNode(node("id: a\nincarnation: billboard")).errors).toBe(0);
- expect(lintGhostNode(node("id: a\nincarnation: voice-kiosk")).errors).toBe(
- 0,
- );
+ expect(lintGhostNode(node("incarnation: billboard")).errors).toBe(0);
+ expect(lintGhostNode(node("incarnation: voice-kiosk")).errors).toBe(0);
});
it("passes through free-form descriptive keys (e.g. audience)", () => {
// Authors may add descriptive keys; Ghost does not gate on them.
- expect(lintGhostNode(node("id: a\naudience: enterprise")).errors).toBe(0);
+ expect(lintGhostNode(node("audience: enterprise")).errors).toBe(0);
});
it("accepts a description (the retrieval payload)", () => {
- expect(
- lintGhostNode(node("id: email\ndescription: Lifecycle email.")).errors,
- ).toBe(0);
+ expect(lintGhostNode(node("description: Lifecycle email.")).errors).toBe(0);
});
- it("round-trips through serialize/parse", () => {
+ it("round-trips through serialize/parse (frontmatter is properties only)", () => {
const original: GhostNodeDocument = {
frontmatter: {
- id: "checkout-trust-signals",
- under: "checkout",
+ description: "Near payment, reduce felt risk.",
relates: [
- { to: "core-trust", as: "reinforces" },
- { to: "checkout-density" },
+ { to: "core/trust", as: "reinforces" },
+ { to: "checkout/density" },
],
incarnation: "web",
},
@@ -107,9 +74,66 @@ describe("ghost.node/v1 schema", () => {
expect(reparsed.node?.body).toBe(original.body);
});
+ it("round-trips an empty-frontmatter node", () => {
+ const original: GhostNodeDocument = {
+ frontmatter: {},
+ body: "Just prose.",
+ };
+ const reparsed = parseNode(serializeNode(original));
+ expect(reparsed.report.errors).toBe(0);
+ expect(reparsed.node?.frontmatter).toEqual({});
+ expect(reparsed.node?.body).toBe("Just prose.");
+ });
+
it("preserves the body verbatim, stripping only frontmatter", () => {
const body = "# Heading\n\n- a list item\n\nA paragraph with `code`.";
- const { node: doc } = parseNode(node("id: a", body));
+ const { node: doc } = parseNode(node("", body));
expect(doc?.body).toBe(body);
});
});
+
+describe("node id / ref grammar (path-based identity)", () => {
+ it("accepts flat and nested path ids", () => {
+ for (const id of [
+ "core",
+ "checkout",
+ "checkout-trust-signals",
+ "marketing/email",
+ "a/b/c",
+ "email.marketing",
+ ]) {
+ expect(NodeIdSchema.safeParse(id).success).toBe(true);
+ }
+ });
+
+ it("rejects malformed ids", () => {
+ for (const id of [
+ "Checkout",
+ "-leading",
+ "_leading",
+ "/leading-slash",
+ "trailing-slash/",
+ "double//slash",
+ "Bad Ref",
+ ]) {
+ expect(NodeIdSchema.safeParse(id).success).toBe(false);
+ }
+ });
+
+ it("accepts local path refs and cross-package refs", () => {
+ for (const ref of [
+ "core",
+ "marketing/email",
+ "brand:core/trust",
+ "brand:core",
+ ]) {
+ expect(NodeRefSchema.safeParse(ref).success).toBe(true);
+ }
+ });
+
+ it("rejects malformed refs", () => {
+ for (const ref of ["Bad Ref", "/x", "x/", "a//b"]) {
+ expect(NodeRefSchema.safeParse(ref).success).toBe(false);
+ }
+ });
+});
diff --git a/packages/ghost/test/ghost-core/surfaces-lint.test.ts b/packages/ghost/test/ghost-core/surfaces-lint.test.ts
deleted file mode 100644
index 7a2725b9..00000000
--- a/packages/ghost/test/ghost-core/surfaces-lint.test.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
- GHOST_SURFACES_SCHEMA,
- lintGhostSurfaces,
-} from "../../src/ghost-core/surfaces/index.js";
-
-function doc(surfaces: unknown[]) {
- return { schema: GHOST_SURFACES_SCHEMA, surfaces };
-}
-
-function rules(report: { issues: { rule: string }[] }): string[] {
- return report.issues.map((issue) => issue.rule);
-}
-
-describe("lintGhostSurfaces", () => {
- it("passes a valid tree (id + parent + description)", () => {
- const report = lintGhostSurfaces(
- doc([
- { id: "core", description: "True everywhere." },
- { id: "email", parent: "core" },
- { id: "email-marketing", parent: "email" },
- { id: "checkout", parent: "core" },
- ]),
- );
-
- expect(report.issues).toEqual([]);
- expect(report.errors).toBe(0);
- });
-
- it("allows parent: core without an explicit core surface (implicit root)", () => {
- const report = lintGhostSurfaces(doc([{ id: "email", parent: "core" }]));
-
- expect(report.errors).toBe(0);
- });
-
- it("errors on a parent that matches no surface", () => {
- const report = lintGhostSurfaces(
- doc([{ id: "email-marketing", parent: "emial" }]),
- );
-
- expect(rules(report)).toContain("surface-parent-unknown");
- expect(report.errors).toBeGreaterThan(0);
- });
-
- it("warns with a near-miss suggestion for an unknown parent close to a real id", () => {
- const report = lintGhostSurfaces(
- doc([
- { id: "email", parent: "core" },
- { id: "marketing", parent: "emial" },
- ]),
- );
-
- const nearMiss = report.issues.find(
- (issue) => issue.rule === "surface-id-near-miss",
- );
- expect(nearMiss?.severity).toBe("warning");
- expect(nearMiss?.message).toContain("email");
- });
-
- it("errors when core declares a parent", () => {
- const report = lintGhostSurfaces(doc([{ id: "core", parent: "root" }]));
-
- expect(rules(report)).toContain("surface-core-reserved");
- });
-
- it("errors on a parent cycle", () => {
- const report = lintGhostSurfaces(
- doc([
- { id: "a", parent: "b" },
- { id: "b", parent: "a" },
- ]),
- );
-
- expect(rules(report)).toContain("surface-parent-cycle");
- });
-
- it("errors on a self-parent", () => {
- const report = lintGhostSurfaces(doc([{ id: "a", parent: "a" }]));
-
- expect(rules(report)).toContain("surface-parent-cycle");
- });
-
- it("errors on duplicate ids", () => {
- const report = lintGhostSurfaces(
- doc([
- { id: "email", parent: "core" },
- { id: "email", parent: "core" },
- ]),
- );
-
- expect(rules(report)).toContain("duplicate-id");
- });
-
- it("reports schema failures as issues rather than throwing", () => {
- const report = lintGhostSurfaces(
- doc([{ id: "email.marketing", parent: "email" }]),
- );
-
- expect(report.errors).toBeGreaterThan(0);
- expect(report.issues[0]?.rule).toContain("schema/");
- });
-});
diff --git a/packages/ghost/test/ghost-core/surfaces-schema.test.ts b/packages/ghost/test/ghost-core/surfaces-schema.test.ts
deleted file mode 100644
index 13d39dd6..00000000
--- a/packages/ghost/test/ghost-core/surfaces-schema.test.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
- GHOST_SURFACES_SCHEMA,
- GhostSurfacesSchema,
-} from "../../src/ghost-core/surfaces/index.js";
-
-describe("ghost.surfaces/v1", () => {
- it("accepts a minimal document and defaults surfaces to []", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- });
-
- expect(result.success).toBe(true);
- if (!result.success) throw new Error("minimal surfaces.yml should parse");
- expect(result.data).toEqual({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [],
- });
- });
-
- it("accepts a realistic tree (id + parent + optional description)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [
- { id: "core", description: "True everywhere." },
- { id: "email", description: "Lifecycle email.", parent: "core" },
- { id: "email-marketing", parent: "email" },
- { id: "checkout", parent: "core" },
- ],
- });
-
- expect(result.success).toBe(true);
- });
-
- it("rejects a dotted id (the tree lives only in parent)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [{ id: "email.marketing", parent: "email" }],
- });
-
- expect(result.success).toBe(false);
- if (result.success) throw new Error("dotted id must be rejected");
- expect(result.error.issues[0]?.message).toContain("flat slug");
- });
-
- it("rejects a parent given as an array (single parent only)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [{ id: "email-marketing", parent: ["email", "marketing"] }],
- });
-
- expect(result.success).toBe(false);
- });
-
- it("rejects an unknown surface key (strict; edges are gone)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [
- { id: "checkout", edges: [{ kind: "composes", to: "payments" }] },
- ],
- });
-
- expect(result.success).toBe(false);
- });
-
- it("rejects an unknown top-level key (strict)", () => {
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [],
- routes: [],
- });
-
- expect(result.success).toBe(false);
- });
-
- it("accepts a parent that does not exist as a surface", () => {
- // INTENTIONAL: dangling-reference detection is a lint concern, not a schema
- // concern. Zod validates a position in isolation and cannot see the tree.
- const result = GhostSurfacesSchema.safeParse({
- schema: GHOST_SURFACES_SCHEMA,
- surfaces: [{ id: "checkout", parent: "nonexistent" }],
- });
-
- expect(result.success).toBe(true);
- });
-});
diff --git a/packages/ghost/test/migrate-legacy.test.ts b/packages/ghost/test/migrate-legacy.test.ts
index 7fb597c2..1f9282c3 100644
--- a/packages/ghost/test/migrate-legacy.test.ts
+++ b/packages/ghost/test/migrate-legacy.test.ts
@@ -1,9 +1,5 @@
import { describe, expect, it } from "vitest";
-import {
- GhostSurfacesSchema,
- lintGhostNode,
- lintGhostSurfaces,
-} from "../src/ghost-core/index.js";
+import { lintGhostNode } from "../src/ghost-core/index.js";
import {
type LegacyPackageInput,
looksLegacy,
@@ -58,12 +54,9 @@ function legacy(): LegacyPackageInput {
}
describe("migrateLegacyPackage", () => {
- it("derives surfaces.yml from topology.scopes", () => {
- const { surfaces } = migrateLegacyPackage(legacy());
- const parsed = GhostSurfacesSchema.safeParse(surfaces);
- expect(parsed.success).toBe(true);
- const ids = (surfaces.surfaces as Array<{ id: string }>).map((s) => s.id);
- expect(ids).toEqual(["lending", "checkout"]);
+ it("derives surface directories from topology.scopes", () => {
+ const { surfaceIds } = migrateLegacyPackage(legacy());
+ expect(surfaceIds).toEqual(["lending", "checkout"]);
});
it("places single-scope nodes via surface: and strips legacy fields", () => {
@@ -126,18 +119,23 @@ describe("migrateLegacyPackage", () => {
expect(principles[0]).toHaveProperty("applies_to");
});
- it("produces a node package: valid surfaces + parseable nodes", () => {
+ it("produces a directory tree of parseable nodes", () => {
const result = migrateLegacyPackage(legacy());
- expect(lintGhostSurfaces(result.surfaces).errors).toBe(0);
-
- // The migration emits one prose node per facet entry.
+ // The migration emits one prose node per facet entry, plus an index.md per
+ // derived surface directory. Placed nodes land under their surface dir.
const files = migratedNodeFiles(result);
expect(files.length).toBeGreaterThan(0);
for (const file of files) {
- expect(file.relativePath).toMatch(/^nodes\/.+\.md$/);
+ expect(file.relativePath).toMatch(/\.md$/);
expect(lintGhostNode(file.content).errors).toBe(0);
}
+ // Each derived surface gets its index.md directory marker.
+ expect(files.some((f) => f.relativePath === "lending/index.md")).toBe(true);
+ // A single-scope node lands inside its surface directory.
+ expect(
+ files.some((f) => f.relativePath === "lending/single-scope.md"),
+ ).toBe(true);
});
});
diff --git a/packages/ghost/test/scan-status.test.ts b/packages/ghost/test/scan-status.test.ts
index c7459b69..d1a9a917 100644
--- a/packages/ghost/test/scan-status.test.ts
+++ b/packages/ghost/test/scan-status.test.ts
@@ -1,6 +1,6 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
-import { join } from "node:path";
+import { dirname, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { scanStatus } from "../src/scan/scan-status.js";
@@ -39,41 +39,33 @@ describe("scanStatus contribution", () => {
expect(status.contribution.node_count).toBe(0);
});
- it("reports node contribution and surface coverage", async () => {
- await writePackage(
- dir,
- `schema: ghost.surfaces/v1
-surfaces:
- - id: checkout
- parent: core
- - id: email
- parent: core
-`,
- {
- "core-voice.md": "---\nid: core-voice\nunder: core\n---\n\nCalm.\n",
- "checkout-trust.md":
- "---\nid: checkout-trust\nunder: checkout\nincarnation: web\n---\n\nReassure.\n",
- },
- );
+ it("reports node contribution and surface coverage over the directory tree", async () => {
+ await writePackage(dir, {
+ // The core root prose (essence).
+ "index.md": "---\n---\n\nCalm.\n",
+ // The checkout surface directory, with one placed node (web incarnation).
+ "checkout/index.md": "---\n---\n\nCheckout surface.\n",
+ "checkout/trust.md": "---\nincarnation: web\n---\n\nReassure.\n",
+ });
const status = await scanStatus(join(dir, ".ghost"));
expect(status.contribution.state).toBe("contributing");
- expect(status.contribution.node_count).toBe(2);
- expect(status.contribution.essence_count).toBe(1);
+ // 3 authored nodes: root index + checkout/index + checkout/trust.
+ expect(status.contribution.node_count).toBe(3);
+ // Two essence (the two index nodes) + one web-tagged (checkout/trust).
+ expect(status.contribution.essence_count).toBe(2);
expect(status.contribution.incarnation_count).toBe(1);
+ // `checkout` is an interior directory holding one node.
const checkout = status.contribution.surfaces.find(
(s) => s.id === "checkout",
);
expect(checkout?.node_count).toBe(1);
- // email surface declared but has no nodes → sparse.
- expect(status.contribution.sparse_surfaces).toContain("email");
});
});
async function writePackage(
dir: string,
- surfacesYml?: string,
nodes?: Record,
): Promise {
await mkdir(join(dir, ".ghost"), { recursive: true });
@@ -81,13 +73,11 @@ async function writePackage(
join(dir, ".ghost", "manifest.yml"),
"schema: ghost.fingerprint-package/v1\nid: local\n",
);
- if (surfacesYml) {
- await writeFile(join(dir, ".ghost", "surfaces.yml"), surfacesYml);
- }
if (nodes) {
- await mkdir(join(dir, ".ghost", "nodes"), { recursive: true });
- for (const [name, content] of Object.entries(nodes)) {
- await writeFile(join(dir, ".ghost", "nodes", name), content);
+ for (const [relPath, content] of Object.entries(nodes)) {
+ const full = join(dir, ".ghost", relPath);
+ await mkdir(dirname(full), { recursive: true });
+ await writeFile(full, content);
}
}
}
diff --git a/schemas/fingerprint.schema.json b/schemas/fingerprint.schema.json
deleted file mode 100644
index abc62175..00000000
--- a/schemas/fingerprint.schema.json
+++ /dev/null
@@ -1,383 +0,0 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "slug": {
- "type": "string"
- },
- "generator": {
- "type": "string"
- },
- "generated": {
- "type": "string"
- },
- "confidence": {
- "type": "number"
- },
- "extends": {
- "type": "string"
- },
- "metadata": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {}
- },
- "id": {
- "type": "string"
- },
- "source": {
- "type": "string",
- "enum": ["registry", "extraction", "llm", "unknown"]
- },
- "timestamp": {
- "type": "string"
- },
- "sources": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "references": {
- "type": "object",
- "properties": {
- "specs": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "components": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "examples": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "additionalProperties": false
- },
- "observation": {
- "type": "object",
- "properties": {
- "personality": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "resembles": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "additionalProperties": false
- },
- "decisions": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "dimension": {
- "type": "string"
- },
- "dimension_kind": {
- "type": "string"
- },
- "embedding": {
- "type": "array",
- "items": {
- "type": "number"
- }
- }
- },
- "required": ["dimension"],
- "additionalProperties": false
- }
- },
- "checks": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "canonical": {
- "type": "string"
- },
- "kind": {
- "type": "string",
- "enum": [
- "color",
- "radius",
- "spacing",
- "type-size",
- "type-family",
- "type-weight",
- "shadow",
- "motion"
- ]
- },
- "summary": {
- "type": "string"
- },
- "pattern": {
- "type": "string"
- },
- "enforce_at": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "severity": {
- "type": "string",
- "enum": ["critical", "serious", "nit"]
- },
- "match": {
- "type": "string",
- "enum": ["exact", "band", "percent", "structural"]
- },
- "tolerance": {
- "type": "number"
- },
- "presence_floor": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991
- },
- "observed_count": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991
- },
- "support": {
- "type": "number",
- "minimum": 0,
- "maximum": 1
- },
- "rationale": {
- "type": "string"
- }
- },
- "required": ["id", "pattern"],
- "additionalProperties": false
- }
- },
- "palette": {
- "type": "object",
- "properties": {
- "dominant": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "role": {
- "type": "string"
- },
- "value": {
- "type": "string"
- },
- "oklch": {
- "type": "array",
- "prefixItems": [
- {
- "type": "number"
- },
- {
- "type": "number"
- },
- {
- "type": "number"
- }
- ]
- }
- },
- "required": ["role", "value"],
- "additionalProperties": false
- }
- },
- "neutrals": {
- "type": "object",
- "properties": {
- "steps": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "count": {
- "type": "number"
- }
- },
- "required": ["steps", "count"],
- "additionalProperties": false
- },
- "semantic": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "role": {
- "type": "string"
- },
- "value": {
- "type": "string"
- },
- "oklch": {
- "type": "array",
- "prefixItems": [
- {
- "type": "number"
- },
- {
- "type": "number"
- },
- {
- "type": "number"
- }
- ]
- }
- },
- "required": ["role", "value"],
- "additionalProperties": false
- }
- },
- "saturationProfile": {
- "type": "string",
- "enum": ["muted", "vibrant", "mixed"]
- },
- "contrast": {
- "type": "string",
- "enum": ["high", "moderate", "low"]
- }
- },
- "required": [
- "dominant",
- "neutrals",
- "semantic",
- "saturationProfile",
- "contrast"
- ],
- "additionalProperties": false
- },
- "spacing": {
- "type": "object",
- "properties": {
- "scale": {
- "type": "array",
- "items": {
- "type": "number"
- }
- },
- "regularity": {
- "type": "number"
- },
- "baseUnit": {
- "anyOf": [
- {
- "type": "number"
- },
- {
- "type": "null"
- }
- ]
- }
- },
- "required": ["scale", "regularity", "baseUnit"],
- "additionalProperties": false
- },
- "typography": {
- "type": "object",
- "properties": {
- "families": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "sizeRamp": {
- "type": "array",
- "items": {
- "type": "number"
- }
- },
- "weightDistribution": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {
- "type": "number"
- }
- },
- "lineHeightPattern": {
- "type": "string",
- "enum": ["tight", "normal", "loose"]
- }
- },
- "required": [
- "families",
- "sizeRamp",
- "weightDistribution",
- "lineHeightPattern"
- ],
- "additionalProperties": false
- },
- "surfaces": {
- "type": "object",
- "properties": {
- "borderRadii": {
- "type": "array",
- "items": {
- "type": "number"
- }
- },
- "shadowComplexity": {
- "type": "string",
- "enum": ["deliberate-none", "subtle", "layered"]
- },
- "borderUsage": {
- "type": "string",
- "enum": ["minimal", "moderate", "heavy"]
- },
- "borderTokenCount": {
- "type": "number"
- }
- },
- "required": ["borderRadii", "shadowComplexity", "borderUsage"],
- "additionalProperties": false
- },
- "embedding": {
- "type": "array",
- "items": {
- "type": "number"
- }
- }
- },
- "required": [
- "id",
- "source",
- "timestamp",
- "palette",
- "spacing",
- "typography",
- "surfaces"
- ],
- "additionalProperties": false,
- "title": "Ghost Fingerprint Frontmatter",
- "description": "Schema for YAML frontmatter in Ghost fingerprint.md files."
-}
diff --git a/scripts/emit-fingerprint-schema.mjs b/scripts/emit-fingerprint-schema.mjs
deleted file mode 100644
index 9f9e01f7..00000000
--- a/scripts/emit-fingerprint-schema.mjs
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env node
-/**
- * Emit schemas/fingerprint.schema.json from the zod source of truth.
- * Run after changes to packages/ghost/src/scan/schema.ts:
- *
- * pnpm --filter @anarchitecture/ghost build && node scripts/emit-fingerprint-schema.mjs
- */
-import { existsSync, mkdirSync, writeFileSync } from "node:fs";
-import { dirname, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-
-const here = dirname(fileURLToPath(import.meta.url));
-const root = resolve(here, "..");
-const { toJsonSchema } = await import(
- resolve(root, "packages/ghost/dist/scan/schema.js")
-);
-
-const schema = toJsonSchema();
-schema.title = "Ghost Fingerprint Frontmatter";
-schema.description =
- "Schema for YAML frontmatter in Ghost fingerprint.md files.";
-
-const outDir = resolve(root, "schemas");
-if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
-const outPath = resolve(outDir, "fingerprint.schema.json");
-writeFileSync(outPath, `${JSON.stringify(schema, null, 2)}\n`);
-console.log(`Wrote ${outPath}`);
diff --git a/scripts/strip-signature.mjs b/scripts/strip-signature.mjs
deleted file mode 100644
index 6d2f1bc5..00000000
--- a/scripts/strip-signature.mjs
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env node
-// One-shot script: remove `# Signature` body blocks from a list of
-// fingerprint.md files. The block runs from the `# Signature` line up to
-// (but not including) the next H1 heading or EOF. Idempotent.
-import { readFileSync, writeFileSync } from "node:fs";
-
-const files = process.argv.slice(2);
-if (files.length === 0) {
- console.error("usage: strip-signature.mjs ...");
- process.exit(2);
-}
-
-let touched = 0;
-for (const f of files) {
- const raw = readFileSync(f, "utf8");
- const lines = raw.split("\n");
- const out = [];
- let inSig = false;
- for (const line of lines) {
- if (/^# Signature\s*$/.test(line)) {
- inSig = true;
- continue;
- }
- if (inSig && /^# /.test(line)) {
- inSig = false;
- }
- if (!inSig) out.push(line);
- }
- const next = out.join("\n").replace(/\n{3,}/g, "\n\n");
- if (next !== raw) {
- writeFileSync(f, next, "utf8");
- touched++;
- console.log(`stripped: ${f}`);
- } else {
- console.log(`unchanged: ${f}`);
- }
-}
-console.log(`${touched}/${files.length} files modified`);