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..8ec94d5c 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-generated-skills.mjs", "type-check": "tsc --noEmit" }, "dependencies": { diff --git a/catalogue/scripts/check-generated-skills.mjs b/catalogue/scripts/check-generated-skills.mjs new file mode 100644 index 00000000..d330556e --- /dev/null +++ b/catalogue/scripts/check-generated-skills.mjs @@ -0,0 +1,86 @@ +/** + * 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"; + +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 + .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 listFiles(dir) { + const entries = await readdir(dir, { withFileTypes: true }); + const files = []; + + 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); + } + + return files; +} + +async function main() { + const [expectedManifest, actualFiles] = await Promise.all([ + readFile(expectedManifestPath, "utf8"), + listFiles(skillsRoot), + ]); + + const actualSkillsFiles = actualFiles + .map((filePath) => relative(corePackageRoot, filePath)) + .sort(); + const expectedSkillsFiles = parseManifest(expectedManifest); + + const { missing, extra } = diffLists(expectedSkillsFiles, actualSkillsFiles); + if (missing.length === 0 && extra.length === 0) { + console.log(`Skills manifest matches expected (${actualSkillsFiles.length} files).`); + return; + } + + 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) => { + 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/__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/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 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(); }); 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