From e7aaf4d5c375193cf87086346de1a6a58f74d1ca Mon Sep 17 00:00:00 2001 From: interacsean Date: Fri, 24 Jul 2026 10:35:24 +1000 Subject: [PATCH 01/15] chore(docs): relocate doc sources via git mv for the docs pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure moves (no content change) so history/lineage is preserved: - component docs: docs/components/{button,dialog,data-table}.md → the colocated *.docs-outline.md source next to each component - patterns: catalogue/src/pattern/{form/modal,list/dense-scan} PATTERN.md + example .tsx → docs-src/patterns/ + docs/patterns/ The follow-up commit adapts these into the docs-kit outline/example format and generates the docs/ output. Catalogue's skill generator is intentionally left to regress — it will be retired and regenerated from these docs later. Co-Authored-By: Claude Opus 4.8 --- .../PATTERN.md => docs-src/patterns/form-modal.docs-outline.md | 0 .../patterns/list-dense-scan.docs-outline.md | 0 .../modal/modal-form.tsx => docs/patterns/form-modal.examples.tsx | 0 .../dense-scan.tsx => docs/patterns/list-dense-scan.examples.tsx | 0 .../core/src/components/button.docs-outline.md | 0 .../core/src/components/data-table/data-table.docs-outline.md | 0 .../core/src/components/dialog.docs-outline.md | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename catalogue/src/pattern/form/modal/PATTERN.md => docs-src/patterns/form-modal.docs-outline.md (100%) rename catalogue/src/pattern/list/dense-scan/PATTERN.md => docs-src/patterns/list-dense-scan.docs-outline.md (100%) rename catalogue/src/pattern/form/modal/modal-form.tsx => docs/patterns/form-modal.examples.tsx (100%) rename catalogue/src/pattern/list/dense-scan/dense-scan.tsx => docs/patterns/list-dense-scan.examples.tsx (100%) rename docs/components/button.md => packages/core/src/components/button.docs-outline.md (100%) rename docs/components/data-table.md => packages/core/src/components/data-table/data-table.docs-outline.md (100%) rename docs/components/dialog.md => packages/core/src/components/dialog.docs-outline.md (100%) diff --git a/catalogue/src/pattern/form/modal/PATTERN.md b/docs-src/patterns/form-modal.docs-outline.md similarity index 100% rename from catalogue/src/pattern/form/modal/PATTERN.md rename to docs-src/patterns/form-modal.docs-outline.md diff --git a/catalogue/src/pattern/list/dense-scan/PATTERN.md b/docs-src/patterns/list-dense-scan.docs-outline.md similarity index 100% rename from catalogue/src/pattern/list/dense-scan/PATTERN.md rename to docs-src/patterns/list-dense-scan.docs-outline.md diff --git a/catalogue/src/pattern/form/modal/modal-form.tsx b/docs/patterns/form-modal.examples.tsx similarity index 100% rename from catalogue/src/pattern/form/modal/modal-form.tsx rename to docs/patterns/form-modal.examples.tsx diff --git a/catalogue/src/pattern/list/dense-scan/dense-scan.tsx b/docs/patterns/list-dense-scan.examples.tsx similarity index 100% rename from catalogue/src/pattern/list/dense-scan/dense-scan.tsx rename to docs/patterns/list-dense-scan.examples.tsx diff --git a/docs/components/button.md b/packages/core/src/components/button.docs-outline.md similarity index 100% rename from docs/components/button.md rename to packages/core/src/components/button.docs-outline.md diff --git a/docs/components/data-table.md b/packages/core/src/components/data-table/data-table.docs-outline.md similarity index 100% rename from docs/components/data-table.md rename to packages/core/src/components/data-table/data-table.docs-outline.md diff --git a/docs/components/dialog.md b/packages/core/src/components/dialog.docs-outline.md similarity index 100% rename from docs/components/dialog.md rename to packages/core/src/components/dialog.docs-outline.md From 2ab5dd59b8d17e1957b6851ef9a937ec977f479e Mon Sep 17 00:00:00 2001 From: interacsean Date: Fri, 24 Jul 2026 10:45:37 +1000 Subject: [PATCH 02/15] feat(docs): docs-management pipeline (docs-kit, outlines, AppShell renderer) Builds on the prep-move commit. Adds packages/docs-kit (ts-morph type-surface extractor + hasher, coverage reconciler, auto prop tables, deterministic check/sync CLI that formats before hashing), adapts the moved component docs into *.docs-outline.md + authored examples, adapts the moved catalogue patterns (form-modal, list-dense-scan) into the outline/example format, adds a styling concept, generates docs/ + docs-manifest.json, the resync-docs skill, and an AppShell file-based docs-app that renders prose + live examples. Patterns are sourced from catalogue/src/pattern (moved in the prep commit); catalogue's generator is knowingly regressed pending its retirement. Refs: tailor-inc/platform-planning#1549 Co-Authored-By: Claude Opus 4.8 --- .agents/skills/resync-docs/SKILL.md | 64 + .../documentation-management-overhaul.md | 209 +++ docs-src/concepts/styling.docs-outline.md | 56 + docs-src/patterns/form-modal.docs-outline.md | 6 +- .../patterns/list-dense-scan.docs-outline.md | 6 +- docs.config.json | 17 + docs/components/button.examples.tsx | 39 + docs/components/button.md | 170 +++ docs/components/data-table.examples.tsx | 50 + docs/components/data-table.md | 866 +++++++++++++ docs/components/dialog.examples.tsx | 41 + docs/components/dialog.md | 166 +++ docs/concepts/styling.md | 59 + docs/docs-manifest.json | 160 +++ docs/patterns/form-modal.examples.tsx | 23 +- docs/patterns/form-modal.md | 79 ++ docs/patterns/list-dense-scan.examples.tsx | 88 +- docs/patterns/list-dense-scan.md | 93 ++ examples/docs-app/index.html | 12 + examples/docs-app/package.json | 30 + examples/docs-app/src/App.tsx | 14 + examples/docs-app/src/_lib/DocPage.tsx | 48 + examples/docs-app/src/docs.tsx | 71 ++ examples/docs-app/src/index.css | 14 + examples/docs-app/src/main.tsx | 14 + .../src/pages/components/button/page.tsx | 11 + .../src/pages/components/data-table/page.tsx | 11 + .../src/pages/components/dialog/page.tsx | 11 + .../src/pages/concepts/styling/page.tsx | 11 + examples/docs-app/src/pages/page.tsx | 22 + .../src/pages/patterns/form-modal/page.tsx | 11 + .../pages/patterns/list-dense-scan/page.tsx | 11 + examples/docs-app/tsconfig.json | 27 + examples/docs-app/vite.config.ts | 57 + package.json | 2 + .../src/components/button.docs-outline.md | 26 +- .../data-table/data-table.docs-outline.md | 13 + .../src/components/dialog.docs-outline.md | 35 +- packages/docs-kit/.oxlintrc.jsonc | 9 + packages/docs-kit/package.json | 35 + packages/docs-kit/src/assemble.ts | 142 +++ packages/docs-kit/src/check.ts | 93 ++ packages/docs-kit/src/cli.ts | 44 + packages/docs-kit/src/config.ts | 20 + packages/docs-kit/src/coverage.ts | 84 ++ packages/docs-kit/src/hash.ts | 14 + packages/docs-kit/src/index.ts | 16 + packages/docs-kit/src/manifest.ts | 16 + packages/docs-kit/src/outline.ts | 67 + packages/docs-kit/src/project.ts | 155 +++ packages/docs-kit/src/sync.ts | 99 ++ packages/docs-kit/src/types.ts | 87 ++ packages/docs-kit/tsconfig.json | 18 + packages/docs-kit/tsdown.config.ts | 11 + pnpm-lock.yaml | 1134 ++++++++++++++--- 55 files changed, 4420 insertions(+), 267 deletions(-) create mode 100644 .agents/skills/resync-docs/SKILL.md create mode 100644 decisions/documentation-management-overhaul.md create mode 100644 docs-src/concepts/styling.docs-outline.md create mode 100644 docs.config.json create mode 100644 docs/components/button.examples.tsx create mode 100644 docs/components/button.md create mode 100644 docs/components/data-table.examples.tsx create mode 100644 docs/components/data-table.md create mode 100644 docs/components/dialog.examples.tsx create mode 100644 docs/components/dialog.md create mode 100644 docs/concepts/styling.md create mode 100644 docs/docs-manifest.json create mode 100644 docs/patterns/form-modal.md create mode 100644 docs/patterns/list-dense-scan.md create mode 100644 examples/docs-app/index.html create mode 100644 examples/docs-app/package.json create mode 100644 examples/docs-app/src/App.tsx create mode 100644 examples/docs-app/src/_lib/DocPage.tsx create mode 100644 examples/docs-app/src/docs.tsx create mode 100644 examples/docs-app/src/index.css create mode 100644 examples/docs-app/src/main.tsx create mode 100644 examples/docs-app/src/pages/components/button/page.tsx create mode 100644 examples/docs-app/src/pages/components/data-table/page.tsx create mode 100644 examples/docs-app/src/pages/components/dialog/page.tsx create mode 100644 examples/docs-app/src/pages/concepts/styling/page.tsx create mode 100644 examples/docs-app/src/pages/page.tsx create mode 100644 examples/docs-app/src/pages/patterns/form-modal/page.tsx create mode 100644 examples/docs-app/src/pages/patterns/list-dense-scan/page.tsx create mode 100644 examples/docs-app/tsconfig.json create mode 100644 examples/docs-app/vite.config.ts create mode 100644 packages/docs-kit/.oxlintrc.jsonc create mode 100644 packages/docs-kit/package.json create mode 100644 packages/docs-kit/src/assemble.ts create mode 100644 packages/docs-kit/src/check.ts create mode 100644 packages/docs-kit/src/cli.ts create mode 100644 packages/docs-kit/src/config.ts create mode 100644 packages/docs-kit/src/coverage.ts create mode 100644 packages/docs-kit/src/hash.ts create mode 100644 packages/docs-kit/src/index.ts create mode 100644 packages/docs-kit/src/manifest.ts create mode 100644 packages/docs-kit/src/outline.ts create mode 100644 packages/docs-kit/src/project.ts create mode 100644 packages/docs-kit/src/sync.ts create mode 100644 packages/docs-kit/src/types.ts create mode 100644 packages/docs-kit/tsconfig.json create mode 100644 packages/docs-kit/tsdown.config.ts diff --git a/.agents/skills/resync-docs/SKILL.md b/.agents/skills/resync-docs/SKILL.md new file mode 100644 index 00000000..508f9929 --- /dev/null +++ b/.agents/skills/resync-docs/SKILL.md @@ -0,0 +1,64 @@ +--- +name: resync-docs +description: Regenerate component/pattern documentation when docs-kit reports drift. Use when `docs-kit check` (or CI) fails with interface-drift, outline-changed, or missing-output findings, or when the user asks to "resync docs", "update the docs for X", or "/resync-docs". This is the ONLY sanctioned way to change generated docs — never hand-edit files under docs/. +--- + +# Resync docs + +The docs pipeline is one-way: **outlines + source → generated docs**. Humans (and this skill) edit only two things — the `*.docs-outline.md` (intent) and the `*.examples.tsx` (runnable examples). Everything under `docs/` is generated by `docs-kit` and must never be hand-edited. See [decisions/documentation-management-overhaul.md](../../../decisions/documentation-management-overhaul.md). + +## When to run + +- `docs-kit check` fails (locally or in CI) with a blocking finding. +- A component's interface changed and its docs need to catch up. +- The user asks to add or revise a documented example. + +## Inputs + +An optional list of unit slugs. If omitted, run `docs-kit check` first and act on every blocking unit it names. + +## Procedure + +For each drifting unit: + +1. **Identify what changed.** Run the gate and read the finding: + + ```bash + node packages/docs-kit/dist/cli.mjs check --root . + ``` + + - `interface (type surface) drifted` → the exported API changed. Re-read the unit's `sources` and update the outline prose + `examples.tsx` so they still match. Check whether any example now uses a removed/renamed prop. + - `outline edited` → expected after you edit the outline; just resync. + - `… was hand-edited or is stale` → someone edited a generated file directly. Do **not** keep that edit; move the intent into the outline/examples, then regenerate. + +2. **Edit intent only** — the two authorable surfaces: + - `*.docs-outline.md` (colocated with the component, or under `docs-src/` for concepts/patterns/references): prose is copied verbatim; example segments are keyed English tokens ``. + - `*.examples.tsx` (under `docs//`): one exported component per token, named in PascalCase matching the token (`basic-usage` → `BasicUsage`). Keep every example **compiling against `@tailor-platform/app-shell`** and minimal. When iterating, treat the current file as the baseline and change only what the interface change forces — do not rewrite untouched examples. + +3. **Regenerate deterministically** (no hand-editing of `.md`): + + ```bash + node packages/docs-kit/dist/cli.mjs sync --root . + ``` + + This rewrites the `.md`, extracts fences from `examples.tsx`, refreshes the API section, and updates `docs/docs-manifest.json` hashes. + +4. **Verify green:** + + ```bash + node packages/docs-kit/dist/cli.mjs check --root . # expect exit 0 + ``` + +5. **Review the diff.** Confirm prose reads well, examples are idiomatic, and the manifest changes are limited to the units you touched. + +## Rules + +- Never edit files under `docs/` by hand — only outlines and `examples.tsx`. +- One example export per token; keep them runnable (they are type-checked in CI). +- Regenerate only what changed; a clean tree must produce a zero-diff `sync`. +- Also refresh the generated `app-shell-patterns` consumer skill when component interfaces change (future: `docs-kit sync` will emit it). + +## Related + +- `packages/docs-kit` — the extractor / gate / assembler. +- `docs.config.json` — roots, categories, coverage enforcement, exclusions. diff --git a/decisions/documentation-management-overhaul.md b/decisions/documentation-management-overhaul.md new file mode 100644 index 00000000..31bafec9 --- /dev/null +++ b/decisions/documentation-management-overhaul.md @@ -0,0 +1,209 @@ +# Documentation Management Overhaul + +## Context + +Today all ~65 docs (`docs/components/` ×37, `docs/api/` ×22, `docs/concepts/` ×6) are maintained by hand, with no enforced link between a component's source and its doc. Nothing detects when an API changes but its doc doesn't — drift is invisible. There's a live example of the failure right now: `timeline.tsx` has a component, a test, **and** `docs/components/timeline.md`, yet it isn't exported from `index.ts` — a documented component consumers can't import. + +An existing agentic workflow (`.github/workflows/docs-update.lock.yml`) patches drift _after_ changesets merge to `main`, but it's heuristic, non-deterministic, and post-merge (drift lands first). + +**Goal:** a one-way `source → output` docs pipeline with clear human/AI author points, deterministic drift gates that block _before_ merge, regeneration only when the interface or authored intent actually changes, and a browsable site with live in-place examples alongside their source. + +This plan was designed interactively; every decision below reflects an explicit choice by the maintainer. + +--- + +## Authorship model (Goal #1: clear human/AI author points) + +| Surface | Owner | Editable by hand? | +| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | ------------------------------------- | +| `*.docs-outline.md` — prose (verbatim) + English example intents + `sources` + flags | **Human owns intent** (AI may draft; human curates & approves at PR review) | ✅ the **only** hand-editable surface | +| `docs/**/[unit].md`, `[unit].examples.tsx`, `docs-manifest.json`, the `app-shell-patterns` skill | **AI synthesizes** (`resync-docs`), human reviews the diff | ❌ never | + +One rule: **humans edit intent (outlines); AI produces everything downstream; nothing generated is ever hand-edited.** + +--- + +## Core model (resolved decisions) + +| Dimension | Decision | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Doc inclusion signal** | **Explicit**: a unit exists because a `*.docs-outline.md` claims it. Reconciled against `index.ts` (see Coverage below). **Folders are NOT the signal** — folderization is a free code-organization choice, decoupled from docs. | +| **Scope** | Public surface (exported from `index.ts`). Internal-only files need no outline and may be folder-grouped freely without implying documentation. | +| **Doc unit / grouping** | A unit = one output doc. Its `docs-outline.md` (colocated with the anchor source) declares a `sources:` glob list that may span `components/`, `hooks/`, `lib/`, `contexts/`, `types/` — so a unit can gather cross-cutting, even _shared_, code (e.g. `collection` types shared across DataTable/Kanban/Gantt). | +| **Output location** | Inferred from where the outline lives: `components/…` → `docs/components/`, `hooks/…` → `docs/api/`, `docs-src/concepts/…` → `docs/concepts/`, etc. Slug from a `group`/filename. | +| **Change signal** | **Blocking:** type-surface hash + outline hash. **Advisory (non-blocking):** test-snapshot hash. | +| **Examples** | Per unit: one runnable **`[unit].examples.tsx`** with keyed named exports; the `.md` code fences are **derived deterministically** by extracting those exports. Compiles in CI. Per-segment keys ⇒ regenerate only the changed example, reuse the rest as baseline. | +| **Docs site** | A dedicated **AppShell-based renderer app** (dogfooding). Build-time `import.meta.glob` + lazy dynamic import of the canonical `docs/**/*.{md,examples.tsx}` — imports in place, **no copy**. Dev serves from source w/ HMR; prod bundles. | +| **Fix path** | Deterministic pre-merge `check-docs` gate (**no LLM**) blocks the PR on drift. The local `resync-docs` skill is the **only** fix path. `docs-update.lock.yml` is **retired**. Pre-commit _warns_; CI _blocks_. | +| **Non-component docs** | Unified source→output; the whole `docs/` output tree is 100% generated. Hooks = code-backed units. Concepts/patterns = prose outlines in `docs-src/` (outline-hash trigger only; may embed live-example tokens). Tokens = code-backed from theme CSS. Re-exports = lightweight **reference units** (prose + upstream link; claim symbols by name; no type-surface hash). | +| **Drift baseline & integrity** | The **committed `docs-manifest.json`** stores per unit: output path, `sources` globs, the **input** hashes (type-surface/outline/snapshot) **and output** hashes (`.md` + `examples.tsx`). `check-docs` recomputes and compares — no git-history diffing. Input drift ⇒ needs resync; **output-hash mismatch ⇒ a generated file was hand-edited ⇒ CI fail**. | +| **Migration** | AI **reverse-generates** outlines + `examples.tsx` + manifest from the existing docs; humans curate/restructure; enforcement phases in per-unit. | +| **`app-shell-patterns`** | A **consumer-facing build skill** synthesized from component interfaces + pattern docs, manifest-tracked, resynced when upstream drifts. | + +--- + +## Workflow + +```mermaid +flowchart TB + subgraph AUTHOR["Authorship — human owns intent"] + OUT["*.docs-outline.md
prose + example intents + sources + flags"] + SRC["component / hook / theme source"] + end + + OUT -->|edit intent| GATE + SRC -->|edit changes type surface| GATE + + subgraph GATE["check-docs — deterministic, NO LLM"] + H["recompute hashes:
type-surface / outline / snapshot"] + R["reconcile vs index.ts:
every export claimed or excluded"] + M["compare vs committed
docs-manifest.json"] + end + + GATE -->|clean| PASS["pre-commit: pass
CI: pass"] + GATE -->|drift OR uncovered export| FAIL["pre-commit: WARN
CI: BLOCK
+ list of units & reasons"] + + FAIL -->|dev runs locally| SKILL + + subgraph SKILL["resync-docs skill — LLM, local"] + RG["regenerate ONLY changed segments
existing output = baseline"] + WR["write outputs + manifest hashes"] + end + + SKILL --> OUTPUT + + subgraph OUTPUT["Generated output — never hand-edited"] + MD["docs/**/*.md"] + EX["docs/**/*.examples.tsx"] + PS["app-shell-patterns
consumer skill"] + MAN["docs-manifest.json"] + end + + OUTPUT -->|human review at PR| COMMIT["commit → CI green"] + MD -->|import.meta.glob| APP["docs renderer app (AppShell)
live examples + source"] + EX -->|import.meta.glob| APP + MD -->|browse as-is| GH["GitHub / external md site"] +``` + +--- + +## Coverage reconciliation (the anti-drift core — answers "what if I forget?") + +`check-docs` treats `index.ts` as the source of truth for the public surface and reconciles it **both ways**: + +``` +for each export E in index.ts: + claimed = a code-backed unit's `sources` resolves to E + OR a reference outline's `claims` lists E by name + excluded = E is in docs.config.ts exclusions (rare escape hatch only) + if not (claimed or excluded): → BLOCK "E is exported but undocumented" + +for each code-backed unit U: + for each public symbol U documents: + if symbol not exported from index.ts: → BLOCK "U documents non-exported symbol" (the timeline case) +``` + +So adding a new exported hook and forgetting to claim it **fails CI** with a specific message; you must add it to a unit's `sources`, a reference outline's `claims`, or (rarely) the escape-hatch exclusions. `sources` is never a trust-me allowlist. + +Coverage applies to everything exported from `index.ts`: **code-backed units** satisfy it via `sources`; **reference units** (re-exports) satisfy it via `claims` (by name) but carry no type-surface hash. Prose concepts/patterns aren't in the public surface, so they're outside coverage entirely — triggered solely by their own outline hash. + +**Code-backed outline frontmatter:** + +```yaml +--- +group: data-table # slug → docs/components/data-table.md (category from outline location) +sources: + - packages/core/src/components/data-table/** + - packages/core/src/hooks/use-collection-variables.ts + - packages/core/src/lib/collection-url-state.ts + - packages/core/src/contexts/collection-control-context.tsx + - packages/core/src/types/collection.ts +--- +``` + +**Reference outline frontmatter** (re-exports we don't own — light mention + pointer, no type surface): + +```yaml +--- +group: react-router # docs-src/references/ → docs/references/react-router.md +upstream: https://reactrouter.com/ +claims: [useNavigate, useParams, useLocation, useSearchParams, useRouteError, Link] +--- +``` + +Prose is copied verbatim; example segments are keyed English tokens, e.g. +` A DataTable with pagination and a filter toolbar.` + +--- + +## Directory layout & packaging (answers "new package?") + +**Yes — keep all tooling out of the published `core` bundle.** + +``` +packages/core/src/**/[unit].docs-outline.md ← authored source, colocated; not published (core ships only dist/** + skills/**, so src/ never ships — no extra config) +packages/docs-kit/ ← NEW private (unpublished) workspace pkg: + type-surface extractor (reuses ts-morph, already a dep in vite-plugin) + check-docs • manifest gen • md assembler (prose + extracted fences) +docs-src/concepts|patterns|tokens/*.docs-outline.md ← authored prose outlines (no code home) +docs-src/references/*.docs-outline.md ← re-export reference outlines (claims by name + upstream link) + +docs/**/[unit].md, docs/**/[unit].examples.tsx ← generated output (repo root, as today) +docs/docs-manifest.json ← committed generated baseline +docs.config.ts ← exclusions list + location→category rules + +examples/docs-app/ ← NEW AppShell renderer app (dogfoods app-shell) +examples/vite-app/ ← STRIPPED to a minimal AppShell MVP (see below) +.agents/skills/resync-docs/ ← NEW local LLM skill +``` + +- **Vite example app is stripped back** to a minimal reference AppShell implementation; its current demo pages (which informally served as examples) are removed — examples now live in `[unit].examples.tsx` and are shown in the docs-app. +- The `app-shell-patterns` consumer skill is generated into **`packages/core/skills/`** — already published to consumers via core's existing `files: ["skills/**"]`, so it ships with the npm package (dir doesn't exist yet; the pipeline creates it). + +--- + +## Pieces to build + +1. **Type-surface extractor + hasher** (`packages/docs-kit`): per unit, resolve `sources` → intersect (`index.ts` exports) ∩ (declarations in sources) → serialize signatures deterministically → hash. Ignores comments/refactors/formatting. +2. **`check-docs`** (deterministic, no LLM): recompute hashes, run coverage reconciliation, compare to manifest. Blocking = type-surface/outline mismatch, uncovered export, missing/stale output, **or an output-hash mismatch (a generated file was hand-edited)**. Advisory = snapshot mismatch (prints, never fails). Emits unit list + reasons; non-zero exit in CI. +3. **`resync-docs`** skill (LLM, local): input optional unit list (else runs `check-docs`); per unit read current output as baseline, classify what changed, regenerate only affected segments, reassemble `.md`, refresh type tables, rewrite manifest hashes. Also re-evaluates the `app-shell-patterns` skill on upstream drift. +4. **Docs renderer app** (`examples/docs-app`): `import.meta.glob` → one AppShell resource per unit; renders prose + at each token the live component (from `examples.tsx`) and its extracted source. Dev needs `server.fs.allow` for `docs/` + tsconfig include so examples type-check against `@tailor-platform/app-shell`. +5. **CI + hooks**: add `check-docs` turbo task to `ci-packages.yaml` (blocks); type-check `docs/**/*.examples.tsx` against core; `lefthook.yml` pre-commit runs `check-docs` in **warn** mode; **delete** `docs-update.lock.yml`. + +--- + +## Execution phases + +1. **Pilot / POC (first)** — implement the full pipeline end-to-end against **2–3 representative components** (e.g. `Button` single-file, `DataTable` multi-dir/shared-types, `Sidebar` compound) **+ 1–2 patterns**. Proves extractor, gate, coverage, examples, renderer, and skill before scaling. Iterate on the outline/manifest format here. +2. **Build-out** — `docs-kit`, `check-docs`, `resync-docs`, renderer app, CI wiring, strip the Vite example app, retire the bot. +3. **Migration** — AI reverse-generates all remaining outlines + `examples.tsx` + manifest; humans curate; coverage enforcement flips to "all public exports required." + +--- + +## Verification + +1. **Extractor unit tests:** comment-only/refactor edit → type-surface hash stable; prop added/removed → changes; class-only tweak → type-surface stable, snapshot advisory fires. +2. **`check-docs` golden cases:** clean tree → exit 0; mutate a prop type → block naming the unit; edit outline prose → block; class tweak → advisory only, exit 0; **add an exported hook with no outline → block** (the forgetting case); **`timeline` → block** (documented, not exported); **hand-edit a generated `.md`/`examples.tsx` → block** (output-hash mismatch). +3. **Examples compile:** break a generated `examples.tsx` (removed prop) → CI type-check fails. +4. **Renderer app:** `preview_start` the docs-app; a unit page shows live component + matching source; edit an `examples.tsx` in dev → HMR updates with no copy step; `.md` fence === rendered example. +5. **Idempotency:** `resync-docs` on a clean tree → zero diffs; change one prop → exactly one unit resyncs. + +--- + +## Implementation notes (from the POC pilot) + +The pilot (`packages/docs-kit` + Button/Dialog/DataTable + form-modal/list-dense-scan + `examples/docs-app`) validated the design and surfaced these concrete details: + +- **Prop tables are auto-generated** from the resolved type surface. `docs-kit` lists a `*Props` symbol's **first-party** props only — members whose declaration resolves into `node_modules` (e.g. the ~250 DOM attributes from `React.ComponentProps<"button">`) are filtered out, leaving the props the component actually adds (`variant`, `size`, `render`, …) with their JSDoc. An `` token in an outline places the generated `## Props` + `## Exports`; otherwise they append. **TODO:** default values aren't extracted yet (they live in cva `defaultVariants` / destructuring defaults, not the type) — capture via an `@default` JSDoc tag or cva parsing. +- **`astw:` is app-shell's INTERNAL Tailwind prefix** — for the library's own components and its shipped stylesheet only. **Consumers (and therefore all generated example code) use ordinary, unprefixed Tailwind.** The new `docs/concepts/styling.md` documents the consumer setup (`@tailwindcss/vite` + `@import "tailwindcss"` then app-shell `styles`/theme); the docs-app is wired exactly that way as the reference. +- **Examples live at repo-root `docs/`, outside any package**, so their bare imports don't resolve against a package's `node_modules` by default. Two wiring details make them type-check and run: + - docs-app **tsconfig** uses `baseUrl` + **end-anchored** `paths` regexes mapping the exact specifiers (`@tailor-platform/app-shell`, `react`, `lucide-react`) to the app's deps — end-anchored so subpath imports like `@tailor-platform/app-shell/styles` still resolve via package `exports`. Core has **no top-level `types`** field (only inside `exports`), so the app-shell path points straight at `dist/app-shell.d.ts`. + - docs-app **Vite** resolves the out-of-tree examples' bare imports via a **`docs/`-scoped `resolveId` plugin** (see the AppShell bullet), keeps a narrow alias only for `@tailor-platform/app-shell-vite-plugin/parser` (core's bundle imports it via `createTypedPaths`, so that package must be built), `dedupe: ["react","react-dom"]`, and Tailwind `@source "../../../docs/**/*.examples.tsx"` so example utility classes are generated. +- **The docs-app is a real `` app** using file-based routing (the "automagic"). One thin `src/pages/**/page.tsx` per unit renders a shared `DocPage` (markdown + live examples); `} />` infers routes **and** the grouped sidebar automatically from the page structure + `appShellPageProps.meta` — no `modules` prop, no manual `SidebarItem`s. This also brings breadcrumbs, the command palette, and the theme switcher for free. The out-of-tree examples resolve via a **`docs/`-scoped Vite `resolveId` plugin** (not a global alias — a global `@tailor-platform/app-shell` alias would clash with the routing plugin's `entrypoint` interception of `App.tsx`). Follow-up: `docs-kit` could generate the per-unit `page.tsx` stubs so new units appear with zero manual wiring. +- **`sync` formats its outputs + sources with oxfmt _before_ hashing**, so the committed bytes match the manifest and the pre-commit oxfmt hook is idempotent (it can never reformat a generated file after the fact and silently break the gate). Hashes are also whitespace-normalized as a second line of defense. This was a real bite during the pilot: oxfmt reformats code _inside_ generated `.md` fences, which a whitespace-only normalization doesn't absorb. +- **Sources are `git mv`'d, not invented.** Component outlines are moved from the hand-written `docs/components/*.md` (a prep commit, preserving `git log --follow` lineage); the generated `docs/components/*.md` then land as new files. Patterns are moved from **`catalogue/src/pattern/`** (`form/modal` → `form-modal`, `list/dense-scan` → `list-dense-scan`) — their `PATTERN.md` becomes the outline and the example `.tsx` becomes the runnable example, then both are adapted to the docs-kit format. Catalogue already generates the `app-shell-patterns` skill; the intent is for docs-kit to **subsume** that (generate the skill from these docs) and retire catalogue's pipeline, so catalogue's generator/tests are knowingly left to regress for now. + +## Optional / follow-up (not blocking) + +- **Internal reorg** — move single-consumer internals into their consumer's folder; bucket framework internals under `app-shell/`/`internals/`. Now purely cosmetic (the `sources` globs make it irrelevant to docs), so a separate cleanup PR. +- **Public docs-site polish** (SEO/search/hosting/prerender) — only if a public-facing site is wanted. diff --git a/docs-src/concepts/styling.docs-outline.md b/docs-src/concepts/styling.docs-outline.md new file mode 100644 index 00000000..defe4aab --- /dev/null +++ b/docs-src/concepts/styling.docs-outline.md @@ -0,0 +1,56 @@ +--- +group: styling +title: Styling & Tailwind setup +description: How to wire Tailwind and app-shell's theme in a consumer app +--- + +app-shell ships its own compiled styles and a set of themes. In **your** app you write **ordinary Tailwind classes** — the `astw:` prefix you may see in app-shell's own source is an **internal** prefix for the library's shipped stylesheet and is not something consumers use. + +## 1. Install Tailwind + +```bash +pnpm add -D tailwindcss @tailwindcss/vite +``` + +## 2. Add the Vite plugin + +```ts +// vite.config.ts +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], +}); +``` + +## 3. Import Tailwind + the app-shell theme + +Import Tailwind first, then app-shell's base styles, then one theme: + +```css +/* src/index.css */ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +@import "@tailor-platform/app-shell/themes/bloom"; +``` + +## 4. Use ordinary Tailwind in your code + +Your own markup uses unprefixed utilities; app-shell components bring their own styling: + +```tsx +import { Button } from "@tailor-platform/app-shell"; + +
+ + +
; +``` + +> **Do not** use the `astw:` prefix in your app — it is reserved for app-shell's internal stylesheet. If you see it in generated example code, that's a bug in the example (report it). + +## Reference implementation + +The docs explorer app (`examples/docs-app`) and the Vite example (`examples/vite-app`) are both wired exactly this way — use them as a copy-paste reference. diff --git a/docs-src/patterns/form-modal.docs-outline.md b/docs-src/patterns/form-modal.docs-outline.md index 0c1c454e..46cf3fb1 100644 --- a/docs-src/patterns/form-modal.docs-outline.md +++ b/docs-src/patterns/form-modal.docs-outline.md @@ -1,6 +1,8 @@ --- slug: pattern/form/modal name: Modal Form +title: Modal Form +group: form-modal category: pattern subcategory: form description: Default form pattern for Create/Edit — keeps user in context on the parent screen @@ -28,11 +30,11 @@ dont: ## Page Implementation - + ## Route-driven Variant - +The route-driven variant renders the same component at both the parent path and the `create` / `edit` path, with `onOpenChange` navigating back so the URL stays in sync. ## Constraints diff --git a/docs-src/patterns/list-dense-scan.docs-outline.md b/docs-src/patterns/list-dense-scan.docs-outline.md index 66d1c763..fc0f6361 100644 --- a/docs-src/patterns/list-dense-scan.docs-outline.md +++ b/docs-src/patterns/list-dense-scan.docs-outline.md @@ -1,6 +1,8 @@ --- slug: pattern/list/dense-scan name: Dense Scan List +title: Dense Scan List +group: list-dense-scan category: pattern subcategory: list description: High-density scannable list backed by GraphQL connections with DataTable, sort, filters, and pagination @@ -39,11 +41,11 @@ dont: ## Column Definition - +Columns are defined with `createColumnHelper` (see the example below) — each column has a `label` and an `accessor`, or a custom `render` for cells like status badges. ## Page Implementation - + ## Page Layout & Internal Scrolling diff --git a/docs.config.json b/docs.config.json new file mode 100644 index 00000000..9d0adc46 --- /dev/null +++ b/docs.config.json @@ -0,0 +1,17 @@ +{ + "$comment": "Config for @tailor-platform/app-shell-docs-kit. See decisions/documentation-management-overhaul.md.", + "roots": ["packages/core/src", "docs-src"], + "tsconfig": "packages/core/tsconfig.json", + "indexFile": "packages/core/src/index.ts", + "manifestFile": "docs/docs-manifest.json", + "categories": [ + { "match": "packages/core/src/components/**", "outDir": "docs/components" }, + { "match": "packages/core/src/hooks/**", "outDir": "docs/api" }, + { "match": "docs-src/concepts/**", "outDir": "docs/concepts" }, + { "match": "docs-src/patterns/**", "outDir": "docs/patterns" }, + { "match": "docs-src/references/**", "outDir": "docs/references" } + ], + "enforceCoverage": false, + "exclusions": [], + "snapshotDir": "packages/core/__snapshots__" +} diff --git a/docs/components/button.examples.tsx b/docs/components/button.examples.tsx new file mode 100644 index 00000000..8b35de9c --- /dev/null +++ b/docs/components/button.examples.tsx @@ -0,0 +1,39 @@ +import { Button } from "@tailor-platform/app-shell"; +import { PlusIcon } from "lucide-react"; + +export function BasicUsage() { + return ( +
+ + + +
+ ); +} + +export function Variants() { + return ( +
+ + + + + + +
+ ); +} + +export function Sizes() { + return ( +
+ + + + + +
+ ); +} diff --git a/docs/components/button.md b/docs/components/button.md new file mode 100644 index 00000000..e697aa54 --- /dev/null +++ b/docs/components/button.md @@ -0,0 +1,170 @@ +--- +title: Button +description: Styled button with multiple variants and sizes +--- + + + +# Button + +# Button + +The `Button` component is a styled button with multiple visual variants and sizes. It supports rendering as a custom element via the `render` prop (Base UI render pattern). + +## Import + +```tsx +import { Button } from "@tailor-platform/app-shell"; +``` + +## Basic Usage + +```tsx +function BasicUsage() { + return ( +
+ + + +
+ ); +} +``` + +## Props + +| Prop | Type | Default | Description | +| ----------- | ----------------------------------------------------------------------------- | ----------- | ------------------------------------------------- | +| `variant` | `"default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| "link"` | `"default"` | Visual style variant | +| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon"` | `"default"` | Button size | +| `render` | `React.ReactElement` | - | Custom element to render as (Base UI render prop) | +| `className` | `string` | - | Additional CSS classes | +| `children` | `React.ReactNode` | - | Button content | + +All standard HTML ` + + + + + + + ); +} +``` + +## Sizes + +```tsx +function Sizes() { + return ( +
+ + + + + +
+ ); +} +``` + +## Render Prop + +The `render` prop lets you use the button's styles on a custom element: + +```tsx +import { Button } from "@tailor-platform/app-shell"; +import { Link } from "@tailor-platform/app-shell"; + +; +``` + +This is the Base UI render pattern — the button's class names and event handlers are applied to the rendered element. + +## Examples + +### Form Actions + +```tsx +
+ + +
+``` + +### Destructive Confirmation + +```tsx + +``` + +### Icon Button + +```tsx +import { PlusIcon } from "lucide-react"; + +; +``` + +## TypeScript + +```typescript +import { type ButtonProps } from "@tailor-platform/app-shell"; +``` + +## Styling + +Use `buttonVariants` to apply button styles to non-button elements: + +```tsx +import { buttonVariants } from "@tailor-platform/app-shell"; + + + View Orders +; +``` + +## Accessibility + +- Buttons render with `cursor: pointer` to signal interactivity consistently across all variants and sizes. +- Disabled buttons suppress the pointer cursor via `pointer-events-none`, so they do not signal clickability. + +## Related Components + +- [Dialog](./dialog.md) - Use buttons as dialog triggers and actions +- [Sheet](./sheet.md) - Use buttons as sheet triggers +- [Layout](./layout.md) - Use buttons in page header actions + +## Props + +_Generated from the type surface — first-party props only._ + +### ButtonProps + +| Prop | Type | Description | +| ---------- | ------------------------------------------------------------------------------------- | ----------- | +| `render?` | `React.ReactElement>` | | +| `size?` | `"default" \| "sm" \| "lg" \| "xs" \| "icon" \| null` | | +| `variant?` | `"link" \| "default" \| "destructive" \| "outline" \| "secondary" \| "ghost" \| null` | | + +## Exports + +- `Button` — function +- `ButtonProps` — type +- `buttonVariants` — const diff --git a/docs/components/data-table.examples.tsx b/docs/components/data-table.examples.tsx new file mode 100644 index 00000000..8bc5f60a --- /dev/null +++ b/docs/components/data-table.examples.tsx @@ -0,0 +1,50 @@ +import { DataTable, createColumnHelper, useDataTable } from "@tailor-platform/app-shell"; +import type { RowAction } from "@tailor-platform/app-shell"; + +interface Product extends Record { + id: string; + name: string; + price: number; + status: string; + createdAt: string; +} + +const PRODUCTS: Product[] = [ + { id: "1", name: "Aluminium widget", price: 19.99, status: "active", createdAt: "2026-01-04" }, + { id: "2", name: "Brass gadget", price: 149, status: "draft", createdAt: "2026-02-11" }, + { id: "3", name: "Copper gizmo", price: 4.5, status: "active", createdAt: "2026-03-02" }, +]; + +const { column } = createColumnHelper(); + +const columns = [ + column({ label: "Name", accessor: (row) => row.name, truncate: true }), + column({ label: "Price", type: "money", accessor: (row) => row.price }), + column({ label: "Status", type: "badge", accessor: (row) => row.status }), + column({ label: "Created", type: "date", accessor: (row) => row.createdAt }), +]; + +const rowActions: RowAction[] = [ + { id: "edit", label: "Edit", onClick: (row) => window.alert(`Edit ${row.name}`) }, + { + id: "delete", + label: "Delete", + variant: "destructive", + onClick: (row) => window.alert(`Delete ${row.name}`), + }, +]; + +export function BasicUsage() { + const table = useDataTable({ + columns, + data: { rows: PRODUCTS, total: PRODUCTS.length }, + onClickRow: (row) => window.alert(`Open ${row.name}`), + rowActions, + }); + + return ( + + + + ); +} diff --git a/docs/components/data-table.md b/docs/components/data-table.md new file mode 100644 index 00000000..e7176d20 --- /dev/null +++ b/docs/components/data-table.md @@ -0,0 +1,866 @@ +--- +title: DataTable +description: Compound data table component with sortable columns, filter chips, cursor-based pagination, row actions, and multi-row selection +--- + + + +# DataTable + +# DataTable + +`DataTable` is a compound component for displaying collections of records. It integrates with the collection variable hooks (`useCollectionVariables`) to drive sorting, filtering, and cursor-based pagination through a GraphQL API. + +## Import + +```tsx +import { + DataTable, + useDataTable, + useDataTableContext, + useCollectionVariables, + createColumnHelper, + type Column, + type DataTableData, + type DataTableRootProps, + type DataTablePaginationProps, + type RowAction, + type UseDataTableOptions, + type UseDataTableReturn, + type MetadataFieldOptions, + type DataTableContextValue, +} from "@tailor-platform/app-shell"; +``` + +## Basic Usage + +```tsx +import { gql, useQuery } from "urql"; +import { + DataTable, + useDataTable, + useCollectionVariables, + createColumnHelper, +} from "@tailor-platform/app-shell"; + +const LIST_JOURNALS = gql` + query ListJournals( + $after: String + $before: String + $first: Int + $last: Int + $order: [JournalOrderInput] + $query: JournalQueryInput + ) { + journals( + after: $after + before: $before + first: $first + last: $last + order: $order + query: $query + ) { + edges { + node { + id + contents + authorID + } + } + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + total + } + } +`; + +type Journal = { id: string; contents: string; authorID: string }; + +const { column } = createColumnHelper(); + +const columns = [ + column({ + label: "ID", + render: (row) => row.id, + filter: { field: "id", type: "uuid" }, + }), + column({ + label: "Author", + render: (row) => row.authorID, + sort: { field: "authorID", type: "string" }, + filter: { field: "authorID", type: "string" }, + }), + column({ + label: "Contents", + render: (row) => row.contents, + filter: { field: "contents", type: "string" }, + }), +]; + +function JournalsPage() { + const { variables, control } = useCollectionVariables({ + params: { pageSize: 20 }, + }); + + const [result] = useQuery({ + query: LIST_JOURNALS, + variables: { + ...variables.pagination, + query: variables.query, + order: variables.order, + }, + }); + + const table = useDataTable({ + columns, + data: result.data + ? { + rows: result.data.journals.edges.map((e) => e.node), + pageInfo: result.data.journals.pageInfo, + total: result.data.journals.total, + } + : undefined, + loading: result.fetching, + control, + }); + + return ( + + + + + + + + + + ); +} +``` + +## Live example + +A self-contained `DataTable` with static rows (no GraphQL) demonstrating the same compound API: + +```tsx +function BasicUsage() { + const table = useDataTable({ + columns, + data: { rows: PRODUCTS, total: PRODUCTS.length }, + onClickRow: (row) => window.alert(`Open ${row.name}`), + rowActions, + }); + + return ( + + + + ); +} +``` + +## Sub-components + +`DataTable` is a namespace object. All sub-components read state from `DataTable.Root` via context. + +| Sub-component | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `DataTable.Root` | Context provider. Wraps all other sub-components. Required. | +| `DataTable.Table` | Renders the `` with headers and body. Required. | +| `DataTable.Toolbar` | Container for toolbar content (e.g. filters). Optional. Pass `columnSettings` to render the built-in "Columns" control (show/hide + reorder + pin) at the top-right. See props below. | +| `DataTable.Filters` | Auto-generated filter chips from column filter configs. Requires `control` from `useCollectionVariables`. | +| `DataTable.Footer` | Footer container for pagination and other footer content. Optional. | +| `DataTable.Pagination` | Pre-built pagination controls with optional row count and selection info. Requires `control` from `useCollectionVariables`. Place inside `DataTable.Footer`. | + +### `DataTable.Root` Props + +| Prop | Type | Description | +| ----------- | -------------------------- | -------------------------------------------- | +| `value` | `UseDataTableReturn` | Return value of `useDataTable()`. Required. | +| `children` | `ReactNode` | Sub-components to render inside the root. | +| `className` | `string` | Additional CSS class for the root container. | + +### `DataTable.Toolbar` Props + +| Prop | Type | Default | Description | +| ---------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `children` | `ReactNode` | — | Toolbar content (e.g. `DataTable.Filters`), laid out on the left. | +| `columnSettings` | `boolean` | `false` | Render the built-in "Columns" control (show/hide + reorder + pin) anchored to the top-right. Persists per-user when `useDataTable` has a `tableId`. | +| `className` | `string` | — | Additional CSS class for the toolbar container. | + +### `DataTable.Pagination` Props + +| Prop | Type | Default | Description | +| ----------------- | ---------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pageSizeOptions` | `number[]` | — | Available page-size options. When provided, a page-size switcher is rendered. First/Last buttons are shown only when the backend returns a `total` count. | + +`DataTable.Pagination` automatically displays a row count and selection info text on the left side of the pagination bar based on context state: + +| Condition | Displayed text | +| ----------------------------------------- | ------------------------ | +| `total` is provided | `X row(s)` | +| Rows selected and `total` is provided | `Y of X row(s) selected` | +| Rows selected and `total` is not provided | `Y row(s) selected` | +| No selection enabled and no `total` | _(nothing displayed)_ | + +Row selection is enabled by providing `onSelectionChange` to `useDataTable`. The `total` value comes from `DataTableData.total`. + +## Column pinning, visibility & ordering + +- **Pin** a column with `pin: "left" | "right"`. Pinned columns stay visible during horizontal scroll; the selection column auto-pins left and the row-actions column auto-pins right. A subtle shadow appears at the frozen edge once the table is scrolled under it. Sticky offsets are measured from the rendered layout, so a `width` isn't required — but setting `width` on pinned columns is recommended so their size stays stable as content changes. +- **Column settings.** Pass `columnSettings` to `DataTable.Toolbar` to render a built-in "Columns" control — a popover to show/hide columns, reorder them (drag), and change pinning by dragging a column between the **Fixed left**, **Scrollable**, and **Fixed right** zones. It's a toolbar prop (not a composed sub-component) because the control always sits in the same top-right position. +- **Persistence.** Pass a stable, **unique** `tableId` to persist each user's column layout (visibility, order, pinning) to `localStorage` (key `as:data-table:v1:`). This is a per-user preference — it is deliberately **not** stored in the URL like filters/sort/pagination, so it survives reloads and isn't reset by shared/filtered links. Omit `tableId` for in-memory-only layout (state simply isn't persisted). Two tables mounted with the same `tableId` share one storage key and overwrite each other — use a unique id per table (e.g. `:`); a dev-mode warning fires on duplicates. + +```tsx +const table = useDataTable({ + columns, // e.g. [{ id: "ref", label: "Ref", width: 140, pin: "left" }, ...] + data, + tableId: "orders-list", +}); + + + + +; +``` + +## `useDataTable` + +Creates the table state object to pass to `DataTable.Root`. + +```tsx +const table = useDataTable({ + columns, + data, + loading, + control, +}); +``` + +### Options + +| Option | Type | Description | +| ------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `columns` | `Column[]` | Column definitions. Required. | +| `data` | `DataTableData \| undefined` | Fetched data. Pass `undefined` while loading. | +| `loading` | `boolean` | When `true`, renders a loading skeleton. | +| `error` | `Error \| null` | When set, renders an error message in the table body. | +| `control` | `CollectionControl` | Collection control from `useCollectionVariables()`. Required for `DataTable.Pagination` and `DataTable.Filters`. | +| `onClickRow` | `(row: TRow) => void` | Called when the user clicks a row. Adds a pointer cursor to rows. | +| `tableId` | `string` | Stable id used to persist per-user column layout (visibility, order, pinning) to `localStorage`. When omitted, column layout is in-memory only and resets on reload. | +| `rowActions` | `RowAction[]` | Per-row action items rendered in a kebab-menu column. The column is omitted when empty or not provided. | +| `onSelectionChange` | `(ids: string[]) => void` | Called with selected row IDs on change. Providing this enables the checkbox column. Rows must have a string `id`. | +| `sort` | `false \| { multiple?: boolean }` | Sort behaviour. `false` disables sorting entirely. `{ multiple: true }` enables multi-column sorting. Omit or pass `{}` for single-column sort (default). | + +### `DataTableData` + +| Property | Type | Description | +| ---------- | ---------------- | -------------------------------------------------------------------- | +| `rows` | `TRow[]` | Row data to display. | +| `pageInfo` | `PageInfo` | Cursor pagination info from the API. | +| `total` | `number \| null` | Total record count. Used for First/Last navigation and page counter. | + +## `Column` + +A column definition passed to `useDataTable`. `Column` is a discriminated union on `type` — the shape of `typeOptions` is narrowed per branch, so mismatches are compile errors rather than silent runtime no-ops. + +### Shared fields + +| Property | Type | Description | +| ---------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `label` | `string` | Column header text. Omit for icon-only columns. | +| `render` | `(row: TRow) => ReactNode` | Renders the cell content. Optional — overrides the built-in `type` renderer when set. | +| `id` | `string` | Stable identifier for column visibility and React key. Falls back to `label` when omitted. | +| `width` | `number` | Fixed column width in pixels. Optional. | +| `pin` | `"left" \| "right"` | Freezes the column to that edge so it stays visible during horizontal scroll (the default; the user can override it via the toolbar's `columnSettings` control). Sticky offsets are measured from the rendered layout, so `width` isn't required — but setting `width` on pinned columns is recommended for stable sizing. The selection column auto-pins left and the row-actions column auto-pins right. | +| `align` | `"left" \| "right"` | Horizontal alignment. Defaults to `"right"` for `type: "number"` and `type: "money"`; `"left"` otherwise. Pass `"left"` to opt a numeric column out. | +| `truncate` | `boolean` | Truncate overflowing text with an ellipsis. Wires up an app-shell `` automatically when the resolved cell value is a string or number — resolved via `accessor` first, then `row[col.id]` as a fallback — so hovering the cell reveals the full value. With `inferColumns`, no explicit `accessor` is needed because `id` is pinned to the field name. Requires another column to anchor the row width (`width` on a neighbor, or a fixed-size column like selection / row actions). | +| `accessor` | _(narrowed per `type`)_ | Extracts the raw value. The return type is narrowed per `type` branch — returning an array is a compile error on all typed columns except `badge`, and returning a plain object is a compile error on all typed columns. Untyped columns (`type` omitted) retain `unknown`. `null` and `undefined` are always allowed. | +| `sort` | `SortConfig` | Sort configuration. When set, the column header becomes clickable (Asc → Desc → off). | +| `filter` | `FilterConfig` | Filter configuration. When set, the column appears as an option in `DataTable.Filters`. | + +### `type`-specific fields + +| `type` | `typeOptions` | +| ----------- | ------------------------------------------------------------ | +| _(omitted)_ | _(not allowed; provide `render` to draw the cell)_ | +| `"text"` | _(not allowed)_ | +| `"number"` | `NumberCellOptions` | +| `"money"` | `MoneyCellOptions` | +| `"date"` | `DateCellOptions` | +| `"badge"` | `BadgeCellOptions` | +| `"link"` | **Required** — `LinkCellOptions` (must include `href`) | + +## Cell types + +When `type` is set, the cell is rendered from `accessor(row)` (or `row[id]` when `accessor` is omitted) using a built-in renderer. Pass `render` to override on a per-column basis. + +```tsx +column({ + label: "Total", + accessor: (row) => row.total, + type: "money", + typeOptions: { currency: "USD", maxDecimals: 4 }, +}); +``` + +| `type` | Accessor return type | Value handling | Options interface | +| -------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `text` | `string \| number \| boolean \| bigint \| null \| undefined` | `String(value)` — falls back to `—` when nullish. | _(no options)_ | +| `number` | `number \| null \| undefined` | `Intl.NumberFormat`. `—` for nullish / NaN. | `NumberCellOptions`: `minDecimals`, `maxDecimals`, `locale` | +| `money` | `number \| null \| undefined` | `Intl.NumberFormat` currency. `—` for nullish. | `MoneyCellOptions`: `currency` (string or `(row) => string`), `maxDecimals`, `locale` | +| `date` | `Date \| string \| number \| null \| undefined` | `Intl.DateTimeFormat`. Accepts `Date`/ISO/epoch. | `DateCellOptions`: `dateFormat` (`"short"` \| `"long"` \| `"datetime"`), `locale` | +| `badge` | `string \| number \| boolean \| null \| undefined \| Array` | `` keyed off the stringified value. Arrays render multiple badges. | `BadgeCellOptions`: `badgeVariantMap`, `badgeLabelMap`, `defaultBadgeVariant` (defaults to `"outline-neutral"`), `maxVisible` | +| `link` | `string \| number \| boolean \| null \| undefined` | app-shell `` to `typeOptions.href(row)`. | `LinkCellOptions`: `href: (row) => string \| null \| undefined` (returning nullish renders plain text; **required**) | + +Empty values (`null`, `undefined`, `""`) render a muted `—` placeholder for every type. Use `render` for custom empty-state handling. + +The discriminated-union shape means: + +```tsx +// ❌ Compile error — badgeVariantMap is not a money option +column({ type: "money", accessor: (r) => r.total, typeOptions: { badgeVariantMap: {} } }); + +// ❌ Compile error — link columns must provide typeOptions.href +column({ type: "link", accessor: (r) => r.title }); + +// ❌ Compile error — text columns reject typeOptions entirely +column({ type: "text", accessor: (r) => r.title, typeOptions: { locale: "en-US" } }); + +// ❌ Compile error — text/number/money/link accessor cannot return an array or object +column({ type: "text", accessor: (row) => row.tags }); // row.tags is string[] +column({ type: "number", accessor: (row) => row.meta }); // row.meta is an object +``` + +## Adding a typed column + +Each column type follows the same three-step shape: pick a `type`, point `accessor` at the value, and pass `typeOptions` for the type-specific bits. `label`, `sort`, `filter`, `align`, `width`, and `id` work the same regardless of `type`. + +### `text` — plain string + +```tsx +column({ + label: "Name", + accessor: (row) => row.name, + type: "text", +}); +``` + +- `null` / `undefined` / `""` render a muted `—`. +- No `typeOptions` are accepted on `text` columns. +- Omit `type` entirely if you want to keep `render` required for that column. + +### `number` — locale-formatted number + +```tsx +column({ + label: "Stock", + accessor: (row) => row.stockOnHand, + type: "number", + typeOptions: { minDecimals: 0, maxDecimals: 0, locale: "en-US" }, +}); +``` + +- `minDecimals` / `maxDecimals` default to `0`. +- `maxDecimals` defaults to `minDecimals` when only `minDecimals` is set — pass both for ranges (e.g. `min: 2, max: 4`). +- `NaN` and `null` render the `—` placeholder. + +### `money` — currency + +```tsx +column({ + label: "Total", + accessor: (row) => row.total, + type: "money", + typeOptions: { currency: "USD" }, +}); +``` + +For mixed-currency tables, read `currency` from the row: + +```tsx +column({ + label: "Total", + accessor: (row) => row.total, + type: "money", + typeOptions: { + currency: (row) => row.currencyCode, // "USD", "JPY", "EUR", … + maxDecimals: 4, // raise the cap above the currency default + }, +}); +``` + +- Default `currency` is `"USD"` when omitted or when the accessor returns falsy. +- Invalid ISO codes fall back to USD (rather than throwing). +- The minimum decimals always stays at the currency default (2 for USD, 0 for JPY); `maxDecimals` raises the cap without padding with trailing zeros. + +### `date` — formatted date + +```tsx +column({ + label: "Placed", + accessor: (row) => row.placedAt, // Date | ISO string | epoch ms + type: "date", + typeOptions: { dateFormat: "short" }, // "short" | "long" | "datetime" +}); +``` + +| `dateFormat` | Example output | +| ------------------- | ---------------------- | +| `"short"` (default) | `Apr 9, 2026` | +| `"long"` | `April 9, 2026` | +| `"datetime"` | `Apr 9, 2026, 3:45 PM` | + +- Accepts a `Date`, an ISO 8601 string, or epoch milliseconds. +- Invalid dates render the `—` placeholder. + +### `badge` — status pill + +```tsx +column({ + label: "Status", + accessor: (row) => row.status, + type: "badge", + typeOptions: { + badgeVariantMap: { + shipped: "success", + processing: "outline-warning", + cancelled: "subtle-error", + }, + badgeLabelMap: { + shipped: "Shipped", + processing: "Processing", + cancelled: "Cancelled", + }, + defaultBadgeVariant: "outline-neutral", // unmapped values fall back here + }, +}); +``` + +- The cell value is stringified before lookup, so `accessor` can return strings, numbers, or booleans. +- `accessor` may also return an **array** of values — each item is rendered as a separate badge. +- Unmapped values render with `defaultBadgeVariant` (or `"outline-neutral"`) and the raw stringified value as the label. + +#### Array badges with overflow + +Use `maxVisible` to cap the number of badges shown. Extra values are hidden behind a hover popover: + +```tsx +column({ + ...infer("tags"), + type: "badge", + typeOptions: { + badgeVariantMap: { Premium: "warning", Office: "outline-info" }, + maxVisible: 2, + }, +}); +``` + +### `link` — clickable text + +```tsx +column({ + label: "Order", + accessor: (row) => row.reference, + type: "link", + typeOptions: { href: (row) => `/orders/${row.id}` }, +}); +``` + +- `href` is **required** on `link` columns — it's enforced by the type. +- Returning `null` / `undefined` from `href` renders the cell value as plain text (useful for "no detail page yet" rows). +- Uses the app-shell `` (react-router) so SPA navigation is preserved. For external URLs, fall back to `render` with a plain ``. + +### Overriding a built-in renderer + +`render` always wins over the built-in renderer, so the escape hatch stays open per column: + +```tsx +column({ + label: "Status", + accessor: (row) => row.status, + type: "badge", + typeOptions: { badgeVariantMap: { active: "success" } }, + // Custom render with an icon — type/typeOptions are still required for + // sort/filter scaffolding but are bypassed for rendering. + render: (row) => ( + + + {row.status} + + ), +}); +``` + +### Combining `type` with `inferColumns` + +`inferColumns` (from `@tailor-platform/app-shell-sdk-plugin`) derives `label`, `sort`, `filter`, and `id` from TailorDB metadata. You can layer a `type` on top to get a built-in renderer without losing the inferred sort/filter config: + +```tsx +const infer = inferColumns(tableMetadata.order); + +const columns = [ + // Inferred column — displays row[id] as plain text + column(infer("reference")), + + // Inferred datetime column, swapped to a `date` cell with long format + column({ + ...infer("placedAt"), + type: "date", + typeOptions: { dateFormat: "long" }, + accessor: (row) => row.placedAt, + }), + + // Inferred enum column, rendered as a badge + column({ + ...infer("status"), + type: "badge", + accessor: (row) => row.status, + typeOptions: { + badgeVariantMap: { active: "success", draft: "neutral" }, + }, + }), +]; +``` + +When you spread `...infer("field")`, add `accessor` when you want a typed renderer to read a specific value — built-in renderers read from `accessor` (or `row[id]`). + +## `FilterConfig` + +The `filter` property on a column accepts a `FilterConfig` object. When set, the column appears as an option in `DataTable.Filters` and the filter chip renders an input editor appropriate for the type. + +| Property | Type | Description | +| --------- | ---------------- | ------------------------------------------------------------ | +| `field` | `string` | API field name used in the generated query input. | +| `type` | `FilterType` | Filter editor type (see table below). | +| `options` | `SelectOption[]` | Required when `type` is `"enum"`. List of selectable values. | + +### Filter Types and Operators + +| Type | Input editor | Supported operators | +| ---------- | -------------- | ------------------------------------------------------------------------------------------------------------ | +| `string` | Text | `eq`, `ne`, `contains`, `notContains`, `hasPrefix`, `hasSuffix`, `notHasPrefix`, `notHasSuffix`, `in`, `nin` | +| `number` | Number | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `datetime` | Datetime-local | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `date` | **DatePicker** | `eq` (_exact date_), `gte` (_after_), `lte` (_before_), **`between`** | +| `time` | Time | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `enum` | Dropdown | `eq`, `ne`, `in`, `nin` | +| `boolean` | Toggle | `eq`, `ne` | +| `uuid` | Text | `eq`, `ne`, `in`, `nin` | + +When the user selects the `between` operator on a `number`, `datetime`, `date`, or `time` column, the filter chip renders a range input with **min** and **max** bounds. + +### Date Filters + +`date` columns use the app-shell [`DatePicker`](./date-picker.md) as the filter input (single value and `between` ranges) and present a friendlier, slimmer operator set: + +| Operator | Label | Meaning | +| --------- | ------------ | -------------------------- | +| `eq` | _exact date_ | matches that calendar date | +| `gte` | _after_ | on or after (inclusive) | +| `lte` | _before_ | on or before (inclusive) | +| `between` | _between_ | inclusive min–max range | + +`gt` / `lt` / `ne` are intentionally dropped — the inclusive _after_ / _before_ cover the intent. The filter chip shows the value as a locale-formatted date (e.g. `15 Jun 2026`), and the picker resolves its locale/timezone from the AppShell context. (Only `date` is remapped this way; `datetime` and `time` keep the full numeric operator set and native inputs.) + +### String Filter Case Sensitivity + +String filters are **case-insensitive by default** — they use the Tailor Platform `regex` operator with an `(?i)` prefix. The filter chip renders a **"Case sensitive"** checkbox that lets users opt into exact-case matching. + +To control this behavior programmatically, pass `caseSensitive: true` to `CollectionControl.addFilter`: + +```tsx +control.addFilter("title", "contains", "acme", { caseSensitive: true }); +``` + +You can also set `caseSensitive` directly on a `Filter` object when using `params.initialFilters` in `useCollectionVariables`: + +```tsx +const { variables, control } = useCollectionVariables({ + params: { + initialFilters: [{ field: "title", operator: "contains", value: "acme", caseSensitive: true }], + }, +}); +``` + +When `caseSensitive` is omitted or `false`, the filter is case-insensitive. When `true`, the filter matches the exact case of the input. + +## `RowAction` + +| Property | Type | Description | +| ------------ | ---------------------------- | ---------------------------------------------------- | +| `id` | `string` | Stable identifier for the action. | +| `label` | `string` | Display label in the kebab menu. | +| `icon` | `ReactNode` | Optional icon shown beside the label. | +| `variant` | `"default" \| "destructive"` | Visual style of the menu item. | +| `isDisabled` | `(row: TRow) => boolean` | Return `true` to disable the action for a given row. | +| `onClick` | `(row: TRow) => void` | Called when the action is clicked. | + +## `createColumnHelper` + +Factory that captures the row type once and returns `column` and `inferColumns` with `TRow` already bound. Prefer this over the standalone `column()` function to avoid repeating the generic parameter. + +```tsx +const { column, inferColumns } = createColumnHelper(); +``` + +### `column(options)` + +Defines a column with an explicit render function. + +```tsx +column({ label: "Name", render: (row) => row.name }); +column({ label: "Actions", render: (row) => }); +``` + +### `inferColumns(tableMetadata)` + +Binds table metadata and returns a per-field column factory. The factory derives `label`, `sort`, `filter` config, and `id` automatically from the field's metadata. `id` is always pinned to the metadata field name — this stabilizes the React key / column-visibility identifier and enables the `truncate` tooltip without an explicit `accessor`. Requires metadata generated by `@tailor-platform/app-shell-sdk-plugin`. + +```tsx +const infer = inferColumns(tableMetadata.order); + +const columns = [ + column(infer("title")), + column(infer("status")), + column({ ...infer("createdAt"), render: (row) => formatDate(row.createdAt) }), +]; +``` + +The factory accepts an optional second argument to override per-column defaults: + +| Option | Type | Default | Description | +| -------- | --------- | ------------------------------------------- | ------------------------------------------------------------ | +| `label` | `string` | Field `description` or `name` from metadata | Override the column header text. | +| `width` | `number` | — | Fixed column width in pixels. | +| `sort` | `boolean` | `true` | Set to `false` to suppress the auto-generated sort config. | +| `filter` | `boolean` | `true` | Set to `false` to suppress the auto-generated filter config. | + +## `useCollectionVariables` + +Manages collection query state (filters, sort, pagination) and derives `variables` for GraphQL queries. + +```tsx +const { variables, control } = useCollectionVariables({ + params: { pageSize: 20 }, +}); + +// variables.pagination → { first, after? } or { last, before? } +// variables.query → filter input object or undefined +// variables.order → sort input array or undefined +``` + +### Options + +| Option | Type | Description | +| ----------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | +| `params.pageSize` | `number` | Initial page size. Default: `20`. | +| `params.initialFilters` | `Filter[]` | Filters applied on first render. | +| `params.initialSort` | `SortState[]` | Sort applied on first render. | +| `tableMetadata` | `TableMetadata` | Generated table metadata. Required for typed GraphQL documents (see [Typed query variables](#typed-query-variables)). | +| `onParamsChange` | `(params: CollectionParams) => void` | Called after each filter, sort, or page-size change with the current params. | + +### Return Value + +| Property | Type | Description | +| ----------- | --------------------- | -------------------------------------------------------------- | +| `variables` | `CollectionVariables` | Derived `query`, `order`, and `pagination` sub-properties. | +| `control` | `CollectionControl` | State and methods for filter, sort, and pagination management. | + +`useCollectionVariables` is decoupled from `DataTable` by design — the hook owns only query state and exposes plain variables. Any collection-based view (Kanban, Gantt, custom components) can use the same hook without modification. + +### Typed query variables + +When using typed GraphQL documents (`TypedDocumentNode`), pass `tableMetadata` to `useCollectionVariables`. This narrows `variables.query` and `variables.order` from `unknown` to the precise types expected by the generated document. + +```tsx +import { tableMetadata } from "@/generated/app-shell-datatable.generated"; + +const { variables, control } = useCollectionVariables({ + tableMetadata: tableMetadata.order, + params: { pageSize: 20 }, +}); + +// variables.query is now BuildQueryVariables +// variables.order is now { field: OrderableFieldName; direction: "Asc" | "Desc" }[] +const [result] = useQuery({ + query: LIST_ORDERS, // TypedDocumentNode — variables are fully type-checked + variables: { + ...variables.pagination, + query: variables.query, + order: variables.order, + }, +}); +``` + +## `useURLCollectionVariables` + +Wraps `useCollectionVariables` with automatic URL persistence. It seeds filter, sort, and page-size state from the current URL search params on mount and writes changes back to the URL as the user interacts with the table — using `replace` navigation so individual interactions don't push new history entries. + +Use this instead of `useCollectionVariables` when you want filters, sort, and pagination to survive page refreshes and be shareable via URL. + +```tsx +import { useURLCollectionVariables } from "@tailor-platform/app-shell"; + +const { variables, control } = useURLCollectionVariables({ + tableMetadata, + params: { pageSize: 20 }, +}); +``` + +The return value is identical to `useCollectionVariables` — `variables` and `control`. + +### Options + +All options accepted by `useCollectionVariables` are accepted here too. `tableMetadata` is optional but recommended for typed variables and correct URL round-tripping of typed field values (numbers and booleans are preserved correctly). + +### URL format + +| State | URL key | Example | +| ---------- | ---------------------- | --------------------- | +| `pageSize` | `p` | `?p=20` | +| Sort | `s` | `?s=createdAt:desc` | +| Filter | `f.:` | `?f.status:eq=ACTIVE` | + +### Custom search-params binding: `withURLCollectionState` + +If you need URL persistence but cannot use react-router's `useSearchParams` (e.g. a different router or test environment), use the pure `withURLCollectionState` decorator to compose URL state with `useCollectionVariables` directly: + +```tsx +import { withURLCollectionState, useCollectionVariables } from "@tailor-platform/app-shell"; + +const [searchParams, setSearchParams] = useSearchParams(); + +const { variables, control } = useCollectionVariables( + withURLCollectionState({ tableMetadata, params: { pageSize: 20 } }, [ + searchParams, + setSearchParams, + ]), +); +``` + +`withURLCollectionState` returns augmented `useCollectionVariables` options — it does not call the hook itself. The `[searchParams, setSearchParams]` tuple must match the `URLSearchParams` + setter shape that `useSearchParams()` returns. + +## `useDataTableContext` + +Accesses the full DataTable state from any component rendered inside `DataTable.Root`. Use this to build custom sub-components when the built-in ones don't fit. + +```tsx +import { useDataTableContext } from "@tailor-platform/app-shell"; + +function MyCustomPagination() { + const { pageInfo, goToNextPage, goToPrevPage, hasNextPage, hasPrevPage } = useDataTableContext(); + // ... +} +``` + +## SDK Plugin (`@tailor-platform/app-shell-sdk-plugin`) + +The SDK plugin generates `tableMetadata` from TailorDB type definitions at code-gen time. This metadata bridges your schema to the DataTable — it specifies how each field should be rendered and filtered (e.g. date pickers for datetime fields, dropdown for enum fields). + +Register the plugin in `tailor.config.ts` and run `tailor-sdk generate`: + +```ts +import { definePlugins } from "@tailor-platform/sdk"; +import { appShellPlugin } from "@tailor-platform/app-shell-sdk-plugin"; + +export const plugins = definePlugins( + appShellPlugin({ + dataTable: { + metadataOutputPath: "src/generated/app-shell-datatable.generated.ts", + }, + }), +); +``` + +The generated file exports `tableMetadata`, `tableNames`, and `TableName`. Pass `tableMetadata` to `inferColumns` to get type-safe column definitions with filter editors automatically configured: + +```ts +import { tableMetadata } from "@/generated/app-shell-datatable.generated"; +import { createColumnHelper } from "@tailor-platform/app-shell"; + +const { column, inferColumns } = createColumnHelper(); +const infer = inferColumns(tableMetadata.order); + +const columns = [ + column(infer("title")), // string → text filter + column(infer("status")), // enum → dropdown filter with generated values + column(infer("createdAt")), // datetime → date picker filter +]; +``` + +## Related + +- [CsvImporter](./csv-importer.md) — Guided CSV import flow +- [Table](./table.md) — Low-level table primitives used internally by DataTable + +## Props + +_Generated from the type surface — first-party props only._ + +### DataTablePaginationProps + +| Prop | Type | Description | +| ------------------ | ---------- | ---------------------------------------------------------------------------------------------------------- | +| `pageSizeOptions?` | `number[]` | Available page-size options shown in a dropdown selector. When provided, a page-size switcher is rendered. | + +### DataTableRootProps + +| Prop | Type | Description | +| ------------ | -------------------------- | ----------- | +| `children` | `ReactNode` | | +| `className?` | `string` | | +| `value` | `UseDataTableReturn` | | + +## Exports + +- `BuildQueryVariables` — type +- `CollectionControl` — interface +- `CollectionControlProvider` — function +- `CollectionInitialState` — type +- `CollectionParams` — interface +- `CollectionPersistedState` — interface +- `CollectionResult` — interface +- `CollectionVariables` — interface +- `Column` — type +- `DataTable` — const +- `DataTableContextValue` — interface +- `DataTableData` — interface +- `DataTablePaginationProps` — interface +- `DataTableRootProps` — interface +- `FieldMetadata` — interface +- `FieldType` — type +- `Filter` — interface +- `FilterConfig` — type +- `FilterOperator` — type +- `MetadataFieldOptions` — interface +- `NodeType` — type +- `OPERATORS_BY_FILTER_TYPE` — const +- `PageInfo` — interface +- `PaginationVariables` — interface +- `RowAction` — interface +- `SelectOption` — interface +- `SortConfig` — type +- `SortState` — interface +- `TableFieldName` — type +- `TableMetadata` — interface +- `TableMetadataFilter` — type +- `TableMetadataMap` — type +- `TableOrderableFieldName` — type +- `UseCollectionOptions` — interface +- `UseCollectionReturn` — interface +- `UseDataTableOptions` — type +- `UseDataTableReturn` — interface +- `createColumnHelper` — function +- `fieldTypeToFilterConfig` — function +- `fieldTypeToSortConfig` — function +- `useCollectionControl` — function +- `useCollectionVariables` — function +- `useDataTable` — function +- `useDataTableContext` — function +- `useURLCollectionVariables` — function +- `withURLCollectionState` — function diff --git a/docs/components/dialog.examples.tsx b/docs/components/dialog.examples.tsx new file mode 100644 index 00000000..def594a6 --- /dev/null +++ b/docs/components/dialog.examples.tsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import { Button, Dialog } from "@tailor-platform/app-shell"; + +export function BasicUsage() { + return ( + + }>Open + + + Confirm Action + Are you sure you want to proceed? + + + }>Cancel + + + + + ); +} + +export function Controlled() { + const [open, setOpen] = useState(false); + return ( + <> + + + + + Settings + + + + + + + + ); +} diff --git a/docs/components/dialog.md b/docs/components/dialog.md new file mode 100644 index 00000000..c49aa6b4 --- /dev/null +++ b/docs/components/dialog.md @@ -0,0 +1,166 @@ +--- +title: Dialog +description: Modal dialog with a compound component API +--- + + + +# Dialog + +# Dialog + +The `Dialog` component provides a modal dialog with a compound component API. It is backed by Base UI's Dialog primitive. + +## Import + +```tsx +import { Dialog } from "@tailor-platform/app-shell"; +``` + +## Basic Usage + +```tsx +function BasicUsage() { + return ( + + }>Open + + + Confirm Action + Are you sure you want to proceed? + + + }>Cancel + + + + + ); +} +``` + +## Sub-components + +| Sub-component | Description | +| -------------------- | ----------------------------------------------------------------------- | +| `Dialog.Root` | Manages open/close state | +| `Dialog.Trigger` | Element that opens the dialog when clicked | +| `Dialog.Content` | The main dialog panel (includes overlay and close button automatically) | +| `Dialog.Header` | Layout wrapper for title and description | +| `Dialog.Footer` | Layout wrapper for action buttons | +| `Dialog.Title` | Dialog title (announced by screen readers) | +| `Dialog.Description` | Additional context below the title | +| `Dialog.Close` | Button that closes the dialog | + +## Props + +### Dialog.Root Props + +| Prop | Type | Default | Description | +| -------------- | ------------------------- | ------- | --------------------------------- | +| `open` | `boolean` | - | Controlled open state | +| `defaultOpen` | `boolean` | `false` | Initial open state (uncontrolled) | +| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes | +| `modal` | `boolean` | `true` | Whether the dialog is modal | +| `children` | `React.ReactNode` | - | Dialog sub-components | + +### Dialog.Content Props + +Accepts `className` and all standard HTML `
` props. + +### Dialog.Trigger / Dialog.Close Props + +Accept a `render` prop for custom element rendering (Base UI render pattern), plus standard button props. + +## Controlled Usage + +```tsx +function Controlled() { + const [open, setOpen] = useState(false); + return ( + <> + + + + + Settings + + + + + + + + ); +} +``` + +## Examples + +### Delete Confirmation + +```tsx +function DeleteConfirmation({ onDelete }: { onDelete: () => void }) { + return ( + + }>Delete + + + Delete Order + + This action cannot be undone. The order will be permanently deleted. + + + + }>Cancel + + + + + ); +} +``` + +### Form Dialog + +```tsx +function EditDialog({ item }: { item: Item }) { + const [name, setName] = useState(item.name); + + return ( + + }>Edit + + + Edit Item + + setName(e.target.value)} /> + + }>Cancel + + + + + ); +} +``` + +## Accessibility + +- Dialog title is announced by screen readers via `Dialog.Title` +- Focus is trapped inside the dialog while open +- Pressing `Escape` closes the dialog +- The close button is always present inside `Dialog.Content` + +## Related Components + +- [Sheet](./sheet.md) - Slide-in panel alternative for non-modal workflows +- [Button](./button.md) - Use as trigger and action buttons + +## Exports + +- `Dialog` — const diff --git a/docs/concepts/styling.md b/docs/concepts/styling.md new file mode 100644 index 00000000..0d5760b8 --- /dev/null +++ b/docs/concepts/styling.md @@ -0,0 +1,59 @@ +--- +title: Styling & Tailwind setup +description: How to wire Tailwind and app-shell's theme in a consumer app +--- + + + +# Styling & Tailwind setup + +app-shell ships its own compiled styles and a set of themes. In **your** app you write **ordinary Tailwind classes** — the `astw:` prefix you may see in app-shell's own source is an **internal** prefix for the library's shipped stylesheet and is not something consumers use. + +## 1. Install Tailwind + +```bash +pnpm add -D tailwindcss @tailwindcss/vite +``` + +## 2. Add the Vite plugin + +```ts +// vite.config.ts +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], +}); +``` + +## 3. Import Tailwind + the app-shell theme + +Import Tailwind first, then app-shell's base styles, then one theme: + +```css +/* src/index.css */ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +@import "@tailor-platform/app-shell/themes/bloom"; +``` + +## 4. Use ordinary Tailwind in your code + +Your own markup uses unprefixed utilities; app-shell components bring their own styling: + +```tsx +import { Button } from "@tailor-platform/app-shell"; + +
+ + +
; +``` + +> **Do not** use the `astw:` prefix in your app — it is reserved for app-shell's internal stylesheet. If you see it in generated example code, that's a bug in the example (report it). + +## Reference implementation + +The docs explorer app (`examples/docs-app`) and the Vite example (`examples/vite-app`) are both wired exactly this way — use them as a copy-paste reference. diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json new file mode 100644 index 00000000..bfbcd09f --- /dev/null +++ b/docs/docs-manifest.json @@ -0,0 +1,160 @@ +{ + "version": 1, + "units": { + "button": { + "slug": "button", + "kind": "code-backed", + "outline": "packages/core/src/components/button.docs-outline.md", + "output": "docs/components/button.md", + "examples": "docs/components/button.examples.tsx", + "sources": ["packages/core/src/components/button.tsx"], + "claims": [], + "symbols": ["Button", "ButtonProps", "buttonVariants"], + "hashes": { + "typeSurface": "16df9927f8394548", + "outline": "e32d0264496ae851", + "snapshot": "d607a315ffce3abc", + "outputMd": "dfa0025001ed309c", + "outputExamples": "8cc0091758773a45" + } + }, + "data-table": { + "slug": "data-table", + "kind": "code-backed", + "outline": "packages/core/src/components/data-table/data-table.docs-outline.md", + "output": "docs/components/data-table.md", + "examples": "docs/components/data-table.examples.tsx", + "sources": [ + "packages/core/src/components/data-table/**", + "packages/core/src/hooks/use-collection-variables.ts", + "packages/core/src/lib/collection-url-state.ts", + "packages/core/src/contexts/collection-control-context.tsx", + "packages/core/src/types/collection.ts" + ], + "claims": [], + "symbols": [ + "BuildQueryVariables", + "CollectionControl", + "CollectionControlProvider", + "CollectionInitialState", + "CollectionParams", + "CollectionPersistedState", + "CollectionResult", + "CollectionVariables", + "Column", + "DataTable", + "DataTableContextValue", + "DataTableData", + "DataTablePaginationProps", + "DataTableRootProps", + "FieldMetadata", + "FieldType", + "Filter", + "FilterConfig", + "FilterOperator", + "MetadataFieldOptions", + "NodeType", + "OPERATORS_BY_FILTER_TYPE", + "PageInfo", + "PaginationVariables", + "RowAction", + "SelectOption", + "SortConfig", + "SortState", + "TableFieldName", + "TableMetadata", + "TableMetadataFilter", + "TableMetadataMap", + "TableOrderableFieldName", + "UseCollectionOptions", + "UseCollectionReturn", + "UseDataTableOptions", + "UseDataTableReturn", + "createColumnHelper", + "fieldTypeToFilterConfig", + "fieldTypeToSortConfig", + "useCollectionControl", + "useCollectionVariables", + "useDataTable", + "useDataTableContext", + "useURLCollectionVariables", + "withURLCollectionState" + ], + "hashes": { + "typeSurface": "b74872a25bf644f3", + "outline": "316243eb4a84e13d", + "snapshot": null, + "outputMd": "89eb77127670e7e6", + "outputExamples": "c4c02203e17221eb" + } + }, + "dialog": { + "slug": "dialog", + "kind": "code-backed", + "outline": "packages/core/src/components/dialog.docs-outline.md", + "output": "docs/components/dialog.md", + "examples": "docs/components/dialog.examples.tsx", + "sources": ["packages/core/src/components/dialog.tsx"], + "claims": [], + "symbols": ["Dialog"], + "hashes": { + "typeSurface": "b1614970f950ceb6", + "outline": "a8f69aa1e5eeb3a2", + "snapshot": "125a10821f041e34", + "outputMd": "c82b34f679f0e4da", + "outputExamples": "934c52d4e8761404" + } + }, + "form-modal": { + "slug": "form-modal", + "kind": "prose", + "outline": "docs-src/patterns/form-modal.docs-outline.md", + "output": "docs/patterns/form-modal.md", + "examples": "docs/patterns/form-modal.examples.tsx", + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "dc2d8147c3ae5670", + "snapshot": null, + "outputMd": "cbce5a585c5e50a9", + "outputExamples": "5851d00933ce0062" + } + }, + "list-dense-scan": { + "slug": "list-dense-scan", + "kind": "prose", + "outline": "docs-src/patterns/list-dense-scan.docs-outline.md", + "output": "docs/patterns/list-dense-scan.md", + "examples": "docs/patterns/list-dense-scan.examples.tsx", + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "3025ac6274b38bbb", + "snapshot": null, + "outputMd": "a29818b09de2cc20", + "outputExamples": "6e46b2f7bab2eaf6" + } + }, + "styling": { + "slug": "styling", + "kind": "prose", + "outline": "docs-src/concepts/styling.docs-outline.md", + "output": "docs/concepts/styling.md", + "examples": null, + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "057d44a8467a93c1", + "snapshot": null, + "outputMd": "57d1a9bcc0e74cd5", + "outputExamples": null + } + } + } +} diff --git a/docs/patterns/form-modal.examples.tsx b/docs/patterns/form-modal.examples.tsx index 2dbe9fd3..e9bf9fe9 100644 --- a/docs/patterns/form-modal.examples.tsx +++ b/docs/patterns/form-modal.examples.tsx @@ -1,11 +1,6 @@ -/* pattern: form/modal */ -import { Button, Dialog, Input, Field } from "@tailor-platform/app-shell"; +import { Button, Dialog, Field, Input } from "@tailor-platform/app-shell"; -type Props = { - onSave: (data: { label: string; street: string; city: string }) => void; -}; - -export default function ModalForm({ onSave }: Props) { +export function ModalForm() { return ( }>Add address @@ -15,17 +10,13 @@ export default function ModalForm({ onSave }: Props) { Add a shipping address to this order.
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - onSave({ - label: formData.get("label") as string, - street: formData.get("street") as string, - city: formData.get("city") as string, - }); + onSubmit={(event) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + window.alert(`Saving ${data.get("label") ?? ""}`); }} > -
+
Label } /> diff --git a/docs/patterns/form-modal.md b/docs/patterns/form-modal.md new file mode 100644 index 00000000..44c0b47c --- /dev/null +++ b/docs/patterns/form-modal.md @@ -0,0 +1,79 @@ +--- +title: Modal Form +description: Default form pattern for Create/Edit — keeps user in context on the parent screen +--- + + + +# Modal Form + +# pattern/form/modal + +## When to Use + +- Default for most Create and Edit forms — keeps user in context on parent screen +- Inline add of a related entity from another screen (add address from order detail) +- Quick configuration changes and single-purpose forms (rename, change status) +- Any form the design hasn't explicitly called out as a full-page routed screen + +## Page Implementation + +```tsx +function ModalForm() { + return ( + + }>Add address + + + Add address + Add a shipping address to this order. + + { + event.preventDefault(); + const data = new FormData(event.currentTarget); + window.alert(`Saving ${data.get("label") ?? ""}`); + }} + > +
+ + Label + } /> + + + Street + } /> + + + City + } /> + +
+ + }>Cancel + + + +
+
+ ); +} +``` + +## Route-driven Variant + +The route-driven variant renders the same component at both the parent path and the `create` / `edit` path, with `onOpenChange` navigating back so the URL stays in sync. + +## Constraints + +- Dialog renders full-screen sheet below 1024px; centered max-w-md at 1024–1280px +- Route-driven variant requires both parent path and create/edit path to render the same component +- `onOpenChange` must navigate back — just calling `setOpen(false)` leaves the URL broken + +## Anti-patterns + +- Nesting modals — opening a Dialog from inside another Dialog +- Modal containing a wizard — promote to a routed `form/wizard` +- Save closes the dialog but parent state is stale — wire refetch or optimistic update +- Building a routed Create/Edit page when the design didn't explicitly call for one — modal is the default +- Registering the create path as a separate top-level route — that unmounts the parent list diff --git a/docs/patterns/list-dense-scan.examples.tsx b/docs/patterns/list-dense-scan.examples.tsx index 80818cc4..cba449a5 100644 --- a/docs/patterns/list-dense-scan.examples.tsx +++ b/docs/patterns/list-dense-scan.examples.tsx @@ -1,27 +1,85 @@ -/* pattern: list/dense-scan */ -import { DataTable, Layout, useDataTable, Button, Input } from "@tailor-platform/app-shell"; -import type { Order } from "./columns"; -import { columns } from "./columns"; -import type { DataTableData } from "@tailor-platform/app-shell"; +import { Badge, Button, DataTable, Input, Layout, useDataTable } from "@tailor-platform/app-shell"; +import type { Column } from "@tailor-platform/app-shell"; -type Props = { - data: DataTableData; - onCreateClick: () => void; +type Order = { + id: string; + orderNumber: string; + customer: string; + status: "draft" | "confirmed" | "shipped" | "delivered"; + amount: number; + createdAt: string; }; -export default function DenseScanList({ data, onCreateClick }: Props) { - const table = useDataTable({ data, columns }); +const statusVariant = { + draft: "neutral", + confirmed: "info", + shipped: "warning", + delivered: "success", +} as const; + +const columns: Column[] = [ + { label: "Order #", accessor: (row) => row.orderNumber }, + { label: "Customer", accessor: (row) => row.customer }, + { + label: "Status", + render: (row) => {row.status}, + }, + { label: "Amount", align: "right", render: (row) => `$${row.amount.toLocaleString()}` }, + { label: "Created", accessor: (row) => row.createdAt }, +]; + +const orders: Order[] = [ + { + id: "1", + orderNumber: "ORD-001", + customer: "Acme Corp", + status: "confirmed", + amount: 1500, + createdAt: "2026-01-15", + }, + { + id: "2", + orderNumber: "ORD-002", + customer: "Globex Inc", + status: "draft", + amount: 3200, + createdAt: "2026-01-16", + }, + { + id: "3", + orderNumber: "ORD-003", + customer: "Initech", + status: "shipped", + amount: 890, + createdAt: "2026-01-17", + }, + { + id: "4", + orderNumber: "ORD-004", + customer: "Umbrella Corp", + status: "delivered", + amount: 4200, + createdAt: "2026-01-18", + }, + { + id: "5", + orderNumber: "ORD-005", + customer: "Stark Industries", + status: "confirmed", + amount: 7800, + createdAt: "2026-01-19", + }, +]; + +export function DenseScan() { + const table = useDataTable({ data: { rows: orders, total: orders.length }, columns }); return ( - // `fill` pins the page chrome for table-first pages: the title, toolbar, - // column header row, and pagination footer stay visible at every viewport - // height — only the table's rows region scrolls. Omit `fill` on pages - // that should flow and scroll naturally (forms, dashboards, articles). + , ]} diff --git a/docs/patterns/list-dense-scan.md b/docs/patterns/list-dense-scan.md new file mode 100644 index 00000000..bccb7901 --- /dev/null +++ b/docs/patterns/list-dense-scan.md @@ -0,0 +1,93 @@ +--- +title: Dense Scan List +description: High-density scannable list backed by GraphQL connections with DataTable, sort, filters, and pagination +--- + + + +# Dense Scan List + +# pattern/list/dense-scan + +## When to Use + +- Browsing many records of one entity type (orders, POs, products, invoices) with GraphQL pagination +- Operators sort, filter, and select rows; row click navigates to detail +- Optionally: a bucket control (`Tabs`, segmented buttons) aligned to one categorical dimension the business cares about (status, fulfillment stage, type) + +## Column Definition + +Columns are defined with `createColumnHelper` (see the example below) — each column has a `label` and an `accessor`, or a custom `render` for cells like status badges. + +## Page Implementation + +```tsx +function DenseScan() { + const table = useDataTable({ data: { rows: orders, total: orders.length }, columns }); + + return ( + + + Create Order + , + ]} + /> + + + + + + + + + + + + + ); +} +``` + +## Page Layout & Internal Scrolling + +Table-first pages should pin their chrome and scroll only the rows region. Wrap the page in ``: + +- `fill` stretches the layout to the available height and bounds the column row, so the `DataTable` shrinks to fit instead of growing past the viewport +- The `Layout.Header` (title/actions), `DataTable.Toolbar`, the column header row (sticky), and `DataTable.Footer` (pagination) stay visible at every viewport height — only the rows scroll vertically +- When the current page of rows fits, nothing stretches and no scrollbar appears — short tables render identically with or without `fill` +- Requires no extra styling on the page: the height chain (`AppShell` content area → `Layout fill` → `Layout.Column` → `DataTable.Root`) is wired by the components + +Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, articles) — the AppShell content area scrolls those. + +## Variants + +- **Toolbar chips only (`DataTable.Filters`)** — best when filters map cleanly to typed column metadata / enum facets +- **Tabs only above `DataTable`** — best when workflows are organized as obvious buckets +- **Tabs + chips** — when buckets are primary and finer filters help +- **Bulk selection** — `onSelectionChange` hook on `useDataTable`; combine with `interaction/multi-select` +- **`Table` primitives** — small static lists without collection hooks + +## Constraints + +- Column count: 4-8 recommended +- Must include pagination — never render unbounded lists +- Table-first pages use `` so title/toolbar/header/footer stay pinned and only rows scroll +- Handle every state: `DataTable` renders the loading skeleton and error row; always provide a **labelled empty state** (what the list is + how to add the first record) rather than a bare empty table +- Status Badge colors must use design system tokens (variant prop): the **primary** status column uses **filled** semantic variants; **secondary** status columns (delivery, billing) use **`outline-*`** (see `design-system.md` → Composition & emphasis rules) +- Bulk actions toolbar appears only when ≥1 row is selected +- Whole row is clickable via `onClickRow`; wrap the primary identifier cell in `` for keyboard/SR access. No per-row "View" / "Open" buttons +- Per-row `Menu` (overflow `…`) is reserved for non-navigation actions (Archive, Duplicate, Delete) +- Wide lists (many columns / horizontal scroll): pin the column users scan by to the **left** — the record's identifier (invoice / order **reference**) or its **name** (customer, product) — so it stays anchored while the rest scrolls; optionally pin a single high-signal **status** or **total** to the **right**. Keep the pinned set small (≈1 left, at most 1 right) — over-pinning eats the scroll area. Let users override via `` (show/hide + reorder + re-pin) with a stable `tableId` so each user's layout persists + +## Anti-patterns + +- Building a bespoke table + custom pagination instead of `DataTable` + `useCollectionVariables` +- Hand-rolled `max-height`/`overflow` wrappers around `DataTable` to contain scrolling — use `` instead +- Tabs that mutate only local UI state while pagination/filters assume the full server set +- Using `
` directly instead of `` for live collections +- Client-side filtering on 1000+ records without server-side support +- Inline editable cells — use `pattern/detail/*` or `pattern/form/modal` instead +- Per-row "View" / "Open" buttons duplicating the row-click navigation diff --git a/examples/docs-app/index.html b/examples/docs-app/index.html new file mode 100644 index 00000000..0018baf8 --- /dev/null +++ b/examples/docs-app/index.html @@ -0,0 +1,12 @@ + + + + + + app-shell docs + + +
+ + + diff --git a/examples/docs-app/package.json b/examples/docs-app/package.json new file mode 100644 index 00000000..c24fe2b3 --- /dev/null +++ b/examples/docs-app/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tailor-platform/app-shell-docs-app", + "private": true, + "description": "Docs explorer — renders the generated docs/ (markdown + live examples) in place. Dogfoods app-shell. SKELETON: see TODOs before shipping.", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@tailor-platform/app-shell": "workspace:*", + "@tailor-platform/app-shell-vite-plugin": "workspace:*", + "lucide-react": "catalog:", + "react": "catalog:", + "react-dom": "catalog:", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0", + "tailwindcss": "catalog:" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.0", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "catalog:", + "typescript": "^5", + "vite": "catalog:" + } +} diff --git a/examples/docs-app/src/App.tsx b/examples/docs-app/src/App.tsx new file mode 100644 index 00000000..ba8de39a --- /dev/null +++ b/examples/docs-app/src/App.tsx @@ -0,0 +1,14 @@ +import { AppShell, DefaultSidebar, SidebarLayout } from "@tailor-platform/app-shell"; + +// The docs explorer is a real AppShell app. Routes and the sidebar are inferred +// automatically from the file structure under src/pages/ (file-based routing + +// auto-generation mode) — no `modules` prop, no manual SidebarItems. +const App = () => { + return ( + + } /> + + ); +}; + +export default App; diff --git a/examples/docs-app/src/_lib/DocPage.tsx b/examples/docs-app/src/_lib/DocPage.tsx new file mode 100644 index 00000000..27f8b628 --- /dev/null +++ b/examples/docs-app/src/_lib/DocPage.tsx @@ -0,0 +1,48 @@ +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +import { Layout } from "@tailor-platform/app-shell"; + +import { units } from "../docs"; + +// Shared renderer for a single documented unit: prose (markdown) + live examples. +// Lives under _lib/ so file-based routing does not treat it as a page. +export function DocPage({ slug }: { slug: string }) { + const unit = units.find((u) => u.slug === slug); + + if (!unit) { + return ( + + + Unknown doc unit: {slug} + + ); + } + + return ( + + + +
+ {unit.markdown} +
+ + {unit.examples.length > 0 && ( +
+

Live examples

+ {unit.examples.map((example) => ( +
+
+ +
+
+ {example.name} +
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/examples/docs-app/src/docs.tsx b/examples/docs-app/src/docs.tsx new file mode 100644 index 00000000..3dae6b4e --- /dev/null +++ b/examples/docs-app/src/docs.tsx @@ -0,0 +1,71 @@ +import type { ComponentType } from "react"; + +import manifestJson from "../../../docs/docs-manifest.json"; + +// Glob the generated docs IN PLACE (repo-root docs/, outside this app). Vite +// resolves these at build time; in dev they are served straight from source. +// NOTE: import.meta.glob's options must be an inline object literal — Vite +// parses them statically, so a shared const is rejected. +const markdown = import.meta.glob("../../../docs/**/*.md", { + query: "?raw", + import: "default", + eager: true, +}) as Record; +const exampleSource = import.meta.glob("../../../docs/**/*.examples.tsx", { + query: "?raw", + import: "default", + eager: true, +}) as Record; +const exampleModule = import.meta.glob("../../../docs/**/*.examples.tsx", { + eager: true, +}) as Record>; + +interface ManifestEntry { + slug: string; + kind: string; + output: string; + examples: string | null; +} +const manifest = manifestJson as { units: Record }; + +export interface Example { + name: string; + Component: ComponentType; +} + +export interface DocUnit { + slug: string; + kind: string; + category: string; + markdown: string; + source: string | null; + examples: Example[]; +} + +const key = (repoRel: string): string => `../../../${repoRel}`; + +/** Strip the generated frontmatter block and HTML comments before rendering — + * react-markdown would otherwise show them as literal text. */ +function cleanMarkdown(md: string): string { + return md + .replace(/^---\n[\s\S]*?\n---\n/, "") + .replace(//g, "") + .trim(); +} + +export const units: DocUnit[] = Object.values(manifest.units).map((entry) => { + const mod = entry.examples ? exampleModule[key(entry.examples)] : undefined; + const examples: Example[] = mod + ? Object.entries(mod) + .filter(([, value]) => typeof value === "function") + .map(([name, value]) => ({ name, Component: value as ComponentType })) + : []; + return { + slug: entry.slug, + kind: entry.kind, + category: entry.output.split("/")[1] ?? "misc", + markdown: cleanMarkdown(markdown[key(entry.output)] ?? ""), + source: entry.examples ? (exampleSource[key(entry.examples)] ?? null) : null, + examples, + }; +}); diff --git a/examples/docs-app/src/index.css b/examples/docs-app/src/index.css new file mode 100644 index 00000000..e1e5a597 --- /dev/null +++ b/examples/docs-app/src/index.css @@ -0,0 +1,14 @@ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +@import "@tailor-platform/app-shell/themes/bloom"; + +/* The live examples live at the repo root (docs/), outside this app's own tree, + so Tailwind won't discover their utility classes by default. Point it at them + explicitly so classes like `flex gap-2` used in examples get generated. */ +@source "../../../docs/**/*.examples.tsx"; + +html, +body { + margin: 0; + padding: 0; +} diff --git a/examples/docs-app/src/main.tsx b/examples/docs-app/src/main.tsx new file mode 100644 index 00000000..d6a9a8ac --- /dev/null +++ b/examples/docs-app/src/main.tsx @@ -0,0 +1,14 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + +// Dogfood: the docs explorer is styled by the same theme it documents. +// index.css wires Tailwind + app-shell styles + theme (the canonical consumer setup). +import "./index.css"; + +import App from "./App"; + +createRoot(document.getElementById("root") as HTMLElement).render( + + + , +); diff --git a/examples/docs-app/src/pages/components/button/page.tsx b/examples/docs-app/src/pages/components/button/page.tsx new file mode 100644 index 00000000..e639dd35 --- /dev/null +++ b/examples/docs-app/src/pages/components/button/page.tsx @@ -0,0 +1,11 @@ +import type { AppShellPageProps } from "@tailor-platform/app-shell"; + +import { DocPage } from "../../../_lib/DocPage"; + +const Page = () => ; + +Page.appShellPageProps = { + meta: { title: "Button" }, +} satisfies AppShellPageProps; + +export default Page; diff --git a/examples/docs-app/src/pages/components/data-table/page.tsx b/examples/docs-app/src/pages/components/data-table/page.tsx new file mode 100644 index 00000000..ef96c91b --- /dev/null +++ b/examples/docs-app/src/pages/components/data-table/page.tsx @@ -0,0 +1,11 @@ +import type { AppShellPageProps } from "@tailor-platform/app-shell"; + +import { DocPage } from "../../../_lib/DocPage"; + +const Page = () => ; + +Page.appShellPageProps = { + meta: { title: "DataTable" }, +} satisfies AppShellPageProps; + +export default Page; diff --git a/examples/docs-app/src/pages/components/dialog/page.tsx b/examples/docs-app/src/pages/components/dialog/page.tsx new file mode 100644 index 00000000..64a9dcf0 --- /dev/null +++ b/examples/docs-app/src/pages/components/dialog/page.tsx @@ -0,0 +1,11 @@ +import type { AppShellPageProps } from "@tailor-platform/app-shell"; + +import { DocPage } from "../../../_lib/DocPage"; + +const Page = () => ; + +Page.appShellPageProps = { + meta: { title: "Dialog" }, +} satisfies AppShellPageProps; + +export default Page; diff --git a/examples/docs-app/src/pages/concepts/styling/page.tsx b/examples/docs-app/src/pages/concepts/styling/page.tsx new file mode 100644 index 00000000..62761945 --- /dev/null +++ b/examples/docs-app/src/pages/concepts/styling/page.tsx @@ -0,0 +1,11 @@ +import type { AppShellPageProps } from "@tailor-platform/app-shell"; + +import { DocPage } from "../../../_lib/DocPage"; + +const Page = () => ; + +Page.appShellPageProps = { + meta: { title: "Styling & Tailwind" }, +} satisfies AppShellPageProps; + +export default Page; diff --git a/examples/docs-app/src/pages/page.tsx b/examples/docs-app/src/pages/page.tsx new file mode 100644 index 00000000..30b7487a --- /dev/null +++ b/examples/docs-app/src/pages/page.tsx @@ -0,0 +1,22 @@ +import { type AppShellPageProps, Layout } from "@tailor-platform/app-shell"; + +const HomePage = () => { + return ( + + + +

