@exxatdesignux/ui 0.1.0 → 0.2.7

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.
Files changed (155) hide show
  1. package/bin/cli.mjs +176 -0
  2. package/bin/init.mjs +15 -1
  3. package/bin/sync-extras.mjs +65 -0
  4. package/consumer-extras/README.md +21 -0
  5. package/consumer-extras/cursor-skills/exxat-accessibility/SKILL.md +282 -0
  6. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +68 -0
  7. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +99 -0
  8. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +713 -0
  9. package/consumer-extras/cursor-skills/exxat-fontawesome-icons/SKILL.md +31 -0
  10. package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +36 -0
  11. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +27 -0
  12. package/consumer-extras/patterns/command-menu-pattern.md +45 -0
  13. package/consumer-extras/patterns/data-views-pattern.md +167 -0
  14. package/package.json +7 -3
  15. package/src/components/ui/sidebar.tsx +7 -2
  16. package/template/.agents/skills/shadcn/SKILL.md +242 -0
  17. package/template/.agents/skills/shadcn/agents/openai.yml +5 -0
  18. package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
  19. package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
  20. package/template/.agents/skills/shadcn/cli.md +257 -0
  21. package/template/.agents/skills/shadcn/customization.md +202 -0
  22. package/template/.agents/skills/shadcn/evals/evals.json +47 -0
  23. package/template/.agents/skills/shadcn/mcp.md +94 -0
  24. package/template/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
  25. package/template/.agents/skills/shadcn/rules/composition.md +195 -0
  26. package/template/.agents/skills/shadcn/rules/forms.md +192 -0
  27. package/template/.agents/skills/shadcn/rules/icons.md +101 -0
  28. package/template/.agents/skills/shadcn/rules/styling.md +162 -0
  29. package/template/.claude/skills/exxat-ds-skill/SKILL.md +712 -0
  30. package/template/.cursor/rules/exxat-accessibility.mdc +33 -0
  31. package/template/.cursor/rules/exxat-command-menu.mdc +23 -0
  32. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +53 -0
  33. package/template/.cursor/rules/exxat-data-tables.mdc +31 -0
  34. package/template/.cursor/rules/exxat-ds-agents.mdc +26 -0
  35. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +100 -0
  36. package/template/.cursor/rules/exxat-list-page-connected-views.mdc +16 -0
  37. package/template/.cursor/rules/exxat-no-toast.mdc +26 -0
  38. package/template/.cursor/rules/exxat-page-vs-drawer.mdc +22 -0
  39. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +40 -0
  40. package/template/AGENTS.md +52 -11
  41. package/template/app/(app)/dashboard/page.tsx +1 -1
  42. package/template/app/(app)/data-list/[id]/page.tsx +24 -8
  43. package/template/app/(app)/data-list/new/page.tsx +7 -4
  44. package/template/app/(app)/data-list/page.tsx +1 -1
  45. package/template/app/(app)/examples/page.tsx +41 -0
  46. package/template/app/(app)/question-bank/page.tsx +3 -3
  47. package/template/app/globals.css +1 -1
  48. package/template/components/app-sidebar.tsx +52 -35
  49. package/template/components/compliance-table.tsx +79 -0
  50. package/template/components/data-list-client.tsx +36 -25
  51. package/template/components/data-list-table.tsx +797 -10
  52. package/template/components/data-views/finder-panel-view.tsx +405 -0
  53. package/template/components/data-views/folder-grid-view.tsx +86 -0
  54. package/template/components/data-views/index.ts +59 -0
  55. package/template/components/data-views/list-page-split-details-placeholder.tsx +39 -0
  56. package/template/components/data-views/list-page-split-hub-chrome.tsx +60 -0
  57. package/template/components/data-views/list-page-split-hub-tokens.ts +16 -0
  58. package/template/components/data-views/list-page-tree-column-header.tsx +31 -0
  59. package/template/components/data-views/list-page-tree-panel-shell.tsx +91 -0
  60. package/template/components/data-views/list-page-view-frame.tsx +53 -0
  61. package/template/components/data-views/os-folder-glyph.tsx +121 -0
  62. package/template/components/folder-details-shell.tsx +230 -0
  63. package/template/components/hub-tree-panel-view.tsx +672 -0
  64. package/template/components/list-hub-status-badge.tsx +17 -3
  65. package/template/components/page-header.tsx +149 -7
  66. package/template/components/placements-page-header.tsx +14 -8
  67. package/template/components/placements-table-columns.tsx +8 -8
  68. package/template/components/question-bank-client.tsx +157 -39
  69. package/template/components/question-bank-new-folder-sheet.tsx +248 -0
  70. package/template/components/question-bank-os-folder-view.tsx +648 -0
  71. package/template/components/question-bank-page-header.tsx +31 -2
  72. package/template/components/question-bank-panel-activator.tsx +9 -0
  73. package/template/components/question-bank-secondary-nav.tsx +226 -0
  74. package/template/components/question-bank-table.tsx +707 -22
  75. package/template/components/secondary-panel.tsx +41 -107
  76. package/template/components/sites-table.tsx +66 -0
  77. package/template/components/team-client.tsx +7 -0
  78. package/template/components/team-table.tsx +156 -1
  79. package/template/components/templates/list-page.tsx +2 -2
  80. package/template/components/ui/avatar.tsx +1 -1
  81. package/template/components/ui/badge.tsx +1 -1
  82. package/template/components/ui/banner.tsx +1 -1
  83. package/template/components/ui/breadcrumb.tsx +1 -1
  84. package/template/components/ui/button.tsx +1 -1
  85. package/template/components/ui/calendar.tsx +1 -1
  86. package/template/components/ui/card.tsx +1 -1
  87. package/template/components/ui/chart.tsx +1 -1
  88. package/template/components/ui/checkbox.tsx +1 -1
  89. package/template/components/ui/coach-mark.tsx +1 -1
  90. package/template/components/ui/collapsible.tsx +1 -1
  91. package/template/components/ui/command.tsx +1 -1
  92. package/template/components/ui/date-picker-field.tsx +1 -1
  93. package/template/components/ui/dialog.tsx +1 -1
  94. package/template/components/ui/drag-handle-grip.tsx +1 -1
  95. package/template/components/ui/drawer.tsx +1 -1
  96. package/template/components/ui/dropdown-menu.tsx +1 -1
  97. package/template/components/ui/field.tsx +1 -1
  98. package/template/components/ui/form.tsx +1 -1
  99. package/template/components/ui/input-group.tsx +1 -1
  100. package/template/components/ui/input-mask.tsx +1 -1
  101. package/template/components/ui/input.tsx +1 -1
  102. package/template/components/ui/kbd.tsx +1 -1
  103. package/template/components/ui/label.tsx +1 -1
  104. package/template/components/ui/payment-card-fields.tsx +1 -1
  105. package/template/components/ui/popover.tsx +1 -1
  106. package/template/components/ui/radio-group.tsx +1 -1
  107. package/template/components/ui/resizable.tsx +68 -0
  108. package/template/components/ui/select.tsx +1 -1
  109. package/template/components/ui/selection-tile-grid.tsx +1 -1
  110. package/template/components/ui/separator.tsx +1 -1
  111. package/template/components/ui/sheet.tsx +1 -1
  112. package/template/components/ui/sidebar.tsx +1 -1
  113. package/template/components/ui/skeleton.tsx +1 -1
  114. package/template/components/ui/sonner.tsx +1 -1
  115. package/template/components/ui/status-badge.tsx +1 -1
  116. package/template/components/ui/table.tsx +1 -1
  117. package/template/components/ui/tabs.tsx +1 -1
  118. package/template/components/ui/textarea.tsx +1 -1
  119. package/template/components/ui/tip.tsx +1 -1
  120. package/template/components/ui/toggle-group.tsx +1 -1
  121. package/template/components/ui/toggle-switch.tsx +1 -1
  122. package/template/components/ui/toggle.tsx +1 -1
  123. package/template/components/ui/tooltip.tsx +1 -1
  124. package/template/components/ui/view-segmented-control.tsx +1 -1
  125. package/template/docs/data-views-pattern.md +7 -0
  126. package/template/hooks/use-app-theme.ts +1 -1
  127. package/template/hooks/use-coach-mark.ts +1 -1
  128. package/template/hooks/use-location-hash.ts +15 -0
  129. package/template/hooks/use-mobile.ts +1 -1
  130. package/template/hooks/use-mod-key-label.ts +1 -1
  131. package/template/hooks/use-sidebar-reflow-zoom.ts +40 -0
  132. package/template/lib/ask-leo-route-context.ts +25 -57
  133. package/template/lib/coach-mark-registry.ts +13 -13
  134. package/template/lib/command-menu-config.ts +28 -23
  135. package/template/lib/command-menu-search-data.ts +10 -9
  136. package/template/lib/data-list-view-surface.ts +12 -1
  137. package/template/lib/data-list-view.ts +6 -3
  138. package/template/lib/date-filter.ts +1 -1
  139. package/template/lib/mock/dashboard.ts +11 -11
  140. package/template/lib/mock/navigation.tsx +22 -63
  141. package/template/lib/mock/placements-kpi.ts +19 -19
  142. package/template/lib/mock/question-bank-folders.ts +167 -0
  143. package/template/lib/mock/question-bank-header-collaborators.ts +14 -0
  144. package/template/lib/mock/question-bank-inspector.ts +109 -0
  145. package/template/lib/mock/question-bank-kpi.ts +1 -1
  146. package/template/lib/mock/question-bank.ts +80 -0
  147. package/template/lib/question-bank-nav.ts +91 -0
  148. package/template/lib/utils.ts +1 -1
  149. package/template/next.config.mjs +8 -0
  150. package/template/package.json +1 -0
  151. package/template/public/folders/icons8-folder-windows-11.svg +1 -0
  152. package/template/app/(app)/compliance/page.tsx +0 -10
  153. package/template/app/(app)/rotations/page.tsx +0 -15
  154. package/template/app/(app)/sites/all/page.tsx +0 -13
  155. package/template/app/(app)/team/page.tsx +0 -10
