diff --git a/.agents/skills/resync-docs/SKILL.md b/.agents/skills/resync-docs/SKILL.md new file mode 100644 index 00000000..3ca78fe8 --- /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 `*.docs.examples.tsx` (runnable examples), both authored SOURCE colocated in `src`. 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 + `*.docs.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 ``. + - `*.docs.examples.tsx` (colocated with the outline in `src`, a sibling of the `*.docs.outline.md`): 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 `*.docs.examples.tsx`, refreshes the API section, updates `docs/docs-manifest.json` hashes, and (re)generates each unit's docs-browser route stub under `docs-browser/src/pages///page.tsx` — so a brand-new unit appears in the browser with no manual wiring. + +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 `*.docs.outline.md` and `*.docs.examples.tsx`. The docs-browser route stubs (`docs-browser/src/pages/**/page.tsx`, except the authored app-shell files: `App.tsx`, `_lib/`, and the home `pages/page.tsx`) are likewise generated by `sync` — never hand-edit them. +- 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/catalogue/src/pattern/form/modal/modal-form.tsx b/catalogue/src/pattern/form/modal/modal-form.tsx deleted file mode 100644 index 2dbe9fd3..00000000 --- a/catalogue/src/pattern/form/modal/modal-form.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* pattern: form/modal */ -import { Button, Dialog, Input, Field } from "@tailor-platform/app-shell"; - -type Props = { - onSave: (data: { label: string; street: string; city: string }) => void; -}; - -export default function ModalForm({ onSave }: Props) { - return ( - - }>Add address - - - Add address - 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, - }); - }} - > -
- - Label - } /> - - - Street - } /> - - - City - } /> - -
- - }>Cancel - - -
-
-
- ); -} diff --git a/catalogue/src/pattern/list/dense-scan/dense-scan.tsx b/catalogue/src/pattern/list/dense-scan/dense-scan.tsx deleted file mode 100644 index 80818cc4..00000000 --- a/catalogue/src/pattern/list/dense-scan/dense-scan.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* 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"; - -type Props = { - data: DataTableData; - onCreateClick: () => void; -}; - -export default function DenseScanList({ data, onCreateClick }: Props) { - const table = useDataTable({ data, 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). - - - Create Order - , - ]} - /> - - - - - - - - - - - - - ); -} diff --git a/decisions/documentation-management-overhaul.md b/decisions/documentation-management-overhaul.md new file mode 100644 index 00000000..ae6ed4fa --- /dev/null +++ b/decisions/documentation-management-overhaul.md @@ -0,0 +1,215 @@ +# 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, **and** `*.docs.examples.tsx` — runnable examples (both authored SOURCE, colocated in `src`) | **Human owns intent** (AI may draft; human curates & approves at PR review) | ✅ the **only** hand-editable surfaces | +| `docs/**/[unit].md`, `docs-manifest.json`, the docs-browser route stubs (`docs-browser/src/pages/**/page.tsx`), the `app-shell-patterns` skill | **AI synthesizes** (`resync-docs`), human reviews the diff | ❌ never | + +One rule: **humans edit intent (outlines + examples, in `src`); AI produces everything under `docs/` (and the browser route stubs) 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].docs.examples.tsx`** — authored SOURCE, colocated with the outline in `src` — 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** (`docs-browser/`, dogfooding). Build-time `import.meta.glob` of the generated `docs/**/*.md` + the authored `**/*.docs.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 / `*.docs.examples.tsx` / snapshot) **and** the generated-`.md` output hash. `check-docs` recomputes and compares — no git-history diffing. Input drift (incl. an examples edit that leaves the `.md` fences stale) ⇒ needs resync; **a hand-edited generated `.md` ⇒ output-hash mismatch ⇒ CI fail**. | +| **Migration** | AI **reverse-generates** outlines + `*.docs.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 (SOURCE, colocated in src)"] + OUT["*.docs.outline.md
prose + example intents + sources + flags"] + EXA["*.docs.examples.tsx
runnable examples"] + SRC["component / hook / theme source"] + end + + OUT -->|edit intent| GATE + EXA -->|edit examples| GATE + SRC -->|edit changes type surface| GATE + + subgraph GATE["check-docs — deterministic, NO LLM"] + H["recompute hashes:
type-surface / outline / examples / 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"] + STUB["docs-browser route stubs
src/pages/**/page.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-browser (AppShell)
live examples + source"] + EXA -->|import.meta.glob| APP + STUB -->|file-based routes| 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.json 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/core/src/**/[unit].docs.examples.tsx ← authored runnable examples, colocated (a sibling of the outline) +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/**/[pattern].docs.examples.tsx ← authored examples for prose patterns +docs-src/references/*.docs.outline.md ← re-export reference outlines (claims by name + upstream link) + +docs/**/[unit].md ← generated output (repo root, as today); examples are NOT here — they are authored source in src +docs/docs-manifest.json ← committed generated baseline +docs.config.json ← exclusions list + location→category rules + pagesDir (route-stub target) + +docs-browser/ ← NEW top-level AppShell renderer app (dogfoods app-shell); src/pages/**/page.tsx route stubs are GENERATED by sync +examples/ ← consumer example apps only (nextjs-app, vite-app) +.agents/skills/resync-docs/ ← NEW local LLM skill +``` + +- **`examples/` holds only consumer example apps** (a reference AppShell implementation); it no longer doubles as the docs surface — authored examples now live in colocated `[unit].docs.examples.tsx` (source) and are shown in the `docs-browser` 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** (top-level `docs-browser/`): `import.meta.glob` → one AppShell route per unit (via generated `src/pages/**/page.tsx` stubs); renders prose + at each token the live component (from the colocated `*.docs.examples.tsx`) and its extracted source. Dev needs `server.fs.allow` for the out-of-app source dirs + a tsconfig include so the examples type-check against `@tailor-platform/app-shell`. +5. **CI + hooks**: add `check-docs` turbo task to `ci-packages.yaml` (blocks); type-check the authored `**/*.docs.examples.tsx` (they are excluded from core's own compile and type-checked by the docs-browser instead); `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 + `*.docs.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; edit a `*.docs.examples.tsx` (stale `.md` fences) → 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` → block** (output-hash mismatch). +3. **Examples compile:** break a `*.docs.examples.tsx` (removed prop) → CI type-check fails. +4. **Renderer app:** `preview_start` the docs-browser; a unit page shows live component + matching source; edit a `*.docs.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 + the `docs-browser` 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-browser is wired exactly that way as the reference. +- **Examples are authored SOURCE colocated in `src` (`*.docs.examples.tsx`), outside the docs-browser app**, and they import the package's own public name `@tailor-platform/app-shell` — so their bare imports don't resolve against the browser app's `node_modules` by default. Two wiring details make them type-check and run: + - docs-browser **tsconfig** `include`s the out-of-app example globs and 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`. Core's own tsconfig **excludes** `**/*.docs.examples.tsx` (they import the package it builds), so they are type-checked by the docs-browser, not by core. + - docs-browser **Vite** resolves the out-of-app examples' bare imports via the **`.docs.examples.tsx`-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` over the example globs so their utility classes are generated. +- **The docs-browser is a real `` app** (top-level `docs-browser/`) 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.examples.tsx`-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`). +- **`sync` generates the per-unit route stubs.** `pagesDir` in `docs.config.json` (`docs-browser/src/pages`) tells `sync` to write `//page.tsx` for every unit — category is the outline's outDir minus its `docs/` prefix, slug + title come from the outline (title falls back to a title-cased slug). Each stub is pure glue that mounts `DocPage`, so a brand-new unit appears in the browser with zero manual wiring. The stubs are regenerated every sync and never hand-edited; the only remaining authored browser surface is the app shell (`App.tsx`, `_lib/`, the home `pages/page.tsx`). They are deliberately **not** manifest-tracked — deterministic app glue, not published output. +- **`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-browser/index.html b/docs-browser/index.html new file mode 100644 index 00000000..0018baf8 --- /dev/null +++ b/docs-browser/index.html @@ -0,0 +1,12 @@ + + + + + + app-shell docs + + +
+ + + diff --git a/docs-browser/package.json b/docs-browser/package.json new file mode 100644 index 00000000..33ba3a5b --- /dev/null +++ b/docs-browser/package.json @@ -0,0 +1,31 @@ +{ + "name": "@tailor-platform/app-shell-docs-browser", + "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", + "rehype-raw": "^7.0.0", + "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/docs-browser/src/App.tsx b/docs-browser/src/App.tsx new file mode 100644 index 00000000..ba8de39a --- /dev/null +++ b/docs-browser/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/docs-browser/src/_lib/DocPage.tsx b/docs-browser/src/_lib/DocPage.tsx new file mode 100644 index 00000000..518d9c01 --- /dev/null +++ b/docs-browser/src/_lib/DocPage.tsx @@ -0,0 +1,207 @@ +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, Tabs } from "@tailor-platform/app-shell"; + +import { units } from "../docs"; + +function pascalCase(key: string): string { + return key + .split(/[-_]/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .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 — 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); + + if (!unit) { + return ( + + + Unknown doc unit: {slug} + + ); + } + + const byExportName = new Map(unit.examples.map((example) => [example.name, example])); + + const components = { + // 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 }) => ( +

+ ), + + // 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).
+    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 (
+    
+      
+      
+        
+ + {unit.markdown} + +
+
+
+ ); +} diff --git a/docs-browser/src/docs.tsx b/docs-browser/src/docs.tsx new file mode 100644 index 00000000..1564c775 --- /dev/null +++ b/docs-browser/src/docs.tsx @@ -0,0 +1,88 @@ +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; +// Examples are authored SOURCE, colocated with each outline in src/ (not in the +// generated docs/ output), so glob the two source trees. +const exampleSource = import.meta.glob( + ["../../packages/core/src/**/*.docs.examples.tsx", "../../docs-src/**/*.docs.examples.tsx"], + { query: "?raw", import: "default", eager: true }, +) as Record; +const exampleModule = import.meta.glob( + ["../../packages/core/src/**/*.docs.examples.tsx", "../../docs-src/**/*.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; + title: string | null; + markdown: string; + source: string | null; + examples: Example[]; +} + +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 — 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(); +} + +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 })) + : []; + const raw = markdown[key(entry.output)] ?? ""; + return { + slug: entry.slug, + kind: entry.kind, + category: entry.output.split("/")[1] ?? "misc", + title: frontmatterTitle(raw), + markdown: cleanMarkdown(raw), + source: entry.examples ? (exampleSource[key(entry.examples)] ?? null) : null, + examples, + }; +}); diff --git a/docs-browser/src/index.css b/docs-browser/src/index.css new file mode 100644 index 00000000..e1e5a597 --- /dev/null +++ b/docs-browser/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/docs-browser/src/main.tsx b/docs-browser/src/main.tsx new file mode 100644 index 00000000..d6a9a8ac --- /dev/null +++ b/docs-browser/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/docs-browser/src/pages/components/button/page.tsx b/docs-browser/src/pages/components/button/page.tsx new file mode 100644 index 00000000..a6e3950f --- /dev/null +++ b/docs-browser/src/pages/components/button/page.tsx @@ -0,0 +1,12 @@ +// GENERATED by docs-kit — do not edit by hand. +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/docs-browser/src/pages/components/data-table/page.tsx b/docs-browser/src/pages/components/data-table/page.tsx new file mode 100644 index 00000000..9a1dd20c --- /dev/null +++ b/docs-browser/src/pages/components/data-table/page.tsx @@ -0,0 +1,12 @@ +// GENERATED by docs-kit — do not edit by hand. +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/docs-browser/src/pages/components/dialog/page.tsx b/docs-browser/src/pages/components/dialog/page.tsx new file mode 100644 index 00000000..9fc12bde --- /dev/null +++ b/docs-browser/src/pages/components/dialog/page.tsx @@ -0,0 +1,12 @@ +// GENERATED by docs-kit — do not edit by hand. +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/docs-browser/src/pages/concepts/styling/page.tsx b/docs-browser/src/pages/concepts/styling/page.tsx new file mode 100644 index 00000000..f7f71c6b --- /dev/null +++ b/docs-browser/src/pages/concepts/styling/page.tsx @@ -0,0 +1,12 @@ +// GENERATED by docs-kit — do not edit by hand. +import type { AppShellPageProps } from "@tailor-platform/app-shell"; + +import { DocPage } from "../../../_lib/DocPage"; + +const Page = () => ; + +Page.appShellPageProps = { + meta: { title: "Styling and Theming" }, +} satisfies AppShellPageProps; + +export default Page; diff --git a/docs-browser/src/pages/page.tsx b/docs-browser/src/pages/page.tsx new file mode 100644 index 00000000..30b7487a --- /dev/null +++ b/docs-browser/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/docs-browser/src/pages/patterns/form-modal/page.tsx b/docs-browser/src/pages/patterns/form-modal/page.tsx new file mode 100644 index 00000000..274584bf --- /dev/null +++ b/docs-browser/src/pages/patterns/form-modal/page.tsx @@ -0,0 +1,12 @@ +// GENERATED by docs-kit — do not edit by hand. +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/docs-browser/src/pages/patterns/list-dense-scan/page.tsx b/docs-browser/src/pages/patterns/list-dense-scan/page.tsx new file mode 100644 index 00000000..d5533527 --- /dev/null +++ b/docs-browser/src/pages/patterns/list-dense-scan/page.tsx @@ -0,0 +1,12 @@ +// GENERATED by docs-kit — do not edit by hand. +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/docs-browser/tsconfig.json b/docs-browser/tsconfig.json new file mode 100644 index 00000000..d3032cb2 --- /dev/null +++ b/docs-browser/tsconfig.json @@ -0,0 +1,32 @@ +{ + "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 authored examples (*.docs.examples.tsx) live in src/ (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", + "../packages/core/src/**/*.docs.examples.tsx", + "../docs-src/**/*.docs.examples.tsx" + ] +} diff --git a/docs-browser/vite.config.ts b/docs-browser/vite.config.ts new file mode 100644 index 00000000..b55554f7 --- /dev/null +++ b/docs-browser/vite.config.ts @@ -0,0 +1,55 @@ +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)); +// The authored examples (`*.docs.examples.tsx`) live in src/ (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 example files by suffix 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.endsWith(".docs.examples.tsx")) 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/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 new file mode 100644 index 00000000..4341b0e6 --- /dev/null +++ b/docs-src/concepts/styling.docs.outline.md @@ -0,0 +1,126 @@ +--- +group: styling +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: + + + +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 +@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-src/patterns/form-modal.docs.examples.tsx b/docs-src/patterns/form-modal.docs.examples.tsx new file mode 100644 index 00000000..4b532a29 --- /dev/null +++ b/docs-src/patterns/form-modal.docs.examples.tsx @@ -0,0 +1,94 @@ +import { useState } from "react"; + +import { Button, Dialog, Field, Input, Layout } from "@tailor-platform/app-shell"; + +export 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 form has its own URL but renders as a popup over the +// list — both `/products` and `/products/create` render this same component, so +// the list stays visible underneath. Local state stands in for the router here; +// in a real app `isCreateOpen` derives from the route and the handlers call +// `useNavigate()` to move between `/products` and `/products/create`. +export 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 + } /> + +
+ + + + +
+
+
+
+ ); +} diff --git a/catalogue/src/pattern/form/modal/PATTERN.md b/docs-src/patterns/form-modal.docs.outline.md similarity index 88% rename from catalogue/src/pattern/form/modal/PATTERN.md rename to docs-src/patterns/form-modal.docs.outline.md index 0c1c454e..bbed559e 100644 --- a/catalogue/src/pattern/form/modal/PATTERN.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,13 @@ 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.examples.tsx b/docs-src/patterns/list-dense-scan.docs.examples.tsx new file mode 100644 index 00000000..b8aa7af2 --- /dev/null +++ b/docs-src/patterns/list-dense-scan.docs.examples.tsx @@ -0,0 +1,100 @@ +import { Badge, Button, DataTable, Input, Layout, useDataTable } from "@tailor-platform/app-shell"; +import type { Column } from "@tailor-platform/app-shell"; + +type Order = { + id: string; + orderNumber: string; + customer: string; + status: "draft" | "confirmed" | "shipped" | "delivered"; + amount: number; + createdAt: string; +}; + +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 ( + + + Create Order + , + ]} + /> + + + + + + + + + + + + + ); +} diff --git a/catalogue/src/pattern/list/dense-scan/PATTERN.md b/docs-src/patterns/list-dense-scan.docs.outline.md similarity index 95% rename from catalogue/src/pattern/list/dense-scan/PATTERN.md rename to docs-src/patterns/list-dense-scan.docs.outline.md index 66d1c763..fc0f6361 100644 --- a/catalogue/src/pattern/list/dense-scan/PATTERN.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..0190027a --- /dev/null +++ b/docs.config.json @@ -0,0 +1,18 @@ +{ + "$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__", + "pagesDir": "docs-browser/src/pages" +} diff --git a/docs/components/button.md b/docs/components/button.md index ff94d548..2226069e 100644 --- a/docs/components/button.md +++ b/docs/components/button.md @@ -3,6 +3,8 @@ title: Button description: Styled button with multiple variants and sizes --- + + # 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). @@ -15,10 +17,18 @@ import { Button } from "@tailor-platform/app-shell"; ## Basic Usage + + ```tsx - - - +function BasicUsage() { + return ( +
+ + + +
+ ); +} ``` ## Props @@ -35,23 +45,41 @@ All standard HTML ` - - - - - +function Variants() { + return ( +
+ + + + + + +
+ ); +} ``` ## Sizes + + ```tsx - - - - - +function Sizes() { + return ( +
+ + + + + +
+ ); +} ``` ## Render Prop @@ -126,3 +154,21 @@ import { buttonVariants } from "@tailor-platform/app-shell"; - [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.md b/docs/components/data-table.md index d17c7e59..10050c99 100644 --- a/docs/components/data-table.md +++ b/docs/components/data-table.md @@ -3,6 +3,8 @@ title: DataTable description: Compound data table component with sortable columns, filter chips, cursor-based pagination, row actions, and multi-row selection --- + + # 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. @@ -138,6 +140,29 @@ function JournalsPage() { } ``` +## 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. @@ -772,3 +797,70 @@ const columns = [ - [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.md b/docs/components/dialog.md index 1fb1e74c..1a7ed9c3 100644 --- a/docs/components/dialog.md +++ b/docs/components/dialog.md @@ -3,6 +3,8 @@ title: Dialog description: Modal dialog with a compound component API --- + + # Dialog The `Dialog` component provides a modal dialog with a compound component API. It is backed by Base UI's Dialog primitive. @@ -15,20 +17,26 @@ import { Dialog } from "@tailor-platform/app-shell"; ## Basic Usage + + ```tsx - - }>Open - - - Confirm Action - Are you sure you want to proceed? - - - }>Cancel - - - - +function BasicUsage() { + return ( + + }>Open + + + Confirm Action + Are you sure you want to proceed? + + + }>Cancel + + + + + ); +} ``` ## Sub-components @@ -66,20 +74,29 @@ Accept a `render` prop for custom element rendering (Base UI render pattern), pl ## Controlled Usage + + ```tsx -const [open, setOpen] = useState(false); - - - }>Open Dialog - - - Settings - - - - - -; +function Controlled() { + const [open, setOpen] = useState(false); + return ( + <> + + + + + Settings + + + + + + + + ); +} ``` ## Examples @@ -145,3 +162,7 @@ function EditDialog({ item }: { item: Item }) { - [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-theming.md b/docs/concepts/styling.md similarity index 94% rename from docs/concepts/styling-theming.md rename to docs/concepts/styling.md index 90a6623f..1e3a411f 100644 --- a/docs/concepts/styling-theming.md +++ b/docs/concepts/styling.md @@ -3,6 +3,8 @@ 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. @@ -56,11 +58,11 @@ Set the initial mode via the `defaultColorTheme` prop on ``. The user' Use the [`useTheme`](../api/use-theme.md) hook to read or change the theme at runtime: -```tsx -import { useTheme } from "@tailor-platform/app-shell"; + +```tsx function ThemeToggle() { - const { theme, resolvedTheme, setTheme } = useTheme(); + const { resolvedTheme, setTheme } = useTheme(); return ( + + + + + ); +} +``` + +## 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. + + + +```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 +- 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.md b/docs/patterns/list-dense-scan.md new file mode 100644 index 00000000..da8f13ae --- /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 + +## 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/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.examples.tsx b/packages/core/src/components/button.docs.examples.tsx new file mode 100644 index 00000000..8b35de9c --- /dev/null +++ b/packages/core/src/components/button.docs.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/packages/core/src/components/button.docs.outline.md b/packages/core/src/components/button.docs.outline.md new file mode 100644 index 00000000..973f94f1 --- /dev/null +++ b/packages/core/src/components/button.docs.outline.md @@ -0,0 +1,114 @@ +--- +title: Button +description: Styled button with multiple variants and sizes +group: button +sources: + - packages/core/src/components/button.tsx +--- + +# 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 + + + +## 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 `; +``` + +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 diff --git a/packages/core/src/components/data-table/data-table.docs.examples.tsx b/packages/core/src/components/data-table/data-table.docs.examples.tsx new file mode 100644 index 00000000..8bc5f60a --- /dev/null +++ b/packages/core/src/components/data-table/data-table.docs.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/packages/core/src/components/data-table/data-table.docs.outline.md b/packages/core/src/components/data-table/data-table.docs.outline.md new file mode 100644 index 00000000..f5963c27 --- /dev/null +++ b/packages/core/src/components/data-table/data-table.docs.outline.md @@ -0,0 +1,787 @@ +--- +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 + +`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: + + + +## 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 diff --git a/packages/core/src/components/dialog.docs.examples.tsx b/packages/core/src/components/dialog.docs.examples.tsx new file mode 100644 index 00000000..def594a6 --- /dev/null +++ b/packages/core/src/components/dialog.docs.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/packages/core/src/components/dialog.docs.outline.md b/packages/core/src/components/dialog.docs.outline.md new file mode 100644 index 00000000..7bac7114 --- /dev/null +++ b/packages/core/src/components/dialog.docs.outline.md @@ -0,0 +1,122 @@ +--- +title: Dialog +description: Modal dialog with a compound component API +group: dialog +sources: + - packages/core/src/components/dialog.tsx +--- + +# 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 + + + +## 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 + + + +## 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 diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index f3eed0b6..c7a958ac 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -22,5 +22,7 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true }, - "include": ["."] + "include": ["."], + "comment:exclude": "Doc examples are authored consumer-style (they import @tailor-platform/app-shell, this package's own public name), so they are not part of core's compilation — the docs-app type-checks them instead.", + "exclude": ["**/*.docs.examples.tsx"] } 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..848756fc --- /dev/null +++ b/packages/docs-kit/src/assemble.ts @@ -0,0 +1,155 @@ +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)); + + // 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) { + throw new Error( + `docs-kit: ${outline.examplesPath} has no exported "${name}" for example token "${key}"`, + ); + } + // Emit an anchor the docs renderer replaces with the LIVE example, in place, + // followed by the extracted source. The anchor is a custom element, so it is + // sanitized away (invisible) on GitHub and other plain markdown renderers. + return ( + `\n\n` + + "```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..6e747400 --- /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.examples) { + 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.examples) { + 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..5d0dec4b --- /dev/null +++ b/packages/docs-kit/src/config.ts @@ -0,0 +1,21 @@ +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, + pagesDir: parsed.pagesDir, + }; +} 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..6686f413 --- /dev/null +++ b/packages/docs-kit/src/outline.ts @@ -0,0 +1,70 @@ +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. Its + * examples live in a sibling `*.docs.examples.tsx` (both are authored SOURCE). */ +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`, + // Examples are authored SOURCE, colocated with the outline (a sibling), + // NOT in the generated output dir. sync only ever writes to `${outDir}`. + examplesPath: outlineRel.replace(/\.docs\.outline\.md$/, ".docs.examples.tsx"), + }); + } + } + outlines.sort((a, b) => a.slug.localeCompare(b.slug)); + return outlines; +} diff --git a/packages/docs-kit/src/pages.ts b/packages/docs-kit/src/pages.ts new file mode 100644 index 00000000..003f6986 --- /dev/null +++ b/packages/docs-kit/src/pages.ts @@ -0,0 +1,57 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +import type { DocsConfig, Outline } from "./types"; + +function titleCase(slug: string): string { + return slug + .split(/[-_]/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} + +/** Repo-relative path of a unit's generated route stub, or null when no + * `pagesDir` is configured. The category is the unit's outDir minus its `docs/` + * prefix (`docs/components` → `components`), so the browser route mirrors the + * published doc tree. */ +export function pagePathFor(outline: Outline, config: DocsConfig): string | null { + if (!config.pagesDir) return null; + const category = outline.outDir.replace(/^docs\//, ""); + return `${config.pagesDir}/${category}/${outline.slug}/page.tsx`; +} + +/** The docs-browser mounts each unit's generated markdown through a tiny + * `DocPage` route stub. It is pure glue — slug + title — so sync regenerates it + * on every run and it must never be hand-edited. `DocPage` lives at + * `/../_lib/DocPage`, so the relative import depth follows the + * category nesting. */ +function renderPageStub(outline: Outline, config: DocsConfig): string { + const category = outline.outDir.replace(/^docs\//, ""); + const toLib = "../".repeat(category.split("/").length + 2) + "_lib/DocPage"; + const title = outline.frontmatter.title ?? titleCase(outline.slug); + return ( + `// GENERATED by docs-kit — do not edit by hand.\n` + + `import type { AppShellPageProps } from "@tailor-platform/app-shell";\n\n` + + `import { DocPage } from "${toLib}";\n\n` + + `const Page = () => ;\n\n` + + `Page.appShellPageProps = {\n` + + ` meta: { title: ${JSON.stringify(title)} },\n` + + `} satisfies AppShellPageProps;\n\n` + + `export default Page;\n` + ); +} + +/** Write a unit's route stub into the docs-browser and return its repo-relative + * path (null when no `pagesDir` is configured). */ +export function writePageStub( + repoRoot: string, + outline: Outline, + config: DocsConfig, +): string | null { + const rel = pagePathFor(outline, config); + if (!rel) return null; + const abs = join(repoRoot, rel); + mkdirSync(dirname(abs), { recursive: true }); + writeFileSync(abs, renderPageStub(outline, config), "utf8"); + return rel; +} 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..61b682c3 --- /dev/null +++ b/packages/docs-kit/src/sync.ts @@ -0,0 +1,106 @@ +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 { writePageStub } from "./pages"; +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, plus the per-unit route + // stub that mounts it in the docs-browser (so new units need zero wiring). + 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); + const pageRel = writePageStub(repoRoot, outline, config); + if (pageRel) { + written.push(pageRel); + toFormat.push(join(repoRoot, pageRel)); + } + } + + // 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"))), + examples: 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..0fb3036a --- /dev/null +++ b/packages/docs-kit/src/types.ts @@ -0,0 +1,91 @@ +/** 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; + examples: 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; + /** Repo-relative dir of the docs-browser's file-based routes. When set, sync + * generates a per-unit `//page.tsx` route stub here so new + * units appear in the browser with zero manual wiring. */ + pagesDir?: 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..2730f8d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,58 @@ importers: specifier: 'catalog:' version: 5.9.3 + docs-browser: + 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) + rehype-raw: + specifier: ^7.0.0 + version: 7.0.0 + 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) + e2e: devDependencies: '@playwright/test': @@ -343,6 +395,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 +2356,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 +2664,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 +2676,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 +2688,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 +2700,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 +2712,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 +2725,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 +2739,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 +2753,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 +2767,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 +2781,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 +2795,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 +2808,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 +2819,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 +2830,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 +2842,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 +3370,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 +3409,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 +3423,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 +3461,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 +3677,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 +3754,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 +3780,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 +3863,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 +3956,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 +4041,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'} @@ -4070,6 +4106,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -4109,6 +4149,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 +4171,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 +4199,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 +4373,27 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -4330,6 +4401,12 @@ packages: hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -4390,6 +4467,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 +4479,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 +4526,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 +4559,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 +4835,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 +4875,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 +4930,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,10 +5274,16 @@ 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'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5197,6 +5434,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 +5499,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 +5556,21 @@ packages: resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} engines: {node: '>=0.10.0'} + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + 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 +5657,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 +5795,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 +5848,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 +5883,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 +5974,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 +6111,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 +6169,15 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + 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: @@ -5987,6 +6288,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -6079,6 +6383,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 +7992,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 +8186,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 +8262,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 +8272,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 +8808,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 +8848,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 +8860,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 +8893,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 +8905,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 +9157,8 @@ snapshots: auto-bind@5.0.1: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -8950,6 +9234,8 @@ snapshots: caniuse-lite@1.0.30001793: {} + ccount@2.0.1: {} + chai@6.2.2: {} chalk-template@0.4.0: @@ -8967,6 +9253,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 +9320,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + commander@12.1.0: {} commander@13.1.0: {} @@ -9092,6 +9388,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 +9480,10 @@ snapshots: transitivePeerDependencies: - supports-color + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@8.0.2: {} dir-glob@3.0.1: @@ -9220,6 +9524,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + entities@7.0.1: {} environment@1.1.0: {} @@ -9294,6 +9600,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 +9616,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 +9646,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 +9831,87 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.2.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.5 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.3 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + 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-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + he@1.2.0: {} hookable@6.1.1: {} + html-url-attributes@3.0.1: {} + + html-void-elements@3.0.0: {} + human-id@4.1.3: {} human-signals@2.1.0: {} @@ -9588,6 +9977,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 +9991,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 +10024,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 +10042,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 +10249,8 @@ snapshots: long@5.3.2: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -9888,10 +10294,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,8 +10961,22 @@ 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: {} + parse5@7.3.0: + dependencies: + entities: 6.0.1 + patch-console@2.0.0: {} path-browserify@1.0.1: {} @@ -10359,6 +11125,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 +11181,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 +11244,46 @@ snapshots: dependencies: rc: 1.2.8 + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.5 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + 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 +11339,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 +11349,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 +11376,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 +11470,8 @@ snapshots: semver@7.7.4: {} - semver@7.8.0: {} + semver@7.8.0: + optional: true semver@7.8.5: {} @@ -10770,6 +11576,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 +11630,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 +11659,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 +11730,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 +11772,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 +11845,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 +11909,21 @@ snapshots: vary@1.1.2: {} + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + 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) @@ -11137,6 +12010,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-namespaces@2.0.1: {} + whatwg-mimetype@3.0.0: {} which@2.0.2: @@ -11204,3 +12079,5 @@ snapshots: zod@4.3.6: {} zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 20b88d7a..3a9fda55 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - catalogue + - docs-browser - e2e - examples/** - packages/**