@exxatdesignux/ui 0.3.0 → 0.4.1

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 (214) hide show
  1. package/CHANGELOG.md +701 -6
  2. package/README.md +138 -0
  3. package/bin/init.mjs +134 -31
  4. package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
  5. package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
  6. package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
  7. package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
  8. package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
  9. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
  10. package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
  11. package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
  12. package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
  13. package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
  14. package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
  15. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
  16. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
  17. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
  18. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
  19. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
  20. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
  21. package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
  22. package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
  23. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
  24. package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
  25. package/consumer-extras/handbook/HANDBOOK.md +2 -0
  26. package/consumer-extras/handbook/glossary.md +2 -1
  27. package/consumer-extras/handbook/reference-implementations.md +31 -4
  28. package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
  29. package/consumer-extras/patterns/data-views-pattern.md +18 -16
  30. package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
  31. package/dist/components/data-table/index.js +2 -2
  32. package/dist/components/data-table/index.js.map +1 -1
  33. package/dist/components/data-table/pagination.js +3 -3
  34. package/dist/components/data-table/pagination.js.map +1 -1
  35. package/dist/components/data-table/use-table-state.d.ts +1 -1
  36. package/dist/components/data-table/use-table-state.js.map +1 -1
  37. package/dist/components/data-views/data-row-list.js.map +1 -1
  38. package/dist/components/data-views/finder-panel-view.d.ts +1 -1
  39. package/dist/components/data-views/finder-panel-view.js.map +1 -1
  40. package/dist/components/data-views/hub-table.d.ts +9 -3
  41. package/dist/components/data-views/hub-table.js +262 -40
  42. package/dist/components/data-views/hub-table.js.map +1 -1
  43. package/dist/components/data-views/index.js +262 -40
  44. package/dist/components/data-views/index.js.map +1 -1
  45. package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
  46. package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
  47. package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
  48. package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
  49. package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
  50. package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
  51. package/dist/components/data-views/os-folder-glyph.js.map +1 -1
  52. package/dist/components/ui/avatar.d.ts +1 -1
  53. package/dist/components/ui/key-metrics.js.map +1 -1
  54. package/dist/index.js +136 -39
  55. package/dist/index.js.map +1 -1
  56. package/package.json +3 -2
  57. package/src/components/data-table/index.tsx +2 -2
  58. package/src/components/data-table/pagination.tsx +5 -1
  59. package/src/components/data-table/use-table-state.ts +1 -1
  60. package/src/components/data-views/data-row-list.tsx +1 -1
  61. package/src/components/data-views/finder-panel-view.tsx +2 -2
  62. package/src/components/data-views/hub-table.tsx +149 -41
  63. package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
  64. package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
  65. package/src/components/data-views/os-folder-glyph.tsx +1 -1
  66. package/src/components/ui/key-metrics.tsx +1 -1
  67. package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
  68. package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
  69. package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
  70. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
  71. package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
  72. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
  73. package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
  74. package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
  75. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
  76. package/template/AGENTS.md +43 -37
  77. package/template/app/(app)/columns/page.tsx +11 -0
  78. package/template/app/(app)/library/all/page.tsx +11 -0
  79. package/template/app/(app)/library/find/page.tsx +12 -0
  80. package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
  81. package/template/app/(app)/library/list/page.tsx +12 -0
  82. package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
  83. package/template/app/(app)/library/page.tsx +11 -0
  84. package/template/app/(app)/tokens-themes/page.tsx +11 -0
  85. package/template/components/ask-leo-composer.tsx +2 -2
  86. package/template/components/columns-client.tsx +158 -0
  87. package/template/components/columns-showcase.tsx +541 -0
  88. package/template/components/data-views/index.ts +32 -6
  89. package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
  90. package/template/components/data-views/table-cells.tsx +673 -0
  91. package/template/components/folder-details-shell.tsx +11 -11
  92. package/template/components/hub-tree-panel-view.tsx +24 -24
  93. package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
  94. package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
  95. package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
  96. package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
  97. package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
  98. package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
  99. package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
  100. package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
  101. package/template/components/library-panel-activator.tsx +8 -0
  102. package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
  103. package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
  104. package/template/components/list-hub-status-badge.tsx +2 -2
  105. package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
  106. package/template/components/sidebar/app-sidebar.tsx +61 -5
  107. package/template/components/sidebar/secondary-panel.tsx +109 -56
  108. package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
  109. package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
  110. package/template/components/table-properties/types.ts +1 -1
  111. package/template/components/templates/discovery-hub-template.tsx +1 -1
  112. package/template/components/templates/new-focus-template.tsx +2 -2
  113. package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
  114. package/template/components/tokens-secondary-nav.tsx +192 -0
  115. package/template/components/tokens-themes-client.tsx +476 -0
  116. package/template/components/tokens-themes-section.tsx +386 -0
  117. package/template/docs/HANDBOOK.md +187 -0
  118. package/template/docs/blueprints/README.md +1 -1
  119. package/template/docs/blueprints/board-card.md +1 -1
  120. package/template/docs/blueprints/data-table.md +2 -2
  121. package/template/docs/blueprints/list-page-template.md +3 -3
  122. package/template/docs/blueprints/page-header.md +4 -4
  123. package/template/docs/collaboration-access-pattern.md +7 -7
  124. package/template/docs/component-selection-guide.md +1 -1
  125. package/template/docs/data-views-pattern.md +18 -16
  126. package/template/docs/glossary.md +58 -0
  127. package/template/docs/kpi-flat-band-pattern.md +3 -3
  128. package/template/docs/kpi-trend-pattern.md +18 -3
  129. package/template/docs/large-dataset-strategy.md +155 -0
  130. package/template/docs/library-hub-header-pattern.md +25 -0
  131. package/template/docs/migrations/_template.md +1 -1
  132. package/template/docs/reference-implementations.md +151 -0
  133. package/template/docs/token-taxonomy.md +1 -1
  134. package/template/docs/voice-and-tone.md +262 -0
  135. package/template/eslint.config.mjs +9 -39
  136. package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
  137. package/template/lib/ask-leo-route-context.ts +6 -18
  138. package/template/lib/coach-mark-registry.ts +0 -16
  139. package/template/lib/command-menu-config.ts +5 -12
  140. package/template/lib/command-menu-search-data.ts +8 -39
  141. package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
  142. package/template/lib/library-dedicated-search.ts +19 -0
  143. package/template/lib/library-hub-search.ts +90 -0
  144. package/template/lib/library-nav.ts +477 -0
  145. package/template/lib/library-recent-searches.ts +22 -0
  146. package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
  147. package/template/lib/list-status-badges.ts +16 -104
  148. package/template/lib/mock/dashboard.ts +1 -1
  149. package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
  150. package/template/lib/mock/library-header-collaborators.ts +54 -0
  151. package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
  152. package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
  153. package/template/lib/mock/library.ts +249 -0
  154. package/template/lib/mock/navigation.tsx +32 -26
  155. package/template/lib/table-state-lifecycle.ts +1 -1
  156. package/template/next.config.mjs +7 -4
  157. package/template/package.json +0 -1
  158. package/tokens/hooks-index.json +2874 -0
  159. package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
  160. package/template/app/(app)/examples/page.tsx +0 -41
  161. package/template/app/(app)/question-bank/find/page.tsx +0 -12
  162. package/template/app/(app)/question-bank/library/page.tsx +0 -11
  163. package/template/app/(app)/question-bank/list/page.tsx +0 -12
  164. package/template/app/(app)/question-bank/page.tsx +0 -11
  165. package/template/components/compliance-board-view.tsx +0 -142
  166. package/template/components/compliance-client.tsx +0 -92
  167. package/template/components/compliance-page-header.tsx +0 -89
  168. package/template/components/compliance-table.tsx +0 -468
  169. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  170. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  171. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  172. package/template/components/new-placement-back-btn.tsx +0 -28
  173. package/template/components/new-placement-form.tsx +0 -942
  174. package/template/components/placement-board-card.tsx +0 -250
  175. package/template/components/placement-detail.tsx +0 -438
  176. package/template/components/placements-board-view.tsx +0 -397
  177. package/template/components/placements-client.tsx +0 -220
  178. package/template/components/placements-list-view.tsx +0 -124
  179. package/template/components/placements-page-header.tsx +0 -166
  180. package/template/components/placements-table-cells.test.tsx +0 -22
  181. package/template/components/placements-table-cells.tsx +0 -173
  182. package/template/components/placements-table-columns.tsx +0 -210
  183. package/template/components/placements-table.tsx +0 -934
  184. package/template/components/question-bank-panel-activator.tsx +0 -8
  185. package/template/components/rotations-empty-state.tsx +0 -50
  186. package/template/components/rotations-panel-activator.tsx +0 -8
  187. package/template/components/sites-board-view.tsx +0 -67
  188. package/template/components/sites-client.tsx +0 -154
  189. package/template/components/sites-table.tsx +0 -249
  190. package/template/components/team-board-view.tsx +0 -122
  191. package/template/components/team-client.tsx +0 -100
  192. package/template/components/team-page-header.tsx +0 -92
  193. package/template/components/team-table.tsx +0 -553
  194. package/template/docs/question-bank-hub-header-pattern.md +0 -25
  195. package/template/lib/compliance-supported-views.ts +0 -10
  196. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  197. package/template/lib/mock/compliance-kpi.ts +0 -61
  198. package/template/lib/mock/compliance.ts +0 -146
  199. package/template/lib/mock/placements-kpi.ts +0 -134
  200. package/template/lib/mock/placements.ts +0 -176
  201. package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
  202. package/template/lib/mock/question-bank.ts +0 -249
  203. package/template/lib/mock/sites-directory.ts +0 -16
  204. package/template/lib/mock/sites-kpi.ts +0 -25
  205. package/template/lib/mock/team-kpi.ts +0 -60
  206. package/template/lib/mock/team.ts +0 -118
  207. package/template/lib/placement-board-card-layout.ts +0 -79
  208. package/template/lib/question-bank-dedicated-search.ts +0 -19
  209. package/template/lib/question-bank-hub-search.ts +0 -90
  210. package/template/lib/question-bank-nav.ts +0 -477
  211. package/template/lib/question-bank-recent-searches.ts +0 -22
  212. package/template/lib/question-bank-supported-views.ts +0 -12
  213. package/template/lib/sites-supported-views.ts +0 -10
  214. package/template/lib/team-supported-views.ts +0 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exxatdesignux/ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Exxat shared design system (components, hooks, tokens). Monorepo setup: clone repo then pnpm bootstrap at workspace root — see github.com/ExxatDesign/Exxat-DS-Workspace README.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "Exxat Design",
