From ada419da6f69d2128cc5294eaf8e08dfc670efef Mon Sep 17 00:00:00 2001 From: interacsean Date: Tue, 29 Sep 2026 14:34:05 +1000 Subject: [PATCH] feat(docs-kit): block unmanaged files in docs/; make the root guides ordinary units MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An agent adding a component wrote `docs/components/toolbar.md` straight into the generated tree (app-shell#559) and `docs:check` said nothing: check walked outlines→outputs and manifest→outlines, so a file that was neither was invisible rather than rejected. Closing that cleanly meant removing the last hand-authored files under `docs/`. The four root guides are now `kind: prose` units in `docs-src/guides/` that map to the `docs/` root, so there is one authoring mechanism rather than a general rule plus four exceptions — and, since they are units, their code fences can later become tokenised, type-checked examples (quickstart carries five). Their published content is unchanged apart from the generated-by banner. `check` then needs no special cases: every `.md` under an output root must be a unit's output, and anything else blocks with "has no source". Two docs-browser fixes this exposed: - category is derived from the output directory, which is empty for a root-level output; root outputs now group under `guides`. - relative `.md` links are authored against the generated tree, but routes are not the same shape (`docs/api/guards/hidden.md` is served at `/api/guards/hidden`). They are now resolved in doc-space against the unit's output and then mapped to a route, so a guide linking `./concepts/x.md` reaches `/concepts/x` rather than `/guides/concepts/x`. CLAUDE.md, CONTRIBUTING, the decision record and the resync-docs skill drop the four-file exception; CLAUDE.md also no longer claims the gate catches a case it did not. Refs tailor-inc/platform-planning#1549 Co-Authored-By: Claude Opus 5 --- .agents/skills/resync-docs/SKILL.md | 1 + CLAUDE.md | 11 +- CONTRIBUTING.md | 24 +-- .../documentation-management-overhaul.md | 34 ++-- docs-browser/src/App.tsx | 3 +- docs-browser/src/_lib/DocPage.tsx | 23 ++- docs-browser/src/docs.tsx | 17 +- docs-kit.config.json | 47 ++++- docs-manifest.json | 70 ++++++- .../guides/design-philosophy.docs.outline.md | 83 ++++++++ docs-src/guides/introduction.docs.outline.md | 47 +++++ docs-src/guides/migrations.docs.outline.md | 174 +++++++++++++++++ docs-src/guides/quickstart.docs.outline.md | 181 ++++++++++++++++++ docs/design-philosophy.md | 2 + docs/introduction.md | 2 + docs/migrations.md | 2 + docs/quickstart.md | 2 + packages/docs-kit/src/check.ts | 25 ++- packages/docs-kit/src/tree.test.ts | 35 ++++ packages/docs-kit/src/tree.ts | 22 +++ 20 files changed, 750 insertions(+), 55 deletions(-) create mode 100644 docs-src/guides/design-philosophy.docs.outline.md create mode 100644 docs-src/guides/introduction.docs.outline.md create mode 100644 docs-src/guides/migrations.docs.outline.md create mode 100644 docs-src/guides/quickstart.docs.outline.md create mode 100644 packages/docs-kit/src/tree.test.ts create mode 100644 packages/docs-kit/src/tree.ts diff --git a/.agents/skills/resync-docs/SKILL.md b/.agents/skills/resync-docs/SKILL.md index 703fb69fd..e59b782ef 100644 --- a/.agents/skills/resync-docs/SKILL.md +++ b/.agents/skills/resync-docs/SKILL.md @@ -53,6 +53,7 @@ For each drifting unit: ## Rules +- The root guides (`introduction`, `quickstart`, `design-philosophy`, `migrations`) are ordinary prose units in `docs-src/guides/` that output to the `docs/` root — edit the outline, not the generated file. - Never edit files under `docs/` by hand — only `*.docs.outline.md` and `*.docs.examples.tsx`. docs-browser derives its routes from `docs-manifest.json` (declarative `modules` in `App.tsx`); there are no generated route stubs. - 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`. diff --git a/CLAUDE.md b/CLAUDE.md index 7579858e0..649dd7a82 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,8 +17,8 @@ Tailor Platform AppShell - A React-based framework for building ERP applications ## Documentation Everything under [`docs/`](./docs/) is **generated** by the `docs-kit` pipeline and must **never be -hand-edited** — the pre-commit hook warns and `pnpm docs:check` in CI blocks on any hand edit or -drift. See [decisions/documentation-management-overhaul.md](./decisions/documentation-management-overhaul.md) +hand-edited** — the pre-commit hook warns and `pnpm docs:check` in CI blocks on any hand edit, +drift, or file created under `docs/` with no source. See [decisions/documentation-management-overhaul.md](./decisions/documentation-management-overhaul.md) and the [`resync-docs`](./.agents/skills/resync-docs/SKILL.md) skill. To change or add a doc, edit the authored **source** under `docs-src/`, then run `pnpm docs:sync`: @@ -34,9 +34,10 @@ units also declare a `sources:` glob binding them to the exports they document, reconciles that against `index.ts` both ways. Frontmatter is a closed schema: an unrecognised key is an error, not a silent no-op. -The only hand-authored files under `docs/` are the four root guides — `introduction.md`, -`quickstart.md`, `design-philosophy.md`, `migrations.md` — which have no `docs-src/` source and are -edited in place (`migrations.md` is also copied into the generated skill). +There are **no exceptions**: every file under `docs/` comes from an outline. The root guides +(`introduction`, `quickstart`, `design-philosophy`, `migrations`) are `kind: prose` units in +`docs-src/guides/` that happen to output to the `docs/` root. Creating a file under `docs/` that no +outline produces is a blocking `docs:check` failure. ## Key Architecture Points (LLM Orientation) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 84f5c9bcf..d139fbd9e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,18 +63,18 @@ repo publishes via changesets (§6) — you won't run `changeset:publish` by han ### Repository layout -| Path | What it is | -| ---------------------- | --------------------------------------------------------------------------------- | -| `packages/core` | `@tailor-platform/app-shell` — the published library (components, hooks, layouts) | -| `packages/vite-plugin` | `@tailor-platform/vite-plugin-app-shell` — file-based routing | -| `packages/sdk-plugin` | `@tailor-platform/sdk-plugin-app-shell` — Tailor SDK plugin | -| `examples/` | `vite-app` reference app and consolidated showcase (what `pnpm dev` runs) | -| `e2e/` | Playwright suite + a real Tailor backend definition | -| `docs-src/` | Authored doc sources — outlines + runnable examples (the only hand-edited docs) | -| `docs/` | **Generated** user-facing documentation — never hand-edited (`pnpm docs:sync`) | -| `docs-browser/` | AppShell app that renders `docs/` with live examples | -| `.agents/skills/` | **Contributor procedures** — the source of truth for how to do the work | -| `.github/` | Agents, prompts, and workflows (CI + agentic bots) | +| Path | What it is | +| ---------------------- | ------------------------------------------------------------------------------------------------- | +| `packages/core` | `@tailor-platform/app-shell` — the published library (components, hooks, layouts) | +| `packages/vite-plugin` | `@tailor-platform/vite-plugin-app-shell` — file-based routing | +| `packages/sdk-plugin` | `@tailor-platform/sdk-plugin-app-shell` — Tailor SDK plugin | +| `examples/` | `vite-app` reference app and consolidated showcase (what `pnpm dev` runs) | +| `e2e/` | Playwright suite + a real Tailor backend definition | +| `docs-src/` | Authored doc sources — outlines (incl. `guides/`) + runnable examples (the only hand-edited docs) | +| `docs/` | **Generated** user-facing documentation — never hand-edited (`pnpm docs:sync`) | +| `docs-browser/` | AppShell app that renders `docs/` with live examples | +| `.agents/skills/` | **Contributor procedures** — the source of truth for how to do the work | +| `.github/` | Agents, prompts, and workflows (CI + agentic bots) | --- diff --git a/decisions/documentation-management-overhaul.md b/decisions/documentation-management-overhaul.md index 462feed87..c258ebbd8 100644 --- a/decisions/documentation-management-overhaul.md +++ b/decisions/documentation-management-overhaul.md @@ -25,21 +25,21 @@ One rule: **humans edit intent (outlines + examples, under `docs-src/`); AI prod ## 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. | -| **Unit kind** | **Declared, never derived.** Every outline states `kind: code-backed` or `kind: prose` in frontmatter. Code-backed ⇒ a `sources` glob list, an owned symbol set, a type-surface hash. Prose ⇒ none of those. Nothing about a unit's contract is inferred from its filename, its directory, or the presence of another key — a unit cannot change kind by accident. | -| **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` (under `docs-src/`) 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 under `docs-src/`: `docs-src/components/…` → `docs/components/`, `docs-src/hooks/…` → `docs/api/`, `docs-src/concepts/…` → `docs/concepts/`, etc. The category is the one thing location decides; `group` (slug), `title` and `description` are **declared** in frontmatter, never defaulted from a filename or title-cased. | -| **Change signal** | **Blocking:** type-surface hash + outline hash. **Advisory (non-blocking):** test-snapshot hash. | -| **Examples** | Per unit: one runnable **`[unit].docs.examples.tsx`** — authored SOURCE, a sibling of the outline under `docs-src/` — with keyed named exports; the `.md` code fences are **derived deterministically** by extracting those exports. Compiles in CI. Per-segment keys ⇒ regenerate only the changed example, reuse the rest as baseline. | -| **Docs site** | A dedicated **AppShell-based renderer app** (`docs-browser/`, dogfooding). Build-time `import.meta.glob` of the generated `docs/**/*.md` + the authored `**/*.docs.examples.tsx` — imports in place, **no copy**. Dev serves from source w/ HMR; prod bundles. | -| **Fix path** | Deterministic pre-merge `check-docs` gate (**no LLM**) blocks the PR on drift. The local `resync-docs` skill is the **only** fix path. `docs-update.lock.yml` is **retired**. Pre-commit _warns_; CI _blocks_. | -| **Non-component docs** | Unified source→output; most of `docs/` is generated, except the four hand-authored root docs (`introduction`, `quickstart`, `design-philosophy`, `migrations`) — and `migrations.md` doubles as an input to the skill generator. Hooks/api are **prose units** today (migrated verbatim; upgradable to code-backed with `sources` later). Concepts/patterns/pages = prose outlines in `docs-src/` (outline-hash trigger only; may embed live-example tokens). Re-exports we don't own are covered by `claims` on the prose unit that documents them — there is no third kind. CSS-token units and the `docs-src/references/` pages are **specified but not yet built**. | -| **Drift baseline & integrity** | The **committed `docs-manifest.json`** stores per unit: output path, `sources` globs, the **input** hashes (type-surface / outline / `*.docs.examples.tsx` / snapshot) **and** the generated-`.md` output hash. `check-docs` recomputes and compares — no git-history diffing. Input drift (incl. an examples edit that leaves the `.md` fences stale) ⇒ needs resync; **a hand-edited generated `.md` ⇒ output-hash mismatch ⇒ CI fail**. | -| **Migration** | AI **reverse-generates** outlines + `*.docs.examples.tsx` + manifest from the existing docs; humans curate/restructure; enforcement phases in per-unit. | -| **`app-shell-patterns`** | A **consumer-facing build skill** emitted by `docs-kit sync` from the generated docs (components, hooks, patterns, pages, concepts) + the authored `SKILL.template.md`, manifest-tracked (hashed in `docs-manifest.json` under `skill`) and validated by `check-docs`. **Implemented** — it subsumes and replaces the retired `catalogue` generator. | +| 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. | +| **Unit kind** | **Declared, never derived.** Every outline states `kind: code-backed` or `kind: prose` in frontmatter. Code-backed ⇒ a `sources` glob list, an owned symbol set, a type-surface hash. Prose ⇒ none of those. Nothing about a unit's contract is inferred from its filename, its directory, or the presence of another key — a unit cannot change kind by accident. | +| **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` (under `docs-src/`) 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 under `docs-src/`: `docs-src/components/…` → `docs/components/`, `docs-src/hooks/…` → `docs/api/`, `docs-src/concepts/…` → `docs/concepts/`, etc. The category is the one thing location decides; `group` (slug), `title` and `description` are **declared** in frontmatter, never defaulted from a filename or title-cased. | +| **Change signal** | **Blocking:** type-surface hash + outline hash. **Advisory (non-blocking):** test-snapshot hash. | +| **Examples** | Per unit: one runnable **`[unit].docs.examples.tsx`** — authored SOURCE, a sibling of the outline under `docs-src/` — with keyed named exports; the `.md` code fences are **derived deterministically** by extracting those exports. Compiles in CI. Per-segment keys ⇒ regenerate only the changed example, reuse the rest as baseline. | +| **Docs site** | A dedicated **AppShell-based renderer app** (`docs-browser/`, dogfooding). Build-time `import.meta.glob` of the generated `docs/**/*.md` + the authored `**/*.docs.examples.tsx` — imports in place, **no copy**. Dev serves from source w/ HMR; prod bundles. | +| **Fix path** | Deterministic pre-merge `check-docs` gate (**no LLM**) blocks the PR on drift. The local `resync-docs` skill is the **only** fix path. `docs-update.lock.yml` is **retired**. Pre-commit _warns_; CI _blocks_. | +| **Non-component docs** | Unified source→output; **all** of `docs/` is generated, with no hand-authored exceptions — the root guides (`introduction`, `quickstart`, `design-philosophy`, `migrations`) are prose units in `docs-src/guides/` that output to the `docs/` root, so they can carry live example tokens like any other unit; `migrations.md` is then read from the generated tree as an input to the skill generator. Hooks/api are **prose units** today (migrated verbatim; upgradable to code-backed with `sources` later). Concepts/patterns/pages = prose outlines in `docs-src/` (outline-hash trigger only; may embed live-example tokens). Re-exports we don't own are covered by `claims` on the prose unit that documents them — there is no third kind. CSS-token units and the `docs-src/references/` pages are **specified but not yet built**. | +| **Drift baseline & integrity** | The **committed `docs-manifest.json`** stores per unit: output path, `sources` globs, the **input** hashes (type-surface / outline / `*.docs.examples.tsx` / snapshot) **and** the generated-`.md` output hash. `check-docs` recomputes and compares — no git-history diffing. Input drift (incl. an examples edit that leaves the `.md` fences stale) ⇒ needs resync; **a hand-edited generated `.md` ⇒ output-hash mismatch ⇒ CI fail**. | +| **Migration** | AI **reverse-generates** outlines + `*.docs.examples.tsx` + manifest from the existing docs; humans curate/restructure; enforcement phases in per-unit. | +| **`app-shell-patterns`** | A **consumer-facing build skill** emitted by `docs-kit sync` from the generated docs (components, hooks, patterns, pages, concepts) + the authored `SKILL.template.md`, manifest-tracked (hashed in `docs-manifest.json` under `skill`) and validated by `check-docs`. **Implemented** — it subsumes and replaces the retired `catalogue` generator. | --- @@ -94,7 +94,7 @@ Declaring `kind` also puts a consequential change where review can see it. A uni | **Examples** | Per unit: one runnable **`[unit].docs.examples.tsx`** — authored SOURCE, a sibling of the outline under `docs-src/` — with keyed named exports; the `.md` code fences are **derived deterministically** by extracting those exports. Compiles in CI. Per-segment keys ⇒ regenerate only the changed example, reuse the rest as baseline. | | **Docs site** | A dedicated **AppShell-based renderer app** (`docs-browser/`, dogfooding). Build-time `import.meta.glob` of the generated `docs/**/*.md` + the authored `**/*.docs.examples.tsx` — imports in place, **no copy**. Dev serves from source w/ HMR; prod bundles. | | **Fix path** | Deterministic pre-merge `check-docs` gate (**no LLM**) blocks the PR on drift. The local `resync-docs` skill is the **only** fix path. `docs-update.lock.yml` is **retired**. Pre-commit _warns_; CI _blocks_. | -| **Non-component docs** | Unified source→output; most of `docs/` is generated, except the four hand-authored root docs (`introduction`, `quickstart`, `design-philosophy`, `migrations`) — and `migrations.md` doubles as an input to the skill generator. Hooks/api are **prose units** today (migrated verbatim; upgradable to code-backed with `sources` later). Concepts/patterns/pages = prose outlines in `docs-src/` (outline-hash trigger only; may embed live-example tokens). CSS-token units and re-export **reference units** are **specified but not yet built**. | +| **Non-component docs** | Unified source→output; **all** of `docs/` is generated, with no hand-authored exceptions — the root guides are prose units in `docs-src/guides/` that output to the `docs/` root. Hooks/api are **prose units** today (migrated verbatim; upgradable to code-backed with `sources` later). Concepts/patterns/pages = prose outlines in `docs-src/` (outline-hash trigger only; may embed live-example tokens). CSS-token units and re-export **reference units** are **specified but not yet built**. | | **Drift baseline & integrity** | The **committed `docs-manifest.json`** stores per unit: output path, `sources` globs, the **input** hashes (type-surface / outline / `*.docs.examples.tsx` / snapshot) **and** the generated-`.md` output hash. `check-docs` recomputes and compares — no git-history diffing. Input drift (incl. an examples edit that leaves the `.md` fences stale) ⇒ needs resync; **a hand-edited generated `.md` ⇒ output-hash mismatch ⇒ CI fail**. | | **Migration** | AI **reverse-generates** outlines + `*.docs.examples.tsx` + manifest from the existing docs; humans curate/restructure; enforcement phases in per-unit. | | **`app-shell-patterns`** | A **consumer-facing build skill** emitted by `docs-kit sync` from the generated docs + authored fundamentals (`docs-src/skill/`), manifest-tracked (hashed in `docs-manifest.json` under `skill`) and validated by `check-docs`. **Implemented** — it subsumes and replaces the retired `catalogue` generator. | @@ -209,7 +209,7 @@ Prose is copied verbatim; example segments are keyed English tokens, e.g. **Yes — keep all tooling out of the published `core` bundle.** ``` -docs-src/{components,hooks,concepts,patterns,references}/**/[unit].docs.outline.md ← authored source: ALL doc sources live here, NOT colocated with components (keeps packages/core/src pure component code and lets the config roots be just ["docs-src"]) +docs-src/{components,hooks,concepts,patterns,pages,guides,references}/**/[unit].docs.outline.md ← authored source: ALL doc sources live here, NOT colocated with components (keeps packages/core/src pure component code and lets the config roots be just ["docs-src"]). `guides/` outputs to the docs/ root. docs-src/**/[unit].docs.examples.tsx ← authored runnable examples (a sibling of the outline) packages/docs-kit/ ← NEW private (unpublished) workspace pkg: type-surface extractor (reuses ts-morph, already a dep in vite-plugin) diff --git a/docs-browser/src/App.tsx b/docs-browser/src/App.tsx index 7b922363c..d909b078a 100644 --- a/docs-browser/src/App.tsx +++ b/docs-browser/src/App.tsx @@ -18,6 +18,7 @@ import HomePage from "./pages/page"; type Res = ReturnType; const CATEGORY_LABEL: Record = { + guides: "Guides", concepts: "Concepts", components: "Components", patterns: "Patterns", @@ -25,7 +26,7 @@ const CATEGORY_LABEL: Record = { api: "API", }; // Sidebar order; any category not listed falls to the end alphabetically. -const CATEGORY_ORDER = ["concepts", "components", "patterns", "pages", "api"]; +const CATEGORY_ORDER = ["guides", "concepts", "components", "patterns", "pages", "api"]; function titleOf(u: DocUnit): string { return u.title ?? u.slug; diff --git a/docs-browser/src/_lib/DocPage.tsx b/docs-browser/src/_lib/DocPage.tsx index af7ae64f4..128c0c195 100644 --- a/docs-browser/src/_lib/DocPage.tsx +++ b/docs-browser/src/_lib/DocPage.tsx @@ -7,16 +7,23 @@ import { Layout, Link, Table, Tabs } from "@tailor-platform/app-shell"; import { units } from "../docs"; -/** Rewrite a relative `*.md` doc link to its docs-browser route, resolved - * against the current unit's route (`//`). Returns null for - * external links, in-page anchors, and non-`.md` targets — left untouched — so - * the raw `.md` files stay browsable on GitHub while the app routes correctly. */ -function mdHrefToRoute(href: string, category: string, slug: string): string | null { +/** Rewrite a relative `*.md` doc link to its docs-browser route. Links are + * authored against the generated tree (`docs/…`), which is not the route shape + * — `docs/api/guards/hidden.md` is served at `/api/guards/hidden`, and the root + * guides are served under `/guides/`. So resolve in doc-space against the + * current unit's output path, then look the target up. Returns null for + * external links, in-page anchors, non-`.md` targets, and docs that aren't + * units — left untouched, so the raw `.md` stays browsable on GitHub. */ +const routeByOutput = new Map(units.map((u) => [u.output, u.route])); + +function mdHrefToRoute(href: string, output: string): string | null { if (/^(https?:|mailto:|#)/.test(href)) return null; const [path, hash] = href.split("#"); if (!/\.md$/.test(path)) return null; - const resolved = new URL(path, `http://x/${category}/${slug}`).pathname.replace(/\.md$/, ""); - return hash ? `${resolved}#${hash}` : resolved; + const target = new URL(path, `http://x/${output}`).pathname.replace(/^\//, ""); + const route = routeByOutput.get(target); + if (!route) return null; + return hash ? `${route}#${hash}` : route; } function pascalCase(key: string): string { @@ -223,7 +230,7 @@ export function DocPage({ slug }: { slug: string }) { // Cross-doc links target the source `.md` files; rewrite to browser routes // at render time (external links + in-page anchors pass through unchanged). a: ({ node, href, ...props }: ComponentProps<"a"> & { node?: unknown }) => { - const to = href ? mdHrefToRoute(href, unit.category, unit.slug) : null; + const to = href ? mdHrefToRoute(href, unit.output) : null; return to ? : ; }, } as Components; diff --git a/docs-browser/src/docs.tsx b/docs-browser/src/docs.tsx index d529d6303..bee99a166 100644 --- a/docs-browser/src/docs.tsx +++ b/docs-browser/src/docs.tsx @@ -39,6 +39,11 @@ export interface DocUnit { slug: string; kind: string; category: string; + /** Repo-relative generated markdown, e.g. `docs/api/guards/hidden.md`. Relative + * links inside a doc are written against this, not against the route. */ + output: string; + /** Browser route, e.g. `/api/guards/hidden`. */ + route: string; title: string | null; markdown: string; source: string | null; @@ -47,6 +52,14 @@ export interface DocUnit { const key = (repoRel: string): string => `../../${repoRel}`; +/** Nav category from a unit's output path. Outputs nested under the docs root + * take their directory (`docs/api/...` → `api`); the root guides sit directly + * in `docs/` and are grouped under `guides`. */ +function categoryOf(output: string): string { + const segments = output.split("/"); + return segments.length > 2 ? segments[1] : "guides"; +} + /** Pull the `title:` out of the generated frontmatter — shown in the page * header (the body's own H1 is stripped by cleanMarkdown to avoid duplication). */ function frontmatterTitle(md: string): string | null { @@ -88,7 +101,9 @@ export const units: DocUnit[] = Object.values(manifest.units).map((entry) => { return { slug: entry.slug, kind: entry.kind, - category: entry.output.split("/")[1] ?? "misc", + output: entry.output, + route: `/${categoryOf(entry.output)}/${entry.slug}`, + category: categoryOf(entry.output), title: frontmatterTitle(raw), markdown: cleanMarkdown(raw), source: entry.examples ? (exampleSource[key(entry.examples)] ?? null) : null, diff --git a/docs-kit.config.json b/docs-kit.config.json index 0dd9660a2..a072f11d0 100644 --- a/docs-kit.config.json +++ b/docs-kit.config.json @@ -1,22 +1,48 @@ { "$comment": "Config for @tailor-platform/app-shell-docs-kit. See decisions/documentation-management-overhaul.md.", "inputs": { - "outlines": { "roots": ["docs-src"] }, + "outlines": { + "roots": ["docs-src"] + }, "api": { "tsconfig": "packages/core/tsconfig.json", "entrypoint": "packages/core/src/index.ts" }, - "snapshots": { "dir": "packages/core/__snapshots__" } + "snapshots": { + "dir": "packages/core/__snapshots__" + } }, "outputs": { "documents": { "mappings": [ - { "match": "docs-src/components/**", "dir": "docs/components" }, - { "match": "docs-src/hooks/**", "dir": "docs/api" }, - { "match": "docs-src/concepts/**", "dir": "docs/concepts" }, - { "match": "docs-src/patterns/**", "dir": "docs/patterns" }, - { "match": "docs-src/pages/**", "dir": "docs/pages" }, - { "match": "docs-src/references/**", "dir": "docs/references" } + { + "match": "docs-src/components/**", + "dir": "docs/components" + }, + { + "match": "docs-src/hooks/**", + "dir": "docs/api" + }, + { + "match": "docs-src/concepts/**", + "dir": "docs/concepts" + }, + { + "match": "docs-src/patterns/**", + "dir": "docs/patterns" + }, + { + "match": "docs-src/pages/**", + "dir": "docs/pages" + }, + { + "match": "docs-src/references/**", + "dir": "docs/references" + }, + { + "match": "docs-src/guides/**", + "dir": "docs" + } ] }, "manifest": "docs-manifest.json", @@ -28,6 +54,9 @@ } }, "policy": { - "coverage": { "enforce": false, "exclusions": [] } + "coverage": { + "enforce": false, + "exclusions": [] + } } } diff --git a/docs-manifest.json b/docs-manifest.json index f62141fe7..e15a93d5e 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -576,6 +576,23 @@ "examples": "8e45ff2d2e3cd59b" } }, + "design-philosophy": { + "slug": "design-philosophy", + "kind": "prose", + "outline": "docs-src/guides/design-philosophy.docs.outline.md", + "output": "docs/design-philosophy.md", + "examples": null, + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "bdf8d430a76790a2", + "snapshot": null, + "outputMd": "b78335ee38bfd71f", + "examples": null + } + }, "dialog": { "slug": "dialog", "kind": "code-backed", @@ -933,6 +950,23 @@ "examples": "7089e7331b95df31" } }, + "introduction": { + "slug": "introduction", + "kind": "prose", + "outline": "docs-src/guides/introduction.docs.outline.md", + "output": "docs/introduction.md", + "examples": null, + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "019ef7a912b92831", + "snapshot": null, + "outputMd": "6c212f2bd28ba8c6", + "examples": null + } + }, "layout": { "slug": "layout", "kind": "code-backed", @@ -1001,6 +1035,23 @@ "examples": "22b876f43b26922c" } }, + "migrations": { + "slug": "migrations", + "kind": "prose", + "outline": "docs-src/guides/migrations.docs.outline.md", + "output": "docs/migrations.md", + "examples": null, + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "7c75151af7cbb267", + "snapshot": null, + "outputMd": "d773d8726a8fba5e", + "examples": null + } + }, "modules-and-resources": { "slug": "modules-and-resources", "kind": "prose", @@ -1018,6 +1069,23 @@ "examples": null } }, + "quickstart": { + "slug": "quickstart", + "kind": "prose", + "outline": "docs-src/guides/quickstart.docs.outline.md", + "output": "docs/quickstart.md", + "examples": null, + "sources": [], + "claims": [], + "symbols": [], + "hashes": { + "typeSurface": null, + "outline": "d5b840df87bc79e9", + "snapshot": null, + "outputMd": "6eb4056555d8ae7b", + "examples": null + } + }, "router/use-location": { "slug": "router/use-location", "kind": "prose", @@ -1649,7 +1717,7 @@ "packages/core/skills/app-shell-patterns/references/patterns/interaction-toast.md": "f68bdab855730f48", "packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md": "8062ea0df9403fbb", "packages/core/skills/app-shell-patterns/references/pages/document-detail.md": "c3fb2588a0699c26", - "packages/core/skills/app-shell-patterns/references/migrations.md": "799fd5010635b2c2", + "packages/core/skills/app-shell-patterns/references/migrations.md": "7119fb4170cc931c", "packages/core/skills/app-shell-patterns/SKILL.md": "165761adda4fbfc0" } } diff --git a/docs-src/guides/design-philosophy.docs.outline.md b/docs-src/guides/design-philosophy.docs.outline.md new file mode 100644 index 000000000..ecabbbe57 --- /dev/null +++ b/docs-src/guides/design-philosophy.docs.outline.md @@ -0,0 +1,83 @@ +--- +kind: prose +group: design-philosophy +title: Design Philosophy +description: The design principles and tradeoffs behind AppShell's architecture. +--- + +# Design Philosophy + +AppShell makes deliberate tradeoffs — coupling over flexibility, convention over configuration, integration over composition. This page explains the reasoning behind those choices. + +## Why a Framework, Not a Library Collection + +ERP applications need authentication, routing, sidebar navigation, breadcrumbs, command palette, and permission guards. Each of these can be solved by an individual library. The hard part is not the individual pieces — it's keeping them in sync. + +When you add a new page, the sidebar needs an entry. The breadcrumb trail needs updating. The command palette needs a new search result. The route guard needs to be evaluated. When you hide a page with a guard, it should disappear from _all_ of those surfaces — not just the router. + +With a library collection, you write and maintain this synchronization code yourself. With AppShell, a single `appShellPageProps` declaration on a page component drives all of these surfaces automatically: + +```tsx +OrdersPage.appShellPageProps = { + meta: { + title: "Orders", + icon: , + }, + guards: [authGuard], +}; +``` + +The cost of this integration is not in the initial setup — it's in the ongoing maintenance. Every time you add, remove, or modify a page, a library-based approach requires manual updates across multiple surfaces. The bugs this produces — "guard is set to hidden but the command palette still shows it" — are silent and hard to test for. A framework eliminates this class of bugs entirely. + +## Default Coupling, Opt-Out Exceptions + +AppShell couples everything by default. A page appears in the sidebar, breadcrumbs, and command palette unless you explicitly opt out. + +This is an intentional asymmetry. Opting out is expressed through the same mechanisms the framework already provides — for example, using a guard that returns `hidden` removes a page from the sidebar, breadcrumbs, and command palette all at once. + +When you need finer control, AppShell provides a compositional layer. The sidebar, for instance, supports two modes: auto-generation (the default, driven entirely by module/resource definitions) and composition mode, where you build the sidebar structure explicitly using `SidebarItem`, `SidebarGroup`, and `SidebarSeparator` components. You can start with full auto-generation and gradually move to composition mode as your needs grow — without an all-or-nothing switch. + +Opting _in_ to coupling from a decoupled starting point — manually wiring sidebar entries, breadcrumb paths, and search results for every page — costs orders of magnitude more. The value of default coupling is not that exceptions never exist, but that expressing exceptions is trivial. + +## Built for ERP, Not for Everything + +AppShell targets applications where the majority of screens are CRUD — lists, detail views, create and edit forms. This is the reality of most ERP systems: even as they mature and add dashboards, approval workflows, and custom visualizations, the number of CRUD screens continues to grow (new master data entities, new transaction types). + +For non-CRUD screens, AppShell stays out of the way. The framework manages the shell — routing, guards, layout, navigation — while page content is entirely yours. The ratio of "framework-managed" to "custom-built" shifts as a project matures, but the shell remains useful regardless of what's inside it. + +If your application is primarily custom visualizations with little CRUD, or if you need a fully custom design system, AppShell's opinions will work against you. Use individual libraries instead. + +## Opinionated UI, Not Headless + +Frameworks like [Refine](https://refine.dev/) and [React Admin](https://marmelab.com/react-admin/) keep UI headless or pluggable to support any backend and any design. AppShell takes the opposite approach: it ships opinionated UI components — tables, forms, description cards, metric cards — that are designed for Tailor Platform ERP projects. + +This tradeoff is viable because the scope is bounded. Tailor Platform projects agree upfront on what falls within standard UI and what requires custom implementation. AppShell optimizes for speed within that agreed boundary, rather than offering unbounded flexibility. + +| | Refine / React Admin | AppShell | +| ------------------- | ------------------------------- | ------------------------------------------------- | +| **Backend** | Any (data provider abstraction) | Tailor Platform | +| **UI** | Headless (bring your own) | Opinionated (included) | +| **Navigation sync** | Routing only | Routing + sidebar + breadcrumbs + command palette | +| **Auth** | Adapter-based (any IdP) | Tailor Platform IdP direct | + +## Vertical Integration as a Feature + +AppShell is developed by the same team that builds Tailor Platform. This means: + +- **Authentication evolves together.** When Tailor Platform changes its auth flow (e.g., adopting DPoP), AppShell ships the update. No waiting for a third-party adapter. +- **Breaking changes are coordinated.** Platform API changes and framework updates ship in lockstep, not as separate upgrade cycles. +- **Lock-in risk is scoped.** AppShell is already specific to Tailor Platform. The incremental lock-in from using it is minimal — you're not giving up backend portability you already don't have. + +## Why a Framework, Not AI Skills + +In an era of AI-assisted development, one might ask: why encode integration patterns in a framework when you could describe them in AI skill files or prompts? + +The answer comes down to what guarantees correctness. + +**Skills describe; frameworks enforce.** An AI skill can explain how to wire a sidebar entry to a route guard, but it cannot guarantee the wiring is correct. A framework, backed by TypeScript's type system, turns incorrect wiring into a compile-time error. The framework makes the _skill's job easier_ — the skill only needs to teach "how to use AppShell," not "how to implement the integration that AppShell already handles." + +**Integration crosses context boundaries.** Correctly implementing guard → routing → sidebar → command palette synchronization requires understanding all four systems simultaneously. AI skills work best when scoped to a single concern. Splitting integration logic across multiple skills risks losing cross-cutting consistency. An integration skill comprehensive enough to cover all the interactions essentially becomes a natural-language restatement of the framework — harder to maintain and impossible to type-check. + +**Conventions reduce AI decision points.** File-based routing + `appShellPageProps` means "where does this page go?" and "what do I declare?" always have exactly one answer. This determinism is what lets AI agents work reliably across multiple projects without per-project context tuning. + +AppShell's position: encode integration correctness in code and types. Use AI skills to teach _how to use_ the framework, not to _replace_ it. diff --git a/docs-src/guides/introduction.docs.outline.md b/docs-src/guides/introduction.docs.outline.md new file mode 100644 index 000000000..e15546301 --- /dev/null +++ b/docs-src/guides/introduction.docs.outline.md @@ -0,0 +1,47 @@ +--- +kind: prose +group: introduction +title: Introduction +description: AppShell is an opinionated React application framework for creating applications on Tailor Platform with built-in authentication, routing, and beautiful UI components. +--- + +# Introduction + +AppShell is a React-based framework that provides the foundation for building custom ERP applications on Tailor Platform. It handles the complex infrastructure so you can focus on building business-level screens and features. + +## Why AppShell? + +Building ERP applications involves a lot of repetitive infrastructure: authentication, routing, sidebar navigation, breadcrumbs, command palette, permission guards — and keeping them all in sync. AppShell exists to solve this integration problem. + +**One declaration drives everything.** When you define a page with `appShellPageProps`, AppShell automatically generates the sidebar item, breadcrumb entry, command palette search result, and route guard — all from a single source of truth. Change a guard to `hidden`, and the page disappears from everywhere at once. No manual sync, no silent bugs. + +**Purpose-built for ERP.** Most ERP screens are variations of the same CRUD patterns — lists, detail views, create forms, edit forms. AppShell absorbs this repetitive structure so you can focus your effort on the screens that actually require custom logic: dashboards, approval workflows, and domain-specific visualizations. + +**Type-safe by design.** AppShell propagates types across your entire application. Module definitions, route paths, and context data are all type-checked at compile time, catching integration errors before they reach production. + +**Vertically integrated with Tailor Platform.** Unlike generic admin frameworks that abstract away the backend, AppShell embraces its tight coupling with Tailor Platform. This lets it provide a complete, opinionated stack — from OAuth2/DPoP authentication to data fetching — without requiring you to write glue code. + +## Our Backstory + +AppShell was born from real-world ERP delivery projects at Tailor Technologies. + +As we built multiple ERP systems on Tailor Platform for different clients, we kept writing the same integration code: wiring up authentication, connecting sidebar navigation to route guards, syncing breadcrumbs with page definitions. Each project had its own slightly different version of this glue, and every variation introduced new opportunities for bugs. + +We extracted these common patterns into a shared framework. Instead of documenting "how to wire things together" and hoping every project follows the guide, we encoded the integration directly into code and types — making incorrect wiring a compile-time error rather than a runtime surprise. + +Today, AppShell also reflects our bet on AI-assisted development. We operate with small teams augmented by AI agents, running multiple client projects in parallel. The framework's file-based conventions and declarative APIs minimize the decisions an AI agent needs to make — "where does this page go?" and "what do I need to declare?" always have exactly one answer. Knowledge gained on one project — whether by a developer or an AI skill file — transfers directly to the next. + +For a deeper look at the tradeoffs and principles behind these choices, see [Design Philosophy](./design-philosophy.md). + +## Next Steps + +- **[Quick Start](./quickstart.md)** — Install AppShell and build your first page in minutes +- **[Modules & Resources](./concepts/modules-and-resources.md)** — Understand how pages are declared and organized +- **[File-Based Routing](./concepts/file-based-routing.md)** — Learn how directory structure drives navigation +- **[Authentication](./concepts/authentication.md)** — Configure OAuth2 and identity providers +- **[Styling & Theming](./concepts/styling-theming.md)** — Customize the look and feel + +## Hosted References + +- **[UI Catalogue](https://ui.tailor.tech)** — Every component rendered live, plus full-page samples, composite UI patterns, and routing recipes with copyable source +- **[Theme Generator](https://theme.tailor.tech/playground)** — Pick a primary color and export a matching AppShell palette CSS file diff --git a/docs-src/guides/migrations.docs.outline.md b/docs-src/guides/migrations.docs.outline.md new file mode 100644 index 000000000..1026f1616 --- /dev/null +++ b/docs-src/guides/migrations.docs.outline.md @@ -0,0 +1,174 @@ +--- +kind: prose +group: migrations +title: Migrations +description: Breaking changes and required migration steps for AppShell upgrades, newest first +--- + +# Migrations + +Every change that requires you to edit your application before or after upgrading, newest first. + +This page is deliberately narrow. It is **not** a changelog — see [`packages/core/CHANGELOG.md`](../packages/core/CHANGELOG.md) for the full release history including features and fixes. A change belongs here only if an app that does nothing will break, misbehave, or silently drift. + +Each entry states which versions are affected, what breaks, how to detect it, and what to change. Entries stay here permanently; they are not pruned when they get old, because apps upgrade across arbitrary version gaps. + +| Version | Change | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| 1.12.0 | [`DateField` / `DatePicker` field chrome moved to `Field.Root`](#1120-datefield--datepicker-field-chrome-moved-to-fieldroot) | +| 1.11.0 | [React 19.2.7 and React Router v8 required](#1110-react-1927-and-react-router-v8-are-now-required) | +| 1.11.0 | [Non-modal `Sheet` renders no backdrop](#1110-non-modal-sheet-no-longer-renders-a-backdrop) | +| 1.8.0 | [`stream` removed from `useAIChat()`](#180-stream-removed-from-useaichat) | +| 1.5.0 → 1.7.0 | [Remove the theme bridge workaround](#150--170-remove-the-theme-bridge-workaround) | +| 1.5.0 | [`loader` removed from file-based pages](#150-loader-removed-from-file-based-page-definitions) | +| 1.3.0 | [Column inference and badge defaults changed](#130-column-inference-and-badge-defaults-changed) | +| 1.0.2 | [`Toaster` no longer accepts `richColors`](#102-toaster-no-longer-accepts-richcolors) | +| before 1.0 | [Pre-1.0 breaking changes](#before-10) | + +## 1.12.0: `DateField` / `DatePicker` field chrome moved to `Field.Root` + +**Applies to:** apps passing `label`, `description`, `errorMessage`, or `hideTimeZone` to `DateField` or `DatePicker`. + +The date controls now follow the same composition model as `Field`, `Select`, `Combobox`, and `Autocomplete`. Field chrome (`label`, `description`, `errorMessage`) has moved out of the control and into `Field.Root`. `hideTimeZone` is removed — it was accepted by the prop types but had no effect. + +TypeScript will error on the removed props; delete them and compose with `Field.Root` instead. + +Before: + +```tsx + +``` + +After: + +```tsx + + Delivery date + + When should we ship your order? + {error} + +``` + +`isInvalid` remains a top-level prop for externally-controlled invalid styling. Semantic date props (`isRequired`, `isDisabled`, `isReadOnly`, `minValue`, `maxValue`, `isDateUnavailable`) remain top-level and unchanged. Standalone usage still works with an accessible name: + +```tsx + +``` + +## 1.11.0: React 19.2.7 and React Router v8 are now required + +**Applies to:** every app upgrading to 1.11.0. + +The minimum supported `react` and `react-dom` is raised to `19.2.7`, and AppShell moves to React Router v8. React 18 is no longer supported. + +Upgrade `react` and `react-dom` to `>=19.2.7` in the same change. If your app imports from `react-router` directly, review the React Router v8 release notes for its own breaking changes — AppShell re-exports a subset (`useNavigate`, `useParams`, `useLocation`, and friends), and those are unaffected. + +Note that 1.10.1 deliberately stayed on React Router v7 to pick up its security fixes while avoiding v8. Going 1.10.1 → 1.11.0 therefore crosses a router major. + +## 1.11.0: non-modal `Sheet` no longer renders a backdrop + +**Applies to:** apps using ``. + +A non-modal sheet previously still rendered a backdrop, dimming and blocking the page behind it. It now omits the backdrop, which is what `modal={false}` implies. Nothing errors — the page behind simply stays undimmed and interactive. + +If you relied on the dimming, drop `modal={false}` and use a modal sheet. + +## 1.8.0: `stream` removed from `useAIChat()` + +**Applies to:** apps passing `stream` to `useAIChat()`, or constructing an `AIGatewayChatRequest` by hand. + +AppShell now selects streaming or JSON transport automatically from the model, so the option is gone. TypeScript errors on the removed property; delete it. There is no replacement. + +## 1.5.0 → 1.7.0: remove the theme bridge workaround + +**Applies to:** apps that pasted the `@theme inline` block, `@custom-variant dark`, and AppShell's palette into their entry CSS — the workaround for `styles` shipping without the Tailwind bridge. + +**`styles` regained the bridge in 1.7.0.** On 1.5.0–1.6.1 the workaround is load-bearing, so upgrade to 1.7.0 or later _before_ deleting any of it. Remove it earlier and every AppShell-token utility — `bg-card`, `bg-background`, `text-muted-foreground`, `border-border` — stops resolving, while `dark:` variants fall back to Tailwind's `prefers-color-scheme` default and stop tracking the `.dark` class. + +From 1.7.0 the workaround is not merely redundant. It actively breaks dark mode, and the build succeeds with no warning: + +- Your pasted `:root` and `.dark` blocks are unlayered, so they beat AppShell's layered default palette. Colours freeze at the values you copied, and any surface AppShell has added since has no dark value at all — so it renders light colours in dark mode: white text on white cards, unreadable disabled inputs. +- `@custom-variant dark (&:is(.dark *))` overrides AppShell's `&:where(.dark, .dark *)`. The `:is(.dark *)` form matches only _descendants_ of `.dark`, so `dark:` utilities stop applying to the `.dark` element itself. + +### Removing it + +Delete from your entry CSS: + +- the `@theme inline { … }` block, +- the `@custom-variant dark (…)` rule, +- every `:root` and `.dark` block copied from AppShell's palette — **all** of it, including the `*-foreground` pairs, `--status-*`, `--alert-*`, `--sidebar-*` and `--semantic-shadow-*`. The foregrounds are what leave text white on white, so a partial deletion reproduces the bug. +- any `@import "@tailor-platform/app-shell/theme.css"` (a no-op shim since 1.6.0, kept only so older apps keep building). + +To find it, search every CSS file the app loads — not just the entry point, since `app/` and `styles/` are as common as `src/`: + +```bash +grep -rnE "@theme inline|@custom-variant|app-shell/theme\.css|--(card|popover|muted|sidebar|destructive|accent)(-foreground)?:" --include="*.css" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.next . +``` + +Excluding `node_modules` matters: AppShell's own palette files declare these tokens too, and they must not be touched. In your own CSS, hits are either the workaround, which goes, or deliberate overrides, which should take the `:root` / `:root.dark` form described in [Overriding tokens](./concepts/styling-theming.md#overriding-tokens). + +What remains is short — [`examples/vite-app/src/index.css`](https://github.com/tailor-platform/app-shell/blob/main/examples/vite-app/src/index.css) is a working reference for the shape (it also imports a branded palette, which is optional): + +```css +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; + +html, +body { + margin: 0; + padding: 0; +} +``` + +### Verifying + +Toggle dark mode and confirm a real surface changes: inspect a `Card` and watch its computed `background-color` go from `rgb(255, 255, 255)` to `rgb(23, 23, 23)` on the default palette. + +Reading the token directly also works — `getComputedStyle(document.documentElement).getPropertyValue("--card")` returns the winning declaration, so a stale copy shows up as its own value. Just compare against the authored notation: AppShell writes `rgba(23, 23, 23, 1)`, not `#171717`, and the computed value preserves that form. + +## 1.5.0: `loader` removed from file-based page definitions + +**Applies to:** file-based routing apps that set `loader` in `Page.appShellPageProps`. + +`loader` was an incomplete API exposed by accident, and `guards` is now the single source of page-level route behaviour. Move any access checks into a guard, and any data loading into the page component or your data layer. + +## 1.3.0: column inference and badge defaults changed + +**Applies to:** apps using `inferColumns()`, or `DataTable`'s status badges. + +Two changes that alter rendering without any error: + +- `inferColumns()` no longer sets a default `render`. A column with neither an explicit `type` nor `render` now displays `—` for null and empty values, matching typed-column behaviour. +- Badge variant resolution moved into a shared helper whose default is `outline-neutral`. `DataTable` previously defaulted to `neutral`, so unstyled status badges change appearance. + +Set `type` or `render` explicitly on any column whose previous rendering you want back, and pass an explicit variant where the old badge styling mattered. `BadgeVariantType` is deprecated in favour of `BadgeVariant`. + +## 1.0.2: `Toaster` no longer accepts `richColors` + +**Applies to:** apps passing `richColors` to ``. + +The prop is removed, and toasts no longer colour-code the success, error, warning, and info variants. TypeScript errors on the prop; delete it. + +## Before 1.0 + +Pre-1.0 releases changed the public API often, mostly around authentication and routing. If you are upgrading from a 0.x version, work through these in order — several supersede each other, so applying them out of sequence will not land you in the right place. + +Each is summarised here; [`packages/core/CHANGELOG.md`](../packages/core/CHANGELOG.md) carries the full before/after code for every one. + +| Version | Change | +| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0.33.0 | `AsyncFetcherFn` receives `string \| null` instead of `string`. It is called with `null` when the user has typed nothing — return initial items, or an empty array to show nothing until they type. | +| 0.28.0 | `EnhancedAuthClient.getAuthHeadersForQuery()` removed. Pass `fetch: authClient.fetch` to your GraphQL client instead; it handles DPoP proofs and token refresh transparently. | +| 0.27.0 | Module-level `guards` and `loaders` no longer cascade to child resources — declare them on each resource or page. A module without a `component` no longer auto-redirects to its first visible resource and must declare `guards`, or it throws at runtime. | +| 0.26.0 | Authentication moved to `@tailor-platform/auth-public-client` with DPoP. `AuthProvider` requires a `client` from `createAuthClient`, `apiEndpoint` is gone, `useAuth` returns its fields directly rather than under `authState`, and built-in user fetching (`meQuery`, `AuthState.user`, `DefaultUser`, `AuthRegister`) is removed — fetch the user with your own GraphQL client. | +| 0.24.0 | `accessControl` replaced by the `guards` array on `defineModule`/`defineResource`. `RedirectConfig` and `redirectToResource` removed — use `guards` with `redirectTo()`. | +| 0.19.0 | `BuiltinIdPAuthProvider` → `AuthProvider`, `useBuiltinIdpAuth` → `useAuth`. The `buildAuthorizationUrl`, `exchangeCodeForToken`, `prepareLogin`, and `handleOAuthCallback` utilities are no longer exported. | +| 0.13.0 | `defaultResourceRedirectPath` removed from `defineModule` in favour of a `redirectToResource` helper — which 0.24.0 then removed in turn. Coming from 0.13.0 or earlier, go straight to the 0.24.0 form: `guards` with `redirectTo()`. | +| 0.4.0 | `meta.title` no longer renders the page title automatically. The title is passed to the resource component via props (`ResourceComponentProps`); render it yourself. | diff --git a/docs-src/guides/quickstart.docs.outline.md b/docs-src/guides/quickstart.docs.outline.md new file mode 100644 index 000000000..7bab09b54 --- /dev/null +++ b/docs-src/guides/quickstart.docs.outline.md @@ -0,0 +1,181 @@ +--- +kind: prose +group: quickstart +title: Quick Start +description: Install and set up your first AppShell application +--- + +# Quick Start + +Get your first AppShell application running in minutes. + +## Prerequisites + +- Node.js 16+ +- React 19.2.7+ +- A React project (Vite, Next.js, or any bundler) + +## Step 1: Install AppShell + +```bash +# npm +npm install @tailor-platform/app-shell + +# yarn +yarn add @tailor-platform/app-shell + +# pnpm +pnpm add @tailor-platform/app-shell +``` + +## Step 2: Set Up File-Based Routing (Vite) + +Add the `appShellRoutes` plugin to your `vite.config.ts`: + +```typescript +// vite.config.ts +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import { appShellRoutes } from "@tailor-platform/app-shell/vite-plugin"; + +export default defineConfig({ + plugins: [react(), tailwindcss(), appShellRoutes({ entrypoint: "src/App.tsx" })], +}); +``` + +## Step 3: Create Your First App + +Add AppShell styles to your global CSS file: + +```css +/* index.css */ +@import "tailwindcss"; +@import "@tailor-platform/app-shell/styles"; +``` + +Create `src/App.tsx`: + +```tsx +// src/App.tsx +import { AppShell, SidebarLayout } from "@tailor-platform/app-shell"; + +function App() { + return ( + + + + ); +} + +export default App; +``` + +Create your first page at `src/pages/page.tsx`: + +```tsx +// src/pages/page.tsx +const HomePage = () => { + return ( +
+

Welcome to AppShell

+
+ ); +}; + +export default HomePage; +``` + +## Step 4: Run + +```bash +npm run dev +``` + +Navigate to `/` - you should see your page with automatic sidebar navigation. + +## Add Nested Pages + +Create pages by adding directories and `page.tsx` files: + +``` +src/pages/ +├── page.tsx → / +├── dashboard/ +│ ├── page.tsx → /dashboard +│ └── orders/ +│ ├── page.tsx → /dashboard/orders +│ └── [id]/ +│ └── page.tsx → /dashboard/orders/:id +``` + +```tsx +// src/pages/dashboard/page.tsx +const DashboardPage = () => { + return ( +
+