+ Browse components, patterns, and concepts from the sidebar. Every page is generated from a + *.docs-outline.md + source and renders live, in-place examples. +

+
+
+ ); +}; + +HomePage.appShellPageProps = { + meta: { title: "Home" }, +} satisfies AppShellPageProps; + +export default HomePage; diff --git a/examples/docs-app/src/pages/patterns/form-modal/page.tsx b/examples/docs-app/src/pages/patterns/form-modal/page.tsx new file mode 100644 index 00000000..c765fbfa --- /dev/null +++ b/examples/docs-app/src/pages/patterns/form-modal/page.tsx @@ -0,0 +1,11 @@ +import type { AppShellPageProps } from "@tailor-platform/app-shell"; + +import { DocPage } from "../../../_lib/DocPage"; + +const Page = () => ; + +Page.appShellPageProps = { + meta: { title: "Modal form" }, +} satisfies AppShellPageProps; + +export default Page; diff --git a/examples/docs-app/src/pages/patterns/list-dense-scan/page.tsx b/examples/docs-app/src/pages/patterns/list-dense-scan/page.tsx new file mode 100644 index 00000000..2291100c --- /dev/null +++ b/examples/docs-app/src/pages/patterns/list-dense-scan/page.tsx @@ -0,0 +1,11 @@ +import type { AppShellPageProps } from "@tailor-platform/app-shell"; + +import { DocPage } from "../../../_lib/DocPage"; + +const Page = () => ; + +Page.appShellPageProps = { + meta: { title: "Dense scan list" }, +} satisfies AppShellPageProps; + +export default Page; diff --git a/examples/docs-app/tsconfig.json b/examples/docs-app/tsconfig.json new file mode 100644 index 00000000..89bd59ce --- /dev/null +++ b/examples/docs-app/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true, + "types": ["vite/client", "node"], + "baseUrl": ".", + "paths": { + "@tailor-platform/app-shell": [ + "./node_modules/@tailor-platform/app-shell/dist/app-shell.d.ts" + ], + "react": ["./node_modules/@types/react"], + "react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime"], + "react-dom": ["./node_modules/@types/react-dom"], + "react-dom/client": ["./node_modules/@types/react-dom/client"], + "lucide-react": ["./node_modules/lucide-react"] + } + }, + "comment": "The generated examples live at repo-root docs/ (outside this package) and are imported in place. baseUrl/paths make their bare imports resolve to this app's deps so tsc can type-check them against core — the CI 'examples compile against core' gate.", + "include": ["src", "vite.config.ts", "../../docs/**/*.examples.tsx"] +} diff --git a/examples/docs-app/vite.config.ts b/examples/docs-app/vite.config.ts new file mode 100644 index 00000000..053b323b --- /dev/null +++ b/examples/docs-app/vite.config.ts @@ -0,0 +1,57 @@ +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { appShellRoutes } from "@tailor-platform/app-shell/vite-plugin"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig, type Plugin } from "vite"; + +const repoRoot = fileURLToPath(new URL("../..", import.meta.url)); +const appRoot = fileURLToPath(new URL(".", import.meta.url)); +const docsDir = resolve(repoRoot, "docs"); + +// The generated examples live at repo-root docs/ (outside this app), so their +// bare imports don't resolve against this app's node_modules. Resolve them as +// if imported from this app — SCOPED to docs/ importers so it never touches +// App.tsx, whose `@tailor-platform/app-shell` import the routing plugin +// intercepts (via `entrypoint`) to inject the file-based pages. +function resolveExampleDeps(): Plugin { + const EXAMPLE_DEPS = /^(@tailor-platform\/app-shell|react|react-dom|lucide-react)(\/|$)/; + return { + name: "docs-app:resolve-example-deps", + enforce: "pre", + async resolveId(source, importer) { + if (!importer || !importer.startsWith(docsDir)) return null; + if (!EXAMPLE_DEPS.test(source)) return null; + const resolved = await this.resolve(source, resolve(appRoot, "src/main.tsx"), { + skipSelf: true, + }); + return resolved?.id ?? null; + }, + }; +} + +export default defineConfig({ + plugins: [ + resolveExampleDeps(), + react(), + tailwindcss(), + // File-based routing: routes + sidebar are inferred from src/pages/. + appShellRoutes({ entrypoint: "src/App.tsx" }), + ], + resolve: { + // Core's built bundle imports the vite-plugin parser; alias it to the built + // output so it resolves from wherever the bundle is loaded. + alias: [ + { + find: /^@tailor-platform\/app-shell-vite-plugin\/parser$/, + replacement: resolve(repoRoot, "packages/vite-plugin/dist/parser.mjs"), + }, + ], + dedupe: ["react", "react-dom"], + }, + server: { + port: 5177, + fs: { allow: [repoRoot] }, + }, +}); diff --git a/package.json b/package.json index acd2f303..83653c1e 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "type-check": "turbo type-check", "lint": "turbo lint", "test": "turbo test", + "docs:sync": "node packages/docs-kit/dist/cli.mjs sync --root .", + "docs:check": "node packages/docs-kit/dist/cli.mjs check --root .", "fmt": "oxfmt", "fmt:check": "oxfmt --check", "changeset:create": "changeset", diff --git a/packages/core/src/components/button.docs-outline.md b/packages/core/src/components/button.docs-outline.md index ff94d548..973f94f1 100644 --- a/packages/core/src/components/button.docs-outline.md +++ b/packages/core/src/components/button.docs-outline.md @@ -1,6 +1,9 @@ --- title: Button description: Styled button with multiple variants and sizes +group: button +sources: + - packages/core/src/components/button.tsx --- # Button @@ -15,11 +18,7 @@ import { Button } from "@tailor-platform/app-shell"; ## Basic Usage -```tsx - - - -``` + ## Props @@ -35,24 +34,11 @@ All standard HTML ` - - - - - -``` + ## Sizes -```tsx - - - - - -``` + ## Render Prop diff --git a/packages/core/src/components/data-table/data-table.docs-outline.md b/packages/core/src/components/data-table/data-table.docs-outline.md index d17c7e59..f5963c27 100644 --- a/packages/core/src/components/data-table/data-table.docs-outline.md +++ b/packages/core/src/components/data-table/data-table.docs-outline.md @@ -1,6 +1,13 @@ --- title: DataTable description: Compound data table component with sortable columns, filter chips, cursor-based pagination, row actions, and multi-row selection +group: data-table +sources: + - packages/core/src/components/data-table/** + - packages/core/src/hooks/use-collection-variables.ts + - packages/core/src/lib/collection-url-state.ts + - packages/core/src/contexts/collection-control-context.tsx + - packages/core/src/types/collection.ts --- # DataTable @@ -138,6 +145,12 @@ function JournalsPage() { } ``` +## Live example + +A self-contained `DataTable` with static rows (no GraphQL) demonstrating the same compound API: + + + ## Sub-components `DataTable` is a namespace object. All sub-components read state from `DataTable.Root` via context. diff --git a/packages/core/src/components/dialog.docs-outline.md b/packages/core/src/components/dialog.docs-outline.md index 1fb1e74c..7bac7114 100644 --- a/packages/core/src/components/dialog.docs-outline.md +++ b/packages/core/src/components/dialog.docs-outline.md @@ -1,6 +1,9 @@ --- title: Dialog description: Modal dialog with a compound component API +group: dialog +sources: + - packages/core/src/components/dialog.tsx --- # Dialog @@ -15,21 +18,7 @@ import { Dialog } from "@tailor-platform/app-shell"; ## Basic Usage -```tsx - - }>Open - - - Confirm Action - Are you sure you want to proceed? - - - }>Cancel - - - - -``` + ## Sub-components @@ -66,21 +55,7 @@ Accept a `render` prop for custom element rendering (Base UI render pattern), pl ## Controlled Usage -```tsx -const [open, setOpen] = useState(false); - - - }>Open Dialog - - - Settings - - - - - -; -``` + ## Examples diff --git a/packages/docs-kit/.oxlintrc.jsonc b/packages/docs-kit/.oxlintrc.jsonc new file mode 100644 index 00000000..cf68e211 --- /dev/null +++ b/packages/docs-kit/.oxlintrc.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "../../node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn", "oxc"], + "categories": { + "correctness": "error", + "suspicious": "error", + }, + "ignorePatterns": ["dist", "node_modules"], +} diff --git a/packages/docs-kit/package.json b/packages/docs-kit/package.json new file mode 100644 index 00000000..af23e20b --- /dev/null +++ b/packages/docs-kit/package.json @@ -0,0 +1,35 @@ +{ + "name": "@tailor-platform/app-shell-docs-kit", + "version": "0.0.0", + "private": true, + "description": "Internal tooling for the app-shell docs pipeline: type-surface extraction, drift detection, and deterministic markdown assembly. Never published.", + "bin": { + "docs-kit": "./dist/cli.mjs" + }, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + } + }, + "scripts": { + "build": "tsdown", + "dev": "tsdown --watch", + "lint": "oxlint -c .oxlintrc.jsonc", + "test": "vitest run", + "type-check": "tsc --incremental" + }, + "dependencies": { + "gray-matter": "^4.0.3", + "picomatch": "^4.0.2", + "ts-morph": "^28.0.0" + }, + "devDependencies": { + "@types/node": "catalog:", + "@types/picomatch": "^4.0.0", + "tsdown": "catalog:", + "typescript": "^5", + "vitest": "catalog:" + } +} diff --git a/packages/docs-kit/src/assemble.ts b/packages/docs-kit/src/assemble.ts new file mode 100644 index 00000000..e1388a1b --- /dev/null +++ b/packages/docs-kit/src/assemble.ts @@ -0,0 +1,142 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +import { Project } from "ts-morph"; + +import type { Surface } from "./project"; +import type { Outline, UnitKind } from "./types"; + +const EXAMPLE_TOKEN = //g; + +function titleCase(slug: string): string { + return slug + .split(/[-_]/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} + +function pascal(key: string): string { + return key + .split(/[-_]/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(""); +} + +function stripExport(src: string): string { + return src.replace(/^export\s+/, ""); +} + +/** Parse an authored examples module syntactically (no type resolution needed) + * and return each exported example's source text, keyed by its export name. */ +function extractExamples(examplesAbsPath: string): Map { + const map = new Map(); + if (!existsSync(examplesAbsPath)) return map; + const project = new Project({ skipAddingFilesFromTsConfig: true, skipLoadingLibFiles: true }); + const sf = project.addSourceFileAtPath(examplesAbsPath); + for (const fn of sf.getFunctions()) { + const name = fn.getName(); + if (fn.isExported() && name) map.set(name, fn.getText()); + } + for (const v of sf.getVariableStatements()) { + if (!v.isExported()) continue; + for (const decl of v.getDeclarations()) map.set(decl.getName(), v.getText()); + } + return map; +} + +const KIND_LABEL: Record = { + FunctionDeclaration: "function", + VariableDeclaration: "const", + TypeAliasDeclaration: "type", + InterfaceDeclaration: "interface", + ClassDeclaration: "class", + EnumDeclaration: "enum", +}; + +/** Escape a value for a single markdown table cell: collapse whitespace and + * escape pipes so union types like `"a" | "b"` don't break the table. */ +function cell(text: string): string { + return text.replace(/\s+/g, " ").replace(/\|/g, "\\|").trim(); +} + +function renderApi(surface: Surface, owned: string[], kind: UnitKind, upstream?: string): string { + if (kind === "reference") { + const lines = owned.map((n) => `- \`${n}\``).join("\n"); + const link = upstream ? `[upstream documentation](${upstream})` : "the upstream documentation"; + return `## Re-exported API\n\nThese symbols are re-exported by \`@tailor-platform/app-shell\` for convenience. See ${link} for full details.\n\n${lines}`; + } + if (kind !== "code-backed" || owned.length === 0) return ""; + + const sections: string[] = []; + + // Auto-generated prop tables for every owned `*Props` symbol. + const tables: string[] = []; + for (const name of owned) { + const sym = surface.symbols.get(name); + if (!sym?.props?.length) continue; + const head = "| Prop | Type | Description |\n| --- | --- | --- |"; + const body = sym.props + .map( + (p) => + `| \`${p.name}${p.optional ? "?" : ""}\` | \`${cell(p.type)}\` | ${cell(p.description)} |`, + ) + .join("\n"); + tables.push(`### ${name}\n\n${head}\n${body}`); + } + if (tables.length > 0) { + sections.push( + `## Props\n\n_Generated from the type surface — first-party props only._\n\n${tables.join("\n\n")}`, + ); + } + + const list = owned + .map((n) => { + const s = surface.symbols.get(n); + return `- \`${n}\` — ${KIND_LABEL[s?.kind ?? ""] ?? s?.kind ?? "unknown"}`; + }) + .join("\n"); + sections.push(`## Exports\n\n${list}`); + + return sections.join("\n\n"); +} + +/** Deterministically assemble the output markdown from an outline, its authored + * examples module, and the resolved type surface. No LLM involved. */ +export function assembleMarkdown(opts: { + repoRoot: string; + outline: Outline; + surface: Surface; + owned: string[]; +}): string { + const { repoRoot, outline, surface, owned } = opts; + const examples = extractExamples(join(repoRoot, outline.examplesPath)); + + const body = outline.body.replace(EXAMPLE_TOKEN, (_match, key: string) => { + const name = pascal(key); + const src = examples.get(name); + if (!src) { + throw new Error( + `docs-kit: ${outline.examplesPath} has no exported "${name}" for example token "${key}"`, + ); + } + return "```tsx\n" + stripExport(src).trim() + "\n```"; + }); + + const title = outline.frontmatter.title ?? titleCase(outline.slug); + const description = outline.frontmatter.description ?? ""; + const front = `---\ntitle: ${title}\ndescription: ${description}\n---`; + const note = ``; + const api = renderApi(surface, owned, outline.kind, outline.frontmatter.upstream); + + // Inject the generated API block at an explicit `` token if the + // outline places one; otherwise append it at the end. + const API_TOKEN = //; + let composed: string; + if (API_TOKEN.test(body)) { + composed = body.replace(API_TOKEN, api).trim(); + } else { + composed = api ? `${body.trim()}\n\n${api}` : body.trim(); + } + + return [front, note, `# ${title}`, composed].filter(Boolean).join("\n\n") + "\n"; +} diff --git a/packages/docs-kit/src/check.ts b/packages/docs-kit/src/check.ts new file mode 100644 index 00000000..3900bcde --- /dev/null +++ b/packages/docs-kit/src/check.ts @@ -0,0 +1,93 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { loadConfig } from "./config"; +import { type Finding, reconcile } from "./coverage"; +import { hash, normalizeText } from "./hash"; +import { readManifest } from "./manifest"; +import { discoverOutlines } from "./outline"; +import { loadSurface, snapshotHashForSlug } from "./project"; + +export interface CheckResult { + findings: Finding[]; + ok: boolean; +} + +/** Deterministic drift gate. No LLM, no writes. Exit non-zero on any block. */ +export function check(repoRoot: string): CheckResult { + const config = loadConfig(repoRoot); + const outlines = discoverOutlines(repoRoot, config); + const surface = loadSurface(repoRoot, config); + const { findings: reconcileFindings, ownedBySlug } = reconcile(surface, outlines, config); + const findings: Finding[] = [...reconcileFindings]; + const manifest = readManifest(repoRoot, config.manifestFile); + + for (const outline of outlines) { + const { slug } = outline; + const entry = manifest?.units[slug]; + const owned = ownedBySlug.get(slug) ?? []; + + if (!entry) { + findings.push({ + level: "block", + slug, + message: `no manifest entry — run \`docs-kit sync\`.`, + }); + continue; + } + + const curOutline = hash(normalizeText(readFileSync(outline.outlinePath, "utf8"))); + const curType = outline.kind === "code-backed" ? surface.hashSymbols(owned) : null; + const curSnap = + outline.kind === "code-backed" ? snapshotHashForSlug(repoRoot, config, slug) : null; + + if (curType !== entry.hashes.typeSurface) { + findings.push({ + level: "block", + slug, + message: `interface (type surface) drifted — resync required.`, + }); + } + if (curOutline !== entry.hashes.outline) { + findings.push({ level: "block", slug, message: `outline edited — resync required.` }); + } + if (curSnap !== entry.hashes.snapshot) { + findings.push({ + level: "warn", + slug, + message: `rendered snapshot changed — advisory: update the outline if a default/behavior changed.`, + }); + } + + // Output integrity — generated files must match the manifest exactly. + const mdAbs = join(repoRoot, outline.mdPath); + if (!existsSync(mdAbs)) { + findings.push({ + level: "block", + slug, + message: `missing generated output ${outline.mdPath} — run sync.`, + }); + } else if (hash(normalizeText(readFileSync(mdAbs, "utf8"))) !== entry.hashes.outputMd) { + findings.push({ + level: "block", + slug, + message: `${outline.mdPath} was hand-edited or is stale — never edit generated files; run sync.`, + }); + } + + if (entry.hashes.outputExamples) { + const exAbs = join(repoRoot, outline.examplesPath); + if (!existsSync(exAbs)) { + findings.push({ level: "block", slug, message: `missing ${outline.examplesPath}.` }); + } else if (hash(normalizeText(readFileSync(exAbs, "utf8"))) !== entry.hashes.outputExamples) { + findings.push({ + level: "block", + slug, + message: `${outline.examplesPath} changed since last sync — its .md code fences are stale; run sync.`, + }); + } + } + } + + return { findings, ok: !findings.some((f) => f.level === "block") }; +} diff --git a/packages/docs-kit/src/cli.ts b/packages/docs-kit/src/cli.ts new file mode 100644 index 00000000..ecfa9a1b --- /dev/null +++ b/packages/docs-kit/src/cli.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env node +import { check } from "./check"; +import type { Finding } from "./coverage"; +import { sync } from "./sync"; + +function printFindings(findings: Finding[]): void { + for (const f of findings) { + const tag = f.level === "block" ? "✖ BLOCK" : "⚠ warn "; + const where = f.slug ? `[${f.slug}] ` : ""; + console.log(` ${tag} ${where}${f.message}`); + } +} + +function resolveRoot(argv: string[]): string { + const i = argv.indexOf("--root"); + return i >= 0 && argv[i + 1] ? argv[i + 1] : process.cwd(); +} + +const cmd = process.argv[2]; +const repoRoot = resolveRoot(process.argv); + +if (cmd === "sync") { + const { written, findings } = sync(repoRoot); + console.log(`docs-kit sync — wrote ${written.length} document(s):`); + for (const w of written) console.log(` • ${w}`); + if (findings.length > 0) { + console.log("\nfindings:"); + printFindings(findings); + } + process.exit(0); +} else if (cmd === "check") { + const { findings, ok } = check(repoRoot); + if (findings.length === 0) { + console.log("docs-kit check — ✓ all units in sync."); + } else { + console.log("docs-kit check:"); + printFindings(findings); + console.log(ok ? "\n✓ no blocking issues (advisories only)." : "\n✖ blocking issues found."); + } + process.exit(ok ? 0 : 1); +} else { + console.log("usage: docs-kit [--root ]"); + process.exit(2); +} diff --git a/packages/docs-kit/src/config.ts b/packages/docs-kit/src/config.ts new file mode 100644 index 00000000..a23abe5a --- /dev/null +++ b/packages/docs-kit/src/config.ts @@ -0,0 +1,20 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import type { DocsConfig } from "./types"; + +/** Load `docs.config.json` from the repo root, applying defaults. */ +export function loadConfig(repoRoot: string): DocsConfig { + const raw = readFileSync(join(repoRoot, "docs.config.json"), "utf8"); + const parsed = JSON.parse(raw) as Partial; + return { + roots: parsed.roots ?? ["packages/core/src", "docs-src"], + tsconfig: parsed.tsconfig ?? "packages/core/tsconfig.json", + indexFile: parsed.indexFile ?? "packages/core/src/index.ts", + manifestFile: parsed.manifestFile ?? "docs/docs-manifest.json", + categories: parsed.categories ?? [], + enforceCoverage: parsed.enforceCoverage ?? false, + exclusions: parsed.exclusions ?? [], + snapshotDir: parsed.snapshotDir, + }; +} diff --git a/packages/docs-kit/src/coverage.ts b/packages/docs-kit/src/coverage.ts new file mode 100644 index 00000000..979b74fc --- /dev/null +++ b/packages/docs-kit/src/coverage.ts @@ -0,0 +1,84 @@ +import picomatch from "picomatch"; + +import type { Surface } from "./project"; +import type { DocsConfig, Outline } from "./types"; + +export interface Finding { + level: "block" | "warn"; + slug: string | null; + message: string; +} + +/** Public export names owned by a code-backed unit — those whose resolved + * declaration file matches one of the unit's `sources` globs. */ +export function ownedSymbols(surface: Surface, sources: string[]): string[] { + const matchers = sources.map((g) => picomatch(g, { dot: true })); + const owned: string[] = []; + for (const sym of surface.symbols.values()) { + if (sym.external) continue; + if (matchers.some((m) => m(sym.file))) owned.push(sym.name); + } + return owned.sort(); +} + +/** Reconcile the public surface against all outlines, both directions. */ +export function reconcile( + surface: Surface, + outlines: Outline[], + config: DocsConfig, +): { findings: Finding[]; ownedBySlug: Map } { + const findings: Finding[] = []; + const ownedBySlug = new Map(); + const claimed = new Set(); + + for (const o of outlines) { + if (o.kind === "code-backed") { + const owned = ownedSymbols(surface, o.frontmatter.sources ?? []); + ownedBySlug.set(o.slug, owned); + owned.forEach((n) => claimed.add(n)); + // Reverse check (the "timeline" class of bug): a documented unit that + // resolves to zero public exports is documenting something unshipped. + if (owned.length === 0) { + findings.push({ + level: "block", + slug: o.slug, + message: `"${o.slug}" is a code-backed unit but its \`sources\` own no export from index.ts — is it exported?`, + }); + } + } else if (o.kind === "reference") { + const claims = o.frontmatter.claims ?? []; + ownedBySlug.set(o.slug, claims); + for (const name of claims) { + claimed.add(name); + if (!surface.symbols.has(name)) { + findings.push({ + level: "block", + slug: o.slug, + message: `reference unit "${o.slug}" claims "${name}", which is not exported from index.ts`, + }); + } + } + } else { + ownedBySlug.set(o.slug, []); + } + } + + // Forward check: every public export must be claimed or excluded. + const excluded = new Set(config.exclusions); + const uncovered: string[] = []; + for (const name of surface.symbols.keys()) { + if (!claimed.has(name) && !excluded.has(name)) uncovered.push(name); + } + if (uncovered.length > 0) { + findings.push({ + level: config.enforceCoverage ? "block" : "warn", + slug: null, + message: + `${uncovered.length} public export(s) undocumented` + + `${config.enforceCoverage ? "" : " (coverage not yet enforced)"}: ` + + uncovered.sort().join(", "), + }); + } + + return { findings, ownedBySlug }; +} diff --git a/packages/docs-kit/src/hash.ts b/packages/docs-kit/src/hash.ts new file mode 100644 index 00000000..c19425fa --- /dev/null +++ b/packages/docs-kit/src/hash.ts @@ -0,0 +1,14 @@ +import { createHash } from "node:crypto"; + +/** Short, stable content hash. 16 hex chars keeps the manifest readable while + * remaining collision-safe for our scale. */ +export function hash(input: string): string { + return createHash("sha256").update(input).digest("hex").slice(0, 16); +} + +/** Normalize text so that formatting churn (line wrapping, indentation, blank + * lines — e.g. from oxfmt) never moves a hash; only content changes do. All + * whitespace runs collapse to a single space. */ +export function normalizeText(input: string): string { + return input.replace(/\s+/g, " ").trim(); +} diff --git a/packages/docs-kit/src/index.ts b/packages/docs-kit/src/index.ts new file mode 100644 index 00000000..5c06d4ba --- /dev/null +++ b/packages/docs-kit/src/index.ts @@ -0,0 +1,16 @@ +export { loadConfig } from "./config"; +export { discoverOutlines } from "./outline"; +export { + loadSurface, + snapshotHashForSlug, + type Surface, + type PublicSymbol, + type PropRow, +} from "./project"; +export { ownedSymbols, reconcile, type Finding } from "./coverage"; +export { assembleMarkdown } from "./assemble"; +export { readManifest, writeManifest } from "./manifest"; +export { sync, type SyncResult } from "./sync"; +export { check, type CheckResult } from "./check"; +export * from "./types"; +export { hash, normalizeText } from "./hash"; diff --git a/packages/docs-kit/src/manifest.ts b/packages/docs-kit/src/manifest.ts new file mode 100644 index 00000000..d3be0f51 --- /dev/null +++ b/packages/docs-kit/src/manifest.ts @@ -0,0 +1,16 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +import type { Manifest } from "./types"; + +export function readManifest(repoRoot: string, manifestRel: string): Manifest | null { + const p = join(repoRoot, manifestRel); + if (!existsSync(p)) return null; + return JSON.parse(readFileSync(p, "utf8")) as Manifest; +} + +export function writeManifest(repoRoot: string, manifestRel: string, manifest: Manifest): void { + const p = join(repoRoot, manifestRel); + mkdirSync(dirname(p), { recursive: true }); + writeFileSync(p, `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); +} diff --git a/packages/docs-kit/src/outline.ts b/packages/docs-kit/src/outline.ts new file mode 100644 index 00000000..6f457dcc --- /dev/null +++ b/packages/docs-kit/src/outline.ts @@ -0,0 +1,67 @@ +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { basename, join, relative } from "node:path"; + +import matter from "gray-matter"; +import picomatch from "picomatch"; + +import type { DocsConfig, Outline, OutlineFrontmatter, UnitKind } from "./types"; + +const OUTLINE_SUFFIX = ".docs-outline.md"; + +function toPosix(p: string): string { + return p.split("\\").join("/"); +} + +function walk(dir: string, out: string[] = []): string[] { + if (!existsSync(dir)) return out; + for (const name of readdirSync(dir)) { + if (name === "node_modules" || name === "dist" || name.startsWith(".")) continue; + const full = join(dir, name); + if (statSync(full).isDirectory()) walk(full, out); + else if (name.endsWith(OUTLINE_SUFFIX)) out.push(full); + } + return out; +} + +function classify(fm: OutlineFrontmatter, outlineRel: string): UnitKind { + if (fm.sources && fm.sources.length > 0) return "code-backed"; + if ((fm.claims && fm.claims.length > 0) || outlineRel.includes("/references/")) + return "reference"; + return "prose"; +} + +function outDirFor(outlineRel: string, config: DocsConfig): string | null { + for (const rule of config.categories) { + if (picomatch(rule.match, { dot: true })(outlineRel)) return rule.outDir; + } + return null; +} + +/** Discover and parse every `*.docs-outline.md` under the configured roots. */ +export function discoverOutlines(repoRoot: string, config: DocsConfig): Outline[] { + const outlines: Outline[] = []; + for (const root of config.roots) { + for (const outlinePath of walk(join(repoRoot, root))) { + const outlineRel = toPosix(relative(repoRoot, outlinePath)); + const parsed = matter(readFileSync(outlinePath, "utf8")); + const fm = parsed.data as OutlineFrontmatter; + const kind = classify(fm, outlineRel); + const slug = fm.group ?? basename(outlinePath).slice(0, -OUTLINE_SUFFIX.length); + const outDir = outDirFor(outlineRel, config); + if (!outDir) throw new Error(`docs-kit: no category rule matches outline "${outlineRel}"`); + outlines.push({ + kind, + slug, + outlinePath, + outlineRel, + frontmatter: fm, + body: parsed.content, + outDir, + mdPath: `${outDir}/${slug}.md`, + examplesPath: `${outDir}/${slug}.examples.tsx`, + }); + } + } + outlines.sort((a, b) => a.slug.localeCompare(b.slug)); + return outlines; +} diff --git a/packages/docs-kit/src/project.ts b/packages/docs-kit/src/project.ts new file mode 100644 index 00000000..fec5a143 --- /dev/null +++ b/packages/docs-kit/src/project.ts @@ -0,0 +1,155 @@ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { join, relative } from "node:path"; + +import { Node, Project, SymbolFlags } from "ts-morph"; + +import { hash, normalizeText } from "./hash"; +import type { DocsConfig } from "./types"; + +function toPosix(p: string): string { + return p.split("\\").join("/"); +} + +export interface PropRow { + name: string; + type: string; + optional: boolean; + description: string; +} + +/** For an exported `*Props` interface/type alias, list its FIRST-PARTY props — + * skipping members inherited purely from `node_modules` (e.g. the hundreds of + * DOM attributes from `React.ComponentProps<"button">`), so only the props this + * component actually adds (variants, render, etc.) surface in the table. */ +function extractProps(name: string, decl: Node): PropRow[] | null { + if (!/Props$/.test(name)) return null; + if (!(Node.isInterfaceDeclaration(decl) || Node.isTypeAliasDeclaration(decl))) return null; + let properties; + try { + properties = decl.getType().getProperties(); + } catch { + return null; + } + const rows: PropRow[] = []; + for (const prop of properties) { + const decls = prop.getDeclarations(); + const allExternal = + decls.length > 0 && + decls.every((d) => d.getSourceFile().getFilePath().includes("/node_modules/")); + if (allExternal) continue; // inherited DOM/lib prop — not part of our surface + + const d0 = decls[0]; + let optional = (prop.getFlags() & SymbolFlags.Optional) !== 0; + let description = ""; + if (d0 && (Node.isPropertySignature(d0) || Node.isPropertyDeclaration(d0))) { + optional = d0.hasQuestionToken(); + const jsdocs = d0.getJsDocs(); + if (jsdocs.length > 0) + description = (jsdocs[jsdocs.length - 1].getCommentText() ?? "").trim(); + } + let typeText = "unknown"; + try { + typeText = prop.getTypeAtLocation(decl).getText(decl); + } catch { + /* keep fallback */ + } + // `?` already conveys optionality — drop the trailing `| undefined` noise. + typeText = typeText.replace(/\s*\|\s*undefined\s*$/, ""); + rows.push({ name: prop.getName(), type: typeText, optional, description }); + } + rows.sort((a, b) => a.name.localeCompare(b.name)); + return rows.length > 0 ? rows : null; +} + +export interface PublicSymbol { + name: string; + /** Repo-relative file of the resolved declaration (may be under node_modules). */ + file: string; + /** Declaration resolves into node_modules — an external re-export. */ + external: boolean; + /** Resolved type text, used for the type-surface hash. */ + typeText: string; + /** Local declaration text (bodies included) — used for the readable API section. */ + declText: string; + kind: string; + /** First-party props, for `*Props` symbols; null otherwise. */ + props: PropRow[] | null; +} + +export interface Surface { + /** Public export name → resolved info. */ + symbols: Map; + /** Type-surface hash over a set of owned names; null when empty. */ + hashSymbols(names: string[]): string | null; +} + +/** Load the public surface from `index.ts` via its tsconfig, resolving every + * re-export to its originating declaration and resolved type. */ +export function loadSurface(repoRoot: string, config: DocsConfig): Surface { + const project = new Project({ + tsConfigFilePath: join(repoRoot, config.tsconfig), + }); + const index = project.getSourceFileOrThrow(join(repoRoot, config.indexFile)); + const symbols = new Map(); + + for (const [name, decls] of index.getExportedDeclarations()) { + const decl = decls[0]; + if (!decl) continue; + const sourceFile = decl.getSourceFile(); + let typeText: string; + try { + typeText = decl.getType().getText(decl); + } catch { + typeText = decl.getText(); + } + symbols.set(name, { + name, + file: toPosix(relative(repoRoot, sourceFile.getFilePath())), + external: sourceFile.isInNodeModules(), + typeText, + declText: safeDeclText(decl.getText()), + kind: decl.getKindName(), + props: extractProps(name, decl), + }); + } + + return { + symbols, + hashSymbols(names: string[]): string | null { + if (names.length === 0) return null; + const parts = names + .slice() + .sort() + .map((n) => { + const s = symbols.get(n); + return `${n}::${s ? s.typeText : ""}`; + }); + return hash(normalizeText(parts.join("\n"))); + }, + }; +} + +/** Trim runaway declaration text (e.g. a component whose body is inline). */ +function safeDeclText(text: string): string { + const trimmed = text.trim(); + return trimmed.length > 2000 ? `${trimmed.slice(0, 2000)}\n/* …truncated… */` : trimmed; +} + +/** Advisory snapshot signal: hash any centralized snapshot files whose flattened + * name references this unit's slug. Best-effort — never blocks. */ +export function snapshotHashForSlug( + repoRoot: string, + config: DocsConfig, + slug: string, +): string | null { + if (!config.snapshotDir) return null; + const dir = join(repoRoot, config.snapshotDir); + if (!existsSync(dir)) return null; + const token = slug.replace(/-/g, "-"); + const matched = readdirSync(dir) + .filter((f) => f.endsWith(".snap") && f.includes(token)) + .sort(); + if (matched.length === 0) return null; + const contents = matched.map((f) => readFileSync(join(dir, f), "utf8")).join("\n"); + return hash(normalizeText(contents)); +} diff --git a/packages/docs-kit/src/sync.ts b/packages/docs-kit/src/sync.ts new file mode 100644 index 00000000..7e073d5e --- /dev/null +++ b/packages/docs-kit/src/sync.ts @@ -0,0 +1,99 @@ +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +import { assembleMarkdown } from "./assemble"; +import { loadConfig } from "./config"; +import { type Finding, reconcile } from "./coverage"; +import { hash, normalizeText } from "./hash"; +import { writeManifest } from "./manifest"; +import { discoverOutlines } from "./outline"; +import { loadSurface, snapshotHashForSlug } from "./project"; +import type { Manifest, ManifestEntry } from "./types"; + +export interface SyncResult { + manifest: Manifest; + findings: Finding[]; + written: string[]; +} + +/** Run the repo formatter (oxfmt) over the given files so that committed output + * is byte-identical to what the manifest hashes. This makes the pre-commit + * oxfmt hook idempotent — it can never reformat a generated file after the fact + * and silently invalidate the drift gate. Best-effort: if oxfmt isn't present, + * hashing simply falls back to sync's own output. */ +function formatFiles(repoRoot: string, paths: string[]): void { + const bin = join(repoRoot, "node_modules/.bin/oxfmt"); + const targets = [...new Set(paths)].filter((p) => existsSync(p)); + if (targets.length === 0 || !existsSync(bin)) return; + try { + execFileSync(bin, ["--no-error-on-unmatched-pattern", ...targets], { + cwd: repoRoot, + stdio: "ignore", + }); + } catch { + /* formatting is best-effort */ + } +} + +/** Deterministically (re)generate every unit's markdown from its outline + + * authored examples + resolved type surface, format everything with the repo + * formatter, then rewrite the manifest with hashes of the FORMATTED files. */ +export function sync(repoRoot: string): SyncResult { + const config = loadConfig(repoRoot); + const outlines = discoverOutlines(repoRoot, config); + const surface = loadSurface(repoRoot, config); + const { findings, ownedBySlug } = reconcile(surface, outlines, config); + + // Phase 1 — assemble + write every markdown output. + const written: string[] = []; + const toFormat: string[] = []; + for (const outline of outlines) { + const owned = ownedBySlug.get(outline.slug) ?? []; + const md = assembleMarkdown({ repoRoot, outline, surface, owned }); + const mdAbs = join(repoRoot, outline.mdPath); + mkdirSync(dirname(mdAbs), { recursive: true }); + writeFileSync(mdAbs, md, "utf8"); + written.push(outline.mdPath); + toFormat.push(mdAbs, outline.outlinePath); + const examplesAbs = join(repoRoot, outline.examplesPath); + if (existsSync(examplesAbs)) toFormat.push(examplesAbs); + } + + // Phase 2 — format outputs + sources so the committed bytes match the hashes. + formatFiles(repoRoot, toFormat); + + // Phase 3 — hash the FORMATTED files and build the manifest. + const units: Record = {}; + for (const outline of outlines) { + const owned = ownedBySlug.get(outline.slug) ?? []; + const mdAbs = join(repoRoot, outline.mdPath); + const examplesAbs = join(repoRoot, outline.examplesPath); + const hasExamples = existsSync(examplesAbs); + + units[outline.slug] = { + slug: outline.slug, + kind: outline.kind, + outline: outline.outlineRel, + output: outline.mdPath, + examples: hasExamples ? outline.examplesPath : null, + sources: outline.frontmatter.sources ?? [], + claims: outline.frontmatter.claims ?? [], + symbols: owned, + hashes: { + typeSurface: outline.kind === "code-backed" ? surface.hashSymbols(owned) : null, + outline: hash(normalizeText(readFileSync(outline.outlinePath, "utf8"))), + snapshot: + outline.kind === "code-backed" + ? snapshotHashForSlug(repoRoot, config, outline.slug) + : null, + outputMd: hash(normalizeText(readFileSync(mdAbs, "utf8"))), + outputExamples: hasExamples ? hash(normalizeText(readFileSync(examplesAbs, "utf8"))) : null, + }, + }; + } + + const manifest: Manifest = { version: 1, units }; + writeManifest(repoRoot, config.manifestFile, manifest); + return { manifest, findings, written }; +} diff --git a/packages/docs-kit/src/types.ts b/packages/docs-kit/src/types.ts new file mode 100644 index 00000000..8fa232ed --- /dev/null +++ b/packages/docs-kit/src/types.ts @@ -0,0 +1,87 @@ +/** A documentable unit is code-backed (owns exported symbols), a reference to + * re-exported third-party API, or free prose (concepts / patterns). */ +export type UnitKind = "code-backed" | "reference" | "prose"; + +export interface OutlineFrontmatter { + /** Slug override; defaults to the filename minus `.docs-outline.md`. */ + group?: string; + title?: string; + description?: string; + /** Code-backed units: globs (repo-relative) whose exported symbols this unit owns. */ + sources?: string[]; + /** Reference units: names of re-exported symbols this unit documents. */ + claims?: string[]; + /** Reference units: upstream docs URL. */ + upstream?: string; +} + +export interface Outline { + kind: UnitKind; + slug: string; + /** Absolute path to the outline file. */ + outlinePath: string; + /** Repo-relative path to the outline file. */ + outlineRel: string; + frontmatter: OutlineFrontmatter; + /** Markdown body with frontmatter stripped. */ + body: string; + /** Repo-relative output directory. */ + outDir: string; + /** Repo-relative path of the generated markdown. */ + mdPath: string; + /** Repo-relative path of the (authored) examples module. */ + examplesPath: string; +} + +export interface UnitHashes { + /** Resolved type-surface hash — null for reference/prose units. */ + typeSurface: string | null; + outline: string; + /** Advisory only. */ + snapshot: string | null; + outputMd: string | null; + outputExamples: string | null; +} + +export interface ManifestEntry { + slug: string; + kind: UnitKind; + outline: string; + output: string; + examples: string | null; + sources: string[]; + claims: string[]; + /** Resolved public export names this unit owns (code-backed) or claims (reference). */ + symbols: string[]; + hashes: UnitHashes; +} + +export interface Manifest { + version: number; + units: Record; +} + +export interface CategoryRule { + /** Glob (repo-relative) matched against an outline's path. */ + match: string; + /** Repo-relative output directory for matching outlines. */ + outDir: string; +} + +export interface DocsConfig { + /** Repo-relative roots scanned for `*.docs-outline.md`. */ + roots: string[]; + /** Repo-relative tsconfig used to load the type project. */ + tsconfig: string; + /** Repo-relative entry file defining the public surface. */ + indexFile: string; + /** Repo-relative manifest path. */ + manifestFile: string; + categories: CategoryRule[]; + /** When false (migration phase), uncovered exports warn instead of blocking. */ + enforceCoverage: boolean; + /** Export names deliberately left undocumented (rare escape hatch). */ + exclusions: string[]; + /** Repo-relative dir holding centralized test snapshots (advisory signal). */ + snapshotDir?: string; +} diff --git a/packages/docs-kit/tsconfig.json b/packages/docs-kit/tsconfig.json new file mode 100644 index 00000000..c44fc99c --- /dev/null +++ b/packages/docs-kit/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"], + "skipLibCheck": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/packages/docs-kit/tsdown.config.ts b/packages/docs-kit/tsdown.config.ts new file mode 100644 index 00000000..ed1c3e4b --- /dev/null +++ b/packages/docs-kit/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: { + index: "src/index.ts", + cli: "src/cli.ts", + }, + format: ["esm"], + platform: "node", + dts: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3b42362..1df2b5ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,55 @@ importers: specifier: 'catalog:' version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) + examples/docs-app: + dependencies: + '@tailor-platform/app-shell': + specifier: workspace:* + version: link:../../packages/core + '@tailor-platform/app-shell-vite-plugin': + specifier: workspace:* + version: link:../../packages/vite-plugin + lucide-react: + specifier: 'catalog:' + version: 1.8.0(react@19.2.6) + react: + specifier: 'catalog:' + version: 19.2.6 + react-dom: + specifier: 'catalog:' + version: 19.2.6(react@19.2.6) + react-markdown: + specifier: ^9.0.1 + version: 9.1.0(@types/react@19.2.13)(react@19.2.6) + remark-gfm: + specifier: ^4.0.0 + version: 4.0.1 + tailwindcss: + specifier: 'catalog:' + version: 4.3.0 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + '@types/node': + specifier: 'catalog:' + version: 25.9.1 + '@types/react': + specifier: 'catalog:' + version: 19.2.13 + '@types/react-dom': + specifier: 'catalog:' + version: 19.2.3(@types/react@19.2.13) + '@vitejs/plugin-react': + specifier: 'catalog:' + version: 5.2.0(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + typescript: + specifier: ^5 + version: 5.9.3 + vite: + specifier: 'catalog:' + version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) + examples/nextjs-app: dependencies: '@hookform/resolvers': @@ -343,6 +392,34 @@ importers: specifier: 'catalog:' version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.9.0)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/docs-kit: + dependencies: + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 + picomatch: + specifier: ^4.0.2 + version: 4.0.4 + ts-morph: + specifier: ^28.0.0 + version: 28.0.0 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 25.9.1 + '@types/picomatch': + specifier: ^4.0.0 + version: 4.0.3 + tsdown: + specifier: 'catalog:' + version: 0.22.0(tsx@4.22.4)(typescript@5.9.3) + typescript: + specifier: ^5 + version: 5.9.3 + vitest: + specifier: 'catalog:' + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.9.0)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/sdk-plugin: devDependencies: '@tailor-platform/app-shell': @@ -2276,9 +2353,6 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - '@oxc-project/types@0.132.0': - resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} - '@oxc-project/types@0.137.0': resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} @@ -2587,12 +2661,6 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.2': - resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@rolldown/binding-android-arm64@1.1.2': resolution: {integrity: sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2605,12 +2673,6 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.2': - resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@rolldown/binding-darwin-arm64@1.1.2': resolution: {integrity: sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2623,12 +2685,6 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.2': - resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@rolldown/binding-darwin-x64@1.1.2': resolution: {integrity: sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2641,12 +2697,6 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.2': - resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@rolldown/binding-freebsd-x64@1.1.2': resolution: {integrity: sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2659,12 +2709,6 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.1.2': resolution: {integrity: sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2678,13 +2722,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.2': - resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.1.2': resolution: {integrity: sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2699,13 +2736,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.2': - resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.1.2': resolution: {integrity: sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2720,13 +2750,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.1.2': resolution: {integrity: sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2741,13 +2764,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.2': - resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.2': resolution: {integrity: sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2762,13 +2778,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.2': - resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.2': resolution: {integrity: sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2783,13 +2792,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.2': - resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-x64-musl@1.1.2': resolution: {integrity: sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2803,12 +2805,6 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.2': - resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.1.2': resolution: {integrity: sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2820,11 +2816,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.2': - resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.1.2': resolution: {integrity: sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2836,12 +2827,6 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.2': - resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.1.2': resolution: {integrity: sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2854,12 +2839,6 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.2': - resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.2': resolution: {integrity: sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3388,21 +3367,36 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/encoding-japanese@2.2.1': resolution: {integrity: sha512-6jjepuTusvySxMLP7W6usamlbgf0F4sIDvm7EzYePjLHY7zWUv4yz2PLUnu0vuNVtXOTLu2cRdFcDg40J5Owsw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/gradient-string@1.1.6': resolution: {integrity: sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -3412,6 +3406,9 @@ packages: '@types/papaparse@5.5.2': resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} + '@types/picomatch@4.0.3': + resolution: {integrity: sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -3423,6 +3420,12 @@ packages: '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -3455,6 +3458,9 @@ packages: resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@urql/core@6.0.1': resolution: {integrity: sha512-FZDiQk6jxbj5hixf2rEPv0jI+IZz0EqqGW8mJBEug68/zHTtT+f34guZDmyjJZyiWbj0vL165LoMr/TkeDHaug==} @@ -3668,6 +3674,9 @@ packages: resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -3742,6 +3751,9 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -3765,6 +3777,18 @@ packages: change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -3836,6 +3860,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -3926,6 +3953,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4008,6 +4038,9 @@ packages: peerDependencies: typescript: ^5.4.4 || ^6.0.2 + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.2: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} @@ -4109,6 +4142,10 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -4127,6 +4164,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -4152,6 +4192,9 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -4323,6 +4366,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -4330,6 +4379,9 @@ packages: hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -4390,6 +4442,9 @@ packages: react-devtools-core: optional: true + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + inquirer@12.6.3: resolution: {integrity: sha512-eX9beYAjr1MqYsIjx1vAheXsRk1jbZRvHLcBu5nA9wX0rXR1IfCZLnVLp4Ym4mrhqmh7AuANwcdtgQ291fZDfQ==} engines: {node: '>=18'} @@ -4399,10 +4454,19 @@ packages: '@types/node': optional: true + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -4437,6 +4501,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ci@1.0.0: resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} engines: {node: '>=18'} @@ -4467,6 +4534,10 @@ packages: resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} engines: {node: '>=0.10.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-port-reachable@4.0.0: resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4739,6 +4810,9 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -4776,6 +4850,54 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4783,6 +4905,90 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -5043,6 +5249,9 @@ packages: papaparse@5.5.3: resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-ms@2.1.0: resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} engines: {node: '>=6'} @@ -5197,6 +5406,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + protobufjs@8.0.3: resolution: {integrity: sha512-LBYnMWkKLB8fE/ljROPDbCl7mgLSlI+oBe1fAAr5MTqFg4TIi0tYrVVurJvQggOjnUYMQtEZBjrej59ojMNTHQ==} engines: {node: '>=12.0.0'} @@ -5259,6 +5471,12 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@9.1.0: + resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-reconciler@0.32.0: resolution: {integrity: sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==} engines: {node: '>=0.10.0'} @@ -5310,6 +5528,18 @@ packages: resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} engines: {node: '>=0.10.0'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -5396,11 +5626,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.2: - resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - rolldown@1.1.2: resolution: {integrity: sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5539,6 +5764,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -5589,6 +5817,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stringify-object@3.3.0: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} @@ -5621,6 +5852,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -5706,6 +5943,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -5837,6 +6080,24 @@ packages: resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} engines: {node: '>=22.19.0'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -5877,6 +6138,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-plugin-dts@4.5.4: resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: @@ -6079,6 +6346,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@0no-co/graphql.web@1.2.0(graphql@16.13.2)': @@ -7685,8 +7955,6 @@ snapshots: '@oxc-project/types@0.127.0': {} - '@oxc-project/types@0.132.0': {} - '@oxc-project/types@0.137.0': {} '@oxfmt/binding-android-arm-eabi@0.47.0': @@ -7881,108 +8149,72 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-android-arm64@1.0.2': - optional: true - '@rolldown/binding-android-arm64@1.1.2': optional: true '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-arm64@1.0.2': - optional: true - '@rolldown/binding-darwin-arm64@1.1.2': optional: true '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-x64@1.0.2': - optional: true - '@rolldown/binding-darwin-x64@1.1.2': optional: true '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-freebsd-x64@1.0.2': - optional: true - '@rolldown/binding-freebsd-x64@1.1.2': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.2': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.2': - optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.2': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.2': - optional: true - '@rolldown/binding-linux-arm64-musl@1.1.2': optional: true '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.2': optional: true '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.2': - optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.2': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.2': - optional: true - '@rolldown/binding-linux-x64-gnu@1.1.2': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-musl@1.0.2': - optional: true - '@rolldown/binding-linux-x64-musl@1.1.2': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-openharmony-arm64@1.0.2': - optional: true - '@rolldown/binding-openharmony-arm64@1.1.2': optional: true @@ -7993,13 +8225,6 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-wasm32-wasi@1.0.2': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - '@rolldown/binding-wasm32-wasi@1.1.2': dependencies: '@emnapi/core': 1.11.1 @@ -8010,18 +8235,12 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.2': - optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.2': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.2': - optional: true - '@rolldown/binding-win32-x64-msvc@1.1.2': optional: true @@ -8552,18 +8771,36 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} '@types/encoding-japanese@2.2.1': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} '@types/gradient-string@1.1.6': dependencies: '@types/tinycolor2': 1.4.6 + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/jsesc@2.5.1': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@12.20.55': {} '@types/node@25.9.1': @@ -8574,6 +8811,8 @@ snapshots: dependencies: '@types/node': 25.9.1 + '@types/picomatch@4.0.3': {} + '@types/react-dom@19.2.3(@types/react@19.2.13)': dependencies: '@types/react': 19.2.13 @@ -8584,6 +8823,10 @@ snapshots: '@types/tinycolor2@1.4.6': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': @@ -8613,7 +8856,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.59.1 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 + semver: 7.8.5 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -8625,6 +8868,8 @@ snapshots: '@typescript-eslint/types': 8.59.1 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.3': {} + '@urql/core@6.0.1(graphql@16.13.2)': dependencies: '@0no-co/graphql.web': 1.2.0(graphql@16.13.2) @@ -8875,6 +9120,8 @@ snapshots: auto-bind@5.0.1: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -8950,6 +9197,8 @@ snapshots: caniuse-lite@1.0.30001793: {} + ccount@2.0.1: {} + chai@6.2.2: {} chalk-template@0.4.0: @@ -8967,6 +9216,14 @@ snapshots: change-case@5.4.4: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chardet@2.1.1: {} chokidar@5.0.0: @@ -9026,6 +9283,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + commander@12.1.0: {} commander@13.1.0: {} @@ -9092,6 +9351,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-extend@0.6.0: {} default-browser-id@5.0.1: {} @@ -9180,6 +9443,10 @@ snapshots: transitivePeerDependencies: - supports-color + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@8.0.2: {} dir-glob@3.0.1: @@ -9294,6 +9561,8 @@ snapshots: escape-string-regexp@2.0.0: {} + escape-string-regexp@5.0.0: {} + escodegen@2.1.0: dependencies: esprima: 4.0.1 @@ -9308,6 +9577,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -9336,6 +9607,8 @@ snapshots: dependencies: is-extendable: 0.1.1 + extend@3.0.2: {} + extendable-error@0.1.7: {} fast-deep-equal@3.1.3: {} @@ -9519,10 +9792,36 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + he@1.2.0: {} hookable@6.1.1: {} + html-url-attributes@3.0.1: {} + human-id@4.1.3: {} human-signals@2.1.0: {} @@ -9588,6 +9887,8 @@ snapshots: - bufferutil - utf-8-validate + inline-style-parser@0.2.7: {} + inquirer@12.6.3(@types/node@25.9.1): dependencies: '@inquirer/core': 10.3.2(@types/node@25.9.1) @@ -9600,10 +9901,19 @@ snapshots: optionalDependencies: '@types/node': 25.9.1 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-decimal@2.0.1: {} + is-docker@2.2.1: {} is-docker@3.0.0: {} @@ -9624,6 +9934,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-in-ci@1.0.0: {} is-in-ssh@1.0.0: {} @@ -9640,6 +9952,8 @@ snapshots: is-obj@1.0.1: {} + is-plain-obj@4.1.0: {} + is-port-reachable@4.0.0: {} is-regexp@1.0.0: {} @@ -9845,6 +10159,8 @@ snapshots: long@5.3.2: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -9888,10 +10204,356 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + merge-stream@2.0.0: {} merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -10209,6 +10871,16 @@ snapshots: papaparse@5.5.3: {} + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-ms@2.1.0: {} patch-console@2.0.0: {} @@ -10359,6 +11031,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.2.0: {} + protobufjs@8.0.3: dependencies: '@types/node': 25.9.1 @@ -10413,6 +11087,24 @@ snapshots: react-is@17.0.2: {} + react-markdown@9.1.0(@types/react@19.2.13)(react@19.2.6): + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/react': 19.2.13 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.6 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-reconciler@0.32.0(react@19.1.1): dependencies: react: 19.1.1 @@ -10458,6 +11150,40 @@ snapshots: dependencies: rc: 1.2.8 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + require-from-string@2.0.2: {} requirejs-config-file@4.0.0: @@ -10513,7 +11239,7 @@ snapshots: reusify@1.1.0: {} - rolldown-plugin-dts@0.25.1(rolldown@1.0.2)(typescript@5.9.3): + rolldown-plugin-dts@0.25.1(rolldown@1.1.2)(typescript@5.9.3): dependencies: '@babel/generator': 8.0.0-rc.5 '@babel/helper-validator-identifier': 8.0.0-rc.5 @@ -10523,7 +11249,7 @@ snapshots: dts-resolver: 3.0.0 get-tsconfig: 5.0.0-beta.5 obug: 2.1.1 - rolldown: 1.0.2 + rolldown: 1.1.2 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -10550,27 +11276,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 - rolldown@1.0.2: - dependencies: - '@oxc-project/types': 0.132.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.2 - '@rolldown/binding-darwin-arm64': 1.0.2 - '@rolldown/binding-darwin-x64': 1.0.2 - '@rolldown/binding-freebsd-x64': 1.0.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.2 - '@rolldown/binding-linux-arm64-musl': 1.0.2 - '@rolldown/binding-linux-ppc64-gnu': 1.0.2 - '@rolldown/binding-linux-s390x-gnu': 1.0.2 - '@rolldown/binding-linux-x64-gnu': 1.0.2 - '@rolldown/binding-linux-x64-musl': 1.0.2 - '@rolldown/binding-openharmony-arm64': 1.0.2 - '@rolldown/binding-wasm32-wasi': 1.0.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.2 - '@rolldown/binding-win32-x64-msvc': 1.0.2 - rolldown@1.1.2: dependencies: '@oxc-project/types': 0.137.0 @@ -10665,7 +11370,8 @@ snapshots: semver@7.7.4: {} - semver@7.8.0: {} + semver@7.8.0: + optional: true semver@7.8.5: {} @@ -10770,6 +11476,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -10822,6 +11530,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + stringify-object@3.3.0: dependencies: get-own-enumerable-property-symbols: 3.0.2 @@ -10846,6 +11559,14 @@ snapshots: strip-json-comments@3.1.1: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + styled-jsx@5.1.6(react@19.2.6): dependencies: client-only: 0.0.1 @@ -10909,6 +11630,10 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -10947,9 +11672,9 @@ snapshots: import-without-cache: 0.4.0 obug: 2.1.1 picomatch: 4.0.4 - rolldown: 1.0.2 - rolldown-plugin-dts: 0.25.1(rolldown@1.0.2)(typescript@5.9.3) - semver: 7.8.0 + rolldown: 1.1.2 + rolldown-plugin-dts: 0.25.1(rolldown@1.1.2)(typescript@5.9.3) + semver: 7.8.5 tinyexec: 1.2.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 @@ -11020,6 +11745,39 @@ snapshots: undici@8.5.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@0.1.2: {} universalify@2.0.1: {} @@ -11051,6 +11809,16 @@ snapshots: vary@1.1.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-plugin-dts@4.5.4(@types/node@25.9.1)(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@microsoft/api-extractor': 7.55.2(@types/node@25.9.1) @@ -11204,3 +11972,5 @@ snapshots: zod@4.3.6: {} zod@4.4.3: {} + + zwitch@2.0.4: {} From 2c09db25a99ce251624501b8a24fda69a6b5381a Mon Sep 17 00:00:00 2001 From: interacsean Date: Tue, 28 Jul 2026 10:06:05 +1000 Subject: [PATCH 03/15] fix(docs-kit): strip leading body H1 to avoid double header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Outlines migrated from docs/*.md keep their own `# Title`, and the assembler also prepends `# {frontmatter.title}` — producing two H1s. Strip a leading body H1 so the frontmatter title is the single canonical header. Co-Authored-By: Claude Opus 4.8 --- packages/docs-kit/src/assemble.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/docs-kit/src/assemble.ts b/packages/docs-kit/src/assemble.ts index e1388a1b..21041a0e 100644 --- a/packages/docs-kit/src/assemble.ts +++ b/packages/docs-kit/src/assemble.ts @@ -111,7 +111,12 @@ export function assembleMarkdown(opts: { const { repoRoot, outline, surface, owned } = opts; const examples = extractExamples(join(repoRoot, outline.examplesPath)); - const body = outline.body.replace(EXAMPLE_TOKEN, (_match, key: string) => { + // The frontmatter title is the canonical H1 (prepended below). Drop a leading + // H1 the outline body may already carry — outlines migrated from docs/*.md + // start with their own `# Title` — so the output has a single header. + const bodyWithoutH1 = outline.body.replace(/^\s*#\s+[^\n]*\r?\n+/, ""); + + const body = bodyWithoutH1.replace(EXAMPLE_TOKEN, (_match, key: string) => { const name = pascal(key); const src = examples.get(name); if (!src) { From ba99a63b55ab3358503ff9ee5ba2ede5058f11dc Mon Sep 17 00:00:00 2001 From: interacsean Date: Tue, 28 Jul 2026 11:09:21 +1000 Subject: [PATCH 04/15] feat(docs): render live examples inline at their doc position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assembler now drops an `` anchor next to each extracted code fence; the docs-app renders the live component at that anchor (rehype-raw), so each example sits with its markup instead of piling at the end. The anchor is a custom element, so it is sanitized away (invisible) on GitHub and other plain markdown renderers. Co-Authored-By: Claude Opus 4.8 --- docs/components/button.md | 8 +- docs/components/data-table.md | 4 +- docs/components/dialog.md | 6 +- docs/docs-manifest.json | 10 +-- docs/patterns/form-modal.md | 4 +- docs/patterns/list-dense-scan.md | 4 +- examples/docs-app/package.json | 1 + examples/docs-app/src/_lib/DocPage.tsx | 55 ++++++++----- packages/docs-kit/src/assemble.ts | 10 ++- pnpm-lock.yaml | 107 +++++++++++++++++++++++++ 10 files changed, 174 insertions(+), 35 deletions(-) diff --git a/docs/components/button.md b/docs/components/button.md index e697aa54..0b078f51 100644 --- a/docs/components/button.md +++ b/docs/components/button.md @@ -7,8 +7,6 @@ description: Styled button with multiple variants and sizes # Button -# Button - The `Button` component is a styled button with multiple visual variants and sizes. It supports rendering as a custom element via the `render` prop (Base UI render pattern). ## Import @@ -19,6 +17,8 @@ import { Button } from "@tailor-platform/app-shell"; ## Basic Usage + + ```tsx function BasicUsage() { return ( @@ -45,6 +45,8 @@ All standard HTML `, + ]} + /> + {/* products list — see list/dense-scan */} + + + + + Create product + +
{ + event.preventDefault(); + const data = new FormData(event.currentTarget); + window.alert(`Saving ${data.get("name") ?? ""}`); + setCreateOpen(false); + }} + > +
+ + Name + } /> + +
+ + + + + +
+
+ + ); +} diff --git a/docs-src/patterns/form-modal.docs.outline.md b/docs-src/patterns/form-modal.docs.outline.md index 46cf3fb1..bbed559e 100644 --- a/docs-src/patterns/form-modal.docs.outline.md +++ b/docs-src/patterns/form-modal.docs.outline.md @@ -36,6 +36,8 @@ dont: The route-driven variant renders the same component at both the parent path and the `create` / `edit` path, with `onOpenChange` navigating back so the URL stays in sync. + + ## Constraints - Dialog renders full-screen sheet below 1024px; centered max-w-md at 1024–1280px diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json index 45de8fe8..77ac3c90 100644 --- a/docs/docs-manifest.json +++ b/docs/docs-manifest.json @@ -116,10 +116,10 @@ "symbols": [], "hashes": { "typeSurface": null, - "outline": "dc2d8147c3ae5670", + "outline": "77184bd233c0533a", "snapshot": null, - "outputMd": "bae6c79bcfe6d5c4", - "examples": "5851d00933ce0062" + "outputMd": "7b2ce1b4ed496fb2", + "examples": "2fe2a4cb5c97a16b" } }, "list-dense-scan": { diff --git a/docs/patterns/form-modal.md b/docs/patterns/form-modal.md index cc40a3a1..ffffcb0e 100644 --- a/docs/patterns/form-modal.md +++ b/docs/patterns/form-modal.md @@ -64,6 +64,56 @@ function ModalForm() { The route-driven variant renders the same component at both the parent path and the `create` / `edit` path, with `onOpenChange` navigating back so the URL stays in sync. + + +```tsx +function ModalFormRouted() { + const [isCreateOpen, setCreateOpen] = useState(false); + return ( + + setCreateOpen(true)}> + Create + , + ]} + /> + {/* products list — see list/dense-scan */} + + + + + Create product + +
{ + event.preventDefault(); + const data = new FormData(event.currentTarget); + window.alert(`Saving ${data.get("name") ?? ""}`); + setCreateOpen(false); + }} + > +
+ + Name + } /> + +
+ + + + + +
+
+
+ ); +} +``` + ## Constraints - Dialog renders full-screen sheet below 1024px; centered max-w-md at 1024–1280px From 3b4ab95d00518be556d1a33c5354123defd3e526 Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 29 Jul 2026 15:39:07 +1000 Subject: [PATCH 11/15] docs(styling): derive styling concept from the original styling-theming.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the freshly-authored styling outline with the pre-overhaul docs/concepts/styling-theming.md prose (git mv → styling.docs.outline.md), retained verbatim, with only two runnable snippets tokenized into live examples (ThemeToggle via useTheme, AppearanceSwitcher) in a colocated styling.docs.examples.tsx. Restores the full Styling & Theming content (themes, palettes, z-index, adding-a-palette) and its git lineage. Note: rename detection pairs styling-theming.md → the generated styling.md (94% similar), with the outline as a new authored file — the same M+A dynamic as the component docs; the doc's history is preserved either way. Co-Authored-By: Claude Opus 4.8 --- .../src/pages/concepts/styling/page.tsx | 2 +- docs-src/concepts/styling.docs.examples.tsx | 14 ++ docs-src/concepts/styling.docs.outline.md | 132 ++++++++++++---- docs/concepts/styling-theming.md | 140 ---------------- docs/concepts/styling.md | 149 ++++++++++++++---- docs/docs-manifest.json | 8 +- 6 files changed, 237 insertions(+), 208 deletions(-) create mode 100644 docs-src/concepts/styling.docs.examples.tsx delete mode 100644 docs/concepts/styling-theming.md diff --git a/docs-browser/src/pages/concepts/styling/page.tsx b/docs-browser/src/pages/concepts/styling/page.tsx index d1296051..f7f71c6b 100644 --- a/docs-browser/src/pages/concepts/styling/page.tsx +++ b/docs-browser/src/pages/concepts/styling/page.tsx @@ -6,7 +6,7 @@ import { DocPage } from "../../../_lib/DocPage"; const Page = () => ; Page.appShellPageProps = { - meta: { title: "Styling & Tailwind setup" }, + meta: { title: "Styling and Theming" }, } satisfies AppShellPageProps; export default Page; diff --git a/docs-src/concepts/styling.docs.examples.tsx b/docs-src/concepts/styling.docs.examples.tsx new file mode 100644 index 00000000..7e4dfdec --- /dev/null +++ b/docs-src/concepts/styling.docs.examples.tsx @@ -0,0 +1,14 @@ +import { AppearanceSwitcher, useTheme } from "@tailor-platform/app-shell"; + +export function ThemeToggle() { + const { resolvedTheme, setTheme } = useTheme(); + return ( + + ); +} + +export function AppearanceToggle() { + return ; +} diff --git a/docs-src/concepts/styling.docs.outline.md b/docs-src/concepts/styling.docs.outline.md index defe4aab..4341b0e6 100644 --- a/docs-src/concepts/styling.docs.outline.md +++ b/docs-src/concepts/styling.docs.outline.md @@ -1,56 +1,126 @@ --- group: styling -title: Styling & Tailwind setup -description: How to wire Tailwind and app-shell's theme in a consumer app +title: Styling and Theming +description: Learn how to style your AppShell application using Tailwind CSS v4 and customize the theme --- -app-shell ships its own compiled styles and a set of themes. In **your** app you write **ordinary Tailwind classes** — the `astw:` prefix you may see in app-shell's own source is an **internal** prefix for the library's shipped stylesheet and is not something consumers use. +# Styling and Theming -## 1. Install Tailwind +Styling is done using Tailwind CSS v4. AppShell exports `@tailor-platform/app-shell/styles`, which includes the default palette and CSS variables. -```bash -pnpm add -D tailwindcss @tailwindcss/vite +To configure your application, import AppShell styles from your global CSS or top-level Tailwind CSS file: + +```css +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +``` + +If you want a branded palette, import exactly one theme file after `styles`: + +```css +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +@import "@tailor-platform/app-shell/themes/bloom"; ``` -## 2. Add the Vite plugin +After including this, your application's Tailwind utilities will resolve against the active AppShell tokens. -```ts -// vite.config.ts -import react from "@vitejs/plugin-react"; -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; +E.g. -export default defineConfig({ - plugins: [react(), tailwindcss()], -}); +```tsx +
...
``` -## 3. Import Tailwind + the app-shell theme +Note, many of these are default Tailwind colors, but there are some differences. If you omit this, much of the UI will look the same, but we will lose some of the Tailor-preferred colors. + +## A note on AppShell component class names + +AppShell components use Tailwind utility classes for their styling. Tailwind classes are generated at build-time, so stylesheet for AppShell components is already built and is separate to the Tailwind stylesheet generated for your application. + +In CSS, the order of style-definition affects the final styles which are computed for an element. Tailwind takes this into account when generating its stylesheet, however because it does not know that there's already a Tailwind-generated stylesheet included in the browser (AppShell's styles), there would be incorrect ordering of style definitions, and clashes can (though do not always) occur. + +To avoid this situation, and to ensure correct style resolution, AppShell components use a class prefix "astw" (AppShell TailWind) to avoid clashes. + +This is important to note for developing in AppShell. -Import Tailwind first, then app-shell's base styles, then one theme: +## Color Themes (Light / Dark / System) + +AppShell supports three color modes: `light`, `dark`, and `system` (follows the OS setting). + +Set the initial mode via the `defaultColorTheme` prop on ``. The user's selection is persisted to `localStorage` and restored on subsequent visits: + +```tsx + + {/* ... */} + +``` + +Use the [`useTheme`](../api/use-theme.md) hook to read or change the theme at runtime: + + + +Drop the pre-built [`AppearanceSwitcher`](../components/appearance-switcher.md) component anywhere in your layout for a ready-made light/dark/system toggle: + + + +## Theme Palettes + +AppShell ships three palettes, each with light and dark variants: + +| Palette | CSS import | +| --------- | -------------------------------------------------------------- | +| `default` | Included automatically via `@tailor-platform/app-shell/styles` | +| `cream` | `@tailor-platform/app-shell/themes/cream` | +| `bloom` | `@tailor-platform/app-shell/themes/bloom` | + +Select a palette by importing its CSS file — no prop needed. Import it in your global CSS **after** `@tailor-platform/app-shell/styles`: ```css -/* src/index.css */ -@import "tailwindcss"; @import "@tailor-platform/app-shell/styles"; -@import "@tailor-platform/app-shell/themes/bloom"; +@import "@tailor-platform/app-shell/themes/cream"; /* overrides default palette */ +@import "tailwindcss"; ``` -## 4. Use ordinary Tailwind in your code +Only import one palette at a time. -Your own markup uses unprefixed utilities; app-shell components bring their own styling: +## Z-Index Layering -```tsx -import { Button } from "@tailor-platform/app-shell"; +AppShell defines CSS custom properties for z-index values so you can adjust the stacking order to integrate with other libraries or overlays in your application. + +These properties are defined in `:root` and can be overridden in your own CSS: + +| Property | Default | Used for | +| ------------------ | ------- | ------------------------------------------------------------------- | +| `--z-sidebar` | `10` | The sidebar panel | +| `--z-sidebar-rail` | `20` | The sidebar collapse rail / toggle button | +| `--z-popup` | `50` | Portal-based popups (Menu, Select, Combobox, Autocomplete, Tooltip) | +| `--z-overlay` | `50` | Modal overlays (Dialog, Sheet) | + +### Example: Raising popup z-index + +```css +/* your-app/globals.css */ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; -
- - -
; +:root { + --z-popup: 100; +} ``` -> **Do not** use the `astw:` prefix in your app — it is reserved for app-shell's internal stylesheet. If you see it in generated example code, that's a bug in the example (report it). +## Adding a palette + +Theme tokens live in `packages/core/src/assets/themes/`. Copy `_template.css` to start a new palette — it lists exactly which sections to fill in for light and dark mode. + +| Section | Required? | What to set | +| --------------------- | --------------------- | ------------------------------------------------------------------------------ | +| **1. Brand** | Yes | `primary`, `secondary`, `accent` (+ foregrounds) — both modes | +| **2. Shell gradient** | Branded palettes only | `--shell-gradient-base`, `--shell-gradient-tint` | +| **3. System** | Tune or copy default | Surfaces: background, card, popover, muted, borders | +| **4. Palette** | Optional | Radius, chart colors, shadows | +| **5. Semantic** | Do not duplicate | Status and alert tokens inherit from `default.css` | +| **6. Structural** | Branded palettes | Copy the structural override block from `bloom.css` or `cream.css` when needed | -## Reference implementation +A palette is selected by CSS import, not by an AppShell prop. Import exactly one theme file after `@tailor-platform/app-shell/styles`; if you import none, the default palette from `styles` is used. -The docs explorer app (`examples/docs-app`) and the Vite example (`examples/vite-app`) are both wired exactly this way — use them as a copy-paste reference. +Preview token values at `/custom-page/color` in the Next.js example app. diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md deleted file mode 100644 index 90a6623f..00000000 --- a/docs/concepts/styling-theming.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Styling and Theming -description: Learn how to style your AppShell application using Tailwind CSS v4 and customize the theme ---- - -# Styling and Theming - -Styling is done using Tailwind CSS v4. AppShell exports `@tailor-platform/app-shell/styles`, which includes the default palette and CSS variables. - -To configure your application, import AppShell styles from your global CSS or top-level Tailwind CSS file: - -```css -@import "tailwindcss"; -@import "@tailor-platform/app-shell/styles"; -``` - -If you want a branded palette, import exactly one theme file after `styles`: - -```css -@import "tailwindcss"; -@import "@tailor-platform/app-shell/styles"; -@import "@tailor-platform/app-shell/themes/bloom"; -``` - -After including this, your application's Tailwind utilities will resolve against the active AppShell tokens. - -E.g. - -```tsx -
...
-``` - -Note, many of these are default Tailwind colors, but there are some differences. If you omit this, much of the UI will look the same, but we will lose some of the Tailor-preferred colors. - -## A note on AppShell component class names - -AppShell components use Tailwind utility classes for their styling. Tailwind classes are generated at build-time, so stylesheet for AppShell components is already built and is separate to the Tailwind stylesheet generated for your application. - -In CSS, the order of style-definition affects the final styles which are computed for an element. Tailwind takes this into account when generating its stylesheet, however because it does not know that there's already a Tailwind-generated stylesheet included in the browser (AppShell's styles), there would be incorrect ordering of style definitions, and clashes can (though do not always) occur. - -To avoid this situation, and to ensure correct style resolution, AppShell components use a class prefix "astw" (AppShell TailWind) to avoid clashes. - -This is important to note for developing in AppShell. - -## Color Themes (Light / Dark / System) - -AppShell supports three color modes: `light`, `dark`, and `system` (follows the OS setting). - -Set the initial mode via the `defaultColorTheme` prop on ``. The user's selection is persisted to `localStorage` and restored on subsequent visits: - -```tsx - - {/* ... */} - -``` - -Use the [`useTheme`](../api/use-theme.md) hook to read or change the theme at runtime: - -```tsx -import { useTheme } from "@tailor-platform/app-shell"; - -function ThemeToggle() { - const { theme, resolvedTheme, setTheme } = useTheme(); - return ( - - ); -} -``` - -Drop the pre-built [`AppearanceSwitcher`](../components/appearance-switcher.md) component anywhere in your layout for a ready-made light/dark/system toggle: - -```tsx -import { AppearanceSwitcher } from "@tailor-platform/app-shell"; - -; -``` - -## Theme Palettes - -AppShell ships three palettes, each with light and dark variants: - -| Palette | CSS import | -| --------- | -------------------------------------------------------------- | -| `default` | Included automatically via `@tailor-platform/app-shell/styles` | -| `cream` | `@tailor-platform/app-shell/themes/cream` | -| `bloom` | `@tailor-platform/app-shell/themes/bloom` | - -Select a palette by importing its CSS file — no prop needed. Import it in your global CSS **after** `@tailor-platform/app-shell/styles`: - -```css -@import "@tailor-platform/app-shell/styles"; -@import "@tailor-platform/app-shell/themes/cream"; /* overrides default palette */ -@import "tailwindcss"; -``` - -Only import one palette at a time. - -## Z-Index Layering - -AppShell defines CSS custom properties for z-index values so you can adjust the stacking order to integrate with other libraries or overlays in your application. - -These properties are defined in `:root` and can be overridden in your own CSS: - -| Property | Default | Used for | -| ------------------ | ------- | ------------------------------------------------------------------- | -| `--z-sidebar` | `10` | The sidebar panel | -| `--z-sidebar-rail` | `20` | The sidebar collapse rail / toggle button | -| `--z-popup` | `50` | Portal-based popups (Menu, Select, Combobox, Autocomplete, Tooltip) | -| `--z-overlay` | `50` | Modal overlays (Dialog, Sheet) | - -### Example: Raising popup z-index - -```css -/* your-app/globals.css */ -@import "tailwindcss"; -@import "@tailor-platform/app-shell/styles"; - -:root { - --z-popup: 100; -} -``` - -## Adding a palette - -Theme tokens live in `packages/core/src/assets/themes/`. Copy `_template.css` to start a new palette — it lists exactly which sections to fill in for light and dark mode. - -| Section | Required? | What to set | -| --------------------- | --------------------- | ------------------------------------------------------------------------------ | -| **1. Brand** | Yes | `primary`, `secondary`, `accent` (+ foregrounds) — both modes | -| **2. Shell gradient** | Branded palettes only | `--shell-gradient-base`, `--shell-gradient-tint` | -| **3. System** | Tune or copy default | Surfaces: background, card, popover, muted, borders | -| **4. Palette** | Optional | Radius, chart colors, shadows | -| **5. Semantic** | Do not duplicate | Status and alert tokens inherit from `default.css` | -| **6. Structural** | Branded palettes | Copy the structural override block from `bloom.css` or `cream.css` when needed | - -A palette is selected by CSS import, not by an AppShell prop. Import exactly one theme file after `@tailor-platform/app-shell/styles`; if you import none, the default palette from `styles` is used. - -Preview token values at `/custom-page/color` in the Next.js example app. diff --git a/docs/concepts/styling.md b/docs/concepts/styling.md index 1a09c7ff..1e3a411f 100644 --- a/docs/concepts/styling.md +++ b/docs/concepts/styling.md @@ -1,59 +1,144 @@ --- -title: Styling & Tailwind setup -description: How to wire Tailwind and app-shell's theme in a consumer app +title: Styling and Theming +description: Learn how to style your AppShell application using Tailwind CSS v4 and customize the theme --- -# Styling & Tailwind setup +# Styling and Theming -app-shell ships its own compiled styles and a set of themes. In **your** app you write **ordinary Tailwind classes** — the `astw:` prefix you may see in app-shell's own source is an **internal** prefix for the library's shipped stylesheet and is not something consumers use. +Styling is done using Tailwind CSS v4. AppShell exports `@tailor-platform/app-shell/styles`, which includes the default palette and CSS variables. -## 1. Install Tailwind +To configure your application, import AppShell styles from your global CSS or top-level Tailwind CSS file: -```bash -pnpm add -D tailwindcss @tailwindcss/vite +```css +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +``` + +If you want a branded palette, import exactly one theme file after `styles`: + +```css +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +@import "@tailor-platform/app-shell/themes/bloom"; +``` + +After including this, your application's Tailwind utilities will resolve against the active AppShell tokens. + +E.g. + +```tsx +
...
+``` + +Note, many of these are default Tailwind colors, but there are some differences. If you omit this, much of the UI will look the same, but we will lose some of the Tailor-preferred colors. + +## A note on AppShell component class names + +AppShell components use Tailwind utility classes for their styling. Tailwind classes are generated at build-time, so stylesheet for AppShell components is already built and is separate to the Tailwind stylesheet generated for your application. + +In CSS, the order of style-definition affects the final styles which are computed for an element. Tailwind takes this into account when generating its stylesheet, however because it does not know that there's already a Tailwind-generated stylesheet included in the browser (AppShell's styles), there would be incorrect ordering of style definitions, and clashes can (though do not always) occur. + +To avoid this situation, and to ensure correct style resolution, AppShell components use a class prefix "astw" (AppShell TailWind) to avoid clashes. + +This is important to note for developing in AppShell. + +## Color Themes (Light / Dark / System) + +AppShell supports three color modes: `light`, `dark`, and `system` (follows the OS setting). + +Set the initial mode via the `defaultColorTheme` prop on ``. The user's selection is persisted to `localStorage` and restored on subsequent visits: + +```tsx + + {/* ... */} + +``` + +Use the [`useTheme`](../api/use-theme.md) hook to read or change the theme at runtime: + + + +```tsx +function ThemeToggle() { + const { resolvedTheme, setTheme } = useTheme(); + return ( + + ); +} ``` -## 2. Add the Vite plugin +Drop the pre-built [`AppearanceSwitcher`](../components/appearance-switcher.md) component anywhere in your layout for a ready-made light/dark/system toggle: -```ts -// vite.config.ts -import react from "@vitejs/plugin-react"; -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; + -export default defineConfig({ - plugins: [react(), tailwindcss()], -}); +```tsx +function AppearanceToggle() { + return ; +} ``` -## 3. Import Tailwind + the app-shell theme +## Theme Palettes -Import Tailwind first, then app-shell's base styles, then one theme: +AppShell ships three palettes, each with light and dark variants: + +| Palette | CSS import | +| --------- | -------------------------------------------------------------- | +| `default` | Included automatically via `@tailor-platform/app-shell/styles` | +| `cream` | `@tailor-platform/app-shell/themes/cream` | +| `bloom` | `@tailor-platform/app-shell/themes/bloom` | + +Select a palette by importing its CSS file — no prop needed. Import it in your global CSS **after** `@tailor-platform/app-shell/styles`: ```css -/* src/index.css */ -@import "tailwindcss"; @import "@tailor-platform/app-shell/styles"; -@import "@tailor-platform/app-shell/themes/bloom"; +@import "@tailor-platform/app-shell/themes/cream"; /* overrides default palette */ +@import "tailwindcss"; ``` -## 4. Use ordinary Tailwind in your code +Only import one palette at a time. -Your own markup uses unprefixed utilities; app-shell components bring their own styling: +## Z-Index Layering -```tsx -import { Button } from "@tailor-platform/app-shell"; +AppShell defines CSS custom properties for z-index values so you can adjust the stacking order to integrate with other libraries or overlays in your application. + +These properties are defined in `:root` and can be overridden in your own CSS: + +| Property | Default | Used for | +| ------------------ | ------- | ------------------------------------------------------------------- | +| `--z-sidebar` | `10` | The sidebar panel | +| `--z-sidebar-rail` | `20` | The sidebar collapse rail / toggle button | +| `--z-popup` | `50` | Portal-based popups (Menu, Select, Combobox, Autocomplete, Tooltip) | +| `--z-overlay` | `50` | Modal overlays (Dialog, Sheet) | + +### Example: Raising popup z-index -
- - -
; +```css +/* your-app/globals.css */ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; + +:root { + --z-popup: 100; +} ``` -> **Do not** use the `astw:` prefix in your app — it is reserved for app-shell's internal stylesheet. If you see it in generated example code, that's a bug in the example (report it). +## Adding a palette + +Theme tokens live in `packages/core/src/assets/themes/`. Copy `_template.css` to start a new palette — it lists exactly which sections to fill in for light and dark mode. + +| Section | Required? | What to set | +| --------------------- | --------------------- | ------------------------------------------------------------------------------ | +| **1. Brand** | Yes | `primary`, `secondary`, `accent` (+ foregrounds) — both modes | +| **2. Shell gradient** | Branded palettes only | `--shell-gradient-base`, `--shell-gradient-tint` | +| **3. System** | Tune or copy default | Surfaces: background, card, popover, muted, borders | +| **4. Palette** | Optional | Radius, chart colors, shadows | +| **5. Semantic** | Do not duplicate | Status and alert tokens inherit from `default.css` | +| **6. Structural** | Branded palettes | Copy the structural override block from `bloom.css` or `cream.css` when needed | -## Reference implementation +A palette is selected by CSS import, not by an AppShell prop. Import exactly one theme file after `@tailor-platform/app-shell/styles`; if you import none, the default palette from `styles` is used. -The docs explorer app (`examples/docs-app`) and the Vite example (`examples/vite-app`) are both wired exactly this way — use them as a copy-paste reference. +Preview token values at `/custom-page/color` in the Next.js example app. diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json index 77ac3c90..183ca586 100644 --- a/docs/docs-manifest.json +++ b/docs/docs-manifest.json @@ -144,16 +144,16 @@ "kind": "prose", "outline": "docs-src/concepts/styling.docs.outline.md", "output": "docs/concepts/styling.md", - "examples": null, + "examples": "docs-src/concepts/styling.docs.examples.tsx", "sources": [], "claims": [], "symbols": [], "hashes": { "typeSurface": null, - "outline": "057d44a8467a93c1", + "outline": "ecf30ad9f0be57dc", "snapshot": null, - "outputMd": "92810a764123d6df", - "examples": null + "outputMd": "4dbd167697db66cd", + "examples": "d3cca347b055a503" } } } From 68bec7a78fd61f576b47f65a5ef6aaf6e1dbec63 Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 29 Jul 2026 15:47:10 +1000 Subject: [PATCH 12/15] feat(docs-browser): render markdown tables with the AppShell Table Dogfood the library: map each markdown table element (table/thead/tbody/ tfoot/tr/th/td/caption) onto the corresponding Table sub-component in DocPage, so every doc table (auto prop tables + prose tables) gets Table's styling and scrollable container instead of the default unstyled
. Assumes basic 2-D tables; drops react-markdown's `node` hast prop and remark-gfm's deprecated `align` attr (alignment still arrives via inline style, which Table honors). Co-Authored-By: Claude Opus 4.8 --- docs-browser/src/_lib/DocPage.tsx | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs-browser/src/_lib/DocPage.tsx b/docs-browser/src/_lib/DocPage.tsx index 8295e947..7e8325b8 100644 --- a/docs-browser/src/_lib/DocPage.tsx +++ b/docs-browser/src/_lib/DocPage.tsx @@ -1,8 +1,9 @@ +import { type ComponentProps } from "react"; import ReactMarkdown, { type Components } from "react-markdown"; import rehypeRaw from "rehype-raw"; import remarkGfm from "remark-gfm"; -import { Layout } from "@tailor-platform/app-shell"; +import { Layout, Table } from "@tailor-platform/app-shell"; import { units } from "../docs"; @@ -44,6 +45,34 @@ export function DocPage({ slug }: { slug: string }) { ); }, + // Dogfood the AppShell Table for every markdown table — remark-gfm emits + // basic 2-D tables, so each element maps 1:1 onto a Table sub-component + // (`node` is react-markdown's hast node, not a DOM prop; drop it). + table: ({ node, ...props }: ComponentProps<"table"> & { node?: unknown }) => ( + + ), + thead: ({ node, ...props }: ComponentProps<"thead"> & { node?: unknown }) => ( + + ), + tbody: ({ node, ...props }: ComponentProps<"tbody"> & { node?: unknown }) => ( + + ), + tfoot: ({ node, ...props }: ComponentProps<"tfoot"> & { node?: unknown }) => ( + + ), + tr: ({ node, ...props }: ComponentProps<"tr"> & { node?: unknown }) => , + // `align` is remark-gfm's deprecated string attr; drop it (alignment also + // arrives as inline `style`, which Table.Head/Cell honor) so it doesn't + // clash with Table's typed `align`. + th: ({ node, align, ...props }: ComponentProps<"th"> & { node?: unknown }) => ( + + ), + td: ({ node, align, ...props }: ComponentProps<"td"> & { node?: unknown }) => ( + + ), + caption: ({ node, ...props }: ComponentProps<"caption"> & { node?: unknown }) => ( + + ), } as Components; return ( From 5c045de030333d8fa479105bdd8dbd8d62490d66 Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 29 Jul 2026 16:36:00 +1000 Subject: [PATCH 13/15] feat(docs-browser): styled headings + Preview/Code tabs for examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Headings: custom h1–h4 renderers with a simple typographic scale on semantic tokens (theme-aware); h2 gets a section hairline. - Example previews: a rehype plugin pairs each `` anchor with the ```tsx``` fence that follows it — unwrapping the paragraph react-markdown parses the inline custom tag into (which also fixes a
-in-

