@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
@@ -0,0 +1,27 @@
1
+ import type { MetricInsight, MetricItem } from "@/components/key-metrics"
2
+ import type { ListHubRecord } from "@/lib/mock/list-hub-directory"
3
+
4
+ export function listHubKpiMetrics(countOrRows: number | ListHubRecord[]): MetricItem[] {
5
+ const count = typeof countOrRows === "number" ? countOrRows : countOrRows.length
6
+ const scheduled = Math.max(0, count - 2)
7
+ const thisWeek = Math.min(count, 4)
8
+ return [
9
+ {
10
+ id: "total",
11
+ label: "Total records",
12
+ value: count,
13
+ delta: "+2",
14
+ trend: "up",
15
+ href: "#",
16
+ metricVariant: "hero",
17
+ },
18
+ { id: "scheduled", label: "Scheduled", value: scheduled, delta: "+1", trend: "up", href: "#" },
19
+ { id: "this-week", label: "This week", value: thisWeek, delta: "—", trend: "neutral", href: "#" },
20
+ { id: "completed", label: "Completed", value: 2, delta: "—", trend: "neutral", href: "#" },
21
+ ]
22
+ }
23
+
24
+ export const LIST_HUB_KPI_INSIGHT: MetricInsight = {
25
+ title: "3 events land this week",
26
+ description: "Filtered calendar and table views share the same row set after search and filters.",
27
+ }
@@ -138,6 +138,7 @@ export const NAV_PRIMARY: NavLinkItem[] = [
138
138
  icon: <i className="fa-light fa-table" aria-hidden="true" />,
139
139
  iconActive: <i className="fa-solid fa-table" aria-hidden="true" />,
140
140
  badge: 24,
141
+ secondaryPanel: "list-hub",
141
142
  },
142
143
  ]
143
144
 
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Maps app routes to loading skeleton layouts — keep in sync with page templates
3
+ * (`PrimaryPageTemplate`, `FocusedWorkflowPageTemplate`, dedicated search, hub landing).
4
+ */
5
+
6
+ export type PageLoadingVariant =
7
+ | "dashboard"
8
+ | "primary-list-hub"
9
+ | "question-bank-hub"
10
+ | "dedicated-search"
11
+ | "focused-workflow"
12
+ | "focused-workflow-sidebar"
13
+ | "simple"
14
+
15
+ function normalizePathname(pathname: string): string {
16
+ if (!pathname || pathname === "/") return "/"
17
+ return pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
18
+ }
19
+
20
+ /** Pick the loading skeleton for the destination route (client-safe). */
21
+ export function resolvePageLoadingVariant(pathname: string): PageLoadingVariant {
22
+ const path = normalizePathname(pathname)
23
+
24
+ if (path === "/dashboard") return "dashboard"
25
+ if (path === "/settings") return "focused-workflow-sidebar"
26
+ if (path === "/help" || path === "/examples" || path.startsWith("/examples/")) {
27
+ return path === "/examples/focused-workflow" ? "focused-workflow-sidebar" : "simple"
28
+ }
29
+ if (path === "/question-bank/new" || path.startsWith("/question-bank/new/")) {
30
+ return "focused-workflow"
31
+ }
32
+ if (path === "/question-bank/find" || path === "/question-bank/list") {
33
+ return "dedicated-search"
34
+ }
35
+ if (path === "/question-bank") return "question-bank-hub"
36
+ if (path.startsWith("/question-bank/")) return "primary-list-hub"
37
+ if (path === "/data-list") return "primary-list-hub"
38
+
39
+ return "primary-list-hub"
40
+ }
@@ -0,0 +1,13 @@
1
+ import type { DataListViewType } from "@/lib/data-list-view"
2
+
3
+ /** Views implemented in `QuestionBankTable` — keep in sync with `ListPageConnectedViewBody` renderers. */
4
+ export const QUESTION_BANK_SUPPORTED_VIEWS = [
5
+ "table",
6
+ "list",
7
+ "board",
8
+ "dashboard",
9
+ "calendar",
10
+ "folder",
11
+ "panel",
12
+ "tree-panel",
13
+ ] as const satisfies readonly DataListViewType[]
@@ -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)
@@ -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
- }