diff --git a/catalogue/scripts/SKILL.template.md b/catalogue/scripts/SKILL.template.md index 85791193..a11d0dd7 100644 --- a/catalogue/scripts/SKILL.template.md +++ b/catalogue/scripts/SKILL.template.md @@ -1,6 +1,6 @@ --- name: app-shell-patterns -description: UI pattern catalog for building pages with @tailor-platform/app-shell components +description: "Best-practice UI patterns and correct component usage for building pages in apps that use @tailor-platform/app-shell. Use when: building or editing any screen, page, list, table, detail view, form, modal, dialog, wizard, or bulk/confirm/toast interaction in an app with @tailor-platform/app-shell installed — or when choosing the right AppShell component, layout, or design token for a UI." --- # App-Shell Patterns @@ -34,3 +34,15 @@ These are the foundational rules that underpin all patterns. All patterns build - NEVER mix patterns in a single page component - ALWAYS use AppShell components — do NOT use raw HTML or third-party UI libraries - If no entry matches, compose directly from fundamental references + +### Cross-cutting UX rules (apply to every screen) + +Full rationale in [`design-system.md`](references/fundamental/design-system.md) → Composition & emphasis rules. + +- **One primary action per view:** at most one primary/filled `Button`; everything else is `outline`/`secondary`/`ghost`. +- **Status badges by semantic color:** a **filled** semantic variant for the record's primary/lifecycle status, **`outline-*`** for secondary statuses, **`subtle-*`** for tags; reserve brand `default` for non-status emphasis. +- **No duplicate actions:** an action lives in exactly one place — never repeat the same action in both `Layout.Header` and `ActionPanel`. +- **Action placement:** primary CTA + status in `Layout.Header`; workflow actions in `ActionPanel`; back/navigation in the breadcrumb — never in `ActionPanel`. +- **Metric tiles always go in a `Grid`** (`columns={{ initial: 1, md: 2, xl: 4 }}`) — never one-per-row. +- **Forms default to `form/modal`** — only build a routed full-page form when the design explicitly calls for one. +- **Handle every state:** loading (skeleton), empty (labelled empty state), and error (inline + retry) — never ship only the happy path. diff --git a/catalogue/scripts/generate-skill.mjs b/catalogue/scripts/generate-skill.mjs index e19d404d..eda65122 100644 --- a/catalogue/scripts/generate-skill.mjs +++ b/catalogue/scripts/generate-skill.mjs @@ -8,7 +8,7 @@ * - skills/app-shell-patterns/references//.md (per-entry docs) */ -import { readdir, readFile, writeFile, mkdir, copyFile } from "node:fs/promises"; +import { readdir, readFile, writeFile, mkdir } from "node:fs/promises"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import matter from "gray-matter"; @@ -24,7 +24,7 @@ const referencesDir = join(skillsDir, "references"); * 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. + * If null, all top-level .md files in the category directory are processed directly. */ const CATEGORIES = [ { @@ -43,7 +43,7 @@ const CATEGORIES = [ /** * Resolve markers in markdown body. - * Reads the referenced file relative to the PATTERN.md directory + * Reads the referenced file relative to the current markdown file directory * and replaces the marker with a fenced code block. */ async function resolveSourceMarkers(body, patternDir) { @@ -87,7 +87,8 @@ async function findEntryFiles(dir, filename) { return results; } - for (const entry of entries) { + const sortedEntries = [...entries].sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of sortedEntries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { results.push(...(await findEntryFiles(fullPath, filename))); @@ -117,7 +118,8 @@ function slugToFilename(slug) { * * 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. + * When sourceFile is null, all .md files in the category dir are read directly and + * source markers are resolved in place. */ async function processCategory(category) { const categoryDir = join(catalogueRoot, "src", category.name); @@ -135,7 +137,9 @@ async function processCopiedCategory(category, categoryDir, outputDir) { for (const filePath of files) { const filename = filePath.split("/").pop(); const outputPath = join(outputDir, filename); - await copyFile(filePath, outputPath); + const content = await readFile(filePath, "utf-8"); + const resolvedContent = await resolveSourceMarkers(content, dirname(filePath)); + await writeFile(outputPath, resolvedContent); console.log(` Generated references/${category.outputDir}/${filename}`); } return { category, files }; @@ -192,31 +196,49 @@ async function main() { /** * Generate an index table for entry-based categories (with frontmatter). */ +function formatMarkdownTable(rows) { + const widths = rows[0].map((_, columnIndex) => + Math.max(...rows.map((row) => String(row[columnIndex]).length)), + ); + + const renderRow = (row) => + `| ${row.map((cell, columnIndex) => String(cell).padEnd(widths[columnIndex])).join(" | ")} |`; + + return [ + renderRow(rows[0]), + `| ${widths.map((width) => "-".repeat(width)).join(" | ")} |`, + ...rows.slice(1).map(renderRow), + ].join("\n"); +} + function generateEntryTable(entries, outputDir) { if (entries.length === 0) return ""; - // Group entries by subcategory - const grouped = {}; - for (const { meta } of entries) { + const grouped = new Map(); + for (const { meta } of [...entries].sort((a, b) => a.meta.slug.localeCompare(b.meta.slug))) { const key = meta.subcategory || meta.category; - if (!grouped[key]) grouped[key] = []; - grouped[key].push(meta); + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(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(); + return [...grouped.entries()] + .map(([group, items]) => { + const rows = [ + ["Slug", "Name", "Description"], + ...items.map((item) => { + const filename = slugToFilename(item.slug) + ".md"; + const displaySlug = item.slug.replace(/^[^/]+\//, ""); + return [ + `[\`${displaySlug}\`](references/${outputDir}/${filename})`, + item.name, + item.description, + ]; + }), + ]; + + return `### ${group}\n\n${formatMarkdownTable(rows)}`; + }) + .join("\n\n"); } /** @@ -225,14 +247,16 @@ function generateEntryTable(entries, outputDir) { function generateCopiedTable(files, outputDir) { if (files.length === 0) return ""; - let table = "| File | Description |\n"; - table += "| ---- | ----------- |\n"; - for (const filePath of files) { - const filename = filePath.split("/").pop(); - const name = filename.replace(".md", ""); - table += `| [${filename}](references/${outputDir}/${filename}) | ${name} reference |\n`; - } - return table.trim(); + const rows = [ + ["File", "Description"], + ...[...files].sort().map((filePath) => { + const filename = filePath.split("/").pop(); + const name = filename.replace(".md", ""); + return [`[${filename}](references/${outputDir}/${filename})`, `${name} reference`]; + }), + ]; + + return formatMarkdownTable(rows); } async function findMarkdownFiles(dir) { @@ -243,7 +267,7 @@ async function findMarkdownFiles(dir) { } catch { return results; } - for (const entry of entries) { + for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) { if (!entry.isDirectory() && entry.name.endsWith(".md")) { results.push(join(dir, entry.name)); } diff --git a/catalogue/src/fundamental/components.md b/catalogue/src/fundamental/components.md index f7a9d61c..d9ab190b 100644 --- a/catalogue/src/fundamental/components.md +++ b/catalogue/src/fundamental/components.md @@ -1,16 +1,16 @@ # AppShell Components -> **AppShell version:** `0.36.0` (matches `packages/erp-kit/templates/scaffold/app/*/frontend/package.json` pinned `@tailor-platform/app-shell` semver) +> **AppShell version:** `1.7.0` (matches `packages/erp-kit/templates/scaffold/app/*/frontend/package.json` pinned `@tailor-platform/app-shell` semver) > **Source of truth:** `@tailor-platform/app-shell` exports > **Update process:** see "Keeping this file in sync" at the bottom ## 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 | +| This file (`components.md`) | `design-system.md` | +| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| 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. @@ -19,7 +19,6 @@ This file intentionally does **not** duplicate full token catalogs. “Every hea Entries follow this shape: ``` -**Import:** how to import it **Purpose:** one sentence **API:** key props or sub-components **Example:** minimal JSX @@ -36,6 +35,7 @@ For the full upstream API of any component, follow the link to the published ref ### `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`. @@ -45,7 +45,6 @@ For the full upstream API of any component, follow the link to the published ref > Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/layout.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/layout.md) -**Import:** `import { Layout } from '@tailor-platform/app-shell'` **Purpose:** Standard page container with header + 1–N column body. The most-used component — every page wraps content in ``. **API:** `Layout` (root) + `Layout.Column` + `Layout.Header`. `LayoutProps`: `fill` (boolean), `columns` (number, default auto-detected from children), `gap`, `title`, `actions`, `className`, `style`. @@ -74,44 +73,32 @@ Desktop breakpoints and desktop-first rationale: **`design-system.md`** §4 Brea **Example — list page header with tabs:** -```tsx -import { Button, Layout, Tabs } from "@tailor-platform/app-shell"; - - - Create]}> - - - All - Open - - - - {/* table — see patterns/list/dense-scan.md */} -; -``` + **Example — detail page, 2-column with area mode:** -```tsx - - Edit]} /> - - - - - - - - -``` + **Notes:** Children that aren't `Layout.Header` or `Layout.Column` are filtered out. Column gap overrides (``) → **`design-system.md`** §5 (`astw:` rules). **Used in patterns:** every page pattern (`list/*`, `detail/*`, `form/*`). +### `Grid` + +**Purpose:** Presentational CSS-Grid container for laying tiles/cards into equal or custom columns with responsive reflow — the canonical wrapper for **metric / KPI strips** and card galleries. Purely layout; makes no data assumptions. +**API:** `GridProps` — `columns` (number | CSS track string like `"280px 1fr"` | responsive object `{ initial, sm, md, lg, xl }`), `gap` (spacing step, default `3`) plus `gapX` / `gapY`, `minChildWidth` (auto-fit: as many ≥Npx columns as fit, no breakpoints needed), `rows`, `flow`, `align`, `justify`. Sub-component **`Grid.Item`** for spanning/placement — `colSpan`, `rowSpan`, `colStart`, `colEnd` (all responsive). Root carries `data-slot="grid"`. +**Example — responsive KPI grid:** + + + +**Used in patterns:** metric / KPI strips on `detail/*` and dashboards; any equal-column card layout. + +**Notes:** Metric tiles and KPI cards **always** go in a `Grid` — never stacked one-per-row or in a single column. Use a responsive `columns` object (e.g. `{ initial: 1, md: 2, xl: 4 }`) so tiles reflow at smaller widths; reach for `minChildWidth` when the tile count is dynamic. + ### `SidebarLayout` **Import:** `import { SidebarLayout } from '@tailor-platform/app-shell'` + **Purpose:** Top-level layout that mounts the sidebar and renders the page outlet. **API:** `SidebarLayoutProps` — `sidebar`, `header`, and `children` are full-region slots (each defaults to a built-in). `sidebar` defaults to `SidebarLayout.DefaultSidebar`; `header` defaults to `SidebarLayout.DefaultHeader`. Used in `App.tsx`. **Used in patterns:** consumed by AppShell init, not directly by page patterns. See `project-setup.md`. @@ -119,6 +106,7 @@ import { Button, Layout, Tabs } from "@tailor-platform/app-shell"; ### `SidebarLayout.DefaultHeader` (`DefaultHeader`) **Import:** `import { SidebarLayout } from '@tailor-platform/app-shell'` → `SidebarLayout.DefaultHeader` (also top-level `DefaultHeader`) + **Purpose:** The built-in top bar (trigger + breadcrumb, plus an `actions` cluster). Drop into `SidebarLayout`'s `header` slot to extend the header without rebuilding it. **API:** `actions?: ReactNode | ReactNode[]` — the right-hand cluster (opinionated flex row). **Defaults to `[]`, and passing `actions` REPLACES the switcher** — include `` in the array to keep it. `actions={[]}` = empty right side. **Used in patterns:** project-level header customization (notification bell, user menu). See `project-setup.md`. @@ -126,12 +114,14 @@ import { Button, Layout, Tabs } from "@tailor-platform/app-shell"; ### `SidebarLayout.DefaultSidebar` (`DefaultSidebar`), `SidebarGroup`, `SidebarItem`, `SidebarSeparator` **Import:** `import { SidebarLayout, SidebarGroup, SidebarItem, SidebarSeparator } from '@tailor-platform/app-shell'` → `SidebarLayout.DefaultSidebar` (also top-level `DefaultSidebar`) + **Purpose:** Sidebar composition. `DefaultSidebar` auto-resolves nav items from `appShellPageProps.meta` on each page; the others let you customize manually. **Used in patterns:** sidebar is a project-level concern. See `project-setup.md`. ### `AppearanceSwitcher` **Import:** `import { AppearanceSwitcher } from '@tailor-platform/app-shell'` + **Purpose:** Palette-icon dropdown for Light/Dark/System color mode. The default `SidebarLayout.DefaultHeader` action; also usable standalone (e.g. a sidebar footer). Include it in `DefaultHeader`'s `actions` array when customizing the header to keep it visible. **API:** No props — reads/writes theme via `useTheme`. **Used in patterns:** project-level. See `project-setup.md`. @@ -139,6 +129,7 @@ import { Button, Layout, Tabs } from "@tailor-platform/app-shell"; ### `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. @@ -151,23 +142,18 @@ import { Button, Layout, Tabs } from "@tailor-platform/app-shell"; > 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). @@ -176,28 +162,11 @@ import { Button, Link } from '@tailor-platform/app-shell'; > 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`. @@ -205,55 +174,25 @@ import { Button, Link } from '@tailor-platform/app-shell'; > 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. +**Notes:** Size the panel with **`className`** on `Sheet.Content` (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). @@ -262,10 +201,23 @@ import { Button, Link } from '@tailor-platform/app-shell'; ### `Tooltip` **Import:** `import { Tooltip } from '@tailor-platform/app-shell'` + **Purpose:** Contextual hint on hover/focus. Use sparingly — for icon-only buttons or constrained labels. **API:** Compound — `Tooltip.Root`, `Tooltip.Trigger`, `Tooltip.Content`. **Used in patterns:** any pattern with icon-only buttons (must have `aria-label` AND a tooltip). +### `Tabs` + +**Purpose:** In-page tab navigation — split one record's sections (Overview / Line items / Activity) or bucket a list (All / Open / …) into switchable panels. Presentational; owns only the active-tab state. +**API:** Compound — `Tabs.Root` (`variant`: `default | line | capsule`; controlled `value` + `onValueChange`, or uncontrolled `defaultValue`), `Tabs.List`, `Tabs.Tab` (`value`), `Tabs.Panel` (`value`). Note: the sub-component is **`Tabs.Tab`**, not `Tabs.Trigger`. +**Example:** + + + +**Used in patterns:** `list-dense-scan` (bucket tabs composed **above** `DataTable.Root`, synced to `useCollectionVariables` — see the `DataTable` "Bucket tabs" note), `detail/*` (sectioned record content). + +**Notes:** For lists, AppShell's own filtering surface is **toolbar chips** (`DataTable.Filters`), not tabs — reach for `Tabs` only when the business genuinely thinks in a small set of named buckets. Don't render a `Tab` per enum value where a filter chip belongs. + --- ## Display @@ -274,48 +226,31 @@ import { Button, Link } from '@tailor-platform/app-shell'; > 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. +**API:** `BadgeProps` — `variant` (15 total): + +- **Filled** (high emphasis): `default` (primary), `success`, `warning`, `error`, `neutral`, `info` +- **Subtle** (low emphasis, tinted): `subtle-success`, `subtle-warning`, `subtle-error`, `subtle-info` +- **Outline** (renders a status dot — for row/list statuses): `outline-success`, `outline-warning`, `outline-error`, `outline-info`, `outline-neutral` + +Plus `badgeVariants` CVA for custom-styled siblings. **Example:** -```tsx -Active -``` + + +**Used in patterns:** `list/*` (status column → `outline-*`), `detail/*` (header status). -**Used in patterns:** `list/*` (status column), `detail/*` (header status). +**Notes:** Encode status by **semantic color** with a primary/secondary split: a record's **primary/lifecycle status** uses a **filled** semantic variant (one per row / one in a detail header); **secondary statuses** (delivery, billing) use **`outline-*`** (status dot); tags use **`subtle-*`**. Reserve **`default`** (brand) for non-status emphasis. Full rule: **`design-system.md`** → Composition & emphasis rules. ### `Table` > Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/table.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/table.md) -**Import:** `import { Table } from '@tailor-platform/app-shell'` **Purpose:** Semantic data table with scrollable container. -**API:** Compound — `Table.Root`, `Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Head`, `Table.Cell`, `Table.Caption`. `Table.Root` accepts `containerClassName` for the outer wrapper, `className` for the inner ``. +**API:** Compound — `Table.Root`, `Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Head`, `Table.Cell`, `Table.Caption`. `Table.Root` accepts `containerClassName` for the outer wrapper, `className` for the inner `
`. `Table.Head` and `Table.Cell` accept **`align`** (`"left" | "center" | "right"`, default `"left"`) — prefer this over ad-hoc `className="astw:text-right"` and pair the same alignment on the head and its column's cells (right for numeric/money, center for compact status/icon columns). **Example:** -```tsx - - - - Order - Status - Total - - - - {orders.map((o) => ( - - {o.number} - - {o.status} - - {formatMoney(o.total)} - - ))} - - -``` + **Used in patterns:** `list-dense-scan` hand-built subsets / static tables (`DataTable` is preferred for wired lists). @@ -328,39 +263,15 @@ import { Button, Link } from '@tailor-platform/app-shell'; > Full API: [https://github.com/tailor-platform/app-shell/blob/main/docs/components/data-table.md](https://github.com/tailor-platform/app-shell/blob/main/docs/components/data-table.md) -**Import:** compound namespace + helpers from `'@tailor-platform/app-shell'`, e.g. `DataTable`, `useDataTable`, `useCollectionVariables`, `createColumnHelper`, and types such as `Column`, `UseDataTableReturn`. - **Purpose:** Production list screens over GraphQL **connections**. Owns toolbar filter chips (**`DataTable.Filters`** from column `filter` configs), header sort, **`DataTable.Pagination`** (cursor-first; First/Last when `total` is provided), loading skeleton/error row, **`onClickRow`**, **`rowActions`** (kebab column), **`onSelectionChange`** (checkbox column). **Primitives:** Builds on low-level **`Table`**; do not reinvent pagination/filters manually unless the dataset is trivial. **Shape:** -```tsx -const { variables, control } = useCollectionVariables({ - params: { pageSize: 20 }, - // tableMetadata: tableMetadata.po, // typed vars when generated -}); - -const table = useDataTable({ - columns, - data: fetching ? undefined : mappedFromQuery, - loading: fetching, - control, - onClickRow: (row) => navigate(detailHref(row)), - // onSelectionChange, rowActions, sort: … -}); - - - - - - - - - -; -``` + + +**Column alignment:** each `Column` accepts **`align`** (`"left" | "right"`) applied to both header and body cell. Numeric `type` columns (`"number"`, `"money"`) default to `"right"` automatically so digits align on the decimal place — pass `align="left"` to opt out; everything else defaults to `"left"`. **Metadata path:** Prefer `createColumnHelper` + `inferColumns(tableMetadata.order)` (`@tailor-platform/app-shell-sdk-plugin` codegen) when available so enum/datetime/string filters bind to the right editors. @@ -370,21 +281,11 @@ const table = useDataTable({ ### `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). @@ -394,118 +295,63 @@ const table = useDataTable({ **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:** +**Example:** metric tiles are laid out in a `Grid` (never one-per-row): -```tsx -} -/> -``` + **Used in patterns:** KPI tiles, dashboards, **`detail/*`** metric strips where specs call for them. +**Notes:** Always wrap metric tiles in **`Grid`** (see the `Grid` entry) — a column of single-width `MetricCard`s stacked one-per-row is an anti-pattern. + ### `DocumentProgressCard` -**Import:** `import { DocumentProgressCard } from '@tailor-platform/app-shell'` -**Purpose:** Generic document lifecycle/fulfilment state — optional percentage, stacked progress bar, status legend; arbitrary `segments`. -**API:** `DocumentProgressCardProps` — `segments` (`{ label, value, color? }[]`), `title?`, `percent?`, `legend?` (defaults to `segments`), `total?` (bar denominator; larger than the sum leaves an empty track), `className?`. View-only — `percent` is explicit. +**Purpose:** Presentational card for a document's lifecycle / fulfilment state — an optional headline percentage, a stacked progress bar, and a status legend. Domain-agnostic and view-only: pass an arbitrary set of `segments` (e.g. shipped / returned / pending) plus an explicit `percent`; derive both in the consumer. +**API:** `DocumentProgressCardProps` — `segments` (required; array of `{ label, value, color }`), `percent?` (0–100, headline shown top-right), `title?`, `legend?` (defaults to `segments`; supply only when the legend should differ from the bar, e.g. overlapping buckets), `total?` (bar denominator; defaults to the sum of segment values — a larger value leaves an unfilled remainder), `className?`. Segment `color`: `indigo | pink | green | amber | red | blue | neutral`. **Example:** -```tsx - -``` + -**Used in patterns:** **`detail/*`** right-rail cards for arbitrary status breakdowns (shipped/cancelled/pending, PO received/returned/yet-to-receive, etc.). For the purchase-order fulfilment recipe (derived %, net-received/returned bar split, legend override), see `docs/components/document-progress-card.md`. +**Used in patterns:** `detail/*` — fulfilment / lifecycle summary cards (e.g. a purchase-order receipt progress) in the main column or right rail. ### `Avatar` **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`. +**API:** `DescriptionCardProps` — `data`, `title`, `fields` (array of `{ key, label, type?, meta?, emptyBehavior? }`), `columns` (3 | 4), `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`. +**API:** `ActionPanelProps` — `title`, `actions` (array of `{ key, label, icon, onClick?, variant?, disabled?, loading? }`), `className`. **Example:** -```tsx - -``` + **Used in patterns:** `detail/hero-with-actions`. @@ -516,19 +362,33 @@ const table = useDataTable({ ### `ActivityCard` **Import:** `import { ActivityCard } from '@tailor-platform/app-shell'` + **Purpose:** Timeline of events on a record (audit log, status changes, comments). **API:** Compound — `ActivityCard.Root`, `ActivityCard.Items` (generic over item type), plus `ActivityCardProps`, `ActivityCardItem`, `ActivityCardItemProps`. Items render with timestamp + actor + description. **Used in patterns:** `detail/hero-with-actions` (right column or bottom section). +### `Alert` + +**Purpose:** Inline, in-page banner for a persistent status message — a form/record error, a warning, or a success/info notice attached to a section. (Transient notifications use `useToast`; blocking confirmations use `Dialog` — see `interaction/confirm`.) +**API:** Compound — `Alert.Root` (`variant`: `neutral | success | warning | error | info`, default `neutral`; a matching icon is rendered automatically; optional `action?: ReactNode`, `dismissible?: boolean`, `onDismiss?: () => void`), `Alert.Title`, `Alert.Description`. +**Example:** + + + +**Used in patterns:** any data-backed screen's **error** state (inline error + retry) and record-level warnings — the error affordance the composition rules require (**`design-system.md`** → Composition & emphasis rules → States). + +**Notes:** Encode severity by `variant` (semantic color), same discipline as `Badge` — don't use `error` for a routine notice. Pair a retry/next-step control via `action` rather than a separate stray button. + --- ## Forms ### `Form` +**Import:** `import { Form } from '@tailor-platform/app-shell'` + > 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`. @@ -537,6 +397,7 @@ const table = useDataTable({ ### `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/*`. @@ -544,6 +405,7 @@ const table = useDataTable({ ### `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`. @@ -551,6 +413,7 @@ const table = useDataTable({ ### `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/*`. @@ -558,13 +421,16 @@ const table = useDataTable({ ### `Select` **Import:** `import { Select } from '@tailor-platform/app-shell'` + **Purpose:** Dropdown for fixed enumerations. **API:** Compound. Sync version (small option lists). For async/typeahead, see `Combobox` and `Autocomplete`. Type: `SelectAsyncFetcher` for the async variant. +**Accessible name:** inside a `Form` the field label provides it automatically. When used **standalone** (outside a `Form`), pass `aria-label` (or `aria-labelledby` / `id`) — these now forward to the underlying combobox element, so it isn't left named only by its current value. Same applies to `Combobox` and `Autocomplete`. **Used in patterns:** all `form/*` (enum fields). ### `Combobox` **Import:** `import { Combobox } from '@tailor-platform/app-shell'` + **Purpose:** Single-select with search, supports async data fetching. **API:** Compound. `ComboboxAsyncFetcher` for paginated/queried option sources (e.g. lookups). **Used in patterns:** `form/*` (foreign-key lookups). @@ -572,6 +438,7 @@ const table = useDataTable({ ### `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). @@ -583,6 +450,7 @@ const table = useDataTable({ ### `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`. @@ -590,6 +458,7 @@ const table = useDataTable({ ### `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. @@ -597,6 +466,7 @@ const table = useDataTable({ ### `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. @@ -604,11 +474,13 @@ const table = useDataTable({ ### `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). @@ -616,6 +488,7 @@ const table = useDataTable({ ### 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. @@ -630,19 +503,21 @@ Types for authoring guard functions used by `WithGuard` and `appShellPageProps.g ### `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. +Types for declaring page props (`appShellPageProps = { meta, guards }`), reading typed route params, and registering route types globally. See `project-setup.md` for usage. --- @@ -651,6 +526,7 @@ Types for declaring page props (`appShellPageProps = { meta, guards, loader }`), ### `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. @@ -658,6 +534,7 @@ Types for declaring page props (`appShellPageProps = { meta, guards, loader }`), ### `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. @@ -665,12 +542,14 @@ Types for declaring page props (`appShellPageProps = { meta, guards, loader }`), ### `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). @@ -681,12 +560,14 @@ Types for declaring page props (`appShellPageProps = { meta, guards, loader }`), ### `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`. @@ -702,12 +583,14 @@ Types returned/consumed by the define-\* helpers above. ### `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. --- diff --git a/catalogue/src/fundamental/design-system.md b/catalogue/src/fundamental/design-system.md index 48bbed82..575febe2 100644 --- a/catalogue/src/fundamental/design-system.md +++ b/catalogue/src/fundamental/design-system.md @@ -54,7 +54,7 @@ AppShell's UI components support data-attribute-based styling, following the [Ba ```css /* Style a component based on its state */ -.SwitchThumb[data-checked] { +.ToggleThumb[data-checked] { background-color: green; } @@ -66,9 +66,7 @@ AppShell's UI components support data-attribute-based styling, following the [Ba 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). @@ -96,14 +94,7 @@ Four families. Pick by **intent**, not by visual taste. | 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. @@ -123,13 +114,7 @@ Linear scale on a 4px base. Padding, margin, and gap all come from this scale. T | `--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. @@ -150,11 +135,7 @@ Named roles. Each token bundles font-size + line-height + weight. Pick by **role | `caption` | `text-caption` | timestamps, labels, metadata | | `mono` | `text-mono` | IDs, code, numbers in tables | -```tsx -

Section

-

Description copy

-Updated 2h ago -``` + ### Radius @@ -220,7 +201,7 @@ Never invent a z value. If you need a new layer, add a token; never `z-index: 99 ### Icon sizes -Pair icon size with the surrounding text scale. Pass via the `size` prop, not raw width/height. +Pair icon size with the surrounding text scale. Use token-backed width/height (or a component-specific `size` prop), not raw dimensions. | Token | Pairs with text | | ----------- | --------------------- | @@ -229,9 +210,7 @@ Pair icon size with the surrounding text scale. Pass via the `size` prop, not ra | `--icon-lg` | `h3`, `h4` | | `--icon-xl` | `h1`, `h2`, `display` | -```tsx - -``` + ### Breakpoints @@ -249,16 +228,13 @@ Two-column **behavior** (right rail stacks under `**lg`**): respect AppShell def ## 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. +AppShell exposes **layout / sizing / overflow** escapes on some components via props like `containerClassName` and `className` on exposed parts. 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: @@ -272,7 +248,7 @@ Most ERP screens compose entirely from AppShell primitives. When you hit a gap, ### 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. +1. **Can you compose existing AppShell primitives?** A summary surface is usually just `Card` + `Badge` + `Button`, 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. @@ -280,43 +256,15 @@ Most ERP screens compose entirely from AppShell primitives. When you hit a gap, - **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 ` + + + ); +} diff --git a/catalogue/src/fundamental/examples/components/card-bare-table.example.tsx b/catalogue/src/fundamental/examples/components/card-bare-table.example.tsx new file mode 100644 index 00000000..fae251c1 --- /dev/null +++ b/catalogue/src/fundamental/examples/components/card-bare-table.example.tsx @@ -0,0 +1,15 @@ +import { Card, Table } from "@tailor-platform/app-shell"; + +export function CardBareTableExample() { + return ( + + + + + PO-1001 + + + + + ); +} diff --git a/catalogue/src/fundamental/examples/components/card-basic.example.tsx b/catalogue/src/fundamental/examples/components/card-basic.example.tsx new file mode 100644 index 00000000..cb710ff2 --- /dev/null +++ b/catalogue/src/fundamental/examples/components/card-basic.example.tsx @@ -0,0 +1,16 @@ +import { Button, Card } from "@tailor-platform/app-shell"; + +export function CardBasicExample() { + return ( + + + + + +

Summary content

+
+
+ ); +} diff --git a/catalogue/src/fundamental/examples/components/card-header-table.example.tsx b/catalogue/src/fundamental/examples/components/card-header-table.example.tsx new file mode 100644 index 00000000..42eb2bd8 --- /dev/null +++ b/catalogue/src/fundamental/examples/components/card-header-table.example.tsx @@ -0,0 +1,18 @@ +import { Card, Table } from "@tailor-platform/app-shell"; + +export function CardHeaderTableExample() { + return ( + + + + + + + Widget A + + + + + + ); +} diff --git a/catalogue/src/fundamental/examples/components/card-table-dont.example.tsx b/catalogue/src/fundamental/examples/components/card-table-dont.example.tsx new file mode 100644 index 00000000..7ed4a0cf --- /dev/null +++ b/catalogue/src/fundamental/examples/components/card-table-dont.example.tsx @@ -0,0 +1,11 @@ +import { Card, Table } from "@tailor-platform/app-shell"; + +export function CardTableDontExample() { + return ( + + + {/* missing containerClassName="astw:px-6" */} + + + ); +} diff --git a/catalogue/src/fundamental/examples/components/data-table-shape.example.tsx b/catalogue/src/fundamental/examples/components/data-table-shape.example.tsx new file mode 100644 index 00000000..54d8bd5f --- /dev/null +++ b/catalogue/src/fundamental/examples/components/data-table-shape.example.tsx @@ -0,0 +1,90 @@ +import { + Badge, + DataTable, + createColumnHelper, + useCollectionVariables, + useDataTable, +} from "@tailor-platform/app-shell"; +import { useMemo, useState } from "react"; + +type OrderStatus = "Draft" | "Confirmed"; + +type OrderRow = { + id: string; + number: string; + status: OrderStatus; + total: number; +}; + +const sampleRows: OrderRow[] = [ + { id: "1", number: "PO-1001", status: "Draft", total: 1200 }, + { id: "2", number: "PO-1002", status: "Confirmed", total: 3400 }, + { id: "3", number: "PO-1003", status: "Confirmed", total: 980 }, +]; + +const statusVariants: Record = { + Draft: "warning", + Confirmed: "success", +}; + +const { column } = createColumnHelper(); + +const columns = [ + column({ + id: "number", + label: "Order", + type: "text", + accessor: (row) => row.number, + }), + column({ + id: "status", + label: "Status", + render: (row) => {row.status}, + }), + column({ + id: "total", + label: "Total", + type: "money", + accessor: (row) => row.total, + typeOptions: { currency: "USD" }, + }), +]; + +export function DataTableShapeExample() { + const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } }); + const [selectedOrderId, setSelectedOrderId] = useState(null); + const pageSize = variables.pagination.first ?? 20; + const rows = useMemo(() => sampleRows.slice(0, pageSize), [pageSize]); + + const table = useDataTable({ + columns, + data: { + rows, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + total: rows.length, + }, + loading: false, + control, + onClickRow: (row) => setSelectedOrderId(row.id), + }); + + return ( + <> + + + + + + + + + + {selectedOrderId ?

Selected: {selectedOrderId}

: null} + + ); +} diff --git a/catalogue/src/fundamental/examples/components/description-card.example.tsx b/catalogue/src/fundamental/examples/components/description-card.example.tsx new file mode 100644 index 00000000..fce8181e --- /dev/null +++ b/catalogue/src/fundamental/examples/components/description-card.example.tsx @@ -0,0 +1,27 @@ +import { DescriptionCard } from "@tailor-platform/app-shell"; + +const order = { + number: "PO-1234", + status: "Confirmed", + createdAt: "2026-07-09", +}; + +export function DescriptionCardExample() { + return ( + + ); +} diff --git a/catalogue/src/fundamental/examples/components/dialog-confirm-delete.example.tsx b/catalogue/src/fundamental/examples/components/dialog-confirm-delete.example.tsx new file mode 100644 index 00000000..34458051 --- /dev/null +++ b/catalogue/src/fundamental/examples/components/dialog-confirm-delete.example.tsx @@ -0,0 +1,23 @@ +import { Button, Dialog } from "@tailor-platform/app-shell"; + +export function DialogConfirmDeleteExample() { + const onDelete = () => {}; + + return ( + + }>Delete + + + Delete order #1234? + This cannot be undone. + + + }>Cancel + + + + + ); +} diff --git a/catalogue/src/fundamental/examples/components/document-progress-card.example.tsx b/catalogue/src/fundamental/examples/components/document-progress-card.example.tsx new file mode 100644 index 00000000..7abaa5d9 --- /dev/null +++ b/catalogue/src/fundamental/examples/components/document-progress-card.example.tsx @@ -0,0 +1,15 @@ +import { DocumentProgressCard } from "@tailor-platform/app-shell"; + +export function DocumentProgressCardExample() { + return ( + + ); +} diff --git a/catalogue/src/fundamental/examples/components/grid-metrics.example.tsx b/catalogue/src/fundamental/examples/components/grid-metrics.example.tsx new file mode 100644 index 00000000..2d888f8c --- /dev/null +++ b/catalogue/src/fundamental/examples/components/grid-metrics.example.tsx @@ -0,0 +1,12 @@ +import { Grid, MetricCard } from "@tailor-platform/app-shell"; + +export function GridMetricsExample() { + return ( + + + + + + + ); +} diff --git a/catalogue/src/fundamental/examples/components/layout-detail-columns.example.tsx b/catalogue/src/fundamental/examples/components/layout-detail-columns.example.tsx new file mode 100644 index 00000000..32787682 --- /dev/null +++ b/catalogue/src/fundamental/examples/components/layout-detail-columns.example.tsx @@ -0,0 +1,79 @@ +import { + ActionPanel, + ActivityCard, + Button, + DescriptionCard, + Layout, +} from "@tailor-platform/app-shell"; + +const order = { + number: "PO-1234", + supplier: "Supplier ABC", + status: "Confirmed", + createdAt: "2026-07-09", +}; + +const items = [ + { + id: "1", + actor: { name: "Hanna" }, + description: "approved this order", + timestamp: new Date("2026-07-09T09:00:00Z"), + }, + { + id: "2", + description: "created this order", + timestamp: new Date("2026-07-08T15:16:00Z"), + }, +]; + +const dot =
`. `Table.Head` and `Table.Cell` accept **`align`** (`"left" | "center" | "right"`, default `"left"`) — prefer this over ad-hoc `className="astw:text-right"` and pair the same alignment on the head and its column's cells (right for numeric/money, center for compact status/icon columns). **Example:** ```tsx - - - - Order - Status - Total - - - - {orders.map((o) => ( - - {o.number} - - {o.status} - - {formatMoney(o.total)} - - ))} - - +import { Badge, Table } from "@tailor-platform/app-shell"; + +type OrderStatus = "Confirmed" | "Draft" | "Overdue"; + +type OrderRow = { + id: string; + number: string; + status: OrderStatus; + total: number; +}; + +const orders: OrderRow[] = [ + { id: "1", number: "PO-1001", status: "Confirmed", total: 1500 }, + { id: "2", number: "PO-1002", status: "Draft", total: 750 }, + { id: "3", number: "PO-1003", status: "Overdue", total: 420 }, +]; + +const statusVariants: Record = { + Confirmed: "success", + Draft: "neutral", + Overdue: "error", +}; + +const formatMoney = (amount: number) => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); + +export function TableOrdersExample() { + return ( + + + + Order + Status + Total + + + + {orders.map((order) => ( + + {order.number} + + {order.status} + + {formatMoney(order.total)} + + ))} + + + ); +} ``` **Used in patterns:** `list-dense-scan` hand-built subsets / static tables (`DataTable` is preferred for wired lists). @@ -385,8 +555,6 @@ Plus `badgeVariants` CVA for custom-styled siblings. > 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. @@ -394,29 +562,96 @@ Plus `badgeVariants` CVA for custom-styled siblings. **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: … -}); - - - - - - - - - -; +import { + Badge, + DataTable, + createColumnHelper, + useCollectionVariables, + useDataTable, +} from "@tailor-platform/app-shell"; +import { useMemo, useState } from "react"; + +type OrderStatus = "Draft" | "Confirmed"; + +type OrderRow = { + id: string; + number: string; + status: OrderStatus; + total: number; +}; + +const sampleRows: OrderRow[] = [ + { id: "1", number: "PO-1001", status: "Draft", total: 1200 }, + { id: "2", number: "PO-1002", status: "Confirmed", total: 3400 }, + { id: "3", number: "PO-1003", status: "Confirmed", total: 980 }, +]; + +const statusVariants: Record = { + Draft: "warning", + Confirmed: "success", +}; + +const { column } = createColumnHelper(); + +const columns = [ + column({ + id: "number", + label: "Order", + type: "text", + accessor: (row) => row.number, + }), + column({ + id: "status", + label: "Status", + render: (row) => {row.status}, + }), + column({ + id: "total", + label: "Total", + type: "money", + accessor: (row) => row.total, + typeOptions: { currency: "USD" }, + }), +]; + +export function DataTableShapeExample() { + const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } }); + const [selectedOrderId, setSelectedOrderId] = useState(null); + const pageSize = variables.pagination.first ?? 20; + const rows = useMemo(() => sampleRows.slice(0, pageSize), [pageSize]); + + const table = useDataTable({ + columns, + data: { + rows, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + total: rows.length, + }, + loading: false, + control, + onClickRow: (row) => setSelectedOrderId(row.id), + }); + + return ( + <> + + + + + + + + + + {selectedOrderId ?

Selected: {selectedOrderId}

: null} + + ); +} ``` **Column alignment:** each `Column` accepts **`align`** (`"left" | "right"`) applied to both header and body cell. Numeric `type` columns (`"number"`, `"money"`) default to `"right"` automatically so digits align on the decimal place — pass `align="left"` to opt out; everything else defaults to `"left"`. @@ -429,20 +664,27 @@ const table = useDataTable({ ### `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 */} - +import { Button, Card } from "@tailor-platform/app-shell"; + +export function CardBasicExample() { + return ( + + + + + +

Summary content

+
+
+ ); +} ``` **Used in patterns:** `detail/*` (related-data sections), `form/wizard` (step container). @@ -454,51 +696,86 @@ const table = useDataTable({ **Bare table-in-card (list pages, no card-level title):** ```tsx - - {/* … */} - +import { Card, Table } from "@tailor-platform/app-shell"; + +export function CardBareTableExample() { + return ( + + + + + PO-1001 + + + + + ); +} ``` **Header + table (detail-page sections like "Line items"):** ```tsx - - - - {/* … */} - - +import { Card, Table } from "@tailor-platform/app-shell"; + +export function CardHeaderTableExample() { + return ( + + + + + + + Widget A + + + + + + ); +} ``` **DON'T — first column lands flush against the card edge:** ```tsx - - - {/* missing containerClassName="astw:px-6" */} - - +import { Card, Table } from "@tailor-platform/app-shell"; + +export function CardTableDontExample() { + return ( + + + {/* missing containerClassName="astw:px-6" */} + + + ); +} ``` ### `MetricCard` -**Import:** `import { MetricCard } from '@tailor-platform/app-shell'` **Purpose:** KPI tile for dashboards or hero metric strip. **API:** `MetricCardProps` — `title`, `value`, `trend: { direction, value }`, `description`, `icon`. **Example:** metric tiles are laid out in a `Grid` (never one-per-row): ```tsx - - } - /> - - {/* …more tiles */} - +import { Grid, MetricCard } from "@tailor-platform/app-shell"; + +export function MetricCardGridExample() { + return ( + + + + + + + ); +} ``` **Used in patterns:** KPI tiles, dashboards, **`detail/*`** metric strips where specs call for them. @@ -507,21 +784,26 @@ const table = useDataTable({ ### `DocumentProgressCard` -**Import:** `import { DocumentProgressCard } from '@tailor-platform/app-shell'` **Purpose:** Presentational card for a document's lifecycle / fulfilment state — an optional headline percentage, a stacked progress bar, and a status legend. Domain-agnostic and view-only: pass an arbitrary set of `segments` (e.g. shipped / returned / pending) plus an explicit `percent`; derive both in the consumer. **API:** `DocumentProgressCardProps` — `segments` (required; array of `{ label, value, color }`), `percent?` (0–100, headline shown top-right), `title?`, `legend?` (defaults to `segments`; supply only when the legend should differ from the bar, e.g. overlapping buckets), `total?` (bar denominator; defaults to the sum of segment values — a larger value leaves an unfilled remainder), `className?`. Segment `color`: `indigo | pink | green | amber | red | blue | neutral`. **Example:** ```tsx - +import { DocumentProgressCard } from "@tailor-platform/app-shell"; + +export function DocumentProgressCardExample() { + return ( + + ); +} ``` **Used in patterns:** `detail/*` — fulfilment / lifecycle summary cards (e.g. a purchase-order receipt progress) in the main column or right rail. @@ -529,47 +811,78 @@ const table = useDataTable({ ### `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`. +**API:** `DescriptionCardProps` — `data`, `title`, `fields` (array of `{ key, label, type?, meta?, emptyBehavior? }`), `columns` (3 | 4), `headerAction`. **Example:** ```tsx - {v} }, - { key: "createdAt", label: "Created", render: formatDate }, - ]} -/> +import { DescriptionCard } from "@tailor-platform/app-shell"; + +const order = { + number: "PO-1234", + status: "Confirmed", + createdAt: "2026-07-09", +}; + +export function DescriptionCardExample() { + return ( + + ); +} ``` **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`. +**API:** `ActionPanelProps` — `title`, `actions` (array of `{ key, label, icon, onClick?, variant?, disabled?, loading? }`), `className`. **Example:** ```tsx - +import { ActionPanel } from "@tailor-platform/app-shell"; + +const dot =