From 10d844e6cc13f366a4d154aa436fee9f3f6d6f84 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Thu, 9 Jul 2026 15:42:42 +0900 Subject: [PATCH 1/7] fix: keep generated skills in sync with catalogue --- catalogue/scripts/SKILL.template.md | 14 +- catalogue/scripts/generate-skill.mjs | 79 +++++++---- catalogue/src/fundamental/components.md | 126 +++++++++++++++--- catalogue/src/fundamental/design-system.md | 24 ++++ catalogue/src/fundamental/graphql.md | 2 +- .../detail/hero-with-actions/PATTERN.md | 50 ++++++- .../hero-with-actions/hero-with-actions.tsx | 8 +- .../src/pattern/list/dense-scan/PATTERN.md | 3 +- .../src/pattern/list/dense-scan/columns.tsx | 11 +- 9 files changed, 254 insertions(+), 63 deletions(-) diff --git a/catalogue/scripts/SKILL.template.md b/catalogue/scripts/SKILL.template.md index 85791193..a11d0dd7 100644 --- a/catalogue/scripts/SKILL.template.md +++ b/catalogue/scripts/SKILL.template.md @@ -1,6 +1,6 @@ --- name: app-shell-patterns -description: UI pattern catalog for building pages with @tailor-platform/app-shell components +description: "Best-practice UI patterns and correct component usage for building pages in apps that use @tailor-platform/app-shell. Use when: building or editing any screen, page, list, table, detail view, form, modal, dialog, wizard, or bulk/confirm/toast interaction in an app with @tailor-platform/app-shell installed — or when choosing the right AppShell component, layout, or design token for a UI." --- # App-Shell Patterns @@ -34,3 +34,15 @@ These are the foundational rules that underpin all patterns. All patterns build - NEVER mix patterns in a single page component - ALWAYS use AppShell components — do NOT use raw HTML or third-party UI libraries - If no entry matches, compose directly from fundamental references + +### Cross-cutting UX rules (apply to every screen) + +Full rationale in [`design-system.md`](references/fundamental/design-system.md) → Composition & emphasis rules. + +- **One primary action per view:** at most one primary/filled `Button`; everything else is `outline`/`secondary`/`ghost`. +- **Status badges by semantic color:** a **filled** semantic variant for the record's primary/lifecycle status, **`outline-*`** for secondary statuses, **`subtle-*`** for tags; reserve brand `default` for non-status emphasis. +- **No duplicate actions:** an action lives in exactly one place — never repeat the same action in both `Layout.Header` and `ActionPanel`. +- **Action placement:** primary CTA + status in `Layout.Header`; workflow actions in `ActionPanel`; back/navigation in the breadcrumb — never in `ActionPanel`. +- **Metric tiles always go in a `Grid`** (`columns={{ initial: 1, md: 2, xl: 4 }}`) — never one-per-row. +- **Forms default to `form/modal`** — only build a routed full-page form when the design explicitly calls for one. +- **Handle every state:** loading (skeleton), empty (labelled empty state), and error (inline + retry) — never ship only the happy path. diff --git a/catalogue/scripts/generate-skill.mjs b/catalogue/scripts/generate-skill.mjs index e19d404d..876860e9 100644 --- a/catalogue/scripts/generate-skill.mjs +++ b/catalogue/scripts/generate-skill.mjs @@ -87,7 +87,8 @@ async function findEntryFiles(dir, filename) { return results; } - for (const entry of entries) { + const sortedEntries = [...entries].sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of sortedEntries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { results.push(...(await findEntryFiles(fullPath, filename))); @@ -192,31 +193,49 @@ async function main() { /** * Generate an index table for entry-based categories (with frontmatter). */ +function formatMarkdownTable(rows) { + const widths = rows[0].map((_, columnIndex) => + Math.max(...rows.map((row) => String(row[columnIndex]).length)), + ); + + const renderRow = (row) => + `| ${row.map((cell, columnIndex) => String(cell).padEnd(widths[columnIndex])).join(" | ")} |`; + + return [ + renderRow(rows[0]), + `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`, + ...rows.slice(1).map(renderRow), + ].join("\n"); +} + function generateEntryTable(entries, outputDir) { if (entries.length === 0) return ""; - // Group entries by subcategory - const grouped = {}; - for (const { meta } of entries) { + const grouped = new Map(); + for (const { meta } of [...entries].sort((a, b) => a.meta.slug.localeCompare(b.meta.slug))) { const key = meta.subcategory || meta.category; - if (!grouped[key]) grouped[key] = []; - grouped[key].push(meta); - } - - let table = ""; - for (const [group, items] of Object.entries(grouped)) { - table += `### ${group}\n\n`; - table += `| Slug | Name | Description |\n`; - table += `| ---- | ---- | ----------- |\n`; - for (const item of items) { - const filename = slugToFilename(item.slug) + ".md"; - const displaySlug = item.slug.replace(/^[^/]+\//, ""); - table += `| [\`${displaySlug}\`](references/${outputDir}/${filename}) | ${item.name} | ${item.description} |\n`; - } - table += `\n`; + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key).push(meta); } - return table.trim(); + return [...grouped.entries()] + .map(([group, items]) => { + const rows = [ + ["Slug", "Name", "Description"], + ...items.map((item) => { + const filename = slugToFilename(item.slug) + ".md"; + const displaySlug = item.slug.replace(/^[^/]+\//, ""); + return [ + `[\`${displaySlug}\`](references/${outputDir}/${filename})`, + item.name, + item.description, + ]; + }), + ]; + + return `### ${group}\n\n${formatMarkdownTable(rows)}`; + }) + .join("\n\n"); } /** @@ -225,14 +244,16 @@ function generateEntryTable(entries, outputDir) { function generateCopiedTable(files, outputDir) { if (files.length === 0) return ""; - let table = "| File | Description |\n"; - table += "| ---- | ----------- |\n"; - for (const filePath of files) { - const filename = filePath.split("/").pop(); - const name = filename.replace(".md", ""); - table += `| [${filename}](references/${outputDir}/${filename}) | ${name} reference |\n`; - } - return table.trim(); + const rows = [ + ["File", "Description"], + ...[...files].sort().map((filePath) => { + const filename = filePath.split("/").pop(); + const name = filename.replace(".md", ""); + return [`[${filename}](references/${outputDir}/${filename})`, `${name} reference`]; + }), + ]; + + return formatMarkdownTable(rows); } async function findMarkdownFiles(dir) { @@ -243,7 +264,7 @@ async function findMarkdownFiles(dir) { } catch { return results; } - for (const entry of entries) { + for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) { if (!entry.isDirectory() && entry.name.endsWith(".md")) { results.push(join(dir, entry.name)); } diff --git a/catalogue/src/fundamental/components.md b/catalogue/src/fundamental/components.md index f7a9d61c..e752f4b9 100644 --- a/catalogue/src/fundamental/components.md +++ b/catalogue/src/fundamental/components.md @@ -1,6 +1,6 @@ # AppShell Components -> **AppShell version:** `0.36.0` (matches `packages/erp-kit/templates/scaffold/app/*/frontend/package.json` pinned `@tailor-platform/app-shell` semver) +> **AppShell version:** `1.7.0` (matches `packages/erp-kit/templates/scaffold/app/*/frontend/package.json` pinned `@tailor-platform/app-shell` semver) > **Source of truth:** `@tailor-platform/app-shell` exports > **Update process:** see "Keeping this file in sync" at the bottom @@ -81,8 +81,8 @@ import { Button, Layout, Tabs } from "@tailor-platform/app-shell"; Create]}> - All - Open + All + Open @@ -109,6 +109,28 @@ import { Button, Layout, Tabs } from "@tailor-platform/app-shell"; **Used in patterns:** every page pattern (`list/*`, `detail/*`, `form/*`). +### `Grid` + +**Import:** `import { Grid } from '@tailor-platform/app-shell'` +**Purpose:** Presentational CSS-Grid container for laying tiles/cards into equal or custom columns with responsive reflow — the canonical wrapper for **metric / KPI strips** and card galleries. Purely layout; makes no data assumptions. +**API:** `GridProps` — `columns` (number | CSS track string like `"280px 1fr"` | responsive object `{ initial, sm, md, lg, xl }`), `gap` (spacing step, default `3`) plus `gapX` / `gapY`, `minChildWidth` (auto-fit: as many ≥Npx columns as fit, no breakpoints needed), `rows`, `flow`, `align`, `justify`. Sub-component **`Grid.Item`** for spanning/placement — `colSpan`, `rowSpan`, `colStart`, `colEnd` (all responsive). Root carries `data-slot="grid"`. +**Example — responsive KPI grid:** + +```tsx +import { Grid, MetricCard } from "@tailor-platform/app-shell"; + + + + + + +; +``` + +**Used in patterns:** metric / KPI strips on `detail/*` and dashboards; any equal-column card layout. + +**Notes:** Metric tiles and KPI cards **always** go in a `Grid` — never stacked one-per-row or in a single column. Use a responsive `columns` object (e.g. `{ initial: 1, md: 2, xl: 4 }`) so tiles reflow at smaller widths; reach for `minChildWidth` when the tile count is dynamic. + ### `SidebarLayout` **Import:** `import { SidebarLayout } from '@tailor-platform/app-shell'` @@ -266,6 +288,28 @@ import { Button, Link } from '@tailor-platform/app-shell'; **API:** Compound — `Tooltip.Root`, `Tooltip.Trigger`, `Tooltip.Content`. **Used in patterns:** any pattern with icon-only buttons (must have `aria-label` AND a tooltip). +### `Tabs` + +**Import:** `import { Tabs } from '@tailor-platform/app-shell'` +**Purpose:** In-page tab navigation — split one record's sections (Overview / Line items / Activity) or bucket a list (All / Open / …) into switchable panels. Presentational; owns only the active-tab state. +**API:** Compound — `Tabs.Root` (`variant`: `default | line | capsule`; controlled `value` + `onValueChange`, or uncontrolled `defaultValue`), `Tabs.List`, `Tabs.Tab` (`value`), `Tabs.Panel` (`value`). Note: the sub-component is **`Tabs.Tab`**, not `Tabs.Trigger`. +**Example:** + +```tsx + + + Overview + Line items + + {/* … */} + {/* … */} + +``` + +**Used in patterns:** `list-dense-scan` (bucket tabs composed **above** `DataTable.Root`, synced to `useCollectionVariables` — see the `DataTable` "Bucket tabs" note), `detail/*` (sectioned record content). + +**Notes:** For lists, AppShell's own filtering surface is **toolbar chips** (`DataTable.Filters`), not tabs — reach for `Tabs` only when the business genuinely thinks in a small set of named buckets. Don't render a `Tab` per enum value where a filter chip belongs. + --- ## Display @@ -276,14 +320,27 @@ import { Button, Link } from '@tailor-platform/app-shell'; **Import:** `import { Badge } from '@tailor-platform/app-shell'` **Purpose:** Status labels and small categorical chips. -**API:** `BadgeProps` — `variant`: `default | success | warning | neutral | error | destructive`. Plus `badgeVariants` CVA for custom-styled siblings. +**API:** `BadgeProps` — `variant` (15 total): + +- **Filled** (high emphasis): `default` (primary), `success`, `warning`, `error`, `neutral`, `info` +- **Subtle** (low emphasis, tinted): `subtle-success`, `subtle-warning`, `subtle-error`, `subtle-info` +- **Outline** (renders a status dot — for row/list statuses): `outline-success`, `outline-warning`, `outline-error`, `outline-info`, `outline-neutral` + +Plus `badgeVariants` CVA for custom-styled siblings. **Example:** ```tsx -Active +// Primary / lifecycle status — filled semantic variant +Confirmed +// Secondary status (delivery, billing) — outline with status dot +Partially received +// Tag / label — subtle +New ``` -**Used in patterns:** `list/*` (status column), `detail/*` (header status). +**Used in patterns:** `list/*` (status column → `outline-*`), `detail/*` (header status). + +**Notes:** Encode status by **semantic color** with a primary/secondary split: a record's **primary/lifecycle status** uses a **filled** semantic variant (one per row / one in a detail header); **secondary statuses** (delivery, billing) use **`outline-*`** (status dot); tags use **`subtle-*`**. Reserve **`default`** (brand) for non-status emphasis. Full rule: **`design-system.md`** → Composition & emphasis rules. ### `Table` @@ -291,7 +348,7 @@ import { Button, Link } from '@tailor-platform/app-shell'; **Import:** `import { Table } from '@tailor-platform/app-shell'` **Purpose:** Semantic data table with scrollable container. -**API:** Compound — `Table.Root`, `Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Head`, `Table.Cell`, `Table.Caption`. `Table.Root` accepts `containerClassName` for the outer wrapper, `className` for the inner ``. +**API:** Compound — `Table.Root`, `Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Head`, `Table.Cell`, `Table.Caption`. `Table.Root` accepts `containerClassName` for the outer wrapper, `className` for the inner `
`. `Table.Head` and `Table.Cell` accept **`align`** (`"left" | "center" | "right"`, default `"left"`) — prefer this over ad-hoc `className="astw:text-right"` and pair the same alignment on the head and its column's cells (right for numeric/money, center for compact status/icon columns). **Example:** ```tsx @@ -300,7 +357,7 @@ import { Button, Link } from '@tailor-platform/app-shell'; Order Status - Total + Total @@ -310,7 +367,7 @@ import { Button, Link } from '@tailor-platform/app-shell'; {o.status} - {formatMoney(o.total)} + {formatMoney(o.total)} ))} @@ -362,6 +419,8 @@ const table = useDataTable({ ; ``` +**Column alignment:** each `Column` accepts **`align`** (`"left" | "right"`) applied to both header and body cell. Numeric `type` columns (`"number"`, `"money"`) default to `"right"` automatically so digits align on the decimal place — pass `align="left"` to opt out; everything else defaults to `"left"`. + **Metadata path:** Prefer `createColumnHelper` + `inferColumns(tableMetadata.order)` (`@tailor-platform/app-shell-sdk-plugin` codegen) when available so enum/datetime/string filters bind to the right editors. **Bucket tabs / segmented UX:** AppShell defines **toolbar chips**, not lifecycle tabs. When design places **`Tabs`** (All / Draft / …) inside the card, compose them **above** `DataTable.Root` and synchronize tab-driven bucket state with **`useCollectionVariables`** (`variables.query` / filters)—see **`patterns/list/dense-scan.md`**. @@ -426,25 +485,31 @@ const table = useDataTable({ **Import:** `import { MetricCard } from '@tailor-platform/app-shell'` **Purpose:** KPI tile for dashboards or hero metric strip. **API:** `MetricCardProps` — `title`, `value`, `trend: { direction, value }`, `description`, `icon`. -**Example:** +**Example:** metric tiles are laid out in a `Grid` (never one-per-row): ```tsx -} -/> + + } + /> + + {/* …more tiles */} + ``` **Used in patterns:** KPI tiles, dashboards, **`detail/*`** metric strips where specs call for them. +**Notes:** Always wrap metric tiles in **`Grid`** (see the `Grid` entry) — a column of single-width `MetricCard`s stacked one-per-row is an anti-pattern. + ### `DocumentProgressCard` **Import:** `import { DocumentProgressCard } from '@tailor-platform/app-shell'` -**Purpose:** Generic document lifecycle/fulfilment state — optional percentage, stacked progress bar, status legend; arbitrary `segments`. -**API:** `DocumentProgressCardProps` — `segments` (`{ label, value, color? }[]`), `title?`, `percent?`, `legend?` (defaults to `segments`), `total?` (bar denominator; larger than the sum leaves an empty track), `className?`. View-only — `percent` is explicit. +**Purpose:** Presentational card for a document's lifecycle / fulfilment state — an optional headline percentage, a stacked progress bar, and a status legend. Domain-agnostic and view-only: pass an arbitrary set of `segments` (e.g. shipped / returned / pending) plus an explicit `percent`; derive both in the consumer. +**API:** `DocumentProgressCardProps` — `segments` (required; array of `{ label, value, color }`), `percent?` (0–100, headline shown top-right), `title?`, `legend?` (defaults to `segments`; supply only when the legend should differ from the bar, e.g. overlapping buckets), `total?` (bar denominator; defaults to the sum of segment values — a larger value leaves an unfilled remainder), `className?`. Segment `color`: `indigo | pink | green | amber | red | blue | neutral`. **Example:** ```tsx @@ -459,7 +524,7 @@ const table = useDataTable({ /> ``` -**Used in patterns:** **`detail/*`** right-rail cards for arbitrary status breakdowns (shipped/cancelled/pending, PO received/returned/yet-to-receive, etc.). For the purchase-order fulfilment recipe (derived %, net-received/returned bar split, legend override), see `docs/components/document-progress-card.md`. +**Used in patterns:** `detail/*` — fulfilment / lifecycle summary cards (e.g. a purchase-order receipt progress) in the main column or right rail. ### `Avatar` @@ -520,6 +585,24 @@ const table = useDataTable({ **API:** Compound — `ActivityCard.Root`, `ActivityCard.Items` (generic over item type), plus `ActivityCardProps`, `ActivityCardItem`, `ActivityCardItemProps`. Items render with timestamp + actor + description. **Used in patterns:** `detail/hero-with-actions` (right column or bottom section). +### `Alert` + +**Import:** `import { Alert } from '@tailor-platform/app-shell'` +**Purpose:** Inline, in-page banner for a persistent status message — a form/record error, a warning, or a success/info notice attached to a section. (Transient notifications use `useToast`; blocking confirmations use `Dialog` — see `interaction/confirm`.) +**API:** Compound — `Alert.Root` (`variant`: `neutral | success | warning | error | info`, default `neutral`; a matching icon is rendered automatically; optional `action?: ReactNode`, `dismissible?: boolean`, `onDismiss?: () => void`), `Alert.Title`, `Alert.Description`. +**Example:** + +```tsx + + Couldn't load line items + Check your connection and try again. + +``` + +**Used in patterns:** any data-backed screen's **error** state (inline error + retry) and record-level warnings — the error affordance the composition rules require (**`design-system.md`** → Composition & emphasis rules → States). + +**Notes:** Encode severity by `variant` (semantic color), same discipline as `Badge` — don't use `error` for a routine notice. Pair a retry/next-step control via `action` rather than a separate stray button. + --- ## Forms @@ -560,6 +643,7 @@ const table = useDataTable({ **Import:** `import { Select } from '@tailor-platform/app-shell'` **Purpose:** Dropdown for fixed enumerations. **API:** Compound. Sync version (small option lists). For async/typeahead, see `Combobox` and `Autocomplete`. Type: `SelectAsyncFetcher` for the async variant. +**Accessible name:** inside a `Form` the field label provides it automatically. When used **standalone** (outside a `Form`), pass `aria-label` (or `aria-labelledby` / `id`) — these now forward to the underlying combobox element, so it isn't left named only by its current value. Same applies to `Combobox` and `Autocomplete`. **Used in patterns:** all `form/*` (enum fields). ### `Combobox` @@ -642,7 +726,7 @@ Types for authoring guard functions used by `WithGuard` and `appShellPageProps.g ### `RouteParams`, `PageComponent`, `PageMeta`, `AppShellPageProps`, `AppShellRegister`, `ContextData` (types) -Types for declaring page props (`appShellPageProps = { meta, guards, loader }`), reading typed route params, and registering route types globally. See `project-setup.md` for usage. +Types for declaring page props (`appShellPageProps = { meta, guards }`), reading typed route params, and registering route types globally. See `project-setup.md` for usage. --- diff --git a/catalogue/src/fundamental/design-system.md b/catalogue/src/fundamental/design-system.md index 48bbed82..78b4d583 100644 --- a/catalogue/src/fundamental/design-system.md +++ b/catalogue/src/fundamental/design-system.md @@ -353,3 +353,27 @@ When a custom component proves reusable across 2+ apps, promote it upstream into | Two-column detail at <1024 | right column collapses below main — do not override | | Inline ID, code, table number | `text-mono` | | Timestamp, label, subtle metadata | `text-caption text-fg-subtle` | + +### Composition & emphasis rules + +These are visual-composition rules every screen must follow, regardless of pattern. They exist because emphasis only works when it is scarce. + +**Emphasis budget.** Attention is a budget you spend once per scan region. + +- **One primary action per view.** A screen (or a card/section) has at most one filled/primary `Button`; everything else is `outline`, `secondary`, or `ghost`. If two things look equally important, neither reads as important. +- **Badges** encode status by semantic color, with a clear primary/secondary split: + - A record's **primary / lifecycle status** (PO status, SO status) → a **filled semantic** variant (`success` / `warning` / `error` / `info` / `neutral`) — one per row in a list, one in a detail header. + - **Secondary statuses** (delivery, billing, fulfilment) and dense supporting columns → **`outline-*`** (with status dot). + - **Tags / labels** ("New", "Returned") → **`subtle-*`**. + - Reserve **`default`** (brand fill) for non-status emphasis — never the brand color as a routine status. The defect to avoid: making _every_ chip a loud fill, or giving secondary statuses the same weight as the primary one. (Variants: **`components.md`** → `Badge`.) +- **Color:** status colors (`success`/`warning`/`error`/`info`) signal meaning, not decoration — don't tint neutral content. + +**Hierarchy.** One `h1` per page (the `Layout.Header` title). Section headings step down (`h2` → `h3`); never skip levels for size — pick the role token (`design-system.md` §4 Typography), not the pixel size. + +**States — never ship only the happy path.** Every data-backed screen handles: + +- **Loading** — skeleton/placeholder, not a blank flash. +- **Empty** — a labelled empty state (what it is, how to add the first record), not a bare empty table. +- **Error** — an inline error with a retry affordance, not a silent failure. + +**Spacing rhythm.** Use the spacing scale (§4) consistently — equal gaps between sibling sections, consistent card padding. A one-off `gap` or `padding` that doesn't match its siblings reads as a mistake. diff --git a/catalogue/src/fundamental/graphql.md b/catalogue/src/fundamental/graphql.md index c889c42d..9096953f 100644 --- a/catalogue/src/fundamental/graphql.md +++ b/catalogue/src/fundamental/graphql.md @@ -191,7 +191,7 @@ Every page component: }; ``` -3. Uses `appShellPageProps.guards` for permission gates and `appShellPageProps.loader` for route loaders. See [project-setup.md](project-setup.md). +3. Uses `appShellPageProps.guards` for permission gates. See [project-setup.md](project-setup.md). ## Quick reference diff --git a/catalogue/src/pattern/detail/hero-with-actions/PATTERN.md b/catalogue/src/pattern/detail/hero-with-actions/PATTERN.md index 7fb38db0..ca1cb101 100644 --- a/catalogue/src/pattern/detail/hero-with-actions/PATTERN.md +++ b/catalogue/src/pattern/detail/hero-with-actions/PATTERN.md @@ -5,7 +5,21 @@ category: pattern subcategory: detail description: Single-record detail view with workflow actions and activity timeline requiredImports: - [Layout, Badge, Button, Menu, DescriptionCard, Card, Table, ActionPanel, ActivityCard] + [ + Layout, + Badge, + Button, + Menu, + DescriptionCard, + Card, + Table, + ActionPanel, + ActivityCard, + Grid, + MetricCard, + DocumentProgressCard, + Alert, + ] tags: [detail, actions, timeline, workflow, two-column] do: - Single-record detail view (Order #1234, Supplier ABC, Product SKU-42) @@ -51,15 +65,43 @@ Composition rules: Responsive: <1024 collapses to single column with right column rendered below main; 1024–1280 two columns with narrow side; >1280 two columns at full width. +## Layout variants + +Choose the column structure from the **record** — the pattern is "hero + workflow + (optional) history," and the layout flexes. The skill never mandates a fixed column count. + +- **Main + right rail** (most common) — the record has workflow actions and/or activity/related summaries → right `Layout.Column area="right"` holds `ActionPanel`, `ActivityCard`, integration cards. +- **Single main column** — the record is mostly descriptive with few/no workflow actions and no meaningful history → one `Layout.Column`. Don't invent a rail just to fill space. +- **Main + left rail** (`area="left"`, wider 320px) — when a persistent section index / summary should lead the reading order. +- Either column holds **N cards** as the record requires. + +## Optional cards + +Add these only when the record calls for them — none is mandatory. Each is a card like any other in the columns above. + +- **Metric strip** — when the record has headline KPIs (totals, counts), lead the main column with `MetricCard`s wrapped in a `Grid` (never one per row). See the `Grid` and `MetricCard` entries in `components.md`. + + ```tsx + + + + + + + ``` + +- **`DocumentProgressCard`** — when the record has a fulfilment / lifecycle breakdown (received vs returned vs pending, shipment status). Sits in the main column or the right rail; derive `percent` and `segments` in the consumer. +- **`Alert`** — a record-level inline banner (blocking error, on-hold warning) above the columns; use the **error** variant for the pattern's required error state. Transient feedback uses `useToast`, not `Alert`. + ## Page Implementation ## Constraints -- More than 2 primary actions in the header → move overflow into a `Menu`. +- **Header carries the single primary CTA + the status `Badge`** — not workflow actions. Workflow actions (Approve, Reject, Archive, …) live in the `ActionPanel`. If the header needs more than ~2 actions, move the overflow into a `Menu`. +- **Never duplicate an action** across `Layout.Header` and `ActionPanel` — each action has exactly one home. - Every content section MUST sit inside `Card.Root` (or `DescriptionCard`, which already self-contains). Raw divs are not allowed. -- `ActionPanel` is workflow-only — never back-navigation. +- `ActionPanel` is workflow-only — never back-navigation (that lives in the breadcrumb). - `Table.Root` inside a Card requires `containerClassName="astw:px-6"`. ## Anti-patterns @@ -69,3 +111,5 @@ Responsive: <1024 collapses to single column with right column rendered below ma - Bare `
` sections in the main column — every content section MUST sit inside `Card.Root`. - `ActionPanel` containing back-navigation (e.g. "Back to Product List") — back navigation lives in `Layout.Header`'s breadcrumb. - A `Table.Root` inside a Card without `containerClassName="astw:px-6"` — the first column lands flush against the card edge. +- The same action in both `Layout.Header` and the `ActionPanel` — duplicating it makes neither read as canonical. Pick one home. +- Giving every status the same loud weight — the record's primary/lifecycle status is a **filled** semantic badge; secondary statuses (fulfilment, billing) use **`outline-*`** (see `design-system.md` → Composition & emphasis rules). diff --git a/catalogue/src/pattern/detail/hero-with-actions/hero-with-actions.tsx b/catalogue/src/pattern/detail/hero-with-actions/hero-with-actions.tsx index 43cd0f7d..f7e30bef 100644 --- a/catalogue/src/pattern/detail/hero-with-actions/hero-with-actions.tsx +++ b/catalogue/src/pattern/detail/hero-with-actions/hero-with-actions.tsx @@ -53,16 +53,16 @@ export default function HeroWithActionsDetail({ order, onApprove, onCancel }: Pr SKU - Qty - Total + Qty + Total {order.lineItems.map((item) => ( {item.sku} - {item.qty} - ${item.total.toLocaleString()} + {item.qty} + ${item.total.toLocaleString()} ))} diff --git a/catalogue/src/pattern/list/dense-scan/PATTERN.md b/catalogue/src/pattern/list/dense-scan/PATTERN.md index 70041e50..baef4dbe 100644 --- a/catalogue/src/pattern/list/dense-scan/PATTERN.md +++ b/catalogue/src/pattern/list/dense-scan/PATTERN.md @@ -69,7 +69,8 @@ Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, a - 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 -- Status Badge colors must use design system tokens (variant prop) +- 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`; no per-row "View" / "Open" buttons - Per-row `Menu` (overflow `…`) is reserved for non-navigation actions (Archive, Duplicate, Delete) diff --git a/catalogue/src/pattern/list/dense-scan/columns.tsx b/catalogue/src/pattern/list/dense-scan/columns.tsx index 6455225c..241a6d5d 100644 --- a/catalogue/src/pattern/list/dense-scan/columns.tsx +++ b/catalogue/src/pattern/list/dense-scan/columns.tsx @@ -10,11 +10,13 @@ export type Order = { createdAt: string; }; +// Primary status column — filled semantic variants (one per row). +// Secondary status columns (e.g. delivery, billing) use outline-* instead. const statusVariant = { draft: "neutral", - confirmed: "outline-info", - shipped: "outline-warning", - delivered: "outline-success", + confirmed: "info", + shipped: "warning", + delivered: "success", } as const; export const columns: Column[] = [ @@ -25,7 +27,10 @@ export const columns: Column[] = [ render: (row) => {row.status}, }, { + // Numeric columns right-align so digits line up. `type: "money" | "number"` + // auto-right; with a custom `render` set `align` explicitly. label: "Amount", + align: "right", render: (row) => `$${row.amount.toLocaleString()}`, }, { label: "Created", accessor: (row) => row.createdAt }, From 024c4feb11b5aee9b7e3dd8318d1529de9599834 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Thu, 9 Jul 2026 16:05:16 +0900 Subject: [PATCH 2/7] chore: stop tracking generated skills --- catalogue/expected-skills-files.txt | 13 + catalogue/package.json | 1 + catalogue/scripts/check-packed-skills.mjs | 86 ++ packages/core/.gitignore | 1 + packages/core/package.json | 1 + .../core/skills/app-shell-patterns/SKILL.md | 79 -- .../references/fundamental/components.md | 825 ------------------ .../references/fundamental/design-system.md | 379 -------- .../references/fundamental/graphql.md | 205 ----- .../patterns/detail-hero-with-actions.md | 201 ----- .../references/patterns/form-modal.md | 169 ---- .../references/patterns/form-sectioned.md | 126 --- .../references/patterns/form-single-page.md | 105 --- .../references/patterns/form-wizard.md | 134 --- .../patterns/interaction-confirm.md | 75 -- .../patterns/interaction-multi-select.md | 173 ---- .../references/patterns/interaction-toast.md | 69 -- .../references/patterns/list-dense-scan.md | 167 ---- 18 files changed, 102 insertions(+), 2707 deletions(-) create mode 100644 catalogue/expected-skills-files.txt create mode 100644 catalogue/scripts/check-packed-skills.mjs delete mode 100644 packages/core/skills/app-shell-patterns/SKILL.md delete mode 100644 packages/core/skills/app-shell-patterns/references/fundamental/components.md delete mode 100644 packages/core/skills/app-shell-patterns/references/fundamental/design-system.md delete mode 100644 packages/core/skills/app-shell-patterns/references/fundamental/graphql.md delete mode 100644 packages/core/skills/app-shell-patterns/references/patterns/detail-hero-with-actions.md delete mode 100644 packages/core/skills/app-shell-patterns/references/patterns/form-modal.md delete mode 100644 packages/core/skills/app-shell-patterns/references/patterns/form-sectioned.md delete mode 100644 packages/core/skills/app-shell-patterns/references/patterns/form-single-page.md delete mode 100644 packages/core/skills/app-shell-patterns/references/patterns/form-wizard.md delete mode 100644 packages/core/skills/app-shell-patterns/references/patterns/interaction-confirm.md delete mode 100644 packages/core/skills/app-shell-patterns/references/patterns/interaction-multi-select.md delete mode 100644 packages/core/skills/app-shell-patterns/references/patterns/interaction-toast.md delete mode 100644 packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md diff --git a/catalogue/expected-skills-files.txt b/catalogue/expected-skills-files.txt new file mode 100644 index 00000000..775aa3c3 --- /dev/null +++ b/catalogue/expected-skills-files.txt @@ -0,0 +1,13 @@ +skills/app-shell-patterns/SKILL.md +skills/app-shell-patterns/references/fundamental/components.md +skills/app-shell-patterns/references/fundamental/design-system.md +skills/app-shell-patterns/references/fundamental/graphql.md +skills/app-shell-patterns/references/patterns/detail-hero-with-actions.md +skills/app-shell-patterns/references/patterns/form-modal.md +skills/app-shell-patterns/references/patterns/form-sectioned.md +skills/app-shell-patterns/references/patterns/form-single-page.md +skills/app-shell-patterns/references/patterns/form-wizard.md +skills/app-shell-patterns/references/patterns/interaction-confirm.md +skills/app-shell-patterns/references/patterns/interaction-multi-select.md +skills/app-shell-patterns/references/patterns/interaction-toast.md +skills/app-shell-patterns/references/patterns/list-dense-scan.md diff --git a/catalogue/package.json b/catalogue/package.json index 93e6c51b..eeaf4767 100644 --- a/catalogue/package.json +++ b/catalogue/package.json @@ -4,6 +4,7 @@ "type": "module", "scripts": { "build": "node scripts/generate-skill.mjs", + "test": "node scripts/check-packed-skills.mjs", "type-check": "tsc --noEmit" }, "dependencies": { diff --git a/catalogue/scripts/check-packed-skills.mjs b/catalogue/scripts/check-packed-skills.mjs new file mode 100644 index 00000000..ff50cdcf --- /dev/null +++ b/catalogue/scripts/check-packed-skills.mjs @@ -0,0 +1,86 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; + +const execFileAsync = promisify(execFile); +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const catalogueRoot = join(__dirname, ".."); +const repoRoot = join(catalogueRoot, ".."); +const expectedManifestPath = join(catalogueRoot, "expected-skills-files.txt"); + +function parseManifest(text) { + return text + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .sort(); +} + +function diffLists(expected, actual) { + const expectedSet = new Set(expected); + const actualSet = new Set(actual); + + return { + missing: expected.filter((file) => !actualSet.has(file)), + extra: actual.filter((file) => !expectedSet.has(file)), + }; +} + +async function main() { + const tempDir = await mkdtemp(join(tmpdir(), "app-shell-pack-")); + + try { + await execFileAsync( + "pnpm", + ["--filter", "@tailor-platform/app-shell", "pack", "--pack-destination", tempDir], + { cwd: repoRoot }, + ); + + const tarballs = (await readdir(tempDir)).filter((file) => file.endsWith(".tgz")); + if (tarballs.length !== 1) { + throw new Error(`Expected exactly one tarball, found ${tarballs.length}`); + } + + const tarballPath = join(tempDir, tarballs[0]); + const [{ stdout: tarList }, expectedManifest] = await Promise.all([ + execFileAsync("tar", ["-tf", tarballPath], { cwd: repoRoot }), + readFile(expectedManifestPath, "utf8"), + ]); + + const actualSkillsFiles = tarList + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.startsWith("package/skills/") && !line.endsWith("/")) + .map((line) => line.replace(/^package\//, "")) + .sort(); + const expectedSkillsFiles = parseManifest(expectedManifest); + + const { missing, extra } = diffLists(expectedSkillsFiles, actualSkillsFiles); + if (missing.length === 0 && extra.length === 0) { + console.log(`Packed skills manifest matches expected (${actualSkillsFiles.length} files).`); + return; + } + + console.error("Packed skills manifest does not match catalogue/expected-skills-files.txt"); + if (missing.length > 0) { + console.error("\nMissing:"); + for (const file of missing) console.error(` - ${file}`); + } + if (extra.length > 0) { + console.error("\nExtra:"); + for (const file of extra) console.error(` - ${file}`); + } + + process.exitCode = 1; + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/core/.gitignore b/packages/core/.gitignore index 3c3629e6..cf961acf 100644 --- a/packages/core/.gitignore +++ b/packages/core/.gitignore @@ -1 +1,2 @@ node_modules +skills diff --git a/packages/core/package.json b/packages/core/package.json index 63f9172f..ec3e9401 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -45,6 +45,7 @@ "scripts": { "dev": "vite build --watch --mode development", "build": "vite build", + "prepack": "node ../../catalogue/scripts/generate-skill.mjs", "lint": "oxlint -c .oxlintrc.jsonc", "test": "vitest run", "type-check": "tsc --incremental" diff --git a/packages/core/skills/app-shell-patterns/SKILL.md b/packages/core/skills/app-shell-patterns/SKILL.md deleted file mode 100644 index 81125253..00000000 --- a/packages/core/skills/app-shell-patterns/SKILL.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -name: app-shell-patterns -description: "Best-practice UI patterns and correct component usage for building pages in apps that use @tailor-platform/app-shell. Use when: building or editing any screen, page, list, table, detail view, form, modal, dialog, wizard, or bulk/confirm/toast interaction in an app with @tailor-platform/app-shell installed — or when choosing the right AppShell component, layout, or design token for a UI." ---- - -# App-Shell Patterns - -## Purpose - -Select and implement the correct UI pattern using @tailor-platform/app-shell components. - -## Fundamental References - -These are the foundational rules that underpin all patterns. All patterns build on top of these references. - -| File | Description | -| ----------------------------------------------------------- | ----------------------- | -| [components.md](references/fundamental/components.md) | components reference | -| [design-system.md](references/fundamental/design-system.md) | design-system reference | -| [graphql.md](references/fundamental/graphql.md) | graphql reference | - -## Available Patterns - -### detail - -| Slug | Name | Description | -| ----------------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------- | -| [`detail/hero-with-actions`](references/patterns/detail-hero-with-actions.md) | Hero With Actions Detail | Single-record detail view with workflow actions and activity timeline | - -### form - -| Slug | Name | Description | -| ------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------- | -| [`form/modal`](references/patterns/form-modal.md) | Modal Form | Default form pattern for Create/Edit — keeps user in context on the parent screen | -| [`form/sectioned`](references/patterns/form-sectioned.md) | Sectioned Form | Complex form with 15+ fields organized into named fieldset sections | -| [`form/single-page`](references/patterns/form-single-page.md) | Single Page Form | Routed full-page form for moderate field count (6-15) without natural sectioning | -| [`form/wizard`](references/patterns/form-wizard.md) | Wizard Form | Multi-stage create flow with 3-7 steps and per-step validation gates | - -### interaction - -| Slug | Name | Description | -| ----------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------- | -| [`interaction/confirm`](references/patterns/interaction-confirm.md) | Confirm | Confirmation dialog before destructive or irreversible actions | -| [`interaction/multi-select`](references/patterns/interaction-multi-select.md) | Multi Select | Floating bottom action bar for bulk operations on selected list rows | -| [`interaction/toast`](references/patterns/interaction-toast.md) | Toast | Lightweight feedback after mutations — success or error notifications | - -### list - -| Slug | Name | Description | -| ----------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------- | -| [`list/dense-scan`](references/patterns/list-dense-scan.md) | Dense Scan List | High-density scannable list backed by GraphQL connections with DataTable, sort, filters, and pagination | - -## How to Use - -1. Identify the user's intent (list, detail, form, interaction, screen composition, recipe) -2. Match constraints to an entry slug from the tables above -3. Read the entry's detailed spec: `references//.md` (relative to this file) -4. Read fundamental references for component APIs, design tokens, and GraphQL conventions: `references/fundamental/` -5. Implement using ONLY the imports listed in the entry's `requiredImports` - -## Rules - -- ALWAYS cite the entry slug in a comment at the top of the file: - `/* pattern: list/dense-scan */` -- NEVER mix patterns in a single page component -- ALWAYS use AppShell components — do NOT use raw HTML or third-party UI libraries -- If no entry matches, compose directly from fundamental references - -### Cross-cutting UX rules (apply to every screen) - -Full rationale in [`design-system.md`](references/fundamental/design-system.md) → Composition & emphasis rules. - -- **One primary action per view:** at most one primary/filled `Button`; everything else is `outline`/`secondary`/`ghost`. -- **Status badges by semantic color:** a **filled** semantic variant for the record's primary/lifecycle status, **`outline-*`** for secondary statuses, **`subtle-*`** for tags; reserve brand `default` for non-status emphasis. -- **No duplicate actions:** an action lives in exactly one place — never repeat the same action in both `Layout.Header` and `ActionPanel`. -- **Action placement:** primary CTA + status in `Layout.Header`; workflow actions in `ActionPanel`; back/navigation in the breadcrumb — never in `ActionPanel`. -- **Metric tiles always go in a `Grid`** (`columns={{ initial: 1, md: 2, xl: 4 }}`) — never one-per-row. -- **Forms default to `form/modal`** — only build a routed full-page form when the design explicitly calls for one. -- **Handle every state:** loading (skeleton), empty (labelled empty state), and error (inline + retry) — never ship only the happy path. diff --git a/packages/core/skills/app-shell-patterns/references/fundamental/components.md b/packages/core/skills/app-shell-patterns/references/fundamental/components.md deleted file mode 100644 index e752f4b9..00000000 --- a/packages/core/skills/app-shell-patterns/references/fundamental/components.md +++ /dev/null @@ -1,825 +0,0 @@ -# AppShell Components - -> **AppShell version:** `1.7.0` (matches `packages/erp-kit/templates/scaffold/app/*/frontend/package.json` pinned `@tailor-platform/app-shell` semver) -> **Source of truth:** `@tailor-platform/app-shell` exports -> **Update process:** see "Keeping this file in sync" at the bottom - -## Scope vs design-system.md - -| This file (`components.md`) | `design-system.md` | -| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | -| **Imports**, compound structure, hooks, canonical **composition** (`Card` + `Table`, `DataTable`, `Dialog`, etc.) | **Tokens**, theme imports, typography/spacing/radius/elevation **tables**, breakpoints **intent** | -| JSX examples tied to ERP patterns (`patterns/*`) | **`astw:`** rules — only on AppShell `*ClassName` props; plain utilities on **your** elements | -| Prop summaries + links to upstream `docs/components/*.md` | **Visual conformance**: no magic colors/px on custom markup, motion, dark mode | - -**Rule of thumb:** “Which component / prop?” → **here.** “Which token / spacing step / elevation?” → **`design-system.md`** §§4–6. - -This file intentionally does **not** duplicate full token catalogs. “Every heading here ≈ documented export cluster” maintenance lives in **Keeping this file in sync** — upstream npm remains authoritative for completeness. - -Entries follow this shape: - -``` -**Import:** how to import it -**Purpose:** one sentence -**API:** key props or sub-components -**Example:** minimal JSX -**Used in patterns:** which patterns//.md cite this component -**Notes:** version-specific quirks (optional) -``` - -For the full upstream API of any component, follow the link to the published reference at the top of its section. - ---- - -## Layout primitives - -### `AppShell` - -**Import:** `import { AppShell } from '@tailor-platform/app-shell'` -**Purpose:** Application root — wraps `` with AppShell context, theme, and routing. -**API:** Compound — `AppShell.Root`, plus subcomponents wired through `AppShellProps`. Configured once in `App.tsx`. -**Example:** see `project-setup.md`. -**Used in patterns:** all (root container). - -### `Layout` - -> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/layout.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/layout.md) - -**Import:** `import { Layout } from '@tailor-platform/app-shell'` -**Purpose:** Standard page container with header + 1–N column body. The most-used component — every page wraps content in ``. -**API:** `Layout` (root) + `Layout.Column` + `Layout.Header`. `LayoutProps`: `fill` (boolean), `columns` (number, default auto-detected from children), `gap`, `title`, `actions`, `className`, `style`. - -**`fill`** — stretches the layout to the available height and bounds the column row so children can scroll internally instead of growing the page. Use on table-first pages (`` + `DataTable`): the title, table toolbar, sticky column headers, and pagination footer stay pinned while only the rows scroll — see **`patterns/list-dense-scan.md`**. Omit on pages that should flow and scroll naturally (forms, dashboards). - -**Column-count → width rules** (column count is auto-detected from `Layout.Column` children): - -| Columns | Breakpoint | Widths | Below breakpoint | -| ------- | ---------- | ------------------------------ | ---------------- | -| 1 | always | full | n/a | -| 2 | ≥ 1024px | flex + 280px | stacks | -| 3 | ≥ 1280px | 320px + flex + 280px | stacks | -| 4+ | ≥ 1280px | equal share — `repeat(N, 1fr)` | stacks | - -Desktop breakpoints and desktop-first rationale: **`design-system.md`** §4 Breakpoints. This table is **`Layout` column mechanics only**. - -**`Layout.Header`** — direct child of ``, above any ``. Only the first is rendered if multiple are passed. - -| Prop | Type | Description | -| ---------- | ------------------- | ------------------------------------------------ | -| `title` | `string` | Page title — `

` on the left | -| `actions` | `React.ReactNode[]` | Buttons on the right | -| `children` | `React.ReactNode` | Full-width row below title — typical use is tabs | - -**`Layout.Column`** — direct child, accepts `area` (`"left" | "main" | "right"`) for advanced placement override. If any column declares `area`, all columns switch to area-based widths (`left`=320, `main`=flex, `right`=280) and render in source order. - -**Example — list page header with tabs:** - -```tsx -import { Button, Layout, Tabs } from "@tailor-platform/app-shell"; - - - Create]}> - - - All - Open - - - - {/* table — see patterns/list/dense-scan.md */} -; -``` - -**Example — detail page, 2-column with area mode:** - -```tsx - - Edit]} /> - - - - - - - - -``` - -**Notes:** Children that aren't `Layout.Header` or `Layout.Column` are filtered out. Column gap overrides (``) → **`design-system.md`** §5 (`astw:` rules). - -**Used in patterns:** every page pattern (`list/*`, `detail/*`, `form/*`). - -### `Grid` - -**Import:** `import { Grid } from '@tailor-platform/app-shell'` -**Purpose:** Presentational CSS-Grid container for laying tiles/cards into equal or custom columns with responsive reflow — the canonical wrapper for **metric / KPI strips** and card galleries. Purely layout; makes no data assumptions. -**API:** `GridProps` — `columns` (number | CSS track string like `"280px 1fr"` | responsive object `{ initial, sm, md, lg, xl }`), `gap` (spacing step, default `3`) plus `gapX` / `gapY`, `minChildWidth` (auto-fit: as many ≥Npx columns as fit, no breakpoints needed), `rows`, `flow`, `align`, `justify`. Sub-component **`Grid.Item`** for spanning/placement — `colSpan`, `rowSpan`, `colStart`, `colEnd` (all responsive). Root carries `data-slot="grid"`. -**Example — responsive KPI grid:** - -```tsx -import { Grid, MetricCard } from "@tailor-platform/app-shell"; - - - - - - -; -``` - -**Used in patterns:** metric / KPI strips on `detail/*` and dashboards; any equal-column card layout. - -**Notes:** Metric tiles and KPI cards **always** go in a `Grid` — never stacked one-per-row or in a single column. Use a responsive `columns` object (e.g. `{ initial: 1, md: 2, xl: 4 }`) so tiles reflow at smaller widths; reach for `minChildWidth` when the tile count is dynamic. - -### `SidebarLayout` - -**Import:** `import { SidebarLayout } from '@tailor-platform/app-shell'` -**Purpose:** Top-level layout that mounts the sidebar and renders the page outlet. -**API:** `SidebarLayoutProps` — `sidebar`, `header`, and `children` are full-region slots (each defaults to a built-in). `sidebar` defaults to `SidebarLayout.DefaultSidebar`; `header` defaults to `SidebarLayout.DefaultHeader`. Used in `App.tsx`. -**Used in patterns:** consumed by AppShell init, not directly by page patterns. See `project-setup.md`. - -### `SidebarLayout.DefaultHeader` (`DefaultHeader`) - -**Import:** `import { SidebarLayout } from '@tailor-platform/app-shell'` → `SidebarLayout.DefaultHeader` (also top-level `DefaultHeader`) -**Purpose:** The built-in top bar (trigger + breadcrumb, plus an `actions` cluster). Drop into `SidebarLayout`'s `header` slot to extend the header without rebuilding it. -**API:** `actions?: ReactNode | ReactNode[]` — the right-hand cluster (opinionated flex row). **Defaults to `[]`, and passing `actions` REPLACES the switcher** — include `` in the array to keep it. `actions={[]}` = empty right side. -**Used in patterns:** project-level header customization (notification bell, user menu). See `project-setup.md`. - -### `SidebarLayout.DefaultSidebar` (`DefaultSidebar`), `SidebarGroup`, `SidebarItem`, `SidebarSeparator` - -**Import:** `import { SidebarLayout, SidebarGroup, SidebarItem, SidebarSeparator } from '@tailor-platform/app-shell'` → `SidebarLayout.DefaultSidebar` (also top-level `DefaultSidebar`) -**Purpose:** Sidebar composition. `DefaultSidebar` auto-resolves nav items from `appShellPageProps.meta` on each page; the others let you customize manually. -**Used in patterns:** sidebar is a project-level concern. See `project-setup.md`. - -### `AppearanceSwitcher` - -**Import:** `import { AppearanceSwitcher } from '@tailor-platform/app-shell'` -**Purpose:** Palette-icon dropdown for Light/Dark/System color mode. The default `SidebarLayout.DefaultHeader` action; also usable standalone (e.g. a sidebar footer). Include it in `DefaultHeader`'s `actions` array when customizing the header to keep it visible. -**API:** No props — reads/writes theme via `useTheme`. -**Used in patterns:** project-level. See `project-setup.md`. - -### `CommandPalette` - -**Import:** `import { CommandPalette } from '@tailor-platform/app-shell'` -**Purpose:** Cmd/Ctrl-K command palette. Auto-discovers searchable resources via `defineResource`. -**API:** Renderless — drop into the layout once. -**Used in patterns:** project-level. Useful for any app with >10 routes. - ---- - -## Interaction surfaces - -### `Button` - -> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/button.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/button.md) - -**Import:** `import { Button } from '@tailor-platform/app-shell'` -**Purpose:** All buttons in the app — including polymorphic rendering via the `render` prop. -**API:** `ButtonProps` extends native ` - -``` - -**Used in patterns:** every pattern. - -### `Link` - -**Import:** `import { Link } from '@tailor-platform/app-shell'` -**Purpose:** Router-aware anchor. Re-exported from `react-router` so the rest of the app stays on the AppShell barrel. -**API:** `to`, `replace`, `state`, etc. — same as `react-router`. -**Used in patterns:** all (navigation). - -### `Dialog` - -> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/dialog.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/dialog.md) - -**Import:** `import { Dialog } from '@tailor-platform/app-shell'` -**Purpose:** Modal dialog for confirmations, ≤5-field forms, blocking workflows. -**API:** Compound — `Dialog.Root`, `Dialog.Trigger`, `Dialog.Content`, `Dialog.Header`, `Dialog.Title`, `Dialog.Description`, `Dialog.Footer`, `Dialog.Close`. Controllable via `open` + `onOpenChange`. -**Example:** - -```tsx - - }>Delete - - - Delete order #1234? - This cannot be undone. - - - }>Cancel - - - - -``` - -**Used in patterns:** `form/modal`, `interaction/confirm`. - -### `Sheet` - -> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/sheet.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/sheet.md) - -**Import:** `import { Sheet } from '@tailor-platform/app-shell'` -**Purpose:** Slide-in panel from any edge. Use for filters, side-work without losing context. -**API:** Compound — `Sheet.Root` (with `side: 'left' | 'right' | 'top' | 'bottom'`), `Sheet.Trigger`, `Sheet.Content`, `Sheet.Header`, `Sheet.Title`, `Sheet.Description`, `Sheet.Footer`, `Sheet.Close`. -**Example:** - -```tsx - - }>Filters - - - Filter orders - - {/* filter inputs */} - - }>Clear - - - - -``` - -**Used in patterns:** `list/*` (filter sheet variant). - -**Notes:** Size the panel with **`contentClassName`** (often `astw:*` utilities). Rules → **`design-system.md`** §5. - -### `Menu` - -> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/menu.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/menu.md) - -**Import:** `import { Menu } from '@tailor-platform/app-shell'` -**Purpose:** Dropdown menu — row actions, overflow actions, grouped commands. -**API:** Compound — `Menu.Root`, `Menu.Trigger`, `Menu.Content`, `Menu.Item`, `Menu.Separator`, `Menu.Group`, `Menu.GroupLabel`. Supports checkbox/radio items and nested sub-menus. -**Example:** - -```tsx - - - - - - handleAssign(id)}>Assign - handleDuplicate(id)}>Duplicate - - handleDelete(id)}>Delete - - -``` - -**Used in patterns:** `list/*` (row actions), `detail/*` (overflow actions). - -**Notes:** **`list-dense-scan`** uses whole-row / primary-column navigation — keep row `Menu` items **non-navigation** (Assign, Duplicate, Delete). Avoid redundant **View**/**Open**. Detail overflows may include navigation only when not duplicating hero content. - -### `Tooltip` - -**Import:** `import { Tooltip } from '@tailor-platform/app-shell'` -**Purpose:** Contextual hint on hover/focus. Use sparingly — for icon-only buttons or constrained labels. -**API:** Compound — `Tooltip.Root`, `Tooltip.Trigger`, `Tooltip.Content`. -**Used in patterns:** any pattern with icon-only buttons (must have `aria-label` AND a tooltip). - -### `Tabs` - -**Import:** `import { Tabs } from '@tailor-platform/app-shell'` -**Purpose:** In-page tab navigation — split one record's sections (Overview / Line items / Activity) or bucket a list (All / Open / …) into switchable panels. Presentational; owns only the active-tab state. -**API:** Compound — `Tabs.Root` (`variant`: `default | line | capsule`; controlled `value` + `onValueChange`, or uncontrolled `defaultValue`), `Tabs.List`, `Tabs.Tab` (`value`), `Tabs.Panel` (`value`). Note: the sub-component is **`Tabs.Tab`**, not `Tabs.Trigger`. -**Example:** - -```tsx - - - Overview - Line items - - {/* … */} - {/* … */} - -``` - -**Used in patterns:** `list-dense-scan` (bucket tabs composed **above** `DataTable.Root`, synced to `useCollectionVariables` — see the `DataTable` "Bucket tabs" note), `detail/*` (sectioned record content). - -**Notes:** For lists, AppShell's own filtering surface is **toolbar chips** (`DataTable.Filters`), not tabs — reach for `Tabs` only when the business genuinely thinks in a small set of named buckets. Don't render a `Tab` per enum value where a filter chip belongs. - ---- - -## Display - -### `Badge` - -> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/badge.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/badge.md) - -**Import:** `import { Badge } from '@tailor-platform/app-shell'` -**Purpose:** Status labels and small categorical chips. -**API:** `BadgeProps` — `variant` (15 total): - -- **Filled** (high emphasis): `default` (primary), `success`, `warning`, `error`, `neutral`, `info` -- **Subtle** (low emphasis, tinted): `subtle-success`, `subtle-warning`, `subtle-error`, `subtle-info` -- **Outline** (renders a status dot — for row/list statuses): `outline-success`, `outline-warning`, `outline-error`, `outline-info`, `outline-neutral` - -Plus `badgeVariants` CVA for custom-styled siblings. -**Example:** - -```tsx -// Primary / lifecycle status — filled semantic variant -Confirmed -// Secondary status (delivery, billing) — outline with status dot -Partially received -// Tag / label — subtle -New -``` - -**Used in patterns:** `list/*` (status column → `outline-*`), `detail/*` (header status). - -**Notes:** Encode status by **semantic color** with a primary/secondary split: a record's **primary/lifecycle status** uses a **filled** semantic variant (one per row / one in a detail header); **secondary statuses** (delivery, billing) use **`outline-*`** (status dot); tags use **`subtle-*`**. Reserve **`default`** (brand) for non-status emphasis. Full rule: **`design-system.md`** → Composition & emphasis rules. - -### `Table` - -> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/table.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/table.md) - -**Import:** `import { Table } from '@tailor-platform/app-shell'` -**Purpose:** Semantic data table with scrollable container. -**API:** Compound — `Table.Root`, `Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Head`, `Table.Cell`, `Table.Caption`. `Table.Root` accepts `containerClassName` for the outer wrapper, `className` for the inner `