Dashboard

+
+ ); +}; + +export default DashboardPage; +``` + +```tsx +// src/pages/dashboard/orders/[id]/page.tsx +import { useParams } from "@tailor-platform/app-shell"; + +const OrderDetailPage = () => { + const { id } = useParams(); + return ( +
+

Order #{id}

+
+ ); +}; + +export default OrderDetailPage; +``` + +AppShell automatically generates sidebar navigation and breadcrumbs. + +[Learn more about File-Based Routing →](./concepts/file-based-routing.md) + +## Framework-Specific Notes + +### Next.js (App Router) + +Next.js does not support file-based routing with the Vite plugin. Use the module-based approach instead: + +```tsx +// app/dashboard/[[...props]]/page.tsx +"use client"; + +import { AppShell, SidebarLayout, defineModule } from "@tailor-platform/app-shell"; + +const dashboardModule = defineModule({ + path: "home", + component: () =>
Home
, + meta: { title: "Home" }, +}); + +export default function Page() { + return ( + + + + ); +} +``` + +See [Modules & Resources](./concepts/modules-and-resources.md) for the module-based API. + +## Next Steps + +- [File-Based Routing](./concepts/file-based-routing.md) - Define pages via directory structure +- [Routing & Navigation](./concepts/routing-navigation.md) - Navigation hooks +- [Authentication](./concepts/authentication.md) - Set up user authentication +- [Sidebar Navigation](./concepts/sidebar-navigation.md) - Customize sidebar menus +- [Styling & Theming](./concepts/styling-theming.md) - Theming and Tailwind CSS configuration +- [Modules & Resources](./concepts/modules-and-resources.md) - Legacy module-based routing diff --git a/docs/design-philosophy.md b/docs/design-philosophy.md index cefe0558f..2c0a6f441 100644 --- a/docs/design-philosophy.md +++ b/docs/design-philosophy.md @@ -3,6 +3,8 @@ title: Design Philosophy description: The design principles and tradeoffs behind AppShell's architecture. --- + + # Design Philosophy AppShell makes deliberate tradeoffs — coupling over flexibility, convention over configuration, integration over composition. This page explains the reasoning behind those choices. diff --git a/docs/introduction.md b/docs/introduction.md index da95df864..7b9b7b7ad 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -3,6 +3,8 @@ title: Introduction description: AppShell is an opinionated React application framework for creating applications on Tailor Platform with built-in authentication, routing, and beautiful UI components. --- + + # Introduction AppShell is a React-based framework that provides the foundation for building custom ERP applications on Tailor Platform. It handles the complex infrastructure so you can focus on building business-level screens and features. diff --git a/docs/migrations.md b/docs/migrations.md index 97432e822..092f3063b 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -3,6 +3,8 @@ title: Migrations description: Breaking changes and required migration steps for AppShell upgrades, newest first --- + + # Migrations Every change that requires you to edit your application before or after upgrading, newest first. diff --git a/docs/quickstart.md b/docs/quickstart.md index 1487ed6f9..e10401a51 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -3,6 +3,8 @@ title: Quick Start description: Install and set up your first AppShell application --- + + # Quick Start Get your first AppShell application running in minutes. diff --git a/packages/docs-kit/src/check.ts b/packages/docs-kit/src/check.ts index 9a36429b6..d43341b28 100644 --- a/packages/docs-kit/src/check.ts +++ b/packages/docs-kit/src/check.ts @@ -1,5 +1,5 @@ import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; +import { join, relative } from "node:path"; import { loadConfig } from "./config"; import { type Finding, reconcile } from "./coverage"; @@ -8,6 +8,7 @@ import { readManifest } from "./manifest"; import { discoverOutlines } from "./outline"; import { loadSurface, snapshotHashForSlug } from "./project"; import { computeSkill } from "./skill"; +import { unmanagedOutputs, walkMarkdown } from "./tree"; export interface CheckResult { findings: Finding[]; @@ -141,5 +142,27 @@ export function check(repoRoot: string): CheckResult { } } + // Nothing unmanaged may live in the generated tree. Every .md under an output + // root must be a unit's output; anything else was hand-authored where it will + // never be regenerated, and no other check would ever see it. + const outputRoots = new Set(config.categories.map((c) => c.outDir.split("/")[0])); + const found = [...outputRoots].flatMap((root) => + walkMarkdown(join(repoRoot, root)).map((abs) => toPosix(relative(repoRoot, abs))), + ); + for (const rel of unmanagedOutputs( + found, + outlines.map((o) => o.mdPath), + )) { + findings.push({ + level: "block", + slug: rel, + message: `${rel} has no source — everything under ${rel.split("/")[0]}/ is generated. Author an outline under \`docs-src/\` and run sync.`, + }); + } + return { findings, ok: !findings.some((f) => f.level === "block") }; } + +function toPosix(p: string): string { + return p.split("\\").join("/"); +} diff --git a/packages/docs-kit/src/tree.test.ts b/packages/docs-kit/src/tree.test.ts new file mode 100644 index 000000000..3c14ef763 --- /dev/null +++ b/packages/docs-kit/src/tree.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; + +import { unmanagedOutputs } from "./tree"; + +const managed = ["docs/components/badge.md", "docs/api/guards/hidden.md", "docs/quickstart.md"]; + +describe("unmanagedOutputs", () => { + it("accepts a tree where every file traces back to an outline", () => { + expect(unmanagedOutputs(managed, managed)).toEqual([]); + }); + + it("flags a doc hand-authored straight into the generated tree", () => { + // app-shell#559: an agent wrote docs/components/toolbar.md with no outline. + expect(unmanagedOutputs([...managed, "docs/components/toolbar.md"], managed)).toEqual([ + "docs/components/toolbar.md", + ]); + }); + + it("flags a file in a directory the pipeline never writes to", () => { + expect(unmanagedOutputs([...managed, "docs/notes/scratch.md"], managed)).toEqual([ + "docs/notes/scratch.md", + ]); + }); + + it("treats a root guide as managed — it is a unit like any other", () => { + expect(unmanagedOutputs(["docs/quickstart.md"], managed)).toEqual([]); + }); + + it("reports every offender, sorted", () => { + expect(unmanagedOutputs([...managed, "docs/z.md", "docs/a.md"], managed)).toEqual([ + "docs/a.md", + "docs/z.md", + ]); + }); +}); diff --git a/packages/docs-kit/src/tree.ts b/packages/docs-kit/src/tree.ts new file mode 100644 index 000000000..01f5b90cd --- /dev/null +++ b/packages/docs-kit/src/tree.ts @@ -0,0 +1,22 @@ +import { existsSync, readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; + +/** Every `.md` under `dir`, recursively. A missing dir yields nothing. */ +export function walkMarkdown(dir: string, out: string[] = []): string[] { + if (!existsSync(dir)) return out; + for (const name of readdirSync(dir)) { + const full = join(dir, name); + if (statSync(full).isDirectory()) walkMarkdown(full, out); + else if (name.endsWith(".md")) out.push(full); + } + return out; +} + +/** Generated-tree files that no outline produces. Every `.md` under an output + * root must trace back to a unit; whatever is left was hand-authored in the + * wrong place and would never be regenerated — the failure mode that let a + * component doc get written straight into `docs/`. */ +export function unmanagedOutputs(found: string[], managed: Iterable): string[] { + const known = new Set(managed); + return found.filter((f) => !known.has(f)).toSorted(); +}