@@ -0,0 +1,713 @@
1
+ ---
2
+ name: exxat-ds
3
+ description: >
4
+ Complete rules, patterns, and architecture guide for the Exxat DS Next.js design system.
5
+ Use this skill whenever working on any feature, page, component, or nav item in the Exxat DS
6
+ codebase — including adding sidebar items, creating list pages, building data tables,
7
+ wiring navigation, writing accessible UI, handling dates, adding tooltips, using icons,
8
+ or adding charts, graphs, KPI cards, or any data visualization.
9
+ Also apply whenever the user asks about Exxat patterns, component reuse, WCAG compliance
10
+ for this project, or asks "how do I build X" in the Exxat DS context.
11
+ ---
12
+
13
+ # Exxat DS — Patterns & Rules Handbook
14
+
15
+ > **Read this before writing any code.** Every section below is binding. "Done" means passing all applicable rules here.
16
+
17
+ ---
18
+
19
+ ## 1. Project Overview
20
+
21
+ - **Stack:** Next.js 16 (App Router), React, TypeScript, Tailwind CSS, shadcn/ui primitives, Font Awesome icons
22
+ - **App root:** `exxat-ds/app/(app)/` — route group that wraps all authenticated pages
23
+ - **Single source of truth:** `exxat-ds/AGENTS.md` for full prose explanations; this skill is the actionable summary
24
+ - **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-accessibility`, `exxat-board-cards` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
25
+
26
+ ---
27
+
28
+ ## 2. Page Architecture
29
+
30
+ Every page inside `app/(app)/` uses this exact shell:
31
+
32
+ ```tsx
33
+ // app/(app)/my-feature/page.tsx
34
+ import { SiteHeader } from "@/components/site-header"
35
+ import { MyFeatureClient } from "@/components/my-feature-client"
36
+ import { SidebarInset } from "@/components/ui/sidebar"
37
+
38
+ export default function MyFeaturePage() {
39
+ return (
40
+ <SidebarInset>
41
+ <SiteHeader title="My Feature" />
42
+ <main id="main-content" tabIndex={-1} className="flex flex-1 flex-col outline-none">
43
+ <div className="@container/main flex flex-1 flex-col w-full max-w-[1440px] mx-auto">
44
+ <MyFeatureClient />
45
+ </div>
46
+ </main>
47
+ </SidebarInset>
48
+ )
49
+ }
50
+ ```
51
+
52
+ **Rules:**
53
+ - `SidebarInset` is always the outermost wrapper
54
+ - `SiteHeader` always goes directly inside it, before `<main>`
55
+ - `<main id="main-content" tabIndex={-1}>` is required — it's the skip-link target
56
+ - Move all interactive/stateful logic into a `"use client"` component (e.g. `MyFeatureClient`) — keep page.tsx as a server component
57
+
58
+ ---
59
+
60
+ ## 3. Adding a Sidebar Nav Item
61
+
62
+ All navigation lives in **`lib/mock/navigation.tsx`** — it is the single source of truth.
63
+
64
+ To add a primary nav item, append to `NAV_PRIMARY`:
65
+
66
+ ```tsx
67
+ {
68
+ key: "my-feature",
69
+ title: "My Feature",
70
+ url: "/my-feature",
71
+ icon: <i className="fa-light fa-<icon-name>" aria-hidden="true" />,
72
+ iconActive: <i className="fa-solid fa-<icon-name>" aria-hidden="true" />,
73
+ }
74
+ ```
75
+
76
+ - `icon` uses `fa-light` weight; `iconActive` uses `fa-solid` — always pair them
77
+ - All icons must have `aria-hidden="true"` (decorative)
78
+ - Optional `badge?: number | string` — `"New"` → green, `"Beta"` → amber, other strings → brand color
79
+ - For document-section items add to `NAV_DOCUMENTS` instead
80
+ - For utility links (Settings, Search, Help) add to `NAV_SECONDARY`
81
+
82
+ **Routing:** create the page at `app/(app)/<key>/page.tsx` — the url must match the key.
83
+
84
+ ### 3.1 Application sidebar shell (`app-sidebar.tsx`)
85
+
86
+ **Data:** `lib/mock/navigation.tsx` also holds **`NAV_SCHOOLS`**, **`NAV_USER`**, and related defaults. School marks use **`logoDevUrl()`** from **`lib/logo-dev.ts`** (publishable token; optional **`NEXT_PUBLIC_LOGO_DEV_TOKEN`**).
87
+
88
+ | Concern | Pattern |
89
+ |--------|---------|
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. |
91
+ | **School/program menu width** | **`DropdownMenuContent`** ships with **`w-(--radix-dropdown-menu-trigger-width)`**, so the panel matches the **narrow sidebar trigger** and long names wrap too early. For **`TeamSwitcher`**, override with e.g. **`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**. |
92
+ | **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
+ | **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
+ | **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** | **Expanded:** **`Collapsible`**. **Desktop icon rail:** **`Popover`** listing child links. **Do not** pass **`tooltip={…}`** on **`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. |
96
+ | **Profile (mock)** | **`stockPortraitUrl()`** from **`lib/stock-portrait.ts`**; **`AvatarImage`** **`referrerPolicy="no-referrer"`** for external URLs. |
97
+
98
+ **Reference:** `components/app-sidebar.tsx`, `components/nav-user.tsx`, `components/product-switcher.tsx`.
99
+
100
+ ---
101
+
102
+ ## 4. Primary Hub Pages — Mandatory Pattern
103
+
104
+ Any **primary nav destination** that shows a list of records **must** use this composition (same as Placements / Team):
105
+
106
+ ```
107
+ ListPageTemplate
108
+ ├── PageHeader (title, subtitle with count, primary CTA, ⋯ more menu)
109
+ ├── KeyMetrics (flat variant, single row)
110
+ └── renderContent()
111
+ └── DataTable + useTableState + TablePropertiesDrawer
112
+ ```
113
+
114
+ **Reference implementations:**
115
+ - `components/team-client.tsx` + `components/team-table.tsx` — canonical pattern
116
+ - `components/data-list-client.tsx` + `components/data-list-table.tsx` — Placements (most complete)
117
+
118
+ **Files to create for a new hub page `Foo`:**
119
+ | File | Purpose |
120
+ |------|---------|
121
+ | `lib/mock/foo.ts` | Mock data + TypeScript interface (12+ rows) |
122
+ | `lib/mock/foo-kpi.ts` | `fooKpiMetrics()` + `fooKpiInsight()` |
123
+ | `components/foo-page-header.tsx` | `PageHeader` + primary CTA + ⋯ menu |
124
+ | `components/foo-table.tsx` | `DataTable` + `useTableState` + `TablePropertiesDrawer` |
125
+ | `components/foo-client.tsx` | `ListPageTemplate` orchestrator |
126
+ | `app/(app)/foo/page.tsx` | Thin server component |
127
+
128
+ **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 **`exxat-ds/AGENTS.md` §4.1**) unless the product explicitly defines a non-data shell.
129
+
130
+ ### Page vs drawer (actions)
131
+
132
+ - **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).
133
+ - **New page** — Use **otherwise**: **primary**, **long-form**, **multi-step**, or flows that need their **own URL** without the hub visible.
134
+
135
+ Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`.cursor/rules/exxat-page-vs-drawer.mdc`**.
136
+
137
+ ---
138
+
139
+ ## 5. Data Table Stack
140
+
141
+ **Always use these — never raw `<table>` or shadcn's `ui/table` for product data lists.**
142
+
143
+ | Import | Purpose |
144
+ |--------|---------|
145
+ | `DataTable` from `@/components/data-table` | Base table |
146
+ | `useTableState` from `@/components/data-table/use-table-state` | Sort, filter, column state |
147
+ | `TablePropertiesDrawer` from `@/components/table-properties` | Columns, density, filters, sort, conditional rules |
148
+ | `ColumnDef` from `@/components/data-table/types` | Column type |
149
+ | `FilterFieldDef`, `FilterOperator`, `ConditionalRule` from `@/components/table-properties/types` | Filter types |
150
+
151
+ **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, Question bank, …): 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 **`data-list-table-cells.tsx`** (wrapper over **`ListHubStatusBadge`** + **`PLACEMENT_STATUS_*`**). **Full rules:** **`exxat-ds/AGENTS.md` §4.4**, **`.cursor/rules/exxat-board-cards.mdc`**, **`.cursor/skills/exxat-board-cards/SKILL.md`**.
152
+
153
+ **Minimum required features on any data list page:**
154
+ - Search (wire `searchable={displayOptions.showToolbarSearch}`)
155
+ - Filters (via `TablePropertiesDrawer` filter fields)
156
+ - Table properties drawer (Properties button with `fa-light fa-sliders`)
157
+ - `selectable={true}` with bulk-actions slot
158
+ - `emptyState` prop with helpful message
159
+
160
+ **Column definition pattern:**
161
+ ```ts
162
+ {
163
+ key: "name",
164
+ label: "Name",
165
+ width: 240,
166
+ minWidth: 160,
167
+ sortable: true,
168
+ sortKey: "name",
169
+ filter: {
170
+ type: "text", // "text" | "select" | "date"
171
+ icon: "fa-user",
172
+ operators: ["contains", "not_contains"],
173
+ },
174
+ cell: row => <span className="text-sm font-medium text-foreground">{row.name}</span>,
175
+ }
176
+ ```
177
+
178
+ **Pin conventions:**
179
+ - `select` column: `defaultPin: "left"`, `lockPin: true`
180
+ - `actions` column: `defaultPin: "right"`, `lockPin: true`
181
+
182
+ **DataTable must wrap in `<div className="pb-6">`.**
183
+
184
+ ---
185
+
186
+ ## 6. Page Header Pattern
187
+
188
+ Use `PageHeader` from `@/components/page-header` for the content-area header (below SiteHeader):
189
+
190
+ ```tsx
191
+ <PageHeader
192
+ title="Foo"
193
+ subtitle={`${count} items · Last updated now`}
194
+ actions={
195
+ <div className="flex items-center gap-2" role="group" aria-label="Foo actions">
196
+ <Tip side="bottom" label="Add a new foo">
197
+ <Button type="button" size="lg" onClick={onAdd}>
198
+ <i className="fa-light fa-plus" aria-hidden="true" />
199
+ Add foo
200
+ </Button>
201
+ </Tip>
202
+ <DropdownMenu ...>
203
+ <Tip side="bottom" label="More actions">
204
+ <DropdownMenuTrigger asChild>
205
+ <Button type="button" size="lg" variant="outline" className="aspect-square px-0" aria-label="More actions">
206
+ <i className="fa-light fa-ellipsis text-base" aria-hidden="true" />
207
+ </Button>
208
+ </DropdownMenuTrigger>
209
+ </Tip>
210
+ <DropdownMenuContent align="end" className="w-52">
211
+ <DropdownMenuItem onClick={onExport}>
212
+ <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
213
+ Export
214
+ </DropdownMenuItem>
215
+ <DropdownMenuSeparator />
216
+ <DropdownMenuItem onClick={onToggleMetrics}>
217
+ <i className={`fa-light ${showMetrics ? "fa-eye-slash" : "fa-eye"}`} aria-hidden="true" />
218
+ {showMetrics ? "Hide metric section" : "Show metric section"}
219
+ </DropdownMenuItem>
220
+ </DropdownMenuContent>
221
+ </DropdownMenu>
222
+ </div>
223
+ }
224
+ />
225
+ ```
226
+
227
+ **Rules:**
228
+ - Primary CTA: `Button size="lg"` (filled/default) — never `variant="outline"` as the sole primary action
229
+ - More (⋯): `variant="outline"` icon-only button → dropdown with Export → `ExportDrawer`
230
+ - Subtitle: `"{count} items · Last updated now"` format
231
+ - Title uses Ivy Presto (`font-heading` variable) — applied automatically by `PageHeader`
232
+
233
+ ---
234
+
235
+ ## 7. Navigation: Breadcrumbs vs Back Link
236
+
237
+ **Never use both on the same page. Pick one:**
238
+
239
+ | Page type | Use |
240
+ |-----------|-----|
241
+ | Detail page (child of a list) | **Breadcrumbs** via `SiteHeader` `breadcrumbs` prop |
242
+ | Full-page form / wizard | **Back link** only (no breadcrumbs) |
243
+
244
+ ```tsx
245
+ // Detail page — breadcrumbs
246
+ <SiteHeader
247
+ title="Sarah Johnson"
248
+ breadcrumbs={[
249
+ { label: "Placements", href: "/data-list" },
250
+ { label: "Placement Details" },
251
+ ]}
252
+ />
253
+
254
+ // Form page — back link + SidebarAutoCollapse
255
+ <SidebarAutoCollapse />
256
+ <Link href="/data-list">← Back</Link>
257
+ ```
258
+
259
+ Breadcrumb separator: `fa-light fa-chevron-right text-[8px]`. Last segment is `font-medium text-foreground`, parent segments are `text-muted-foreground`.
260
+
261
+ ---
262
+
263
+ ## 8. Component Reuse — Hard Rules
264
+
265
+ Never install new packages or create parallel components. Always use what exists.
266
+
267
+ | Need | Use |
268
+ |------|-----|
269
+ | Any button | `Button` from `@/components/ui/button` |
270
+ | Chart / graph card | `ChartCard` from `@/components/charts-overview` |
271
+ | Drawer / panel | `Sheet` from `@/components/ui/sheet` (floating style — see memory) |
272
+ | Tooltip | `Tip` from `@/components/ui/tip` — never `title` attribute |
273
+ | Keyboard hint | `Kbd` / `KbdGroup` from `@/components/ui/kbd` |
274
+ | Badge | `Badge` from `@/components/ui/badge` |
275
+ | Tabs | `Tabs`/`TabsList`/`TabsTrigger` from `@/components/ui/tabs` |
276
+ | Banner (page-level) | `SystemBanner` / `LocalBanner` from `@/components/ui/banner` |
277
+ | Success / error feedback | Inline copy, `LocalBanner`, or dialog — **never** `toast()` / Sonner / snackbars (**`AGENTS.md` §6.5**, **`.cursor/rules/exxat-no-toast.mdc`**) |
278
+ | Date formatting | `formatDateUS()` / `formatDateTimeUS()` from `@/lib/date-filter` |
279
+ | Modifier key label | `useModKeyLabel()` from `@/hooks/use-mod-key-label` |
280
+ | Class merging | `cn()` from `@/lib/utils` |
281
+ | Color | CSS design tokens only — no hardcoded hex/rgb |
282
+ | Minimum font size | **`text-xs`** (11px at 16px root via `--text-xs`) or larger — never arbitrary classes below 11px (`AGENTS.md` §8.3) |
283
+
284
+ Before adding any component: search `components/ui/` first. Add a prop/variant to an existing component rather than creating a parallel one.
285
+
286
+ ---
287
+
288
+ ## 9. Date & Time Format
289
+
290
+ Mandatory across the **entire** app — tables, filters, tooltips, forms, everywhere:
291
+
292
+ | Type | Format | Example |
293
+ |------|--------|---------|
294
+ | Date only | `MM/DD/YYYY` | `03/15/2026` |
295
+ | Date + Time | `MM/DD/YYYY hh:mm AM/PM EST` | `03/15/2026 09:30 AM EST` |
296
+
297
+ - Zero-pad month and day: `03` not `3`
298
+ - 4-digit year always
299
+ - 12-hour clock, uppercase AM/PM, append `EST`
300
+ - Never ISO (`2026-03-15`) or verbose (`Mar 15, 2026`) in the UI
301
+ - Date picker: Calendar + Popover component; format displayed as `MM/DD/YYYY`
302
+
303
+ ### 9.1 Format hints MUST be persistent (WCAG 3.3.2)
304
+
305
+ Any input whose value must follow a specific format — **dates, times, phone, currency, GPA, Student IDs, URLs, unit-bearing numbers** — MUST render the format as **persistent helper text** via `FormDescription` (or any element tied to the input via `aria-describedby`). Placeholders disappear on focus and are not reliably announced — they **MUST NOT** be the sole carrier of the format.
306
+
307
+ ```tsx
308
+ <FormField name="startDate" render={({ field }) => (
309
+ <FormItem>
310
+ <FormLabel>Start date<span aria-hidden="true"> *</span></FormLabel>
311
+ <FormControl><DatePickerField value={field.value} onChange={field.onChange} /></FormControl>
312
+ <FormDescription>MM/DD/YYYY</FormDescription>
313
+ <FormMessage />
314
+ </FormItem>
315
+ )} />
316
+ ```
317
+
318
+ Units belong in `FormDescription` (e.g. GPA → "Out of 4.0"; Hours/week → "hrs/wk"), not hidden in the placeholder. Prefer picker primitives (`DatePickerField`, `Select`) over free-text wherever one exists. Full checklist: `.cursor/skills/exxat-accessibility/SKILL.md` — *Form fields — format hints*.
319
+
320
+ ---
321
+
322
+ ## 10. Ask Leo Icon
323
+
324
+ Every Ask Leo / AI surface uses this exact class pattern:
325
+
326
+ ```tsx
327
+ <i className="fa-duotone fa-solid fa-star-christmas text-brand" aria-hidden="true" />
328
+ ```
329
+
330
+ - `fa-duotone fa-solid fa-star-christmas` — must be duotone solid weight
331
+ - `text-brand` — resolves to `var(--brand-color-dark)`, passes WCAG AA 4.5:1 on white
332
+ - Applies to: Ask Leo toggle (site header), sidebar, chart cards, KPI insight cards, promo banners
333
+ - Never: `fa-light fa-sparkles`, `fa-solid` without `fa-duotone`, or any other star/sparkle variant
334
+
335
+ ---
336
+
337
+ ## 11. Keyboard Shortcuts
338
+
339
+ Show `Kbd` / `KbdGroup` on: primary CTAs, secondary/overflow actions, Search, Ask Leo toggle, Sidebar toggle.
340
+
341
+ ```tsx
342
+ <Tip side="bottom" label={<span className="flex items-center gap-1.5">New <KbdGroup><Kbd>{mod}</Kbd><Kbd>⌥</Kbd><Kbd>N</Kbd></KbdGroup></span>}>
343
+ <Button ...>Add foo</Button>
344
+ </Tip>
345
+ ```
346
+
347
+ **If you show a shortcut, implement it.** Use the shared primitives from `@/components/ui/dropdown-menu`:
348
+
349
+ ```tsx
350
+ import { DropdownMenuItem, Shortcut } from "@/components/ui/dropdown-menu"
351
+
352
+ // Visual hint on the menu item (renders a DropdownMenuShortcut automatically).
353
+ <DropdownMenuItem shortcut="⌘⇧E" onSelect={onExport}>Export</DropdownMenuItem>
354
+
355
+ // Global binding — render in a parent that stays mounted (menu items unmount when closed).
356
+ <Shortcut keys="⌘⇧E" onInvoke={onExport} />
357
+ ```
358
+
359
+ - `shortcut` prop = visual only. `<Shortcut>` = actual key binding.
360
+ - Accepts symbols (`⌘⇧⌥⌃⌫⌦⏎↑↓←→`) or words (`Cmd+Shift+D`, `Alt+P`).
361
+ - The hook automatically skips input/textarea/contenteditable targets and any open modal dialog.
362
+ - Prefer this over ad-hoc `document.addEventListener("keydown", …)` + `isEditableTarget` — one source of truth, fewer stale refs.
363
+
364
+ **Every action menu should carry shortcuts.** Standard bindings across the app:
365
+
366
+ | Action | Shortcut |
367
+ |--------|----------|
368
+ | Toggle sidebar | ⌘/Ctrl + B |
369
+ | Table search | ⌘/Ctrl + K |
370
+ | Ask Leo | ⌘/Ctrl + ⌥/Alt + K |
371
+ | New (primary CTA) | ⌘/Ctrl + ⌥/Alt + N |
372
+ | More (⋯ menu open) | ⌘/Ctrl + ⌥/Alt + M |
373
+ | Export | ⌘/Ctrl + ⇧/Shift + E |
374
+ | Hide/Show metric section | ⌘/Ctrl + ⌥/Alt + H |
375
+ | Rename (view, tab) | F2 |
376
+ | Duplicate | ⌘/Ctrl + D |
377
+ | Review / Info | ⌘/Ctrl + I |
378
+ | Remove / Delete item | ⌘/Ctrl + ⌫ |
379
+ | Add view (1..n) | ⌘/Ctrl + ⇧/Shift + 1..9 |
380
+ | **Submit a workflow** (Create, Save, Export, Apply) | **Enter** ⏎ — scoped to the open form/drawer/dialog |
381
+ | **Cancel / dismiss** a workflow | **Esc** (Radix Dialog/Sheet/AlertDialog already bind this) |
382
+ | **Advance a multi-step wizard** | ⌘/Ctrl + Enter (plain Enter must not submit mid-flow) |
383
+ | **Back in a wizard** | ⌘/Ctrl + ⌥/Alt + ← |
384
+
385
+ **Avoid browser-reserved chords:** ⌘⇧N, ⌘⇧T, ⌘⇧O, ⌘⇧B, ⌘L.
386
+
387
+ ### 11.0 Every workflow primary/secondary action MUST carry Enter / Esc
388
+
389
+ Every form, dialog, drawer, sheet, or wizard MUST show and bind:
390
+
391
+ - **Primary (submit/commit)** → **Enter** ⏎. Render the `<Kbd>⏎</Kbd>` **inline inside the button** (after the label, wrapped in `<KbdGroup className="ml-1.5">`) — NOT in a hover Tip. Workflow buttons must expose the shortcut at rest. Pair with a `<Shortcut keys="Enter" onInvoke={submit} />` mounted while the surface is open. `useShortcut` skips input/textarea/contenteditable events, so Enter in a text field still types — it only fires on surface chrome.
392
+ - **Secondary (Cancel/Dismiss)** → **Esc**. Inline `<Kbd>Esc</Kbd>` in the Cancel button (same `ml-1.5` pattern). Radix `Dialog` / `Sheet` / `AlertDialog` bind Esc natively.
393
+
394
+ Inside a button, use **`<Kbd variant="bare">`** — no background, no border, inherits `currentColor` at 70% opacity — so the hint reads as part of the button label, not a pasted-on tile. The default `variant="tile"` (filled, bordered) is for tooltips, menus, docs, and standalone contexts.
395
+
396
+ ```tsx
397
+ <Button type="submit">
398
+ <i className="fa-light fa-check" aria-hidden="true" />
399
+ Create placement
400
+ <KbdGroup className="ml-1.5"><Kbd variant="bare">⏎</Kbd></KbdGroup>
401
+ </Button>
402
+ <Button type="button" variant="outline" onClick={onCancel}>
403
+ Cancel
404
+ <KbdGroup className="ml-1.5"><Kbd variant="bare">Esc</Kbd></KbdGroup>
405
+ </Button>
406
+ ```
407
+
408
+ Glue multi-key chords into a single `<Kbd variant="bare">` (`⌘⌥←`, `⌘⏎`) rather than one tile per key — otherwise the gap between tiles reintroduces the "patch" look.
409
+
410
+ Hover Tips remain correct for **page-level** actions (e.g. page header "New", ⋯ overflow trigger) where inline Kbd would crowd dense chrome. Inline Kbd is for **workflow surfaces** — forms, dialogs, drawers, wizard footers.
411
+ - **Multi-step wizard safety**: plain Enter MUST NOT submit on intermediate steps, or the final review auto-closes when the user hits Enter inside an input. Gate `form.onSubmit` on `step === lastStep`:
412
+
413
+ ```tsx
414
+ <form onSubmit={(e) => {
415
+ if (step !== LAST) { e.preventDefault(); return }
416
+ form.handleSubmit(onSubmit)(e)
417
+ }}>
418
+ ```
419
+
420
+ Use `⌘Enter` via `<Shortcut>` to advance intermediate steps. On the final step, plain Enter submits and the Kbd hint shows **⏎**.
421
+
422
+ Reference implementations: `new-placement-form.tsx` (Create placement = Enter on step 5, Back = ⌘⌥←), `export-drawer.tsx` (Export = Enter, Cancel = Esc).
423
+
424
+ ### 11.1 Global command palette (⌘K)
425
+
426
+ **`CommandMenu`** is **global search** and the main **AI entry** (not a second nav). Config: **`buildCommandMenuConfig()`** in **`lib/command-menu-config.ts`**, **`CommandMenuProvider`** in **`app/(app)/layout.tsx`**.
427
+
428
+ - **Navigation / library / patterns:** Search and pick a row—**Enter** to go.
429
+ - **Searchable row data:** Pass **`dataGroups`** into **`buildCommandMenuConfig`**. Map mocks/API rows in **`lib/command-menu-search-data.ts`** (e.g. **`getCommandMenuSearchDataGroups()`** from placements / student fields). Do **not** embed data mapping inside **`command-menu.tsx`**.
430
+ - **`searchOnly` groups:** For large indexes, set **`searchOnly: true`** on **`CommandMenuGroup`** so that group is **not rendered** until the user types (cmdk otherwise shows every item when the query is empty). Static groups stay visible on open.
431
+ - **Natural language / AI:** Product **SHOULD** show **quick results in the palette** when the response fits; use **Ask Leo** (**⌘⌥K**) for **longer or complex** answers.
432
+ - **Do not** treat the palette as a static link list only—leave room for inline AI results as they ship.
433
+
434
+ **Details:** `exxat-ds/docs/command-menu-pattern.md`, **`exxat-ds/AGENTS.md` §7.1** (or `./` when the app folder is the workspace root).
435
+
436
+ ---
437
+
438
+ ## 12. Accessibility — WCAG 2.1 AA (Non-Negotiable)
439
+
440
+ Full checklist in `references/accessibility.md`. Summary of the most-violated rules:
441
+
442
+ ### Structure
443
+ - One `<main id="main-content" tabIndex={-1}>` per page
444
+ - One `<h1>` per page (via `PageHeader`) — `SiteHeader` title is NOT an h1
445
+ - `DialogTitle` / `SheetTitle` / `DrawerTitle` always present (use `className="sr-only"` if visually hidden)
446
+
447
+ ### ARIA roles
448
+ - `role="tablist"` → only `role="tab"` children. **Never** put buttons, menus, or other controls inside `tablist`
449
+ - View switchers with extra controls (tabs + remove + settings): use `role="toolbar"` + `aria-label`
450
+
451
+ ### Icons that communicate information — always have a text alternative
452
+
453
+ This rule covers **every icon that carries meaning**, not only icon-only buttons. FA glyphs, inline SVGs, avatar placeholders, trend arrows, status dots, chart-legend squares, calendar/clock/pin icons in cells — if the icon **tells the user something**, that something MUST be reachable by screen readers AND discoverable to sighted users who don't recognise the glyph. SC 1.1.1, 3.3.2, 2.4.6.
454
+
455
+ **Case A — Decorative icon next to text that already names it** → icon is `aria-hidden`, no `aria-label`, no tooltip. The text is the alt.
456
+
457
+ ```tsx
458
+ <span className="flex items-center gap-1.5">
459
+ <i className="fa-light fa-calendar-days" aria-hidden />
460
+ <span>12/14/2025 – 12/20/2025</span>
461
+ </span>
462
+ ```
463
+
464
+ **Case B — Informational icon standing alone** (calendar = "date range", clock = "updated at", pin = "site", cap = "student", trend arrow, status dot, icon-only chart legend) → MUST pair `role="img"` + `aria-label` with a visible `Tooltip`. Wrapper MUST be keyboard-focusable (`tabIndex={0}`).
465
+
466
+ ```tsx
467
+ <Tooltip>
468
+ <TooltipTrigger asChild>
469
+ <span role="img" aria-label="Placement date range" tabIndex={0}
470
+ className="inline-flex size-6 items-center justify-center rounded-md text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
471
+ <i className="fa-light fa-calendar-days" aria-hidden />
472
+ </span>
473
+ </TooltipTrigger>
474
+ <TooltipContent side="top">Placement date range</TooltipContent>
475
+ </Tooltip>
476
+ ```
477
+
478
+ **Case C — Interactive icon-only button / link** (close `×`, chevron, overflow `⋯`, sort, filter-dismiss, copy, Ask Leo toggle, row actions) → MUST pair `aria-label` on the `<button>` with a wrapping `Tooltip`. Inner `<i>` / `<svg>` is `aria-hidden`. Target ≥ 24×24.
479
+
480
+ ```tsx
481
+ <Tooltip>
482
+ <TooltipTrigger asChild>
483
+ <button type="button" aria-label="Close insight" className="size-7 …">
484
+ <i className="fa-solid fa-xmark" aria-hidden />
485
+ </button>
486
+ </TooltipTrigger>
487
+ <TooltipContent side="top" className="flex items-center gap-1.5">
488
+ <span>Close</span>
489
+ <Kbd>Esc</Kbd>
490
+ </TooltipContent>
491
+ </Tooltip>
492
+ ```
493
+
494
+ **Decision tree:** adjacent text label? → A. Else interactive? → C. Else → B. When in doubt: add the accessible name + tooltip. Narrow exception for all cases: a chevron inside a labelled composite (`Select`, `Combobox`) where the parent already carries the name.
495
+
496
+ ### Touch targets
497
+ - Minimum **24×24 CSS px** for all interactive controls
498
+ - Icon-only: `size-6` or `min-h-6 min-w-6` — never `size-4` as sole target
499
+
500
+ ### Color & contrast
501
+ - Normal text ≥ 4.5:1; UI components ≥ 3:1
502
+ - Status never by color alone — always include text label or icon
503
+ - Decorative icons: `aria-hidden="true"`
504
+
505
+ ### Dynamic content
506
+ - Filter/result count changes: `aria-live="polite"`
507
+ - Loading states: `aria-busy="true"`
508
+ - Toasts: `role="status"` or `aria-live="polite"`
509
+
510
+ ---
511
+
512
+ ## 13. Charts & Graphs — Use Existing Cards
513
+
514
+ **Never create a custom card shell for a chart. Always use the existing components.**
515
+
516
+ ### ChartCard — the only chart wrapper
517
+
518
+ Use `ChartCard` from `@/components/charts-overview` for every chart/graph in the app.
519
+
520
+ ```tsx
521
+ import { ChartCard } from "@/components/charts-overview"
522
+
523
+ <ChartCard
524
+ title="Placements Over Time"
525
+ description="Monthly placement activity for the current academic year"
526
+ variant="normal" // see variants below
527
+ >
528
+ {/* Recharts chart goes here */}
529
+ <ChartContainer config={chartConfig} className="h-[200px] w-full">
530
+ <AreaChart data={data}>...</AreaChart>
531
+ </ChartContainer>
532
+ </ChartCard>
533
+ ```
534
+
535
+ ### ChartCard variants — pick the right one
536
+
537
+ | `variant` | When to use |
538
+ |-----------|-------------|
539
+ | `"normal"` | Single chart with Ask Leo button in the header |
540
+ | `"tabs"` | Chart view + Trend (line) toggle, or any custom tab pair |
541
+ | `"selector"` | Dropdown filter (period, program, cohort) above the chart |
542
+ | `"metrics-tabs"` | KPI strip whose tabs drive the chart (metric cells ARE the tab triggers) |
543
+ | `"kpi-chart"` | Hero chart card with prominent KPI number + mini chart |
544
+
545
+ ```tsx
546
+ // selector variant — adds a dropdown filter
547
+ <ChartCard
548
+ variant="selector"
549
+ title="Placements by Program"
550
+ description="Filter by program to compare activity"
551
+ filterOptions={[
552
+ { value: "all", label: "All programs" },
553
+ { value: "nursing", label: "Nursing" },
554
+ { value: "pt", label: "PT" },
555
+ ]}
556
+ defaultFilter="all"
557
+ onFilterChange={setFilter}
558
+ >
559
+ {(filter) => <MyChart program={filter} />}
560
+ </ChartCard>
561
+
562
+ // metrics-tabs variant — KPI cells drive chart
563
+ <ChartCard
564
+ variant="metrics-tabs"
565
+ title="Compliance Trend"
566
+ description="Select a metric to view its trend"
567
+ miniMetrics={[
568
+ { label: "Completed", value: "84%", trend: "up" },
569
+ { label: "Pending", value: "12", trend: "neutral" },
570
+ { label: "Overdue", value: "3", trend: "down" },
571
+ ]}
572
+ >
573
+ {(activeMetric) => <MetricChart metric={activeMetric} />}
574
+ </ChartCard>
575
+ ```
576
+
577
+ ### ChartFigure — accessibility wrapper inside ChartCard
578
+
579
+ Wrap the Recharts chart inside `ChartFigure` to get keyboard navigation (arrow keys through data points) and screen-reader announcements:
580
+
581
+ ```tsx
582
+ import { ChartFigure } from "@/components/charts-overview" // internal export
583
+
584
+ // ChartFigure is used inside ChartCard's children — it handles:
585
+ // - role="application" with aria-label
586
+ // - ArrowLeft/Right to cycle data points
587
+ // - Escape to exit chart navigation
588
+ // - Live region announcements
589
+ ```
590
+
591
+ ### ChartContainer + color tokens
592
+
593
+ ```tsx
594
+ import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
595
+ import type { ChartConfig } from "@/components/ui/chart"
596
+
597
+ const chartConfig: ChartConfig = {
598
+ placements: { label: "Placements", color: "var(--color-chart-1)" },
599
+ compliance: { label: "Compliance", color: "var(--color-chart-2)" },
600
+ }
601
+
602
+ // Always use CSS chart color tokens — never hardcoded hex/rgb:
603
+ // var(--brand-color) — primary brand
604
+ // var(--color-chart-1) through var(--color-chart-5) — series colors
605
+ // var(--chart-2) — success/positive
606
+ // var(--chart-4) — warning
607
+ // var(--destructive) — error/negative
608
+ ```
609
+
610
+ ### Chart accessibility rules
611
+
612
+ These are non-negotiable (built into `ChartCard`/`ChartFigure` when used correctly):
613
+
614
+ 1. **Accessible data table** — add `ChartDataTable` (sr-only) after every chart so screen-reader users can navigate data as a table
615
+ 2. **Color is never the only differentiator** — pair series colors with dashed vs solid lines, shape markers, or inline labels
616
+ 3. **Chart series colors ≥ 3:1** contrast against card background
617
+ 4. **Text inside charts ≥ 4.5:1** on their local background
618
+ 5. **Tooltips on keyboard focus**, not hover only — `ChartTooltipContent` handles this automatically
619
+
620
+ ### KeyMetrics — for KPI strips
621
+
622
+ Use `KeyMetrics` from `@/components/key-metrics` for metric/KPI strips. Do not build a custom metric grid.
623
+
624
+ ```tsx
625
+ <KeyMetrics
626
+ variant="flat" // "card" | "flat" | "compact"
627
+ metrics={metrics} // MetricItem[]
628
+ insight={insight} // MetricInsight
629
+ showHeader={false}
630
+ metricsSingleRow
631
+ />
632
+ ```
633
+
634
+ ### What NOT to do
635
+
636
+ - Do not use raw `Card` + `CardHeader` + a Recharts chart without `ChartCard`
637
+ - Do not install new charting libraries (`react-chartjs-2`, `victory`, `nivo`, etc.) — Recharts is the only chart library
638
+ - Do not hardcode chart colors — use CSS tokens only
639
+ - Do not build a custom KPI/metric row — use `KeyMetrics`
640
+ - Do not add an Ask Leo button manually to chart cards — `ChartCard` includes it automatically
641
+
642
+ ---
643
+
644
+ `DataTable` already applies its own horizontal inset. Do not wrap it in extra `px-*` / `mx-*` — that creates staggered margins between the filter bar and table vs tabs.
645
+
646
+ Follow `ListPageTemplate` → `DataTable`'s own inset — one horizontal rhythm only.
647
+
648
+ ---
649
+
650
+ ## 14. KPI Metrics Pattern
651
+
652
+ Every primary hub page has a collapsible metrics strip:
653
+
654
+ ```tsx
655
+ // lib/mock/foo-kpi.ts
656
+ export function fooKpiMetrics(items: Foo[]): MetricItem[] {
657
+ return [
658
+ { id: "total", label: "Total", value: items.length, delta: "+1", trend: "up", href: "#", metricVariant: "hero" },
659
+ { id: "active", label: "Active", value: activeCount, delta: "—", trend: "neutral", href: "#" },
660
+ // ...more metrics
661
+ ]
662
+ }
663
+
664
+ export function fooKpiInsight(items: Foo[]): MetricInsight {
665
+ return {
666
+ title: "Insight title",
667
+ description: "Short actionable insight based on the data.",
668
+ severity: "info" | "warning",
669
+ actionLabel: "Ask Leo",
670
+ }
671
+ }
672
+ ```
673
+
674
+ In `FooClient`:
675
+ ```tsx
676
+ <KeyMetrics variant="flat" metrics={metrics} insight={insight} showHeader={false} metricsSingleRow />
677
+ ```
678
+
679
+ ---
680
+
681
+ ## 15. AI Execution Checklist
682
+
683
+ Copy and complete for every list/table/hub page:
684
+
685
+ - [ ] Page shell: `SidebarInset` → `SiteHeader` → `<main id="main-content" tabIndex={-1}>` → `@container/main div`
686
+ - [ ] Sidebar item added to `lib/mock/navigation.tsx` with light/solid icon pair
687
+ - [ ] **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**
688
+ - [ ] Hub pages: `ListPageTemplate` + `DataTable` + `useTableState` + `TablePropertiesDrawer`
689
+ - [ ] Board view: `ListPageBoardCard` shell + `ListHubStatusBadge` + `list-status-badges` when applicable (`exxat-ds/AGENTS.md` §4.4)
690
+ - [ ] New primary hubs: not placeholder-only — full template + data + views (`exxat-ds/AGENTS.md` §4.1)
691
+ - [ ] **§6.4:** Parent **context** + quick view/actions → drawer/sheet; primary or long flows → **new page** (`AGENTS.md`, `docs/data-views-pattern.md`)
692
+ - [ ] No raw `<table>` or `ui/table` for product data lists
693
+ - [ ] No double horizontal padding around `DataTable`
694
+ - [ ] Primary CTA: filled `Button size="lg"`; ⋯ more menu with Export + toggle metrics
695
+ - [ ] Breadcrumbs OR back link — never both
696
+ - [ ] Charts: wrapped in `ChartCard` with correct `variant`; no raw `Card` + Recharts combos; color tokens only
697
+ - [ ] All dates: `MM/DD/YYYY` / `MM/DD/YYYY hh:mm AM/PM EST`
698
+ - [ ] All tooltips via `<Tip>` — no `title` attribute
699
+ - [ ] All icons: `aria-hidden="true"`; Ask Leo: `fa-duotone fa-solid fa-star-christmas text-brand`
700
+ - [ ] **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*.
701
+ - [ ] **`Kbd` inside a `Button` uses `variant="bare"`** (glue chords into one bare kbd); **`Kbd` inside `TooltipContent` uses the default tile** — see §11 Keyboard shortcuts
702
+ - [ ] `DialogTitle`/`SheetTitle`/`DrawerTitle` present on every overlay
703
+ - [ ] `role="tablist"` contains only tab-role children
704
+ - [ ] No new shadcn components, no hardcoded colors, no duplicate component abstractions
705
+
706
+ ---
707
+
708
+ ## Reference Files
709
+
710
+ - `references/accessibility.md` — Full WCAG 2.1 AA checklist (interactive elements, keyboard, forms, semantics, contrast, dynamic content, component-specific rules)
711
+ - `references/data-table-pattern.md` — Complete data table implementation guide with full column/filter/drawer wiring
712
+
713
+ Read the relevant reference file when implementing the corresponding feature.