@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
@@ -0,0 +1,158 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Column types — hub client.
5
+ *
6
+ * Same composition as Placements / Library
7
+ * (`PrimaryPageTemplate` + `ListPageTemplate`):
8
+ * - `header` : `PageHeader` with title + one-line subtitle describing the demo.
9
+ * - `metrics` : `KeyMetrics` `variant="flat"` — patterns, pinned, sortable, demo rows.
10
+ * - tabs : single `table` view tab (one demo table — no list / board variants).
11
+ * - `renderContent` : the `<ColumnsShowcase />` DataTable surface.
12
+ *
13
+ * Cell patterns are exercised inside `columns-showcase.tsx` so the rendered
14
+ * DataTable mirrors what real product hubs ship (favorite star, mono IDs,
15
+ * `ListHubStatusBadge`, `AvatarGroup` + `+N`, etc.).
16
+ */
17
+
18
+ import * as React from "react"
19
+
20
+ import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
21
+ import { PageHeader } from "@/components/page-header"
22
+ import {
23
+ KeyMetrics,
24
+ type MetricInsight,
25
+ type MetricItem,
26
+ } from "@/components/key-metrics"
27
+ import {
28
+ ListPageTemplate,
29
+ type ViewTab,
30
+ } from "@/components/data-views"
31
+ import {
32
+ ColumnsShowcase,
33
+ COLUMNS_SHOWCASE_PATTERN_COUNT,
34
+ COLUMNS_SHOWCASE_PINNED_COUNT,
35
+ COLUMNS_SHOWCASE_SORTABLE_COUNT,
36
+ } from "@/components/columns-showcase"
37
+
38
+ const COLUMNS_DEFAULT_TABS: ViewTab[] = [
39
+ {
40
+ id: "columns-all",
41
+ label: "All columns",
42
+ viewType: "table",
43
+ icon: "fa-table",
44
+ filterId: "all",
45
+ },
46
+ ]
47
+
48
+ const COLUMNS_SUBTITLE =
49
+ "Every cell pattern the design system ships — checkbox select, primary identity, avatar group, status chip, inline toggle, tag overflow, rating stars, progress bar, currency, attachments, external link, relative time, absolute date, and row actions overflow."
50
+
51
+ const COLUMNS_TABLE_ANCHOR = "columns-table"
52
+
53
+ /**
54
+ * Canonical KPI shape (matches `placement-kpi.ts` precedent):
55
+ * - every `MetricItem` is clickable (`href` anchor-jumps the table region),
56
+ * - a `MetricInsight` provides the narrative on the right side.
57
+ * See `apps/web/docs/kpi-flat-band-pattern.md` + `exxat-kpi-trends.mdc`.
58
+ */
59
+ const COLUMNS_KPIS: MetricItem[] = [
60
+ {
61
+ id: "patterns",
62
+ label: "Cell patterns",
63
+ value: COLUMNS_SHOWCASE_PATTERN_COUNT,
64
+ delta: "",
65
+ trend: "neutral",
66
+ trendPolarity: "informational",
67
+ metricVariant: "hero",
68
+ description: "every SaaS-grid pattern, in one HubTable",
69
+ href: `#${COLUMNS_TABLE_ANCHOR}`,
70
+ },
71
+ {
72
+ id: "pinned",
73
+ label: "Pinned columns",
74
+ value: COLUMNS_SHOWCASE_PINNED_COUNT,
75
+ delta: "",
76
+ trend: "neutral",
77
+ trendPolarity: "informational",
78
+ description: "select + question on the left, actions on the right",
79
+ href: `#${COLUMNS_TABLE_ANCHOR}`,
80
+ },
81
+ {
82
+ id: "sortable",
83
+ label: "Sortable",
84
+ value: COLUMNS_SHOWCASE_SORTABLE_COUNT,
85
+ delta: "",
86
+ trend: "neutral",
87
+ trendPolarity: "informational",
88
+ description: "click any sortable header",
89
+ href: `#${COLUMNS_TABLE_ANCHOR}`,
90
+ },
91
+ {
92
+ id: "rows",
93
+ label: "Demo rows",
94
+ value: 12,
95
+ delta: "",
96
+ trend: "neutral",
97
+ trendPolarity: "informational",
98
+ description: "real library mocks + demo augmentations",
99
+ href: `#${COLUMNS_TABLE_ANCHOR}`,
100
+ },
101
+ ]
102
+
103
+ const COLUMNS_INSIGHT: MetricInsight = {
104
+ title: "Catalog, not playground",
105
+ description:
106
+ "Every cell pattern below is an importable named export from `@/components/data-views` — `ProgressCell`, `CurrencyCell`, `RatingCell`, `RowActionsCell`, and ten more. On a real hub, do not inline-implement these; import the named cell and pass the value.",
107
+ severity: "info",
108
+ actionLabel: "Ask Leo",
109
+ }
110
+
111
+ export function ColumnsClient() {
112
+ const [tabs, setTabs] = React.useState<ViewTab[]>(COLUMNS_DEFAULT_TABS)
113
+ const [activeTabId, setActiveTabId] = React.useState<string>(COLUMNS_DEFAULT_TABS[0]!.id)
114
+
115
+ const getTabCount = React.useCallback(() => 12, [])
116
+
117
+ return (
118
+ <PrimaryPageTemplate
119
+ siteHeader={{
120
+ breadcrumbs: [{ label: "Dashboard", href: "/dashboard" }],
121
+ title: "Column types",
122
+ }}
123
+ >
124
+ <ListPageTemplate
125
+ defaultTabs={COLUMNS_DEFAULT_TABS}
126
+ tabs={tabs}
127
+ onTabsChange={setTabs}
128
+ activeTabId={activeTabId}
129
+ onActiveTabChange={setActiveTabId}
130
+ supportedViewTypes={["table"]}
131
+ getTabCount={getTabCount}
132
+ header={
133
+ <PageHeader
134
+ title="Column types"
135
+ subtitle={COLUMNS_SUBTITLE}
136
+ />
137
+ }
138
+ metrics={
139
+ <KeyMetrics
140
+ variant="flat"
141
+ metrics={COLUMNS_KPIS}
142
+ insight={COLUMNS_INSIGHT}
143
+ showHeader={false}
144
+ metricsSingleRow
145
+ />
146
+ }
147
+ renderContent={(tab, updateTab) => (
148
+ <div id={COLUMNS_TABLE_ANCHOR}>
149
+ <ColumnsShowcase
150
+ view={tab.viewType}
151
+ onViewChange={(v) => updateTab({ viewType: v })}
152
+ />
153
+ </div>
154
+ )}
155
+ />
156
+ </PrimaryPageTemplate>
157
+ )
158
+ }
@@ -0,0 +1,541 @@
1
+ "use client"
2
+
3
+ /**
4
+ * ColumnsShowcase — a single `HubTable` exercising every cell pattern the
5
+ * design system already ships. Lives at `/columns` under Resources.
6
+ *
7
+ * Hosted inside `columns-client.tsx` so the page client owns `ListPageTemplate`,
8
+ * `PageHeader`, and `KeyMetrics`. `HubTable` (NOT raw `<DataTable>`) is the
9
+ * canonical primitive for a hub view body — it wires `useTableState`, search,
10
+ * filter chips, the filter dropdown, sort, the **Table properties** drawer,
11
+ * bulk-actions, and conditional rules. Pages that drop down to raw `<DataTable>`
12
+ * silently lose filters and Properties; do not do that.
13
+ *
14
+ * **All cell renderers come from `@/components/data-views`** (re-exported from
15
+ * `components/data-views/table-cells.tsx`). This file is the **catalog page** —
16
+ * if you need any of these cells in a real hub, **import them**, do not
17
+ * re-implement. The token-economy skill (`.cursor/skills/exxat-token-economy/SKILL.md`
18
+ * §3) lists each one by name so the AI imports directly.
19
+ *
20
+ * Rows are real `LibraryItem` mocks (so the favorite/star pattern lights
21
+ * up out of the box), augmented with demo-only fields — `reviewStatus`,
22
+ * `reviewers`, `attempts`, `progress`, `cost`, `rating`, `lastActivityAt`,
23
+ * `sourceUrl`, `attachmentCount`, `published` — courtesy of the row type's
24
+ * `Record<string, unknown>` extension.
25
+ *
26
+ * Patterns in column order (mirrors what Linear / Notion / Airtable / Asana /
27
+ * Salesforce / Stripe / Jira / Monday all ship for grid surfaces):
28
+ *
29
+ * 1. Row select — explicit `key: "select"`, pinned-left, locked
30
+ * 2. Stem + ID + ⭐ — primary identity (QB favorite-button pattern)
31
+ * 3. Author identity — avatar + name + mailto email (two-line cell)
32
+ * 4. Reviewers face rail — `PeopleAvatarRailCell` (+N more overflow)
33
+ * 5. Type pill w/ icon — `PillCell` + leading FA icon
34
+ * 6. Difficulty signal — `SignalBarsCell` (Wi-Fi-style ordinal)
35
+ * 7. Status (chip+icon) — `ListHubStatusBadge` (color + icon, never alone)
36
+ * 8. Published toggle — `BooleanToggleCell` (inline `ToggleSwitch`)
37
+ * 9. Tag list +N — `TagListCell` (soft `Badge`s with overflow tip)
38
+ * 10. Rating — `RatingCell` (1–5 FA stars + value)
39
+ * 11. Progress — `ProgressCell` (track + filled bar + label)
40
+ * 12. Cost — `CurrencyCell` (right-aligned `tabular-nums`)
41
+ * 13. Attempts — `NumericCell` (right-aligned `tabular-nums`)
42
+ * 14. Files — `AttachmentCountCell` (paperclip + count)
43
+ * 15. Source — `ExternalLinkCell` (host + new-tab icon)
44
+ * 16. Last activity — `RelativeTimeCell` (+ absolute on hover)
45
+ * 17. Updated — absolute date (matches QB column)
46
+ * 18. Row actions ⋯ — `RowActionsCell<LibraryItem>` (generic)
47
+ */
48
+
49
+ import * as React from "react"
50
+ import {
51
+ HubTable,
52
+ AttachmentCountCell,
53
+ BooleanToggleCell,
54
+ CurrencyCell,
55
+ ExternalLinkCell,
56
+ NumericCell,
57
+ PeopleAvatarRailCell,
58
+ PillCell,
59
+ ProgressCell,
60
+ RatingCell,
61
+ RelativeTimeCell,
62
+ RowActionsCell,
63
+ SignalBarsCell,
64
+ TagListCell,
65
+ type PersonStub,
66
+ type RowActionDef,
67
+ } from "@/components/data-views"
68
+ import type { DataListViewType } from "@/lib/data-list-view"
69
+ import { AvatarInitials } from "@/components/ui/avatar"
70
+ import { cn } from "@/lib/utils"
71
+ import {
72
+ LibraryFavoriteButton,
73
+ LIBRARY_FAVORITE_HOVER_GROUP,
74
+ } from "@/components/library-favorite-button"
75
+ import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
76
+ import {
77
+ LIST_HUB_STATUS_TINT_DANGER,
78
+ LIST_HUB_STATUS_TINT_INFO,
79
+ LIST_HUB_STATUS_TINT_NEUTRAL,
80
+ LIST_HUB_STATUS_TINT_SUCCESS,
81
+ LIST_HUB_STATUS_TINT_WARNING,
82
+ } from "@/lib/list-status-badges"
83
+ import { initialsFromDisplayName } from "@/lib/initials-from-name"
84
+ import { formatDateUS } from "@/lib/date-filter"
85
+ import { mailtoHref } from "@/lib/mailto"
86
+ import {
87
+ LIBRARY_ITEMS,
88
+ type LibraryItem,
89
+ type LibraryItemType,
90
+ type LibraryLevel,
91
+ } from "@/lib/mock/library"
92
+ import type { ColumnDef } from "@/components/data-table/types"
93
+
94
+ /* ── Demo-only row augmentation ────────────────────────────────────────── */
95
+
96
+ type ReviewStatus = "draft" | "in_review" | "approved" | "needs_update" | "archived"
97
+
98
+ const STATUS_LABEL: Record<ReviewStatus, string> = {
99
+ draft: "Draft",
100
+ in_review: "In review",
101
+ approved: "Approved",
102
+ needs_update: "Needs update",
103
+ archived: "Archived",
104
+ }
105
+
106
+ const STATUS_TINT: Record<ReviewStatus, string> = {
107
+ draft: LIST_HUB_STATUS_TINT_NEUTRAL,
108
+ in_review: LIST_HUB_STATUS_TINT_INFO,
109
+ approved: LIST_HUB_STATUS_TINT_SUCCESS,
110
+ needs_update: LIST_HUB_STATUS_TINT_WARNING,
111
+ archived: LIST_HUB_STATUS_TINT_DANGER,
112
+ }
113
+
114
+ const STATUS_ICON: Record<ReviewStatus, string> = {
115
+ draft: "fa-pen-to-square",
116
+ in_review: "fa-eye",
117
+ approved: "fa-circle-check",
118
+ needs_update: "fa-triangle-exclamation",
119
+ archived: "fa-box-archive",
120
+ }
121
+
122
+ const TYPE_LABEL: Record<LibraryItemType, string> = {
123
+ multiple_choice: "Multiple choice",
124
+ true_false: "True / false",
125
+ short_answer: "Short answer",
126
+ }
127
+
128
+ const TYPE_ICON: Record<LibraryItemType, string> = {
129
+ multiple_choice: "fa-list-check",
130
+ true_false: "fa-toggle-on",
131
+ short_answer: "fa-pen-line",
132
+ }
133
+
134
+ const DIFFICULTY_LEVEL: Record<LibraryLevel, number> = {
135
+ easy: 1, medium: 2, hard: 3,
136
+ }
137
+
138
+ const DIFFICULTY_TONE: Record<LibraryLevel, "success" | "warning" | "danger"> = {
139
+ easy: "success", medium: "warning", hard: "danger",
140
+ }
141
+
142
+ const REVIEWER_POOL: PersonStub[] = [
143
+ { name: "Aisha Khan", initials: "AK" },
144
+ { name: "Marcus Patel", initials: "MP" },
145
+ { name: "Sofia Rinaldi", initials: "SR" },
146
+ { name: "Jamal Brooks", initials: "JB" },
147
+ { name: "Priya Iyer", initials: "PI" },
148
+ { name: "Diego Suarez", initials: "DS" },
149
+ { name: "Hannah Reed", initials: "HR" },
150
+ { name: "Mei Lin", initials: "ML" },
151
+ ]
152
+
153
+ const REVIEW_STATUSES: ReviewStatus[] = [
154
+ "draft", "in_review", "approved", "needs_update", "approved", "in_review",
155
+ "draft", "archived", "approved", "in_review", "needs_update", "approved",
156
+ ]
157
+
158
+ const SOURCE_URLS: string[] = [
159
+ "https://nlm.nih.gov/medlineplus",
160
+ "https://merckmanuals.com/professional",
161
+ "https://uptodate.com/contents/diabetes",
162
+ "https://cdc.gov/asthma/clinical-care",
163
+ "https://ada.org/resources/research",
164
+ "https://heart.org/health-topics",
165
+ "https://aap.org/en/practice-management",
166
+ "https://nice.org.uk/guidance/ng17",
167
+ ]
168
+
169
+ const PUBLISHED_BY_INDEX = [
170
+ true, true, false, true, false, true, true, false, true, true, false, true,
171
+ ]
172
+
173
+ /** Build the showcase dataset once. Keeps `LibraryItem` as the row type so
174
+ * `LibraryFavoriteButton` plugs in with zero adaptation. The demo
175
+ * augmentations exercise the long tail of SaaS-grid cell patterns. */
176
+ function buildRows(): LibraryItem[] {
177
+ const NOW = Date.UTC(2026, 4, 21, 10, 30, 0)
178
+ return LIBRARY_ITEMS.slice(0, 12).map((item, i) => {
179
+ const lastActivityAt = new Date(
180
+ NOW - i * 1000 * 60 * 60 * 17 - 1000 * 60 * 13,
181
+ ).toISOString()
182
+ return {
183
+ ...item,
184
+ reviewStatus: REVIEW_STATUSES[i % REVIEW_STATUSES.length],
185
+ reviewers: REVIEWER_POOL.slice(i % 3, (i % 3) + 3 + (i % 3)),
186
+ attempts: 27 + ((i * 11) % 96),
187
+ isStarred: i % 4 === 0,
188
+ progress: 8 + ((i * 17) % 92),
189
+ cost: 12 + ((i * 91) % 488) + ((i * 31) % 100) / 100,
190
+ rating: 1 + ((i * 7) % 5),
191
+ attachmentCount: i === 1 ? 0 : 1 + ((i * 5) % 7),
192
+ sourceUrl: SOURCE_URLS[i % SOURCE_URLS.length],
193
+ lastActivityAt,
194
+ published: PUBLISHED_BY_INDEX[i % PUBLISHED_BY_INDEX.length],
195
+ }
196
+ })
197
+ }
198
+
199
+ /* ── Row actions definition ────────────────────────────────────────────── */
200
+
201
+ const ROW_ACTIONS: RowActionDef<LibraryItem>[] = [
202
+ { label: "Open", icon: "fa-arrow-up-right", onSelect: () => {} },
203
+ { label: "Edit", icon: "fa-pen-to-square", onSelect: () => {} },
204
+ { label: "Duplicate", icon: "fa-clone", onSelect: () => {} },
205
+ { label: "Archive", icon: "fa-box-archive", onSelect: () => {}, variant: "destructive" },
206
+ ]
207
+
208
+ /* ── Column definitions ────────────────────────────────────────────────── */
209
+
210
+ function useColumns(
211
+ onToggleFavorite: (row: LibraryItem) => void,
212
+ onTogglePublished: (row: LibraryItem) => void,
213
+ ): ColumnDef<LibraryItem>[] {
214
+ return React.useMemo<ColumnDef<LibraryItem>[]>(() => [
215
+ // 1. Select — explicit checkbox column. DataTable renders the checkbox cell
216
+ // automatically; declaring it here makes it visible in the Properties
217
+ // drawer column list and pins it left.
218
+ {
219
+ key: "select",
220
+ label: "",
221
+ width: 40,
222
+ minWidth: 40,
223
+ defaultPin: "left",
224
+ lockPin: true,
225
+ },
226
+ // 2. Primary identity — name + mono ID + favorite star.
227
+ {
228
+ key: "stem",
229
+ label: "Name",
230
+ width: 320,
231
+ minWidth: 220,
232
+ defaultPin: "left",
233
+ sortable: true,
234
+ sortKey: "stem",
235
+ cell: (row) => (
236
+ <div className={cn(LIBRARY_FAVORITE_HOVER_GROUP, "flex min-w-0 items-start gap-2")}>
237
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5 pe-1">
238
+ <span className="line-clamp-2 text-sm font-medium text-foreground">{row.stem}</span>
239
+ <span className="font-mono text-xs text-muted-foreground tabular-nums">{row.questionId}</span>
240
+ </div>
241
+ <LibraryFavoriteButton row={row} onToggleFavorite={onToggleFavorite} />
242
+ </div>
243
+ ),
244
+ },
245
+ // 3. Person identity — avatar + name + email (two-line cell).
246
+ {
247
+ key: "author",
248
+ label: "Owner",
249
+ width: 260,
250
+ minWidth: 200,
251
+ sortable: true,
252
+ sortKey: "author",
253
+ filter: { type: "text", icon: "fa-user", operators: ["contains", "not_contains"] },
254
+ cell: (row) => {
255
+ const initials = initialsFromDisplayName(row.author)
256
+ return (
257
+ <div className="flex min-w-0 items-center gap-2.5">
258
+ <AvatarInitials initials={initials} className="size-8 shrink-0 text-xs" />
259
+ <div className="flex min-w-0 flex-col gap-0.5">
260
+ <span className="truncate text-sm font-medium text-foreground">{row.author}</span>
261
+ {row.authorEmail ? (
262
+ <a
263
+ href={mailtoHref(row.authorEmail)}
264
+ className="truncate text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
265
+ onClick={(e) => e.stopPropagation()}
266
+ >
267
+ {row.authorEmail}
268
+ </a>
269
+ ) : null}
270
+ </div>
271
+ </div>
272
+ )
273
+ },
274
+ },
275
+ // 4. Face rail (+N overflow) — `PeopleAvatarRailCell`.
276
+ {
277
+ key: "reviewers",
278
+ label: "Reviewers",
279
+ width: 160,
280
+ minWidth: 140,
281
+ cell: (row) => (
282
+ <PeopleAvatarRailCell people={row.reviewers as PersonStub[] | undefined} />
283
+ ),
284
+ },
285
+ // 5. Single-select pill with icon — `PillCell`.
286
+ {
287
+ key: "type",
288
+ label: "Type",
289
+ width: 170,
290
+ minWidth: 150,
291
+ sortable: true,
292
+ sortKey: "type",
293
+ filter: {
294
+ type: "select",
295
+ icon: "fa-list-check",
296
+ options: (Object.keys(TYPE_LABEL) as LibraryItemType[]).map((k) => ({ value: k, label: TYPE_LABEL[k] })),
297
+ },
298
+ cell: (row) => <PillCell label={TYPE_LABEL[row.type]} icon={TYPE_ICON[row.type]} />,
299
+ },
300
+ // 6. Level signal — `SignalBarsCell` (Wi-Fi metaphor).
301
+ {
302
+ key: "difficulty",
303
+ label: "Level",
304
+ width: 100,
305
+ minWidth: 90,
306
+ sortable: true,
307
+ sortKey: "difficulty",
308
+ filter: {
309
+ type: "select",
310
+ icon: "fa-signal-bars",
311
+ options: [
312
+ { value: "easy", label: "Low" },
313
+ { value: "medium", label: "Normal" },
314
+ { value: "hard", label: "High" },
315
+ ],
316
+ },
317
+ cell: (row) => (
318
+ <SignalBarsCell
319
+ level={DIFFICULTY_LEVEL[row.difficulty]}
320
+ tone={DIFFICULTY_TONE[row.difficulty]}
321
+ label={`Difficulty: ${row.difficulty}`}
322
+ />
323
+ ),
324
+ },
325
+ // 7. Status — chip + icon (color + glyph; never color alone).
326
+ {
327
+ key: "reviewStatus",
328
+ label: "Status",
329
+ width: 150,
330
+ minWidth: 130,
331
+ filter: {
332
+ type: "select",
333
+ icon: "fa-circle-check",
334
+ options: (Object.keys(STATUS_LABEL) as ReviewStatus[]).map((k) => ({ value: k, label: STATUS_LABEL[k] })),
335
+ },
336
+ cell: (row) => {
337
+ const s = (row.reviewStatus as ReviewStatus | undefined) ?? "draft"
338
+ return (
339
+ <ListHubStatusBadge
340
+ label={STATUS_LABEL[s]}
341
+ tintClassName={STATUS_TINT[s]}
342
+ icon={STATUS_ICON[s]}
343
+ />
344
+ )
345
+ },
346
+ },
347
+ // 8. Inline toggle — `BooleanToggleCell` for a boolean lifecycle field.
348
+ {
349
+ key: "published",
350
+ label: "Active",
351
+ width: 110,
352
+ minWidth: 100,
353
+ cell: (row) => (
354
+ <BooleanToggleCell
355
+ checked={Boolean((row as Record<string, unknown>).published)}
356
+ onChange={() => onTogglePublished(row)}
357
+ labelOn="Active — click to disable"
358
+ labelOff="Inactive — click to activate"
359
+ />
360
+ ),
361
+ },
362
+ // 9. Tag list +N — `TagListCell`.
363
+ {
364
+ key: "tags",
365
+ label: "Tags",
366
+ width: 180,
367
+ minWidth: 140,
368
+ cell: (row) => <TagListCell tags={row.tags} />,
369
+ },
370
+ // 10. Rating — `RatingCell` (5 stars + value).
371
+ {
372
+ key: "rating",
373
+ label: "Rating",
374
+ width: 130,
375
+ minWidth: 110,
376
+ sortable: true,
377
+ sortKey: "rating",
378
+ cell: (row) => <RatingCell value={(row.rating as number | undefined) ?? 0} />,
379
+ },
380
+ // 11. Progress — `ProgressCell` (track + filled + label).
381
+ {
382
+ key: "progress",
383
+ label: "Progress",
384
+ width: 180,
385
+ minWidth: 150,
386
+ sortable: true,
387
+ sortKey: "progress",
388
+ cell: (row) => <ProgressCell value={(row.progress as number | undefined) ?? 0} />,
389
+ },
390
+ // 12. Currency — `CurrencyCell` (right-aligned tabular-nums USD).
391
+ {
392
+ key: "cost",
393
+ label: "Cost",
394
+ width: 110,
395
+ minWidth: 90,
396
+ sortable: true,
397
+ sortKey: "cost",
398
+ cell: (row) => <CurrencyCell value={(row.cost as number | undefined) ?? 0} />,
399
+ },
400
+ // 13. Plain numeric — `NumericCell` (right-aligned).
401
+ {
402
+ key: "attempts",
403
+ label: "Count",
404
+ width: 100,
405
+ minWidth: 80,
406
+ sortable: true,
407
+ sortKey: "attempts",
408
+ cell: (row) => <NumericCell value={(row.attempts as number | undefined) ?? 0} />,
409
+ },
410
+ // 14. Attachment count — `AttachmentCountCell`.
411
+ {
412
+ key: "attachmentCount",
413
+ label: "Files",
414
+ width: 80,
415
+ minWidth: 70,
416
+ sortable: true,
417
+ sortKey: "attachmentCount",
418
+ cell: (row) => (
419
+ <AttachmentCountCell count={(row.attachmentCount as number | undefined) ?? 0} />
420
+ ),
421
+ },
422
+ // 15. External link — `ExternalLinkCell` (host + new-tab icon).
423
+ {
424
+ key: "sourceUrl",
425
+ label: "Link",
426
+ width: 200,
427
+ minWidth: 160,
428
+ cell: (row) => <ExternalLinkCell url={(row.sourceUrl as string | undefined) ?? ""} />,
429
+ },
430
+ // 16. Relative time + absolute on hover — `RelativeTimeCell`.
431
+ {
432
+ key: "lastActivityAt",
433
+ label: "Last activity",
434
+ width: 150,
435
+ minWidth: 130,
436
+ sortable: true,
437
+ sortKey: "lastActivityAt",
438
+ cell: (row) => (
439
+ <RelativeTimeCell iso={(row.lastActivityAt as string | undefined) ?? ""} />
440
+ ),
441
+ },
442
+ // 17. Absolute date.
443
+ {
444
+ key: "updatedAt",
445
+ label: "Updated",
446
+ width: 120,
447
+ minWidth: 100,
448
+ sortable: true,
449
+ sortKey: "updatedAt",
450
+ cell: (row) => (
451
+ <span className="text-sm tabular-nums text-foreground/90 whitespace-nowrap">
452
+ {formatDateUS(row.updatedAt)}
453
+ </span>
454
+ ),
455
+ },
456
+ // 18. Row actions overflow — `RowActionsCell<LibraryItem>`.
457
+ {
458
+ key: "actions",
459
+ label: "",
460
+ width: 48,
461
+ minWidth: 48,
462
+ defaultPin: "right",
463
+ lockPin: true,
464
+ cell: (row) => (
465
+ <div className="flex items-center justify-center">
466
+ <RowActionsCell row={row} actions={ROW_ACTIONS} triggerLabel={`Actions for item ${row.questionId}`} />
467
+ </div>
468
+ ),
469
+ },
470
+ ], [onToggleFavorite, onTogglePublished])
471
+ }
472
+
473
+ /* ── Public ───────────────────────────────────────────────────────────── */
474
+
475
+ /** Column patterns showcased in this HubTable — surfaced as KPIs by the page client. */
476
+ export const COLUMNS_SHOWCASE_PATTERN_COUNT = 18
477
+ export const COLUMNS_SHOWCASE_PINNED_COUNT = 3 // select + name + actions
478
+ export const COLUMNS_SHOWCASE_SORTABLE_COUNT = 11 // name, owner, type, level, rating, progress, cost, count, files, lastActivityAt, updatedAt
479
+
480
+ const COLUMNS_SUPPORTED_VIEWS: readonly DataListViewType[] = ["table"] as const
481
+
482
+ export interface ColumnsShowcaseProps {
483
+ /** Active view from `ListPageTemplate.renderContent`. */
484
+ view: DataListViewType
485
+ /** Tab update callback from `ListPageTemplate.renderContent`. */
486
+ onViewChange: (v: DataListViewType) => void
487
+ }
488
+
489
+ /**
490
+ * The actual hub surface — wrapped by `columns-client.tsx` inside
491
+ * `ListPageTemplate.renderContent`. No outer card chrome — keep this lean so
492
+ * the host template owns header / KPIs / view tabs.
493
+ */
494
+ export function ColumnsShowcase({ view, onViewChange }: ColumnsShowcaseProps) {
495
+ const [rows, setRows] = React.useState<LibraryItem[]>(() => buildRows())
496
+ const [pagination, setPagination] = React.useState(false)
497
+
498
+ const toggleFavorite = React.useCallback((row: LibraryItem) => {
499
+ setRows((current) =>
500
+ current.map((r) => (r.id === row.id ? { ...r, isStarred: !r.isStarred } : r)),
501
+ )
502
+ }, [])
503
+
504
+ const togglePublished = React.useCallback((row: LibraryItem) => {
505
+ setRows((current) =>
506
+ current.map((r) =>
507
+ r.id === row.id
508
+ ? { ...r, published: !(r as Record<string, unknown>).published }
509
+ : r,
510
+ ),
511
+ )
512
+ }, [])
513
+
514
+ const columns = useColumns(toggleFavorite, togglePublished)
515
+
516
+ return (
517
+ <HubTable<LibraryItem>
518
+ rows={rows}
519
+ columns={columns}
520
+ view={view}
521
+ onViewChange={onViewChange}
522
+ supportedViewTypes={COLUMNS_SUPPORTED_VIEWS}
523
+ hubLabel="Column types"
524
+ lifecycleTabLabel="Column types"
525
+ searchAriaLabel="Search columns showcase"
526
+ getRowId={(r) => r.id}
527
+ getRowSelectionLabel={(r) => r.stem}
528
+ defaultSort={{ key: "stem", dir: "asc" }}
529
+ pagination={pagination}
530
+ onPaginationChange={setPagination}
531
+ paginationInitialPageSize={5}
532
+ paginationPageSizeOptions={[5, 10, 25]}
533
+ emptyState={
534
+ <p className="text-sm text-muted-foreground">
535
+ No rows match your filters.
536
+ </p>
537
+ }
538
+ renderers={{}}
539
+ />
540
+ )
541
+ }