nesting error) and nesting the

 into the anchor — so the example renders as a
  Preview/Code tab pair dogfooding the AppShell Tabs. The .md stays unchanged
  (anchor + plain fence), so GitHub/raw-md rendering is unaffected.
- Header now shows the real frontmatter title; the loader strips the body's
  leading H1 so it isn't duplicated.

Co-Authored-By: Claude Opus 4.8 
---
 docs-browser/src/_lib/DocPage.tsx | 126 ++++++++++++++++++++++++++----
 docs-browser/src/docs.tsx         |  19 ++++-
 2 files changed, 128 insertions(+), 17 deletions(-)

diff --git a/docs-browser/src/_lib/DocPage.tsx b/docs-browser/src/_lib/DocPage.tsx
index 7e8325b8..15470bbb 100644
--- a/docs-browser/src/_lib/DocPage.tsx
+++ b/docs-browser/src/_lib/DocPage.tsx
@@ -1,9 +1,9 @@
-import { type ComponentProps } from "react";
+import { type ComponentProps, type ReactNode } from "react";
 import ReactMarkdown, { type Components } from "react-markdown";
 import rehypeRaw from "rehype-raw";
 import remarkGfm from "remark-gfm";
 
-import { Layout, Table } from "@tailor-platform/app-shell";
+import { Layout, Table, Tabs } from "@tailor-platform/app-shell";
 
 import { units } from "../docs";
 
