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