diff --git a/.agents/skills/resync-docs/SKILL.md b/.agents/skills/resync-docs/SKILL.md new file mode 100644 index 00000000..508f9929 --- /dev/null +++ b/.agents/skills/resync-docs/SKILL.md @@ -0,0 +1,64 @@ +--- +name: resync-docs +description: Regenerate component/pattern documentation when docs-kit reports drift. Use when `docs-kit check` (or CI) fails with interface-drift, outline-changed, or missing-output findings, or when the user asks to "resync docs", "update the docs for X", or "/resync-docs". This is the ONLY sanctioned way to change generated docs — never hand-edit files under docs/. +--- + +# Resync docs + +The docs pipeline is one-way: **outlines + source → generated docs**. Humans (and this skill) edit only two things — the `*.docs-outline.md` (intent) and the `*.examples.tsx` (runnable examples). Everything under `docs/` is generated by `docs-kit` and must never be hand-edited. See [decisions/documentation-management-overhaul.md](../../../decisions/documentation-management-overhaul.md). + +## When to run + +- `docs-kit check` fails (locally or in CI) with a blocking finding. +- A component's interface changed and its docs need to catch up. +- The user asks to add or revise a documented example. + +## Inputs + +An optional list of unit slugs. If omitted, run `docs-kit check` first and act on every blocking unit it names. + +## Procedure + +For each drifting unit: + +1. **Identify what changed.** Run the gate and read the finding: + + ```bash + node packages/docs-kit/dist/cli.mjs check --root . + ``` + + - `interface (type surface) drifted` → the exported API changed. Re-read the unit's `sources` and update the outline prose + `examples.tsx` so they still match. Check whether any example now uses a removed/renamed prop. + - `outline edited` → expected after you edit the outline; just resync. + - `… was hand-edited or is stale` → someone edited a generated file directly. Do **not** keep that edit; move the intent into the outline/examples, then regenerate. + +2. **Edit intent only** — the two authorable surfaces: + - `*.docs-outline.md` (colocated with the component, or under `docs-src/` for concepts/patterns/references): prose is copied verbatim; example segments are keyed English tokens ``. + - `*.examples.tsx` (under `docs//`): one exported component per token, named in PascalCase matching the token (`basic-usage` → `BasicUsage`). Keep every example **compiling against `@tailor-platform/app-shell`** and minimal. When iterating, treat the current file as the baseline and change only what the interface change forces — do not rewrite untouched examples. + +3. **Regenerate deterministically** (no hand-editing of `.md`): + + ```bash + node packages/docs-kit/dist/cli.mjs sync --root . + ``` + + This rewrites the `.md`, extracts fences from `examples.tsx`, refreshes the API section, and updates `docs/docs-manifest.json` hashes. + +4. **Verify green:** + + ```bash + node packages/docs-kit/dist/cli.mjs check --root . # expect exit 0 + ``` + +5. **Review the diff.** Confirm prose reads well, examples are idiomatic, and the manifest changes are limited to the units you touched. + +## Rules + +- Never edit files under `docs/` by hand — only outlines and `examples.tsx`. +- One example export per token; keep them runnable (they are type-checked in CI). +- Regenerate only what changed; a clean tree must produce a zero-diff `sync`. +- Also refresh the generated `app-shell-patterns` consumer skill when component interfaces change (future: `docs-kit sync` will emit it). + +## Related + +- `packages/docs-kit` — the extractor / gate / assembler. +- `docs.config.json` — roots, categories, coverage enforcement, exclusions. diff --git a/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..31bafec9 --- /dev/null +++ b/decisions/documentation-management-overhaul.md @@ -0,0 +1,209 @@ +# Documentation Management Overhaul + +## Context + +Today all ~65 docs (`docs/components/` ×37, `docs/api/` ×22, `docs/concepts/` ×6) are maintained by hand, with no enforced link between a component's source and its doc. Nothing detects when an API changes but its doc doesn't — drift is invisible. There's a live example of the failure right now: `timeline.tsx` has a component, a test, **and** `docs/components/timeline.md`, yet it isn't exported from `index.ts` — a documented component consumers can't import. + +An existing agentic workflow (`.github/workflows/docs-update.lock.yml`) patches drift _after_ changesets merge to `main`, but it's heuristic, non-deterministic, and post-merge (drift lands first). + +**Goal:** a one-way `source → output` docs pipeline with clear human/AI author points, deterministic drift gates that block _before_ merge, regeneration only when the interface or authored intent actually changes, and a browsable site with live in-place examples alongside their source. + +This plan was designed interactively; every decision below reflects an explicit choice by the maintainer. + +--- + +## Authorship model (Goal #1: clear human/AI author points) + +| Surface | Owner | Editable by hand? | +| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | ------------------------------------- | +| `*.docs-outline.md` — prose (verbatim) + English example intents + `sources` + flags | **Human owns intent** (AI may draft; human curates & approves at PR review) | ✅ the **only** hand-editable surface | +| `docs/**/[unit].md`, `[unit].examples.tsx`, `docs-manifest.json`, the `app-shell-patterns` skill | **AI synthesizes** (`resync-docs`), human reviews the diff | ❌ never | + +One rule: **humans edit intent (outlines); AI produces everything downstream; nothing generated is ever hand-edited.** + +--- + +## Core model (resolved decisions) + +| Dimension | Decision | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Doc inclusion signal** | **Explicit**: a unit exists because a `*.docs-outline.md` claims it. Reconciled against `index.ts` (see Coverage below). **Folders are NOT the signal** — folderization is a free code-organization choice, decoupled from docs. | +| **Scope** | Public surface (exported from `index.ts`). Internal-only files need no outline and may be folder-grouped freely without implying documentation. | +| **Doc unit / grouping** | A unit = one output doc. Its `docs-outline.md` (colocated with the anchor source) declares a `sources:` glob list that may span `components/`, `hooks/`, `lib/`, `contexts/`, `types/` — so a unit can gather cross-cutting, even _shared_, code (e.g. `collection` types shared across DataTable/Kanban/Gantt). | +| **Output location** | Inferred from where the outline lives: `components/…` → `docs/components/`, `hooks/…` → `docs/api/`, `docs-src/concepts/…` → `docs/concepts/`, etc. Slug from a `group`/filename. | +| **Change signal** | **Blocking:** type-surface hash + outline hash. **Advisory (non-blocking):** test-snapshot hash. | +| **Examples** | Per unit: one runnable **`[unit].examples.tsx`** with keyed named exports; the `.md` code fences are **derived deterministically** by extracting those exports. Compiles in CI. Per-segment keys ⇒ regenerate only the changed example, reuse the rest as baseline. | +| **Docs site** | A dedicated **AppShell-based renderer app** (dogfooding). Build-time `import.meta.glob` + lazy dynamic import of the canonical `docs/**/*.{md,examples.tsx}` — imports in place, **no copy**. Dev serves from source w/ HMR; prod bundles. | +| **Fix path** | Deterministic pre-merge `check-docs` gate (**no LLM**) blocks the PR on drift. The local `resync-docs` skill is the **only** fix path. `docs-update.lock.yml` is **retired**. Pre-commit _warns_; CI _blocks_. | +| **Non-component docs** | Unified source→output; the whole `docs/` output tree is 100% generated. Hooks = code-backed units. Concepts/patterns = prose outlines in `docs-src/` (outline-hash trigger only; may embed live-example tokens). Tokens = code-backed from theme CSS. Re-exports = lightweight **reference units** (prose + upstream link; claim symbols by name; no type-surface hash). | +| **Drift baseline & integrity** | The **committed `docs-manifest.json`** stores per unit: output path, `sources` globs, the **input** hashes (type-surface/outline/snapshot) **and output** hashes (`.md` + `examples.tsx`). `check-docs` recomputes and compares — no git-history diffing. Input drift ⇒ needs resync; **output-hash mismatch ⇒ a generated file was hand-edited ⇒ CI fail**. | +| **Migration** | AI **reverse-generates** outlines + `examples.tsx` + manifest from the existing docs; humans curate/restructure; enforcement phases in per-unit. | +| **`app-shell-patterns`** | A **consumer-facing build skill** synthesized from component interfaces + pattern docs, manifest-tracked, resynced when upstream drifts. | + +--- + +## Workflow + +```mermaid +flowchart TB + subgraph AUTHOR["Authorship — human owns intent"] + OUT["*.docs-outline.md
prose + example intents + sources + flags"] + SRC["component / hook / theme source"] + end + + OUT -->|edit intent| GATE + SRC -->|edit changes type surface| GATE + + subgraph GATE["check-docs — deterministic, NO LLM"] + H["recompute hashes:
type-surface / outline / snapshot"] + R["reconcile vs index.ts:
every export claimed or excluded"] + M["compare vs committed
docs-manifest.json"] + end + + GATE -->|clean| PASS["pre-commit: pass
CI: pass"] + GATE -->|drift OR uncovered export| FAIL["pre-commit: WARN
CI: BLOCK
+ list of units & reasons"] + + FAIL -->|dev runs locally| SKILL + + subgraph SKILL["resync-docs skill — LLM, local"] + RG["regenerate ONLY changed segments
existing output = baseline"] + WR["write outputs + manifest hashes"] + end + + SKILL --> OUTPUT + + subgraph OUTPUT["Generated output — never hand-edited"] + MD["docs/**/*.md"] + EX["docs/**/*.examples.tsx"] + PS["app-shell-patterns
consumer skill"] + MAN["docs-manifest.json"] + end + + OUTPUT -->|human review at PR| COMMIT["commit → CI green"] + MD -->|import.meta.glob| APP["docs renderer app (AppShell)
live examples + source"] + EX -->|import.meta.glob| APP + MD -->|browse as-is| GH["GitHub / external md site"] +``` + +--- + +## Coverage reconciliation (the anti-drift core — answers "what if I forget?") + +`check-docs` treats `index.ts` as the source of truth for the public surface and reconciles it **both ways**: + +``` +for each export E in index.ts: + claimed = a code-backed unit's `sources` resolves to E + OR a reference outline's `claims` lists E by name + excluded = E is in docs.config.ts exclusions (rare escape hatch only) + if not (claimed or excluded): → BLOCK "E is exported but undocumented" + +for each code-backed unit U: + for each public symbol U documents: + if symbol not exported from index.ts: → BLOCK "U documents non-exported symbol" (the timeline case) +``` + +So adding a new exported hook and forgetting to claim it **fails CI** with a specific message; you must add it to a unit's `sources`, a reference outline's `claims`, or (rarely) the escape-hatch exclusions. `sources` is never a trust-me allowlist. + +Coverage applies to everything exported from `index.ts`: **code-backed units** satisfy it via `sources`; **reference units** (re-exports) satisfy it via `claims` (by name) but carry no type-surface hash. Prose concepts/patterns aren't in the public surface, so they're outside coverage entirely — triggered solely by their own outline hash. + +**Code-backed outline frontmatter:** + +```yaml +--- +group: data-table # slug → docs/components/data-table.md (category from outline location) +sources: + - packages/core/src/components/data-table/** + - packages/core/src/hooks/use-collection-variables.ts + - packages/core/src/lib/collection-url-state.ts + - packages/core/src/contexts/collection-control-context.tsx + - packages/core/src/types/collection.ts +--- +``` + +**Reference outline frontmatter** (re-exports we don't own — light mention + pointer, no type surface): + +```yaml +--- +group: react-router # docs-src/references/ → docs/references/react-router.md +upstream: https://reactrouter.com/ +claims: [useNavigate, useParams, useLocation, useSearchParams, useRouteError, Link] +--- +``` + +Prose is copied verbatim; example segments are keyed English tokens, e.g. +` A DataTable with pagination and a filter toolbar.` + +--- + +## Directory layout & packaging (answers "new package?") + +**Yes — keep all tooling out of the published `core` bundle.** + +``` +packages/core/src/**/[unit].docs-outline.md ← authored source, colocated; not published (core ships only dist/** + skills/**, so src/ never ships — no extra config) +packages/docs-kit/ ← NEW private (unpublished) workspace pkg: + type-surface extractor (reuses ts-morph, already a dep in vite-plugin) + check-docs • manifest gen • md assembler (prose + extracted fences) +docs-src/concepts|patterns|tokens/*.docs-outline.md ← authored prose outlines (no code home) +docs-src/references/*.docs-outline.md ← re-export reference outlines (claims by name + upstream link) + +docs/**/[unit].md, docs/**/[unit].examples.tsx ← generated output (repo root, as today) +docs/docs-manifest.json ← committed generated baseline +docs.config.ts ← exclusions list + location→category rules + +examples/docs-app/ ← NEW AppShell renderer app (dogfoods app-shell) +examples/vite-app/ ← STRIPPED to a minimal AppShell MVP (see below) +.agents/skills/resync-docs/ ← NEW local LLM skill +``` + +- **Vite example app is stripped back** to a minimal reference AppShell implementation; its current demo pages (which informally served as examples) are removed — examples now live in `[unit].examples.tsx` and are shown in the docs-app. +- The `app-shell-patterns` consumer skill is generated into **`packages/core/skills/`** — already published to consumers via core's existing `files: ["skills/**"]`, so it ships with the npm package (dir doesn't exist yet; the pipeline creates it). + +--- + +## Pieces to build + +1. **Type-surface extractor + hasher** (`packages/docs-kit`): per unit, resolve `sources` → intersect (`index.ts` exports) ∩ (declarations in sources) → serialize signatures deterministically → hash. Ignores comments/refactors/formatting. +2. **`check-docs`** (deterministic, no LLM): recompute hashes, run coverage reconciliation, compare to manifest. Blocking = type-surface/outline mismatch, uncovered export, missing/stale output, **or an output-hash mismatch (a generated file was hand-edited)**. Advisory = snapshot mismatch (prints, never fails). Emits unit list + reasons; non-zero exit in CI. +3. **`resync-docs`** skill (LLM, local): input optional unit list (else runs `check-docs`); per unit read current output as baseline, classify what changed, regenerate only affected segments, reassemble `.md`, refresh type tables, rewrite manifest hashes. Also re-evaluates the `app-shell-patterns` skill on upstream drift. +4. **Docs renderer app** (`examples/docs-app`): `import.meta.glob` → one AppShell resource per unit; renders prose + at each token the live component (from `examples.tsx`) and its extracted source. Dev needs `server.fs.allow` for `docs/` + tsconfig include so examples type-check against `@tailor-platform/app-shell`. +5. **CI + hooks**: add `check-docs` turbo task to `ci-packages.yaml` (blocks); type-check `docs/**/*.examples.tsx` against core; `lefthook.yml` pre-commit runs `check-docs` in **warn** mode; **delete** `docs-update.lock.yml`. + +--- + +## Execution phases + +1. **Pilot / POC (first)** — implement the full pipeline end-to-end against **2–3 representative components** (e.g. `Button` single-file, `DataTable` multi-dir/shared-types, `Sidebar` compound) **+ 1–2 patterns**. Proves extractor, gate, coverage, examples, renderer, and skill before scaling. Iterate on the outline/manifest format here. +2. **Build-out** — `docs-kit`, `check-docs`, `resync-docs`, renderer app, CI wiring, strip the Vite example app, retire the bot. +3. **Migration** — AI reverse-generates all remaining outlines + `examples.tsx` + manifest; humans curate; coverage enforcement flips to "all public exports required." + +--- + +## Verification + +1. **Extractor unit tests:** comment-only/refactor edit → type-surface hash stable; prop added/removed → changes; class-only tweak → type-surface stable, snapshot advisory fires. +2. **`check-docs` golden cases:** clean tree → exit 0; mutate a prop type → block naming the unit; edit outline prose → block; class tweak → advisory only, exit 0; **add an exported hook with no outline → block** (the forgetting case); **`timeline` → block** (documented, not exported); **hand-edit a generated `.md`/`examples.tsx` → block** (output-hash mismatch). +3. **Examples compile:** break a generated `examples.tsx` (removed prop) → CI type-check fails. +4. **Renderer app:** `preview_start` the docs-app; a unit page shows live component + matching source; edit an `examples.tsx` in dev → HMR updates with no copy step; `.md` fence === rendered example. +5. **Idempotency:** `resync-docs` on a clean tree → zero diffs; change one prop → exactly one unit resyncs. + +--- + +## Implementation notes (from the POC pilot) + +The pilot (`packages/docs-kit` + Button/Dialog/DataTable + form-modal/list-dense-scan + `examples/docs-app`) validated the design and surfaced these concrete details: + +- **Prop tables are auto-generated** from the resolved type surface. `docs-kit` lists a `*Props` symbol's **first-party** props only — members whose declaration resolves into `node_modules` (e.g. the ~250 DOM attributes from `React.ComponentProps<"button">`) are filtered out, leaving the props the component actually adds (`variant`, `size`, `render`, …) with their JSDoc. An `` token in an outline places the generated `## Props` + `## Exports`; otherwise they append. **TODO:** default values aren't extracted yet (they live in cva `defaultVariants` / destructuring defaults, not the type) — capture via an `@default` JSDoc tag or cva parsing. +- **`astw:` is app-shell's INTERNAL Tailwind prefix** — for the library's own components and its shipped stylesheet only. **Consumers (and therefore all generated example code) use ordinary, unprefixed Tailwind.** The new `docs/concepts/styling.md` documents the consumer setup (`@tailwindcss/vite` + `@import "tailwindcss"` then app-shell `styles`/theme); the docs-app is wired exactly that way as the reference. +- **Examples live at repo-root `docs/`, outside any package**, so their bare imports don't resolve against a package's `node_modules` by default. Two wiring details make them type-check and run: + - docs-app **tsconfig** uses `baseUrl` + **end-anchored** `paths` regexes mapping the exact specifiers (`@tailor-platform/app-shell`, `react`, `lucide-react`) to the app's deps — end-anchored so subpath imports like `@tailor-platform/app-shell/styles` still resolve via package `exports`. Core has **no top-level `types`** field (only inside `exports`), so the app-shell path points straight at `dist/app-shell.d.ts`. + - docs-app **Vite** resolves the out-of-tree examples' bare imports via a **`docs/`-scoped `resolveId` plugin** (see the AppShell bullet), keeps a narrow alias only for `@tailor-platform/app-shell-vite-plugin/parser` (core's bundle imports it via `createTypedPaths`, so that package must be built), `dedupe: ["react","react-dom"]`, and Tailwind `@source "../../../docs/**/*.examples.tsx"` so example utility classes are generated. +- **The docs-app is a real `` app** using file-based routing (the "automagic"). One thin `src/pages/**/page.tsx` per unit renders a shared `DocPage` (markdown + live examples); `} />` infers routes **and** the grouped sidebar automatically from the page structure + `appShellPageProps.meta` — no `modules` prop, no manual `SidebarItem`s. This also brings breadcrumbs, the command palette, and the theme switcher for free. The out-of-tree examples resolve via a **`docs/`-scoped Vite `resolveId` plugin** (not a global alias — a global `@tailor-platform/app-shell` alias would clash with the routing plugin's `entrypoint` interception of `App.tsx`). Follow-up: `docs-kit` could generate the per-unit `page.tsx` stubs so new units appear with zero manual wiring. +- **`sync` formats its outputs + sources with oxfmt _before_ hashing**, so the committed bytes match the manifest and the pre-commit oxfmt hook is idempotent (it can never reformat a generated file after the fact and silently break the gate). Hashes are also whitespace-normalized as a second line of defense. This was a real bite during the pilot: oxfmt reformats code _inside_ generated `.md` fences, which a whitespace-only normalization doesn't absorb. +- **Sources are `git mv`'d, not invented.** Component outlines are moved from the hand-written `docs/components/*.md` (a prep commit, preserving `git log --follow` lineage); the generated `docs/components/*.md` then land as new files. Patterns are moved from **`catalogue/src/pattern/`** (`form/modal` → `form-modal`, `list/dense-scan` → `list-dense-scan`) — their `PATTERN.md` becomes the outline and the example `.tsx` becomes the runnable example, then both are adapted to the docs-kit format. Catalogue already generates the `app-shell-patterns` skill; the intent is for docs-kit to **subsume** that (generate the skill from these docs) and retire catalogue's pipeline, so catalogue's generator/tests are knowingly left to regress for now. + +## Optional / follow-up (not blocking) + +- **Internal reorg** — move single-consumer internals into their consumer's folder; bucket framework internals under `app-shell/`/`internals/`. Now purely cosmetic (the `sources` globs make it irrelevant to docs), so a separate cleanup PR. +- **Public docs-site polish** (SEO/search/hosting/prerender) — only if a public-facing site is wanted. diff --git a/docs-src/concepts/styling.docs-outline.md b/docs-src/concepts/styling.docs-outline.md new file mode 100644 index 00000000..defe4aab --- /dev/null +++ b/docs-src/concepts/styling.docs-outline.md @@ -0,0 +1,56 @@ +--- +group: styling +title: Styling & Tailwind setup +description: How to wire Tailwind and app-shell's theme in a consumer app +--- + +app-shell ships its own compiled styles and a set of themes. In **your** app you write **ordinary Tailwind classes** — the `astw:` prefix you may see in app-shell's own source is an **internal** prefix for the library's shipped stylesheet and is not something consumers use. + +## 1. Install Tailwind + +```bash +pnpm add -D tailwindcss @tailwindcss/vite +``` + +## 2. Add the Vite plugin + +```ts +// vite.config.ts +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], +}); +``` + +## 3. Import Tailwind + the app-shell theme + +Import Tailwind first, then app-shell's base styles, then one theme: + +```css +/* src/index.css */ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +@import "@tailor-platform/app-shell/themes/bloom"; +``` + +## 4. Use ordinary Tailwind in your code + +Your own markup uses unprefixed utilities; app-shell components bring their own styling: + +```tsx +import { Button } from "@tailor-platform/app-shell"; + +
+ + +
; +``` + +> **Do not** use the `astw:` prefix in your app — it is reserved for app-shell's internal stylesheet. If you see it in generated example code, that's a bug in the example (report it). + +## Reference implementation + +The docs explorer app (`examples/docs-app`) and the Vite example (`examples/vite-app`) are both wired exactly this way — use them as a copy-paste reference. diff --git a/catalogue/src/pattern/form/modal/PATTERN.md b/docs-src/patterns/form-modal.docs-outline.md similarity index 89% rename from catalogue/src/pattern/form/modal/PATTERN.md rename to docs-src/patterns/form-modal.docs-outline.md index 0c1c454e..46cf3fb1 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,11 @@ dont: ## Page Implementation - + ## Route-driven Variant - +The route-driven variant renders the same component at both the parent path and the `create` / `edit` path, with `onOpenChange` navigating back so the URL stays in sync. ## Constraints diff --git a/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..9d0adc46 --- /dev/null +++ b/docs.config.json @@ -0,0 +1,17 @@ +{ + "$comment": "Config for @tailor-platform/app-shell-docs-kit. See decisions/documentation-management-overhaul.md.", + "roots": ["packages/core/src", "docs-src"], + "tsconfig": "packages/core/tsconfig.json", + "indexFile": "packages/core/src/index.ts", + "manifestFile": "docs/docs-manifest.json", + "categories": [ + { "match": "packages/core/src/components/**", "outDir": "docs/components" }, + { "match": "packages/core/src/hooks/**", "outDir": "docs/api" }, + { "match": "docs-src/concepts/**", "outDir": "docs/concepts" }, + { "match": "docs-src/patterns/**", "outDir": "docs/patterns" }, + { "match": "docs-src/references/**", "outDir": "docs/references" } + ], + "enforceCoverage": false, + "exclusions": [], + "snapshotDir": "packages/core/__snapshots__" +} diff --git a/docs/components/button.examples.tsx b/docs/components/button.examples.tsx new file mode 100644 index 00000000..8b35de9c --- /dev/null +++ b/docs/components/button.examples.tsx @@ -0,0 +1,39 @@ +import { Button } from "@tailor-platform/app-shell"; +import { PlusIcon } from "lucide-react"; + +export function BasicUsage() { + return ( +
+ + + +
+ ); +} + +export function Variants() { + return ( +
+ + + + + + +
+ ); +} + +export function Sizes() { + return ( +
+ + + + + +
+ ); +} diff --git a/docs/components/button.md b/docs/components/button.md index ff94d548..e697aa54 100644 --- a/docs/components/button.md +++ b/docs/components/button.md @@ -3,6 +3,10 @@ title: Button description: Styled button with multiple variants and sizes --- + + +# Button + # Button The `Button` component is a styled button with multiple visual variants and sizes. It supports rendering as a custom element via the `render` prop (Base UI render pattern). @@ -16,9 +20,15 @@ import { Button } from "@tailor-platform/app-shell"; ## Basic Usage ```tsx - - - +function BasicUsage() { + return ( +
+ + + +
+ ); +} ``` ## Props @@ -36,22 +46,36 @@ All standard HTML ` - - - - - +function Variants() { + return ( +
+ + + + + + +
+ ); +} ``` ## Sizes ```tsx - - - - - +function Sizes() { + return ( +
+ + + + + +
+ ); +} ``` ## Render Prop @@ -126,3 +150,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.examples.tsx b/docs/components/data-table.examples.tsx new file mode 100644 index 00000000..8bc5f60a --- /dev/null +++ b/docs/components/data-table.examples.tsx @@ -0,0 +1,50 @@ +import { DataTable, createColumnHelper, useDataTable } from "@tailor-platform/app-shell"; +import type { RowAction } from "@tailor-platform/app-shell"; + +interface Product extends Record { + id: string; + name: string; + price: number; + status: string; + createdAt: string; +} + +const PRODUCTS: Product[] = [ + { id: "1", name: "Aluminium widget", price: 19.99, status: "active", createdAt: "2026-01-04" }, + { id: "2", name: "Brass gadget", price: 149, status: "draft", createdAt: "2026-02-11" }, + { id: "3", name: "Copper gizmo", price: 4.5, status: "active", createdAt: "2026-03-02" }, +]; + +const { column } = createColumnHelper(); + +const columns = [ + column({ label: "Name", accessor: (row) => row.name, truncate: true }), + column({ label: "Price", type: "money", accessor: (row) => row.price }), + column({ label: "Status", type: "badge", accessor: (row) => row.status }), + column({ label: "Created", type: "date", accessor: (row) => row.createdAt }), +]; + +const rowActions: RowAction[] = [ + { id: "edit", label: "Edit", onClick: (row) => window.alert(`Edit ${row.name}`) }, + { + id: "delete", + label: "Delete", + variant: "destructive", + onClick: (row) => window.alert(`Delete ${row.name}`), + }, +]; + +export function BasicUsage() { + const table = useDataTable({ + columns, + data: { rows: PRODUCTS, total: PRODUCTS.length }, + onClickRow: (row) => window.alert(`Open ${row.name}`), + rowActions, + }); + + return ( + + + + ); +} diff --git a/docs/components/data-table.md b/docs/components/data-table.md index d17c7e59..e7176d20 100644 --- a/docs/components/data-table.md +++ b/docs/components/data-table.md @@ -3,6 +3,10 @@ title: DataTable description: Compound data table component with sortable columns, filter chips, cursor-based pagination, row actions, and multi-row selection --- + + +# DataTable + # DataTable `DataTable` is a compound component for displaying collections of records. It integrates with the collection variable hooks (`useCollectionVariables`) to drive sorting, filtering, and cursor-based pagination through a GraphQL API. @@ -138,6 +142,27 @@ 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.examples.tsx b/docs/components/dialog.examples.tsx new file mode 100644 index 00000000..def594a6 --- /dev/null +++ b/docs/components/dialog.examples.tsx @@ -0,0 +1,41 @@ +import { useState } from "react"; +import { Button, Dialog } from "@tailor-platform/app-shell"; + +export function BasicUsage() { + return ( + + }>Open + + + Confirm Action + Are you sure you want to proceed? + + + }>Cancel + + + + + ); +} + +export function Controlled() { + const [open, setOpen] = useState(false); + return ( + <> + + + + + Settings + + + + + + + + ); +} diff --git a/docs/components/dialog.md b/docs/components/dialog.md index 1fb1e74c..c49aa6b4 100644 --- a/docs/components/dialog.md +++ b/docs/components/dialog.md @@ -3,6 +3,10 @@ title: Dialog description: Modal dialog with a compound component API --- + + +# Dialog + # Dialog The `Dialog` component provides a modal dialog with a compound component API. It is backed by Base UI's Dialog primitive. @@ -16,19 +20,23 @@ 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 @@ -67,19 +75,26 @@ 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 +160,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.md b/docs/concepts/styling.md new file mode 100644 index 00000000..0d5760b8 --- /dev/null +++ b/docs/concepts/styling.md @@ -0,0 +1,59 @@ +--- +title: Styling & Tailwind setup +description: How to wire Tailwind and app-shell's theme in a consumer app +--- + + + +# Styling & Tailwind setup + +app-shell ships its own compiled styles and a set of themes. In **your** app you write **ordinary Tailwind classes** — the `astw:` prefix you may see in app-shell's own source is an **internal** prefix for the library's shipped stylesheet and is not something consumers use. + +## 1. Install Tailwind + +```bash +pnpm add -D tailwindcss @tailwindcss/vite +``` + +## 2. Add the Vite plugin + +```ts +// vite.config.ts +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react(), tailwindcss()], +}); +``` + +## 3. Import Tailwind + the app-shell theme + +Import Tailwind first, then app-shell's base styles, then one theme: + +```css +/* src/index.css */ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +@import "@tailor-platform/app-shell/themes/bloom"; +``` + +## 4. Use ordinary Tailwind in your code + +Your own markup uses unprefixed utilities; app-shell components bring their own styling: + +```tsx +import { Button } from "@tailor-platform/app-shell"; + +
+ + +
; +``` + +> **Do not** use the `astw:` prefix in your app — it is reserved for app-shell's internal stylesheet. If you see it in generated example code, that's a bug in the example (report it). + +## Reference implementation + +The docs explorer app (`examples/docs-app`) and the Vite example (`examples/vite-app`) are both wired exactly this way — use them as a copy-paste reference. diff --git a/docs/docs-manifest.json b/docs/docs-manifest.json new file mode 100644 index 00000000..bfbcd09f --- /dev/null +++ b/docs/docs-manifest.json @@ -0,0 +1,160 @@ +{ + "version": 1, + "units": { + "button": { + "slug": "button", + "kind": "code-backed", + "outline": "packages/core/src/components/button.docs-outline.md", + "output": "docs/components/button.md", + "examples": "docs/components/button.examples.tsx", + "sources": ["packages/core/src/components/button.tsx"], + "claims": [], + "symbols": ["Button", "ButtonProps", "buttonVariants"], + "hashes": { + "typeSurface": "16df9927f8394548", + "outline": "e32d0264496ae851", + "snapshot": "d607a315ffce3abc", + "outputMd": "dfa0025001ed309c", + "outputExamples": "8cc0091758773a45" + } + }, + "data-table": { + "slug": "data-table", + "kind": "code-backed", + "outline": "packages/core/src/components/data-table/data-table.docs-outline.md", + "output": "docs/components/data-table.md", + "examples": "docs/components/data-table.examples.tsx", + "sources": [ + "packages/core/src/components/data-table/**", + "packages/core/src/hooks/use-collection-variables.ts", + "packages/core/src/lib/collection-url-state.ts", + "packages/core/src/contexts/collection-control-context.tsx", + "packages/core/src/types/collection.ts" + ], + "claims": [], + "symbols": [ + "BuildQueryVariables", + "CollectionControl", + "CollectionControlProvider", + "CollectionInitialState", + "CollectionParams", + "CollectionPersistedState", + "CollectionResult", + "CollectionVariables", + "Column", + "DataTable", + "DataTableContextValue", + "DataTableData", + "DataTablePaginationProps", + "DataTableRootProps", + "FieldMetadata", + "FieldType", + "Filter", + "FilterConfig", + "FilterOperator", + "MetadataFieldOptions", + "NodeType", + "OPERATORS_BY_FILTER_TYPE", + "PageInfo", + "PaginationVariables", + "RowAction", + "SelectOption", + "SortConfig", + "SortState", + "TableFieldName", + "TableMetadata", + "TableMetadataFilter", + "TableMetadataMap", + "TableOrderableFieldName", + "UseCollectionOptions", + "UseCollectionReturn", + "UseDataTableOptions", + "UseDataTableReturn", + "createColumnHelper", + "fieldTypeToFilterConfig", + "fieldTypeToSortConfig", + "useCollectionControl", + "useCollectionVariables", + "useDataTable", + "useDataTableContext", + "useURLCollectionVariables", + "withURLCollectionState" + ], + "hashes": { + "typeSurface": "b74872a25bf644f3", + "outline": "316243eb4a84e13d", + "snapshot": null, + "outputMd": "89eb77127670e7e6", + "outputExamples": "c4c02203e17221eb" + } + }, + "dialog": { + "slug": "dialog", + "kind": "code-backed", + "outline": "packages/core/src/components/dialog.docs-outline.md", + "output": "docs/components/dialog.md", + "examples": "docs/components/dialog.examples.tsx", + "sources": ["packages/core/src/components/dialog.tsx"], + "claims": [], + "symbols": ["Dialog"], + "hashes": { + "typeSurface": "b1614970f950ceb6", + "outline": "a8f69aa1e5eeb3a2", + "snapshot": "125a10821f041e34", + "outputMd": "c82b34f679f0e4da", + "outputExamples": "934c52d4e8761404" + } + }, + "form-modal": { + "slug": "form-modal", + "kind": "prose", + "outline": "docs-src/patterns/form-modal.docs-outline.md", + "output": "docs/patterns/form-modal.md", + "examples": "docs/patterns/form-modal.examples.tsx", + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "dc2d8147c3ae5670", + "snapshot": null, + "outputMd": "cbce5a585c5e50a9", + "outputExamples": "5851d00933ce0062" + } + }, + "list-dense-scan": { + "slug": "list-dense-scan", + "kind": "prose", + "outline": "docs-src/patterns/list-dense-scan.docs-outline.md", + "output": "docs/patterns/list-dense-scan.md", + "examples": "docs/patterns/list-dense-scan.examples.tsx", + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "3025ac6274b38bbb", + "snapshot": null, + "outputMd": "a29818b09de2cc20", + "outputExamples": "6e46b2f7bab2eaf6" + } + }, + "styling": { + "slug": "styling", + "kind": "prose", + "outline": "docs-src/concepts/styling.docs-outline.md", + "output": "docs/concepts/styling.md", + "examples": null, + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "057d44a8467a93c1", + "snapshot": null, + "outputMd": "57d1a9bcc0e74cd5", + "outputExamples": null + } + } + } +} diff --git a/catalogue/src/pattern/form/modal/modal-form.tsx b/docs/patterns/form-modal.examples.tsx similarity index 64% rename from catalogue/src/pattern/form/modal/modal-form.tsx rename to docs/patterns/form-modal.examples.tsx index 2dbe9fd3..e9bf9fe9 100644 --- a/catalogue/src/pattern/form/modal/modal-form.tsx +++ b/docs/patterns/form-modal.examples.tsx @@ -1,11 +1,6 @@ -/* pattern: form/modal */ -import { Button, Dialog, Input, Field } from "@tailor-platform/app-shell"; +import { Button, Dialog, Field, Input } from "@tailor-platform/app-shell"; -type Props = { - onSave: (data: { label: string; street: string; city: string }) => void; -}; - -export default function ModalForm({ onSave }: Props) { +export function ModalForm() { return ( }>Add address @@ -15,17 +10,13 @@ export default function ModalForm({ onSave }: Props) { Add a shipping address to this order.
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - onSave({ - label: formData.get("label") as string, - street: formData.get("street") as string, - city: formData.get("city") as string, - }); + onSubmit={(event) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + window.alert(`Saving ${data.get("label") ?? ""}`); }} > -
+
Label } /> diff --git a/docs/patterns/form-modal.md b/docs/patterns/form-modal.md new file mode 100644 index 00000000..44c0b47c --- /dev/null +++ b/docs/patterns/form-modal.md @@ -0,0 +1,79 @@ +--- +title: Modal Form +description: Default form pattern for Create/Edit — keeps user in context on the parent screen +--- + + + +# Modal Form + +# pattern/form/modal + +## When to Use + +- Default for most Create and Edit forms — keeps user in context on parent screen +- Inline add of a related entity from another screen (add address from order detail) +- Quick configuration changes and single-purpose forms (rename, change status) +- Any form the design hasn't explicitly called out as a full-page routed screen + +## Page Implementation + +```tsx +function ModalForm() { + return ( + + }>Add address + + + Add address + Add a shipping address to this order. + + { + event.preventDefault(); + const data = new FormData(event.currentTarget); + window.alert(`Saving ${data.get("label") ?? ""}`); + }} + > +
+ + Label + } /> + + + Street + } /> + + + City + } /> + +
+ + }>Cancel + + + +
+
+ ); +} +``` + +## Route-driven Variant + +The route-driven variant renders the same component at both the parent path and the `create` / `edit` path, with `onOpenChange` navigating back so the URL stays in sync. + +## Constraints + +- Dialog renders full-screen sheet below 1024px; centered max-w-md at 1024–1280px +- Route-driven variant requires both parent path and create/edit path to render the same component +- `onOpenChange` must navigate back — just calling `setOpen(false)` leaves the URL broken + +## Anti-patterns + +- Nesting modals — opening a Dialog from inside another Dialog +- Modal containing a wizard — promote to a routed `form/wizard` +- Save closes the dialog but parent state is stale — wire refetch or optimistic update +- Building a routed Create/Edit page when the design didn't explicitly call for one — modal is the default +- Registering the create path as a separate top-level route — that unmounts the parent list diff --git a/docs/patterns/list-dense-scan.examples.tsx b/docs/patterns/list-dense-scan.examples.tsx new file mode 100644 index 00000000..cba449a5 --- /dev/null +++ b/docs/patterns/list-dense-scan.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/docs/patterns/list-dense-scan.md b/docs/patterns/list-dense-scan.md new file mode 100644 index 00000000..bccb7901 --- /dev/null +++ b/docs/patterns/list-dense-scan.md @@ -0,0 +1,93 @@ +--- +title: Dense Scan List +description: High-density scannable list backed by GraphQL connections with DataTable, sort, filters, and pagination +--- + + + +# Dense Scan List + +# pattern/list/dense-scan + +## When to Use + +- Browsing many records of one entity type (orders, POs, products, invoices) with GraphQL pagination +- Operators sort, filter, and select rows; row click navigates to detail +- Optionally: a bucket control (`Tabs`, segmented buttons) aligned to one categorical dimension the business cares about (status, fulfillment stage, type) + +## Column Definition + +Columns are defined with `createColumnHelper` (see the example below) — each column has a `label` and an `accessor`, or a custom `render` for cells like status badges. + +## Page Implementation + +```tsx +function DenseScan() { + const table = useDataTable({ data: { rows: orders, total: orders.length }, columns }); + + return ( + + + Create Order + , + ]} + /> + + + + + + + + + + + + + ); +} +``` + +## Page Layout & Internal Scrolling + +Table-first pages should pin their chrome and scroll only the rows region. Wrap the page in ``: + +- `fill` stretches the layout to the available height and bounds the column row, so the `DataTable` shrinks to fit instead of growing past the viewport +- The `Layout.Header` (title/actions), `DataTable.Toolbar`, the column header row (sticky), and `DataTable.Footer` (pagination) stay visible at every viewport height — only the rows scroll vertically +- When the current page of rows fits, nothing stretches and no scrollbar appears — short tables render identically with or without `fill` +- Requires no extra styling on the page: the height chain (`AppShell` content area → `Layout fill` → `Layout.Column` → `DataTable.Root`) is wired by the components + +Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, articles) — the AppShell content area scrolls those. + +## Variants + +- **Toolbar chips only (`DataTable.Filters`)** — best when filters map cleanly to typed column metadata / enum facets +- **Tabs only above `DataTable`** — best when workflows are organized as obvious buckets +- **Tabs + chips** — when buckets are primary and finer filters help +- **Bulk selection** — `onSelectionChange` hook on `useDataTable`; combine with `interaction/multi-select` +- **`Table` primitives** — small static lists without collection hooks + +## Constraints + +- Column count: 4-8 recommended +- Must include pagination — never render unbounded lists +- Table-first pages use `` so title/toolbar/header/footer stay pinned and only rows scroll +- Handle every state: `DataTable` renders the loading skeleton and error row; always provide a **labelled empty state** (what the list is + how to add the first record) rather than a bare empty table +- Status Badge colors must use design system tokens (variant prop): the **primary** status column uses **filled** semantic variants; **secondary** status columns (delivery, billing) use **`outline-*`** (see `design-system.md` → Composition & emphasis rules) +- Bulk actions toolbar appears only when ≥1 row is selected +- Whole row is clickable via `onClickRow`; wrap the primary identifier cell in `` for keyboard/SR access. No per-row "View" / "Open" buttons +- Per-row `Menu` (overflow `…`) is reserved for non-navigation actions (Archive, Duplicate, Delete) +- Wide lists (many columns / horizontal scroll): pin the column users scan by to the **left** — the record's identifier (invoice / order **reference**) or its **name** (customer, product) — so it stays anchored while the rest scrolls; optionally pin a single high-signal **status** or **total** to the **right**. Keep the pinned set small (≈1 left, at most 1 right) — over-pinning eats the scroll area. Let users override via `` (show/hide + reorder + re-pin) with a stable `tableId` so each user's layout persists + +## Anti-patterns + +- Building a bespoke table + custom pagination instead of `DataTable` + `useCollectionVariables` +- Hand-rolled `max-height`/`overflow` wrappers around `DataTable` to contain scrolling — use `` instead +- Tabs that mutate only local UI state while pagination/filters assume the full server set +- Using `` directly instead of `` for live collections +- Client-side filtering on 1000+ records without server-side support +- Inline editable cells — use `pattern/detail/*` or `pattern/form/modal` instead +- Per-row "View" / "Open" buttons duplicating the row-click navigation diff --git a/examples/docs-app/index.html b/examples/docs-app/index.html new file mode 100644 index 00000000..0018baf8 --- /dev/null +++ b/examples/docs-app/index.html @@ -0,0 +1,12 @@ + + + + + + app-shell docs + + +
+ + + diff --git a/examples/docs-app/package.json b/examples/docs-app/package.json new file mode 100644 index 00000000..c24fe2b3 --- /dev/null +++ b/examples/docs-app/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tailor-platform/app-shell-docs-app", + "private": true, + "description": "Docs explorer — renders the generated docs/ (markdown + live examples) in place. Dogfoods app-shell. SKELETON: see TODOs before shipping.", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@tailor-platform/app-shell": "workspace:*", + "@tailor-platform/app-shell-vite-plugin": "workspace:*", + "lucide-react": "catalog:", + "react": "catalog:", + "react-dom": "catalog:", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0", + "tailwindcss": "catalog:" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.0", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitejs/plugin-react": "catalog:", + "typescript": "^5", + "vite": "catalog:" + } +} diff --git a/examples/docs-app/src/App.tsx b/examples/docs-app/src/App.tsx new file mode 100644 index 00000000..ba8de39a --- /dev/null +++ b/examples/docs-app/src/App.tsx @@ -0,0 +1,14 @@ +import { AppShell, DefaultSidebar, SidebarLayout } from "@tailor-platform/app-shell"; + +// The docs explorer is a real AppShell app. Routes and the sidebar are inferred +// automatically from the file structure under src/pages/ (file-based routing + +// auto-generation mode) — no `modules` prop, no manual SidebarItems. +const App = () => { + return ( + + } /> + + ); +}; + +export default App; diff --git a/examples/docs-app/src/_lib/DocPage.tsx b/examples/docs-app/src/_lib/DocPage.tsx new file mode 100644 index 00000000..27f8b628 --- /dev/null +++ b/examples/docs-app/src/_lib/DocPage.tsx @@ -0,0 +1,48 @@ +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +import { Layout } from "@tailor-platform/app-shell"; + +import { units } from "../docs"; + +// Shared renderer for a single documented unit: prose (markdown) + live examples. +// Lives under _lib/ so file-based routing does not treat it as a page. +export function DocPage({ slug }: { slug: string }) { + const unit = units.find((u) => u.slug === slug); + + if (!unit) { + return ( + + + Unknown doc unit: {slug} + + ); + } + + return ( + + + +
+ {unit.markdown} +
+ + {unit.examples.length > 0 && ( +
+

Live examples

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

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

+
+
+ ); +}; + +HomePage.appShellPageProps = { + meta: { title: "Home" }, +} satisfies AppShellPageProps; + +export default HomePage; diff --git a/examples/docs-app/src/pages/patterns/form-modal/page.tsx b/examples/docs-app/src/pages/patterns/form-modal/page.tsx new file mode 100644 index 00000000..c765fbfa --- /dev/null +++ b/examples/docs-app/src/pages/patterns/form-modal/page.tsx @@ -0,0 +1,11 @@ +import type { AppShellPageProps } from "@tailor-platform/app-shell"; + +import { DocPage } from "../../../_lib/DocPage"; + +const Page = () => ; + +Page.appShellPageProps = { + meta: { title: "Modal form" }, +} satisfies AppShellPageProps; + +export default Page; diff --git a/examples/docs-app/src/pages/patterns/list-dense-scan/page.tsx b/examples/docs-app/src/pages/patterns/list-dense-scan/page.tsx new file mode 100644 index 00000000..2291100c --- /dev/null +++ b/examples/docs-app/src/pages/patterns/list-dense-scan/page.tsx @@ -0,0 +1,11 @@ +import type { AppShellPageProps } from "@tailor-platform/app-shell"; + +import { DocPage } from "../../../_lib/DocPage"; + +const Page = () => ; + +Page.appShellPageProps = { + meta: { title: "Dense scan list" }, +} satisfies AppShellPageProps; + +export default Page; diff --git a/examples/docs-app/tsconfig.json b/examples/docs-app/tsconfig.json new file mode 100644 index 00000000..89bd59ce --- /dev/null +++ b/examples/docs-app/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true, + "types": ["vite/client", "node"], + "baseUrl": ".", + "paths": { + "@tailor-platform/app-shell": [ + "./node_modules/@tailor-platform/app-shell/dist/app-shell.d.ts" + ], + "react": ["./node_modules/@types/react"], + "react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime"], + "react-dom": ["./node_modules/@types/react-dom"], + "react-dom/client": ["./node_modules/@types/react-dom/client"], + "lucide-react": ["./node_modules/lucide-react"] + } + }, + "comment": "The generated examples live at repo-root docs/ (outside this package) and are imported in place. baseUrl/paths make their bare imports resolve to this app's deps so tsc can type-check them against core — the CI 'examples compile against core' gate.", + "include": ["src", "vite.config.ts", "../../docs/**/*.examples.tsx"] +} diff --git a/examples/docs-app/vite.config.ts b/examples/docs-app/vite.config.ts new file mode 100644 index 00000000..053b323b --- /dev/null +++ b/examples/docs-app/vite.config.ts @@ -0,0 +1,57 @@ +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { appShellRoutes } from "@tailor-platform/app-shell/vite-plugin"; +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig, type Plugin } from "vite"; + +const repoRoot = fileURLToPath(new URL("../..", import.meta.url)); +const appRoot = fileURLToPath(new URL(".", import.meta.url)); +const docsDir = resolve(repoRoot, "docs"); + +// The generated examples live at repo-root docs/ (outside this app), so their +// bare imports don't resolve against this app's node_modules. Resolve them as +// if imported from this app — SCOPED to docs/ importers so it never touches +// App.tsx, whose `@tailor-platform/app-shell` import the routing plugin +// intercepts (via `entrypoint`) to inject the file-based pages. +function resolveExampleDeps(): Plugin { + const EXAMPLE_DEPS = /^(@tailor-platform\/app-shell|react|react-dom|lucide-react)(\/|$)/; + return { + name: "docs-app:resolve-example-deps", + enforce: "pre", + async resolveId(source, importer) { + if (!importer || !importer.startsWith(docsDir)) return null; + if (!EXAMPLE_DEPS.test(source)) return null; + const resolved = await this.resolve(source, resolve(appRoot, "src/main.tsx"), { + skipSelf: true, + }); + return resolved?.id ?? null; + }, + }; +} + +export default defineConfig({ + plugins: [ + resolveExampleDeps(), + react(), + tailwindcss(), + // File-based routing: routes + sidebar are inferred from src/pages/. + appShellRoutes({ entrypoint: "src/App.tsx" }), + ], + resolve: { + // Core's built bundle imports the vite-plugin parser; alias it to the built + // output so it resolves from wherever the bundle is loaded. + alias: [ + { + find: /^@tailor-platform\/app-shell-vite-plugin\/parser$/, + replacement: resolve(repoRoot, "packages/vite-plugin/dist/parser.mjs"), + }, + ], + dedupe: ["react", "react-dom"], + }, + server: { + port: 5177, + fs: { allow: [repoRoot] }, + }, +}); diff --git a/package.json b/package.json index acd2f303..83653c1e 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "type-check": "turbo type-check", "lint": "turbo lint", "test": "turbo test", + "docs:sync": "node packages/docs-kit/dist/cli.mjs sync --root .", + "docs:check": "node packages/docs-kit/dist/cli.mjs check --root .", "fmt": "oxfmt", "fmt:check": "oxfmt --check", "changeset:create": "changeset", diff --git a/packages/core/src/components/button.docs-outline.md b/packages/core/src/components/button.docs-outline.md 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-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-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/docs-kit/.oxlintrc.jsonc b/packages/docs-kit/.oxlintrc.jsonc new file mode 100644 index 00000000..cf68e211 --- /dev/null +++ b/packages/docs-kit/.oxlintrc.jsonc @@ -0,0 +1,9 @@ +{ + "$schema": "../../node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "unicorn", "oxc"], + "categories": { + "correctness": "error", + "suspicious": "error", + }, + "ignorePatterns": ["dist", "node_modules"], +} diff --git a/packages/docs-kit/package.json b/packages/docs-kit/package.json new file mode 100644 index 00000000..af23e20b --- /dev/null +++ b/packages/docs-kit/package.json @@ -0,0 +1,35 @@ +{ + "name": "@tailor-platform/app-shell-docs-kit", + "version": "0.0.0", + "private": true, + "description": "Internal tooling for the app-shell docs pipeline: type-surface extraction, drift detection, and deterministic markdown assembly. Never published.", + "bin": { + "docs-kit": "./dist/cli.mjs" + }, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + } + }, + "scripts": { + "build": "tsdown", + "dev": "tsdown --watch", + "lint": "oxlint -c .oxlintrc.jsonc", + "test": "vitest run", + "type-check": "tsc --incremental" + }, + "dependencies": { + "gray-matter": "^4.0.3", + "picomatch": "^4.0.2", + "ts-morph": "^28.0.0" + }, + "devDependencies": { + "@types/node": "catalog:", + "@types/picomatch": "^4.0.0", + "tsdown": "catalog:", + "typescript": "^5", + "vitest": "catalog:" + } +} diff --git a/packages/docs-kit/src/assemble.ts b/packages/docs-kit/src/assemble.ts new file mode 100644 index 00000000..e1388a1b --- /dev/null +++ b/packages/docs-kit/src/assemble.ts @@ -0,0 +1,142 @@ +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +import { Project } from "ts-morph"; + +import type { Surface } from "./project"; +import type { Outline, UnitKind } from "./types"; + +const EXAMPLE_TOKEN = //g; + +function titleCase(slug: string): string { + return slug + .split(/[-_]/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} + +function pascal(key: string): string { + return key + .split(/[-_]/) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(""); +} + +function stripExport(src: string): string { + return src.replace(/^export\s+/, ""); +} + +/** Parse an authored examples module syntactically (no type resolution needed) + * and return each exported example's source text, keyed by its export name. */ +function extractExamples(examplesAbsPath: string): Map { + const map = new Map(); + if (!existsSync(examplesAbsPath)) return map; + const project = new Project({ skipAddingFilesFromTsConfig: true, skipLoadingLibFiles: true }); + const sf = project.addSourceFileAtPath(examplesAbsPath); + for (const fn of sf.getFunctions()) { + const name = fn.getName(); + if (fn.isExported() && name) map.set(name, fn.getText()); + } + for (const v of sf.getVariableStatements()) { + if (!v.isExported()) continue; + for (const decl of v.getDeclarations()) map.set(decl.getName(), v.getText()); + } + return map; +} + +const KIND_LABEL: Record = { + FunctionDeclaration: "function", + VariableDeclaration: "const", + TypeAliasDeclaration: "type", + InterfaceDeclaration: "interface", + ClassDeclaration: "class", + EnumDeclaration: "enum", +}; + +/** Escape a value for a single markdown table cell: collapse whitespace and + * escape pipes so union types like `"a" | "b"` don't break the table. */ +function cell(text: string): string { + return text.replace(/\s+/g, " ").replace(/\|/g, "\\|").trim(); +} + +function renderApi(surface: Surface, owned: string[], kind: UnitKind, upstream?: string): string { + if (kind === "reference") { + const lines = owned.map((n) => `- \`${n}\``).join("\n"); + const link = upstream ? `[upstream documentation](${upstream})` : "the upstream documentation"; + return `## Re-exported API\n\nThese symbols are re-exported by \`@tailor-platform/app-shell\` for convenience. See ${link} for full details.\n\n${lines}`; + } + if (kind !== "code-backed" || owned.length === 0) return ""; + + const sections: string[] = []; + + // Auto-generated prop tables for every owned `*Props` symbol. + const tables: string[] = []; + for (const name of owned) { + const sym = surface.symbols.get(name); + if (!sym?.props?.length) continue; + const head = "| Prop | Type | Description |\n| --- | --- | --- |"; + const body = sym.props + .map( + (p) => + `| \`${p.name}${p.optional ? "?" : ""}\` | \`${cell(p.type)}\` | ${cell(p.description)} |`, + ) + .join("\n"); + tables.push(`### ${name}\n\n${head}\n${body}`); + } + if (tables.length > 0) { + sections.push( + `## Props\n\n_Generated from the type surface — first-party props only._\n\n${tables.join("\n\n")}`, + ); + } + + const list = owned + .map((n) => { + const s = surface.symbols.get(n); + return `- \`${n}\` — ${KIND_LABEL[s?.kind ?? ""] ?? s?.kind ?? "unknown"}`; + }) + .join("\n"); + sections.push(`## Exports\n\n${list}`); + + return sections.join("\n\n"); +} + +/** Deterministically assemble the output markdown from an outline, its authored + * examples module, and the resolved type surface. No LLM involved. */ +export function assembleMarkdown(opts: { + repoRoot: string; + outline: Outline; + surface: Surface; + owned: string[]; +}): string { + const { repoRoot, outline, surface, owned } = opts; + const examples = extractExamples(join(repoRoot, outline.examplesPath)); + + const body = outline.body.replace(EXAMPLE_TOKEN, (_match, key: string) => { + const name = pascal(key); + const src = examples.get(name); + if (!src) { + throw new Error( + `docs-kit: ${outline.examplesPath} has no exported "${name}" for example token "${key}"`, + ); + } + return "```tsx\n" + stripExport(src).trim() + "\n```"; + }); + + const title = outline.frontmatter.title ?? titleCase(outline.slug); + const description = outline.frontmatter.description ?? ""; + const front = `---\ntitle: ${title}\ndescription: ${description}\n---`; + const note = ``; + const api = renderApi(surface, owned, outline.kind, outline.frontmatter.upstream); + + // Inject the generated API block at an explicit `` token if the + // outline places one; otherwise append it at the end. + const API_TOKEN = //; + let composed: string; + if (API_TOKEN.test(body)) { + composed = body.replace(API_TOKEN, api).trim(); + } else { + composed = api ? `${body.trim()}\n\n${api}` : body.trim(); + } + + return [front, note, `# ${title}`, composed].filter(Boolean).join("\n\n") + "\n"; +} diff --git a/packages/docs-kit/src/check.ts b/packages/docs-kit/src/check.ts new file mode 100644 index 00000000..3900bcde --- /dev/null +++ b/packages/docs-kit/src/check.ts @@ -0,0 +1,93 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { loadConfig } from "./config"; +import { type Finding, reconcile } from "./coverage"; +import { hash, normalizeText } from "./hash"; +import { readManifest } from "./manifest"; +import { discoverOutlines } from "./outline"; +import { loadSurface, snapshotHashForSlug } from "./project"; + +export interface CheckResult { + findings: Finding[]; + ok: boolean; +} + +/** Deterministic drift gate. No LLM, no writes. Exit non-zero on any block. */ +export function check(repoRoot: string): CheckResult { + const config = loadConfig(repoRoot); + const outlines = discoverOutlines(repoRoot, config); + const surface = loadSurface(repoRoot, config); + const { findings: reconcileFindings, ownedBySlug } = reconcile(surface, outlines, config); + const findings: Finding[] = [...reconcileFindings]; + const manifest = readManifest(repoRoot, config.manifestFile); + + for (const outline of outlines) { + const { slug } = outline; + const entry = manifest?.units[slug]; + const owned = ownedBySlug.get(slug) ?? []; + + if (!entry) { + findings.push({ + level: "block", + slug, + message: `no manifest entry — run \`docs-kit sync\`.`, + }); + continue; + } + + const curOutline = hash(normalizeText(readFileSync(outline.outlinePath, "utf8"))); + const curType = outline.kind === "code-backed" ? surface.hashSymbols(owned) : null; + const curSnap = + outline.kind === "code-backed" ? snapshotHashForSlug(repoRoot, config, slug) : null; + + if (curType !== entry.hashes.typeSurface) { + findings.push({ + level: "block", + slug, + message: `interface (type surface) drifted — resync required.`, + }); + } + if (curOutline !== entry.hashes.outline) { + findings.push({ level: "block", slug, message: `outline edited — resync required.` }); + } + if (curSnap !== entry.hashes.snapshot) { + findings.push({ + level: "warn", + slug, + message: `rendered snapshot changed — advisory: update the outline if a default/behavior changed.`, + }); + } + + // Output integrity — generated files must match the manifest exactly. + const mdAbs = join(repoRoot, outline.mdPath); + if (!existsSync(mdAbs)) { + findings.push({ + level: "block", + slug, + message: `missing generated output ${outline.mdPath} — run sync.`, + }); + } else if (hash(normalizeText(readFileSync(mdAbs, "utf8"))) !== entry.hashes.outputMd) { + findings.push({ + level: "block", + slug, + message: `${outline.mdPath} was hand-edited or is stale — never edit generated files; run sync.`, + }); + } + + if (entry.hashes.outputExamples) { + const exAbs = join(repoRoot, outline.examplesPath); + if (!existsSync(exAbs)) { + findings.push({ level: "block", slug, message: `missing ${outline.examplesPath}.` }); + } else if (hash(normalizeText(readFileSync(exAbs, "utf8"))) !== entry.hashes.outputExamples) { + findings.push({ + level: "block", + slug, + message: `${outline.examplesPath} changed since last sync — its .md code fences are stale; run sync.`, + }); + } + } + } + + return { findings, ok: !findings.some((f) => f.level === "block") }; +} diff --git a/packages/docs-kit/src/cli.ts b/packages/docs-kit/src/cli.ts new file mode 100644 index 00000000..ecfa9a1b --- /dev/null +++ b/packages/docs-kit/src/cli.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env node +import { check } from "./check"; +import type { Finding } from "./coverage"; +import { sync } from "./sync"; + +function printFindings(findings: Finding[]): void { + for (const f of findings) { + const tag = f.level === "block" ? "✖ BLOCK" : "⚠ warn "; + const where = f.slug ? `[${f.slug}] ` : ""; + console.log(` ${tag} ${where}${f.message}`); + } +} + +function resolveRoot(argv: string[]): string { + const i = argv.indexOf("--root"); + return i >= 0 && argv[i + 1] ? argv[i + 1] : process.cwd(); +} + +const cmd = process.argv[2]; +const repoRoot = resolveRoot(process.argv); + +if (cmd === "sync") { + const { written, findings } = sync(repoRoot); + console.log(`docs-kit sync — wrote ${written.length} document(s):`); + for (const w of written) console.log(` • ${w}`); + if (findings.length > 0) { + console.log("\nfindings:"); + printFindings(findings); + } + process.exit(0); +} else if (cmd === "check") { + const { findings, ok } = check(repoRoot); + if (findings.length === 0) { + console.log("docs-kit check — ✓ all units in sync."); + } else { + console.log("docs-kit check:"); + printFindings(findings); + console.log(ok ? "\n✓ no blocking issues (advisories only)." : "\n✖ blocking issues found."); + } + process.exit(ok ? 0 : 1); +} else { + console.log("usage: docs-kit [--root ]"); + process.exit(2); +} diff --git a/packages/docs-kit/src/config.ts b/packages/docs-kit/src/config.ts new file mode 100644 index 00000000..a23abe5a --- /dev/null +++ b/packages/docs-kit/src/config.ts @@ -0,0 +1,20 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import type { DocsConfig } from "./types"; + +/** Load `docs.config.json` from the repo root, applying defaults. */ +export function loadConfig(repoRoot: string): DocsConfig { + const raw = readFileSync(join(repoRoot, "docs.config.json"), "utf8"); + const parsed = JSON.parse(raw) as Partial; + return { + roots: parsed.roots ?? ["packages/core/src", "docs-src"], + tsconfig: parsed.tsconfig ?? "packages/core/tsconfig.json", + indexFile: parsed.indexFile ?? "packages/core/src/index.ts", + manifestFile: parsed.manifestFile ?? "docs/docs-manifest.json", + categories: parsed.categories ?? [], + enforceCoverage: parsed.enforceCoverage ?? false, + exclusions: parsed.exclusions ?? [], + snapshotDir: parsed.snapshotDir, + }; +} diff --git a/packages/docs-kit/src/coverage.ts b/packages/docs-kit/src/coverage.ts new file mode 100644 index 00000000..979b74fc --- /dev/null +++ b/packages/docs-kit/src/coverage.ts @@ -0,0 +1,84 @@ +import picomatch from "picomatch"; + +import type { Surface } from "./project"; +import type { DocsConfig, Outline } from "./types"; + +export interface Finding { + level: "block" | "warn"; + slug: string | null; + message: string; +} + +/** Public export names owned by a code-backed unit — those whose resolved + * declaration file matches one of the unit's `sources` globs. */ +export function ownedSymbols(surface: Surface, sources: string[]): string[] { + const matchers = sources.map((g) => picomatch(g, { dot: true })); + const owned: string[] = []; + for (const sym of surface.symbols.values()) { + if (sym.external) continue; + if (matchers.some((m) => m(sym.file))) owned.push(sym.name); + } + return owned.sort(); +} + +/** Reconcile the public surface against all outlines, both directions. */ +export function reconcile( + surface: Surface, + outlines: Outline[], + config: DocsConfig, +): { findings: Finding[]; ownedBySlug: Map } { + const findings: Finding[] = []; + const ownedBySlug = new Map(); + const claimed = new Set(); + + for (const o of outlines) { + if (o.kind === "code-backed") { + const owned = ownedSymbols(surface, o.frontmatter.sources ?? []); + ownedBySlug.set(o.slug, owned); + owned.forEach((n) => claimed.add(n)); + // Reverse check (the "timeline" class of bug): a documented unit that + // resolves to zero public exports is documenting something unshipped. + if (owned.length === 0) { + findings.push({ + level: "block", + slug: o.slug, + message: `"${o.slug}" is a code-backed unit but its \`sources\` own no export from index.ts — is it exported?`, + }); + } + } else if (o.kind === "reference") { + const claims = o.frontmatter.claims ?? []; + ownedBySlug.set(o.slug, claims); + for (const name of claims) { + claimed.add(name); + if (!surface.symbols.has(name)) { + findings.push({ + level: "block", + slug: o.slug, + message: `reference unit "${o.slug}" claims "${name}", which is not exported from index.ts`, + }); + } + } + } else { + ownedBySlug.set(o.slug, []); + } + } + + // Forward check: every public export must be claimed or excluded. + const excluded = new Set(config.exclusions); + const uncovered: string[] = []; + for (const name of surface.symbols.keys()) { + if (!claimed.has(name) && !excluded.has(name)) uncovered.push(name); + } + if (uncovered.length > 0) { + findings.push({ + level: config.enforceCoverage ? "block" : "warn", + slug: null, + message: + `${uncovered.length} public export(s) undocumented` + + `${config.enforceCoverage ? "" : " (coverage not yet enforced)"}: ` + + uncovered.sort().join(", "), + }); + } + + return { findings, ownedBySlug }; +} diff --git a/packages/docs-kit/src/hash.ts b/packages/docs-kit/src/hash.ts new file mode 100644 index 00000000..c19425fa --- /dev/null +++ b/packages/docs-kit/src/hash.ts @@ -0,0 +1,14 @@ +import { createHash } from "node:crypto"; + +/** Short, stable content hash. 16 hex chars keeps the manifest readable while + * remaining collision-safe for our scale. */ +export function hash(input: string): string { + return createHash("sha256").update(input).digest("hex").slice(0, 16); +} + +/** Normalize text so that formatting churn (line wrapping, indentation, blank + * lines — e.g. from oxfmt) never moves a hash; only content changes do. All + * whitespace runs collapse to a single space. */ +export function normalizeText(input: string): string { + return input.replace(/\s+/g, " ").trim(); +} diff --git a/packages/docs-kit/src/index.ts b/packages/docs-kit/src/index.ts new file mode 100644 index 00000000..5c06d4ba --- /dev/null +++ b/packages/docs-kit/src/index.ts @@ -0,0 +1,16 @@ +export { loadConfig } from "./config"; +export { discoverOutlines } from "./outline"; +export { + loadSurface, + snapshotHashForSlug, + type Surface, + type PublicSymbol, + type PropRow, +} from "./project"; +export { ownedSymbols, reconcile, type Finding } from "./coverage"; +export { assembleMarkdown } from "./assemble"; +export { readManifest, writeManifest } from "./manifest"; +export { sync, type SyncResult } from "./sync"; +export { check, type CheckResult } from "./check"; +export * from "./types"; +export { hash, normalizeText } from "./hash"; diff --git a/packages/docs-kit/src/manifest.ts b/packages/docs-kit/src/manifest.ts new file mode 100644 index 00000000..d3be0f51 --- /dev/null +++ b/packages/docs-kit/src/manifest.ts @@ -0,0 +1,16 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +import type { Manifest } from "./types"; + +export function readManifest(repoRoot: string, manifestRel: string): Manifest | null { + const p = join(repoRoot, manifestRel); + if (!existsSync(p)) return null; + return JSON.parse(readFileSync(p, "utf8")) as Manifest; +} + +export function writeManifest(repoRoot: string, manifestRel: string, manifest: Manifest): void { + const p = join(repoRoot, manifestRel); + mkdirSync(dirname(p), { recursive: true }); + writeFileSync(p, `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); +} diff --git a/packages/docs-kit/src/outline.ts b/packages/docs-kit/src/outline.ts new file mode 100644 index 00000000..6f457dcc --- /dev/null +++ b/packages/docs-kit/src/outline.ts @@ -0,0 +1,67 @@ +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { basename, join, relative } from "node:path"; + +import matter from "gray-matter"; +import picomatch from "picomatch"; + +import type { DocsConfig, Outline, OutlineFrontmatter, UnitKind } from "./types"; + +const OUTLINE_SUFFIX = ".docs-outline.md"; + +function toPosix(p: string): string { + return p.split("\\").join("/"); +} + +function walk(dir: string, out: string[] = []): string[] { + if (!existsSync(dir)) return out; + for (const name of readdirSync(dir)) { + if (name === "node_modules" || name === "dist" || name.startsWith(".")) continue; + const full = join(dir, name); + if (statSync(full).isDirectory()) walk(full, out); + else if (name.endsWith(OUTLINE_SUFFIX)) out.push(full); + } + return out; +} + +function classify(fm: OutlineFrontmatter, outlineRel: string): UnitKind { + if (fm.sources && fm.sources.length > 0) return "code-backed"; + if ((fm.claims && fm.claims.length > 0) || outlineRel.includes("/references/")) + return "reference"; + return "prose"; +} + +function outDirFor(outlineRel: string, config: DocsConfig): string | null { + for (const rule of config.categories) { + if (picomatch(rule.match, { dot: true })(outlineRel)) return rule.outDir; + } + return null; +} + +/** Discover and parse every `*.docs-outline.md` under the configured roots. */ +export function discoverOutlines(repoRoot: string, config: DocsConfig): Outline[] { + const outlines: Outline[] = []; + for (const root of config.roots) { + for (const outlinePath of walk(join(repoRoot, root))) { + const outlineRel = toPosix(relative(repoRoot, outlinePath)); + const parsed = matter(readFileSync(outlinePath, "utf8")); + const fm = parsed.data as OutlineFrontmatter; + const kind = classify(fm, outlineRel); + const slug = fm.group ?? basename(outlinePath).slice(0, -OUTLINE_SUFFIX.length); + const outDir = outDirFor(outlineRel, config); + if (!outDir) throw new Error(`docs-kit: no category rule matches outline "${outlineRel}"`); + outlines.push({ + kind, + slug, + outlinePath, + outlineRel, + frontmatter: fm, + body: parsed.content, + outDir, + mdPath: `${outDir}/${slug}.md`, + examplesPath: `${outDir}/${slug}.examples.tsx`, + }); + } + } + outlines.sort((a, b) => a.slug.localeCompare(b.slug)); + return outlines; +} diff --git a/packages/docs-kit/src/project.ts b/packages/docs-kit/src/project.ts new file mode 100644 index 00000000..fec5a143 --- /dev/null +++ b/packages/docs-kit/src/project.ts @@ -0,0 +1,155 @@ +import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { join, relative } from "node:path"; + +import { Node, Project, SymbolFlags } from "ts-morph"; + +import { hash, normalizeText } from "./hash"; +import type { DocsConfig } from "./types"; + +function toPosix(p: string): string { + return p.split("\\").join("/"); +} + +export interface PropRow { + name: string; + type: string; + optional: boolean; + description: string; +} + +/** For an exported `*Props` interface/type alias, list its FIRST-PARTY props — + * skipping members inherited purely from `node_modules` (e.g. the hundreds of + * DOM attributes from `React.ComponentProps<"button">`), so only the props this + * component actually adds (variants, render, etc.) surface in the table. */ +function extractProps(name: string, decl: Node): PropRow[] | null { + if (!/Props$/.test(name)) return null; + if (!(Node.isInterfaceDeclaration(decl) || Node.isTypeAliasDeclaration(decl))) return null; + let properties; + try { + properties = decl.getType().getProperties(); + } catch { + return null; + } + const rows: PropRow[] = []; + for (const prop of properties) { + const decls = prop.getDeclarations(); + const allExternal = + decls.length > 0 && + decls.every((d) => d.getSourceFile().getFilePath().includes("/node_modules/")); + if (allExternal) continue; // inherited DOM/lib prop — not part of our surface + + const d0 = decls[0]; + let optional = (prop.getFlags() & SymbolFlags.Optional) !== 0; + let description = ""; + if (d0 && (Node.isPropertySignature(d0) || Node.isPropertyDeclaration(d0))) { + optional = d0.hasQuestionToken(); + const jsdocs = d0.getJsDocs(); + if (jsdocs.length > 0) + description = (jsdocs[jsdocs.length - 1].getCommentText() ?? "").trim(); + } + let typeText = "unknown"; + try { + typeText = prop.getTypeAtLocation(decl).getText(decl); + } catch { + /* keep fallback */ + } + // `?` already conveys optionality — drop the trailing `| undefined` noise. + typeText = typeText.replace(/\s*\|\s*undefined\s*$/, ""); + rows.push({ name: prop.getName(), type: typeText, optional, description }); + } + rows.sort((a, b) => a.name.localeCompare(b.name)); + return rows.length > 0 ? rows : null; +} + +export interface PublicSymbol { + name: string; + /** Repo-relative file of the resolved declaration (may be under node_modules). */ + file: string; + /** Declaration resolves into node_modules — an external re-export. */ + external: boolean; + /** Resolved type text, used for the type-surface hash. */ + typeText: string; + /** Local declaration text (bodies included) — used for the readable API section. */ + declText: string; + kind: string; + /** First-party props, for `*Props` symbols; null otherwise. */ + props: PropRow[] | null; +} + +export interface Surface { + /** Public export name → resolved info. */ + symbols: Map; + /** Type-surface hash over a set of owned names; null when empty. */ + hashSymbols(names: string[]): string | null; +} + +/** Load the public surface from `index.ts` via its tsconfig, resolving every + * re-export to its originating declaration and resolved type. */ +export function loadSurface(repoRoot: string, config: DocsConfig): Surface { + const project = new Project({ + tsConfigFilePath: join(repoRoot, config.tsconfig), + }); + const index = project.getSourceFileOrThrow(join(repoRoot, config.indexFile)); + const symbols = new Map(); + + for (const [name, decls] of index.getExportedDeclarations()) { + const decl = decls[0]; + if (!decl) continue; + const sourceFile = decl.getSourceFile(); + let typeText: string; + try { + typeText = decl.getType().getText(decl); + } catch { + typeText = decl.getText(); + } + symbols.set(name, { + name, + file: toPosix(relative(repoRoot, sourceFile.getFilePath())), + external: sourceFile.isInNodeModules(), + typeText, + declText: safeDeclText(decl.getText()), + kind: decl.getKindName(), + props: extractProps(name, decl), + }); + } + + return { + symbols, + hashSymbols(names: string[]): string | null { + if (names.length === 0) return null; + const parts = names + .slice() + .sort() + .map((n) => { + const s = symbols.get(n); + return `${n}::${s ? s.typeText : ""}`; + }); + return hash(normalizeText(parts.join("\n"))); + }, + }; +} + +/** Trim runaway declaration text (e.g. a component whose body is inline). */ +function safeDeclText(text: string): string { + const trimmed = text.trim(); + return trimmed.length > 2000 ? `${trimmed.slice(0, 2000)}\n/* …truncated… */` : trimmed; +} + +/** Advisory snapshot signal: hash any centralized snapshot files whose flattened + * name references this unit's slug. Best-effort — never blocks. */ +export function snapshotHashForSlug( + repoRoot: string, + config: DocsConfig, + slug: string, +): string | null { + if (!config.snapshotDir) return null; + const dir = join(repoRoot, config.snapshotDir); + if (!existsSync(dir)) return null; + const token = slug.replace(/-/g, "-"); + const matched = readdirSync(dir) + .filter((f) => f.endsWith(".snap") && f.includes(token)) + .sort(); + if (matched.length === 0) return null; + const contents = matched.map((f) => readFileSync(join(dir, f), "utf8")).join("\n"); + return hash(normalizeText(contents)); +} diff --git a/packages/docs-kit/src/sync.ts b/packages/docs-kit/src/sync.ts new file mode 100644 index 00000000..7e073d5e --- /dev/null +++ b/packages/docs-kit/src/sync.ts @@ -0,0 +1,99 @@ +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; + +import { assembleMarkdown } from "./assemble"; +import { loadConfig } from "./config"; +import { type Finding, reconcile } from "./coverage"; +import { hash, normalizeText } from "./hash"; +import { writeManifest } from "./manifest"; +import { discoverOutlines } from "./outline"; +import { loadSurface, snapshotHashForSlug } from "./project"; +import type { Manifest, ManifestEntry } from "./types"; + +export interface SyncResult { + manifest: Manifest; + findings: Finding[]; + written: string[]; +} + +/** Run the repo formatter (oxfmt) over the given files so that committed output + * is byte-identical to what the manifest hashes. This makes the pre-commit + * oxfmt hook idempotent — it can never reformat a generated file after the fact + * and silently invalidate the drift gate. Best-effort: if oxfmt isn't present, + * hashing simply falls back to sync's own output. */ +function formatFiles(repoRoot: string, paths: string[]): void { + const bin = join(repoRoot, "node_modules/.bin/oxfmt"); + const targets = [...new Set(paths)].filter((p) => existsSync(p)); + if (targets.length === 0 || !existsSync(bin)) return; + try { + execFileSync(bin, ["--no-error-on-unmatched-pattern", ...targets], { + cwd: repoRoot, + stdio: "ignore", + }); + } catch { + /* formatting is best-effort */ + } +} + +/** Deterministically (re)generate every unit's markdown from its outline + + * authored examples + resolved type surface, format everything with the repo + * formatter, then rewrite the manifest with hashes of the FORMATTED files. */ +export function sync(repoRoot: string): SyncResult { + const config = loadConfig(repoRoot); + const outlines = discoverOutlines(repoRoot, config); + const surface = loadSurface(repoRoot, config); + const { findings, ownedBySlug } = reconcile(surface, outlines, config); + + // Phase 1 — assemble + write every markdown output. + const written: string[] = []; + const toFormat: string[] = []; + for (const outline of outlines) { + const owned = ownedBySlug.get(outline.slug) ?? []; + const md = assembleMarkdown({ repoRoot, outline, surface, owned }); + const mdAbs = join(repoRoot, outline.mdPath); + mkdirSync(dirname(mdAbs), { recursive: true }); + writeFileSync(mdAbs, md, "utf8"); + written.push(outline.mdPath); + toFormat.push(mdAbs, outline.outlinePath); + const examplesAbs = join(repoRoot, outline.examplesPath); + if (existsSync(examplesAbs)) toFormat.push(examplesAbs); + } + + // Phase 2 — format outputs + sources so the committed bytes match the hashes. + formatFiles(repoRoot, toFormat); + + // Phase 3 — hash the FORMATTED files and build the manifest. + const units: Record = {}; + for (const outline of outlines) { + const owned = ownedBySlug.get(outline.slug) ?? []; + const mdAbs = join(repoRoot, outline.mdPath); + const examplesAbs = join(repoRoot, outline.examplesPath); + const hasExamples = existsSync(examplesAbs); + + units[outline.slug] = { + slug: outline.slug, + kind: outline.kind, + outline: outline.outlineRel, + output: outline.mdPath, + examples: hasExamples ? outline.examplesPath : null, + sources: outline.frontmatter.sources ?? [], + claims: outline.frontmatter.claims ?? [], + symbols: owned, + hashes: { + typeSurface: outline.kind === "code-backed" ? surface.hashSymbols(owned) : null, + outline: hash(normalizeText(readFileSync(outline.outlinePath, "utf8"))), + snapshot: + outline.kind === "code-backed" + ? snapshotHashForSlug(repoRoot, config, outline.slug) + : null, + outputMd: hash(normalizeText(readFileSync(mdAbs, "utf8"))), + outputExamples: hasExamples ? hash(normalizeText(readFileSync(examplesAbs, "utf8"))) : null, + }, + }; + } + + const manifest: Manifest = { version: 1, units }; + writeManifest(repoRoot, config.manifestFile, manifest); + return { manifest, findings, written }; +} diff --git a/packages/docs-kit/src/types.ts b/packages/docs-kit/src/types.ts new file mode 100644 index 00000000..8fa232ed --- /dev/null +++ b/packages/docs-kit/src/types.ts @@ -0,0 +1,87 @@ +/** A documentable unit is code-backed (owns exported symbols), a reference to + * re-exported third-party API, or free prose (concepts / patterns). */ +export type UnitKind = "code-backed" | "reference" | "prose"; + +export interface OutlineFrontmatter { + /** Slug override; defaults to the filename minus `.docs-outline.md`. */ + group?: string; + title?: string; + description?: string; + /** Code-backed units: globs (repo-relative) whose exported symbols this unit owns. */ + sources?: string[]; + /** Reference units: names of re-exported symbols this unit documents. */ + claims?: string[]; + /** Reference units: upstream docs URL. */ + upstream?: string; +} + +export interface Outline { + kind: UnitKind; + slug: string; + /** Absolute path to the outline file. */ + outlinePath: string; + /** Repo-relative path to the outline file. */ + outlineRel: string; + frontmatter: OutlineFrontmatter; + /** Markdown body with frontmatter stripped. */ + body: string; + /** Repo-relative output directory. */ + outDir: string; + /** Repo-relative path of the generated markdown. */ + mdPath: string; + /** Repo-relative path of the (authored) examples module. */ + examplesPath: string; +} + +export interface UnitHashes { + /** Resolved type-surface hash — null for reference/prose units. */ + typeSurface: string | null; + outline: string; + /** Advisory only. */ + snapshot: string | null; + outputMd: string | null; + outputExamples: string | null; +} + +export interface ManifestEntry { + slug: string; + kind: UnitKind; + outline: string; + output: string; + examples: string | null; + sources: string[]; + claims: string[]; + /** Resolved public export names this unit owns (code-backed) or claims (reference). */ + symbols: string[]; + hashes: UnitHashes; +} + +export interface Manifest { + version: number; + units: Record; +} + +export interface CategoryRule { + /** Glob (repo-relative) matched against an outline's path. */ + match: string; + /** Repo-relative output directory for matching outlines. */ + outDir: string; +} + +export interface DocsConfig { + /** Repo-relative roots scanned for `*.docs-outline.md`. */ + roots: string[]; + /** Repo-relative tsconfig used to load the type project. */ + tsconfig: string; + /** Repo-relative entry file defining the public surface. */ + indexFile: string; + /** Repo-relative manifest path. */ + manifestFile: string; + categories: CategoryRule[]; + /** When false (migration phase), uncovered exports warn instead of blocking. */ + enforceCoverage: boolean; + /** Export names deliberately left undocumented (rare escape hatch). */ + exclusions: string[]; + /** Repo-relative dir holding centralized test snapshots (advisory signal). */ + snapshotDir?: string; +} diff --git a/packages/docs-kit/tsconfig.json b/packages/docs-kit/tsconfig.json new file mode 100644 index 00000000..c44fc99c --- /dev/null +++ b/packages/docs-kit/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"], + "skipLibCheck": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true + }, + "include": ["src"] +} diff --git a/packages/docs-kit/tsdown.config.ts b/packages/docs-kit/tsdown.config.ts new file mode 100644 index 00000000..ed1c3e4b --- /dev/null +++ b/packages/docs-kit/tsdown.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: { + index: "src/index.ts", + cli: "src/cli.ts", + }, + format: ["esm"], + platform: "node", + dts: true, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3b42362..1df2b5ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,6 +142,55 @@ importers: specifier: 'catalog:' version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) + examples/docs-app: + dependencies: + '@tailor-platform/app-shell': + specifier: workspace:* + version: link:../../packages/core + '@tailor-platform/app-shell-vite-plugin': + specifier: workspace:* + version: link:../../packages/vite-plugin + lucide-react: + specifier: 'catalog:' + version: 1.8.0(react@19.2.6) + react: + specifier: 'catalog:' + version: 19.2.6 + react-dom: + specifier: 'catalog:' + version: 19.2.6(react@19.2.6) + react-markdown: + specifier: ^9.0.1 + version: 9.1.0(@types/react@19.2.13)(react@19.2.6) + remark-gfm: + specifier: ^4.0.0 + version: 4.0.1 + tailwindcss: + specifier: 'catalog:' + version: 4.3.0 + devDependencies: + '@tailwindcss/vite': + specifier: ^4.3.0 + version: 4.3.0(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + '@types/node': + specifier: 'catalog:' + version: 25.9.1 + '@types/react': + specifier: 'catalog:' + version: 19.2.13 + '@types/react-dom': + specifier: 'catalog:' + version: 19.2.3(@types/react@19.2.13) + '@vitejs/plugin-react': + specifier: 'catalog:' + version: 5.2.0(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + typescript: + specifier: ^5 + version: 5.9.3 + vite: + specifier: 'catalog:' + version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0) + examples/nextjs-app: dependencies: '@hookform/resolvers': @@ -343,6 +392,34 @@ importers: specifier: 'catalog:' version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.9.0)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/docs-kit: + dependencies: + gray-matter: + specifier: ^4.0.3 + version: 4.0.3 + picomatch: + specifier: ^4.0.2 + version: 4.0.4 + ts-morph: + specifier: ^28.0.0 + version: 28.0.0 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 25.9.1 + '@types/picomatch': + specifier: ^4.0.0 + version: 4.0.3 + tsdown: + specifier: 'catalog:' + version: 0.22.0(tsx@4.22.4)(typescript@5.9.3) + typescript: + specifier: ^5 + version: 5.9.3 + vitest: + specifier: 'catalog:' + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.9.0)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/sdk-plugin: devDependencies: '@tailor-platform/app-shell': @@ -2276,9 +2353,6 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - '@oxc-project/types@0.132.0': - resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} - '@oxc-project/types@0.137.0': resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} @@ -2587,12 +2661,6 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.2': - resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@rolldown/binding-android-arm64@1.1.2': resolution: {integrity: sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2605,12 +2673,6 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.2': - resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@rolldown/binding-darwin-arm64@1.1.2': resolution: {integrity: sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2623,12 +2685,6 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.2': - resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@rolldown/binding-darwin-x64@1.1.2': resolution: {integrity: sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2641,12 +2697,6 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.2': - resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@rolldown/binding-freebsd-x64@1.1.2': resolution: {integrity: sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2659,12 +2709,6 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.1.2': resolution: {integrity: sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2678,13 +2722,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.2': - resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.1.2': resolution: {integrity: sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2699,13 +2736,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.2': - resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.1.2': resolution: {integrity: sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2720,13 +2750,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.1.2': resolution: {integrity: sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2741,13 +2764,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.2': - resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.2': resolution: {integrity: sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2762,13 +2778,6 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.2': - resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.2': resolution: {integrity: sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2783,13 +2792,6 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.2': - resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@rolldown/binding-linux-x64-musl@1.1.2': resolution: {integrity: sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2803,12 +2805,6 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.2': - resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.1.2': resolution: {integrity: sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2820,11 +2816,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.2': - resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.1.2': resolution: {integrity: sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2836,12 +2827,6 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.2': - resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.1.2': resolution: {integrity: sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2854,12 +2839,6 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.2': - resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.2': resolution: {integrity: sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3388,21 +3367,36 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/encoding-japanese@2.2.1': resolution: {integrity: sha512-6jjepuTusvySxMLP7W6usamlbgf0F4sIDvm7EzYePjLHY7zWUv4yz2PLUnu0vuNVtXOTLu2cRdFcDg40J5Owsw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/gradient-string@1.1.6': resolution: {integrity: sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/jsesc@2.5.1': resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -3412,6 +3406,9 @@ packages: '@types/papaparse@5.5.2': resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} + '@types/picomatch@4.0.3': + resolution: {integrity: sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -3423,6 +3420,12 @@ packages: '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/whatwg-mimetype@3.0.2': resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} @@ -3455,6 +3458,9 @@ packages: resolution: {integrity: sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@urql/core@6.0.1': resolution: {integrity: sha512-FZDiQk6jxbj5hixf2rEPv0jI+IZz0EqqGW8mJBEug68/zHTtT+f34guZDmyjJZyiWbj0vL165LoMr/TkeDHaug==} @@ -3668,6 +3674,9 @@ packages: resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -3742,6 +3751,9 @@ packages: caniuse-lite@1.0.30001793: resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -3765,6 +3777,18 @@ packages: change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -3836,6 +3860,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -3926,6 +3953,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4008,6 +4038,9 @@ packages: peerDependencies: typescript: ^5.4.4 || ^6.0.2 + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.2: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} @@ -4109,6 +4142,10 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + escodegen@2.1.0: resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} engines: {node: '>=6.0'} @@ -4127,6 +4164,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -4152,6 +4192,9 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -4323,6 +4366,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -4330,6 +4379,9 @@ packages: hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true @@ -4390,6 +4442,9 @@ packages: react-devtools-core: optional: true + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + inquirer@12.6.3: resolution: {integrity: sha512-eX9beYAjr1MqYsIjx1vAheXsRk1jbZRvHLcBu5nA9wX0rXR1IfCZLnVLp4Ym4mrhqmh7AuANwcdtgQ291fZDfQ==} engines: {node: '>=18'} @@ -4399,10 +4454,19 @@ packages: '@types/node': optional: true + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -4437,6 +4501,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ci@1.0.0: resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} engines: {node: '>=18'} @@ -4467,6 +4534,10 @@ packages: resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} engines: {node: '>=0.10.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-port-reachable@4.0.0: resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -4739,6 +4810,9 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -4776,6 +4850,54 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -4783,6 +4905,90 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -5043,6 +5249,9 @@ packages: papaparse@5.5.3: resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-ms@2.1.0: resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} engines: {node: '>=6'} @@ -5197,6 +5406,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + protobufjs@8.0.3: resolution: {integrity: sha512-LBYnMWkKLB8fE/ljROPDbCl7mgLSlI+oBe1fAAr5MTqFg4TIi0tYrVVurJvQggOjnUYMQtEZBjrej59ojMNTHQ==} engines: {node: '>=12.0.0'} @@ -5259,6 +5471,12 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@9.1.0: + resolution: {integrity: sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-reconciler@0.32.0: resolution: {integrity: sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==} engines: {node: '>=0.10.0'} @@ -5310,6 +5528,18 @@ packages: resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} engines: {node: '>=0.10.0'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -5396,11 +5626,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.2: - resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - rolldown@1.1.2: resolution: {integrity: sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5539,6 +5764,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -5589,6 +5817,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stringify-object@3.3.0: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} @@ -5621,6 +5852,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -5706,6 +5943,12 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -5837,6 +6080,24 @@ packages: resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} engines: {node: '>=22.19.0'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -5877,6 +6138,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-plugin-dts@4.5.4: resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: @@ -6079,6 +6346,9 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@0no-co/graphql.web@1.2.0(graphql@16.13.2)': @@ -7685,8 +7955,6 @@ snapshots: '@oxc-project/types@0.127.0': {} - '@oxc-project/types@0.132.0': {} - '@oxc-project/types@0.137.0': {} '@oxfmt/binding-android-arm-eabi@0.47.0': @@ -7881,108 +8149,72 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-android-arm64@1.0.2': - optional: true - '@rolldown/binding-android-arm64@1.1.2': optional: true '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-arm64@1.0.2': - optional: true - '@rolldown/binding-darwin-arm64@1.1.2': optional: true '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-x64@1.0.2': - optional: true - '@rolldown/binding-darwin-x64@1.1.2': optional: true '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-freebsd-x64@1.0.2': - optional: true - '@rolldown/binding-freebsd-x64@1.1.2': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.2': - optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.2': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.2': - optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.2': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.2': - optional: true - '@rolldown/binding-linux-arm64-musl@1.1.2': optional: true '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.2': - optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.2': optional: true '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.2': - optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.2': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.2': - optional: true - '@rolldown/binding-linux-x64-gnu@1.1.2': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-musl@1.0.2': - optional: true - '@rolldown/binding-linux-x64-musl@1.1.2': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-openharmony-arm64@1.0.2': - optional: true - '@rolldown/binding-openharmony-arm64@1.1.2': optional: true @@ -7993,13 +8225,6 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-wasm32-wasi@1.0.2': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - '@rolldown/binding-wasm32-wasi@1.1.2': dependencies: '@emnapi/core': 1.11.1 @@ -8010,18 +8235,12 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.2': - optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.2': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.2': - optional: true - '@rolldown/binding-win32-x64-msvc@1.1.2': optional: true @@ -8552,18 +8771,36 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} '@types/encoding-japanese@2.2.1': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} '@types/gradient-string@1.1.6': dependencies: '@types/tinycolor2': 1.4.6 + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/jsesc@2.5.1': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@12.20.55': {} '@types/node@25.9.1': @@ -8574,6 +8811,8 @@ snapshots: dependencies: '@types/node': 25.9.1 + '@types/picomatch@4.0.3': {} + '@types/react-dom@19.2.3(@types/react@19.2.13)': dependencies: '@types/react': 19.2.13 @@ -8584,6 +8823,10 @@ snapshots: '@types/tinycolor2@1.4.6': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': @@ -8613,7 +8856,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.59.1 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 + semver: 7.8.5 tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -8625,6 +8868,8 @@ snapshots: '@typescript-eslint/types': 8.59.1 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.3': {} + '@urql/core@6.0.1(graphql@16.13.2)': dependencies: '@0no-co/graphql.web': 1.2.0(graphql@16.13.2) @@ -8875,6 +9120,8 @@ snapshots: auto-bind@5.0.1: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -8950,6 +9197,8 @@ snapshots: caniuse-lite@1.0.30001793: {} + ccount@2.0.1: {} + chai@6.2.2: {} chalk-template@0.4.0: @@ -8967,6 +9216,14 @@ snapshots: change-case@5.4.4: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + chardet@2.1.1: {} chokidar@5.0.0: @@ -9026,6 +9283,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + commander@12.1.0: {} commander@13.1.0: {} @@ -9092,6 +9351,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-extend@0.6.0: {} default-browser-id@5.0.1: {} @@ -9180,6 +9443,10 @@ snapshots: transitivePeerDependencies: - supports-color + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@8.0.2: {} dir-glob@3.0.1: @@ -9294,6 +9561,8 @@ snapshots: escape-string-regexp@2.0.0: {} + escape-string-regexp@5.0.0: {} + escodegen@2.1.0: dependencies: esprima: 4.0.1 @@ -9308,6 +9577,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -9336,6 +9607,8 @@ snapshots: dependencies: is-extendable: 0.1.1 + extend@3.0.2: {} + extendable-error@0.1.7: {} fast-deep-equal@3.1.3: {} @@ -9519,10 +9792,36 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + he@1.2.0: {} hookable@6.1.1: {} + html-url-attributes@3.0.1: {} + human-id@4.1.3: {} human-signals@2.1.0: {} @@ -9588,6 +9887,8 @@ snapshots: - bufferutil - utf-8-validate + inline-style-parser@0.2.7: {} + inquirer@12.6.3(@types/node@25.9.1): dependencies: '@inquirer/core': 10.3.2(@types/node@25.9.1) @@ -9600,10 +9901,19 @@ snapshots: optionalDependencies: '@types/node': 25.9.1 + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-core-module@2.16.1: dependencies: hasown: 2.0.2 + is-decimal@2.0.1: {} + is-docker@2.2.1: {} is-docker@3.0.0: {} @@ -9624,6 +9934,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-in-ci@1.0.0: {} is-in-ssh@1.0.0: {} @@ -9640,6 +9952,8 @@ snapshots: is-obj@1.0.1: {} + is-plain-obj@4.1.0: {} + is-port-reachable@4.0.0: {} is-regexp@1.0.0: {} @@ -9845,6 +10159,8 @@ snapshots: long@5.3.2: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -9888,10 +10204,356 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-table@3.0.4: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + merge-stream@2.0.0: {} merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -10209,6 +10871,16 @@ snapshots: papaparse@5.5.3: {} + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-ms@2.1.0: {} patch-console@2.0.0: {} @@ -10359,6 +11031,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.2.0: {} + protobufjs@8.0.3: dependencies: '@types/node': 25.9.1 @@ -10413,6 +11087,24 @@ snapshots: react-is@17.0.2: {} + react-markdown@9.1.0(@types/react@19.2.13)(react@19.2.6): + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/react': 19.2.13 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.6 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-reconciler@0.32.0(react@19.1.1): dependencies: react: 19.1.1 @@ -10458,6 +11150,40 @@ snapshots: dependencies: rc: 1.2.8 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + require-from-string@2.0.2: {} requirejs-config-file@4.0.0: @@ -10513,7 +11239,7 @@ snapshots: reusify@1.1.0: {} - rolldown-plugin-dts@0.25.1(rolldown@1.0.2)(typescript@5.9.3): + rolldown-plugin-dts@0.25.1(rolldown@1.1.2)(typescript@5.9.3): dependencies: '@babel/generator': 8.0.0-rc.5 '@babel/helper-validator-identifier': 8.0.0-rc.5 @@ -10523,7 +11249,7 @@ snapshots: dts-resolver: 3.0.0 get-tsconfig: 5.0.0-beta.5 obug: 2.1.1 - rolldown: 1.0.2 + rolldown: 1.1.2 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -10550,27 +11276,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 - rolldown@1.0.2: - dependencies: - '@oxc-project/types': 0.132.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.2 - '@rolldown/binding-darwin-arm64': 1.0.2 - '@rolldown/binding-darwin-x64': 1.0.2 - '@rolldown/binding-freebsd-x64': 1.0.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 - '@rolldown/binding-linux-arm64-gnu': 1.0.2 - '@rolldown/binding-linux-arm64-musl': 1.0.2 - '@rolldown/binding-linux-ppc64-gnu': 1.0.2 - '@rolldown/binding-linux-s390x-gnu': 1.0.2 - '@rolldown/binding-linux-x64-gnu': 1.0.2 - '@rolldown/binding-linux-x64-musl': 1.0.2 - '@rolldown/binding-openharmony-arm64': 1.0.2 - '@rolldown/binding-wasm32-wasi': 1.0.2 - '@rolldown/binding-win32-arm64-msvc': 1.0.2 - '@rolldown/binding-win32-x64-msvc': 1.0.2 - rolldown@1.1.2: dependencies: '@oxc-project/types': 0.137.0 @@ -10665,7 +11370,8 @@ snapshots: semver@7.7.4: {} - semver@7.8.0: {} + semver@7.8.0: + optional: true semver@7.8.5: {} @@ -10770,6 +11476,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -10822,6 +11530,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + stringify-object@3.3.0: dependencies: get-own-enumerable-property-symbols: 3.0.2 @@ -10846,6 +11559,14 @@ snapshots: strip-json-comments@3.1.1: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + styled-jsx@5.1.6(react@19.2.6): dependencies: client-only: 0.0.1 @@ -10909,6 +11630,10 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -10947,9 +11672,9 @@ snapshots: import-without-cache: 0.4.0 obug: 2.1.1 picomatch: 4.0.4 - rolldown: 1.0.2 - rolldown-plugin-dts: 0.25.1(rolldown@1.0.2)(typescript@5.9.3) - semver: 7.8.0 + rolldown: 1.1.2 + rolldown-plugin-dts: 0.25.1(rolldown@1.1.2)(typescript@5.9.3) + semver: 7.8.5 tinyexec: 1.2.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 @@ -11020,6 +11745,39 @@ snapshots: undici@8.5.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@0.1.2: {} universalify@2.0.1: {} @@ -11051,6 +11809,16 @@ snapshots: vary@1.1.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-plugin-dts@4.5.4(@types/node@25.9.1)(rollup@4.60.0)(typescript@5.9.3)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@microsoft/api-extractor': 7.55.2(@types/node@25.9.1) @@ -11204,3 +11972,5 @@ snapshots: zod@4.3.6: {} zod@4.4.3: {} + + zwitch@2.0.4: {}