@@ -14,10 +14,68 @@ function pascalCase(key: string): string {
     .join("");
 }
 
+interface HastNode {
+  type: string;
+  tagName?: string;
+  value?: string;
+  children?: HastNode[];
+}
+
+const isBlank = (n: HastNode | undefined): boolean =>
+  !!n && n.type === "text" && !(n.value ?? "").trim();
+
+/** An `` element, either bare or as the sole child of a `

` + * (react-markdown parses the custom tag as INLINE html, so it lands inside a + * paragraph). */ +function anchorIn(node: HastNode): HastNode | null { + if (node.type === "element" && node.tagName === "example-preview") return node; + if (node.type === "element" && node.tagName === "p") { + const inner = (node.children ?? []).filter((n) => !isBlank(n)); + if (inner.length === 1 && inner[0].tagName === "example-preview") return inner[0]; + } + return null; +} + +// The assembler emits an `` anchor immediately followed by the +// example's ```tsx``` fence (two separate blocks, so GitHub/raw-md still shows +// the fence as ordinary code). In the browser we tie them together: unwrap the +// paragraph react-markdown parses the inline custom tag into, then nest the +// following `

` INTO the anchor — so a single `example-preview` component
+// receives both the example name and its rendered code and can tab between
+// them (and its 
is no longer illegally nested in a

). Runs after +// rehype-raw, which turns the raw anchor into an element. +function rehypePairExamples() { + return (tree: HastNode) => { + const walk = (node: HastNode): void => { + const kids = node.children; + if (!kids) return; + for (let i = 0; i < kids.length; i++) { + const anchor = anchorIn(kids[i]); + if (anchor) { + let j = i + 1; + while (j < kids.length && isBlank(kids[j])) j++; + const next = kids[j] as HastNode | undefined; + if (next && next.type === "element" && next.tagName === "pre") { + anchor.children = [next]; + kids.splice(i + 1, j - i); // drop the whitespace + the now-nested

+          }
+          kids[i] = anchor; // hoist out of the wrapping 

if there was one + } + walk(kids[i]); + } + }; + walk(tree); + }; +} + +const rehypePlugins = [rehypeRaw, rehypePairExamples] as ComponentProps< + typeof ReactMarkdown +>["rehypePlugins"]; + // Shared renderer for a single documented unit: prose (markdown) with each live -// example rendered IN PLACE at the `` anchor the -// assembler drops next to that example's code fence. -// Lives under _lib/ so file-based routing does not treat it as a page. +// example rendered IN PLACE — as a Preview/Code tab pair — at the +// `` anchor, and every markdown table rendered via the AppShell +// Table. Lives under _lib/ so file-based routing does not treat it as a page. export function DocPage({ slug }: { slug: string }) { const unit = units.find((u) => u.slug === slug); @@ -33,18 +91,56 @@ export function DocPage({ slug }: { slug: string }) { const byExportName = new Map(unit.examples.map((example) => [example.name, example])); const components = { - "example-preview": (props: { name?: string; node?: { properties?: { name?: string } } }) => { - const key = props.name ?? props.node?.properties?.name; - const example = key ? byExportName.get(pascalCase(String(key))) : undefined; + // A tabbed Preview / Code pair. `children` is the `

