@exxatdesignux/ui 0.2.17 → 0.2.19

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 (162) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/consumer-extras/AGENTS.md +76 -0
  3. package/consumer-extras/README.md +5 -1
  4. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
  5. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
  9. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
  10. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  11. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  12. package/consumer-extras/patterns/data-views-pattern.md +42 -3
  13. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  14. package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
  15. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
  16. package/package.json +2 -1
  17. package/src/components/ui/button-group.tsx +81 -0
  18. package/src/components/ui/button.tsx +4 -4
  19. package/src/components/ui/sidebar.tsx +2 -2
  20. package/src/globals.css +7 -1807
  21. package/src/theme.css +10 -1126
  22. package/src/tokens/README.md +15 -0
  23. package/src/tokens/base.css +337 -0
  24. package/src/tokens/high-contrast.css +1195 -0
  25. package/src/tokens/layers.css +224 -0
  26. package/src/tokens/tailwind-bridge.css +118 -0
  27. package/src/tokens/themes.css +201 -0
  28. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
  29. package/template/AGENTS.md +66 -21
  30. package/template/app/(app)/dashboard/loading.tsx +3 -15
  31. package/template/app/(app)/dashboard/page.tsx +2 -14
  32. package/template/app/(app)/data-list/layout.tsx +43 -0
  33. package/template/app/(app)/data-list/page.tsx +2 -2
  34. package/template/app/(app)/error.tsx +22 -6
  35. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  36. package/template/app/(app)/examples/page.tsx +1 -0
  37. package/template/app/(app)/layout.tsx +13 -6
  38. package/template/app/(app)/loading.tsx +1 -18
  39. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  40. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  41. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  42. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  43. package/template/app/(app)/question-bank/page.tsx +2 -1
  44. package/template/app/(app)/settings/page.tsx +4 -5
  45. package/template/app/global-error.tsx +63 -0
  46. package/template/app/globals.css +7 -1934
  47. package/template/app/layout.tsx +2 -0
  48. package/template/components/app-route-loading.tsx +14 -0
  49. package/template/components/app-sidebar.tsx +71 -55
  50. package/template/components/data-table/index.tsx +31 -67
  51. package/template/components/data-table/use-table-state.ts +33 -6
  52. package/template/components/data-views/index.ts +37 -9
  53. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  54. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  55. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  56. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  57. package/template/components/dev-chunk-load-recovery.tsx +41 -0
  58. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  59. package/template/components/exxat-product-logo.tsx +2 -6
  60. package/template/components/key-metrics.tsx +54 -22
  61. package/template/components/list-hub-board-view.tsx +68 -0
  62. package/template/components/list-hub-client.tsx +186 -0
  63. package/template/components/list-hub-list-view.tsx +36 -0
  64. package/template/components/list-hub-panel-activator.tsx +8 -0
  65. package/template/components/list-hub-secondary-nav.tsx +121 -0
  66. package/template/components/list-hub-table.tsx +336 -0
  67. package/template/components/new-question-composer.tsx +6 -24
  68. package/template/components/product-switcher.tsx +5 -5
  69. package/template/components/product-wordmark.tsx +4 -7
  70. package/template/components/question-bank-client.tsx +4 -1
  71. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  72. package/template/components/question-bank-hub-client.tsx +2 -5
  73. package/template/components/question-bank-table.tsx +155 -509
  74. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  75. package/template/components/secondary-panel.tsx +4 -44
  76. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  77. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  78. package/template/components/secondary-panels/registry.tsx +15 -0
  79. package/template/components/settings-appearance-card.tsx +3 -2
  80. package/template/components/settings-client.tsx +59 -15
  81. package/template/components/settings-form-row.tsx +9 -4
  82. package/template/components/sidebar-shell.tsx +2 -1
  83. package/template/components/table-properties/drawer-button.tsx +51 -20
  84. package/template/components/table-properties/drawer.tsx +81 -17
  85. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  86. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  87. package/template/components/templates/list-page.tsx +40 -13
  88. package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
  89. package/template/components/templates/page-loading-shell.tsx +262 -0
  90. package/template/components/ui/button-group.tsx +1 -0
  91. package/template/contexts/product-context.tsx +21 -2
  92. package/template/docs/consumer-app-pattern.md +39 -0
  93. package/template/docs/data-views-pattern.md +42 -3
  94. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  95. package/template/docs/focused-workflow-page-pattern.md +84 -0
  96. package/template/docs/kpi-flat-band-pattern.md +57 -0
  97. package/template/docs/kpi-strip-max-four-pattern.md +1 -0
  98. package/template/docs/shell-surface-elevation-pattern.md +54 -0
  99. package/template/lib/chunk-load-error.ts +13 -0
  100. package/template/lib/command-menu-search-data.ts +11 -27
  101. package/template/lib/conditional-rule-match.ts +87 -22
  102. package/template/lib/data-list-display-options.ts +16 -2
  103. package/template/lib/data-list-view-registry.ts +104 -0
  104. package/template/lib/data-list-view-surface.ts +15 -1
  105. package/template/lib/data-list-view.ts +16 -1
  106. package/template/lib/data-view-dashboard-storage.ts +38 -35
  107. package/template/lib/hub-connected-view-renderers.ts +58 -0
  108. package/template/lib/list-hub-nav.ts +121 -0
  109. package/template/lib/list-hub-supported-views.ts +10 -0
  110. package/template/lib/list-page-table-properties.ts +3 -7
  111. package/template/lib/list-status-badges.ts +4 -97
  112. package/template/lib/mock/list-hub-directory.ts +27 -0
  113. package/template/lib/mock/list-hub-kpi.ts +27 -0
  114. package/template/lib/mock/navigation.tsx +1 -0
  115. package/template/lib/page-loading-variant.ts +40 -0
  116. package/template/lib/question-bank-supported-views.ts +13 -0
  117. package/template/lib/sidebar-state-cookie.ts +9 -0
  118. package/template/lib/table-state-lifecycle.ts +60 -13
  119. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  120. package/template/app/(app)/data-list/new/page.tsx +0 -34
  121. package/template/components/compliance-board-view.tsx +0 -142
  122. package/template/components/compliance-client.tsx +0 -92
  123. package/template/components/compliance-list-view.tsx +0 -54
  124. package/template/components/compliance-page-header.tsx +0 -89
  125. package/template/components/compliance-table.tsx +0 -632
  126. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  127. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  128. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  129. package/template/components/new-placement-back-btn.tsx +0 -28
  130. package/template/components/new-placement-form.tsx +0 -1068
  131. package/template/components/placement-board-card.tsx +0 -262
  132. package/template/components/placement-detail.tsx +0 -438
  133. package/template/components/placements-board-view.tsx +0 -404
  134. package/template/components/placements-client.tsx +0 -252
  135. package/template/components/placements-list-view.tsx +0 -171
  136. package/template/components/placements-page-header.tsx +0 -166
  137. package/template/components/placements-table-cells.test.tsx +0 -22
  138. package/template/components/placements-table-cells.tsx +0 -173
  139. package/template/components/placements-table-columns.tsx +0 -640
  140. package/template/components/placements-table.tsx +0 -1675
  141. package/template/components/rotations-empty-state.tsx +0 -50
  142. package/template/components/rotations-panel-activator.tsx +0 -8
  143. package/template/components/sites-all-client.tsx +0 -154
  144. package/template/components/sites-board-view.tsx +0 -67
  145. package/template/components/sites-list-view.tsx +0 -42
  146. package/template/components/sites-table.tsx +0 -402
  147. package/template/components/team-board-view.tsx +0 -122
  148. package/template/components/team-client.tsx +0 -100
  149. package/template/components/team-list-view.tsx +0 -59
  150. package/template/components/team-page-header.tsx +0 -92
  151. package/template/components/team-table.tsx +0 -714
  152. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  153. package/template/lib/mock/compliance-kpi.ts +0 -61
  154. package/template/lib/mock/compliance.ts +0 -146
  155. package/template/lib/mock/placements-kpi.ts +0 -134
  156. package/template/lib/mock/placements.ts +0 -183
  157. package/template/lib/mock/sites-directory.ts +0 -16
  158. package/template/lib/mock/sites-kpi.ts +0 -25
  159. package/template/lib/mock/team-kpi.ts +0 -60
  160. package/template/lib/mock/team.ts +0 -118
  161. package/template/lib/placement-board-card-layout.ts +0 -79
  162. package/template/lib/placement-lifecycle.ts +0 -5
