diff --git a/.github/workflows/ci-packages.yaml b/.github/workflows/ci-packages.yaml index 815c8351..5d65094c 100644 --- a/.github/workflows/ci-packages.yaml +++ b/.github/workflows/ci-packages.yaml @@ -6,6 +6,7 @@ on: - "packages/**" - "examples/**" - "docs/**" + - "catalogue/**" - "package.json" - "pnpm-lock.yaml" - "pnpm-workspace.yaml" diff --git a/catalogue/README.md b/catalogue/README.md new file mode 100644 index 00000000..e0998bfc --- /dev/null +++ b/catalogue/README.md @@ -0,0 +1,17 @@ +# app-shell-catalogue + +UI pattern catalogue for `@tailor-platform/app-shell`. Contains reference implementations and generates skill documentation for AI coding agents. + +## Structure + +- `src/fundamental/` — Foundational references (components, design system, GraphQL) +- `src/pattern/` — UI pattern implementations (list, detail, form, interaction) +- `scripts/` — Generation tooling + +## Generate Skills + +```bash +pnpm build +``` + +Outputs skill files to `packages/core/skills/app-shell-patterns/` for distribution via npm. diff --git a/catalogue/package.json b/catalogue/package.json new file mode 100644 index 00000000..93e6c51b --- /dev/null +++ b/catalogue/package.json @@ -0,0 +1,20 @@ +{ + "name": "app-shell-catalogue", + "private": true, + "type": "module", + "scripts": { + "build": "node scripts/generate-skill.mjs", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@tailor-platform/app-shell": "workspace:*", + "gray-matter": "4.0.3", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "typescript": "catalog:" + } +} diff --git a/catalogue/scripts/SKILL.template.md b/catalogue/scripts/SKILL.template.md new file mode 100644 index 00000000..85791193 --- /dev/null +++ b/catalogue/scripts/SKILL.template.md @@ -0,0 +1,36 @@ +--- +name: app-shell-patterns +description: UI pattern catalog for building pages with @tailor-platform/app-shell components +--- + +# 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. + +{{FUNDAMENTAL_TABLE}} + +## Available Patterns + +{{PATTERNS_TABLE}} + +## 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 diff --git a/catalogue/scripts/generate-skill.mjs b/catalogue/scripts/generate-skill.mjs new file mode 100644 index 00000000..e19d404d --- /dev/null +++ b/catalogue/scripts/generate-skill.mjs @@ -0,0 +1,276 @@ +/** + * generate-skill.mjs + * + * Reads catalogue entry files, resolves markers + * by embedding the referenced file content as fenced code blocks, + * and outputs: + * - skills/app-shell-patterns/SKILL.md (index table) + * - skills/app-shell-patterns/references//.md (per-entry docs) + */ + +import { readdir, readFile, writeFile, mkdir, copyFile } from "node:fs/promises"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import matter from "gray-matter"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const catalogueRoot = join(__dirname, ".."); +const repoRoot = join(catalogueRoot, ".."); +const skillsDir = join(repoRoot, "packages", "core", "skills", "app-shell-patterns"); +const referencesDir = join(skillsDir, "references"); + +/** + * Category definitions. To add a new category, append an entry here + * and add a corresponding {{}} placeholder to SKILL.template.md. + * + * - entryFile: marker filename to search recursively (e.g. "PATTERN.md"). + * If null, all .md files in the category directory are copied as-is. + */ +const CATEGORIES = [ + { + name: "fundamental", + entryFile: null, + outputDir: "fundamental", + templateKey: "FUNDAMENTAL_TABLE", + }, + { + name: "pattern", + entryFile: "PATTERN.md", + outputDir: "patterns", + templateKey: "PATTERNS_TABLE", + }, +]; + +/** + * Resolve markers in markdown body. + * Reads the referenced file relative to the PATTERN.md directory + * and replaces the marker with a fenced code block. + */ +async function resolveSourceMarkers(body, patternDir) { + const sourceRegex = //g; + let result = body; + let match; + + // Collect all matches first to avoid issues with async replacement + const replacements = []; + while ((match = sourceRegex.exec(body)) !== null) { + const filename = match[1]; + const filePath = join(patternDir, filename); + try { + const content = await readFile(filePath, "utf-8"); + const ext = filename.split(".").pop(); + replacements.push({ + original: match[0], + replacement: `\`\`\`${ext}\n${content.trim()}\n\`\`\``, + }); + } catch (err) { + console.warn(`Warning: Could not read ${filePath}: ${err.message}`); + } + } + + for (const { original, replacement } of replacements) { + result = result.replace(original, replacement); + } + + return result; +} + +/** + * Recursively find files matching the given filename in a directory tree. + */ +async function findEntryFiles(dir, filename) { + const results = []; + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return results; + } + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...(await findEntryFiles(fullPath, filename))); + } else if (entry.name === filename) { + results.push(fullPath); + } + } + return results; +} + +/** + * Generate a slug suitable for filenames. + * e.g., "pattern/list/dense-scan" → "list-dense-scan" + */ +function slugToFilename(slug) { + const parts = slug.split("/"); + const categoryNames = CATEGORIES.map((c) => c.name); + if (parts.length > 1 && categoryNames.includes(parts[0])) { + parts.shift(); + } + return parts.join("-"); +} + +/** + * Process a single category: find entry files, parse/copy them, + * and write output files. + * + * When sourceFile is set, entries are discovered by recursive search + * for that filename, parsed for frontmatter, and source markers are resolved. + * When sourceFile is null, all .md files in the category dir are copied as-is. + */ +async function processCategory(category) { + const categoryDir = join(catalogueRoot, "src", category.name); + const outputDir = join(referencesDir, category.outputDir); + await mkdir(outputDir, { recursive: true }); + + if (category.entryFile === null) { + return processCopiedCategory(category, categoryDir, outputDir); + } + return processEntryCategory(category, categoryDir, outputDir); +} + +async function processCopiedCategory(category, categoryDir, outputDir) { + const files = await findMarkdownFiles(categoryDir); + for (const filePath of files) { + const filename = filePath.split("/").pop(); + const outputPath = join(outputDir, filename); + await copyFile(filePath, outputPath); + console.log(` Generated references/${category.outputDir}/${filename}`); + } + return { category, files }; +} + +async function processEntryCategory(category, categoryDir, outputDir) { + const entryFiles = await findEntryFiles(categoryDir, category.entryFile); + const entries = []; + + for (const filePath of entryFiles) { + const content = await readFile(filePath, "utf-8"); + const { data: meta, content: body, matter: rawFrontmatter } = matter(content); + if (!meta.slug) { + console.warn(`Warning: No slug in frontmatter of ${filePath}`); + continue; + } + + const entryDir = dirname(filePath); + const resolvedBody = await resolveSourceMarkers(body.trim(), entryDir); + + entries.push({ meta, body: resolvedBody, rawFrontmatter }); + } + + for (const { meta, body, rawFrontmatter } of entries) { + const filename = slugToFilename(meta.slug) + ".md"; + const outputPath = join(outputDir, filename); + const output = `---\n${rawFrontmatter.trim()}\n---\n\n${body}\n`; + await writeFile(outputPath, output); + console.log(` Generated references/${category.outputDir}/${filename}`); + } + + return { category, entries }; +} + +async function main() { + // Process all categories + const results = []; + for (const category of CATEGORIES) { + const result = await processCategory(category); + results.push(result); + } + + // Generate SKILL.md index + const skillMd = await generateSkillIndex(results); + await writeFile(join(skillsDir, "SKILL.md"), skillMd); + console.log(` Generated SKILL.md`); + + const total = results.reduce((sum, r) => sum + (r.entries?.length ?? r.files?.length ?? 0), 0); + console.log( + `\nDone: ${total} file(s) across ${results.length} categories → skills/app-shell-patterns/`, + ); +} + +/** + * Generate an index table for entry-based categories (with frontmatter). + */ +function generateEntryTable(entries, outputDir) { + if (entries.length === 0) return ""; + + // Group entries by subcategory + const grouped = {}; + for (const { meta } of entries) { + 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`; + } + + return table.trim(); +} + +/** + * Generate an index table for copied categories (plain .md files). + */ +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(); +} + +async function findMarkdownFiles(dir) { + const results = []; + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return results; + } + for (const entry of entries) { + if (!entry.isDirectory() && entry.name.endsWith(".md")) { + results.push(join(dir, entry.name)); + } + } + return results; +} + +function generateTable(result) { + if (result.entries) { + return generateEntryTable(result.entries, result.category.outputDir); + } + return generateCopiedTable(result.files, result.category.outputDir); +} + +async function generateSkillIndex(results) { + const templatePath = join(__dirname, "SKILL.template.md"); + let template = await readFile(templatePath, "utf-8"); + + for (const result of results) { + const table = generateTable(result); + template = template.replace(`{{${result.category.templateKey}}}`, table); + } + + return template; +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/catalogue/src/fundamental/components.md b/catalogue/src/fundamental/components.md new file mode 100644 index 00000000..8b5e935c --- /dev/null +++ b/catalogue/src/fundamental/components.md @@ -0,0 +1,704 @@ +# AppShell Components + +> **AppShell version:** `0.36.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`: `columns` (number, default auto-detected from children), `gap`, `title`, `actions`, `className`, `style`. + +**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/*`). + +### `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 config, header config, content slot. Used in `App.tsx`. +**Used in patterns:** consumed by AppShell init, not directly by page patterns. See `project-setup.md`. + +### `DefaultSidebar`, `SidebarGroup`, `SidebarItem`, `SidebarSeparator` + +**Import:** `import { DefaultSidebar, SidebarGroup, SidebarItem, SidebarSeparator } from '@tailor-platform/app-shell'` +**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`. + +### `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). + +--- + +## 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`: `default | success | warning | neutral | error | destructive`. Plus `badgeVariants` CVA for custom-styled siblings. +**Example:** + +```tsx +Active +``` + +**Used in patterns:** `list/*` (status column), `detail/*` (header status). + +### `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 ``. +**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: … +}); + + + + + + + + + +; +``` + +**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:** + +```tsx +} +/> +``` + +**Used in patterns:** KPI tiles, dashboards, **`detail/*`** metric strips where specs call for them. + +### `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). + +--- + +## 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. +**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, loader }`), 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/catalogue/src/fundamental/design-system.md b/catalogue/src/fundamental/design-system.md new file mode 100644 index 00000000..48bbed82 --- /dev/null +++ b/catalogue/src/fundamental/design-system.md @@ -0,0 +1,355 @@ +# 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 }, + ]} + /> + + + + ); +} diff --git a/catalogue/src/pattern/detail/hero-with-actions/mock.ts b/catalogue/src/pattern/detail/hero-with-actions/mock.ts new file mode 100644 index 00000000..5fe37371 --- /dev/null +++ b/catalogue/src/pattern/detail/hero-with-actions/mock.ts @@ -0,0 +1,56 @@ +export type LineItem = { + id: string; + sku: string; + qty: number; + total: number; +}; + +export type ActivityItem = { + id: string; + actor: { name: string }; + description: string; + timestamp: Date; +}; + +export type Order = { + id: string; + number: string; + status: string; + customer: string; + total: string; + lineItems: LineItem[]; + activities: ActivityItem[]; +}; + +export const mockOrder: Order = { + id: "1", + number: "ORD-1234", + status: "Confirmed", + customer: "Acme Corp", + total: "$4,500.00", + lineItems: [ + { id: "li-1", sku: "SKU-001", qty: 10, total: 2500 }, + { id: "li-2", sku: "SKU-002", qty: 5, total: 1200 }, + { id: "li-3", sku: "SKU-003", qty: 3, total: 800 }, + ], + activities: [ + { + id: "a-1", + actor: { name: "Hanna" }, + description: "confirmed the order", + timestamp: new Date("2025-01-20T10:30:00"), + }, + { + id: "a-2", + actor: { name: "System" }, + description: "Status changed to CONFIRMED", + timestamp: new Date("2025-01-20T10:30:00"), + }, + { + id: "a-3", + actor: { name: "Alex" }, + description: "created the order", + timestamp: new Date("2025-01-19T14:00:00"), + }, + ], +}; diff --git a/catalogue/src/pattern/form/modal/PATTERN.md b/catalogue/src/pattern/form/modal/PATTERN.md new file mode 100644 index 00000000..0c1c454e --- /dev/null +++ b/catalogue/src/pattern/form/modal/PATTERN.md @@ -0,0 +1,49 @@ +--- +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 + + + +## Route-driven Variant + + + +## 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/catalogue/src/pattern/form/modal/modal-form-routed.tsx b/catalogue/src/pattern/form/modal/modal-form-routed.tsx new file mode 100644 index 00000000..96b5c9d6 --- /dev/null +++ b/catalogue/src/pattern/form/modal/modal-form-routed.tsx @@ -0,0 +1,68 @@ +/* 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 + } /> + +
+ + + + + +
+
+
+ ); +} diff --git a/catalogue/src/pattern/form/modal/modal-form.tsx b/catalogue/src/pattern/form/modal/modal-form.tsx new file mode 100644 index 00000000..2dbe9fd3 --- /dev/null +++ b/catalogue/src/pattern/form/modal/modal-form.tsx @@ -0,0 +1,50 @@ +/* pattern: form/modal */ +import { Button, Dialog, Input, Field } from "@tailor-platform/app-shell"; + +type Props = { + onSave: (data: { label: string; street: string; city: string }) => void; +}; + +export default function ModalForm({ onSave }: Props) { + return ( + + }>Add address + + + Add address + Add a shipping address to this order. + +
{ + e.preventDefault(); + const formData = new FormData(e.currentTarget); + onSave({ + label: formData.get("label") as string, + street: formData.get("street") as string, + city: formData.get("city") as string, + }); + }} + > +
+ + Label + } /> + + + Street + } /> + + + City + } /> + +
+ + }>Cancel + + + +
+
+ ); +} diff --git a/catalogue/src/pattern/form/sectioned/PATTERN.md b/catalogue/src/pattern/form/sectioned/PATTERN.md new file mode 100644 index 00000000..612e0dbc --- /dev/null +++ b/catalogue/src/pattern/form/sectioned/PATTERN.md @@ -0,0 +1,39 @@ +--- +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 + + + +## Constraints + +- Max ~6 sections — more than that is too hard to scan; promote to `form/wizard` +- Required-marker convention must be consistent across all sections +- Section legends must match anchor-nav labels + +## Anti-patterns + +- More than ~6 sections — hard to scan; promote to `form/wizard` +- Required-marker convention varies between sections — pick one rule and apply everywhere +- Section legends that don't match anchor-nav labels diff --git a/catalogue/src/pattern/form/sectioned/sectioned-form.tsx b/catalogue/src/pattern/form/sectioned/sectioned-form.tsx new file mode 100644 index 00000000..8c275f12 --- /dev/null +++ b/catalogue/src/pattern/form/sectioned/sectioned-form.tsx @@ -0,0 +1,86 @@ +/* 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 + } /> + + + + + ); +} diff --git a/catalogue/src/pattern/form/wizard/PATTERN.md b/catalogue/src/pattern/form/wizard/PATTERN.md new file mode 100644 index 00000000..77b26ce4 --- /dev/null +++ b/catalogue/src/pattern/form/wizard/PATTERN.md @@ -0,0 +1,41 @@ +--- +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 + + + +## 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/catalogue/src/pattern/form/wizard/wizard-form.tsx b/catalogue/src/pattern/form/wizard/wizard-form.tsx new file mode 100644 index 00000000..1d7d9d97 --- /dev/null +++ b/catalogue/src/pattern/form/wizard/wizard-form.tsx @@ -0,0 +1,92 @@ +/* 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.