` the rehype plugin
+    // nested into the anchor; the live component comes from the examples module.
+    "example-preview": ({
+      name,
+      node,
+      children,
+    }: {
+      name?: string;
+      node?: { properties?: { name?: string } };
+      children?: ReactNode;
+    }) => {
+      const exampleKey = name ?? node?.properties?.name;
+      const example = exampleKey ? byExportName.get(pascalCase(String(exampleKey))) : undefined;
       if (!example) return null;
       return (
-        
-
- -
+
+ + + Preview + Code + + + + + + {children} + +
); }, + + // Headings — no app-shell typography primitive yet, so give the docs a + // simple scale with semantic tokens (theme-aware in light/dark). + h1: ({ node, ...props }: ComponentProps<"h1"> & { node?: unknown }) => ( +

+ ), + h2: ({ node, ...props }: ComponentProps<"h2"> & { node?: unknown }) => ( +

+ ), + h3: ({ node, ...props }: ComponentProps<"h3"> & { node?: unknown }) => ( +

+ ), + h4: ({ node, ...props }: ComponentProps<"h4"> & { node?: unknown }) => ( +

+ ), + // Dogfood the AppShell Table for every markdown table — remark-gfm emits // basic 2-D tables, so each element maps 1:1 onto a Table sub-component // (`node` is react-markdown's hast node, not a DOM prop; drop it). @@ -77,12 +173,12 @@ export function DocPage({ slug }: { slug: string }) { return ( - + -
+
{unit.markdown} diff --git a/docs-browser/src/docs.tsx b/docs-browser/src/docs.tsx index 1066e686..1564c775 100644 --- a/docs-browser/src/docs.tsx +++ b/docs-browser/src/docs.tsx @@ -39,6 +39,7 @@ export interface DocUnit { slug: string; kind: string; category: string; + title: string | null; markdown: string; source: string | null; examples: Example[]; @@ -46,12 +47,24 @@ export interface DocUnit { const key = (repoRel: string): string => `../../${repoRel}`; +/** Pull the `title:` out of the generated frontmatter — shown in the page + * header (the body's own H1 is stripped by cleanMarkdown to avoid duplication). */ +function frontmatterTitle(md: string): string | null { + const block = md.match(/^---\n([\s\S]*?)\n---/); + const line = block?.[1].match(/^title:\s*(.+)$/m); + return line ? line[1].trim() : null; +} + /** Strip the generated frontmatter block and HTML comments before rendering — - * react-markdown would otherwise show them as literal text. */ + * react-markdown would otherwise show them as literal text — and drop the + * leading H1 (the page header renders the title instead, so keeping it would + * duplicate it). */ function cleanMarkdown(md: string): string { return md .replace(/^---\n[\s\S]*?\n---\n/, "") .replace(//g, "") + .trim() + .replace(/^#\s+[^\n]*\r?\n+/, "") .trim(); } @@ -62,11 +75,13 @@ export const units: DocUnit[] = Object.values(manifest.units).map((entry) => { .filter(([, value]) => typeof value === "function") .map(([name, value]) => ({ name, Component: value as ComponentType })) : []; + const raw = markdown[key(entry.output)] ?? ""; return { slug: entry.slug, kind: entry.kind, category: entry.output.split("/")[1] ?? "misc", - markdown: cleanMarkdown(markdown[key(entry.output)] ?? ""), + title: frontmatterTitle(raw), + markdown: cleanMarkdown(raw), source: entry.examples ? (exampleSource[key(entry.examples)] ?? null) : null, examples, }; From e2266c4d19a0695a16bc86ffc06a88db2a5ea891 Mon Sep 17 00:00:00 2001 From: interacsean Date: Wed, 29 Jul 2026 16:51:11 +1000 Subject: [PATCH 14/15] feat(docs-browser): consistent code-block styling (no highlighting yet) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give every code block a tidy, distinct muted surface + monospace via `pre`/ `code` renderer overrides — shared by standalone fences and the example Code tab (the tab panel just flattens the pre's margin/radius so it fills the card). Inline code (prose, tables, headings) keeps a subtle chip; `pre` resets that chip for its child so fenced blocks read as one surface. No real syntax highlighting — deferred. Co-Authored-By: Claude Opus 4.8 --- docs-browser/src/_lib/DocPage.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs-browser/src/_lib/DocPage.tsx b/docs-browser/src/_lib/DocPage.tsx index 15470bbb..518d9c01 100644 --- a/docs-browser/src/_lib/DocPage.tsx +++ b/docs-browser/src/_lib/DocPage.tsx @@ -115,7 +115,7 @@ export function DocPage({ slug }: { slug: string }) { - + {children} @@ -141,6 +141,23 @@ export function DocPage({ slug }: { slug: string }) {

), + // Code — a tidy, distinct surface + monospace (no real syntax highlighting + // yet). The same styles apply to standalone fences AND the Code tab; a + // fenced block is `pre > code`, so `pre` resets the inline `code` chip for + // its child. Inline `code` (in prose, tables, headings) keeps the chip. + pre: ({ node, ...props }: ComponentProps<"pre"> & { node?: unknown }) => ( +
+    ),
+    code: ({ node, className, ...props }: ComponentProps<"code"> & { node?: unknown }) => (
+      
+    ),
+
     // Dogfood the AppShell Table for every markdown table — remark-gfm emits
     // basic 2-D tables, so each element maps 1:1 onto a Table sub-component
     // (`node` is react-markdown's hast node, not a DOM prop; drop it).

From 7d2b9c724f6e4a26440f8f8974ada915d8b5a11f Mon Sep 17 00:00:00 2001
From: interacsean 
Date: Wed, 29 Jul 2026 19:53:25 +1000
Subject: [PATCH 15/15] fix(docs): drop `Layout fill` from the dense-scan
 example so the docs page scrolls

`` sets `data-layout-fill`, which the AppShell content area detects
via `:has([data-layout-fill])` and switches into fill-mode (page scroll off,
inner scroll on). Nested inside the docs page, the example's attribute leaked up
through `:has()` and disabled the whole page's scroll whenever the Preview tab
was mounted. A fill *page* embedded inside another page is the conflict; the
example demonstrates the pattern fine with a normal ``.

Co-Authored-By: Claude Opus 4.8 
---
 docs-src/patterns/list-dense-scan.docs.examples.tsx | 2 +-
 docs/docs-manifest.json                             | 4 ++--
 docs/patterns/list-dense-scan.md                    | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs-src/patterns/list-dense-scan.docs.examples.tsx b/docs-src/patterns/list-dense-scan.docs.examples.tsx
index cba449a5..b8aa7af2 100644
--- a/docs-src/patterns/list-dense-scan.docs.examples.tsx
+++ b/docs-src/patterns/list-dense-scan.docs.examples.tsx
@@ -75,7 +75,7 @@ export function DenseScan() {
   const table = useDataTable({ data: { rows: orders, total: orders.length }, columns });
 
   return (
-    
+    
       
+