@@ -1,632 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Compliance obligations — DataTable + TablePropertiesDrawer + list/board/dashboard.
5
- */
6
-
7
- import * as React from "react"
8
- import {
9
- COMPLIANCE_STATUS_BADGE_CLASS,
10
- COMPLIANCE_STATUS_ICON,
11
- COMPLIANCE_STATUS_LABEL,
12
- } from "@/lib/list-status-badges"
13
- import type { ComplianceItem } from "@/lib/mock/compliance"
14
- import { DataTable, DataTableToolbar } from "@/components/data-table"
15
- import {
16
- ComplianceDashboardChartsSection,
17
- ALL_COMPLIANCE_DASHBOARD_CARDS,
18
- DEFAULT_COMPLIANCE_CHART_TYPES,
19
- DEFAULT_COMPLIANCE_SPANS,
20
- loadComplianceDashboardLayout,
21
- mergeComplianceDashboardLayout,
22
- saveComplianceDashboardLayout,
23
- } from "@/components/data-view-dashboard-charts-compliance"
24
- import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
25
- import type { ChartType, DashboardLayout } from "@/lib/data-view-dashboard-placements-layout"
26
- import { ComplianceListView } from "@/components/compliance-list-view"
27
- import { ComplianceBoardView, COMPLIANCE_BOARD_GROUP_OPTIONS } from "@/components/compliance-board-view"
28
- import { complianceKpiInsight, complianceKpiMetrics } from "@/lib/mock/compliance-kpi"
29
- import type { DataListViewType } from "@/lib/data-list-view"
30
- import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
31
- import type { ColumnDef } from "@/components/data-table/types"
32
- import { useTableState } from "@/components/data-table/use-table-state"
33
- import { useTableStateLifecycle } from "@/lib/table-state-lifecycle"
34
- import { TablePropertiesDrawerButton } from "@/components/table-properties"
35
- import type { ConditionalRule, FilterFieldDef, FilterOperator } from "@/components/table-properties/types"
36
- import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
37
- import { Button } from "@/components/ui/button"
38
- import {
39
- DropdownMenu,
40
- DropdownMenuContent,
41
- DropdownMenuItem,
42
- DropdownMenuTrigger,
43
- } from "@/components/ui/dropdown-menu"
44
- import { Tip } from "@/components/ui/tip"
45
- import { CoachMark } from "@/components/ui/coach-mark"
46
- import { useCoachMark } from "@/hooks/use-coach-mark"
47
- import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
48
- import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
49
- import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
50
- import {
51
- DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
52
- type DataListDisplayOptions,
53
- } from "@/lib/data-list-display-options"
54
-
55
- function uniqueCategories(items: ComplianceItem[]) {
56
- return [...new Set(items.map(i => i.category))].sort().map(c => ({ value: c, label: c }))
57
- }
58
-
59
- const STATUS_FILTER_OPTS = [
60
- { value: "compliant", label: COMPLIANCE_STATUS_LABEL.compliant },
61
- { value: "due_soon", label: COMPLIANCE_STATUS_LABEL.due_soon },
62
- { value: "overdue", label: COMPLIANCE_STATUS_LABEL.overdue },
63
- { value: "pending", label: COMPLIANCE_STATUS_LABEL.pending },
64
- ]
65
-
66
- function columnToFilterFieldDef(c: ColumnDef<ComplianceItem>): FilterFieldDef | null {
67
- if (!c.filter) return null
68
- const f = c.filter
69
- const defaultOps: FilterOperator[] =
70
- f.type === "select" || f.type === "date"
71
- ? ["is", "is_not"]
72
- : ["contains", "not_contains"]
73
- return {
74
- key: c.key,
75
- label: c.label,
76
- icon: f.icon ?? "fa-filter",
77
- type: f.type,
78
- operators: (f.operators ?? defaultOps) as FilterOperator[],
79
- options: f.options,
80
- ...(f.textMask ? { textMask: f.textMask } : {}),
81
- }
82
- }
83
-
84
- function columnsToFilterFields(cols: ColumnDef<ComplianceItem>[]) {
85
- return cols.map(columnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
86
- }
87
-
88
- function buildComplianceColumns(items: ComplianceItem[]): ColumnDef<ComplianceItem>[] {
89
- const catOpts = uniqueCategories(items)
90
-
91
- const COLUMN_SELECT: ColumnDef<ComplianceItem> = {
92
- key: "select",
93
- label: "",
94
- width: 40,
95
- minWidth: 40,
96
- defaultPin: "left",
97
- lockPin: true,
98
- }
99
-
100
- const cols: ColumnDef<ComplianceItem>[] = [
101
- COLUMN_SELECT,
102
- {
103
- key: "title",
104
- label: "Obligation",
105
- width: 280,
106
- minWidth: 140,
107
- sortable: true,
108
- sortKey: "title",
109
- defaultPin: "left",
110
- filter: {
111
- type: "text",
112
- icon: "fa-file-lines",
113
- operators: ["contains", "not_contains"],
114
- },
115
- cell: row => (
116
- <span className="line-clamp-2 text-sm font-medium text-foreground">{row.title}</span>
117
- ),
118
- },
119
- {
120
- key: "category",
121
- label: "Category",
122
- width: 160,
123
- minWidth: 120,
124
- sortable: true,
125
- sortKey: "category",
126
- filter: {
127
- type: "select",
128
- icon: "fa-layer-group",
129
- operators: ["is", "is_not"],
130
- options: catOpts,
131
- },
132
- cell: row => <span className="text-sm text-foreground/90">{row.category}</span>,
133
- },
134
- {
135
- key: "status",
136
- label: "Status",
137
- width: 120,
138
- minWidth: 100,
139
- sortable: true,
140
- sortKey: "status",
141
- filter: {
142
- type: "select",
143
- icon: "fa-circle-dot",
144
- operators: ["is", "is_not"],
145
- options: STATUS_FILTER_OPTS,
146
- },
147
- cell: row => (
148
- <ListHubStatusBadge
149
- label={COMPLIANCE_STATUS_LABEL[row.status]}
150
- tintClassName={COMPLIANCE_STATUS_BADGE_CLASS[row.status]}
151
- icon={COMPLIANCE_STATUS_ICON[row.status]}
152
- />
153
- ),
154
- },
155
- {
156
- key: "dueDate",
157
- label: "Due",
158
- width: 120,
159
- minWidth: 100,
160
- sortable: true,
161
- sortKey: "dueDate",
162
- filter: { type: "date", icon: "fa-calendar-days", operators: ["is", "is_not"] },
163
- cell: row => (
164
- <span className="text-sm tabular-nums text-foreground/90 whitespace-nowrap">{row.dueDate}</span>
165
- ),
166
- },
167
- {
168
- key: "owner",
169
- label: "Owner",
170
- width: 160,
171
- minWidth: 120,
172
- sortable: true,
173
- sortKey: "owner",
174
- filter: {
175
- type: "text",
176
- icon: "fa-user",
177
- operators: ["contains", "not_contains"],
178
- },
179
- cell: row => <span className="text-sm text-foreground/90">{row.owner}</span>,
180
- },
181
- {
182
- key: "lastReviewed",
183
- label: "Last reviewed",
184
- width: 120,
185
- minWidth: 100,
186
- sortable: true,
187
- sortKey: "lastReviewed",
188
- filter: { type: "date", icon: "fa-calendar-check", operators: ["is", "is_not"] },
189
- cell: row => (
190
- <span className="text-sm tabular-nums text-muted-foreground whitespace-nowrap">{row.lastReviewed}</span>
191
- ),
192
- },
193
- {
194
- key: "actions",
195
- label: "",
196
- width: 48,
197
- minWidth: 48,
198
- defaultPin: "right",
199
- lockPin: true,
200
- cell: row => (
201
- <div className="flex items-center justify-center">
202
- <DropdownMenu>
203
- <DropdownMenuTrigger asChild>
204
- <Button size="icon-sm" variant="ghost" aria-label={`Actions for ${row.title}`}>
205
- <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
206
- </Button>
207
- </DropdownMenuTrigger>
208
- <DropdownMenuContent align="end">
209
- <DropdownMenuItem disabled>
210
- <i className="fa-light fa-eye" aria-hidden="true" />
211
- View details
212
- </DropdownMenuItem>
213
- <DropdownMenuItem disabled>
214
- <i className="fa-light fa-user-check" aria-hidden="true" />
215
- Assign owner
216
- </DropdownMenuItem>
217
- </DropdownMenuContent>
218
- </DropdownMenu>
219
- </div>
220
- ),
221
- },
222
- ]
223
-
224
- return cols
225
- }
226
-
227
-
228
- export type ComplianceTableHandle = OpenTablePropertiesHandle
229
-
230
- export const ComplianceTable = React.forwardRef<
231
- ComplianceTableHandle,
232
- { items: ComplianceItem[]; view?: DataListViewType; onViewChange?: (v: DataListViewType) => void }
233
- >(function ComplianceTable({ items, view = "table", onViewChange }, ref) {
234
- const columns = React.useMemo(() => buildComplianceColumns(items), [items])
235
- const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
236
- const fieldDefinitionsForDrawer = React.useMemo(
237
- () =>
238
- columns
239
- .filter(c => c.key !== "select" && c.key !== "actions")
240
- .map(c => ({ key: c.key, label: c.label, sortable: !!(c.sortable && (c.sortKey ?? c.key)) })),
241
- [columns],
242
- )
243
-
244
- const resolveColumnLabel = React.useCallback(
245
- (key: string) => columns.find(c => c.key === key)?.label ?? key,
246
- [columns],
247
- )
248
-
249
- const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
250
- const patchDisplay = React.useCallback((patch: Partial<DataListDisplayOptions>) => {
251
- setDisplayOptions(prev => ({ ...prev, ...patch }))
252
- }, [])
253
-
254
- const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
255
- const addConditionalRule = React.useCallback((rule: Omit<ConditionalRule, "id">) => {
256
- setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
257
- }, [])
258
- const removeConditionalRule = React.useCallback((id: string) => {
259
- setConditionalRules(prev => prev.filter(r => r.id !== id))
260
- }, [])
261
- const updateConditionalRule = React.useCallback((id: string, patch: Partial<ConditionalRule>) => {
262
- setConditionalRules(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r))
263
- }, [])
264
-
265
- const tableState = useTableState(items, columns, { key: "dueDate", dir: "asc" })
266
-
267
- // Persist this hub's table lifecycle (sort / search / filters / column
268
- // visibility / etc.) to localStorage. See `lib/table-state-lifecycle`.
269
- const lifecycleColumnKeys = React.useMemo(
270
- () => new Set(columns.map(c => c.key)),
271
- [columns],
272
- )
273
- useTableStateLifecycle({
274
- namespace: "compliance",
275
- tabId: "main",
276
- tableState,
277
- columnKeys: lifecycleColumnKeys,
278
- extras: { conditionalRules },
279
- onLoadExtras: e => {
280
- if (e && Array.isArray(e.conditionalRules)) {
281
- setConditionalRules(e.conditionalRules as ConditionalRule[])
282
- }
283
- },
284
- })
285
-
286
- const dashboardKpi = React.useMemo(
287
- () => ({
288
- metrics: complianceKpiMetrics(tableState.rows as ComplianceItem[]),
289
- insight: complianceKpiInsight(tableState.rows as ComplianceItem[]),
290
- }),
291
- [tableState.rows],
292
- )
293
-
294
- const [visibleComplianceCards, setVisibleComplianceCards] = React.useState<string[]>(() =>
295
- ALL_COMPLIANCE_DASHBOARD_CARDS.map(c => c.id),
296
- )
297
- const [complianceCardOrder, setComplianceCardOrder] = React.useState<string[]>(() =>
298
- ALL_COMPLIANCE_DASHBOARD_CARDS.map(c => c.id),
299
- )
300
- const [complianceCardSpans, setComplianceCardSpans] = React.useState<Record<string, 1 | 2>>(() => ({
301
- ...DEFAULT_COMPLIANCE_SPANS,
302
- }))
303
- const [complianceCardChartTypes, setComplianceCardChartTypes] = React.useState<Record<string, ChartType>>(() => ({
304
- ...DEFAULT_COMPLIANCE_CHART_TYPES,
305
- }))
306
- const [complianceKeyMetricsKpiCount, setComplianceKeyMetricsKpiCount] = React.useState<number>(
307
- KEY_METRICS_KPI_COUNT_DEFAULT,
308
- )
309
- const [complianceDashboardLayoutEdit, setComplianceDashboardLayoutEdit] = React.useState(false)
310
- const complianceDashboardLayoutHydrated = React.useRef(false)
311
- const complianceDashboardLayoutEditBaselineRef = React.useRef<DashboardLayout | null>(null)
312
-
313
- React.useEffect(() => {
314
- const saved = loadComplianceDashboardLayout()
315
- const m = mergeComplianceDashboardLayout(saved)
316
- setVisibleComplianceCards(m.visible)
317
- setComplianceCardOrder(m.order)
318
- setComplianceCardSpans(m.spans ?? { ...DEFAULT_COMPLIANCE_SPANS })
319
- setComplianceCardChartTypes(m.chartTypes ?? { ...DEFAULT_COMPLIANCE_CHART_TYPES })
320
- setComplianceKeyMetricsKpiCount(m.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
321
- complianceDashboardLayoutHydrated.current = true
322
- }, [])
323
-
324
- React.useEffect(() => {
325
- if (!complianceDashboardLayoutHydrated.current) return
326
- saveComplianceDashboardLayout({
327
- visible: visibleComplianceCards,
328
- order: complianceCardOrder,
329
- spans: complianceCardSpans,
330
- chartTypes: complianceCardChartTypes,
331
- keyMetricsKpiCount: complianceKeyMetricsKpiCount,
332
- })
333
- }, [visibleComplianceCards, complianceCardOrder, complianceCardSpans, complianceCardChartTypes, complianceKeyMetricsKpiCount])
334
-
335
- const handleComplianceVisibleChange = React.useCallback((v: string[]) => {
336
- setVisibleComplianceCards(v)
337
- }, [])
338
-
339
- const handleComplianceOrderChange = React.useCallback((o: string[]) => {
340
- setComplianceCardOrder(o)
341
- }, [])
342
-
343
- const handleComplianceSpanChange = React.useCallback((id: string, span: 1 | 2) => {
344
- setComplianceCardSpans(prev => ({ ...prev, [id]: span }))
345
- }, [])
346
-
347
- const handleComplianceChartTypeChange = React.useCallback((id: string, t: ChartType) => {
348
- setComplianceCardChartTypes(prev => ({ ...prev, [id]: t }))
349
- }, [])
350
-
351
- const handleResetComplianceDashboardLayout = React.useCallback(() => {
352
- setVisibleComplianceCards(ALL_COMPLIANCE_DASHBOARD_CARDS.map(c => c.id))
353
- setComplianceCardOrder(ALL_COMPLIANCE_DASHBOARD_CARDS.map(c => c.id))
354
- setComplianceCardSpans({ ...DEFAULT_COMPLIANCE_SPANS })
355
- setComplianceCardChartTypes({ ...DEFAULT_COMPLIANCE_CHART_TYPES })
356
- setComplianceKeyMetricsKpiCount(KEY_METRICS_KPI_COUNT_DEFAULT)
357
- }, [])
358
-
359
- const handleComplianceDashboardLayoutEditStart = React.useCallback(() => {
360
- complianceDashboardLayoutEditBaselineRef.current = {
361
- visible: [...visibleComplianceCards],
362
- order: [...complianceCardOrder],
363
- spans: { ...complianceCardSpans },
364
- chartTypes: { ...complianceCardChartTypes },
365
- keyMetricsKpiCount: complianceKeyMetricsKpiCount,
366
- }
367
- setComplianceDashboardLayoutEdit(true)
368
- }, [visibleComplianceCards, complianceCardOrder, complianceCardSpans, complianceCardChartTypes, complianceKeyMetricsKpiCount])
369
-
370
- const handleComplianceDashboardLayoutEditDone = React.useCallback(() => {
371
- setComplianceDashboardLayoutEdit(false)
372
- }, [])
373
-
374
- const handleComplianceDashboardLayoutEditCancel = React.useCallback(() => {
375
- const b = complianceDashboardLayoutEditBaselineRef.current
376
- if (b) {
377
- setVisibleComplianceCards(b.visible)
378
- setComplianceCardOrder(b.order)
379
- setComplianceCardSpans(b.spans ?? { ...DEFAULT_COMPLIANCE_SPANS })
380
- setComplianceCardChartTypes(b.chartTypes ?? { ...DEFAULT_COMPLIANCE_CHART_TYPES })
381
- setComplianceKeyMetricsKpiCount(b.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
382
- }
383
- setComplianceDashboardLayoutEdit(false)
384
- }, [])
385
-
386
- const dashboardCustomizeCoach = useCoachMark({
387
- flowId: "compliance-dashboard-customize",
388
- steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
389
- delay: 700,
390
- enabled: view === "dashboard",
391
- })
392
-
393
- React.useImperativeHandle(ref, () => ({
394
- openPropertiesDrawer: () => {
395
- tableState.setSheetOpen(true)
396
- },
397
- // `tableState` is freshly returned each render by useTableState; depending on
398
- // it would re-create the imperative handle on every render. Only the React
399
- // setter is needed (and is referentially stable).
400
- // eslint-disable-next-line react-hooks/exhaustive-deps
401
- }), [tableState.setSheetOpen])
402
-
403
- const complianceBoardGroupKey = COMPLIANCE_BOARD_GROUP_OPTIONS.some(
404
- o => o.key === displayOptions.boardGroupByColumnKey,
405
- )
406
- ? displayOptions.boardGroupByColumnKey
407
- : "status"
408
-
409
- // Build panel groups from categories
410
- const panelGroupsBuilder = (rows: ComplianceItem[]): FinderGroup[] => {
411
- // Group items by category
412
- const itemsByCategory = new Map<string, ComplianceItem[]>()
413
- for (const item of rows) {
414
- const category = item.category
415
- if (!itemsByCategory.has(category)) {
416
- itemsByCategory.set(category, [])
417
- }
418
- itemsByCategory.get(category)!.push(item)
419
- }
420
-
421
- // Build groups from categories, sorted alphabetically
422
- const groups: FinderGroup[] = []
423
- const categories = Array.from(itemsByCategory.keys()).sort()
424
-
425
- for (const category of categories) {
426
- const categoryItems = itemsByCategory.get(category) ?? []
427
- groups.push({
428
- id: category,
429
- label: category,
430
- icon: "fa-folder",
431
- count: categoryItems.length,
432
- })
433
- }
434
-
435
- return groups
436
- }
437
-
438
- const panelRenderListRow = (row: ComplianceItem, _isSelected: boolean) => (
439
- <div className="flex-1 min-w-0">
440
- <p className="text-sm font-medium text-foreground truncate">{row.title}</p>
441
- <p className="text-xs text-muted-foreground mt-1">{row.category}</p>
442
- </div>
443
- )
444
-
445
- const panelRenderDetail = (row: ComplianceItem) => (
446
- <div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4">
447
- <div>
448
- <h3 className="text-sm font-semibold text-foreground mb-2">Obligation</h3>
449
- <p className="text-sm text-foreground">{row.title}</p>
450
- </div>
451
- <div className="flex flex-col gap-2">
452
- <div>
453
- <span className="text-xs font-medium text-muted-foreground">Category</span>
454
- <p className="text-sm text-foreground">{row.category}</p>
455
- </div>
456
- <div>
457
- <span className="text-xs font-medium text-muted-foreground">Status</span>
458
- <p className="text-sm text-foreground">{COMPLIANCE_STATUS_LABEL[row.status]}</p>
459
- </div>
460
- </div>
461
- </div>
462
- )
463
-
464
- const drawerToolbarProps = {
465
- totalRows: items.length,
466
- filterFields,
467
- fieldDefinitions: fieldDefinitionsForDrawer,
468
- resolveColumnLabel,
469
- displayOptions,
470
- onDisplayOptionsChange: patchDisplay,
471
- conditionalRules,
472
- onAddConditionalRule: addConditionalRule,
473
- onRemoveConditionalRule: removeConditionalRule,
474
- onUpdateConditionalRule: updateConditionalRule,
475
- currentView: view,
476
- onViewChange,
477
- lifecycleTabLabel: "Compliance",
478
- boardGroupByColumnOptions: [...COMPLIANCE_BOARD_GROUP_OPTIONS],
479
- }
480
-
481
- const tableProps = {
482
- data: items,
483
- columns,
484
- getRowId: (row: ComplianceItem) => row.id,
485
- getRowSelectionLabel: (row: ComplianceItem) => row.title,
486
- selectable: true,
487
- searchable: displayOptions.showToolbarSearch,
488
- showColumnHeaders: displayOptions.showColumnLabels,
489
- groupable: true,
490
- defaultSort: { key: "dueDate", dir: "asc" as const },
491
- emptyState: <p className="text-sm text-muted-foreground">No compliance items.</p>,
492
- conditionalRules,
493
- state: tableState,
494
- toolbarSlot: (s: ReturnType<typeof useTableState<ComplianceItem>>) => (
495
- <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />
496
- ),
497
- bulkActionsSlot: (selected: Set<string | number>) => {
498
- const n = selected.size
499
- if (n === 0) return null
500
- return (
501
- <>
502
- <span className="sr-only">{n} selected</span>
503
- <Tip label="Export selection (demo)">
504
- <Button size="sm" variant="outline" type="button">
505
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
506
- Export
507
- </Button>
508
- </Tip>
509
- </>
510
- )
511
- },
512
- }
513
-
514
- if (view === "table") {
515
- return (
516
- <div className="pb-6">
517
- <DataTable<ComplianceItem> {...tableProps} />
518
- </div>
519
- )
520
- }
521
-
522
- const sharedToolbar = (
523
- <DataTableToolbar
524
- state={tableState}
525
- columns={columns}
526
- searchable={displayOptions.showToolbarSearch}
527
- searchAriaLabel="Search compliance obligations"
528
- toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
529
- />
530
- )
531
-
532
- if (view === "list") {
533
- return (
534
- <div className="flex min-h-0 flex-1 flex-col">
535
- {sharedToolbar}
536
- <ComplianceListView
537
- rows={tableState.rows as ComplianceItem[]}
538
- onRowActivate={row => tableState.toggleRow(row.id)}
539
- />
540
- </div>
541
- )
542
- }
543
-
544
- if (view === "board") {
545
- return (
546
- <div className="flex min-h-0 flex-1 flex-col">
547
- {sharedToolbar}
548
- <ComplianceBoardView
549
- rows={tableState.rows as ComplianceItem[]}
550
- groupByColumnKey={complianceBoardGroupKey}
551
- onRowActivate={row => tableState.toggleRow(row.id)}
552
- />
553
- </div>
554
- )
555
- }
556
-
557
- if (view === "panel") {
558
- return (
559
- <div className="flex min-h-0 flex-1 flex-col">
560
- {sharedToolbar}
561
- <ListPageSplitHubChrome aria-label="Compliance obligations panel view">
562
- <FinderPanelView<ComplianceItem>
563
- embedded
564
- groupsColumnTitle="Category"
565
- groups={panelGroupsBuilder(tableState.rows)}
566
- rows={tableState.rows}
567
- getRowId={(row) => row.id}
568
- getRowGroupId={(row) => row.category}
569
- autoSaveId="compliance-panel-view"
570
- renderListRow={panelRenderListRow}
571
- renderDetail={panelRenderDetail}
572
- emptyList={<p className="text-sm text-muted-foreground">No obligations found.</p>}
573
- />
574
- </ListPageSplitHubChrome>
575
- </div>
576
- )
577
- }
578
-
579
- return (
580
- <div className="flex min-h-0 flex-1 flex-col">
581
- <CoachMark state={dashboardCustomizeCoach} />
582
- {!complianceDashboardLayoutEdit ? (
583
- <DataTableToolbar
584
- state={tableState}
585
- columns={columns}
586
- searchable={displayOptions.showToolbarSearch}
587
- searchAriaLabel="Search compliance obligations"
588
- toolbarSlot={s => (
589
- <TablePropertiesDrawerButton
590
- {...drawerToolbarProps}
591
- state={s}
592
- extraActions={
593
- <Tip side="bottom" label="Edit dashboard layout on canvas">
594
- <Button
595
- type="button"
596
- variant="ghost"
597
- size="icon-sm"
598
- aria-label="Edit dashboard layout"
599
- onClick={handleComplianceDashboardLayoutEditStart}
600
- className="text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover"
601
- >
602
- <i className="fa-light fa-pen-ruler text-[13px]" aria-hidden="true" />
603
- </Button>
604
- </Tip>
605
- }
606
- />
607
- )}
608
- />
609
- ) : null}
610
- <ComplianceDashboardChartsSection
611
- rows={tableState.rows as ComplianceItem[]}
612
- keyMetrics={dashboardKpi}
613
- visibleCards={visibleComplianceCards}
614
- cardOrder={complianceCardOrder}
615
- cardSpans={complianceCardSpans}
616
- cardChartTypes={complianceCardChartTypes}
617
- keyMetricsKpiCount={complianceKeyMetricsKpiCount}
618
- layoutEditMode={complianceDashboardLayoutEdit}
619
- onVisibleChange={handleComplianceVisibleChange}
620
- onOrderChange={handleComplianceOrderChange}
621
- onSpanChange={handleComplianceSpanChange}
622
- onChartTypeChange={handleComplianceChartTypeChange}
623
- onKeyMetricsKpiCountChange={setComplianceKeyMetricsKpiCount}
624
- onResetLayout={handleResetComplianceDashboardLayout}
625
- onLayoutEditDone={handleComplianceDashboardLayoutEditDone}
626
- onLayoutEditCancel={handleComplianceDashboardLayoutEditCancel}
627
- />
628
- </div>
629
- )
630
- })
631
-
632
- ComplianceTable.displayName = "ComplianceTable"