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