@exxatdesignux/ui 0.5.11 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/bin/cli.mjs +70 -1
- package/bin/init.mjs +18 -4
- package/bin/sync-extras.mjs +28 -4
- package/consumer-extras/README.md +41 -5
- package/consumer-extras/cursor-rules/exxat-accessibility.mdc +2 -1
- package/consumer-extras/cursor-rules/exxat-board-cards.mdc +5 -3
- package/consumer-extras/cursor-rules/exxat-breadcrumbs-no-back.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-card-vs-list-rows.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +4 -2
- package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-command-menu.mdc +3 -2
- package/consumer-extras/cursor-rules/exxat-data-tables.mdc +5 -3
- package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +7 -0
- package/consumer-extras/cursor-rules/exxat-drawer-vs-dialog.mdc +2 -1
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-fontawesome-icons.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-hub-supported-views.mdc +6 -4
- package/consumer-extras/cursor-rules/exxat-kbd-shortcuts.mdc +6 -5
- package/consumer-extras/cursor-rules/exxat-kpi-flat-band.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-kpi-max-four.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-kpi-trends.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +2 -1
- package/consumer-extras/cursor-rules/exxat-list-page-connected-views.mdc +6 -2
- package/consumer-extras/cursor-rules/exxat-list-page-view-shells.mdc +2 -1
- package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +2 -1
- package/consumer-extras/cursor-rules/exxat-nav-single-active.mdc +4 -3
- package/consumer-extras/cursor-rules/exxat-no-image-pixel-copy.mdc +25 -14
- package/consumer-extras/cursor-rules/exxat-no-slds-leakage.mdc +8 -2
- package/consumer-extras/cursor-rules/exxat-no-toast.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-no-vaul.mdc +2 -1
- package/consumer-extras/cursor-rules/exxat-page-header-actions.mdc +6 -4
- package/consumer-extras/cursor-rules/exxat-page-vs-drawer.mdc +2 -1
- package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +2 -1
- package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-sidebar-shell.mdc +13 -7
- package/consumer-extras/cursor-rules/exxat-table-properties-drawer.mdc +5 -3
- package/consumer-extras/cursor-rules/exxat-table-row-preview.mdc +1 -0
- package/consumer-extras/cursor-rules/exxat-tabs-chrome.mdc +6 -4
- package/consumer-extras/cursor-rules/exxat-token-discipline.mdc +6 -0
- package/consumer-extras/cursor-rules/exxat-ux-discovery-protocol.mdc +28 -0
- package/consumer-extras/cursor-rules/exxat-ux-principles.mdc +1 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +5 -3
- package/consumer-extras/patterns/command-menu-pattern.md +2 -2
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +1 -1
- package/consumer-extras/patterns/jobs/README.md +1 -1
- package/consumer-extras/patterns/perf-memory-pattern.md +115 -150
- package/consumer-extras/scripts/dev-guard.mjs +156 -0
- package/consumer-extras/templates/README.md +23 -0
- package/consumer-extras/templates/handoff.md +190 -0
- package/package.json +2 -3
- package/{template → template-vite}/.claude/skills/exxat-ds-skill/SKILL.md +184 -23
- package/template-vite/.cursor/rules/exxat-accessibility.mdc +40 -0
- package/template-vite/.cursor/rules/exxat-board-cards.mdc +28 -0
- package/template-vite/.cursor/rules/exxat-breadcrumbs-no-back.mdc +22 -0
- package/template-vite/.cursor/rules/exxat-card-vs-list-rows.mdc +22 -0
- package/template-vite/.cursor/rules/exxat-centralized-list-dataset.mdc +46 -0
- package/template-vite/.cursor/rules/exxat-collaboration-access.mdc +33 -0
- package/{template → template-vite}/.cursor/rules/exxat-command-menu.mdc +5 -5
- package/template-vite/.cursor/rules/exxat-data-tables.mdc +47 -0
- package/template-vite/.cursor/rules/exxat-dedicated-search-surfaces.mdc +32 -0
- package/template-vite/.cursor/rules/exxat-drawer-vs-dialog.mdc +23 -0
- package/template-vite/.cursor/rules/exxat-ds-agents.mdc +87 -0
- package/template-vite/.cursor/rules/exxat-fontawesome-icons.mdc +32 -0
- package/template-vite/.cursor/rules/exxat-hub-supported-views.mdc +56 -0
- package/{template → template-vite}/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -0
- package/template-vite/.cursor/rules/exxat-kpi-flat-band.mdc +29 -0
- package/template-vite/.cursor/rules/exxat-kpi-max-four.mdc +22 -0
- package/template-vite/.cursor/rules/exxat-kpi-trends.mdc +32 -0
- package/template-vite/.cursor/rules/exxat-library-hub-header.mdc +29 -0
- package/template-vite/.cursor/rules/exxat-list-page-connected-views.mdc +28 -0
- package/template-vite/.cursor/rules/exxat-list-page-view-shells.mdc +32 -0
- package/{template → template-vite}/.cursor/rules/exxat-mono-ids.mdc +1 -0
- package/template-vite/.cursor/rules/exxat-nav-single-active.mdc +32 -0
- package/template-vite/.cursor/rules/exxat-no-image-pixel-copy.mdc +46 -0
- package/template-vite/.cursor/rules/exxat-no-slds-leakage.mdc +84 -0
- package/{template → template-vite}/.cursor/rules/exxat-no-toast.mdc +2 -2
- package/template-vite/.cursor/rules/exxat-no-vaul.mdc +26 -0
- package/template-vite/.cursor/rules/exxat-page-header-actions.mdc +33 -0
- package/{template → template-vite}/.cursor/rules/exxat-page-vs-drawer.mdc +5 -3
- package/template-vite/.cursor/rules/exxat-person-identity-display.mdc +48 -0
- package/template-vite/.cursor/rules/exxat-primary-nav-secondary-panel.mdc +53 -0
- package/template-vite/.cursor/rules/exxat-reuse-before-custom.mdc +37 -0
- package/template-vite/.cursor/rules/exxat-sidebar-shell.mdc +41 -0
- package/template-vite/.cursor/rules/exxat-table-properties-drawer.mdc +79 -0
- package/template-vite/.cursor/rules/exxat-table-row-preview.mdc +25 -0
- package/template-vite/.cursor/rules/exxat-tabs-chrome.mdc +33 -0
- package/template-vite/.cursor/rules/exxat-token-discipline.mdc +109 -0
- package/template-vite/.cursor/rules/exxat-ux-discovery-protocol.mdc +202 -0
- package/template-vite/.cursor/rules/exxat-ux-principles.mdc +187 -0
- package/template-vite/.cursor/skills/exxat-accessibility/SKILL.md +282 -0
- package/template-vite/.cursor/skills/exxat-board-cards/SKILL.md +68 -0
- package/template-vite/.cursor/skills/exxat-card-vs-list-rows/SKILL.md +20 -0
- package/template-vite/.cursor/skills/exxat-centralized-list-dataset/SKILL.md +99 -0
- package/template-vite/.cursor/skills/exxat-collaboration-access/SKILL.md +35 -0
- package/template-vite/.cursor/skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
- package/template-vite/.cursor/skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
- package/template-vite/.cursor/skills/exxat-ds-skill/SKILL.md +893 -0
- package/template-vite/.cursor/skills/exxat-ds-skill/references/accessibility.md +142 -0
- package/template-vite/.cursor/skills/exxat-ds-skill/references/coach-marks.md +169 -0
- package/template-vite/.cursor/skills/exxat-ds-skill/references/data-table-pattern.md +392 -0
- package/template-vite/.cursor/skills/exxat-fontawesome-icons/SKILL.md +31 -0
- package/template-vite/.cursor/skills/exxat-kpi-flat-band/SKILL.md +38 -0
- package/template-vite/.cursor/skills/exxat-kpi-max-four/SKILL.md +19 -0
- package/template-vite/.cursor/skills/exxat-kpi-trends/SKILL.md +29 -0
- package/template-vite/.cursor/skills/exxat-list-page-view-shells/SKILL.md +36 -0
- package/template-vite/.cursor/skills/exxat-mono-ids/SKILL.md +56 -0
- package/template-vite/.cursor/skills/exxat-primary-nav-secondary-panel/SKILL.md +49 -0
- package/template-vite/.cursor/skills/exxat-senior-ux/SKILL.md +198 -0
- package/template-vite/.cursor/skills/exxat-token-economy/SKILL.md +287 -0
- package/template-vite/.cursor/skills/exxat-ux-audit/SKILL.md +303 -0
- package/{template → template-vite}/components/ask-leo-sidebar.tsx +10 -8
- package/{template → template-vite}/components/command-menu.tsx +1 -1
- package/{template → template-vite}/components/data-views/library-folder-tree-branch.tsx +1 -1
- package/{template → template-vite}/components/dedicated-search-recents.tsx +1 -1
- package/{template → template-vite}/components/dedicated-search-url-composer.tsx +1 -1
- package/{template → template-vite}/components/exxat-product-logo.tsx +3 -3
- package/{template → template-vite}/components/library-client.tsx +1 -1
- package/{template → template-vite}/components/library-hub-client.tsx +2 -2
- package/{template → template-vite}/components/library-secondary-nav.tsx +2 -2
- package/{template → template-vite}/components/library-table.tsx +35 -27
- package/{template → template-vite}/components/new-library-item-form.tsx +1 -1
- package/{template → template-vite}/components/page-breadcrumb-trail.tsx +1 -1
- package/{template → template-vite}/components/settings-client.tsx +1 -1
- package/{template → template-vite}/components/sidebar/app-sidebar.tsx +2 -2
- package/{template → template-vite}/components/sidebar/nav-main.tsx +1 -1
- package/{template → template-vite}/components/sidebar/nav-user.tsx +1 -1
- package/{template → template-vite}/components/sidebar/secondary-nav.tsx +1 -1
- package/{template → template-vite}/components/system-banner-slot.tsx +1 -1
- package/{template → template-vite}/components/templates/discovery-hub-template.tsx +2 -2
- package/{template → template-vite}/components/templates/new-focus-template.tsx +1 -1
- package/{template → template-vite}/components/tokens-secondary-nav.tsx +2 -2
- package/{template → template-vite}/components/tokens-themes-client.tsx +1 -1
- package/{template → template-vite}/hooks/use-secondary-panel-hub-nav.ts +1 -1
- package/template-vite/index.html +49 -0
- package/template-vite/lib/next-compat.tsx +98 -0
- package/{template → template-vite}/package.json +15 -27
- package/template-vite/scripts/port-next-imports.mjs +70 -0
- package/template-vite/src/App.tsx +103 -0
- package/template-vite/src/main.tsx +50 -0
- package/{template/app/(app)/error.tsx → template-vite/src/pages/_error.tsx} +12 -24
- package/{template/app/(app)/loading.tsx → template-vite/src/pages/_loading.tsx} +4 -2
- package/template-vite/src/pages/_not-found.tsx +17 -0
- package/template-vite/src/pages/dashboard.tsx +48 -0
- package/{template/app/(app)/help/page.tsx → template-vite/src/pages/help.tsx} +3 -2
- package/{template/app/(app)/library/layout.tsx → template-vite/src/pages/library/_layout.tsx} +18 -16
- package/{template/app/(app)/library/all/page.tsx → template-vite/src/pages/library/all.tsx} +1 -1
- package/{template/app/(app)/library/new/page.tsx → template-vite/src/pages/library/new.tsx} +12 -18
- package/template-vite/src/routes.tsx +72 -0
- package/template-vite/src/styles/globals.css +25 -0
- package/{template → template-vite}/tsconfig.json +5 -14
- package/template-vite/vite.config.ts +52 -0
- package/consumer-extras/cursor-rules/exxat-dashboard-view-charts.mdc +0 -53
- package/template/.agents/skills/shadcn/SKILL.md +0 -242
- package/template/.agents/skills/shadcn/agents/openai.yml +0 -5
- package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
- package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
- package/template/.agents/skills/shadcn/cli.md +0 -257
- package/template/.agents/skills/shadcn/customization.md +0 -202
- package/template/.agents/skills/shadcn/evals/evals.json +0 -47
- package/template/.agents/skills/shadcn/mcp.md +0 -94
- package/template/.agents/skills/shadcn/rules/base-vs-radix.md +0 -306
- package/template/.agents/skills/shadcn/rules/composition.md +0 -195
- package/template/.agents/skills/shadcn/rules/forms.md +0 -192
- package/template/.agents/skills/shadcn/rules/icons.md +0 -101
- package/template/.agents/skills/shadcn/rules/styling.md +0 -162
- package/template/.cursor/rules/exxat-accessibility.mdc +0 -33
- package/template/.cursor/rules/exxat-data-tables.mdc +0 -32
- package/template/.cursor/rules/exxat-ds-agents.mdc +0 -26
- package/template/.cursor/rules/exxat-list-page-connected-views.mdc +0 -16
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +0 -40
- package/template/.nvmrc +0 -1
- package/template/.prettierignore +0 -7
- package/template/Logo/Exxat_Prism.svg +0 -39
- package/template/Logo/Exxat_one.svg +0 -36
- package/template/app/(app)/dashboard/loading.tsx +0 -18
- package/template/app/(app)/dashboard/page.tsx +0 -36
- package/template/app/(app)/layout.tsx +0 -77
- package/template/app/global-error.tsx +0 -63
- package/template/app/globals.css +0 -20
- package/template/app/layout.tsx +0 -133
- package/template/app/page.tsx +0 -9
- package/template/docs/HANDBOOK.md +0 -187
- package/template/docs/blueprints/README.md +0 -86
- package/template/docs/blueprints/_template.md +0 -91
- package/template/docs/blueprints/board-card.md +0 -123
- package/template/docs/blueprints/data-table.md +0 -139
- package/template/docs/blueprints/key-metrics.md +0 -128
- package/template/docs/blueprints/list-page-template.md +0 -123
- package/template/docs/blueprints/page-header.md +0 -130
- package/template/docs/card-vs-rows-pattern.md +0 -36
- package/template/docs/collaboration-access-pattern.md +0 -116
- package/template/docs/command-menu-pattern.md +0 -45
- package/template/docs/component-selection-guide.md +0 -224
- package/template/docs/components-audit-2026-05.md +0 -158
- package/template/docs/consumer-upgrade-checklist.md +0 -52
- package/template/docs/data-views-pattern.md +0 -185
- package/template/docs/drawer-vs-dialog-pattern.md +0 -50
- package/template/docs/glossary.md +0 -59
- package/template/docs/hub-supported-views-pattern.md +0 -53
- package/template/docs/jobs/README.md +0 -59
- package/template/docs/jobs/record-detail.md +0 -177
- package/template/docs/kpi-flat-band-pattern.md +0 -57
- package/template/docs/kpi-strip-max-four-pattern.md +0 -30
- package/template/docs/kpi-trend-pattern.md +0 -58
- package/template/docs/large-dataset-strategy.md +0 -155
- package/template/docs/library-hub-header-pattern.md +0 -25
- package/template/docs/migrations/0001-brand-deep-alias-stabilization.md +0 -95
- package/template/docs/migrations/0002-exxat-token-namespace.md +0 -154
- package/template/docs/migrations/0003-globals-css-canonical.md +0 -110
- package/template/docs/migrations/README.md +0 -100
- package/template/docs/migrations/_template.md +0 -64
- package/template/docs/modern-saas-patterns.md +0 -165
- package/template/docs/perf-memory-pattern.md +0 -206
- package/template/docs/reference-implementations.md +0 -153
- package/template/docs/shell-surface-elevation-pattern.md +0 -52
- package/template/docs/token-taxonomy.md +0 -416
- package/template/docs/voice-and-tone.md +0 -262
- package/template/ecosystem.config.cjs +0 -32
- package/template/next.config.mjs +0 -216
- package/template/postcss.config.mjs +0 -8
- package/template/public/favicon/favicon.ico +0 -0
- package/template/tests/setup.ts +0 -26
- package/template/vitest.config.ts +0 -18
- /package/{template → template-vite}/.cursor/rules/exxat-dashboard-view-charts.mdc +0 -0
- /package/{template → template-vite}/.prettierrc +0 -0
- /package/{template → template-vite}/AGENTS.md +0 -0
- /package/{template → template-vite}/README.md +0 -0
- /package/{template → template-vite}/components/.gitkeep +0 -0
- /package/{template → template-vite}/components/ask-leo-composer.tsx +0 -0
- /package/{template → template-vite}/components/brand-color-picker.tsx +0 -0
- /package/{template → template-vite}/components/chart-area-interactive.tsx +0 -0
- /package/{template → template-vite}/components/charts-overview.tsx +0 -0
- /package/{template → template-vite}/components/collaboration-access-flow.tsx +0 -0
- /package/{template → template-vite}/components/columns-client.tsx +0 -0
- /package/{template → template-vite}/components/columns-showcase.tsx +0 -0
- /package/{template → template-vite}/components/dashboard-promo-banner.tsx +0 -0
- /package/{template → template-vite}/components/dashboard-quota-progress-card.tsx +0 -0
- /package/{template → template-vite}/components/dashboard-report-charts.tsx +0 -0
- /package/{template → template-vite}/components/dashboard-section-heading.tsx +0 -0
- /package/{template → template-vite}/components/dashboard-tabs.tsx +0 -0
- /package/{template → template-vite}/components/data-table/filter-date-calendar.tsx +0 -0
- /package/{template → template-vite}/components/data-table/filter-text-value-input.tsx +0 -0
- /package/{template → template-vite}/components/data-table/index.tsx +0 -0
- /package/{template → template-vite}/components/data-table/pagination.tsx +0 -0
- /package/{template → template-vite}/components/data-table/types.ts +0 -0
- /package/{template → template-vite}/components/data-table/use-table-state.test.ts +0 -0
- /package/{template → template-vite}/components/data-table/use-table-state.ts +0 -0
- /package/{template → template-vite}/components/data-views/board-card-primitives.tsx +0 -0
- /package/{template → template-vite}/components/data-views/data-row-list.tsx +0 -0
- /package/{template → template-vite}/components/data-views/finder-panel-view.tsx +0 -0
- /package/{template → template-vite}/components/data-views/folder-grid-view.tsx +0 -0
- /package/{template → template-vite}/components/data-views/hub-table.tsx +0 -0
- /package/{template → template-vite}/components/data-views/index.ts +0 -0
- /package/{template → template-vite}/components/data-views/list-page-board-card.tsx +0 -0
- /package/{template → template-vite}/components/data-views/list-page-board-template.tsx +0 -0
- /package/{template → template-vite}/components/data-views/list-page-connected-view-body.tsx +0 -0
- /package/{template → template-vite}/components/data-views/list-page-split-details-placeholder.tsx +0 -0
- /package/{template → template-vite}/components/data-views/list-page-split-hub-chrome.tsx +0 -0
- /package/{template → template-vite}/components/data-views/list-page-split-hub-tokens.ts +0 -0
- /package/{template → template-vite}/components/data-views/list-page-tree-column-header.tsx +0 -0
- /package/{template → template-vite}/components/data-views/list-page-tree-panel-shell.tsx +0 -0
- /package/{template → template-vite}/components/data-views/list-page-view-frame.tsx +0 -0
- /package/{template → template-vite}/components/data-views/os-folder-glyph.tsx +0 -0
- /package/{template → template-vite}/components/data-views/outline-tree-menu.tsx +0 -0
- /package/{template → template-vite}/components/data-views/table-cells.tsx +0 -0
- /package/{template → template-vite}/components/dev-chunk-load-recovery.tsx +0 -0
- /package/{template → template-vite}/components/export-drawer.test.tsx +0 -0
- /package/{template → template-vite}/components/export-drawer.tsx +0 -0
- /package/{template → template-vite}/components/folder-details-shell.tsx +0 -0
- /package/{template → template-vite}/components/form-layout-01.tsx +0 -0
- /package/{template → template-vite}/components/hub-tree-panel-view.tsx +0 -0
- /package/{template → template-vite}/components/invite-collaborators-drawer.tsx +0 -0
- /package/{template → template-vite}/components/key-metrics-ask-leo-bridge.tsx +0 -0
- /package/{template → template-vite}/components/key-metrics.tsx +0 -0
- /package/{template → template-vite}/components/leo-insight-indicator.tsx +0 -0
- /package/{template → template-vite}/components/leo-typing-dots.tsx +0 -0
- /package/{template → template-vite}/components/library-board-view.tsx +0 -0
- /package/{template → template-vite}/components/library-dashboard-charts.tsx +0 -0
- /package/{template → template-vite}/components/library-favorite-button.tsx +0 -0
- /package/{template → template-vite}/components/library-new-folder-sheet.tsx +0 -0
- /package/{template → template-vite}/components/library-os-folder-view.tsx +0 -0
- /package/{template → template-vite}/components/library-page-header.tsx +0 -0
- /package/{template → template-vite}/components/library-panel-activator.tsx +0 -0
- /package/{template → template-vite}/components/list-hub-status-badge.tsx +0 -0
- /package/{template → template-vite}/components/list-page-dashboard-charts.tsx +0 -0
- /package/{template → template-vite}/components/onboarding/getting-started.tsx +0 -0
- /package/{template → template-vite}/components/onboarding/index.ts +0 -0
- /package/{template → template-vite}/components/onboarding/onboarding-01.tsx +0 -0
- /package/{template → template-vite}/components/onboarding/onboarding-02.tsx +0 -0
- /package/{template → template-vite}/components/onboarding/onboarding-03.tsx +0 -0
- /package/{template → template-vite}/components/onboarding/onboarding-04.tsx +0 -0
- /package/{template → template-vite}/components/page-header.tsx +0 -0
- /package/{template → template-vite}/components/product-switcher.tsx +0 -0
- /package/{template → template-vite}/components/product-wordmark.tsx +0 -0
- /package/{template → template-vite}/components/settings-appearance-card.tsx +0 -0
- /package/{template → template-vite}/components/settings-form-row.tsx +0 -0
- /package/{template → template-vite}/components/sidebar/app-sidebar-dynamic.tsx +0 -0
- /package/{template → template-vite}/components/sidebar/index.ts +0 -0
- /package/{template → template-vite}/components/sidebar/nav-documents.tsx +0 -0
- /package/{template → template-vite}/components/sidebar/nav-secondary.tsx +0 -0
- /package/{template → template-vite}/components/sidebar/secondary-panel.tsx +0 -0
- /package/{template → template-vite}/components/sidebar/sidebar-auto-collapse.tsx +0 -0
- /package/{template → template-vite}/components/sidebar/sidebar-auto-open.tsx +0 -0
- /package/{template → template-vite}/components/sidebar/sidebar-shell.tsx +0 -0
- /package/{template → template-vite}/components/site-header.tsx +0 -0
- /package/{template → template-vite}/components/table-properties/column-row.tsx +0 -0
- /package/{template → template-vite}/components/table-properties/draggable-list.ts +0 -0
- /package/{template → template-vite}/components/table-properties/drawer-button.tsx +0 -0
- /package/{template → template-vite}/components/table-properties/drawer.tsx +0 -0
- /package/{template → template-vite}/components/table-properties/filter-card.tsx +0 -0
- /package/{template → template-vite}/components/table-properties/index.ts +0 -0
- /package/{template → template-vite}/components/table-properties/sort-card.tsx +0 -0
- /package/{template → template-vite}/components/table-properties/types.ts +0 -0
- /package/{template → template-vite}/components/task-list-panel.tsx +0 -0
- /package/{template → template-vite}/components/task-priority-badge.tsx +0 -0
- /package/{template → template-vite}/components/templates/dedicated-search-landing-template.tsx +0 -0
- /package/{template → template-vite}/components/templates/dedicated-search-results-template.tsx +0 -0
- /package/{template → template-vite}/components/templates/list-page.tsx +0 -0
- /package/{template → template-vite}/components/templates/nested-secondary-panel-shell.tsx +0 -0
- /package/{template → template-vite}/components/templates/primary-page-template.tsx +0 -0
- /package/{template → template-vite}/components/templates/secondary-panel-hub-template.tsx +0 -0
- /package/{template → template-vite}/components/theme-color-sync.tsx +0 -0
- /package/{template → template-vite}/components/theme-provider.tsx +0 -0
- /package/{template → template-vite}/components/tinted-icon-disc.tsx +0 -0
- /package/{template → template-vite}/components/tokens-hub-auxiliary-views.tsx +0 -0
- /package/{template → template-vite}/components/tokens-themes-section.tsx +0 -0
- /package/{template → template-vite}/components/ui/accordion.tsx +0 -0
- /package/{template → template-vite}/components/ui/ai-thinking-surface.tsx +0 -0
- /package/{template → template-vite}/components/ui/alert-dialog.tsx +0 -0
- /package/{template → template-vite}/components/ui/avatar.tsx +0 -0
- /package/{template → template-vite}/components/ui/badge.tsx +0 -0
- /package/{template → template-vite}/components/ui/banner.tsx +0 -0
- /package/{template → template-vite}/components/ui/breadcrumb.tsx +0 -0
- /package/{template → template-vite}/components/ui/button.tsx +0 -0
- /package/{template → template-vite}/components/ui/calendar.tsx +0 -0
- /package/{template → template-vite}/components/ui/card.tsx +0 -0
- /package/{template → template-vite}/components/ui/chart.tsx +0 -0
- /package/{template → template-vite}/components/ui/checkbox.tsx +0 -0
- /package/{template → template-vite}/components/ui/coach-mark.tsx +0 -0
- /package/{template → template-vite}/components/ui/collapsible.tsx +0 -0
- /package/{template → template-vite}/components/ui/command.tsx +0 -0
- /package/{template → template-vite}/components/ui/context-menu.tsx +0 -0
- /package/{template → template-vite}/components/ui/date-picker-field.tsx +0 -0
- /package/{template → template-vite}/components/ui/dialog.tsx +0 -0
- /package/{template → template-vite}/components/ui/dot-pattern.tsx +0 -0
- /package/{template → template-vite}/components/ui/drag-handle-grip.tsx +0 -0
- /package/{template → template-vite}/components/ui/dropdown-menu.tsx +0 -0
- /package/{template → template-vite}/components/ui/field.tsx +0 -0
- /package/{template → template-vite}/components/ui/form.tsx +0 -0
- /package/{template → template-vite}/components/ui/hover-card.tsx +0 -0
- /package/{template → template-vite}/components/ui/input-group.tsx +0 -0
- /package/{template → template-vite}/components/ui/input-mask.tsx +0 -0
- /package/{template → template-vite}/components/ui/input.tsx +0 -0
- /package/{template → template-vite}/components/ui/kbd.tsx +0 -0
- /package/{template → template-vite}/components/ui/label.tsx +0 -0
- /package/{template → template-vite}/components/ui/leo-icon.tsx +0 -0
- /package/{template → template-vite}/components/ui/payment-card-fields.tsx +0 -0
- /package/{template → template-vite}/components/ui/popover.tsx +0 -0
- /package/{template → template-vite}/components/ui/radio-group.tsx +0 -0
- /package/{template → template-vite}/components/ui/resizable.tsx +0 -0
- /package/{template → template-vite}/components/ui/scroll-area.tsx +0 -0
- /package/{template → template-vite}/components/ui/select.tsx +0 -0
- /package/{template → template-vite}/components/ui/selection-tile-grid.tsx +0 -0
- /package/{template → template-vite}/components/ui/separator.tsx +0 -0
- /package/{template → template-vite}/components/ui/sheet.tsx +0 -0
- /package/{template → template-vite}/components/ui/sidebar.tsx +0 -0
- /package/{template → template-vite}/components/ui/skeleton.tsx +0 -0
- /package/{template → template-vite}/components/ui/slider.tsx +0 -0
- /package/{template → template-vite}/components/ui/sonner.tsx +0 -0
- /package/{template → template-vite}/components/ui/status-badge.tsx +0 -0
- /package/{template → template-vite}/components/ui/table.tsx +0 -0
- /package/{template → template-vite}/components/ui/tabs.tsx +0 -0
- /package/{template → template-vite}/components/ui/textarea.tsx +0 -0
- /package/{template → template-vite}/components/ui/tip.tsx +0 -0
- /package/{template → template-vite}/components/ui/toggle-group.tsx +0 -0
- /package/{template → template-vite}/components/ui/toggle-switch.tsx +0 -0
- /package/{template → template-vite}/components/ui/toggle.tsx +0 -0
- /package/{template → template-vite}/components/ui/tooltip.tsx +0 -0
- /package/{template → template-vite}/components/ui/view-segmented-control.tsx +0 -0
- /package/{template → template-vite}/components.json +0 -0
- /package/{template → template-vite}/contexts/chart-variant-context.tsx +0 -0
- /package/{template → template-vite}/contexts/command-menu-context.tsx +0 -0
- /package/{template → template-vite}/contexts/dashboard-view-context.tsx +0 -0
- /package/{template → template-vite}/contexts/product-context.tsx +0 -0
- /package/{template → template-vite}/contexts/system-banner-context.tsx +0 -0
- /package/{template → template-vite}/eslint.config.mjs +0 -0
- /package/{template → template-vite}/fontawesome-subset.manifest.json +0 -0
- /package/{template → template-vite}/hooks/.gitkeep +0 -0
- /package/{template → template-vite}/hooks/use-app-theme.ts +0 -0
- /package/{template → template-vite}/hooks/use-coach-mark.ts +0 -0
- /package/{template → template-vite}/hooks/use-location-hash.ts +0 -0
- /package/{template → template-vite}/hooks/use-mobile.ts +0 -0
- /package/{template → template-vite}/hooks/use-mod-key-label.ts +0 -0
- /package/{template → template-vite}/hooks/use-sidebar-reflow-zoom.ts +0 -0
- /package/{template → template-vite}/lib/.gitkeep +0 -0
- /package/{template → template-vite}/lib/ask-leo-route-context.ts +0 -0
- /package/{template → template-vite}/lib/chart-keyboard-selection.test.ts +0 -0
- /package/{template → template-vite}/lib/chart-keyboard-selection.ts +0 -0
- /package/{template → template-vite}/lib/chart-line-dash.ts +0 -0
- /package/{template → template-vite}/lib/chunk-load-error.ts +0 -0
- /package/{template → template-vite}/lib/coach-mark-registry.ts +0 -0
- /package/{template → template-vite}/lib/collaborator-access.ts +0 -0
- /package/{template → template-vite}/lib/command-menu-config.ts +0 -0
- /package/{template → template-vite}/lib/command-menu-search-data.ts +0 -0
- /package/{template → template-vite}/lib/conditional-rule-match.ts +0 -0
- /package/{template → template-vite}/lib/dashboard-customize-coach-mark.ts +0 -0
- /package/{template → template-vite}/lib/dashboard-layout-merge.ts +0 -0
- /package/{template → template-vite}/lib/data-list-display-options.ts +0 -0
- /package/{template → template-vite}/lib/data-list-persistence.ts +0 -0
- /package/{template → template-vite}/lib/data-list-view-registry.ts +0 -0
- /package/{template → template-vite}/lib/data-list-view-surface.ts +0 -0
- /package/{template → template-vite}/lib/data-list-view.ts +0 -0
- /package/{template → template-vite}/lib/data-view-dashboard-storage.ts +0 -0
- /package/{template → template-vite}/lib/date-filter.ts +0 -0
- /package/{template → template-vite}/lib/dedicated-search-recents.ts +0 -0
- /package/{template → template-vite}/lib/dedicated-search-url.ts +0 -0
- /package/{template → template-vite}/lib/dev-log.test.ts +0 -0
- /package/{template → template-vite}/lib/dev-log.ts +0 -0
- /package/{template → template-vite}/lib/discovery-hub.ts +0 -0
- /package/{template → template-vite}/lib/editable-target.ts +0 -0
- /package/{template → template-vite}/lib/exxat-palette.json +0 -0
- /package/{template → template-vite}/lib/exxat-palette.ts +0 -0
- /package/{template → template-vite}/lib/floating-sheet-panel.ts +0 -0
- /package/{template → template-vite}/lib/full-hub-supported-views.ts +0 -0
- /package/{template → template-vite}/lib/hub-connected-view-renderers.ts +0 -0
- /package/{template → template-vite}/lib/initials-from-name.ts +0 -0
- /package/{template → template-vite}/lib/library-authoring.ts +0 -0
- /package/{template → template-vite}/lib/library-dedicated-search.ts +0 -0
- /package/{template → template-vite}/lib/library-hub-search.ts +0 -0
- /package/{template → template-vite}/lib/library-nav.ts +0 -0
- /package/{template → template-vite}/lib/library-recent-searches.ts +0 -0
- /package/{template → template-vite}/lib/library-supported-views.ts +0 -0
- /package/{template → template-vite}/lib/list-hub-supported-views.ts +0 -0
- /package/{template → template-vite}/lib/list-page-table-properties.ts +0 -0
- /package/{template → template-vite}/lib/list-status-badges.ts +0 -0
- /package/{template → template-vite}/lib/logo-dev.ts +0 -0
- /package/{template → template-vite}/lib/mailto.ts +0 -0
- /package/{template → template-vite}/lib/mock/dashboard.ts +0 -0
- /package/{template → template-vite}/lib/mock/library-folders.ts +0 -0
- /package/{template → template-vite}/lib/mock/library-header-collaborators.ts +0 -0
- /package/{template → template-vite}/lib/mock/library-inspector.ts +0 -0
- /package/{template → template-vite}/lib/mock/library-kpi.ts +0 -0
- /package/{template → template-vite}/lib/mock/library.ts +0 -0
- /package/{template → template-vite}/lib/mock/navigation.tsx +0 -0
- /package/{template → template-vite}/lib/motion-ui.ts +0 -0
- /package/{template → template-vite}/lib/product-brand.ts +0 -0
- /package/{template → template-vite}/lib/raf-throttle.ts +0 -0
- /package/{template → template-vite}/lib/row-height.ts +0 -0
- /package/{template → template-vite}/lib/sidebar-state-cookie.ts +0 -0
- /package/{template → template-vite}/lib/stock-portrait.ts +0 -0
- /package/{template → template-vite}/lib/table-state-lifecycle.ts +0 -0
- /package/{template → template-vite}/lib/utils.test.ts +0 -0
- /package/{template → template-vite}/lib/utils.ts +0 -0
- /package/{template → template-vite}/public/.gitkeep +0 -0
- /package/{template → template-vite}/public/Illustration/Rotation.svg +0 -0
- /package/{template → template-vite}/public/avatars/user.svg +0 -0
- /package/{template/public → template-vite/public/favicon}/favicon.ico +0 -0
- /package/{template/app → template-vite/public}/favicon.ico +0 -0
- /package/{template → template-vite}/public/folders/icons8-folder-windows-11.svg +0 -0
- /package/{template → template-vite}/public/logos/exxat-one.svg +0 -0
- /package/{template → template-vite}/public/logos/exxat-prism.svg +0 -0
- /package/{template → template-vite}/public/mock-schools/emory.svg +0 -0
- /package/{template → template-vite}/public/mock-schools/rush.svg +0 -0
- /package/{template → template-vite}/scripts/fontawesome-subset-audit.mjs +0 -0
- /package/{template → template-vite}/scripts/pm2-startup-macos.sh +0 -0
- /package/{template → template-vite}/skills-lock.json +0 -0
- /package/{template/app/(app)/columns/page.tsx → template-vite/src/pages/columns.tsx} +0 -0
- /package/{template/app/(app)/library/find/page.tsx → template-vite/src/pages/library/find.tsx} +0 -0
- /package/{template/app/(app)/library/page.tsx → template-vite/src/pages/library/index.tsx} +0 -0
- /package/{template/app/(app)/library/list/page.tsx → template-vite/src/pages/library/list.tsx} +0 -0
- /package/{template/app/(app)/settings/page.tsx → template-vite/src/pages/settings.tsx} +0 -0
- /package/{template/app/(app)/tokens-themes/page.tsx → template-vite/src/pages/tokens-themes.tsx} +0 -0
- /package/{template → template-vite}/stores/app-store.ts +0 -0
- /package/{template → template-vite}/types/react-payment-inputs.d.ts +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Design → Engineering Handoff
|
|
2
|
+
|
|
3
|
+
This template is the **single artifact** the agent produces at the end of any
|
|
4
|
+
design task that creates or rebuilds a surface (page, hub, detail view,
|
|
5
|
+
wizard, drawer, sheet). It is the contract between the designer who shaped
|
|
6
|
+
the work in the brief and the engineer who ships it.
|
|
7
|
+
|
|
8
|
+
The agent fills this in as part of the `exxat-ux-discovery-protocol` flow.
|
|
9
|
+
The engineer reads it once, runs `exxat-ui audit <file>` against the
|
|
10
|
+
generated files, and is done.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Job-to-be-done
|
|
15
|
+
|
|
16
|
+
> Single sentence. The decision or action this surface enables for the user.
|
|
17
|
+
> Copy from the design brief — do not invent.
|
|
18
|
+
|
|
19
|
+
(example) *Coordinator identifies and acts on at-risk students before placement.*
|
|
20
|
+
|
|
21
|
+
## 2. Audience & frequency
|
|
22
|
+
|
|
23
|
+
> Persona (admin / coordinator / faculty / student / preceptor) + how often
|
|
24
|
+
> they hit this surface (daily / weekly / occasional).
|
|
25
|
+
|
|
26
|
+
(example) *Program coordinator, daily, keyboard-first.*
|
|
27
|
+
|
|
28
|
+
## 3. Pattern + IA
|
|
29
|
+
|
|
30
|
+
> The DS pattern this surface uses + the information architecture shape.
|
|
31
|
+
> Should match the brief.
|
|
32
|
+
|
|
33
|
+
- **Surface kind**: route | sheet | dialog | inline (one of the four)
|
|
34
|
+
- **Layout**: page-header + ?? (single column / split / data-table grid / …)
|
|
35
|
+
- **Navigation**: breadcrumb? back affordance? anchor sub-nav?
|
|
36
|
+
- **Header tier**: identity, status, KPI, ⋯ overflow, primary CTA
|
|
37
|
+
|
|
38
|
+
## 4. Reference implementations
|
|
39
|
+
|
|
40
|
+
> One **repo** reference + two **modern SaaS** analogues (cite by product
|
|
41
|
+
> name + Mx codes from `docs/exxat-ds/modern-saas-patterns.md`).
|
|
42
|
+
|
|
43
|
+
- **Repo**: `apps/web/components/<file>.tsx`
|
|
44
|
+
- **Modern**: `<Product 1>` (Mx, Mx); `<Product 2>` (Mx, Mx)
|
|
45
|
+
|
|
46
|
+
## 5. Components used (DS inventory)
|
|
47
|
+
|
|
48
|
+
> Every DS primitive consumed. The agent fills this in by inspecting its
|
|
49
|
+
> own diff. No new bespoke widgets unless approved in the brief.
|
|
50
|
+
|
|
51
|
+
| Primitive | From | Why |
|
|
52
|
+
|-----------|------|-----|
|
|
53
|
+
| `PageHeader` | `@exxatdesignux/ui/page-header` | Identity + actions row |
|
|
54
|
+
| `HubTable` | `@/components/data-views` | Rows + filters + properties |
|
|
55
|
+
| … | … | … |
|
|
56
|
+
|
|
57
|
+
## 6. Tokens consumed
|
|
58
|
+
|
|
59
|
+
> Every CSS custom property the agent referenced. No hex literals — see
|
|
60
|
+
> `exxat-token-discipline`.
|
|
61
|
+
|
|
62
|
+
- Colors: `--exxat-color-bg-app`, `--exxat-color-text-primary`, …
|
|
63
|
+
- Spacing: `--exxat-spacing-*`
|
|
64
|
+
- Radii: `--exxat-radius-*`
|
|
65
|
+
- Shadow / elevation: `--exxat-shadow-*`
|
|
66
|
+
|
|
67
|
+
## 7. Icons used
|
|
68
|
+
|
|
69
|
+
> Font Awesome Pro classes. Add to the subset audit (`fa:subset-audit`)
|
|
70
|
+
> if any are new to the kit.
|
|
71
|
+
|
|
72
|
+
- `fa-light fa-graduation-cap` — student identity
|
|
73
|
+
- `fa-light fa-shield-check` — compliance status
|
|
74
|
+
- …
|
|
75
|
+
|
|
76
|
+
## 8. Accessibility floor
|
|
77
|
+
|
|
78
|
+
> Filled by the agent automatically (the universal a11y rule is on). Engineer
|
|
79
|
+
> verifies by running axe / Deque on the deployed surface.
|
|
80
|
+
|
|
81
|
+
- [x] One H1 per page; record name in `PageHeader.title` only
|
|
82
|
+
- [x] Single primary CTA; everything else is outline / ghost / link
|
|
83
|
+
- [x] `Tablist` semantics correct (no buttons inside `role=tablist`)
|
|
84
|
+
- [x] All interactive targets ≥ 24 × 24 CSS px
|
|
85
|
+
- [x] Contrast ≥ 4.5 : 1 for text, ≥ 3 : 1 for UI / focus
|
|
86
|
+
- [x] Every icon has either `aria-hidden` (decorative) or `aria-label` + tooltip (informational)
|
|
87
|
+
- [x] Format hints rendered as `FormDescription`, not placeholder
|
|
88
|
+
- [x] Every primary action has an `Enter` shortcut + visible Kbd hint
|
|
89
|
+
- [x] Every cancel action accepts `Esc`
|
|
90
|
+
|
|
91
|
+
## 9. Data shape
|
|
92
|
+
|
|
93
|
+
> What rows / records / fields this surface reads from. Engineer wires the
|
|
94
|
+
> real API; the agent uses `lib/mock/<entity>.ts` for the demo dataset.
|
|
95
|
+
|
|
96
|
+
- **Mock**: `apps/web/lib/mock/<entity>.ts`
|
|
97
|
+
- **API contract** (when known): `<endpoint>` returns `<RowShape>`
|
|
98
|
+
- **State container**: `useTableState({ data, columns })`
|
|
99
|
+
|
|
100
|
+
## 10. Empty / loading / error
|
|
101
|
+
|
|
102
|
+
> Every list, detail, and action ships all three states. Filled in by the
|
|
103
|
+
> agent; engineer verifies the copy.
|
|
104
|
+
|
|
105
|
+
- **Loading**: skeleton (`<HubTableSkeleton />` or matched shape)
|
|
106
|
+
- **Empty**: copy + primary CTA to start the flow
|
|
107
|
+
- **Error**: copy + retry affordance + Ask Leo entry point
|
|
108
|
+
|
|
109
|
+
## 11. Keyboard map
|
|
110
|
+
|
|
111
|
+
> Every shortcut bound on this surface. The agent must declare these so the
|
|
112
|
+
> engineer can write a Playwright test against them.
|
|
113
|
+
|
|
114
|
+
| Action | Shortcut |
|
|
115
|
+
|--------|----------|
|
|
116
|
+
| Open Add view | `1..9` |
|
|
117
|
+
| Submit primary form | `Enter` |
|
|
118
|
+
| Cancel | `Esc` |
|
|
119
|
+
| Search | `⌘K` / `Ctrl+K` |
|
|
120
|
+
| Ask Leo | `⌘⌥K` / `Ctrl+Alt+K` |
|
|
121
|
+
| New record | `⌘⌥N` |
|
|
122
|
+
|
|
123
|
+
## 12. Deviations from principles
|
|
124
|
+
|
|
125
|
+
> Any P9–P20 principle the design intentionally breaks, and the one-sentence
|
|
126
|
+
> reason. P1–P8 cannot be deviated from. If this section has rows, the
|
|
127
|
+
> engineer must include them in the PR description.
|
|
128
|
+
|
|
129
|
+
| Principle | Reason |
|
|
130
|
+
|-----------|--------|
|
|
131
|
+
| P14 (density) | Mixed audience — added density toggle, default cozy |
|
|
132
|
+
|
|
133
|
+
## 13. Out of scope
|
|
134
|
+
|
|
135
|
+
> What this surface deliberately does **not** do. Helps the engineer decide
|
|
136
|
+
> what tickets *not* to file.
|
|
137
|
+
|
|
138
|
+
(example)
|
|
139
|
+
- Inline editing of student name (v2)
|
|
140
|
+
- Bulk message (lives on the list hub, not on the detail)
|
|
141
|
+
|
|
142
|
+
## 14. Open questions
|
|
143
|
+
|
|
144
|
+
> Things the brief could not answer. Engineer pings the designer or product.
|
|
145
|
+
|
|
146
|
+
- (example) Should the compliance badge link out to the source document or open in a side panel?
|
|
147
|
+
|
|
148
|
+
## 15. Designer-stack → engineering-stack port (delete if same stack)
|
|
149
|
+
|
|
150
|
+
> **Skip this section** if your designer prototype and your production app
|
|
151
|
+
> are on the same framework (both Vite, both Next.js).
|
|
152
|
+
>
|
|
153
|
+
> **Fill this in** if the designer prototyped on the **Vite scaffold**
|
|
154
|
+
> (`create-exxat-app`, the lightweight default) but the production app
|
|
155
|
+
> still runs on **Next.js**. The agent or engineer maps each Vite-specific
|
|
156
|
+
> import to its Next equivalent before merging the work into prod.
|
|
157
|
+
|
|
158
|
+
| Vite scaffold (designer) | Next.js prod (engineer) | Notes |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| `import { Link } from "@/lib/next-compat"` → `<Link to="…">` | `import Link from "next/link"` → `<Link href="…">` | Drop the shim re-export; rename `to` → `href` |
|
|
161
|
+
| `import { useNavigate } from "react-router-dom"` | `import { useRouter } from "next/navigation"` + `router.push(...)` | One-line swap |
|
|
162
|
+
| `import { useLocation } from "react-router-dom"` → `useLocation().pathname` | `import { usePathname } from "next/navigation"` | Direct equivalent |
|
|
163
|
+
| `import { useSearchParams } from "react-router-dom"` returns `[params, setParams]` | `import { useSearchParams } from "next/navigation"` returns `ReadonlyURLSearchParams` | Drop the destructure, accept readonly shape |
|
|
164
|
+
| `import { Navigate } from "react-router-dom"` (JSX redirect) | `import { redirect } from "next/navigation"` (function call) | Move from JSX to component body |
|
|
165
|
+
| `React.lazy(() => import(...))` + `<Suspense>` | `dynamic(() => import(...), { loading, ssr: false })` | Functionally equivalent — pick whichever the surrounding file already uses |
|
|
166
|
+
| `vite.config.ts` aliases | `next.config.mjs` `experimental.turbopack.resolveAlias` or `tsconfig.json` `paths` | Rare — most aliases live in `tsconfig.json` already |
|
|
167
|
+
| `index.html` `<meta>` tags + `<title>` | `app/layout.tsx` `metadata` export | Move static meta into the route's `metadata`; dynamic meta moves into `generateMetadata` |
|
|
168
|
+
| `document.cookie` reads in client components | `cookies()` from `next/headers` (server) | Most cases are client-only; only swap if the value must be SSR-rendered |
|
|
169
|
+
| `@fontsource-variable/inter` import in `main.tsx` | `Inter()` from `next/font/google` in `app/layout.tsx` | Both produce zero-CLS variable fonts; Next has the edge on hashed asset names |
|
|
170
|
+
|
|
171
|
+
The agent runs this map as a codemod — see
|
|
172
|
+
`packages/ui/template-vite/scripts/port-next-imports.mjs` (template-vite
|
|
173
|
+
internal use) for the inverse direction (Next → Vite, used during PR-2).
|
|
174
|
+
Engineers porting **Vite → Next** can copy that script and reverse the
|
|
175
|
+
regexes; the patterns are symmetric.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## How the engineer uses this file
|
|
180
|
+
|
|
181
|
+
1. Read sections 1–3 to understand intent.
|
|
182
|
+
2. Skim sections 4–7 to confirm the agent reused DS primitives + tokens (no surprises).
|
|
183
|
+
3. Run `npx --package=@exxatdesignux/ui exxat-ui audit <generated-file>.tsx` to validate the floor automatically.
|
|
184
|
+
4. Wire real data per section 9.
|
|
185
|
+
5. Verify section 10 states render with real API data (not just mocks).
|
|
186
|
+
6. Add section 11 shortcuts to the route's e2e test.
|
|
187
|
+
7. Surface section 12 deviations + section 14 open questions in the PR description.
|
|
188
|
+
|
|
189
|
+
If any section is missing or hand-wavy, the handoff is incomplete. Push it
|
|
190
|
+
back to the designer / agent before merging.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exxatdesignux/ui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "Exxat shared design system (components, hooks, tokens). Monorepo setup: clone repo then pnpm bootstrap at workspace root — see github.com/ExxatDesign/Exxat-DS-Workspace README.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Exxat Design",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"dist",
|
|
98
98
|
"src",
|
|
99
99
|
"bin",
|
|
100
|
-
"template",
|
|
100
|
+
"template-vite",
|
|
101
101
|
"consumer-extras",
|
|
102
102
|
"tokens"
|
|
103
103
|
],
|
|
@@ -155,7 +155,6 @@
|
|
|
155
155
|
"lint": "eslint src/",
|
|
156
156
|
"test": "vitest run",
|
|
157
157
|
"test:watch": "vitest",
|
|
158
|
-
"sync-template": "node ./scripts/sync-template-from-web.mjs",
|
|
159
158
|
"vendor:consumer-extras": "node ./scripts/vendor-consumer-extras.mjs",
|
|
160
159
|
"tokens:index": "node ./scripts/build-tokens-index.mjs",
|
|
161
160
|
"tokens:check": "node ./scripts/build-tokens-index.mjs --check"
|
|
@@ -19,9 +19,11 @@ description: >
|
|
|
19
19
|
## 1. Project Overview
|
|
20
20
|
|
|
21
21
|
- **Stack:** Next.js 16 (App Router), React, TypeScript, Tailwind CSS, shadcn/ui primitives, Font Awesome icons
|
|
22
|
-
- **App root:** `
|
|
23
|
-
- **Single source of truth:** `
|
|
22
|
+
- **App root:** `apps/web/app/(app)/` — route group that wraps all authenticated pages
|
|
23
|
+
- **Single source of truth:** `apps/web/AGENTS.md` for full prose explanations; this skill is the actionable summary
|
|
24
24
|
- **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-mono-ids`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-dedicated-search-surfaces`, `exxat-accessibility`, `exxat-board-cards`, `exxat-collaboration-access` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
|
|
25
|
+
- **Library folder-scoped header (rule + doc):** **`.cursor/rules/exxat-library-hub-header.mdc`** and **`docs/library-hub-header-pattern.md`** — pair with **`exxat-primary-nav-secondary-panel`** when URL **`scope=folder`** drives the hub title.
|
|
26
|
+
- **Consumer repos (npm install of `@exxatdesignux/ui`):** After a version bump, read **`node_modules/@exxatdesignux/ui/CHANGELOG.md`**, run **`npx --package=@exxatdesignux/ui@latest exxat-ui sync-extras`** so **`docs/exxat-ds/consumer-upgrade-checklist.md`** and Cursor skills match the tarball, and diff the host app against **`node_modules/@exxatdesignux/ui/template/`** for anything new to port (routes, re-exports, AGENTS). Use **`exxat-ui changelog`**, **`exxat-ui update`**, and **`exxat-ui doctor`** for CLI guidance.
|
|
25
27
|
|
|
26
28
|
---
|
|
27
29
|
|
|
@@ -87,16 +89,161 @@ To add a primary nav item, append to `NAV_PRIMARY`:
|
|
|
87
89
|
|
|
88
90
|
| Concern | Pattern |
|
|
89
91
|
|--------|---------|
|
|
90
|
-
| **Product (One / Prism)** | **`ExxatProductLogo`** (`components/exxat-product-logo.tsx`) for the header control and **`ProductSwitcher`** — **not** logo.dev rasters unless product explicitly changes that. |
|
|
92
|
+
| **Product (One / Prism)** | **`ExxatProductLogo`** (`components/exxat-product-logo.tsx`) for the header control and **`ProductSwitcher`** — **not** logo.dev rasters unless product explicitly changes that. The logo is now **generated from a config**, not hand-built SVG paths — see **§3.4** to add a new product. |
|
|
91
93
|
| **School/program menu width** | **`DropdownMenuContent`** defaults to **intrinsic width** (**`min-w-52 w-max max-w-[min(24rem,calc(100vw-2rem))]`** via **`DROPDOWN_MENU_CONTENT_SURFACE_CLASS`** in **`@exxatdesignux/ui/lib/dropdown-menu-surface`**) — pure CSS, no **`ResizeObserver`**. The **school / program** switcher still uses an explicit wider surface (**`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**) so dense rows stay readable. |
|
|
92
94
|
| **School/program copy** | **Do not truncate** school or program names in the switcher; wrap (**`break-words`**, **`whitespace-normal`**, **`items-start`** on multi-line rows). The selected-school summary shows **school name + current program**. |
|
|
93
95
|
| **Team switcher trigger** | **`SidebarMenuButton` `size="lg"`** uses **`h-12`** + **`overflow-hidden`**, which **clips** a second line (program). When the sidebar is **expanded** or **mobile**, add **`h-auto min-h-12`** and **`overflow-x-clip overflow-y-visible`**. On **icon rail**, hide label rows with **`group-data-[collapsible=icon]:hidden`** (tooltip still exposes the full string). Icon mode defaults **`size-8` + `p-2`** (~16px inner) **clips** school logos — override **`!size-9`**, **`!p-0`**, **`overflow-visible`**. Omit header **chevrons** next to logos if they look like stray chrome. |
|
|
94
96
|
| **Motion / Animate UI** | [Animate UI](https://animate-ui.com/docs) — open **copy-first** animated components (Motion + Tailwind). This repo uses **`motion/react`** + **`lib/motion-ui.ts`** presets; pull more animations from their registry into `components/` when needed. |
|
|
95
|
-
| **Nav items with children** |
|
|
97
|
+
| **Nav items with children** | See **§3.2** below for the full parent ↔ children pattern (collapsible vs popover, active-state rules, chevron + slide animation, reduced motion). |
|
|
96
98
|
| **Profile (mock)** | **`stockPortraitUrl()`** from **`lib/stock-portrait.ts`**; **`AvatarImage`** **`referrerPolicy="no-referrer"`** for external URLs. |
|
|
97
99
|
|
|
98
100
|
**Reference:** `components/app-sidebar.tsx`, `components/nav-user.tsx`, `components/product-switcher.tsx`.
|
|
99
101
|
|
|
102
|
+
### 3.2 Parent ↔ children nav (collapsible vs popover vs secondary panel)
|
|
103
|
+
|
|
104
|
+
A primary nav row that owns sub-routes has **three possible shapes** — pick exactly one per item:
|
|
105
|
+
|
|
106
|
+
| Shape | When to use | Where it lives |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| **A. Collapsible children** (e.g. Library → All / My / Favorites / Folders) | Small finite child list that benefits from inline browsing (≤ 40 items, no extra page chrome). | `NavLinkItem.children` rendered by **`CollapsibleNavItem`** in `app-sidebar.tsx`. |
|
|
109
|
+
| **B. Secondary panel** (separate nested rail) | Same nav row needs **scoped search / tree / metrics** alongside the hub content. | `NavLinkItem.secondaryPanel = "<id>"` + `PANELS[id]` — see companion skill `exxat-primary-nav-secondary-panel`. |
|
|
110
|
+
| **C. Both A + B on one row** (Library does this) | Most cases when a hub has a sub-list AND a rich rail. The sidebar still shows the collapsible children; clicking the parent route also opens the secondary panel via `useAutoPanel`. | Combine A + B; the active-state and animation rules in §3.2 still apply to the sidebar children. |
|
|
111
|
+
|
|
112
|
+
#### Active-state rules — **the single biggest mistake to avoid**
|
|
113
|
+
|
|
114
|
+
1. **Expanded sidebar (full rail):** parent stays **visually neutral** when a child is active — **never double-highlight**. `isCollapsibleParentMenuButtonActive` returns `false` if `anyChildActive` so the active child row carries `data-active` alone.
|
|
115
|
+
2. **Collapsed sidebar (icon rail):** the parent icon is the **only** affordance, so it lights up when **any child** route is active. Implementation: `iconRailActive = isAnyChildActive` is fed into `SidebarMenuButton.isActive` and selects `item.iconActive` (`fa-solid`) over `item.icon` (`fa-light`).
|
|
116
|
+
3. **Tooltip / aria copy** in icon mode names the parent (e.g. "Library — open subpages"); the **popover** content lists children with their own active state so users see which sub-route is selected.
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// Inside CollapsibleNavItem
|
|
120
|
+
const isAnyChildActive = item.children?.some(c => isCollapsibleChildActive(...))
|
|
121
|
+
const parentMenuButtonActive = isCollapsibleParentMenuButtonActive(pathname, item) // false when anyChildActive
|
|
122
|
+
const iconRailActive = isAnyChildActive
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Chevron + content animation (shadcn / Radix collapsible)
|
|
126
|
+
|
|
127
|
+
The collapsible rotates a chevron and slides the children in/out, mirroring shadcn's `Accordion`/`Collapsible` motion. **Three pieces wired together:**
|
|
128
|
+
|
|
129
|
+
1. **`group/collapsible`** on the **`SidebarMenuItem`** that wraps the trigger — opts the chevron into `group-data-[state=open]/collapsible:rotate-90` (or whatever direction the icon needs). Without `group/collapsible` the chevron never rotates.
|
|
130
|
+
2. **`CollapsibleContent`** uses Radix's `--radix-collapsible-content-height` CSS var via the shared keyframes:
|
|
131
|
+
```tsx
|
|
132
|
+
<CollapsibleContent className="overflow-hidden
|
|
133
|
+
data-[state=open]:[animation:collapsible-down_200ms_ease-out]
|
|
134
|
+
data-[state=closed]:[animation:collapsible-up_200ms_ease-out]
|
|
135
|
+
motion-reduce:animate-none
|
|
136
|
+
group-data-[collapsible=icon]:hidden">
|
|
137
|
+
<SidebarMenuSub>...</SidebarMenuSub>
|
|
138
|
+
</CollapsibleContent>
|
|
139
|
+
```
|
|
140
|
+
`overflow-hidden` is **required** so the height clip is visible during the animation. `motion-reduce:animate-none` honours `prefers-reduced-motion` (WCAG 2.3.3).
|
|
141
|
+
3. **Keyframes in `app/globals.css`** (shared, reused by `Accordion` too):
|
|
142
|
+
```css
|
|
143
|
+
@keyframes collapsible-down { from { height: 0 } to { height: var(--radix-collapsible-content-height) } }
|
|
144
|
+
@keyframes collapsible-up { from { height: var(--radix-collapsible-content-height) } to { height: 0 } }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Icon-rail popover (collapsed sidebar)
|
|
148
|
+
|
|
149
|
+
When `state === "collapsed"` (or `isMobile === false` on a tablet icon rail), `CollapsibleNavItem` renders a **`Popover`** anchored to the parent icon, listing children as full rows. **Do not** pass `tooltip={…}` to a `SidebarMenuButton` that is the **direct** child of `CollapsibleTrigger asChild` — the tooltip wrapper inserts an extra `Tooltip` root and breaks Radix `Slot` (`React.Children.only`). Compose `Tooltip > TooltipTrigger > CollapsibleTrigger > SidebarMenuButton` without the `tooltip` prop, or use the popover branch only.
|
|
150
|
+
|
|
151
|
+
#### Hydration
|
|
152
|
+
|
|
153
|
+
`CollapsibleNavItem` is an isolated component with its own controlled `open` state initialised in `useEffect`. **Do not** pass `defaultOpen` based on pathname at render time — server and client resolve it differently and Radix throws a hydration mismatch.
|
|
154
|
+
|
|
155
|
+
#### Cap
|
|
156
|
+
|
|
157
|
+
The data shape supports any number of children, but the collapsible variant is rendered only when `childCount <= 40`. Beyond that, model the children as a **secondary panel** (shape B) so the user gets search + scroll.
|
|
158
|
+
|
|
159
|
+
**Reference:** `components/app-sidebar.tsx` (`CollapsibleNavItem`, `isCollapsibleParentMenuButtonActive`, `isCollapsibleChildActive`), `app/globals.css` (`@keyframes collapsible-down/up`), `lib/mock/navigation.tsx` (`NavLinkItem.children`).
|
|
160
|
+
|
|
161
|
+
### 3.3 Secondary panel auto-collapse on high zoom
|
|
162
|
+
|
|
163
|
+
`SecondaryPanelProvider` (`components/secondary-panel.tsx`) reads **`useSidebarReflowZoom()`** (browser zoom ≥ 200% **or** very short viewport — same WCAG 1.4.10 signal the primary sidebar uses) and **auto-collapses the nested rail to its icon variant on entering high zoom**. The user can re-expand once collapsed; the next zoom-out → zoom-in cycle re-collapses. `openPanel` also opens directly in compact mode when high zoom is active so freshly-navigated panels don't briefly flash expanded.
|
|
164
|
+
|
|
165
|
+
Any future secondary-panel-like rail should reuse `useSidebarReflowZoom` rather than inventing a parallel zoom hook.
|
|
166
|
+
|
|
167
|
+
**Reference:** `components/secondary-panel.tsx` (`SecondaryPanelProvider`), `hooks/use-sidebar-reflow-zoom.ts`.
|
|
168
|
+
|
|
169
|
+
### 3.4 Product wordmark + brand registry
|
|
170
|
+
|
|
171
|
+
The product logo ("**Exxat** *One*" / "**Exxat** *Prism*") is **generated from a `ProductBrandConfig`** in `lib/product-brand.ts`, not from hand-built SVG paths. The suffix word renders as real **Ivy Presto** italic text (`var(--font-heading)`, Adobe Fonts kit `wuk5wqn` preloaded in `app/layout.tsx`); the circular mark is the same Exxat "E" geometry, recolored from the brand's gradient.
|
|
172
|
+
|
|
173
|
+
**To add a new product:**
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// lib/my-product-brand.ts
|
|
177
|
+
import { defineProductBrand, registerProductBrand } from "@/lib/product-brand"
|
|
178
|
+
|
|
179
|
+
export const EXXAT_PULSE = defineProductBrand({
|
|
180
|
+
id: "exxat-pulse",
|
|
181
|
+
prefix: "Exxat", // optional, defaults to "Exxat"
|
|
182
|
+
suffix: "Pulse", // rendered in Ivy Presto italic
|
|
183
|
+
brandColor: "#00A8E8", // any CSS color — drives suffix text + mark fill
|
|
184
|
+
markGradient: ["#0083C7", "#3FC6FF"], // optional 2-stop linear gradient on the mark
|
|
185
|
+
markShadow: "#006FAA", // optional inner shadow behind the "E" cut-out
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
registerProductBrand(EXXAT_PULSE)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Then render anywhere via either the **generic** primitives or the **Exxat** convenience wrappers:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { ProductWordmark, ProductMark, ProductLogo } from "@/components/product-wordmark"
|
|
195
|
+
import { ExxatProductLogo, ExxatProductMark } from "@/components/exxat-product-logo"
|
|
196
|
+
|
|
197
|
+
// Generic — works for any registered brand
|
|
198
|
+
<ProductLogo config={EXXAT_PULSE} className="h-7" variant="mutedSuffix" />
|
|
199
|
+
<ProductMark config={EXXAT_PULSE} className="size-7" />
|
|
200
|
+
<ProductWordmark config={EXXAT_PULSE} className="h-7" />
|
|
201
|
+
|
|
202
|
+
// Existing Exxat call-sites unchanged
|
|
203
|
+
<ExxatProductLogo product="exxat-one" variant="mutedSuffix" className="h-7" />
|
|
204
|
+
<ExxatProductMark product="exxat-prism" className="size-7" />
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Visual contract (so new brands look like real logos, not styled text):**
|
|
208
|
+
|
|
209
|
+
1. **Prefix** uses Inter `font-extrabold` (800) `tracking-tight` in deep slate (`#273441`) / soft grey on dark (`#A8B2BA`). Always.
|
|
210
|
+
2. **Suffix** — **official Exxat brand spec from Figma** — `var(--font-heading)` (IvyPresto Text), **upright `font-semibold` (600)** with `tracking-[-0.03em]` (Figma "letter spacing -3 %"), tinted with `brandColor`. **Not italic**, **not Bold/ExtraBold** — IvyPresto's Bodoni-lineage SemiBold already has the thick verticals that read as a logo, and pushing to 700/800 makes the letterforms visually heavier than the brand asset (`ExxatOne_WordmarkLogo_WithMinClearSpace.png`). Avoid `medium` (500) / `regular` (400) — those read as inline text, not as a wordmark.
|
|
211
|
+
3. **Size relationship** — the `ExxatProductLogo` outer span pins `text-base leading-none` (16 px inherited) so the wordmark's `text-[1.78em]` resolves to ~28 px font / ~20 px cap-height regardless of host surface (sidebar `text-sm`, dropdown `text-base`, etc.). The mark renders at **`h-full`** of the outer height — the original hand-built `viewBox="0 0 766 164"` already bakes breathing room around the circle artwork, so giving the mark the full outer height reproduces the brand asset's 1.56:1 mark-to-cap ratio (caps ≈ 20 px against a 32 px mark at `h-8`). Shrinking the mark (e.g. `h-[88%]`) makes it visually smaller than the wordmark span and inverts the intended hierarchy. Use **`h-8`** (32 px) as the default sidebar / dropdown / switcher height; `h-7` is too tight for the new wordmark scale and produces a sub-30 px mark.
|
|
212
|
+
4. **Cap-to-mark centring** — the wordmark adds `translate-y-[0.09em]` to compensate for the cap-midpoint vs em-box-midpoint offset (Inter / Ivy Presto put cap glyphs in the upper portion of the line box). Without this, `items-center` aligns the *spans* but not the *visible cap* and mark — the wordmark visually rides ~3 px above the mark. Keep the translate when forking.
|
|
213
|
+
5. **`variant="mutedSuffix"`** (sidebar / switcher) keeps the brand color in **light** mode and only tints to `--muted-foreground` in **dark** mode. Don't mute the suffix in light mode — it loses brand recognition.
|
|
214
|
+
6. **Mark** stays the canonical Exxat "E" geometry so existing pixel-aligned layouts (sidebar header avatar slot, switcher dropdown rows) keep working when you swap brands; only colors change.
|
|
215
|
+
7. **Host span** uses `overflow-visible` because the wordmark's `1.78em` line-box can overshoot the parent `h-X` by ~1 px — let it render, don't clip.
|
|
216
|
+
8. **Don't pass `object-*` classes** to the logo `className` — `ExxatProductLogo` is a `<span>`, not a replaced element, so `object-contain` / `object-left` are silently dropped. Use width / max-width constraints instead.
|
|
217
|
+
9. **`ProductMark` has no size default** — callers MUST set explicit dimensions (`size-7`, `h-full w-auto`, etc.). A `size-*` default would silently lose against a downstream `h-full / w-auto` whenever `tailwind-merge` failed to recognise the shorthand-to-pair equivalence, and the mark would render at the default size (28 px) instead of the parent height (32 px in h-8). `ExxatProductLogo` passes `h-full w-auto`; the icon-rail / collapsed-sidebar usages pass `size-7`. Keep the explicit size.
|
|
218
|
+
|
|
219
|
+
**Where to extend the registry:** brand configs live alongside `lib/product-brand.ts` for the two built-ins. Co-locate new product configs near their feature (e.g. `app/(app)/<product>/_lib/brand.ts`) and call `registerProductBrand` at module import time so `ProductSwitcher` / `getProductBrand(id)` resolve it without ordering issues.
|
|
220
|
+
|
|
221
|
+
**MUST NOT:** add new hand-traced SVG paths for an additional product. Author a `ProductBrandConfig` instead.
|
|
222
|
+
|
|
223
|
+
**Reference:** `lib/product-brand.ts`, `components/product-wordmark.tsx`, `components/exxat-product-logo.tsx`, `components/product-switcher.tsx`.
|
|
224
|
+
|
|
225
|
+
### 3.5 Appearance preview tiles (Theme / Contrast)
|
|
226
|
+
|
|
227
|
+
`components/settings-appearance-card.tsx` renders the Theme (Light / Dark / System) and Contrast (Normal / High / Windows / System) pickers using a shared **`ChromeIllustration`** SVG helper — a polished mini browser window (Mac-style traffic lights, sidebar with brand-tinted mark + 5 nav rows, header bar with search pill + avatar, KPI card + mini bar-chart card + list rows).
|
|
228
|
+
|
|
229
|
+
Two knobs to make new variants without forking the geometry:
|
|
230
|
+
|
|
231
|
+
- **`tokens: ChromeTokens`** — palette per labeled mode. Override individual fills via `{...CHROME_LIGHT, shellStroke: "..."}`. Don't reach for `var(--background)` — preview tiles must show their target mode regardless of the active theme so users can compare before committing.
|
|
232
|
+
- **`strokeBoost: number`** — multiplier applied to every border weight. `1.8` is the high-contrast value used by both **High** and **Windows** tiles.
|
|
233
|
+
|
|
234
|
+
For "System" variants, use the **`SplitSystemSvg`** helper. It renders `ChromeIllustration` twice in the **same** 96 × 56 viewBox, each clipped to a triangular half (top-left triangle = "light", bottom-right triangle = "dark") via SVG `<clipPath>` polygons. The result is **one window with a diagonal theme split** — the macOS / iOS "Auto" pattern — not two adjacent windows.
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
<SplitSystemSvg
|
|
238
|
+
light={{ tokens: CHROME_LIGHT, sidebar: split.light, sidebarMark: split.markLight }}
|
|
239
|
+
dark={{ tokens: CHROME_DARK, sidebar: split.dark, sidebarMark: split.markDark }}
|
|
240
|
+
/>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**MUST NOT** invent ad-hoc rect-stacks for new appearance options, or render two side-by-side mini-windows for a single "System" tile — extend `ChromeIllustration` or compose `SplitSystemSvg`.
|
|
244
|
+
|
|
245
|
+
**Reference:** `components/settings-appearance-card.tsx` (`ChromeIllustration`, `SplitSystemSvg`, `CHROME_LIGHT`, `CHROME_DARK`, `SPLIT_SIDEBAR`).
|
|
246
|
+
|
|
100
247
|
---
|
|
101
248
|
|
|
102
249
|
## 4. Primary Hub Pages — Mandatory Pattern
|
|
@@ -104,37 +251,41 @@ To add a primary nav item, append to `NAV_PRIMARY`:
|
|
|
104
251
|
Any **primary nav destination** that shows a list of records **must** use this composition (same as Placements / Team):
|
|
105
252
|
|
|
106
253
|
```
|
|
107
|
-
ListPageTemplate
|
|
254
|
+
ListPageTemplate (supportedViewTypes = FULL_HUB_SUPPORTED_VIEWS — seven views)
|
|
108
255
|
├── PageHeader (title, subtitle with count, primary CTA, ⋯ more menu)
|
|
109
256
|
├── KeyMetrics (flat variant, single row)
|
|
110
257
|
└── renderContent()
|
|
111
|
-
└──
|
|
258
|
+
└── HubTable + useTableState + TablePropertiesDrawer + renderers per view
|
|
112
259
|
```
|
|
113
260
|
|
|
261
|
+
**Add view parity (binding):** `.cursor/rules/exxat-hub-supported-views.mdc`, `apps/web/docs/hub-supported-views-pattern.md`. **MUST NOT** use `supportedViewTypes={["table"]}` or four-view-only allowlists without a documented exception. List view **MUST** use **`ListPageBoardCard`** (`library-table.tsx`).
|
|
262
|
+
|
|
114
263
|
**Reference implementations:**
|
|
115
|
-
- `components/library-
|
|
116
|
-
- `components/columns-showcase.tsx` —
|
|
117
|
-
- `components/tokens-themes-client.tsx` + `components/tokens-
|
|
118
|
-
- `components/
|
|
264
|
+
- `components/library-client.tsx` + `components/library-table.tsx` — **canonical seven-view hub** (All questions)
|
|
265
|
+
- `components/columns-showcase.tsx` — custom table via **`LibraryTable`** + same seven views
|
|
266
|
+
- `components/tokens-themes-client.tsx` + `components/tokens-hub-auxiliary-views.tsx`
|
|
267
|
+
- `components/team-client.tsx` + `components/team-table.tsx` — entity hub pattern
|
|
268
|
+
- `components/placements-client.tsx` + `components/placements-table.tsx` — Placements (most complete)
|
|
119
269
|
|
|
120
270
|
**Files to create for a new hub page `Foo`:**
|
|
121
271
|
| File | Purpose |
|
|
122
272
|
|------|---------|
|
|
123
273
|
| `lib/mock/foo.ts` | Mock data + TypeScript interface (12+ rows) |
|
|
124
|
-
| `lib/mock/foo-kpi.ts` | `fooKpiMetrics()`
|
|
274
|
+
| `lib/mock/foo-kpi.ts` | `fooKpiMetrics()` + `fooKpiInsight()` |
|
|
125
275
|
| `components/foo-page-header.tsx` | `PageHeader` + primary CTA + ⋯ menu |
|
|
126
|
-
| `components/foo-
|
|
127
|
-
| `components/foo-client.tsx` | `ListPageTemplate` orchestrator
|
|
276
|
+
| `components/foo-table.tsx` | `DataTable` + `useTableState` + `TablePropertiesDrawer` |
|
|
277
|
+
| `components/foo-client.tsx` | `ListPageTemplate` orchestrator |
|
|
128
278
|
| `app/(app)/foo/page.tsx` | Thin server component |
|
|
129
279
|
|
|
130
|
-
**Do not** ship a **nav-linked hub** as an **empty page** or a single “replace this later” paragraph. If the route appears in **`lib/mock/navigation.tsx`**, implement the full hub (mock rows, **`ListPageTemplate`**, connected views per **`
|
|
280
|
+
**Do not** ship a **nav-linked hub** as an **empty page** or a single “replace this later” paragraph. If the route appears in **`lib/mock/navigation.tsx`**, implement the full hub (mock rows, **`ListPageTemplate`**, connected views per **`apps/web/AGENTS.md` §4.1**) unless the product explicitly defines a non-data shell.
|
|
131
281
|
|
|
132
282
|
### Page vs drawer (actions)
|
|
133
283
|
|
|
134
284
|
- **Drawer / sheet** — Use when the user needs **the current page behind them** *and* a **quick view**, **quick actions**, or a **short step** (e.g. properties, export, glance at a row).
|
|
285
|
+
- **Dialog** — **Blocking** confirm/alert/short choice — **`docs/drawer-vs-dialog-pattern.md`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
|
|
135
286
|
- **New page** — Use **otherwise**: **primary**, **long-form**, **multi-step**, or flows that need their **own URL** without the hub visible.
|
|
136
287
|
|
|
137
|
-
Align with **`
|
|
288
|
+
Align with **`apps/web/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`docs/drawer-vs-dialog-pattern.md`**, **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
|
|
138
289
|
|
|
139
290
|
---
|
|
140
291
|
|
|
@@ -150,7 +301,7 @@ Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`
|
|
|
150
301
|
| `ColumnDef` from `@/components/data-table/types` | Column type |
|
|
151
302
|
| `FilterFieldDef`, `FilterOperator`, `ConditionalRule` from `@/components/table-properties/types` | Filter types |
|
|
152
303
|
|
|
153
|
-
**Board (kanban) cards:** Use **`ListPageBoardCard`** and related parts from **`components/data-views/list-page-board-card.tsx`**; **`BoardCardTwoLineBlock`** / **`BoardCardIconRow`** from **`board-card-primitives.tsx`**. **List hub** status (Team, Compliance, Library, …): maps in **`lib/list-status-badges.ts`**; render with **`ListHubStatusBadge`** (**`surface="table"`** in table/list, **`surface="board"`** on cards); semantic tints **`LIST_HUB_STATUS_TINT_*`** for new domains; no **`uppercase`**. **Placements** uses **`StatusBadge`** in **`
|
|
304
|
+
**Board (kanban) cards:** Use **`ListPageBoardCard`** and related parts from **`components/data-views/list-page-board-card.tsx`**; **`BoardCardTwoLineBlock`** / **`BoardCardIconRow`** from **`board-card-primitives.tsx`**. **List hub** status (Team, Compliance, Library, …): maps in **`lib/list-status-badges.ts`**; render with **`ListHubStatusBadge`** (**`surface="table"`** in table/list, **`surface="board"`** on cards); semantic tints **`LIST_HUB_STATUS_TINT_*`** for new domains; no **`uppercase`**. **Placements** uses **`StatusBadge`** in **`placements-table-cells.tsx`** (wrapper over **`ListHubStatusBadge`** + **`PLACEMENT_STATUS_*`**). **Full rules:** **`apps/web/AGENTS.md` §4.4**, **`.cursor/rules/exxat-board-cards.mdc`**, **`.cursor/skills/exxat-board-cards/SKILL.md`**.
|
|
154
305
|
|
|
155
306
|
**Minimum required features on any data list page:**
|
|
156
307
|
- Search (wire `searchable={displayOptions.showToolbarSearch}`)
|
|
@@ -192,6 +343,14 @@ Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`
|
|
|
192
343
|
|
|
193
344
|
**`MetricItem.trend`** must match the **signed change** (arrow direction = truth). **`trendPolarity`** (`higher_is_better` default, **`lower_is_better`**, **`informational`**) controls **tints** and **`aria-label`** — e.g. **low PBI / review flags** rising → `trend: "up"` + **`lower_is_better`** → unfavourable (red), not green. **Doc:** **`docs/kpi-trend-pattern.md`** · **Rule:** **`.cursor/rules/exxat-kpi-trends.mdc`** · **Skill:** **`.cursor/skills/exxat-kpi-trends/SKILL.md`**.
|
|
194
345
|
|
|
346
|
+
### 5.3 KPI count (max four)
|
|
347
|
+
|
|
348
|
+
**`ListPageTemplate`** metrics and **Data tab** key-metrics cards: **≤ 4** `MetricItem` — **`docs/kpi-strip-max-four-pattern.md`**, **`lib/dashboard-layout-merge.ts`**, **`.cursor/rules/exxat-kpi-max-four.mdc`**, **`.cursor/skills/exxat-kpi-max-four/SKILL.md`**.
|
|
349
|
+
|
|
350
|
+
### 5.4 Cards vs table rows
|
|
351
|
+
|
|
352
|
+
Dense comparable hub → **`DataTable`**. Boards / folders / visual browse → **`ListPageBoardCard`** + **`ListPageViewFrame`**. **Doc:** **`docs/card-vs-rows-pattern.md`** · **Rule:** **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
|
|
353
|
+
|
|
195
354
|
**DataTable must wrap in `<div className="pb-6">`.**
|
|
196
355
|
|
|
197
356
|
---
|
|
@@ -247,6 +406,8 @@ Use `PageHeader` from `@/components/page-header` for the content-area header (be
|
|
|
247
406
|
|
|
248
407
|
When a hub is **shared**, use **`PageHeader` `variant="collaboration"`**: **empty roster** → outline **Add collaborator**; **non-empty** → face rail (faces / **`+N`** open the invite sheet). **Invite people** also lives under the entity header **⋯ More** and opens **`InviteCollaboratorsDrawer`** via **`CollaborationAccessFlow`** when possible. Library access (Owner / Editor / Commenter / Viewer) comes from **`lib/collaborator-access.ts`**; directory tags (Faculty, Program coordinator, Director) use **`PageHeaderCollaborator.roles`**.
|
|
249
408
|
|
|
409
|
+
**Library library — folder URL scope:** When **`?scope=folder&folderId=`** applies, **⋯ More** must also offer **Customize folder** (**`LibraryPageHeader`** **`onCustomizeFolder`**) and the **`LibraryNewFolderSheet`** must be mounted on **`LibraryClient`** so it works on every **`ListPageTemplate`** view tab. **`.cursor/rules/exxat-library-hub-header.mdc`** · **`docs/library-hub-header-pattern.md`** (app: **`apps/web/docs/...`**).
|
|
410
|
+
|
|
250
411
|
**Handbook:** `apps/web/AGENTS.md` §4.7 · **Doc:** `docs/collaboration-access-pattern.md` · **Skill:** `.cursor/skills/exxat-collaboration-access/SKILL.md` · **Reference:** Library header + client.
|
|
251
412
|
|
|
252
413
|
---
|
|
@@ -300,7 +461,7 @@ Never install new packages or create parallel components. Always use what exists
|
|
|
300
461
|
| Color | CSS design tokens only — no hardcoded hex/rgb |
|
|
301
462
|
| Minimum font size | **`text-xs`** (11px at 16px root via `--text-xs`) or larger — never arbitrary classes below 11px (`AGENTS.md` §8.3) |
|
|
302
463
|
|
|
303
|
-
Before adding any component: search `components/ui/` first. Add a prop/variant to an existing component rather than creating a parallel one.
|
|
464
|
+
Before adding any component: search `components/ui/` first. Add a prop/variant to an existing component rather than creating a parallel one. **If nothing fits** and you need a **new shared primitive or large bespoke widget**, **ask the user** before new files — **`.cursor/rules/exxat-reuse-before-custom.mdc`** (unless the task already approved greenfield).
|
|
304
465
|
|
|
305
466
|
---
|
|
306
467
|
|
|
@@ -395,7 +556,7 @@ import { DropdownMenuItem, Shortcut } from "@/components/ui/dropdown-menu"
|
|
|
395
556
|
| Duplicate | ⌘/Ctrl + D |
|
|
396
557
|
| Review / Info | ⌘/Ctrl + I |
|
|
397
558
|
| Remove / Delete item | ⌘/Ctrl + ⌫ |
|
|
398
|
-
| Add view (1..n) |
|
|
559
|
+
| Add view (1..n) | **1..9** (plain digit; `dataListViewAddShortcut`) |
|
|
399
560
|
| **Submit a workflow** (Create, Save, Export, Apply) | **Enter** ⏎ — scoped to the open form/drawer/dialog |
|
|
400
561
|
| **Cancel / dismiss** a workflow | **Esc** (Radix Dialog/Sheet/AlertDialog already bind this) |
|
|
401
562
|
| **Advance a multi-step wizard** | ⌘/Ctrl + Enter (plain Enter must not submit mid-flow) |
|
|
@@ -450,7 +611,7 @@ Reference implementations: `new-placement-form.tsx` (Create placement = Enter on
|
|
|
450
611
|
- **Natural language / AI:** Product **SHOULD** show **quick results in the palette** when the response fits; use **Ask Leo** (**⌘⌥K**) for **longer or complex** answers.
|
|
451
612
|
- **Do not** treat the palette as a static link list only—leave room for inline AI results as they ship.
|
|
452
613
|
|
|
453
|
-
**Details:** `
|
|
614
|
+
**Details:** `apps/web/docs/command-menu-pattern.md`, **`apps/web/AGENTS.md` §7.1** (or `./` when the app folder is the workspace root).
|
|
454
615
|
|
|
455
616
|
---
|
|
456
617
|
|
|
@@ -461,7 +622,7 @@ Full checklist in `references/accessibility.md`. Summary of the most-violated ru
|
|
|
461
622
|
### Structure
|
|
462
623
|
- One `<main id="main-content" tabIndex={-1}>` per page
|
|
463
624
|
- One `<h1>` per page (via `PageHeader`) — `SiteHeader` title is NOT an h1
|
|
464
|
-
- `DialogTitle` / `SheetTitle`
|
|
625
|
+
- `DialogTitle` / `SheetTitle` always present (use `className="sr-only"` if visually hidden)
|
|
465
626
|
|
|
466
627
|
### ARIA roles
|
|
467
628
|
- `role="tablist"` → only `role="tab"` children. **Never** put buttons, menus, or other controls inside `tablist`
|
|
@@ -705,8 +866,8 @@ Copy and complete for every list/table/hub page:
|
|
|
705
866
|
- [ ] Sidebar item added to `lib/mock/navigation.tsx` with light/solid icon pair
|
|
706
867
|
- [ ] **Shell sidebar:** Product header uses **`ExxatProductLogo`**; school **`logoDevUrl`** + **`lib/logo-dev`**; team switcher menu **`!w-max`** (not trigger-width-only); expanded switcher **`h-auto min-h-12`** so school + program lines are not clipped; no **`CollapsibleTrigger` → `SidebarMenuButton` with `tooltip` prop**; child nav uses **popover** on icon rail per **§3.1**
|
|
707
868
|
- [ ] Hub pages: `ListPageTemplate` + `DataTable` + `useTableState` + `TablePropertiesDrawer`
|
|
708
|
-
- [ ] Board view: `ListPageBoardCard` shell + `ListHubStatusBadge` + `list-status-badges` when applicable (`
|
|
709
|
-
- [ ] New primary hubs: not placeholder-only — full template + data + views (`
|
|
869
|
+
- [ ] Board view: `ListPageBoardCard` shell + `ListHubStatusBadge` + `list-status-badges` when applicable (`apps/web/AGENTS.md` §4.4)
|
|
870
|
+
- [ ] New primary hubs: not placeholder-only — full template + data + views (`apps/web/AGENTS.md` §4.1)
|
|
710
871
|
- [ ] **§6.4:** Parent **context** + quick view/actions → drawer/sheet; primary or long flows → **new page** (`AGENTS.md`, `docs/data-views-pattern.md`)
|
|
711
872
|
- [ ] No raw `<table>` or `ui/table` for product data lists
|
|
712
873
|
- [ ] No double horizontal padding around `DataTable`
|
|
@@ -718,7 +879,7 @@ Copy and complete for every list/table/hub page:
|
|
|
718
879
|
- [ ] All icons: `aria-hidden="true"`; Ask Leo: `fa-duotone fa-solid fa-star-christmas text-brand`
|
|
719
880
|
- [ ] **Every icon that communicates info has a text alternative** — Case A adjacent label (preferred), Case B `role="img"` + `aria-label` + `Tooltip` (calendar-for-date, status dot, trend arrow, icon-only legend), Case C `aria-label` + `Tooltip` on icon-only buttons; target ≥ 24×24 px. See §12 *Icons that communicate information*.
|
|
720
881
|
- [ ] **`Kbd` inside a `Button` uses `variant="bare"`** (glue chords into one bare kbd); **`Kbd` inside `TooltipContent` uses the default tile** — see §11 Keyboard shortcuts
|
|
721
|
-
- [ ] `DialogTitle`/`SheetTitle
|
|
882
|
+
- [ ] `DialogTitle`/`SheetTitle` present on every overlay
|
|
722
883
|
- [ ] `role="tablist"` contains only tab-role children
|
|
723
884
|
- [ ] No new shadcn components, no hardcoded colors, no duplicate component abstractions
|
|
724
885
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Exxat DS — WCAG 2.1 AA, ARIA tablists, 24px targets, contrast; see AGENTS.md §8 and exxat-accessibility skill
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
appliesTo: [universal]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Exxat DS — accessibility (binding summary)
|
|
8
|
+
|
|
9
|
+
**Full checklist (ARIA, touch targets, color, sidebar badges, audit follow-ups):** repo **`.cursor/skills/exxat-accessibility/SKILL.md`**.
|
|
10
|
+
|
|
11
|
+
**Product rules in prose + checklist:** **`apps/web/AGENTS.md` §8**.
|
|
12
|
+
|
|
13
|
+
## Non‑negotiables
|
|
14
|
+
|
|
15
|
+
1. **Target:** **WCAG 2.1 Level AA** (2.2 where noted — e.g. target size).
|
|
16
|
+
2. **`role="tablist"`** — only **`role="tab"`** (or equivalent) as direct tab semantics. **MUST NOT** put `role="button"`, menus (`aria-haspopup`), or other controls **inside** the same `tablist` container.
|
|
17
|
+
3. **Composite view switchers** (tabs + per-tab menu + remove): use **`role="toolbar"`** + **`aria-label`**; **`aria-pressed`** on toggles — **MUST NOT** misuse `tab`/`tablist` for those controls.
|
|
18
|
+
4. **Touch targets (2.5.8):** interactive controls **≥ 24×24 CSS px** or **24px** spacing so hit areas do not overlap — use **`min-h-6 min-w-6` / `size-6`** for icon-only targets; avoid **`size-4`** as the sole target.
|
|
19
|
+
5. **Contrast:** normal text **≥ 4.5:1**; UI components / focus where required **≥ 3:1**; muted text on tinted surfaces use tokens against the correct surface (e.g. sidebar), not only `--background`.
|
|
20
|
+
6. **Minimum text size:** visible product copy **≥ 11px** — use **`text-xs`** or larger; **MUST NOT** use arbitrary Tailwind font sizes below that (see **`apps/web/AGENTS.md` §8.3**, **`apps/web/app/globals.css`** `--text-xs`).
|
|
21
|
+
7. **Dialogs / sheets:** must expose a **Title** (`DialogTitle` / `SheetTitle`); use **`sr-only`** if visually hidden (shadcn pattern).
|
|
22
|
+
8. **Format hints are persistent, not placeholders (SC 3.3.2 Labels or Instructions, 1.3.1).** Any field that expects a specific format — **date, time, phone, currency, ID pattern, GPA scale, URL, hours, unit-bearing numbers** — MUST show the format as **persistent helper text** via **`FormDescription`** (or the field's description slot). Placeholders disappear on focus and are unreliable for AT, so **MUST NOT** be the sole carrier of the format. Example: GPA → "Out of 4.0"; Date → "MM/DD/YYYY"; Phone → "+1 (555) 555-0100"; Student ID → "STU-YYYY-####". Pair with `inputMode`, `pattern`, or a picker primitive (e.g. `DatePickerField`) where applicable; never rely on a free-text date input.
|
|
23
|
+
9. **Every icon that communicates information MUST have a text alternative** — not just icon-only buttons. Three cases (SC 1.1.1 Non-text Content, 3.3.2 Labels or Instructions, 2.4.6 Headings and Labels):
|
|
24
|
+
|
|
25
|
+
- **A. Decorative icon next to text that already names it** (e.g. `<i class="fa-light fa-calendar-days" aria-hidden /> 12/14/2025 – 12/20/2025`) → icon MUST be **`aria-hidden`**; MUST NOT add `aria-label` (screen readers would announce the meaning twice). No tooltip needed.
|
|
26
|
+
- **B. Informational icon standing alone** — a calendar glyph used to mean "date range", clock for "updated at", pin for "site", graduation cap for "student", trending arrow for direction, status dot, icon-only chart legend — MUST pair **`role="img"` + `aria-label`** (or `aria-labelledby` on a wrapping element) with a visible **`Tooltip`** so sighted users who don't recognise the glyph learn the meaning. The icon wrapper MUST be keyboard-focusable (`tabIndex={0}` on the `span[role="img"]`) so the tooltip opens on focus.
|
|
27
|
+
- **C. Interactive icon-only button/link** (close `×`, chevron, overflow `⋯`, sort direction, filter chip dismiss, copy, Ask Leo toggle, row actions) → MUST pair **`aria-label`** on the `<button>` with a wrapping **`Tooltip`**. `aria-label` alone is NOT enough — sighted mouse users and keyboard users rely on the tooltip to discover what a bare icon does.
|
|
28
|
+
|
|
29
|
+
In all three cases, the inner `<i>` / `<svg>` MUST be `aria-hidden`; the accessible name lives on the wrapping element. Tooltip text MUST match the accessible name. Narrow exception: a chevron inside a labelled composite (`Select`, `Combobox`) where the parent control already names the whole thing. See **§8.6 (Case A/B/C)** in `AGENTS.md` and the accessibility skill.
|
|
30
|
+
10. **Keyboard shortcut hints inside buttons** MUST use **`<Kbd variant="bare">`** (no background, no border, inherits `currentColor` at 70%). The default `tile` variant is reserved for **tooltips** and **menu `shortcut=` slots**. Glue multi-key chords into one bare kbd (e.g. `<Kbd variant="bare">⌘⌥K</Kbd>`), not one tile per key. Reference: `Next` / `Back` buttons in `new-library-item-form.tsx`; see `.cursor/rules/exxat-kbd-shortcuts.mdc`.
|
|
31
|
+
|
|
32
|
+
After changing **views toolbar** or **tab** UIs, re-run **axe** (or equivalent) on **Placements** (or the affected page).
|
|
33
|
+
|
|
34
|
+
**Charts:** Keyboard exploration uses **`ChartFigure`**; selected data points should have **visible** focus feedback — see skill **§ Charts (keyboard exploration)** and **`AGENTS.md` §4.3** (`chart-keyboard-selection`).
|
|
35
|
+
|
|
36
|
+
## See also
|
|
37
|
+
|
|
38
|
+
- **`apps/web/AGENTS.md`** §8 — accessibility in project context.
|
|
39
|
+
- **`.cursor/rules/exxat-kbd-shortcuts.mdc`** — shortcuts paired with `Kbd` hints.
|
|
40
|
+
- **`apps/web/.cursor/rules/exxat-dashboard-view-charts.mdc`** — Data view chart keyboard parity.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Exxat DS — board (kanban) cards via ListPageBoardCard, badges, primitives. Auto-attaches when editing React board/list files; ask explicitly when designing kanban surfaces.
|
|
3
|
+
globs: apps/web/components/**/*.{tsx,ts}
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
appliesTo: [react]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Exxat DS — board cards
|
|
9
|
+
|
|
10
|
+
**Authoritative detail:** **`apps/web/AGENTS.md` §4.4** and **`.cursor/skills/exxat-board-cards/SKILL.md`**.
|
|
11
|
+
|
|
12
|
+
## MUST
|
|
13
|
+
|
|
14
|
+
1. **Shell** — Use **`ListPageBoardCard`** from **`components/data-views/list-page-board-card.tsx`** for product board cards (same **`Card` `size="sm"`** treatment as Placements).
|
|
15
|
+
2. **Hierarchy** — **Title** (`ListPageBoardCardTitleRow`) → optional **avatar** (`ListPageBoardCardAvatar` on `trailing`) → **status row** (`ListPageBoardCardBadgeRow` + **`ListHubStatusBadge`** **`surface="board"`**) when the entity has status → **body** (`ListPageBoardCardBody`) with **`BoardCardTwoLineBlock`** / **`BoardCardIconRow`** (**`components/data-views/board-card-primitives.tsx`**).
|
|
16
|
+
3. **Status** — Use **`ListHubStatusBadge`** with **`surface="table"`** in grid / list rows and **`surface="board"`** on kanban cards. Generic semantic tints live in **`lib/list-status-badges.ts`** (**`LIST_HUB_STATUS_TINT_SUCCESS / WARNING / INFO / NEUTRAL / DANGER`**); compose per-domain label / icon maps next to the entity's mock data. Reference: **`library-board-view.tsx`** + **`lib/mock/library.ts`**. **MUST NOT** use **`uppercase`** on those chips.
|
|
17
|
+
4. **Simple column boards** — **`ListPageBoardTemplate`** + **`renderCard`**; compose **`ListPageBoardCard`** inside **`renderCard`**.
|
|
18
|
+
|
|
19
|
+
## MUST NOT
|
|
20
|
+
|
|
21
|
+
- Duplicate **`ListPageBoardCard`** with ad-hoc **`<button>`** + border/padding classes for the same hub pattern.
|
|
22
|
+
- Show **status** only as plain text in the **body** when the hub uses **status** elsewhere — use the **badge row** + shared maps.
|
|
23
|
+
|
|
24
|
+
## See also
|
|
25
|
+
|
|
26
|
+
- **`apps/web/AGENTS.md` §4.4**, **§13** checklist
|
|
27
|
+
- **`apps/web/docs/data-views-pattern.md`** — Board UI reuse
|
|
28
|
+
- **`lib/initials-from-name.ts`** — owner initials when mock has no `initials` field
|