@@ -98,7 +98,8 @@
98
98
  "src",
99
99
  "bin",
100
100
  "template",
101
- "consumer-extras"
101
+ "consumer-extras",
102
+ "tokens"
102
103
  ],
103
104
  "dependencies": {
104
105
  "@hookform/resolvers": "^5.2.2",
@@ -1100,8 +1100,8 @@ function DataTableInner<TData extends Record<string, unknown>>({
1100
1100
  setHeaderScrollLeft((e.currentTarget as HTMLDivElement).scrollLeft)
1101
1101
  }}
1102
1102
  className={cn(
1103
- "mx-4 lg:mx-6 overflow-x-auto border border-border",
1104
- hasFooter ? "rounded-t-lg" : "rounded-lg",
1103
+ "mx-4 lg:mx-6 overflow-x-auto border-t border-x border-border",
1104
+ hasFooter ? "rounded-t-lg" : "border-b rounded-lg",
1105
1105
  )}
1106
1106
  >
1107
1107
  <table
@@ -215,7 +215,11 @@ export function DataTablePaginated<TData extends Record<string, unknown>>({
215
215
  paginationOverride={{ page: safePage, pageSize }}
216
216
  hasFooter
217
217
  />
218
- <div className="mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden">
218
+ {/* z-40 sits above pinned cells (z-20), group headers (z-25), and pinned headers
219
+ (z-40) so the pagination chrome stays opaque over any table content that
220
+ scrolls behind it. Pinned-left cells ship with their own `bg-dt-row-bg`,
221
+ which would otherwise paint over a lower-z footer. */}
222
+ <div className="sticky bottom-0 z-40 mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden bg-background">
219
223
  <PaginationBar
220
224
  page={safePage}
221
225
  pageSize={pageSize}
@@ -93,7 +93,7 @@ export function useTableState<TData extends Record<string, unknown>>(
93
93
  paginationOverride?: { page: number; pageSize: number },
94
94
  /**
95
95
  * When defined (including `""`), toolbar search is synced from the URL (`?q=`).
96
- * Use `searchParams.get("q") ?? ""` on question bank list routes; omit for other hubs.
96
+ * Use `searchParams.get("q") ?? ""` on library list routes; omit for other hubs.
97
97
  */
98
98
  syncedSearchFromUrl?: string,
99
99
  ) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * DataRowList — generic vertical-stack list view used by every hub's "list"
5
- * tab (placements, team, compliance, sites, question-bank, …). Replaces the
5
+ * tab (placements, team, compliance, sites, library, …). Replaces the
6
6
  * hand-rolled `<ul …flex-col gap-2 px-4 pb-8 pt-2 lg:px-6> {rows.map(<li>…)}`
7
7
  * shell that was duplicated across `*-list-view.tsx` files.
8
8
  *
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * FinderPanelView — Miller-style 3-column split for list-page hubs.
5
5
  *
6
- * Visual shell matches Question bank panel (`ListPageTreeColumnHeader`, `LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS`,
6
+ * Visual shell matches Library panel (`ListPageTreeColumnHeader`, `LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS`,
7
7
  * shared resizable handles) — see `list-page-split-hub-tokens.ts`.
8
8
  */
9
9
 
@@ -50,7 +50,7 @@ export interface FinderPanelViewProps<T> {
50
50
  * `ListPageSplitHubChrome` (shared split surface across hubs).
51
51
  */
52
52
  embedded?: boolean
53
- /** Left column title (Question bank: “Categories”). */
53
+ /** Left column title (Library: “Categories”). */
54
54
  groupsColumnTitle?: string
55
55
  /** Middle column title; defaults from the active group label. */
56
56
  getListColumnTitle?: (activeGroup: FinderGroup | undefined) => string
@@ -4,7 +4,7 @@
4
4
  * `<HubTable<TRow>>` — single centralized table surface used by every list-page hub.
5
5
  *
6
6
  * Owns all the per-hub scaffolding that was previously duplicated in `placements-table.tsx`,
7
- * `team-table.tsx`, `compliance-table.tsx`, `question-bank-table.tsx`, and `sites-table.tsx`:
7
+ * `team-table.tsx`, `compliance-table.tsx`, `library-table.tsx`, and `sites-table.tsx`:
8
8
  *
9
9
  * • `useTableState` setup tied to the centralized row dataset
10
10
  * • `displayOptions` state + `patchDisplay`
@@ -27,7 +27,9 @@
27
27
  */
28
28
 
29
29
  import * as React from "react"
30
+ import { cn } from "../../lib/utils"
30
31
  import { DataTable, DataTableToolbar } from "../data-table"
32
+ import { CountSyncer, PaginationBar } from "../data-table/pagination"
31
33
  import type { ColumnDef } from "../data-table/types"
32
34
  import { useTableState } from "../data-table/use-table-state"
33
35
  import type { DataListViewType } from "../../lib/data-list-view"
@@ -204,11 +206,17 @@ export interface HubTableProps<TRow extends Record<string, unknown>> {
204
206
  tableRenderer?: (args: HubTableRendererArgs<TRow>) => React.ReactNode
205
207
  /**
206
208
  * Forwarded to `useTableState` so the hub can switch on server-style pagination
207
- * (e.g. Placements toggles between paged and unpaged tables).
209
+ * with externally-controlled page/pageSize (advanced; most hubs should leave
210
+ * this undefined and let `HubTable` own the internal page state — see
211
+ * `pagination` + `paginationPageSizeOptions`).
208
212
  */
209
213
  paginationOverride?: { page: number; pageSize: number }
214
+ /** Page size options shown in the toolbar `<PaginationBar>`. Default `[10, 25, 50, 100]`. */
215
+ paginationPageSizeOptions?: number[]
216
+ /** Initial page size when `HubTable` owns pagination internally. Default `10`. */
217
+ paginationInitialPageSize?: number
210
218
  /**
211
- * Forwarded to `useTableState` to sync toolbar search from `?q=` (Question bank search routes).
219
+ * Forwarded to `useTableState` to sync toolbar search from `?q=` (Library search routes).
212
220
  * Defining as `""` enables sync without an initial query.
213
221
  */
214
222
  syncedSearchFromUrl?: string
@@ -273,6 +281,8 @@ export function HubTable<TRow extends Record<string, unknown>>({
273
281
  handleRef,
274
282
  tableRenderer,
275
283
  paginationOverride,
284
+ paginationPageSizeOptions = [10, 25, 50, 100],
285
+ paginationInitialPageSize = 10,
276
286
  syncedSearchFromUrl,
277
287
  renderListRow,
278
288
  listAriaLabel,
@@ -324,14 +334,33 @@ export function HubTable<TRow extends Record<string, unknown>>({
324
334
  [],
325
335
  )
326
336
 
337
+ // ─── Pagination chrome (centralized) ─────────────────────────────────────
338
+ // When `pagination === true` and the parent did NOT supply `paginationOverride`,
339
+ // `HubTable` owns the page/pageSize internally and wraps the default table +
340
+ // list renderers with `<CountSyncer>` + `<PaginationBar>`. Hubs that need full
341
+ // control (e.g. server-side pagination) keep using `paginationOverride`.
342
+ const [internalPage, setInternalPage] = React.useState(1)
343
+ const [internalPageSize, setInternalPageSize] = React.useState(paginationInitialPageSize)
344
+ const chromeOwnedPagination = pagination === true && paginationOverride === undefined
345
+ const effectivePaginationOverride =
346
+ paginationOverride ??
347
+ (chromeOwnedPagination ? { page: internalPage, pageSize: internalPageSize } : undefined)
348
+
327
349
  const tableState = useTableState<TRow>(
328
350
  rows,
329
351
  columns,
330
352
  defaultSort,
331
- paginationOverride,
353
+ effectivePaginationOverride,
332
354
  syncedSearchFromUrl,
333
355
  )
334
356
 
357
+ const handlePageChange = React.useCallback((p: number) => setInternalPage(p), [])
358
+ const handlePageSizeChange = React.useCallback((n: number) => {
359
+ setInternalPageSize(n)
360
+ setInternalPage(1)
361
+ }, [])
362
+ const resetPage = React.useCallback(() => setInternalPage(1), [])
363
+
335
364
  // Extract the stable setter from `useTableState` first so the
336
365
  // `useImperativeHandle` deps array sees the exact value the hook reads.
337
366
  // `setSheetOpen` is referentially stable, so the handle is created once.
@@ -395,31 +424,70 @@ export function HubTable<TRow extends Record<string, unknown>>({
395
424
  ],
396
425
  )
397
426
 
398
- // Default `data-table` renderer — full DataTable with toolbar + bulk actions. Hubs can
399
- // override via `tableRenderer` for pagination chrome (CountSyncer + PaginationBar) or
400
- // any other table-view-specific wrapping.
401
- const defaultTableRenderer = (args: HubTableRendererArgs<TRow>) => (
402
- <div className="pb-6">
403
- <DataTable<TRow>
404
- data={rows}
405
- columns={columns}
406
- getRowId={getRowId}
407
- getRowSelectionLabel={getRowSelectionLabel}
408
- selectable={selectable}
409
- searchable={displayOptions.showToolbarSearch}
410
- showColumnHeaders={displayOptions.showColumnLabels}
411
- groupable={groupable}
412
- defaultSort={defaultSort}
413
- emptyState={emptyState ?? <p className="text-sm text-muted-foreground">No records match your filters.</p>}
414
- conditionalRules={conditionalRules}
415
- state={args.state}
416
- renderFilterOptionValue={renderFilterOptionValue}
417
- toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
418
- bulkActionsSlot={bulkActionsSlot}
419
- onRowClick={onRowClick}
420
- />
421
- </div>
422
- )
427
+ // Default `data-table` renderer — full DataTable with toolbar + bulk actions. When
428
+ // `pagination === true` and the parent did not provide `paginationOverride`, the chrome
429
+ // (CountSyncer + PaginationBar) is wrapped automatically so the drawer toggle "Show
430
+ // pagination" works out of the box. Hubs that need finer control (custom chrome,
431
+ // server-side paging) can still override via `tableRenderer`.
432
+ const defaultTableRenderer = (args: HubTableRendererArgs<TRow>) => {
433
+ const filteredCount = (args.state.rows as TRow[]).length
434
+ const totalPages = Math.max(1, Math.ceil(filteredCount / Math.max(1, internalPageSize)))
435
+ const safePage = Math.min(internalPage, totalPages)
436
+ return (
437
+ <div className="pb-6">
438
+ {chromeOwnedPagination ? (
439
+ <CountSyncer
440
+ count={filteredCount}
441
+ onSync={(n) => {
442
+ const next = Math.max(1, Math.ceil(n / Math.max(1, internalPageSize)))
443
+ if (safePage > next) setInternalPage(next)
444
+ }}
445
+ onReset={resetPage}
446
+ />
447
+ ) : null}
448
+ <DataTable<TRow>
449
+ data={rows}
450
+ columns={columns}
451
+ getRowId={getRowId}
452
+ getRowSelectionLabel={getRowSelectionLabel}
453
+ selectable={selectable}
454
+ searchable={displayOptions.showToolbarSearch}
455
+ showColumnHeaders={displayOptions.showColumnLabels}
456
+ groupable={groupable}
457
+ defaultSort={defaultSort}
458
+ emptyState={emptyState ?? <p className="text-sm text-muted-foreground">No records match your filters.</p>}
459
+ conditionalRules={conditionalRules}
460
+ state={args.state}
461
+ renderFilterOptionValue={renderFilterOptionValue}
462
+ toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
463
+ bulkActionsSlot={bulkActionsSlot}
464
+ onRowClick={onRowClick}
465
+ hasFooter={chromeOwnedPagination}
466
+ />
467
+ {chromeOwnedPagination ? (
468
+ <div
469
+ className={cn(
470
+ "mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden bg-background",
471
+ // z-40 sits above pinned cells (z-20), group headers (z-25), and column headers
472
+ // (z-30 / z-40) so the sticky footer paints over any table content that scrolls
473
+ // behind it. Pinned-left cells ship with their own `bg-dt-row-bg`, which
474
+ // otherwise wins because of z-20 > z-10.
475
+ "sticky bottom-0 z-40",
476
+ )}
477
+ >
478
+ <PaginationBar
479
+ page={safePage}
480
+ pageSize={internalPageSize}
481
+ total={filteredCount}
482
+ pageSizeOptions={paginationPageSizeOptions}
483
+ onPageChange={handlePageChange}
484
+ onPageSizeChange={handlePageSizeChange}
485
+ />
486
+ </div>
487
+ ) : null}
488
+ </div>
489
+ )
490
+ }
423
491
 
424
492
  const args: HubTableRendererArgs<TRow> = {
425
493
  state: tableState,
@@ -455,20 +523,60 @@ export function HubTable<TRow extends Record<string, unknown>>({
455
523
  }
456
524
 
457
525
  // Default centralized list renderer: same DataRowList shell every hub used to roll
458
- // by hand. Hub provides only the per-row body via `renderListRow`.
526
+ // by hand. Hub provides only the per-row body via `renderListRow`. When `pagination`
527
+ // is on (and `paginationOverride` is not externally supplied), the list view reads
528
+ // `state.pagedRows` and adds the same `CountSyncer` + `PaginationBar` chrome as the
529
+ // table view.
459
530
  if (renderers["list-with-toolbar"] == null && renderListRow != null) {
460
- composed["list-with-toolbar"] = () =>
461
- args.toolbarShell(
462
- <DataRowList<TRow>
463
- rows={args.state.rows as TRow[]}
464
- getRowId={row => getRowId(row)}
465
- ariaLabel={listAriaLabel ?? hubLabel}
466
- emptyState={listEmptyState ?? "No records match your filters."}
467
- {...(listVirtualizeThreshold !== undefined ? { virtualizeThreshold: listVirtualizeThreshold } : {})}
468
- {...(listEstimatedRowHeight !== undefined ? { estimatedRowHeight: listEstimatedRowHeight } : {})}
469
- renderRow={renderListRow}
470
- />,
531
+ composed["list-with-toolbar"] = () => {
532
+ const fullRows = args.state.rows as TRow[]
533
+ const pagedRows = chromeOwnedPagination ? (args.state.pagedRows as TRow[]) : fullRows
534
+ const filteredCount = fullRows.length
535
+ const totalPages = Math.max(1, Math.ceil(filteredCount / Math.max(1, internalPageSize)))
536
+ const safePage = Math.min(internalPage, totalPages)
537
+ return args.toolbarShell(
538
+ <>
539
+ {chromeOwnedPagination ? (
540
+ <CountSyncer
541
+ count={filteredCount}
542
+ onSync={(n) => {
543
+ const next = Math.max(1, Math.ceil(n / Math.max(1, internalPageSize)))
544
+ if (safePage > next) setInternalPage(next)
545
+ }}
546
+ onReset={resetPage}
547
+ />
548
+ ) : null}
549
+ <DataRowList<TRow>
550
+ rows={pagedRows}
551
+ getRowId={row => getRowId(row)}
552
+ ariaLabel={listAriaLabel ?? hubLabel}
553
+ emptyState={listEmptyState ?? "No records match your filters."}
554
+ {...(listVirtualizeThreshold !== undefined ? { virtualizeThreshold: listVirtualizeThreshold } : {})}
555
+ {...(listEstimatedRowHeight !== undefined ? { estimatedRowHeight: listEstimatedRowHeight } : {})}
556
+ renderRow={renderListRow}
557
+ />
558
+ {chromeOwnedPagination ? (
559
+ <div
560
+ className={cn(
561
+ "mx-4 lg:mx-6 border-x border-b border-border rounded-b-lg overflow-hidden bg-background",
562
+ // Match the table-view footer — above pinned cells (z-20) and column
563
+ // headers (z-30 / z-40) so the sticky chrome paints over scrolling rows.
564
+ "sticky bottom-0 z-40",
565
+ )}
566
+ >
567
+ <PaginationBar
568
+ page={safePage}
569
+ pageSize={internalPageSize}
570
+ total={filteredCount}
571
+ pageSizeOptions={paginationPageSizeOptions}
572
+ onPageChange={handlePageChange}
573
+ onPageSizeChange={handlePageSizeChange}
574
+ />
575
+ </div>
576
+ ) : null}
577
+ </>,
471
578
  )
579
+ }
472
580
  }
473
581
 
474
582
  // Default centralized board renderer: same ListPageBoardTemplate every hub used to wrap.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Shared layout tokens for list-hub split surfaces (Miller columns, tree + details).
3
- * Keeps Question bank panel / tree and generic `FinderPanelView` visually aligned.
3
+ * Keeps Library panel / tree and generic `FinderPanelView` visually aligned.
4
4
  */
5
5
 
6
- /** `ResizableHandle` between miller / tree columns — matches Question bank panel. */
6
+ /** `ResizableHandle` between miller / tree columns — matches Library panel. */
7
7
  export const LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS =
8
8
  "w-1 bg-border/40 hover:bg-brand/20 transition-colors"
9
9
 
@@ -11,7 +11,7 @@ export interface ListPageTreeColumnHeaderProps {
11
11
  }
12
12
 
13
13
  /**
14
- * Shared left-column header for tree / outline surfaces — matches Question bank “Questions” bar.
14
+ * Shared left-column header for tree / outline surfaces — matches Library “Questions” bar.
15
15
  */
16
16
  export function ListPageTreeColumnHeader({
17
17
  title,
@@ -10,7 +10,7 @@ import { cn } from "../../lib/utils"
10
10
 
11
11
  /**
12
12
  * Color palette tones shared across folder hubs. Domain-specific palette
13
- * names (e.g. `QuestionBankFolderColorKey`) are structurally identical to
13
+ * names (e.g. `LibraryFolderColorKey`) are structurally identical to
14
14
  * this union and pass through without conversion.
15
15
  */
16
16
  export type FolderGlyphColorKey =
@@ -301,7 +301,7 @@ function metricsRowColumnsClass(rowLength: number, metricsHalfWidthLayout: boole
301
301
  // Step 1 → 2 (2×2 grid) → 4. Skip 3 — that's the awkward 3+1 layout.
302
302
  // Aggressive 4-col thresholds so the strip fits all four tiles even
303
303
  // when the primary sidebar + secondary panel + insight rail are all
304
- // expanded (typical question-bank layout puts the KPI grid at ~27rem).
304
+ // expanded (typical library layout puts the KPI grid at ~27rem).
305
305
  return half
306
306
  ? "grid-cols-1 @[14rem]:grid-cols-2 @[26rem]:grid-cols-4"
307
307
  : "grid-cols-1 @[18rem]:grid-cols-2 @[30rem]:grid-cols-4"
@@ -112,17 +112,18 @@ ListPageTemplate
112
112
  ```
113
113
 
114
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)
115
+ - `components/columns-showcase.tsx` + `components/columns-client.tsx` — smallest single-view hub (start here)
116
+ - `components/tokens-themes-client.tsx` + `components/tokens-secondary-nav.tsx` — hub with secondary panel + URL-driven scope + built-in pagination chrome
117
+ - `components/library-table.tsx` + `components/library-hub-client.tsx` — full multi-view hub (table, board, dashboard)
117
118
 
118
119
  **Files to create for a new hub page `Foo`:**
119
120
  | File | Purpose |
120
121
  |------|---------|
121
122
  | `lib/mock/foo.ts` | Mock data + TypeScript interface (12+ rows) |
122
- | `lib/mock/foo-kpi.ts` | `fooKpiMetrics()` + `fooKpiInsight()` |
123
+ | `lib/mock/foo-kpi.ts` | `fooKpiMetrics()` (≤ 4 `MetricItem`s, each with `onClick` or `href`) + `fooKpiInsight()` returning a single `MetricInsight` |
123
124
  | `components/foo-page-header.tsx` | `PageHeader` + primary CTA + ⋯ menu |
124
- | `components/foo-table.tsx` | `DataTable` + `useTableState` + `TablePropertiesDrawer` |
125
- | `components/foo-client.tsx` | `ListPageTemplate` orchestrator |
125
+ | `components/foo-showcase.tsx` *or* `components/foo-table.tsx` | `HubTable` (NOT raw `DataTable`) pass `pagination` / `onPaginationChange` if the hub needs pagination chrome |
126
+ | `components/foo-client.tsx` | `ListPageTemplate` orchestrator, mounts `KeyMetrics` with `items` + `insight` |
126
127
  | `app/(app)/foo/page.tsx` | Thin server component |
127
128
 
128
129
  **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.
@@ -148,7 +149,7 @@ Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`
148
149
  | `ColumnDef` from `@/components/data-table/types` | Column type |
149
150
  | `FilterFieldDef`, `FilterOperator`, `ConditionalRule` from `@/components/table-properties/types` | Filter types |
150
151
 
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
+ **Board (kanban) cards:** Use **`ListPageBoardCard`** and related parts from **`components/data-views/list-page-board-card.tsx`**; **`BoardCardTwoLineBlock`** / **`BoardCardIconRow`** from **`board-card-primitives.tsx`**. **List hub** status (Team, Compliance, Library, …): maps in **`lib/list-status-badges.ts`**; render with **`ListHubStatusBadge`** (**`surface="table"`** in table/list, **`surface="board"`** on cards); semantic tints **`LIST_HUB_STATUS_TINT_*`** for new domains; no **`uppercase`**. **Placements** uses **`StatusBadge`** in **`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
 
153
154
  **Minimum required features on any data list page:**
154
155
  - Search (wire `searchable={displayOptions.showToolbarSearch}`)
@@ -245,7 +246,7 @@ Use `PageHeader` from `@/components/page-header` for the content-area header (be
245
246
 
246
247
  When a hub is **shared**, use **`PageHeader` `variant="collaboration"`**: **empty roster** → outline **Add collaborator**; **non-empty** → face rail (faces / **`+N`** open the invite sheet). **Invite people** also lives under the entity header **⋯ More** and opens **`InviteCollaboratorsDrawer`** via **`CollaborationAccessFlow`** when possible. Library access (Owner / Editor / Commenter / Viewer) comes from **`lib/collaborator-access.ts`**; directory tags (Faculty, Program coordinator, Director) use **`PageHeaderCollaborator.roles`**.
247
248
 
248
- **Handbook:** `apps/web/AGENTS.md` §4.7 · **Doc:** `docs/collaboration-access-pattern.md` · **Skill:** `.cursor/skills/exxat-collaboration-access/SKILL.md` · **Reference:** Question bank header + client.
249
+ **Handbook:** `apps/web/AGENTS.md` §4.7 · **Doc:** `docs/collaboration-access-pattern.md` · **Skill:** `.cursor/skills/exxat-collaboration-access/SKILL.md` · **Reference:** Library header + client.
249
250
 
250
251
  ---
251
252
 
@@ -26,7 +26,7 @@ alwaysApply: true
26
26
  - **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`**. `aria-label` alone is not enough — sighted users rely on the tooltip too.
27
27
 
28
28
  In all cases the inner `<i>` / `<svg>` is `aria-hidden`; tooltip text matches the accessible name. Narrow exception: chevron inside a labelled composite (`Select`, `Combobox`). See **`AGENTS.md` §8.6 (Case A/B/C)**.
29
- 10. **Keyboard shortcut hints inside buttons** MUST use **`<Kbd variant="bare">`** (no background/border, inherits `currentColor` at 70%). The default `tile` variant is for **tooltips** and **menu `shortcut=` slots** only. Glue multi-key chords into one bare kbd (e.g. `<Kbd variant="bare">⌘⌥K</Kbd>`). Reference: Next/Back in `new-placement-form.tsx`; see **`.cursor/rules/exxat-kbd-shortcuts.mdc`**.
29
+ 10. **Keyboard shortcut hints inside buttons** MUST use **`<Kbd variant="bare">`** (no background/border, inherits `currentColor` at 70%). The default `tile` variant is for **tooltips** and **menu `shortcut=` slots** only. Glue multi-key chords into one bare kbd (e.g. `<Kbd variant="bare">⌘⌥K</Kbd>`). Reference: Next/Back in `new-library-item-form.tsx`; see **`.cursor/rules/exxat-kbd-shortcuts.mdc`**.
30
30
 
31
31
  Re-run **axe** on **Placements** (or affected page) after changing **views toolbar** or **tabs**.
32
32
 
@@ -7,7 +7,7 @@ alwaysApply: true
7
7
 
8
8
  ## Intent
9
9
 
10
- - **`CommandMenu`** (**⌘K** / **Ctrl+K**) is **global search** (routes, library, patterns, AI starters, optional row data such as placements / student names)—see **`AGENTS.md` §7.1** and **`docs/command-menu-pattern.md`**.
10
+ - **`CommandMenu`** (**⌘K** / **Ctrl+K**) is **global search** (routes, library, patterns, AI starters, optional row data such as student names / question stems) see **`AGENTS.md` §7.1** and **`docs/command-menu-pattern.md`**.
11
11
  - **Quick / lookup / short AI:** Prefer **results inside the palette** when the product can return compact answers or lightweight “research” without leaving the flow.
12
12
  - **Long or complex answers:** **Ask Leo** side panel (**⌘⌥K** / **Ctrl+Alt+K**)—not forced into the palette.
13
13
 
@@ -12,7 +12,7 @@ alwaysApply: true
12
12
  | Surface | Entry | Shared building blocks |
13
13
  |--------|--------|-------------------------|
14
14
  | **Full-page dashboard** | `app/(app)/dashboard/page.tsx` | `DashboardTabs`, `ChartsOverview` / gallery demos in `charts-overview.tsx` |
15
- | **Data tab** on Placements / Team / Compliance | `PlacementsTable`, `TeamTable`, `ComplianceTable` `*DashboardChartsSection` | `ChartFigure`, `ChartCard`, `useChartVariant()`, `data-view-dashboard-charts*.tsx` |
15
+ | **Data tab** on a hub | `library-table.tsx` `LibraryDashboardChartsSection` (reference) | `ChartFigure`, `ChartCard`, `useChartVariant()`, the hub's own `*-dashboard-charts.tsx` module |
16
16
 
17
17
  **MUST NOT** duplicate “another” chart system for Data view — extend **`charts-overview`** patterns and **`lib/chart-keyboard-selection`**.
18
18
 
@@ -36,8 +36,8 @@ alwaysApply: true
36
36
 
37
37
  ## MUST — persistence (centralized)
38
38
 
39
- - **One bundle:** **`lib/data-view-dashboard-storage.ts`** — key **`exxat-ds:data-view-dashboards:v1`**, scopes **`placements` | `team` | `compliance`**.
40
- - Placements helpers: **`loadDashboardLayout`** / **`saveDashboardLayout`** in **`data-view-dashboard-charts.tsx`** (wrap storage API).
39
+ - **One bundle:** **`lib/data-view-dashboard-storage.ts`** — key **`exxat-ds:data-view-dashboards:v1`**. Each hub registers under a scope string (e.g. **`library`**); add new scopes there, never invent a sibling `localStorage` key.
40
+ - Hub-side: pair **`loadDataViewLayout`** + **`saveDataViewLayout`** with **`mergeDashboardLayoutGeneric`** (`lib/dashboard-layout-merge.ts`) for default-layout safety. Reference: the Library dashboard wiring inside **`library-table.tsx`** + **`library-dashboard-charts.tsx`**.
41
41
  - **MUST NOT** introduce parallel **`localStorage`** keys for the same **`DashboardLayout`** shape without updating the storage module.
42
42
 
43
43
  ## SHOULD — coach marks
@@ -47,7 +47,7 @@ alwaysApply: true
47
47
 
48
48
  ## Reference files
49
49
 
50
- - `components/data-view-dashboard-charts.tsx` (Placements)
51
- - `components/data-view-dashboard-charts-team.tsx`, `data-view-dashboard-charts-compliance.tsx`
52
- - `components/placements-table.tsx` — dashboard tab body wires `PlacementsDashboardChartsSection` + layout-edit toolbar inline (no separate `DashboardShell` component)
50
+ - `components/library-dashboard-charts.tsx` — canonical Data-tab dashboard section (`LibraryDashboardChartsSection`) — chart cards, `MetricsCard`, layout-edit toolbar.
51
+ - `components/library-table.tsx` — dashboard tab body wires `LibraryDashboardChartsSection` + layout-edit toolbar inline (no separate `DashboardShell` component).
52
+ - `components/charts-overview.tsx` — full-page dashboard chart gallery referenced from `app/(app)/dashboard/page.tsx`.
53
53
  - `lib/chart-keyboard-selection.ts`, `lib/data-view-dashboard-storage.ts`, `lib/dashboard-customize-coach-mark.ts`
@@ -7,16 +7,16 @@ alwaysApply: true
7
7
 
8
8
  ## Use one stack for product data lists
9
9
 
10
- For **any app screen that shows a browsable, filterable grid of records** (lists, directories, placements, etc.):
10
+ For **any app screen that shows a browsable, filterable grid of records** (directories, tokens, columns showcase, question banks, etc.):
11
11
 
12
12
  1. **Base table:** `DataTable` from `@/components/data-table` (optionally wrapped with `DataTablePaginated` when pagination is required).
13
13
  2. **Search:** Wire the table’s search/query UX (toolbar search or equivalent) — do not ship a “bare” table without find-in-list behavior when the page is a data list.
14
14
  3. **Filters:** Use the shared filter model (filter chips / `FilterFieldDef` and operators) consistent with existing list pages — not one-off filter UIs that bypass the table stack.
15
- 4. **Table properties:** Include **Table properties** via `TablePropertiesDrawer` from `@/components/table-properties/drawer` (or the same toolbar + drawer pattern used on reference pages such as placements / data list). Users must be able to adjust columns, density, and related table settings from one place.
15
+ 4. **Table properties:** Include **Table properties** via `TablePropertiesDrawer` from `@/components/table-properties/drawer` (or the same toolbar + drawer pattern used on the reference pages `library-table.tsx`, `columns-showcase.tsx`, `tokens-themes-client.tsx`). Users must be able to adjust columns, density, and related table settings from one place.
16
16
  5. **Active view:** On **`ListPageTemplate`** pages with **table / list / board / dashboard** tabs, **`TablePropertiesDrawer`** **MUST** receive **`currentView`** and **`onViewChange`** (see **`./AGENTS.md` §4.2** and **`.cursor/rules/exxat-table-properties-drawer.mdc`**) so Properties matches the selected view (not table-only copy on Board).
17
17
  6. **Dropdown menus:** **`DropdownMenuContent`** uses the shared **`@exxatdesignux/ui`** default (**intrinsic `w-max`**, **`min-w-52`**, capped **`max-w`**) for view settings, row ⋯, column menus, and filter pickers — **pure CSS**, no **`ResizeObserver`**. Override only for deliberate narrow/wide rails (e.g. pagination **`w-20`**, account trigger-width, school switcher **`!w-max min-w-72 …`**). See **`docs/data-views-pattern.md`** (“Dropdown menus”).
18
18
 
19
- **Reference implementation:** `components/placements-table.tsx` (placements) shows how `DataTable`, filters, and `TablePropertiesDrawer` compose together.
19
+ **Reference implementations:** `components/library-table.tsx` (full hub: table / board / dashboard, conditional rules, bulk actions), `components/columns-showcase.tsx` (catalog hub composing every `table-cells.tsx` primitive — built-in pagination via `HubTable`), and `components/tokens-themes-client.tsx` (table + secondary-panel category rail). Each shows how `HubTable`, filters, and `TablePropertiesDrawer` compose together.
20
20
 
21
21
  ## Do not
22
22
 
@@ -24,7 +24,7 @@ Use `@/components/ui/kbd` (`Kbd` + `KbdGroup`) anywhere users discover actions b
24
24
  | Inside a `DropdownMenuItem` via `shortcut=` | menu handles it — pass the chord string |
25
25
  | Standalone helper text on a surface | **default `tile`** |
26
26
 
27
- Glue multi-key chords into **one** bare kbd (`<Kbd variant="bare">⌘⌥K</Kbd>`), not one tile per key. See `new-placement-form.tsx` (Next = `{mod}⏎`, Back = `{mod}{alt}←`) and the primary "Ask Leo" button inside chart insight popovers.
27
+ Glue multi-key chords into **one** bare kbd (`<Kbd variant="bare">⌘⌥K</Kbd>`), not one tile per key. See `new-library-item-form.tsx` (Next = `{mod}⏎`, Back = `{mod}{alt}←`) and the primary "Ask Leo" button inside chart insight popovers.
28
28
 
29
29
  1. **Pair hint with behavior** — If `Kbd` shows a chord, implement the same shortcut. **Preferred:** the shared primitives from `@/components/ui/dropdown-menu`:
30
30
 
@@ -58,8 +58,8 @@ Use `@/components/ui/kbd` (`Kbd` + `KbdGroup`) anywhere users discover actions b
58
58
  | Toggle main sidebar | ⌘/Ctrl + **B** (`components/ui/sidebar.tsx`) |
59
59
  | Table search | ⌘/Ctrl + **K** (no Alt — `DataTable`) |
60
60
  | Ask Leo | ⌘/Ctrl + **⌥/Alt** + **K** |
61
- | New placement (Placements header) | ⌘/Ctrl + **⌥/Alt** + **N** |
62
- | Placements overflow menu | ⌘/Ctrl + **⌥/Alt** + **M** |
61
+ | New record (primary hub header) | ⌘/Ctrl + **⌥/Alt** + **N** |
62
+ | Hub overflow menu (⋯) | ⌘/Ctrl + **⌥/Alt** + **M** |
63
63
  | Export | ⌘/Ctrl + **⇧/Shift** + **E** |
64
64
  | Hide/Show metric section | ⌘/Ctrl + **⌥/Alt** + **H** |
65
65
  | Rename (view, tab) | **F2** |
@@ -79,14 +79,14 @@ Every **workflow surface** (form, dialog, drawer, sheet, multi-step wizard final
79
79
  1. **Primary action (submit/commit)** — **Enter** (⏎). Render the `<Kbd>⏎</Kbd>` **inline inside the button** (after the label, inside a `<KbdGroup className="ml-1.5">`) — NOT inside a hover `Tip`. Primary/secondary workflow buttons must expose the shortcut at rest so it is discoverable without hovering. Pair with a `<Shortcut keys="Enter" onInvoke={...}>` mounted while the surface is open. The shared `useShortcut` hook skips events from inputs/textarea/contenteditable, so Enter inside a text field still types normally — it only fires when focus is on the surface chrome.
80
80
  2. **Secondary action (Cancel/Dismiss)** — **Esc**. Inline `<Kbd>Esc</Kbd>` inside the Cancel button (same `ml-1.5` pattern). Radix `Dialog` / `Sheet` / `AlertDialog` already bind Esc natively.
81
81
 
82
- > Tip-on-hover Kbd hints remain correct for **page-level** actions (e.g. "New placement", ⋯ overflow triggers) where the button is part of dense page chrome and a persistent Kbd would crowd the layout. Workflow buttons inside a form/drawer/dialog are spacious enough to render the Kbd inline.
82
+ > Tip-on-hover Kbd hints remain correct for **page-level** actions (e.g. primary "New " CTA, ⋯ overflow triggers) where the button is part of dense page chrome and a persistent Kbd would crowd the layout. Workflow buttons inside a form/drawer/dialog are spacious enough to render the Kbd inline.
83
83
 
84
84
  **Variant inside a button:** always use `<Kbd variant="bare">` — no background, no border, inherits `currentColor` at 70% opacity. The default tile variant looks like a pasted-on patch on filled primary buttons. Glue multi-key chords into one `<Kbd variant="bare">⌘⌥←</Kbd>` rather than one tile per key.
85
85
  3. **Multi-step wizards** — plain **Enter** must NOT submit on intermediate steps (it would auto-close the review/final step when users hit Enter inside an input). Either:
86
86
  - Gate `form.onSubmit` on `step === lastStep` (`if (step !== N) { e.preventDefault(); return }`), **or**
87
87
  - Remove `type="submit"` on intermediate Next buttons and bind **⌘Enter** to "Next" via `<Shortcut>`.
88
88
  On the final step, plain **Enter** submits and the Kbd hint shows **⏎**.
89
- 4. Examples in-app: `new-placement-form.tsx` (Create placement = Enter on step 5, Back = ⌘⌥←), `export-drawer.tsx` (Export = Enter, Cancel = Esc).
89
+ 4. Examples in-app: `new-library-item-form.tsx` (Create = Enter on the final step, Back = ⌘⌥←), `export-drawer.tsx` (Export = Enter, Cancel = Esc).
90
90
 
91
91
  ## Every action menu MUST carry shortcuts
92
92
 
@@ -16,7 +16,7 @@ Use this when rendering **system identifiers** — values a user copies, searche
16
16
 
17
17
  ## SHOULD
18
18
 
19
- - Match existing hubs: **`question-bank-table.tsx`**, **`question-bank-list-view.tsx`**, **`new-question-composer.tsx`** (header subtitle), **`sites-table.tsx`** (`row.id`).
19
+ - Match existing hubs: **`library-table.tsx`**, **`columns-showcase.tsx`** (mono record IDs in the showcase row), **`new-library-item-form.tsx`** (header subtitle).
20
20
  - Prefer **`truncate`** / **`min-w-0`** on mono IDs in tight layouts so long tokens do not blow out columns.
21
21
 
22
22
  ## MUST NOT
@@ -13,7 +13,7 @@ alwaysApply: true
13
13
  ## Product examples (this repo)
14
14
 
15
15
  - **Drawer-appropriate:** `TablePropertiesDrawer`, `ExportDrawer`, lightweight panels that supplement a hub.
16
- - **Page-appropriate:** Full placement or settings flows that are the main task, multi-screen wizards.
16
+ - **Page-appropriate:** Full settings flows, multi-step record-creation wizards (e.g. `new-library-item-form.tsx`), or any task that is itself the user's primary intent.
17
17
 
18
18
  ## Authoritative detail
19
19
 
@@ -28,7 +28,7 @@ When **`ListPageTemplate`** drives **`tab.viewType`** and the page renders **`Ta
28
28
 
29
29
  3. Thread **`view`** and **`onViewChange`** through: **client → table component → drawer toolbar → `TablePropertiesDrawer`**.
30
30
 
31
- **Reference implementations:** `components/placements-table.tsx` (`PlacementsTable`), `components/team-client.tsx` + `team-table.tsx`, `components/compliance-client.tsx` + `compliance-table.tsx`.
31
+ **Reference implementations:** `components/library-hub-client.tsx` + `library-table.tsx` (full hub: table / board / dashboard, conditional rules, bulk actions), `components/columns-showcase.tsx` (single-table catalog + built-in pagination via `HubTable`), `components/tokens-themes-client.tsx` (table + `SecondaryPanel` category rail).
32
32
 
33
33
  ## MUST NOT
34
34