+
+ )} +
+
+ +
+ + +
+
+
+ ); +} diff --git a/catalogue/src/pattern/interaction/confirm/PATTERN.md b/catalogue/src/pattern/interaction/confirm/PATTERN.md new file mode 100644 index 00000000..bb164e69 --- /dev/null +++ b/catalogue/src/pattern/interaction/confirm/PATTERN.md @@ -0,0 +1,45 @@ +--- +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 + + + +## 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/catalogue/src/pattern/interaction/confirm/confirm.tsx b/catalogue/src/pattern/interaction/confirm/confirm.tsx new file mode 100644 index 00000000..35830d4e --- /dev/null +++ b/catalogue/src/pattern/interaction/confirm/confirm.tsx @@ -0,0 +1,29 @@ +/* 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 + + + + + ); +} diff --git a/catalogue/src/pattern/interaction/multi-select/PATTERN.md b/catalogue/src/pattern/interaction/multi-select/PATTERN.md new file mode 100644 index 00000000..34a8e7d4 --- /dev/null +++ b/catalogue/src/pattern/interaction/multi-select/PATTERN.md @@ -0,0 +1,67 @@ +--- +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 + + + +## 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/catalogue/src/pattern/interaction/multi-select/mock.ts b/catalogue/src/pattern/interaction/multi-select/mock.ts new file mode 100644 index 00000000..e32df28d --- /dev/null +++ b/catalogue/src/pattern/interaction/multi-select/mock.ts @@ -0,0 +1,14 @@ +export type Order = { + id: string; + number: string; + status: string; + total: number; +}; + +export const mockOrders: Order[] = [ + { id: "1", number: "ORD-001", status: "Open", total: 1240 }, + { id: "2", number: "ORD-002", status: "Confirmed", total: 3800 }, + { id: "3", number: "ORD-003", status: "Open", total: 920 }, + { id: "4", number: "ORD-004", status: "Shipped", total: 5600 }, + { id: "5", number: "ORD-005", status: "Open", total: 2100 }, +]; diff --git a/catalogue/src/pattern/interaction/multi-select/multi-select.tsx b/catalogue/src/pattern/interaction/multi-select/multi-select.tsx new file mode 100644 index 00000000..bdd9e0a7 --- /dev/null +++ b/catalogue/src/pattern/interaction/multi-select/multi-select.tsx @@ -0,0 +1,105 @@ +/* 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 + + + +
+ )} + + ); +} diff --git a/catalogue/src/pattern/interaction/toast/PATTERN.md b/catalogue/src/pattern/interaction/toast/PATTERN.md new file mode 100644 index 00000000..000b521d --- /dev/null +++ b/catalogue/src/pattern/interaction/toast/PATTERN.md @@ -0,0 +1,46 @@ +--- +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 + + + +## 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/catalogue/src/pattern/interaction/toast/toast-example.tsx b/catalogue/src/pattern/interaction/toast/toast-example.tsx new file mode 100644 index 00000000..146d4550 --- /dev/null +++ b/catalogue/src/pattern/interaction/toast/toast-example.tsx @@ -0,0 +1,22 @@ +/* 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 ; +} diff --git a/catalogue/src/pattern/list/dense-scan/PATTERN.md b/catalogue/src/pattern/list/dense-scan/PATTERN.md new file mode 100644 index 00000000..c5eacf50 --- /dev/null +++ b/catalogue/src/pattern/list/dense-scan/PATTERN.md @@ -0,0 +1,72 @@ +--- +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 + + + +## Page Implementation + + + +## 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 +- Status Badge colors must use design system tokens (variant prop) +- 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` +- 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/catalogue/src/pattern/list/dense-scan/columns.tsx b/catalogue/src/pattern/list/dense-scan/columns.tsx new file mode 100644 index 00000000..6455225c --- /dev/null +++ b/catalogue/src/pattern/list/dense-scan/columns.tsx @@ -0,0 +1,32 @@ +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; +}; + +const statusVariant = { + draft: "neutral", + confirmed: "outline-info", + shipped: "outline-warning", + delivered: "outline-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}, + }, + { + label: "Amount", + render: (row) => `$${row.amount.toLocaleString()}`, + }, + { label: "Created", accessor: (row) => row.createdAt }, +]; diff --git a/catalogue/src/pattern/list/dense-scan/dense-scan.tsx b/catalogue/src/pattern/list/dense-scan/dense-scan.tsx new file mode 100644 index 00000000..8d1b4b08 --- /dev/null +++ b/catalogue/src/pattern/list/dense-scan/dense-scan.tsx @@ -0,0 +1,26 @@ +/* pattern: list/dense-scan */ +import { DataTable, 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 ( +
+
+ + +
+ + + +
+ ); +} diff --git a/catalogue/src/pattern/list/dense-scan/mock.ts b/catalogue/src/pattern/list/dense-scan/mock.ts new file mode 100644 index 00000000..86fa6138 --- /dev/null +++ b/catalogue/src/pattern/list/dense-scan/mock.ts @@ -0,0 +1,44 @@ +import type { Order } from "./columns"; + +export const mockOrders: Order[] = [ + { + id: "1", + orderNumber: "ORD-001", + customer: "Acme Corp", + status: "confirmed", + amount: 1500, + createdAt: "2025-01-15", + }, + { + id: "2", + orderNumber: "ORD-002", + customer: "Globex Inc", + status: "draft", + amount: 3200, + createdAt: "2025-01-16", + }, + { + id: "3", + orderNumber: "ORD-003", + customer: "Initech", + status: "shipped", + amount: 890, + createdAt: "2025-01-17", + }, + { + id: "4", + orderNumber: "ORD-004", + customer: "Umbrella Corp", + status: "delivered", + amount: 4200, + createdAt: "2025-01-18", + }, + { + id: "5", + orderNumber: "ORD-005", + customer: "Stark Industries", + status: "confirmed", + amount: 7800, + createdAt: "2025-01-19", + }, +]; diff --git a/catalogue/tsconfig.json b/catalogue/tsconfig.json new file mode 100644 index 00000000..60099a8c --- /dev/null +++ b/catalogue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/packages/core/package.json b/packages/core/package.json index e3a73fec..1b12728f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -22,7 +22,8 @@ "directory": "packages/core" }, "files": [ - "dist/**" + "dist/**", + "skills/**" ], "type": "module", "exports": { diff --git a/packages/core/skills/app-shell-patterns/SKILL.md b/packages/core/skills/app-shell-patterns/SKILL.md new file mode 100644 index 00000000..6aa9c48d --- /dev/null +++ b/packages/core/skills/app-shell-patterns/SKILL.md @@ -0,0 +1,67 @@ +--- +name: app-shell-patterns +description: UI pattern catalog for building pages with @tailor-platform/app-shell components +--- + +# 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 diff --git a/packages/core/skills/app-shell-patterns/references/fundamental/components.md b/packages/core/skills/app-shell-patterns/references/fundamental/components.md new file mode 100644 index 00000000..8b5e935c --- /dev/null +++ b/packages/core/skills/app-shell-patterns/references/fundamental/components.md @@ -0,0 +1,704 @@ +# AppShell Components + +> **AppShell version:** `0.36.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`: `columns` (number, default auto-detected from children), `gap`, `title`, `actions`, `className`, `style`. + +**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/*`). + +### `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 config, header config, content slot. Used in `App.tsx`. +**Used in patterns:** consumed by AppShell init, not directly by page patterns. See `project-setup.md`. + +### `DefaultSidebar`, `SidebarGroup`, `SidebarItem`, `SidebarSeparator` + +**Import:** `import { DefaultSidebar, SidebarGroup, SidebarItem, SidebarSeparator } from '@tailor-platform/app-shell'` +**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`. + +### `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). + +--- + +## 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`: `default | success | warning | neutral | error | destructive`. Plus `badgeVariants` CVA for custom-styled siblings. +**Example:** + +```tsx +Active +``` + +**Used in patterns:** `list/*` (status column), `detail/*` (header status). + +### `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 `

`. +**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: … +}); + + + + + + + + + +; +``` + +**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:** + +```tsx +} +/> +``` + +**Used in patterns:** KPI tiles, dashboards, **`detail/*`** metric strips where specs call for them. + +### `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). + +--- + +## 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. +**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, loader }`), 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 new file mode 100644 index 00000000..48bbed82 --- /dev/null +++ b/packages/core/skills/app-shell-patterns/references/fundamental/design-system.md @@ -0,0 +1,355 @@ +# 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 + +- More than 2 primary actions in the header → move overflow into a `Menu`. +- 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. +- `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. 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 new file mode 100644 index 00000000..32235a10 --- /dev/null +++ b/packages/core/skills/app-shell-patterns/references/patterns/form-modal.md @@ -0,0 +1,169 @@ +--- +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 new file mode 100644 index 00000000..ff4fd720 --- /dev/null +++ b/packages/core/skills/app-shell-patterns/references/patterns/form-sectioned.md @@ -0,0 +1,126 @@ +--- +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 new file mode 100644 index 00000000..50ddf79d --- /dev/null +++ b/packages/core/skills/app-shell-patterns/references/patterns/form-wizard.md @@ -0,0 +1,134 @@ +--- +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 new file mode 100644 index 00000000..d79b0475 --- /dev/null +++ b/packages/core/skills/app-shell-patterns/references/patterns/interaction-confirm.md @@ -0,0 +1,75 @@ +--- +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 new file mode 100644 index 00000000..77ec11a1 --- /dev/null +++ b/packages/core/skills/app-shell-patterns/references/patterns/interaction-multi-select.md @@ -0,0 +1,173 @@ +--- +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 new file mode 100644 index 00000000..2f069e7f --- /dev/null +++ b/packages/core/skills/app-shell-patterns/references/patterns/interaction-toast.md @@ -0,0 +1,69 @@ +--- +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 new file mode 100644 index 00000000..47efe8ea --- /dev/null +++ b/packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md @@ -0,0 +1,132 @@ +--- +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; +}; + +const statusVariant = { + draft: "neutral", + confirmed: "outline-info", + shipped: "outline-warning", + delivered: "outline-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}, + }, + { + label: "Amount", + render: (row) => `${row.amount.toLocaleString()}`, + }, + { label: "Created", accessor: (row) => row.createdAt }, +]; +``` + +## Page Implementation + +```tsx +/* pattern: list/dense-scan */ +import { DataTable, 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 ( +
+
+ + +
+ + + +
+ ); +} +``` + +## 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 +- Status Badge colors must use design system tokens (variant prop) +- 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` +- 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/pnpm-lock.yaml b/pnpm-lock.yaml index e8ded701..22796b24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,7 +50,7 @@ catalogs: version: 7.3.2 vitest: specifier: ^4.1.6 - version: 4.1.6 + version: 4.1.7 importers: @@ -72,6 +72,31 @@ importers: specifier: ^2.9.14 version: 2.9.14 + catalogue: + dependencies: + '@tailor-platform/app-shell': + specifier: workspace:* + version: link:../packages/core + gray-matter: + specifier: 4.0.3 + version: 4.0.3 + react: + specifier: ^19.0.0 + version: 19.2.5 + react-dom: + specifier: ^19.0.0 + version: 19.2.5(react@19.2.5) + devDependencies: + '@types/react': + specifier: 'catalog:' + version: 19.2.13 + '@types/react-dom': + specifier: 'catalog:' + version: 19.2.3(@types/react@19.2.13) + typescript: + specifier: 'catalog:' + version: 5.9.3 + e2e: devDependencies: '@playwright/test': @@ -319,7 +344,7 @@ importers: version: 6.1.1(typescript@5.9.3)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.6.2)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.6.2)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) packages/sdk-plugin: devDependencies: @@ -340,7 +365,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.6.2)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.6.2)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) packages/vite-plugin: dependencies: @@ -362,7 +387,7 @@ importers: version: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) vitest: specifier: 'catalog:' - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.6.2)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.6.2)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) packages: @@ -432,8 +457,8 @@ packages: resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==} engines: {node: ^20.19.0 || >=22.12.0} - '@babel/helper-string-parser@8.0.0-rc.5': - resolution: {integrity: sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A==} + '@babel/helper-string-parser@8.0.0-rc.6': + resolution: {integrity: sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==} engines: {node: ^22.18.0 || >=24.11.0} '@babel/helper-validator-identifier@7.28.5': @@ -448,6 +473,10 @@ packages: resolution: {integrity: sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg==} engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-identifier@8.0.0-rc.6': + resolution: {integrity: sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -471,8 +500,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - '@babel/parser@8.0.0-rc.5': - resolution: {integrity: sha512-/Mfg83rK3+jsRbl4Vbd0jqxc6M1A1/WNFtgrowRM1unEsD3XcNnrBdMM0JWakd0/RN9lseQKwPduW1TiEwKOlQ==} + '@babel/parser@8.0.0-rc.6': + resolution: {integrity: sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==} engines: {node: ^22.18.0 || >=24.11.0} hasBin: true @@ -512,8 +541,8 @@ packages: resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==} engines: {node: ^20.19.0 || >=22.12.0} - '@babel/types@8.0.0-rc.5': - resolution: {integrity: sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==} + '@babel/types@8.0.0-rc.6': + resolution: {integrity: sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==} engines: {node: ^22.18.0 || >=24.11.0} '@badgateway/oauth2-client@3.3.1': @@ -1640,8 +1669,8 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - '@oxc-project/types@0.130.0': - resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} '@oxfmt/binding-android-arm-eabi@0.47.0': resolution: {integrity: sha512-KrMQRdMi/upr81qT4ijK6X6BNp6jqpMY7FwILQnwIy9QLc3qpnhUx5rsCLGzn4ewsCQ0CNAspN2ogmP1GXLyLw==} @@ -1948,8 +1977,8 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.1': - resolution: {integrity: sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==} + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -1960,8 +1989,8 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.1': - resolution: {integrity: sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==} + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -1972,8 +2001,8 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.1': - resolution: {integrity: sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==} + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -1984,8 +2013,8 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.1': - resolution: {integrity: sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==} + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -1996,8 +2025,8 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.1': - resolution: {integrity: sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -2009,8 +2038,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-gnu@1.0.1': - resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -2023,8 +2052,8 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-arm64-musl@1.0.1': - resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -2037,8 +2066,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-ppc64-gnu@1.0.1': - resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] @@ -2051,8 +2080,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.1': - resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] @@ -2065,8 +2094,8 @@ packages: os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.1': - resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -2079,8 +2108,8 @@ packages: os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-musl@1.0.1': - resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -2092,8 +2121,8 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.1': - resolution: {integrity: sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==} + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -2103,8 +2132,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.1': - resolution: {integrity: sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==} + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] @@ -2114,8 +2143,8 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.1': - resolution: {integrity: sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==} + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -2126,8 +2155,8 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.1': - resolution: {integrity: sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==} + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2714,11 +2743,11 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - '@vitest/expect@4.1.6': - resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} - '@vitest/mocker@4.1.6': - resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2728,20 +2757,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.6': - resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} - '@vitest/runner@4.1.6': - resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} - '@vitest/snapshot@4.1.6': - resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} - '@vitest/spy@4.1.6': - resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} - '@vitest/utils@4.1.6': - resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} '@volar/language-core@2.4.27': resolution: {integrity: sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==} @@ -3387,6 +3416,10 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -3538,6 +3571,10 @@ packages: resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + happy-dom@20.6.2: resolution: {integrity: sha512-Xk/Y0cuq9ngN/my8uvK4gKoyDl6sBKkIl8A/hJ0IabZVH7E5SJLHNE7uKRPVmSrQbhJaLIHTEcvTct4GgNtsRA==} engines: {node: '>=20.0.0'} @@ -3640,6 +3677,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3787,6 +3828,10 @@ packages: jsonfile@6.2.1: resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -4411,6 +4456,11 @@ packages: peerDependencies: react: ^19.1.1 + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + react-dom@19.2.6: resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: @@ -4452,6 +4502,10 @@ packages: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + react@19.2.6: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} @@ -4561,8 +4615,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.1: - resolution: {integrity: sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==} + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4605,6 +4659,10 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4752,6 +4810,10 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -4833,8 +4895,8 @@ packages: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + tinyexec@1.2.2: + resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==} engines: {node: '>=18'} tinyglobby@0.2.16: @@ -5079,20 +5141,20 @@ packages: yaml: optional: true - vitest@4.1.6: - resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.6 - '@vitest/browser-preview': 4.1.6 - '@vitest/browser-webdriverio': 4.1.6 - '@vitest/coverage-istanbul': 4.1.6 - '@vitest/coverage-v8': 4.1.6 - '@vitest/ui': 4.1.6 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -5277,8 +5339,8 @@ snapshots: '@babel/generator@8.0.0-rc.5': dependencies: - '@babel/parser': 8.0.0-rc.5 - '@babel/types': 8.0.0-rc.5 + '@babel/parser': 8.0.0-rc.6 + '@babel/types': 8.0.0-rc.6 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 '@types/jsesc': 2.5.1 @@ -5316,7 +5378,7 @@ snapshots: '@babel/helper-string-parser@8.0.0-rc.3': {} - '@babel/helper-string-parser@8.0.0-rc.5': {} + '@babel/helper-string-parser@8.0.0-rc.6': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -5324,6 +5386,8 @@ snapshots: '@babel/helper-validator-identifier@8.0.0-rc.5': {} + '@babel/helper-validator-identifier@8.0.0-rc.6': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.6': @@ -5341,11 +5405,11 @@ snapshots: '@babel/parser@8.0.0-rc.4': dependencies: - '@babel/types': 8.0.0-rc.5 + '@babel/types': 8.0.0-rc.6 - '@babel/parser@8.0.0-rc.5': + '@babel/parser@8.0.0-rc.6': dependencies: - '@babel/types': 8.0.0-rc.5 + '@babel/types': 8.0.0-rc.6 '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': dependencies: @@ -5389,10 +5453,10 @@ snapshots: '@babel/helper-string-parser': 8.0.0-rc.3 '@babel/helper-validator-identifier': 8.0.0-rc.3 - '@babel/types@8.0.0-rc.5': + '@babel/types@8.0.0-rc.6': dependencies: - '@babel/helper-string-parser': 8.0.0-rc.5 - '@babel/helper-validator-identifier': 8.0.0-rc.5 + '@babel/helper-string-parser': 8.0.0-rc.6 + '@babel/helper-validator-identifier': 8.0.0-rc.6 '@badgateway/oauth2-client@3.3.1': {} @@ -6418,7 +6482,7 @@ snapshots: '@oxc-project/types@0.127.0': {} - '@oxc-project/types@0.130.0': {} + '@oxc-project/types@0.132.0': {} '@oxfmt/binding-android-arm-eabi@0.47.0': optional: true @@ -6612,73 +6676,73 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-android-arm64@1.0.1': + '@rolldown/binding-android-arm64@1.0.2': optional: true '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-arm64@1.0.1': + '@rolldown/binding-darwin-arm64@1.0.2': optional: true '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-x64@1.0.1': + '@rolldown/binding-darwin-x64@1.0.2': optional: true '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-freebsd-x64@1.0.1': + '@rolldown/binding-freebsd-x64@1.0.2': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.1': + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.1': + '@rolldown/binding-linux-arm64-gnu@1.0.2': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.1': + '@rolldown/binding-linux-arm64-musl@1.0.2': optional: true '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.1': + '@rolldown/binding-linux-ppc64-gnu@1.0.2': optional: true '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.1': + '@rolldown/binding-linux-s390x-gnu@1.0.2': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.1': + '@rolldown/binding-linux-x64-gnu@1.0.2': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-musl@1.0.1': + '@rolldown/binding-linux-x64-musl@1.0.2': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-openharmony-arm64@1.0.1': + '@rolldown/binding-openharmony-arm64@1.0.2': optional: true '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': @@ -6688,7 +6752,7 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-wasm32-wasi@1.0.1': + '@rolldown/binding-wasm32-wasi@1.0.2': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 @@ -6698,13 +6762,13 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.1': + '@rolldown/binding-win32-arm64-msvc@1.0.2': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.1': + '@rolldown/binding-win32-x64-msvc@1.0.2': optional: true '@rolldown/pluginutils@1.0.0-rc.17': {} @@ -7323,44 +7387,44 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/expect@4.1.6': + '@vitest/expect@4.1.7': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0))': + '@vitest/mocker@4.1.7(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0))': dependencies: - '@vitest/spy': 4.1.6 + '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0) - '@vitest/pretty-format@4.1.6': + '@vitest/pretty-format@4.1.7': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.6': + '@vitest/runner@4.1.7': dependencies: - '@vitest/utils': 4.1.6 + '@vitest/utils': 4.1.7 pathe: 2.0.3 - '@vitest/snapshot@4.1.6': + '@vitest/snapshot@4.1.7': dependencies: - '@vitest/pretty-format': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.6': {} + '@vitest/spy@4.1.7': {} - '@vitest/utils@4.1.6': + '@vitest/utils@4.1.7': dependencies: - '@vitest/pretty-format': 4.1.6 + '@vitest/pretty-format': 4.1.7 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -7973,6 +8037,10 @@ snapshots: exsolve@1.0.8: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + extendable-error@0.1.7: {} fast-deep-equal@3.1.3: {} @@ -8129,6 +8197,13 @@ snapshots: graphql@16.14.0: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + happy-dom@20.6.2: dependencies: '@types/node': 25.6.0 @@ -8275,6 +8350,8 @@ snapshots: is-docker@3.0.0: {} + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -8380,6 +8457,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + kind-of@6.0.3: {} + kleur@3.0.3: {} kolorist@1.8.0: {} @@ -9013,6 +9092,11 @@ snapshots: react: 19.1.1 scheduler: 0.26.0 + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + react-dom@19.2.6(react@19.2.6): dependencies: react: 19.2.6 @@ -9043,6 +9127,8 @@ snapshots: react@19.1.1: {} + react@19.2.5: {} + react@19.2.6: {} read-yaml-file@1.1.0: @@ -9124,7 +9210,7 @@ snapshots: reusify@1.1.0: {} - rolldown-plugin-dts@0.25.1(rolldown@1.0.1)(typescript@5.9.3): + rolldown-plugin-dts@0.25.1(rolldown@1.0.2)(typescript@5.9.3): dependencies: '@babel/generator': 8.0.0-rc.5 '@babel/helper-validator-identifier': 8.0.0-rc.5 @@ -9134,7 +9220,7 @@ snapshots: dts-resolver: 3.0.0 get-tsconfig: 5.0.0-beta.5 obug: 2.1.1 - rolldown: 1.0.1 + rolldown: 1.0.2 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -9161,26 +9247,26 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 - rolldown@1.0.1: + rolldown@1.0.2: dependencies: - '@oxc-project/types': 0.130.0 + '@oxc-project/types': 0.132.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.1 - '@rolldown/binding-darwin-arm64': 1.0.1 - '@rolldown/binding-darwin-x64': 1.0.1 - '@rolldown/binding-freebsd-x64': 1.0.1 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.1 - '@rolldown/binding-linux-arm64-gnu': 1.0.1 - '@rolldown/binding-linux-arm64-musl': 1.0.1 - '@rolldown/binding-linux-ppc64-gnu': 1.0.1 - '@rolldown/binding-linux-s390x-gnu': 1.0.1 - '@rolldown/binding-linux-x64-gnu': 1.0.1 - '@rolldown/binding-linux-x64-musl': 1.0.1 - '@rolldown/binding-openharmony-arm64': 1.0.1 - '@rolldown/binding-wasm32-wasi': 1.0.1 - '@rolldown/binding-win32-arm64-msvc': 1.0.1 - '@rolldown/binding-win32-x64-msvc': 1.0.1 + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 rollup@4.60.0: dependencies: @@ -9242,6 +9328,11 @@ snapshots: scheduler@0.27.0: {} + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver@6.3.1: {} semver@7.5.4: @@ -9419,6 +9510,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom-string@1.0.0: {} + strip-bom@3.0.0: {} strip-final-newline@2.0.0: {} @@ -9472,7 +9565,7 @@ snapshots: tinyexec@1.0.4: {} - tinyexec@1.1.2: {} + tinyexec@1.2.2: {} tinyglobby@0.2.16: dependencies: @@ -9532,10 +9625,10 @@ snapshots: import-without-cache: 0.4.0 obug: 2.1.1 picomatch: 4.0.4 - rolldown: 1.0.1 - rolldown-plugin-dts: 0.25.1(rolldown@1.0.1)(typescript@5.9.3) + rolldown: 1.0.2 + rolldown-plugin-dts: 0.25.1(rolldown@1.0.2)(typescript@5.9.3) semver: 7.8.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 unconfig-core: 7.5.0 @@ -9674,15 +9767,15 @@ snapshots: lightningcss: 1.32.0 tsx: 4.21.0 - vitest@4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.6.2)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)): + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(happy-dom@20.6.2)(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)): dependencies: - '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) - '@vitest/pretty-format': 4.1.6 - '@vitest/runner': 4.1.6 - '@vitest/snapshot': 4.1.6 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@7.3.2(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 212717d1..d77dffb2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: + - catalogue - e2e - examples/** - packages/**