`. `Table.Head` and `Table.Cell` accept **`align`** (`"left" | "center" | "right"`, default `"left"`) — prefer this over ad-hoc `className="astw:text-right"` and pair the same alignment on the head and its column's cells (right for numeric/money, center for compact status/icon columns). -**Example:** - -```tsx - - - - Order - Status - Total - - - - {orders.map((o) => ( - - {o.number} - - {o.status} - - {formatMoney(o.total)} - - ))} - - -``` - -**Used in patterns:** `list-dense-scan` hand-built subsets / static tables (`DataTable` is preferred for wired lists). - -**Notes:** - -- **Inside a card?** Pass `containerClassName="astw:px-6"` on `Table.Root` for the horizontal inset, and either drop `Card.Content` (bare list form) or pass `Card.Content className="astw:px-0"` (header+content form). Skipping the `containerClassName` lands the first column flush against the card edge. See the `Card` entry for the two canonical forms and a DON'T example. Dense cell typography (**`text-body-sm`**, **`text-mono`**) → **`design-system.md`** §4 Typography. -- **Whole row is clickable.** Use ` navigate(detailPath)} className="astw:cursor-pointer">`. For keyboard and screen-reader users, also wrap the primary identifier cell content in `` (so the row is reachable via Tab; `Table.Row` is a `` and cannot itself be a Link — wrapping a `` in `` is invalid HTML). **No per-row "View" / "Open" / "→" buttons.** Per-row `Menu` (overflow `…`) is the only allowed per-row action surface and is reserved for non-navigation actions like Archive, Duplicate. - -### `DataTable` - -> Full API: [https://github.com/tailor-platform/app-shell/blob/main/docs/components/data-table.md](https://github.com/tailor-platform/app-shell/blob/main/docs/components/data-table.md) - -**Import:** compound namespace + helpers from `'@tailor-platform/app-shell'`, e.g. `DataTable`, `useDataTable`, `useCollectionVariables`, `createColumnHelper`, and types such as `Column`, `UseDataTableReturn`. - -**Purpose:** Production list screens over GraphQL **connections**. Owns toolbar filter chips (**`DataTable.Filters`** from column `filter` configs), header sort, **`DataTable.Pagination`** (cursor-first; First/Last when `total` is provided), loading skeleton/error row, **`onClickRow`**, **`rowActions`** (kebab column), **`onSelectionChange`** (checkbox column). - -**Primitives:** Builds on low-level **`Table`**; do not reinvent pagination/filters manually unless the dataset is trivial. - -**Shape:** - -```tsx -const { variables, control } = useCollectionVariables({ - params: { pageSize: 20 }, - // tableMetadata: tableMetadata.po, // typed vars when generated -}); - -const table = useDataTable({ - columns, - data: fetching ? undefined : mappedFromQuery, - loading: fetching, - control, - onClickRow: (row) => navigate(detailHref(row)), - // onSelectionChange, rowActions, sort: … -}); - - - - - - - - - -; -``` - -**Column alignment:** each `Column` accepts **`align`** (`"left" | "right"`) applied to both header and body cell. Numeric `type` columns (`"number"`, `"money"`) default to `"right"` automatically so digits align on the decimal place — pass `align="left"` to opt out; everything else defaults to `"left"`. - -**Metadata path:** Prefer `createColumnHelper` + `inferColumns(tableMetadata.order)` (`@tailor-platform/app-shell-sdk-plugin` codegen) when available so enum/datetime/string filters bind to the right editors. - -**Bucket tabs / segmented UX:** AppShell defines **toolbar chips**, not lifecycle tabs. When design places **`Tabs`** (All / Draft / …) inside the card, compose them **above** `DataTable.Root` and synchronize tab-driven bucket state with **`useCollectionVariables`** (`variables.query` / filters)—see **`patterns/list/dense-scan.md`**. - -**Used in patterns:** `list-dense-scan` (preferred for live collections). - -### `Card` - -**Import:** `import { Card } from '@tailor-platform/app-shell'` -**Purpose:** Generic container with header, content, optional action. -**API:** Compound — `Card.Root`, `Card.Header` (props: `title`, `description`, plus children for actions), `Card.Content`. -**Example:** - -```tsx - - - - - {/* anything */} - -``` - -**Used in patterns:** `detail/*` (related-data sections), `form/wizard` (step container). - -**Notes:** - -- **Tables inside a card need TWO co-requisite geometry changes** (token-backed spacing rationale → **`design-system.md`** §4 Spacing): (a) Card stops imposing horizontal padding — drop `Card.Content` for the bare form, or pass `Card.Content className="astw:px-0"` for the header+content form. (b) `Table.Root` provides the inset itself via `containerClassName="astw:px-6"`. Skipping (b) lands the first column flush against the card edge — `Table.Cell`'s intrinsic `astw:first:pl-6` does NOT render reliably in this composition. - -**Bare table-in-card (list pages, no card-level title):** - -```tsx - - {/* … */} - -``` - -**Header + table (detail-page sections like "Line items"):** - -```tsx - - - - {/* … */} - - -``` - -**DON'T — first column lands flush against the card edge:** - -```tsx - - - {/* missing containerClassName="astw:px-6" */} - - -``` - -### `MetricCard` - -**Import:** `import { MetricCard } from '@tailor-platform/app-shell'` -**Purpose:** KPI tile for dashboards or hero metric strip. -**API:** `MetricCardProps` — `title`, `value`, `trend: { direction, value }`, `description`, `icon`. -**Example:** metric tiles are laid out in a `Grid` (never one-per-row): - -```tsx - - } - /> - - {/* …more tiles */} - -``` - -**Used in patterns:** KPI tiles, dashboards, **`detail/*`** metric strips where specs call for them. - -**Notes:** Always wrap metric tiles in **`Grid`** (see the `Grid` entry) — a column of single-width `MetricCard`s stacked one-per-row is an anti-pattern. - -### `DocumentProgressCard` - -**Import:** `import { DocumentProgressCard } from '@tailor-platform/app-shell'` -**Purpose:** Presentational card for a document's lifecycle / fulfilment state — an optional headline percentage, a stacked progress bar, and a status legend. Domain-agnostic and view-only: pass an arbitrary set of `segments` (e.g. shipped / returned / pending) plus an explicit `percent`; derive both in the consumer. -**API:** `DocumentProgressCardProps` — `segments` (required; array of `{ label, value, color }`), `percent?` (0–100, headline shown top-right), `title?`, `legend?` (defaults to `segments`; supply only when the legend should differ from the bar, e.g. overlapping buckets), `total?` (bar denominator; defaults to the sum of segment values — a larger value leaves an unfilled remainder), `className?`. Segment `color`: `indigo | pink | green | amber | red | blue | neutral`. -**Example:** - -```tsx - -``` - -**Used in patterns:** `detail/*` — fulfilment / lifecycle summary cards (e.g. a purchase-order receipt progress) in the main column or right rail. - -### `Avatar` - -**Import:** `import { Avatar } from '@tailor-platform/app-shell'` -**Purpose:** User/entity avatar with fallback initials. -**API:** Compound — `Avatar.Root`, `Avatar.Image`, `Avatar.Fallback`. Plus `avatarVariants` CVA. -**Used in patterns:** `detail/*` (assignee/owner, comments threads). - -### `DescriptionCard` - -**Import:** `import { DescriptionCard } from '@tailor-platform/app-shell'` -**Purpose:** Key/value display for a single record's metadata. -**API:** `DescriptionCardProps` — `data`, `title`, `fields` (array of `{ key, label, render? }`), `columns` (1 | 2), `headerAction`. -**Example:** - -```tsx - {v} }, - { key: "createdAt", label: "Created", render: formatDate }, - ]} -/> -``` - -**Used in patterns:** `detail/hero-with-actions` (body sections). - -### `ActionPanel` - -**Import:** `import { ActionPanel } from '@tailor-platform/app-shell'` -**Purpose:** Right-rail panel listing workflow actions for a detail page (approve, reject, archive, etc.). -**API:** `ActionPanelProps` — `title`, `actions` (array of `{ label, onSelect, variant?, disabled?, hidden? }`), `className`. -**Example:** - -```tsx - -``` - -**Used in patterns:** `detail/hero-with-actions`. - -**Notes:** - -- **Workflow only — never navigation.** `ActionPanel` lists state-change actions on the current record (Approve, Reject, Archive, Generate Variants, Manage Categories, Duplicate). It must NEVER contain back-navigation, breadcrumb-replacement, or "Go to X list" / "Back to X" links — those live in `Layout.Header`'s breadcrumb. If an entry would just navigate the user somewhere else, it does not belong here. - -### `ActivityCard` - -**Import:** `import { ActivityCard } from '@tailor-platform/app-shell'` -**Purpose:** Timeline of events on a record (audit log, status changes, comments). -**API:** Compound — `ActivityCard.Root`, `ActivityCard.Items` (generic over item type), plus `ActivityCardProps`, `ActivityCardItem`, `ActivityCardItemProps`. Items render with timestamp + actor + description. -**Used in patterns:** `detail/hero-with-actions` (right column or bottom section). - -### `Alert` - -**Import:** `import { Alert } from '@tailor-platform/app-shell'` -**Purpose:** Inline, in-page banner for a persistent status message — a form/record error, a warning, or a success/info notice attached to a section. (Transient notifications use `useToast`; blocking confirmations use `Dialog` — see `interaction/confirm`.) -**API:** Compound — `Alert.Root` (`variant`: `neutral | success | warning | error | info`, default `neutral`; a matching icon is rendered automatically; optional `action?: ReactNode`, `dismissible?: boolean`, `onDismiss?: () => void`), `Alert.Title`, `Alert.Description`. -**Example:** - -```tsx - - Couldn't load line items - Check your connection and try again. - -``` - -**Used in patterns:** any data-backed screen's **error** state (inline error + retry) and record-level warnings — the error affordance the composition rules require (**`design-system.md`** → Composition & emphasis rules → States). - -**Notes:** Encode severity by `variant` (semantic color), same discipline as `Badge` — don't use `error` for a routine notice. Pair a retry/next-step control via `action` rather than a separate stray button. - ---- - -## Forms - -### `Form` - -> Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/form.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/form.md) - -**Import:** `import { Form } from '@tailor-platform/app-shell'` -**Purpose:** Form root wired to react-hook-form. Use with `Field`, `Fieldset`, and Zod for validation. -**API:** `FormProps` — `errors`, `actionsRef`, `validationMode`, `noValidate`, plus a namespace exposing form-related sub-helpers. Generic over `FormValues`. -**Example:** see `form/single-page.md`. -**Used in patterns:** all `form/*`. - -### `Field` - -**Import:** `import { Field } from '@tailor-platform/app-shell'` -**Purpose:** Single form field with label + control + error message wiring. -**API:** Compound. Wraps any input control (`Input`, `Select`, `Combobox`, etc.) and binds it to react-hook-form via `name`. -**Used in patterns:** all `form/*`. - -### `Fieldset` - -**Import:** `import { Fieldset } from '@tailor-platform/app-shell'` -**Purpose:** Group related fields under a legend (subtle section header). -**API:** Compound — `Fieldset.Root`, `Fieldset.Legend`. -**Used in patterns:** `form/sectioned`, `form/wizard`. - -### `Input` - -**Import:** `import { Input } from '@tailor-platform/app-shell'` -**Purpose:** Text input. Maps to spec field types: `string`, `email`, `number`, `password`, `tel`, `url`. -**API:** `InputProps` — extends native `` props. -**Used in patterns:** all `form/*`. - -### `Select` - -**Import:** `import { Select } from '@tailor-platform/app-shell'` -**Purpose:** Dropdown for fixed enumerations. -**API:** Compound. Sync version (small option lists). For async/typeahead, see `Combobox` and `Autocomplete`. Type: `SelectAsyncFetcher` for the async variant. -**Accessible name:** inside a `Form` the field label provides it automatically. When used **standalone** (outside a `Form`), pass `aria-label` (or `aria-labelledby` / `id`) — these now forward to the underlying combobox element, so it isn't left named only by its current value. Same applies to `Combobox` and `Autocomplete`. -**Used in patterns:** all `form/*` (enum fields). - -### `Combobox` - -**Import:** `import { Combobox } from '@tailor-platform/app-shell'` -**Purpose:** Single-select with search, supports async data fetching. -**API:** Compound. `ComboboxAsyncFetcher` for paginated/queried option sources (e.g. lookups). -**Used in patterns:** `form/*` (foreign-key lookups). - -### `Autocomplete` - -**Import:** `import { Autocomplete } from '@tailor-platform/app-shell'` -**Purpose:** Free-text input with completion suggestions (multi-select capable). -**API:** Compound. `AutocompleteAsyncFetcher` for async suggestion sources. `ItemGroup` for grouped suggestions. -**Used in patterns:** `form/*` (tags, free-text + suggested values). - ---- - -## Auth & access - -### `AuthProvider` - -**Import:** `import { AuthProvider } from '@tailor-platform/app-shell'` -**Purpose:** Wraps the app to expose auth state via `useAuth`. Mount in `App.tsx`. -**API:** `AuthProviderProps` — `client` (from `createAuthClient`), `autoLogin`, `guardComponent`. -**Used in patterns:** project-level. See `project-setup.md`. - -### `createAuthClient` - -**Import:** `import { createAuthClient, type EnhancedAuthClient } from '@tailor-platform/app-shell'` -**Purpose:** Factory for the auth client, configured with platform URL + client ID. -**API:** `createAuthClient(config: AuthClientConfig): EnhancedAuthClient`. -**Used in patterns:** project-level. - -### `useAuth`, `useAuthSuspense` - -**Import:** `import { useAuth, useAuthSuspense } from '@tailor-platform/app-shell'` -**Purpose:** Read auth state in components. `useAuth` returns nullable state; `useAuthSuspense` suspends until ready (use inside route loaders / suspense boundaries). -**API:** Returns `AuthState` (user, status, login/logout actions). -**Used in patterns:** any pattern needing user identity. - -### `AuthClient` (type) - -**Import:** `import { type AuthClient } from '@tailor-platform/app-shell'` -**Purpose:** Public auth client interface (re-exported from `@tailor-platform/auth-public-client`). - -### `WithGuard` - -**Import:** `import { WithGuard } from '@tailor-platform/app-shell'` -**Purpose:** Permission gate around a UI subtree. Hides, denies, or shows a loading state based on the guard result. -**API:** `WithGuardProps` — `guard: Guard`, `fallback`, `mode: 'hidden' | 'deny'`. -**Used in patterns:** `detail/*` (action gating), `list/*` (row-level action visibility). - -### Guard helpers — `pass`, `hidden`, `redirectTo` - -**Import:** `import { pass, hidden, redirectTo } from '@tailor-platform/app-shell'` -**Purpose:** Return values for guard functions. `pass()` allows, `hidden()` hides, `redirectTo(path)` navigates. -**Used in patterns:** any `Guard` definition. - -### `Guard`, `GuardContext`, `GuardResult` (types) - -Types for authoring guard functions used by `WithGuard` and `appShellPageProps.guards`. - ---- - -## Routing helpers - -### `useNavigate`, `useParams`, `useSearchParams`, `useLocation`, `useRouteError` - -**Import:** `import { useNavigate, useParams, useSearchParams, useLocation, useRouteError } from '@tailor-platform/app-shell'` -**Purpose:** Re-exported from `react-router`. Use the AppShell barrel — never import from `react-router` directly. -**Used in patterns:** all (navigation, route params, query state). - -### `createTypedPaths` - -**Import:** `import { createTypedPaths } from '@tailor-platform/app-shell'` -**Purpose:** Type-safe path builder generated from the route tree. -**API:** `createTypedPaths()` returns a `paths.for(...)` helper. -**Used in patterns:** project-level. See `project-setup.md`. - -### `RouteParams`, `PageComponent`, `PageMeta`, `AppShellPageProps`, `AppShellRegister`, `ContextData` (types) - -Types for declaring page props (`appShellPageProps = { meta, guards }`), reading typed route params, and registering route types globally. See `project-setup.md` for usage. - ---- - -## Hooks - -### `useToast` - -**Import:** `import { useToast } from '@tailor-platform/app-shell'` -**Purpose:** Imperative toast feedback after mutations. -**API:** Returns the `sonner` `toast` function — `toast.success(message)`, `toast.error(message)`, `toast.loading(message)`, plus options for duration, action button, etc. -**Used in patterns:** `interaction/toast`. Called from any mutation handler. - -### `useTheme` - -**Import:** `import { useTheme } from '@tailor-platform/app-shell'` -**Purpose:** Read/write the active theme (light/dark/system). -**API:** Returns `ThemeProviderState`. -**Used in patterns:** any theme-toggle UI. - -### `useAppShell`, `useAppShellConfig`, `useAppShellData` - -**Import:** `import { useAppShell, useAppShellConfig, useAppShellData } from '@tailor-platform/app-shell'` -**Purpose:** Access AppShell context — the resolved `AppShellRegister` data, config, and runtime state. -**Used in patterns:** advanced — when a component needs to read app-wide config or registered resources. - -### `usePageMeta`, `useOverrideBreadcrumb` - -**Import:** `import { usePageMeta, useOverrideBreadcrumb } from '@tailor-platform/app-shell'` -**Purpose:** Read the current page's `meta` (resolved from `appShellPageProps.meta`) or override the breadcrumb title at runtime (e.g. show the order number once data loads). -**Used in patterns:** `detail/*` (dynamic breadcrumbs from loaded entity). - ---- - -## i18n & resource definitions - -### `defineI18nLabels` - -**Import:** `import { defineI18nLabels } from '@tailor-platform/app-shell'` -**Purpose:** Declare i18n labels in a typed way. Returns `I18nLabels`. -**Used in patterns:** project-level for multi-locale apps. - -### `defineModule`, `defineResource` - -**Import:** `import { defineModule, defineResource } from '@tailor-platform/app-shell'` -**Purpose:** Register an app module / a resource (entity with CRUD pages). Wires routes, sidebar, and command palette automatically. -**API:** `defineModule(props: DefineModuleProps): Module`, `defineResource(props: DefineResourceProps): Resource`. -**Used in patterns:** project-level. See `project-setup.md`. - -### `MappedItem`, `Module`, `Resource`, `ResourceComponentProps` (types) - -Types returned/consumed by the define-\* helpers above. - ---- - -## Style helpers - -### `avatarVariants`, `badgeVariants`, `buttonVariants` - -**Import:** `import { avatarVariants, badgeVariants, buttonVariants } from '@tailor-platform/app-shell'` -**Purpose:** CVA (`class-variance-authority`) variant functions backing `Avatar`, `Badge`, `Button`. Use when authoring a custom component that should match an AppShell variant inline. -**Used in patterns:** custom-component fallbacks (see `design-system.md` § "When AppShell doesn't have a component you need"). - -### `ErrorBoundaryComponent` (type) - -**Import:** `import { type ErrorBoundaryComponent } from '@tailor-platform/app-shell'` -**Purpose:** Type for an error boundary component slot in routing. - ---- - -## Keeping this file in sync - -When `@tailor-platform/app-shell` publishes a new version: - -1. Bump `package.json` in the scaffold templates (`templates/scaffold/app/*/frontend/`) to the new version. -2. Update the AppShell version header at the top of this file. -3. Diff exports between old and new: - -```bash - # Run in a scratch dir: - mkdir -p /tmp/appshell-diff && cd /tmp/appshell-diff - npm init -y >/dev/null && npm install --no-save --silent @tailor-platform/app-shell@ - node -e "console.log(Object.keys(require('@tailor-platform/app-shell')).sort().join('\n'))" \ - > new-exports.txt - # Compare against the headings in this file: - grep -E '^### ' \ - packages/erp-kit/skills/erp-kit-app-6-impl-frontend/references/components.md \ - | sed 's/^### //; s/ ,.*//' | sort > current-headings.txt - diff current-headings.txt new-exports.txt -``` - -4. For each new export — add a section using the fixed shape above. -5. For each removed export — delete its section, then grep `references/patterns/` for the component name and update or retire the patterns that cited it. -6. For each changed export (new prop, behavior change) — update the section and add a one-line note under "Notes:". -7. Run `pnpm erp-kit update skills` to regenerate `.agents/skills/`. - -The pattern-citation review check (`erp-kit-app-7-impl-review` → `design-parity.md`) catches drift indirectly: if a pattern lists a component that no longer exists, generated pages will fail the review. diff --git a/packages/core/skills/app-shell-patterns/references/fundamental/design-system.md b/packages/core/skills/app-shell-patterns/references/fundamental/design-system.md deleted file mode 100644 index 78b4d583..00000000 --- a/packages/core/skills/app-shell-patterns/references/fundamental/design-system.md +++ /dev/null @@ -1,379 +0,0 @@ -# Design System - -Authority for **visual-only** decisions — tokens, theme imports, breakpoints intent, `**astw:`** rules, and custom-component conformance. For **React component APIs** (imports, props, JSX composition), pair this file with `**components.md`\*\*; that split avoids duplicating tables and lengthy examples across both docs. - -`@tailor-platform/app-shell` (ERP scaffolds target **≥0.36**; bump your app’s pinned version deliberately) ships an opinionated design system via CSS variables and a `theme.css` import. Use it whether you are consuming AppShell components (most cases) or building a custom component to fill a gap. - -**The tokens are the rails.** Consistency across customers, apps, and AI runs comes from the token system, not from rules written in prose. A hand-typed `#fff` or `padding: 13px` is not a "small deviation" — it is the mechanism by which consistency dies. Every visual value you reach for must resolve to a token in this file. If a token is missing, add one; never inline. - -## 1. Setup - -Authoritative app wiring also lives in `**project-setup.md`**; **scaffold `index.css`\*\* currently does: - -```css -@import "tailwindcss"; -@import "tw-animate-css"; - -@import "@tailor-platform/app-shell/styles"; -@import "@tailor-platform/app-shell/theme.css"; -``` - -Adjust if your App Shell version documents a different barrel filename, but keep this split: - -- `**theme.css**` — design tokens as CSS variables on `:root`. -- `**styles**` (package export) — bundled component styles AppShell ships for primitives. -- `**tailwindcss**` — utilities; token-backed classes (`bg-surface-1`, `text-fg-muted`) resolve through the theme. - -Older docs referred to `app-shell.css`; prefer the `**styles**` import the template uses. - -Tailwind v4 stays CSS-first; minimal `vite` / PostCSS wiring is in `**project-setup.md**`. - -## 2. Theming via CSS variables - -AppShell controls its theme through CSS variables. Override them in `:root` (global) or a scoped selector (per-section, per-tenant, dark mode) to customize. Any token defined in `theme.css` can be overridden after the import. - -```css -:root { - --color-primary: #3b82f6; - --color-background: #ffffff; -} - -[data-theme="dark"] { - --color-background: #0a0a0a; - --color-foreground: #fafafa; -} -``` - -Override at the highest scope where the change applies. Do not duplicate token values across files — change them at the source. - -**Dark mode** is supported via `[data-theme="dark"]` on the root element. AppShell primitives respect it automatically. Custom components inherit dark-mode behaviour for free as long as they reference tokens (`bg-surface-1`, `text-fg-default`) and never inline literal colors. - -## 3. Component styling with data attributes - -AppShell's UI components support data-attribute-based styling, following the [Base UI data attributes](https://base-ui.com/react/handbook/styling#data-attributes) convention. Components expose `data-`\* attributes that reflect their internal state, enabling CSS-only style control without JavaScript: - -```css -/* Style a component based on its state */ -.SwitchThumb[data-checked] { - background-color: green; -} - -.MenuItem[data-highlighted] { - background-color: var(--color-primary); - color: white; -} -``` - -This works with Tailwind as well — **use theme tokens**, not raw Tailwind grays: - -```tsx - -``` - -Check each component's API reference (`components.md`) for the data attributes it exposes. Custom components must follow the same convention (see Section 6). - -## 4. Tokens - -All values below are exposed by `theme.css`. **Use the token, never hand-type the value.** A hex literal or magic px in a PR is a review failure. - -### Color - -Four families. Pick by **intent**, not by visual taste. - -| Family | Token | Use | Tailwind | -| ---------- | ------------------------ | ----------------------------------- | ----------------------------- | -| Surface | `--color-surface-1` | page background | `bg-surface-1` | -| Surface | `--color-surface-2` | card on page | `bg-surface-2` | -| Surface | `--color-surface-3` | nested card on card | `bg-surface-3` | -| Foreground | `--color-fg-default` | primary text | `text-fg-default` | -| Foreground | `--color-fg-muted` | secondary text, descriptions | `text-fg-muted` | -| Foreground | `--color-fg-subtle` | tertiary text, captions, timestamps | `text-fg-subtle` | -| Brand | `--color-primary` | primary buttons, links, focus rings | `bg-primary` / `text-primary` | -| Brand | `--color-primary-hover` | brand hover state | `hover:bg-primary-hover` | -| Brand | `--color-primary-active` | brand pressed state | `active:bg-primary-active` | -| Status | `--color-danger` | destructive actions, errors | `bg-danger` / `text-danger` | -| Status | `--color-warning` | non-blocking caution | `bg-warning` / `text-warning` | -| Status | `--color-success` | confirmations, completed states | `bg-success` / `text-success` | -| Status | `--color-info` | neutral callouts | `bg-info` / `text-info` | - -```tsx -// Good — Button exposes a destructive variant; prefer variants over bolting tokens on className when available -
- - -// Bad — raw colors bypass the theme -
-``` - -Never reach for raw color names. If a status doesn't fit, that's a content problem, not a token problem. - -### Spacing - -Linear scale on a 4px base. Padding, margin, and gap all come from this scale. Tailwind's `p-4`, `gap-2`, `mt-8` resolve to the same tokens. - -| Token | Value | Common use | -| ------------ | ----- | ------------------------- | -| `--space-0` | 0 | reset | -| `--space-1` | 4px | tight icon-text gap | -| `--space-2` | 8px | inline gap, small padding | -| `--space-3` | 12px | row gap, button padding | -| `--space-4` | 16px | card padding, default gap | -| `--space-6` | 24px | section gap | -| `--space-8` | 32px | major section gap | -| `--space-12` | 48px | page section break | -| `--space-16` | 64px | hero spacing | - -```tsx -// Good — scale step -
- -// Bad — magic value -
-``` - -Hand-typing `padding: 13px` is a smell. Round to the nearest scale step; if nothing fits, the layout is wrong, not the scale. - -### Typography - -Named roles. Each token bundles font-size + line-height + weight. Pick by **role**, not by size. Don't set `font-size` and `line-height` independently. - -| Token | Tailwind | Use | -| --------- | -------------- | ---------------------------- | -| `display` | `text-display` | hero / marketing surfaces | -| `h1` | `text-h1` | page title | -| `h2` | `text-h2` | section heading | -| `h3` | `text-h3` | subsection / card title | -| `h4` | `text-h4` | nested heading | -| `body-lg` | `text-body-lg` | emphasised body | -| `body` | `text-body` | default body copy | -| `body-sm` | `text-body-sm` | dense rows, secondary copy | -| `caption` | `text-caption` | timestamps, labels, metadata | -| `mono` | `text-mono` | IDs, code, numbers in tables | - -```tsx -

Section

-

Description copy

-Updated 2h ago -``` - -### Radius - -Pick by component role. A card is always `md`, regardless of its size on screen. - -| Token | Use | -| --------------- | ------------------- | -| `--radius-sm` | inputs, small chips | -| `--radius-md` | cards, buttons | -| `--radius-lg` | modals, sheets | -| `--radius-xl` | large surfaces | -| `--radius-full` | pills, avatars | - -### Elevation - -Higher elevation reads as "more transient" — match the component's lifetime. Never hand-craft a `box-shadow`. - -| Token | Use | -| --------------- | ------------------------ | -| `--elevation-0` | flat surface | -| `--elevation-1` | persistent panel, card | -| `--elevation-2` | sticky bar, hovered card | -| `--elevation-3` | popover, menu | -| `--elevation-4` | modal, dialog, sheet | - -### Motion - -Duration paired with easing. Match motion to the change's lifetime — short events get short durations. - -| Token | ~Duration | Use | -| ----------------- | --------- | ------------------------------ | -| `--motion-fast` | 120ms | hover, focus, button press | -| `--motion-base` | 200ms | state changes (toggle, select) | -| `--motion-slow` | 320ms | entrance, dialog open | -| `--motion-slower` | 500ms | full-page transitions | -| `--ease-out` | — | entrances, reveals | -| `--ease-in-out` | — | symmetric state changes | - -```css -.menu-item { - transition: background-color var(--motion-fast) var(--ease-out); -} - -@media (prefers-reduced-motion: reduce) { - .menu-item { - transition: none; - } -} -``` - -Always wrap motion in `@media (prefers-reduced-motion: reduce)` and collapse to instant or near-instant transitions. AppShell components handle this internally; custom components must do the same. - -### Z-index - -Never invent a z value. If you need a new layer, add a token; never `z-index: 9999`. Popups and overlays share `50` intentionally — sequencing comes from DOM order, not z escalation. - -| Token | Value | Use | -| ------------------ | ----- | ----------------------------- | -| `--z-sidebar` | 10 | persistent sidebar | -| `--z-sidebar-rail` | 10 | sidebar collapsed rail | -| `--z-popup` | 50 | menu, tooltip, popover | -| `--z-overlay` | 50 | modal, sheet, dialog backdrop | - -### Icon sizes - -Pair icon size with the surrounding text scale. Pass via the `size` prop, not raw width/height. - -| Token | Pairs with text | -| ----------- | --------------------- | -| `--icon-sm` | `body-sm`, `caption` | -| `--icon-md` | `body`, `body-lg` | -| `--icon-lg` | `h3`, `h4` | -| `--icon-xl` | `h1`, `h2`, `display` | - -```tsx - -``` - -### Breakpoints - -| Token | Width | -| ----- | ------ | -| `sm` | 640px | -| `md` | 768px | -| `lg` | 1024px | -| `xl` | 1280px | -| `2xl` | 1536px | - -**ERP target is `xl`/`2xl` desktop.** Pages should be designed for those widths first; smaller breakpoints exist for graceful degradation, not parity. Don't waste effort on mobile-first composition unless a screen explicitly calls for it. A list page that collapses gracefully at `md` is fine; a list page redesigned for `sm` is over-investment. - -Two-column **behavior** (right rail stacks under `**lg`**): respect AppShell defaults — do not force side-by-side grids on narrow viewports. `**Layout`column width table** numbers live in`**components.md` → Layout**; reuse them instead of guessing rem values here. - -## 5. The `astw:` prefix - -AppShell exposes **layout / sizing / overflow** escapes on some components via props like `containerClassName`, `contentClassName`, `className` on roots. Prefix those utilities with `**astw:`\*\* so they apply to the wrapper AppShell controls. - -**Do not duplicate full component trees here.** Typical patterns (full `**DataTable`** composition, `**Sheet`+ footer**,`**Table.Root` + card insets**) live in `**components.md`\*\* with JSX you can copy. - -Minimal illustrations — same rules apply to other `*ClassName` hooks: - -```tsx - - -``` - -Rules: - -- `**astw:**` only on AppShell `*ClassName` / root `className` hooks each component exposes. Use **plain** Tailwind (`flex`, `gap-4`, `bg-surface-1`, …) on **your** markup. -- Stick to **layout** utilities (`flex`, `grid`, `max-h-*`, `min-h-0`, `overflow-*`, widths). Avoid painting over internal AppShell padding or colors via `astw:` — prefer an upstream prop or composition change. -- Steps like `**astw:p-4`\*\* still resolve through tokens — never arbitrary `astw:p-[13px]`. - -## 6. When AppShell doesn't have a component you need - -Most ERP screens compose entirely from AppShell primitives. When you hit a gap, work through this decision tree before building anything: - -### Decision tree - -1. **Can you compose existing AppShell primitives?** A "card with metric and trend arrow" is `Card` + `Stat` + `Icon`, not a new component. Compose first. -2. **If composition won't work, is the behavior one-off?** Build it locally under `src/components//` and flag it for the `build-component` skill, which promotes useful customs into AppShell upstream. -3. **If it's already proven reusable across 2+ apps**, skip local entirely — use the `build-component` skill to add it to AppShell directly. - -### Conformance rules (non-negotiable for any custom component) - -- **Tokens only.** No hex literals, no magic px values, no hand-rolled shadows. Every visual property maps to a token from Section 4. -- **Base UI data-attribute pattern for state.** Expose `data-*` attributes that reflect internal state; never style off React props alone. A custom toggle exposes `data-checked`; a custom step indicator exposes `data-active`, `data-completed`, etc. -- **Compose AppShell primitives inside.** If the custom needs a button, use `Button` — not raw `, - ]} - /> - - - - - - - - - SKU - Qty - Total - - - - {order.lineItems.map((item) => ( - - {item.sku} - {item.qty} - ${item.total.toLocaleString()} - - ))} - - - - - - - ✓, onClick: onApprove }, - { key: "cancel", label: "Cancel", icon: , onClick: onCancel }, - ]} - /> - - - - ); -} -``` - -## Constraints - -- **Header carries the single primary CTA + the status `Badge`** — not workflow actions. Workflow actions (Approve, Reject, Archive, …) live in the `ActionPanel`. If the header needs more than ~2 actions, move the overflow into a `Menu`. -- **Never duplicate an action** across `Layout.Header` and `ActionPanel` — each action has exactly one home. -- Every content section MUST sit inside `Card.Root` (or `DescriptionCard`, which already self-contains). Raw divs are not allowed. -- `ActionPanel` is workflow-only — never back-navigation (that lives in the breadcrumb). -- `Table.Root` inside a Card requires `containerClassName="astw:px-6"`. - -## Anti-patterns - -- No status `Badge` on stateful entities — users can't tell where the record is in its lifecycle. -- `ActionPanel` mixed with metadata in the same card — keep workflow separate from descriptive fields. -- Bare `
` sections in the main column — every content section MUST sit inside `Card.Root`. -- `ActionPanel` containing back-navigation (e.g. "Back to Product List") — back navigation lives in `Layout.Header`'s breadcrumb. -- A `Table.Root` inside a Card without `containerClassName="astw:px-6"` — the first column lands flush against the card edge. -- The same action in both `Layout.Header` and the `ActionPanel` — duplicating it makes neither read as canonical. Pick one home. -- Giving every status the same loud weight — the record's primary/lifecycle status is a **filled** semantic badge; secondary statuses (fulfilment, billing) use **`outline-*`** (see `design-system.md` → Composition & emphasis rules). diff --git a/packages/core/skills/app-shell-patterns/references/patterns/form-modal.md b/packages/core/skills/app-shell-patterns/references/patterns/form-modal.md deleted file mode 100644 index 32235a10..00000000 --- a/packages/core/skills/app-shell-patterns/references/patterns/form-modal.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -slug: pattern/form/modal -name: Modal Form -category: pattern -subcategory: form -description: Default form pattern for Create/Edit — keeps user in context on the parent screen -requiredImports: [Dialog, Button, Form, Field, Input] -tags: [form, modal, dialog, create, edit, inline-add] -do: - - 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 -dont: - - Design explicitly calls for a full-page (non-overlay) routed Create or Edit - - Form is complex with 15+ fields or multiple grouped sections — use form/sectioned - - Multi-stage flow with per-step validation — use form/wizard ---- - -# 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 -/* pattern: form/modal */ -import { Button, Dialog, Input, Field } from "@tailor-platform/app-shell"; - -type Props = { - onSave: (data: { label: string; street: string; city: string }) => void; -}; - -export default function ModalForm({ onSave }: Props) { - return ( - - }>Add address - - - Add address - Add a shipping address to this order. - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - onSave({ - label: formData.get("label") as string, - street: formData.get("street") as string, - city: formData.get("city") as string, - }); - }} - > -
- - Label - } /> - - - Street - } /> - - - City - } /> - -
- - }>Cancel - - - -
-
- ); -} -``` - -## Route-driven Variant - -```tsx -/* pattern: form/modal (route-driven variant) */ -import { Button, Dialog, Input, Layout, Field } from "@tailor-platform/app-shell"; - -type Props = { - isCreateOpen: boolean; - onNavigateToCreate: () => void; - onNavigateToList: () => void; - onSave: (data: { name: string }) => void; -}; - -/** - * Route-driven modal: the form has its own URL but renders as a popup - * over the list. Both `/products` and `/products/create` render this - * same component — the parent list stays visible underneath. - */ -export default function ModalFormRouted({ - isCreateOpen, - onNavigateToCreate, - onNavigateToList, - onSave, -}: Props) { - return ( - - - Create - , - ]} - /> - {/* products list — see list/dense-scan */} - - { - if (!open) onNavigateToList(); - }} - > - - - Create product - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - onSave({ name: formData.get("name") as string }); - }} - > -
- - Name - } /> - -
- - - - - -
-
-
- ); -} -``` - -## Constraints - -- Dialog renders full-screen sheet below 1024px; centered max-w-md at 1024–1280px -- Route-driven variant requires both parent path and create/edit path to render the same component -- `onOpenChange` must navigate back — just calling `setOpen(false)` leaves the URL broken - -## Anti-patterns - -- Nesting modals — opening a Dialog from inside another Dialog -- Modal containing a wizard — promote to a routed `form/wizard` -- Save closes the dialog but parent state is stale — wire refetch or optimistic update -- Building a routed Create/Edit page when the design didn't explicitly call for one — modal is the default -- Registering the create path as a separate top-level route — that unmounts the parent list diff --git a/packages/core/skills/app-shell-patterns/references/patterns/form-sectioned.md b/packages/core/skills/app-shell-patterns/references/patterns/form-sectioned.md deleted file mode 100644 index ff4fd720..00000000 --- a/packages/core/skills/app-shell-patterns/references/patterns/form-sectioned.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -slug: pattern/form/sectioned -name: Sectioned Form -category: pattern -subcategory: form -description: Complex form with 15+ fields organized into named fieldset sections -requiredImports: [Layout, Form, Fieldset, Field, Input, Select, Combobox, Button] -tags: [form, sections, fieldset, settings, complex] -do: - - Form is complex with 15+ fields or multiple grouped sections (Identity, Pricing, Inventory) - - Configure-style settings pages with named boundaries -dont: - - Simple Create/Edit — use form/modal (the default) - - Routed Create/Edit at moderate size with no grouping — use form/single-page - - Step-gated validation across stages — use form/wizard ---- - -# pattern/form/sectioned - -## When to Use - -- Form is complex with 15+ fields or multiple grouped sections (Identity, Pricing, Inventory) -- Configure-style settings pages with named boundaries - -## Page Implementation - -```tsx -/* pattern: form/sectioned */ -import { Button, Layout, Input, Select, Field, Fieldset } from "@tailor-platform/app-shell"; - -type Props = { - onSave: (data: Record) => void; - onCancel: () => void; -}; - -export default function SectionedForm({ onSave, onCancel }: Props) { - return ( - - - Cancel - , - , - ]} - /> - -
{ - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const entries: Record = {}; - formData.forEach((value, key) => { - entries[key] = value as string; - }); - onSave(entries); - }} - className="space-y-8" - > - - Identity -
- - Name - } /> - - - SKU - } /> - - - Description - } /> - -
-
- - - Pricing -
- - Price - } /> - - - Currency - - - - Price - } /> - - - Description - } /> - - - - - ); -} -``` - -## Constraints - -- Single column full width below 1024px; single column max-w constrained at 1024–1280px -- Without an explicit routed-page requirement, the answer is `form/modal` -- A `/create` or `/edit` route in the screen spec does NOT require a full-page replacement - -## Anti-patterns - -- Two-column layout for unrelated fields — breaks the linear reading order -- No required-field markers — users can't predict which fields will error -- Errors shown above the form rather than below the offending field -- Choosing this pattern for a Create flow because it's a Create flow — without explicit need, use `form/modal` diff --git a/packages/core/skills/app-shell-patterns/references/patterns/form-wizard.md b/packages/core/skills/app-shell-patterns/references/patterns/form-wizard.md deleted file mode 100644 index 50ddf79d..00000000 --- a/packages/core/skills/app-shell-patterns/references/patterns/form-wizard.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -slug: pattern/form/wizard -name: Wizard Form -category: pattern -subcategory: form -description: Multi-stage create flow with 3-7 steps and per-step validation gates -requiredImports: [Layout, Card, Form, Fieldset, Field, Input, Select, Badge, Button] -tags: [form, wizard, multi-step, import, stepper] -do: - - Multi-stage Create with 3-7 steps - - Import flows (upload → map → validate → confirm) - - Per-step validation gates progression -dont: - - Single screen of fields — use form/modal or form/single-page - - More than 7 steps — split into separate routed pages or reduce scope ---- - -# pattern/form/wizard - -## When to Use - -- Multi-stage Create with 3–7 steps -- Import flows (upload → map → validate → confirm) -- Per-step validation gates progression - -## Page Implementation - -```tsx -/* pattern: form/wizard */ -import { useState } from "react"; -import { Button, Card, Layout, Badge, Input, Field } from "@tailor-platform/app-shell"; - -const STEPS = ["Upload", "Map", "Review", "Done"] as const; - -type Props = { - onComplete: () => void; -}; - -export default function WizardForm({ onComplete }: Props) { - const [currentStep, setCurrentStep] = useState(0); - - const handleNext = () => { - if (currentStep < STEPS.length - 1) { - setCurrentStep(currentStep + 1); - } else { - onComplete(); - } - }; - - const handleBack = () => { - if (currentStep > 0) { - setCurrentStep(currentStep - 1); - } - }; - - return ( - - - - - -
- {STEPS.map((step, i) => ( - - {i + 1}. {step} - - ))} -
-
-
- - - - {currentStep === 0 && ( -
- - CSV file - } /> - -
- )} - {currentStep === 1 && ( -
-

Map CSV columns to product fields

- - Name column - } /> - - - SKU column - } /> - -
- )} - {currentStep === 2 && ( -
-

Review your import — 42 products will be created.

-
- )} - {currentStep === 3 && ( -
-

Import complete! 42 products created.

-
- )} -
-
- -
- - -
-
-
- ); -} -``` - -## Constraints - -- Max 7 steps — more than that causes user abandonment -- Back-navigation must preserve prior step's input -- Validation must be per-step — don't defer until final submit -- Step indicator collapses to "Step 2 of 4" label below 1024px - -## Anti-patterns - -- More than 7 steps — users lose context and abandon -- No back-navigation preservation — pressing Back loses prior step's input -- Validation deferred until final submit — failures force full re-traversal diff --git a/packages/core/skills/app-shell-patterns/references/patterns/interaction-confirm.md b/packages/core/skills/app-shell-patterns/references/patterns/interaction-confirm.md deleted file mode 100644 index d79b0475..00000000 --- a/packages/core/skills/app-shell-patterns/references/patterns/interaction-confirm.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -slug: pattern/interaction/confirm -name: Confirm -category: pattern -subcategory: interaction -description: Confirmation dialog before destructive or irreversible actions -requiredImports: [Dialog, Button, Input] -tags: [dialog, confirm, destructive, delete, irreversible] -do: - - Before a destructive or irreversible action (delete, cancel, void, archive) - - Before bulk actions that affect many records - - When the action's consequence isn't obvious from the trigger -dont: - - Routine reversible actions — use interaction/toast with optional Undo instead - - Form submission for non-destructive create/edit — submit handlers don't need a confirm ---- - -# pattern/interaction/confirm - -## When to Use - -- Before a destructive or irreversible action (delete, cancel, void, archive) -- Before bulk actions that affect many records -- When the action's consequence isn't obvious from the trigger - -## Page Implementation - -```tsx -/* pattern: interaction/confirm */ -import { Button, Dialog } from "@tailor-platform/app-shell"; - -type Props = { - orderId: string; - onDelete: () => void; -}; - -export default function ConfirmDialog({ orderId, onDelete }: Props) { - return ( - - }>Delete - - - Delete order {orderId}? - - This will remove the order and all its line items. This action cannot be undone. - - - - }>Cancel - - - - - ); -} -``` - -## Copy Rules - -- Title is a question, names the object: "Delete order ORD-1234?", "Cancel invoice INV-001?" -- Body names the object and the consequence: what will change, what will be lost, whether it's reversible -- Confirm button verb matches the title: "Delete", "Cancel invoice" — never "OK" or "Yes" - -## Constraints - -- Dialog renders as bottom sheet below 1024px; centered max-w-sm at 1024+ -- Confirm button must use `variant="destructive"` for destructive actions - -## Anti-patterns - -- Vague titles like "Are you sure?" — gives users nothing to evaluate -- Cancel rendered as the primary visual treatment — promotes the wrong default -- Confirm button without `variant="destructive"` for destructive actions — no visual signal diff --git a/packages/core/skills/app-shell-patterns/references/patterns/interaction-multi-select.md b/packages/core/skills/app-shell-patterns/references/patterns/interaction-multi-select.md deleted file mode 100644 index 77ec11a1..00000000 --- a/packages/core/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -slug: pattern/interaction/multi-select -name: Multi Select -category: pattern -subcategory: interaction -description: Floating bottom action bar for bulk operations on selected list rows -requiredImports: [Table, Checkbox, Button, Menu] -tags: [bulk, selection, toolbar, floating-bar, multi-select, batch] -do: - - ANY list page where rows can be acted on in bulk (archive, assign, export, approve, delete) - - Selection is initiated by clicking a leading-column checkbox on rows - - Selection state needs to persist across pagination and filter changes -dont: - - A list where bulk action is genuinely impossible (single-select only) - - A pure picker/selector inside a Dialog whose footer already gates the action - - Destructive bulk action triggered without confirmation — pair with interaction/confirm ---- - -# pattern/interaction/multi-select - -## When to Use - -- ANY list page where rows can be acted on in bulk (archive, assign, export, approve, delete) -- Selection is initiated by clicking a leading-column checkbox on rows -- Selection state needs to persist across pagination and filter changes - -## Layout - -Floating action bar appears the moment selection count goes from 0 → 1, anchored to the bottom of the viewport, centered horizontally, with elevation. It disappears when selection returns to 0. - -``` -+---------------------------------------------------------+ -| Layout.Header title [Filter] [Create] | -+---------------------------------------------------------+ -| Layout.Column | -| Table.Root | -| [x] | Col | Col | Col | Col | -| [x] | row | row | row | row | -| [ ] | row | row | row | row | -| [x] | row | row | row | row | -| | -| +--------------------------------------+ | -| | 3 selected [Archive] [Export] [⋯] [Clear] | | -| +--------------------------------------+ | -+---------------------------------------------------------+ -``` - -## Page Implementation - -```tsx -/* pattern: interaction/multi-select */ -import { useState } from "react"; -import { Button, Table, Menu } from "@tailor-platform/app-shell"; -import type { Order } from "./mock"; - -type Props = { - orders: Order[]; - onArchive: (ids: string[]) => void; - onExport: (ids: string[]) => void; -}; - -export default function MultiSelect({ orders, onArchive, onExport }: Props) { - const [selectedIds, setSelectedIds] = useState>(new Set()); - - const toggleRow = (id: string) => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }; - - const toggleAll = () => { - if (selectedIds.size === orders.length) { - setSelectedIds(new Set()); - } else { - setSelectedIds(new Set(orders.map((o) => o.id))); - } - }; - - const clearSelection = () => setSelectedIds(new Set()); - const selectedCount = selectedIds.size; - - return ( - <> - - - - - 0} - onChange={toggleAll} - aria-label="Select all on page" - /> - - Order # - Status - Total - - - - {orders.map((order) => ( - - - toggleRow(order.id)} - aria-label={`Select ${order.number}`} - /> - - {order.number} - {order.status} - ${order.total.toLocaleString()} - - ))} - - - - {selectedCount > 0 && ( -
- {selectedCount} selected - - - - - - - - Assign owner - Tag - - Delete - - - -
- )} - - ); -} -``` - -## Constraints - -- Count label + Clear button are always present in the bar -- Max 3 inline action buttons — 4th onward collapse behind an overflow `Menu` -- Destructive bulk actions MUST open an `interaction/confirm` dialog -- Filter or sort change must NOT silently clear the selection -- Pagination MUST preserve selection across pages - -## Anti-patterns - -- Placing bulk-action buttons in the page header — bulk actions belong only in the floating bar -- Hiding the bar behind row hover or right-click — bar must be visible when selection > 0 -- Omitting the count or the Clear affordance — both are mandatory -- Letting filter/sort changes silently drop selection -- Firing destructive bulk actions without an interaction/confirm step -- Per-row `Menu` actions as a substitute for bulk actions when selection > 0 diff --git a/packages/core/skills/app-shell-patterns/references/patterns/interaction-toast.md b/packages/core/skills/app-shell-patterns/references/patterns/interaction-toast.md deleted file mode 100644 index 2f069e7f..00000000 --- a/packages/core/skills/app-shell-patterns/references/patterns/interaction-toast.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -slug: pattern/interaction/toast -name: Toast -category: pattern -subcategory: interaction -description: Lightweight feedback after mutations — success or error notifications -requiredImports: [Button] -tags: [toast, feedback, notification, mutation, success, error] -do: - - Feedback after a mutation (create, update, delete) — success or error - - Lightweight async signal that doesn't need to block the UI - - Confirming work the user just initiated -dont: - - Destructive action that has not yet executed — use interaction/confirm - - Long-running blocking operation — use an inline progress UI, not a toast ---- - -# pattern/interaction/toast - -## When to Use - -- Feedback after a mutation (create, update, delete) — success or error -- Lightweight async signal that doesn't need to block the UI -- Confirming work the user just initiated - -## Page Implementation - -```tsx -/* pattern: interaction/toast */ -import { Button, useToast } from "@tailor-platform/app-shell"; - -type Props = { - orderId: string; - onApprove: () => Promise; -}; - -export default function ToastExample({ orderId, onApprove }: Props) { - const toast = useToast(); - - const handleApprove = async () => { - try { - await onApprove(); - toast.success(`Order ${orderId} approved`); - } catch { - toast.error("Failed to approve order. Try again."); - } - }; - - return ; -} -``` - -## Copy Rules - -- Success: name what happened, including the object identifier. "Order #1234 created", "Product archived". -- Error: state what failed and why. "Failed to save: SKU already exists", "Couldn't archive product: network error". -- Avoid generic messages like "Success" or "Something went wrong". - -## Constraints - -- Toast renders as a top-right overlay; mobile (<1024) anchors to bottom-center -- Stack max one visible at a time; replace prior toast on new emission -- Success auto-dismisses after 3s; Error is sticky (no auto-dismiss) - -## Anti-patterns - -- Toast on every navigation — creates noise; reserve for mutation feedback -- More than one toast stacking — replace, don't accumulate -- Blocking the UI on a toast — toasts are non-modal by definition diff --git a/packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md b/packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md deleted file mode 100644 index dfbe8b34..00000000 --- a/packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -slug: pattern/list/dense-scan -name: Dense Scan List -category: pattern -subcategory: list -description: High-density scannable list backed by GraphQL connections with DataTable, sort, filters, and pagination -requiredImports: - [ - DataTable, - useDataTable, - useCollectionVariables, - createColumnHelper, - Layout, - Card, - Button, - Badge, - Link, - Menu, - Tabs, - ] -tags: [table, bulk-action, filter, pagination, datatable, connection] -do: - - Browsing many records of one entity type (orders, POs, products) with GraphQL pagination - - Operators sort, filter, and select rows; row click navigates to detail - - Optionally a bucket control (Tabs) aligned to one categorical dimension (status, type) -dont: - - Side-by-side match/reconcile views comparing two grids - - A tiny/static list where DataTable would be heavyweight — use Table.Root manually - - Inline editable cells — use pattern/detail or pattern/form/modal instead ---- - -# 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 - -```tsx -import type { Column } from "@tailor-platform/app-shell"; -import { Badge } from "@tailor-platform/app-shell"; - -export type Order = { - id: string; - orderNumber: string; - customer: string; - status: "draft" | "confirmed" | "shipped" | "delivered"; - amount: number; - createdAt: string; -}; - -// Primary status column — filled semantic variants (one per row). -// Secondary status columns (e.g. delivery, billing) use outline-* instead. -const statusVariant = { - draft: "neutral", - confirmed: "info", - shipped: "warning", - delivered: "success", -} as const; - -export const columns: Column[] = [ - { label: "Order #", accessor: (row) => row.orderNumber }, - { label: "Customer", accessor: (row) => row.customer }, - { - label: "Status", - render: (row) => {row.status}, - }, - { - // Numeric columns right-align so digits line up. `type: "money" | "number"` - // auto-right; with a custom `render` set `align` explicitly. - label: "Amount", - align: "right", - render: (row) => `${row.amount.toLocaleString()}`, - }, - { label: "Created", accessor: (row) => row.createdAt }, -]; -``` - -## Page Implementation - -```tsx -/* 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 - , - ]} - /> - - - - - - - - - - - - - ); -} -``` - -## 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`; no per-row "View" / "Open" buttons -- Per-row `Menu` (overflow `…`) is reserved for non-navigation actions (Archive, Duplicate, Delete) - -## 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 From 3472061f408886cce83848efaf8f774a1fc2a5bc Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 10 Jul 2026 11:01:11 +0900 Subject: [PATCH 3/7] chore: move skill generation to root publish flow --- catalogue/package.json | 2 +- catalogue/scripts/check-packed-skills.mjs | 87 +++++++++++------------ package.json | 3 +- packages/core/package.json | 1 - 4 files changed, 43 insertions(+), 50 deletions(-) diff --git a/catalogue/package.json b/catalogue/package.json index eeaf4767..d48227a1 100644 --- a/catalogue/package.json +++ b/catalogue/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "build": "node scripts/generate-skill.mjs", - "test": "node scripts/check-packed-skills.mjs", + "test": "node scripts/generate-skill.mjs && node scripts/check-packed-skills.mjs", "type-check": "tsc --noEmit" }, "dependencies": { diff --git a/catalogue/scripts/check-packed-skills.mjs b/catalogue/scripts/check-packed-skills.mjs index ff50cdcf..3c375f2d 100644 --- a/catalogue/scripts/check-packed-skills.mjs +++ b/catalogue/scripts/check-packed-skills.mjs @@ -1,15 +1,13 @@ -import { execFile } from "node:child_process"; -import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { promisify } from "node:util"; +import { readFile, readdir } from "node:fs/promises"; +import { join, relative } from "node:path"; import { fileURLToPath } from "node:url"; -const execFileAsync = promisify(execFile); const __dirname = fileURLToPath(new URL(".", import.meta.url)); const catalogueRoot = join(__dirname, ".."); const repoRoot = join(catalogueRoot, ".."); const expectedManifestPath = join(catalogueRoot, "expected-skills-files.txt"); +const corePackageRoot = join(repoRoot, "packages", "core"); +const skillsRoot = join(corePackageRoot, "skills"); function parseManifest(text) { return text @@ -29,55 +27,50 @@ function diffLists(expected, actual) { }; } -async function main() { - const tempDir = await mkdtemp(join(tmpdir(), "app-shell-pack-")); - - try { - await execFileAsync( - "pnpm", - ["--filter", "@tailor-platform/app-shell", "pack", "--pack-destination", tempDir], - { cwd: repoRoot }, - ); +async function listFiles(dir) { + const entries = await readdir(dir, { withFileTypes: true }); + const files = []; - const tarballs = (await readdir(tempDir)).filter((file) => file.endsWith(".tgz")); - if (tarballs.length !== 1) { - throw new Error(`Expected exactly one tarball, found ${tarballs.length}`); + for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await listFiles(fullPath))); + continue; } + files.push(fullPath); + } - const tarballPath = join(tempDir, tarballs[0]); - const [{ stdout: tarList }, expectedManifest] = await Promise.all([ - execFileAsync("tar", ["-tf", tarballPath], { cwd: repoRoot }), - readFile(expectedManifestPath, "utf8"), - ]); + return files; +} - const actualSkillsFiles = tarList - .split("\n") - .map((line) => line.trim()) - .filter((line) => line.startsWith("package/skills/") && !line.endsWith("/")) - .map((line) => line.replace(/^package\//, "")) - .sort(); - const expectedSkillsFiles = parseManifest(expectedManifest); +async function main() { + const [expectedManifest, actualFiles] = await Promise.all([ + readFile(expectedManifestPath, "utf8"), + listFiles(skillsRoot), + ]); - const { missing, extra } = diffLists(expectedSkillsFiles, actualSkillsFiles); - if (missing.length === 0 && extra.length === 0) { - console.log(`Packed skills manifest matches expected (${actualSkillsFiles.length} files).`); - return; - } + const actualSkillsFiles = actualFiles + .map((filePath) => relative(corePackageRoot, filePath)) + .sort(); + const expectedSkillsFiles = parseManifest(expectedManifest); - console.error("Packed skills manifest does not match catalogue/expected-skills-files.txt"); - if (missing.length > 0) { - console.error("\nMissing:"); - for (const file of missing) console.error(` - ${file}`); - } - if (extra.length > 0) { - console.error("\nExtra:"); - for (const file of extra) console.error(` - ${file}`); - } + const { missing, extra } = diffLists(expectedSkillsFiles, actualSkillsFiles); + if (missing.length === 0 && extra.length === 0) { + console.log(`Skills manifest matches expected (${actualSkillsFiles.length} files).`); + return; + } - process.exitCode = 1; - } finally { - await rm(tempDir, { recursive: true, force: true }); + console.error("Skills manifest does not match catalogue/expected-skills-files.txt"); + if (missing.length > 0) { + console.error("\nMissing:"); + for (const file of missing) console.error(` - ${file}`); + } + if (extra.length > 0) { + console.error("\nExtra:"); + for (const file of extra) console.error(` - ${file}`); } + + process.exitCode = 1; } main().catch((error) => { diff --git a/package.json b/package.json index acd2f303..52bc8c6d 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,14 @@ "scripts": { "dev": "turbo watch dev --filter='./examples/*'", "build": "turbo build", + "generate:skills": "node catalogue/scripts/generate-skill.mjs", "type-check": "turbo type-check", "lint": "turbo lint", "test": "turbo test", "fmt": "oxfmt", "fmt:check": "oxfmt --check", "changeset:create": "changeset", - "changeset:publish": "pnpm build && changeset publish", + "changeset:publish": "pnpm build && pnpm generate:skills && changeset publish", "prepare": "lefthook install" }, "devDependencies": { diff --git a/packages/core/package.json b/packages/core/package.json index 41ef2c82..327e83ab 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -45,7 +45,6 @@ "scripts": { "dev": "vite build --watch --mode development", "build": "vite build", - "prepack": "node ../../catalogue/scripts/generate-skill.mjs", "lint": "oxlint -c .oxlintrc.jsonc", "test": "vitest run", "type-check": "tsc --incremental" From 94acde6fd564939012e1045bd80453c01d0b5def Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 10 Jul 2026 11:43:44 +0900 Subject: [PATCH 4/7] chore: simplify generated skills validation --- catalogue/package.json | 2 +- .../{check-packed-skills.mjs => check-generated-skills.mjs} | 0 package.json | 3 +-- turbo.json | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) rename catalogue/scripts/{check-packed-skills.mjs => check-generated-skills.mjs} (100%) diff --git a/catalogue/package.json b/catalogue/package.json index d48227a1..8ec94d5c 100644 --- a/catalogue/package.json +++ b/catalogue/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "build": "node scripts/generate-skill.mjs", - "test": "node scripts/generate-skill.mjs && node scripts/check-packed-skills.mjs", + "test": "node scripts/check-generated-skills.mjs", "type-check": "tsc --noEmit" }, "dependencies": { diff --git a/catalogue/scripts/check-packed-skills.mjs b/catalogue/scripts/check-generated-skills.mjs similarity index 100% rename from catalogue/scripts/check-packed-skills.mjs rename to catalogue/scripts/check-generated-skills.mjs diff --git a/package.json b/package.json index 52bc8c6d..acd2f303 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,13 @@ "scripts": { "dev": "turbo watch dev --filter='./examples/*'", "build": "turbo build", - "generate:skills": "node catalogue/scripts/generate-skill.mjs", "type-check": "turbo type-check", "lint": "turbo lint", "test": "turbo test", "fmt": "oxfmt", "fmt:check": "oxfmt --check", "changeset:create": "changeset", - "changeset:publish": "pnpm build && pnpm generate:skills && changeset publish", + "changeset:publish": "pnpm build && changeset publish", "prepare": "lefthook install" }, "devDependencies": { diff --git a/turbo.json b/turbo.json index 3129dd37..b97eb025 100644 --- a/turbo.json +++ b/turbo.json @@ -15,7 +15,7 @@ }, "lint": {}, "test": { - "dependsOn": ["^build"] + "dependsOn": ["build", "^build"] }, "fmt": { "cache": false From 2ad63e30cec9e527eae79a245288531f3ecea5fa Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 10 Jul 2026 11:47:26 +0900 Subject: [PATCH 5/7] docs: clarify generated skills check script --- catalogue/scripts/check-generated-skills.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/catalogue/scripts/check-generated-skills.mjs b/catalogue/scripts/check-generated-skills.mjs index 3c375f2d..d330556e 100644 --- a/catalogue/scripts/check-generated-skills.mjs +++ b/catalogue/scripts/check-generated-skills.mjs @@ -1,3 +1,10 @@ +/** + * Verifies that the generated skills tree under packages/core/skills matches + * the expected file manifest from catalogue/expected-skills-files.txt. + * + * Purpose: keep generated skills out of git while still catching drift between + * catalogue inputs and the generated output during test runs. + */ import { readFile, readdir } from "node:fs/promises"; import { join, relative } from "node:path"; import { fileURLToPath } from "node:url"; From 589f357ff1333907891683b46ff07aa3ba1c0e05 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 10 Jul 2026 11:58:13 +0900 Subject: [PATCH 6/7] kick tests From 1cabdb9e444ce54df00eebb469bd5b8173d24ea4 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 10 Jul 2026 12:18:04 +0900 Subject: [PATCH 7/7] test: stabilize combobox and autocomplete snapshots --- .../__snapshots__/src__components__autocomplete.test.tsx.snap | 2 +- .../core/__snapshots__/src__components__combobox.test.tsx.snap | 2 +- packages/core/src/components/autocomplete.test.tsx | 3 +++ packages/core/src/components/combobox.test.tsx | 3 +++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/__snapshots__/src__components__autocomplete.test.tsx.snap b/packages/core/__snapshots__/src__components__autocomplete.test.tsx.snap index 35b2f788..1f6d60df 100644 --- a/packages/core/__snapshots__/src__components__autocomplete.test.tsx.snap +++ b/packages/core/__snapshots__/src__components__autocomplete.test.tsx.snap @@ -2,6 +2,6 @@ exports[`Autocomplete.Parts > snapshots > closed autocomplete with placeholder 1`] = `"
"`; -exports[`Autocomplete.Parts > snapshots > open autocomplete 1`] = `"
"`; +exports[`Autocomplete.Parts > snapshots > open autocomplete 1`] = `"
"`; exports[`Autocomplete.Parts > snapshots > with groups 1`] = `""`; diff --git a/packages/core/__snapshots__/src__components__combobox.test.tsx.snap b/packages/core/__snapshots__/src__components__combobox.test.tsx.snap index 27d77a57..bca3c0bc 100644 --- a/packages/core/__snapshots__/src__components__combobox.test.tsx.snap +++ b/packages/core/__snapshots__/src__components__combobox.test.tsx.snap @@ -2,7 +2,7 @@ exports[`Combobox.Parts > snapshots > closed combobox with placeholder 1`] = `""`; -exports[`Combobox.Parts > snapshots > open combobox 1`] = `"
"`; +exports[`Combobox.Parts > snapshots > open combobox 1`] = `"
"`; exports[`Combobox.Parts > snapshots > with InputGroup, Clear, and Trigger 1`] = `"
"`; diff --git a/packages/core/src/components/autocomplete.test.tsx b/packages/core/src/components/autocomplete.test.tsx index f5e848a0..63c0a3b6 100644 --- a/packages/core/src/components/autocomplete.test.tsx +++ b/packages/core/src/components/autocomplete.test.tsx @@ -55,6 +55,9 @@ describe("Autocomplete.Parts", () => { await waitFor(() => { expect(screen.getByText("Apple")).toBeDefined(); }); + await waitFor(() => { + expect(screen.getByRole("status").textContent).toBe("No suggestions"); + }); expect(baseElement.innerHTML).toMatchSnapshot(); }); diff --git a/packages/core/src/components/combobox.test.tsx b/packages/core/src/components/combobox.test.tsx index 4fb537ee..1b977101 100644 --- a/packages/core/src/components/combobox.test.tsx +++ b/packages/core/src/components/combobox.test.tsx @@ -51,6 +51,9 @@ describe("Combobox.Parts", () => { await waitFor(() => { expect(screen.getByText("Apple")).toBeDefined(); }); + await waitFor(() => { + expect(screen.getByRole("status").textContent).toBe("No results found"); + }); expect(baseElement.innerHTML).toMatchSnapshot(); });