@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
@@ -1,468 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Compliance obligations — thin wrapper around the centralized `<HubTable>`. Owns only the
5
- * column defs, panel-view helpers, dashboard layout state, and per-view renderers.
6
- */
7
-
8
- import * as React from "react"
9
- import {
10
- COMPLIANCE_STATUS_BADGE_CLASS,
11
- COMPLIANCE_STATUS_ICON,
12
- COMPLIANCE_STATUS_LABEL,
13
- } from "@/lib/list-status-badges"
14
- import type { ComplianceItem } from "@/lib/mock/compliance"
15
- import { DataTableToolbar } from "@/components/data-table"
16
- import {
17
- ComplianceDashboardChartsSection,
18
- ALL_COMPLIANCE_DASHBOARD_CARDS,
19
- DEFAULT_COMPLIANCE_CHART_TYPES,
20
- DEFAULT_COMPLIANCE_SPANS,
21
- loadComplianceDashboardLayout,
22
- mergeComplianceDashboardLayout,
23
- saveComplianceDashboardLayout,
24
- } from "@/components/data-view-dashboard-charts-compliance"
25
- import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
26
- import type { ChartType, DashboardLayout } from "@/lib/data-view-dashboard-placements-layout"
27
- import { ListPageBoardCard } from "@/components/data-views/list-page-board-card"
28
- import { ComplianceBoardView, COMPLIANCE_BOARD_GROUP_OPTIONS } from "@/components/compliance-board-view"
29
- import { complianceKpiInsight, complianceKpiMetrics } from "@/lib/mock/compliance-kpi"
30
- import type { DataListViewType } from "@/lib/data-list-view"
31
- import type { ColumnDef } from "@/components/data-table/types"
32
- import { TablePropertiesDrawerButton } from "@/components/table-properties"
33
- import {
34
- HubTable,
35
- type HubTableHandle,
36
- type HubTableRenderers,
37
- type HubTableRendererArgs,
38
- } from "@/components/data-views"
39
- import { COMPLIANCE_SUPPORTED_VIEWS } from "@/lib/compliance-supported-views"
40
- import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
41
- import { Button } from "@/components/ui/button"
42
- import {
43
- DropdownMenu,
44
- DropdownMenuContent,
45
- DropdownMenuItem,
46
- DropdownMenuTrigger,
47
- } from "@/components/ui/dropdown-menu"
48
- import { Tip } from "@/components/ui/tip"
49
- import { CoachMark } from "@/components/ui/coach-mark"
50
- import { useCoachMark } from "@/hooks/use-coach-mark"
51
- import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
52
- import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
53
- import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
54
-
55
- // ─── Helpers ─────────────────────────────────────────────────────────────────
56
-
57
- function uniqueCategories(items: ComplianceItem[]) {
58
- return [...new Set(items.map(i => i.category))].sort().map(c => ({ value: c, label: c }))
59
- }
60
-
61
- const STATUS_FILTER_OPTS = [
62
- { value: "compliant", label: COMPLIANCE_STATUS_LABEL.compliant },
63
- { value: "due_soon", label: COMPLIANCE_STATUS_LABEL.due_soon },
64
- { value: "overdue", label: COMPLIANCE_STATUS_LABEL.overdue },
65
- { value: "pending", label: COMPLIANCE_STATUS_LABEL.pending },
66
- ]
67
-
68
- function buildComplianceColumns(items: ComplianceItem[]): ColumnDef<ComplianceItem>[] {
69
- const catOpts = uniqueCategories(items)
70
- return [
71
- { key: "select", label: "", width: 40, minWidth: 40, defaultPin: "left", lockPin: true },
72
- {
73
- key: "title",
74
- label: "Obligation",
75
- width: 280,
76
- minWidth: 140,
77
- sortable: true,
78
- sortKey: "title",
79
- defaultPin: "left",
80
- filter: { type: "text", icon: "fa-file-lines", operators: ["contains", "not_contains"] },
81
- cell: row => (
82
- <span className="line-clamp-2 text-sm font-medium text-foreground">{row.title}</span>
83
- ),
84
- },
85
- {
86
- key: "category",
87
- label: "Category",
88
- width: 160,
89
- minWidth: 120,
90
- sortable: true,
91
- sortKey: "category",
92
- filter: { type: "select", icon: "fa-layer-group", operators: ["is", "is_not"], options: catOpts },
93
- cell: row => <span className="text-sm text-foreground/90">{row.category}</span>,
94
- },
95
- {
96
- key: "status",
97
- label: "Status",
98
- width: 120,
99
- minWidth: 100,
100
- sortable: true,
101
- sortKey: "status",
102
- filter: { type: "select", icon: "fa-circle-dot", operators: ["is", "is_not"], options: STATUS_FILTER_OPTS },
103
- cell: row => (
104
- <ListHubStatusBadge
105
- label={COMPLIANCE_STATUS_LABEL[row.status]}
106
- tintClassName={COMPLIANCE_STATUS_BADGE_CLASS[row.status]}
107
- icon={COMPLIANCE_STATUS_ICON[row.status]}
108
- />
109
- ),
110
- },
111
- {
112
- key: "dueDate",
113
- label: "Due",
114
- width: 120,
115
- minWidth: 100,
116
- sortable: true,
117
- sortKey: "dueDate",
118
- filter: { type: "date", icon: "fa-calendar-days", operators: ["is", "is_not"] },
119
- cell: row => (
120
- <span className="text-sm tabular-nums text-foreground/90 whitespace-nowrap">{row.dueDate}</span>
121
- ),
122
- },
123
- {
124
- key: "owner",
125
- label: "Owner",
126
- width: 160,
127
- minWidth: 120,
128
- sortable: true,
129
- sortKey: "owner",
130
- filter: { type: "text", icon: "fa-user", operators: ["contains", "not_contains"] },
131
- cell: row => <span className="text-sm text-foreground/90">{row.owner}</span>,
132
- },
133
- {
134
- key: "lastReviewed",
135
- label: "Last reviewed",
136
- width: 120,
137
- minWidth: 100,
138
- sortable: true,
139
- sortKey: "lastReviewed",
140
- filter: { type: "date", icon: "fa-calendar-check", operators: ["is", "is_not"] },
141
- cell: row => (
142
- <span className="text-sm tabular-nums text-muted-foreground whitespace-nowrap">{row.lastReviewed}</span>
143
- ),
144
- },
145
- {
146
- key: "actions",
147
- label: "",
148
- width: 48,
149
- minWidth: 48,
150
- defaultPin: "right",
151
- lockPin: true,
152
- cell: row => (
153
- <div className="flex items-center justify-center">
154
- <DropdownMenu>
155
- <DropdownMenuTrigger asChild>
156
- <Button size="icon-sm" variant="ghost" aria-label={`Actions for ${row.title}`}>
157
- <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
158
- </Button>
159
- </DropdownMenuTrigger>
160
- <DropdownMenuContent align="end">
161
- <DropdownMenuItem disabled>
162
- <i className="fa-light fa-eye" aria-hidden="true" />
163
- View details
164
- </DropdownMenuItem>
165
- <DropdownMenuItem disabled>
166
- <i className="fa-light fa-user-check" aria-hidden="true" />
167
- Assign owner
168
- </DropdownMenuItem>
169
- </DropdownMenuContent>
170
- </DropdownMenu>
171
- </div>
172
- ),
173
- },
174
- ]
175
- }
176
-
177
- // ─── Panel view helpers ─────────────────────────────────────────────────────
178
-
179
- function buildCompliancePanelGroups(rows: ComplianceItem[]): FinderGroup[] {
180
- const byCategory = new Map<string, ComplianceItem[]>()
181
- for (const item of rows) {
182
- if (!byCategory.has(item.category)) byCategory.set(item.category, [])
183
- byCategory.get(item.category)!.push(item)
184
- }
185
- return Array.from(byCategory.keys())
186
- .sort()
187
- .map(category => ({
188
- id: category,
189
- label: category,
190
- icon: "fa-folder",
191
- count: (byCategory.get(category) ?? []).length,
192
- }))
193
- }
194
-
195
- // ─── Dashboard body ─────────────────────────────────────────────────────────
196
-
197
- interface ComplianceDashboardBodyProps {
198
- args: HubTableRendererArgs<ComplianceItem>
199
- columns: ColumnDef<ComplianceItem>[]
200
- }
201
-
202
- function ComplianceDashboardBody({ args, columns }: ComplianceDashboardBodyProps) {
203
- const { state, drawerToolbarProps, displayOptions } = args
204
- const rows = state.rows as ComplianceItem[]
205
-
206
- const dashboardKpi = React.useMemo(
207
- () => ({ metrics: complianceKpiMetrics(rows), insight: complianceKpiInsight(rows) }),
208
- [rows],
209
- )
210
-
211
- const [visibleCards, setVisibleCards] = React.useState<string[]>(() =>
212
- ALL_COMPLIANCE_DASHBOARD_CARDS.map(c => c.id),
213
- )
214
- const [cardOrder, setCardOrder] = React.useState<string[]>(() =>
215
- ALL_COMPLIANCE_DASHBOARD_CARDS.map(c => c.id),
216
- )
217
- const [cardSpans, setCardSpans] = React.useState<Record<string, 1 | 2>>(() => ({ ...DEFAULT_COMPLIANCE_SPANS }))
218
- const [cardChartTypes, setCardChartTypes] = React.useState<Record<string, ChartType>>(() => ({
219
- ...DEFAULT_COMPLIANCE_CHART_TYPES,
220
- }))
221
- const [kpiCount, setKpiCount] = React.useState<number>(KEY_METRICS_KPI_COUNT_DEFAULT)
222
- const [layoutEdit, setLayoutEdit] = React.useState(false)
223
- const hydrated = React.useRef(false)
224
- const baselineRef = React.useRef<DashboardLayout | null>(null)
225
-
226
- React.useEffect(() => {
227
- const saved = loadComplianceDashboardLayout()
228
- const m = mergeComplianceDashboardLayout(saved)
229
- setVisibleCards(m.visible)
230
- setCardOrder(m.order)
231
- setCardSpans(m.spans ?? { ...DEFAULT_COMPLIANCE_SPANS })
232
- setCardChartTypes(m.chartTypes ?? { ...DEFAULT_COMPLIANCE_CHART_TYPES })
233
- setKpiCount(m.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
234
- hydrated.current = true
235
- }, [])
236
-
237
- React.useEffect(() => {
238
- if (!hydrated.current) return
239
- saveComplianceDashboardLayout({
240
- visible: visibleCards,
241
- order: cardOrder,
242
- spans: cardSpans,
243
- chartTypes: cardChartTypes,
244
- keyMetricsKpiCount: kpiCount,
245
- })
246
- }, [visibleCards, cardOrder, cardSpans, cardChartTypes, kpiCount])
247
-
248
- const onResetLayout = React.useCallback(() => {
249
- setVisibleCards(ALL_COMPLIANCE_DASHBOARD_CARDS.map(c => c.id))
250
- setCardOrder(ALL_COMPLIANCE_DASHBOARD_CARDS.map(c => c.id))
251
- setCardSpans({ ...DEFAULT_COMPLIANCE_SPANS })
252
- setCardChartTypes({ ...DEFAULT_COMPLIANCE_CHART_TYPES })
253
- setKpiCount(KEY_METRICS_KPI_COUNT_DEFAULT)
254
- }, [])
255
-
256
- const onLayoutEditStart = React.useCallback(() => {
257
- baselineRef.current = {
258
- visible: [...visibleCards],
259
- order: [...cardOrder],
260
- spans: { ...cardSpans },
261
- chartTypes: { ...cardChartTypes },
262
- keyMetricsKpiCount: kpiCount,
263
- }
264
- setLayoutEdit(true)
265
- }, [visibleCards, cardOrder, cardSpans, cardChartTypes, kpiCount])
266
-
267
- const onLayoutEditCancel = React.useCallback(() => {
268
- const b = baselineRef.current
269
- if (b) {
270
- setVisibleCards(b.visible)
271
- setCardOrder(b.order)
272
- setCardSpans(b.spans ?? { ...DEFAULT_COMPLIANCE_SPANS })
273
- setCardChartTypes(b.chartTypes ?? { ...DEFAULT_COMPLIANCE_CHART_TYPES })
274
- setKpiCount(b.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
275
- }
276
- setLayoutEdit(false)
277
- }, [])
278
-
279
- const coach = useCoachMark({
280
- flowId: "compliance-dashboard-customize",
281
- steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
282
- delay: 700,
283
- enabled: true,
284
- })
285
-
286
- return (
287
- <div className="flex min-h-0 flex-1 flex-col">
288
- <CoachMark state={coach} />
289
- {!layoutEdit ? (
290
- <DataTableToolbar
291
- state={state}
292
- columns={columns}
293
- searchable={displayOptions.showToolbarSearch}
294
- searchAriaLabel="Search compliance obligations"
295
- toolbarSlot={s => (
296
- <TablePropertiesDrawerButton
297
- {...drawerToolbarProps}
298
- state={s}
299
- extraActions={
300
- <Tip side="bottom" label="Edit dashboard layout on canvas">
301
- <Button
302
- type="button"
303
- variant="ghost"
304
- size="icon-sm"
305
- aria-label="Edit dashboard layout"
306
- onClick={onLayoutEditStart}
307
- className="text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover"
308
- >
309
- <i className="fa-light fa-pen-ruler text-[13px]" aria-hidden="true" />
310
- </Button>
311
- </Tip>
312
- }
313
- />
314
- )}
315
- />
316
- ) : null}
317
- <ComplianceDashboardChartsSection
318
- rows={rows}
319
- keyMetrics={dashboardKpi}
320
- visibleCards={visibleCards}
321
- cardOrder={cardOrder}
322
- cardSpans={cardSpans}
323
- cardChartTypes={cardChartTypes}
324
- keyMetricsKpiCount={kpiCount}
325
- layoutEditMode={layoutEdit}
326
- onVisibleChange={setVisibleCards}
327
- onOrderChange={setCardOrder}
328
- onSpanChange={(id, span) => setCardSpans(prev => ({ ...prev, [id]: span }))}
329
- onChartTypeChange={(id, t) => setCardChartTypes(prev => ({ ...prev, [id]: t }))}
330
- onKeyMetricsKpiCountChange={setKpiCount}
331
- onResetLayout={onResetLayout}
332
- onLayoutEditDone={() => setLayoutEdit(false)}
333
- onLayoutEditCancel={onLayoutEditCancel}
334
- />
335
- </div>
336
- )
337
- }
338
-
339
- // ─── Public component ───────────────────────────────────────────────────────
340
-
341
- export type ComplianceTableHandle = HubTableHandle
342
-
343
- export const ComplianceTable = React.forwardRef<
344
- ComplianceTableHandle,
345
- { items: ComplianceItem[]; view?: DataListViewType; onViewChange?: (v: DataListViewType) => void }
346
- >(function ComplianceTable({ items, view = "table", onViewChange }, ref) {
347
- const columns = React.useMemo(() => buildComplianceColumns(items), [items])
348
-
349
- const renderers: HubTableRenderers<ComplianceItem> = {
350
- "board-with-toolbar": ({ state, toolbarShell, displayOptions }) => {
351
- const boardGroupKey = COMPLIANCE_BOARD_GROUP_OPTIONS.some(
352
- o => o.key === displayOptions.boardGroupByColumnKey,
353
- )
354
- ? displayOptions.boardGroupByColumnKey
355
- : "status"
356
- return toolbarShell(
357
- <ComplianceBoardView
358
- rows={state.rows as ComplianceItem[]}
359
- groupByColumnKey={boardGroupKey}
360
- onRowActivate={row => state.toggleRow(row.id)}
361
- />,
362
- )
363
- },
364
- "panel-with-toolbar": ({ state, toolbarShell }) =>
365
- toolbarShell(
366
- <ListPageSplitHubChrome aria-label="Compliance obligations panel view">
367
- <FinderPanelView<ComplianceItem>
368
- embedded
369
- groupsColumnTitle="Category"
370
- groups={buildCompliancePanelGroups(state.rows as ComplianceItem[])}
371
- rows={state.rows as ComplianceItem[]}
372
- getRowId={row => row.id}
373
- getRowGroupId={row => row.category}
374
- autoSaveId="compliance-panel-view"
375
- renderListRow={row => (
376
- <div className="flex-1 min-w-0">
377
- <p className="text-sm font-medium text-foreground truncate">{row.title}</p>
378
- <p className="text-xs text-muted-foreground mt-1">{row.category}</p>
379
- </div>
380
- )}
381
- renderDetail={row => (
382
- <div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4">
383
- <div>
384
- <h3 className="text-sm font-semibold text-foreground mb-2">Obligation</h3>
385
- <p className="text-sm text-foreground">{row.title}</p>
386
- </div>
387
- <div className="flex flex-col gap-2">
388
- <div>
389
- <span className="text-xs font-medium text-muted-foreground">Category</span>
390
- <p className="text-sm text-foreground">{row.category}</p>
391
- </div>
392
- <div>
393
- <span className="text-xs font-medium text-muted-foreground">Status</span>
394
- <p className="text-sm text-foreground">{COMPLIANCE_STATUS_LABEL[row.status]}</p>
395
- </div>
396
- </div>
397
- </div>
398
- )}
399
- emptyList={<p className="text-sm text-muted-foreground">No obligations found.</p>}
400
- />
401
- </ListPageSplitHubChrome>,
402
- ),
403
- "dashboard-with-toolbar": args => <ComplianceDashboardBody args={args} columns={columns} />,
404
- }
405
-
406
- return (
407
- <HubTable<ComplianceItem>
408
- rows={items}
409
- columns={columns}
410
- view={view}
411
- onViewChange={onViewChange}
412
- supportedViewTypes={COMPLIANCE_SUPPORTED_VIEWS}
413
- hubLabel="Compliance"
414
- lifecycleTabLabel="Compliance"
415
- searchAriaLabel="Search compliance obligations"
416
- getRowId={row => row.id}
417
- getRowSelectionLabel={row => row.title}
418
- defaultSort={{ key: "dueDate", dir: "asc" }}
419
- emptyState={<p className="text-sm text-muted-foreground">No compliance items.</p>}
420
- boardGroupByColumnOptions={[...COMPLIANCE_BOARD_GROUP_OPTIONS]}
421
- listAriaLabel="Compliance items"
422
- listEmptyState="No compliance items match your filters."
423
- renderListRow={row => (
424
- <ListPageBoardCard
425
- layout="row"
426
- rowContainerClassName="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"
427
- rowEnd={
428
- <div className="flex shrink-0 items-center gap-2">
429
- <ListHubStatusBadge
430
- surface="board"
431
- label={COMPLIANCE_STATUS_LABEL[row.status]}
432
- tintClassName={COMPLIANCE_STATUS_BADGE_CLASS[row.status]}
433
- icon={COMPLIANCE_STATUS_ICON[row.status]}
434
- />
435
- <i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden="true" />
436
- </div>
437
- }
438
- >
439
- <div className="space-y-0.5">
440
- <p className="text-sm font-semibold text-foreground">{row.title}</p>
441
- <p className="text-xs text-muted-foreground">
442
- {row.category} · Due {row.dueDate}
443
- </p>
444
- <p className="text-xs text-muted-foreground">Owner: {row.owner}</p>
445
- </div>
446
- </ListPageBoardCard>
447
- )}
448
- bulkActionsSlot={selected => {
449
- if (selected.size === 0) return null
450
- return (
451
- <>
452
- <span className="sr-only">{selected.size} selected</span>
453
- <Tip label="Export selection (demo)">
454
- <Button size="sm" variant="outline" type="button">
455
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
456
- Export
457
- </Button>
458
- </Tip>
459
- </>
460
- )
461
- }}
462
- renderers={renderers}
463
- handleRef={ref}
464
- />
465
- )
466
- })
467
-
468
- ComplianceTable.displayName = "ComplianceTable"