@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
@@ -26,7 +26,7 @@ import type { RowHeight } from "@/lib/row-height"
26
26
  import type { DataListDisplayOptions } from "@/lib/data-list-display-options"
27
27
  import type { ActiveFilter, ConditionalRule, SortRule } from "@/components/table-properties/types"
28
28
  import type { ViewTab } from "@/components/templates/list-page"
29
- import type { DataListViewType } from "@/lib/data-list-view"
29
+ import { DATA_LIST_VIEW_TILES, type DataListViewType } from "@/lib/data-list-view"
30
30
 
31
31
  // ─────────────────────────────────────────────────────────────────────────────
32
32
  // Storage key + debounce config
@@ -139,7 +139,7 @@ export interface TableStatePersistSlice {
139
139
  // Parsers + validators
140
140
  // ─────────────────────────────────────────────────────────────────────────────
141
141
 
142
- const VIEW_TYPES: DataListViewType[] = ["table", "list", "board", "dashboard"]
142
+ const VIEW_TYPES: DataListViewType[] = DATA_LIST_VIEW_TILES.map(t => t.value)
143
143
 
144
144
  function isViewType(v: unknown): v is DataListViewType {
145
145
  return typeof v === "string" && (VIEW_TYPES as string[]).includes(v)
@@ -229,7 +229,19 @@ function filterRecordKeys<T extends Record<string, unknown>>(obj: T, keys: Set<s
229
229
  return out
230
230
  }
231
231
 
232
- export function applyLifecyclePersisted(
232
+ function sanitizeActiveFilters(
233
+ filters: ActiveFilter[],
234
+ columnKeys: Set<string>,
235
+ ): ActiveFilter[] {
236
+ return filters.filter(f => columnKeys.has(f.fieldKey))
237
+ }
238
+
239
+ function sanitizeSortRules(rules: SortRule[], columnKeys: Set<string>): SortRule[] {
240
+ return rules.filter(r => columnKeys.has(r.fieldKey))
241
+ }
242
+
243
+ /** Column layout only — keeps in-memory search / filters when the column set changes. */
244
+ export function applyLifecycleColumnLayout(
233
245
  ts: TableStatePersistSlice,
234
246
  p: PersistedLifecycleV1,
235
247
  columnKeys: Set<string>,
@@ -241,11 +253,6 @@ export function applyLifecyclePersisted(
241
253
  const colWrap = filterRecordKeys(p.colWrap, columnKeys) as Record<string, boolean>
242
254
  const colMenuSearch = filterRecordKeys(p.colMenuSearch, columnKeys) as Record<string, string>
243
255
 
244
- ts.setSortRules(p.sortRules)
245
- ts.setSearch(p.search)
246
- ts.setActiveFilters(p.activeFilters)
247
- ts.setFilterConnectors(p.filterConnectors)
248
- ts.setGroupBy(p.groupBy)
249
256
  ts.setColOrder(colOrder)
250
257
  ts.setHiddenCols(hidden)
251
258
  ts.setColWidths(colWidths)
@@ -254,6 +261,20 @@ export function applyLifecyclePersisted(
254
261
  ts.setColMenuSearch(colMenuSearch)
255
262
  ts.setRowHeight(p.rowHeight)
256
263
  ts.setShowGridlines(p.showGridlines)
264
+ }
265
+
266
+ export function applyLifecyclePersisted(
267
+ ts: TableStatePersistSlice,
268
+ p: PersistedLifecycleV1,
269
+ columnKeys: Set<string>,
270
+ ): void {
271
+ applyLifecycleColumnLayout(ts, p, columnKeys)
272
+
273
+ ts.setSortRules(sanitizeSortRules(p.sortRules, columnKeys))
274
+ ts.setSearch(p.search)
275
+ ts.setActiveFilters(sanitizeActiveFilters(p.activeFilters, columnKeys))
276
+ ts.setFilterConnectors(p.filterConnectors)
277
+ ts.setGroupBy(p.groupBy != null && columnKeys.has(p.groupBy) ? p.groupBy : null)
257
278
  ts.setFilterBarVisible(p.filterBarVisible)
258
279
  ts.setSearchOpen(p.searchOpen)
259
280
  }
@@ -427,20 +448,46 @@ export function useTableStateLifecycle<TExtras extends Record<string, unknown> |
427
448
  onLoadExtrasRef.current = onLoadExtras
428
449
  })
429
450
 
451
+ const columnKeysFingerprint = React.useMemo(
452
+ () => [...columnKeys].sort().join("\0"),
453
+ [columnKeys],
454
+ )
455
+
456
+ const loadedScopeRef = React.useRef<string | null>(null)
457
+ const appliedColumnFingerprintRef = React.useRef<string | null>(null)
458
+
430
459
  // ── Load ────────────────────────────────────────────────────────────────
431
460
  // useLayoutEffect so the rehydrated state paints in the first frame after
432
461
  // mount instead of flashing the unhydrated defaults first.
433
462
  React.useLayoutEffect(() => {
463
+ // Wait until column defs exist — applying persisted sort/filters against an
464
+ // empty key set would sanitize everything away and look like Properties broke.
465
+ if (columnKeys.size === 0) return
466
+
467
+ const scope = `${namespace}:${tabId}`
434
468
  const raw = loadLifecycleFromStorage(namespace, tabId)
469
+
470
+ if (loadedScopeRef.current !== scope) {
471
+ loadedScopeRef.current = scope
472
+ appliedColumnFingerprintRef.current = columnKeysFingerprint
473
+ if (!raw) return
474
+ applyLifecyclePersisted(tableState, raw, columnKeys)
475
+ const e = readLifecycleExtras<Record<string, unknown>>(raw)
476
+ onLoadExtrasRef.current?.(e as TExtras | Record<string, unknown> | undefined)
477
+ return
478
+ }
479
+
480
+ if (appliedColumnFingerprintRef.current === columnKeysFingerprint) return
481
+ appliedColumnFingerprintRef.current = columnKeysFingerprint
435
482
  if (!raw) return
436
- applyLifecyclePersisted(tableState, raw, columnKeys)
437
- const e = readLifecycleExtras<Record<string, unknown>>(raw)
438
- onLoadExtrasRef.current?.(e as TExtras | Record<string, unknown> | undefined)
483
+ // Column defs changed (e.g. hub scope / dynamic filter options) — re-merge
484
+ // layout only; do not wipe in-memory filters the user set in Properties.
485
+ applyLifecycleColumnLayout(tableState, raw, columnKeys)
439
486
  // `tableState` is freshly returned each render; depending on it would
440
487
  // re-apply persisted state on every keystroke and undo edits. Depend only
441
- // on the load scope (namespace + tabId + column set).
488
+ // on the load scope (namespace + tabId + column fingerprint).
442
489
  // eslint-disable-next-line react-hooks/exhaustive-deps
443
- }, [namespace, tabId, columnKeys])
490
+ }, [namespace, tabId, columnKeysFingerprint])
444
491
 
445
492
  // ── Save ────────────────────────────────────────────────────────────────
446
493
  // Serialise + debounce on every persisted slice change. Don't depend on
@@ -1,44 +0,0 @@
1
- import Link from "next/link"
2
- import { notFound } from "next/navigation"
3
- import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
4
- import { Button } from "@/components/ui/button"
5
- import { getPlacementById } from "@/lib/mock/placements"
6
-
7
- export default async function RecordDetailPage({
8
- params,
9
- }: {
10
- params: Promise<{ id: string }>
11
- }) {
12
- const { id } = await params
13
- const row = getPlacementById(Number(id))
14
- if (!row) notFound()
15
-
16
- return (
17
- <PrimaryPageTemplate
18
- siteHeader={{
19
- title: "Record",
20
- breadcrumbs: [{ label: "List hub", href: "/data-list" }],
21
- }}
22
- maxWidthClassName="max-w-2xl"
23
- contentClassName="px-4 lg:px-6 py-6"
24
- bodyClassName="overflow-y-auto"
25
- >
26
- <p className="text-sm text-muted-foreground mb-6">
27
- Demo read-only detail — replace with your domain route and data fetch.
28
- </p>
29
- <dl className="grid gap-3 text-sm sm:grid-cols-[minmax(0,10rem)_1fr]">
30
- <dt className="text-muted-foreground">Primary label</dt>
31
- <dd className="font-medium text-foreground">{row.student}</dd>
32
- <dt className="text-muted-foreground">Status</dt>
33
- <dd>{row.status}</dd>
34
- <dt className="text-muted-foreground">Site</dt>
35
- <dd>{row.site}</dd>
36
- <dt className="text-muted-foreground">Program</dt>
37
- <dd>{row.program}</dd>
38
- </dl>
39
- <Button asChild variant="outline" className="mt-8">
40
- <Link href="/data-list">Back to list</Link>
41
- </Button>
42
- </PrimaryPageTemplate>
43
- )
44
- }
@@ -1,34 +0,0 @@
1
- import Link from "next/link"
2
- import { NewPlacementForm } from "@/components/new-placement-form"
3
- import { SidebarAutoCollapse } from "@/components/sidebar-auto-collapse"
4
- import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
5
-
6
- export default function NewRecordPage() {
7
- return (
8
- <PrimaryPageTemplate
9
- beforeSiteHeader={<SidebarAutoCollapse />}
10
- bodyClassName="overflow-y-auto"
11
- maxWidthClassName="max-w-3xl"
12
- contentClassName="px-8 pt-10 pb-32"
13
- >
14
- <Link
15
- href="/data-list"
16
- className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-interactive-hover-foreground transition-colors mb-5 group"
17
- aria-label="Back to list hub"
18
- >
19
- <i className="fa-light fa-arrow-left text-xs transition-transform group-hover:-translate-x-0.5" aria-hidden="true" />
20
- Back
21
- </Link>
22
- <h1
23
- className="text-[2.25rem] font-semibold tracking-tight leading-none text-foreground mb-2"
24
- style={{ fontFamily: "var(--font-heading)" }}
25
- >
26
- New record
27
- </h1>
28
- <p className="text-sm text-muted-foreground mb-8">
29
- Multi-step wizard shell (demo fields) — swap the form for your product flow.
30
- </p>
31
- <NewPlacementForm />
32
- </PrimaryPageTemplate>
33
- )
34
- }
@@ -1,142 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { initialsFromDisplayName } from "@/lib/initials-from-name"
5
- import {
6
- COMPLIANCE_STATUS_BADGE_CLASS,
7
- COMPLIANCE_STATUS_ICON,
8
- COMPLIANCE_STATUS_LABEL,
9
- } from "@/lib/list-status-badges"
10
- import type { ComplianceItem } from "@/lib/mock/compliance"
11
- import { BoardCardTwoLineBlock } from "@/components/data-views/board-card-primitives"
12
- import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
13
- import {
14
- ListPageBoardCard,
15
- ListPageBoardCardAvatar,
16
- ListPageBoardCardBadgeRow,
17
- ListPageBoardCardBody,
18
- ListPageBoardCardHeader,
19
- ListPageBoardCardTitleRow,
20
- } from "@/components/data-views/list-page-board-card"
21
- import {
22
- ListPageBoardTemplate,
23
- type ListPageBoardColumnDef,
24
- } from "@/components/data-views/list-page-board-template"
25
-
26
- const NEUTRAL_COUNT_BADGE = "bg-muted/90 text-foreground"
27
-
28
- const STATUS_BOARD_COLUMNS: ListPageBoardColumnDef<ComplianceItem>[] = [
29
- {
30
- id: "compliant",
31
- label: "Compliant",
32
- description: "On track",
33
- filter: r => r.status === "compliant",
34
- },
35
- {
36
- id: "due_soon",
37
- label: "Due soon",
38
- description: "Within window",
39
- filter: r => r.status === "due_soon",
40
- },
41
- {
42
- id: "overdue",
43
- label: "Overdue",
44
- description: "Action required",
45
- filter: r => r.status === "overdue",
46
- },
47
- {
48
- id: "pending",
49
- label: "Pending",
50
- description: "Not started",
51
- filter: r => r.status === "pending",
52
- },
53
- ]
54
-
55
- function categoryBoardColumns(rows: ComplianceItem[]): {
56
- columns: ListPageBoardColumnDef<ComplianceItem>[]
57
- badgeMap: Record<string, string>
58
- } {
59
- const labels = [...new Set(rows.map(r => r.category))].sort((a, b) => a.localeCompare(b))
60
- const columns: ListPageBoardColumnDef<ComplianceItem>[] = labels.map(label => ({
61
- id: `category:${label}`,
62
- label,
63
- filter: (r: ComplianceItem) => r.category === label,
64
- }))
65
- const badgeMap = Object.fromEntries(labels.map(l => [`category:${l}`, NEUTRAL_COUNT_BADGE]))
66
- return { columns, badgeMap }
67
- }
68
-
69
- function useComplianceBoardModel(rows: ComplianceItem[], groupByColumnKey: string) {
70
- return React.useMemo(() => {
71
- if (groupByColumnKey === "category") {
72
- const { columns, badgeMap } = categoryBoardColumns(rows)
73
- return { columns, badgeMap }
74
- }
75
- return {
76
- columns: STATUS_BOARD_COLUMNS,
77
- badgeMap: COMPLIANCE_STATUS_BADGE_CLASS as Record<string, string>,
78
- }
79
- }, [rows, groupByColumnKey])
80
- }
81
-
82
- function ComplianceBoardCard({
83
- row,
84
- onRowActivate,
85
- }: {
86
- row: ComplianceItem
87
- onRowActivate?: (row: ComplianceItem) => void
88
- }) {
89
- const ownerInitials = initialsFromDisplayName(row.owner)
90
- return (
91
- <ListPageBoardCard className="w-full" onClick={onRowActivate ? () => onRowActivate(row) : undefined}>
92
- <ListPageBoardCardHeader>
93
- <ListPageBoardCardTitleRow
94
- title={row.title}
95
- titleClassName="line-clamp-2"
96
- trailing={<ListPageBoardCardAvatar initials={ownerInitials} />}
97
- />
98
- <ListPageBoardCardBadgeRow>
99
- <ListHubStatusBadge
100
- surface="board"
101
- label={COMPLIANCE_STATUS_LABEL[row.status]}
102
- tintClassName={COMPLIANCE_STATUS_BADGE_CLASS[row.status]}
103
- icon={COMPLIANCE_STATUS_ICON[row.status]}
104
- />
105
- </ListPageBoardCardBadgeRow>
106
- <ListPageBoardCardBody>
107
- <BoardCardTwoLineBlock iconClass="fa-tag" line1={row.category} line2={`Due ${row.dueDate}`} />
108
- <BoardCardTwoLineBlock iconClass="fa-user" line1={row.owner} line2="Owner" />
109
- </ListPageBoardCardBody>
110
- </ListPageBoardCardHeader>
111
- </ListPageBoardCard>
112
- )
113
- }
114
-
115
- export const COMPLIANCE_BOARD_GROUP_OPTIONS = [
116
- { key: "status", label: "Status" },
117
- { key: "category", label: "Category" },
118
- ] as const
119
-
120
- export function ComplianceBoardView({
121
- rows,
122
- groupByColumnKey,
123
- onRowActivate,
124
- }: {
125
- rows: ComplianceItem[]
126
- groupByColumnKey: string
127
- onRowActivate?: (row: ComplianceItem) => void
128
- }) {
129
- const key = groupByColumnKey === "category" ? "category" : "status"
130
- const { columns, badgeMap } = useComplianceBoardModel(rows, key)
131
-
132
- return (
133
- <ListPageBoardTemplate
134
- columns={columns}
135
- rows={rows}
136
- getRowKey={r => r.id}
137
- columnCountBadgeClassName={badgeMap}
138
- emptyColumnLabel="No items"
139
- renderCard={row => <ComplianceBoardCard row={row} onRowActivate={onRowActivate} />}
140
- />
141
- )
142
- }
@@ -1,92 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Compliance list page — `ListPageTemplate` + `ComplianceTable`; view types from `@/components/data-views`.
5
- */
6
-
7
- import * as React from "react"
8
- import {
9
- ListPageTemplate,
10
- type ViewTab,
11
- dataListViewIcon,
12
- type DataListViewType,
13
- } from "@/components/data-views"
14
- import { CompliancePageHeader } from "@/components/compliance-page-header"
15
- import { ComplianceTable, type ComplianceTableHandle } from "@/components/compliance-table"
16
- import { KeyMetrics } from "@/components/key-metrics"
17
- import { useAskLeoPageContext } from "@/components/ask-leo-sidebar"
18
- import { COMPLIANCE_ITEMS } from "@/lib/mock/compliance"
19
- import { complianceKpiInsight, complianceKpiMetrics } from "@/lib/mock/compliance-kpi"
20
-
21
- const DEFAULT_TABS: ViewTab[] = [
22
- {
23
- id: "obligations",
24
- label: "Obligations",
25
- viewType: "table",
26
- icon: "fa-table",
27
- filterId: "all",
28
- },
29
- ]
30
-
31
- export function ComplianceClient() {
32
- const [exportOpen, setExportOpen] = React.useState(false)
33
- const [showMetrics, setShowMetrics] = React.useState(true)
34
- const tableRef = React.useRef<ComplianceTableHandle>(null)
35
- const count = COMPLIANCE_ITEMS.length
36
-
37
- const metrics = React.useMemo(() => complianceKpiMetrics(COMPLIANCE_ITEMS), [])
38
- const insight = React.useMemo(() => complianceKpiInsight(COMPLIANCE_ITEMS), [])
39
-
40
- useAskLeoPageContext(
41
- React.useMemo(
42
- () => ({
43
- title: "Compliance",
44
- description: `${count} obligations tracked on this hub.`,
45
- suggestions: [
46
- "What’s due this week?",
47
- "Summarize open items by student",
48
- ],
49
- }),
50
- [count],
51
- ),
52
- )
53
-
54
- return (
55
- <ListPageTemplate
56
- defaultTabs={DEFAULT_TABS}
57
- getTabCount={() => count}
58
- tablePropertiesRef={tableRef}
59
- header={
60
- <CompliancePageHeader
61
- itemCount={count}
62
- onAddReview={() => {}}
63
- onExport={() => setExportOpen(true)}
64
- showMetrics={showMetrics}
65
- onToggleMetrics={() => setShowMetrics(v => !v)}
66
- />
67
- }
68
- metrics={
69
- <KeyMetrics
70
- variant="flat"
71
- metrics={metrics}
72
- insight={insight}
73
- showHeader={false}
74
- metricsSingleRow
75
- />
76
- }
77
- showMetrics={showMetrics}
78
- exportOpen={exportOpen}
79
- onExportOpenChange={setExportOpen}
80
- exportTotalRows={count}
81
- renderContent={(tab, updateTab) => (
82
- <ComplianceTable
83
- key={tab.id}
84
- ref={tableRef}
85
- items={COMPLIANCE_ITEMS}
86
- view={tab.viewType}
87
- onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
88
- />
89
- )}
90
- />
91
- )
92
- }
@@ -1,54 +0,0 @@
1
- "use client"
2
-
3
- import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
4
- import { ListPageBoardCard } from "@/components/data-views/list-page-board-card"
5
- import { DataRowList } from "@/components/data-views/data-row-list"
6
- import {
7
- COMPLIANCE_STATUS_BADGE_CLASS,
8
- COMPLIANCE_STATUS_ICON,
9
- COMPLIANCE_STATUS_LABEL,
10
- } from "@/lib/list-status-badges"
11
- import type { ComplianceItem } from "@/lib/mock/compliance"
12
-
13
- export function ComplianceListView({
14
- rows,
15
- onRowActivate,
16
- }: {
17
- rows: ComplianceItem[]
18
- onRowActivate?: (row: ComplianceItem) => void
19
- }) {
20
- return (
21
- <DataRowList<ComplianceItem>
22
- rows={rows}
23
- getRowId={row => row.id}
24
- emptyState="No compliance items match your filters."
25
- ariaLabel="Compliance items"
26
- renderRow={row => (
27
- <ListPageBoardCard
28
- layout="row"
29
- rowContainerClassName="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"
30
- onClick={onRowActivate ? () => onRowActivate(row) : undefined}
31
- rowEnd={
32
- <div className="flex shrink-0 items-center gap-2">
33
- <ListHubStatusBadge
34
- surface="board"
35
- label={COMPLIANCE_STATUS_LABEL[row.status]}
36
- tintClassName={COMPLIANCE_STATUS_BADGE_CLASS[row.status]}
37
- icon={COMPLIANCE_STATUS_ICON[row.status]}
38
- />
39
- <i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden="true" />
40
- </div>
41
- }
42
- >
43
- <div className="space-y-0.5">
44
- <p className="text-sm font-semibold text-foreground">{row.title}</p>
45
- <p className="text-xs text-muted-foreground">
46
- {row.category} · Due {row.dueDate}
47
- </p>
48
- <p className="text-xs text-muted-foreground">Owner: {row.owner}</p>
49
- </div>
50
- </ListPageBoardCard>
51
- )}
52
- />
53
- )
54
- }
@@ -1,89 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { Button } from "@/components/ui/button"
5
- import { PageHeader } from "@/components/page-header"
6
- import {
7
- DropdownMenu,
8
- DropdownMenuContent,
9
- DropdownMenuItem,
10
- DropdownMenuSeparator,
11
- DropdownMenuTrigger,
12
- } from "@/components/ui/dropdown-menu"
13
- import { Tip } from "@/components/ui/tip"
14
-
15
- export interface CompliancePageHeaderProps {
16
- itemCount: number
17
- onAddReview: () => void
18
- onExport: () => void
19
- showMetrics: boolean
20
- onToggleMetrics: () => void
21
- showTitleBlock?: boolean
22
- }
23
-
24
- export function CompliancePageHeader({
25
- itemCount,
26
- onAddReview,
27
- onExport,
28
- showMetrics,
29
- onToggleMetrics,
30
- showTitleBlock = true,
31
- }: CompliancePageHeaderProps) {
32
- const [moreOpen, setMoreOpen] = React.useState(false)
33
- const countLine = `${itemCount} ${itemCount === 1 ? "item" : "items"} · Last updated now`
34
-
35
- return (
36
- <PageHeader
37
- title="Compliance"
38
- subtitle={countLine}
39
- showTitleBlock={showTitleBlock}
40
- actions={(
41
- <div className="flex items-center gap-2" role="group" aria-label="Compliance actions">
42
- <Tip side="bottom" label="Schedule a review (demo)">
43
- <Button type="button" size="lg" onClick={onAddReview}>
44
- <i className="fa-light fa-calendar-check" aria-hidden="true" />
45
- New review
46
- </Button>
47
- </Tip>
48
- <DropdownMenu open={moreOpen} onOpenChange={setMoreOpen}>
49
- <Tip side="bottom" label="More actions">
50
- <DropdownMenuTrigger asChild>
51
- <Button
52
- type="button"
53
- size="lg"
54
- variant="outline"
55
- className="aspect-square px-0"
56
- aria-label="More actions"
57
- >
58
- <i className="fa-light fa-ellipsis text-base" aria-hidden="true" />
59
- </Button>
60
- </DropdownMenuTrigger>
61
- </Tip>
62
- <DropdownMenuContent align="end">
63
- <DropdownMenuItem
64
- onSelect={() => {
65
- window.setTimeout(() => onExport(), 0)
66
- }}
67
- >
68
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
69
- Export
70
- </DropdownMenuItem>
71
- <DropdownMenuSeparator />
72
- <DropdownMenuItem
73
- onSelect={() => {
74
- window.setTimeout(() => onToggleMetrics(), 0)
75
- }}
76
- >
77
- <i
78
- className={`fa-light ${showMetrics ? "fa-eye-slash" : "fa-eye"}`}
79
- aria-hidden="true"
80
- />
81
- {showMetrics ? "Hide metric section" : "Show metric section"}
82
- </DropdownMenuItem>
83
- </DropdownMenuContent>
84
- </DropdownMenu>
85
- </div>
86
- )}
87
- />
88
- )
89
- }