@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,402 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Sites hub — same stack as Team / Compliance: DataTableToolbar (search, filters)
5
- * + TablePropertiesDrawer, shared `useTableState` across table | list | board | dashboard.
6
- */
7
-
8
- import * as React from "react"
9
- import Link from "next/link"
10
- import type { SiteDirectoryRow } from "@/lib/mock/sites-directory"
11
- import { DataTable, DataTableToolbar } from "@/components/data-table"
12
- import { useTableState } from "@/components/data-table/use-table-state"
13
- import { useTableStateLifecycle } from "@/lib/table-state-lifecycle"
14
- import type { ColumnDef } from "@/components/data-table/types"
15
- import type { DataListViewType } from "@/lib/data-list-view"
16
- import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
17
- import { TablePropertiesDrawerButton } from "@/components/table-properties"
18
- import type { ConditionalRule, FilterFieldDef, FilterOperator } from "@/components/table-properties/types"
19
- import { Button } from "@/components/ui/button"
20
- import {
21
- DropdownMenu,
22
- DropdownMenuContent,
23
- DropdownMenuItem,
24
- DropdownMenuTrigger,
25
- } from "@/components/ui/dropdown-menu"
26
- import { Tip } from "@/components/ui/tip"
27
- import { Avatar, AvatarFallback } from "@/components/ui/avatar"
28
- import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
29
- import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
30
- import { SitesCardGrid } from "@/components/sites-board-view"
31
- import { SitesListView } from "@/components/sites-list-view"
32
- import { KeyMetrics } from "@/components/key-metrics"
33
- import { SITES_KPI_INSIGHT, sitesKpiMetrics } from "@/lib/mock/sites-kpi"
34
- import {
35
- DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
36
- type DataListDisplayOptions,
37
- } from "@/lib/data-list-display-options"
38
-
39
- function columnToFilterFieldDef(c: ColumnDef<SiteDirectoryRow>): FilterFieldDef | null {
40
- if (!c.filter) return null
41
- const f = c.filter
42
- const defaultOps: FilterOperator[] =
43
- f.type === "select" || f.type === "date" ? ["is", "is_not"] : ["contains", "not_contains"]
44
- return {
45
- key: c.key,
46
- label: c.label,
47
- icon: f.icon ?? "fa-filter",
48
- type: f.type,
49
- operators: (f.operators ?? defaultOps) as FilterOperator[],
50
- options: f.options,
51
- ...(f.textMask ? { textMask: f.textMask } : {}),
52
- }
53
- }
54
-
55
- function columnsToFilterFields(cols: ColumnDef<SiteDirectoryRow>[]) {
56
- return cols.map(columnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
57
- }
58
-
59
- function buildSitesColumns(): ColumnDef<SiteDirectoryRow>[] {
60
- const COLUMN_SELECT: ColumnDef<SiteDirectoryRow> = {
61
- key: "select",
62
- label: "",
63
- width: 40,
64
- minWidth: 40,
65
- defaultPin: "left",
66
- lockPin: true,
67
- }
68
-
69
- return [
70
- COLUMN_SELECT,
71
- {
72
- key: "name",
73
- label: "Site",
74
- width: 260,
75
- minWidth: 160,
76
- sortable: true,
77
- sortKey: "name",
78
- filter: {
79
- type: "text",
80
- icon: "fa-hospital",
81
- operators: ["contains", "not_contains"],
82
- },
83
- cell: row => (
84
- <div className="flex min-w-0 items-center gap-2">
85
- <Avatar size="sm" className="size-8 shrink-0">
86
- <AvatarFallback className="bg-brand/10 p-0 text-brand">
87
- <i className="fa-light fa-hospital text-sm" aria-hidden="true" />
88
- </AvatarFallback>
89
- </Avatar>
90
- <span className="truncate text-sm font-medium text-foreground">{row.name}</span>
91
- </div>
92
- ),
93
- },
94
- {
95
- key: "id",
96
- label: "Key",
97
- width: 160,
98
- minWidth: 120,
99
- sortable: true,
100
- sortKey: "id",
101
- filter: {
102
- type: "text",
103
- icon: "fa-hashtag",
104
- operators: ["contains", "not_contains"],
105
- },
106
- cell: row => <span className="text-sm text-foreground/90">{row.id}</span>,
107
- },
108
- {
109
- key: "url",
110
- label: "Path",
111
- width: 220,
112
- minWidth: 140,
113
- sortable: true,
114
- sortKey: "url",
115
- filter: {
116
- type: "text",
117
- icon: "fa-link",
118
- operators: ["contains", "not_contains"],
119
- },
120
- cell: row => (
121
- <span className="truncate text-sm text-muted-foreground" title={row.url}>
122
- {row.url}
123
- </span>
124
- ),
125
- },
126
- {
127
- key: "actions",
128
- label: "",
129
- width: 48,
130
- minWidth: 48,
131
- defaultPin: "right",
132
- lockPin: true,
133
- cell: row => (
134
- <div className="flex items-center justify-center">
135
- <DropdownMenu>
136
- <DropdownMenuTrigger asChild>
137
- <Button size="icon-sm" variant="ghost" aria-label={`Actions for ${row.name}`}>
138
- <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
139
- </Button>
140
- </DropdownMenuTrigger>
141
- <DropdownMenuContent align="end">
142
- <DropdownMenuItem asChild>
143
- <Link href={row.url} className="flex cursor-pointer items-center gap-2">
144
- <i className="fa-light fa-arrow-up-right-from-square" aria-hidden="true" />
145
- Open site
146
- </Link>
147
- </DropdownMenuItem>
148
- </DropdownMenuContent>
149
- </DropdownMenu>
150
- </div>
151
- ),
152
- },
153
- ]
154
- }
155
-
156
-
157
- export type SitesTableHandle = OpenTablePropertiesHandle
158
-
159
- export const SitesTable = React.forwardRef<
160
- SitesTableHandle,
161
- { sites: SiteDirectoryRow[]; view?: DataListViewType; onViewChange?: (v: DataListViewType) => void }
162
- >(function SitesTable({ sites, view = "board", onViewChange }, ref) {
163
- const columns = React.useMemo(() => buildSitesColumns(), [])
164
- const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
165
- const fieldDefinitionsForDrawer = React.useMemo(
166
- () =>
167
- columns
168
- .filter(c => c.key !== "select" && c.key !== "actions")
169
- .map(c => ({ key: c.key, label: c.label, sortable: !!(c.sortable && (c.sortKey ?? c.key)) })),
170
- [columns],
171
- )
172
-
173
- const resolveColumnLabel = React.useCallback(
174
- (key: string) => columns.find(c => c.key === key)?.label ?? key,
175
- [columns],
176
- )
177
-
178
- const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
179
- const patchDisplay = React.useCallback((patch: Partial<DataListDisplayOptions>) => {
180
- setDisplayOptions(prev => ({ ...prev, ...patch }))
181
- }, [])
182
-
183
- const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
184
-
185
- const addConditionalRule = React.useCallback((rule: Omit<ConditionalRule, "id">) => {
186
- setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
187
- }, [])
188
- const removeConditionalRule = React.useCallback((id: string) => {
189
- setConditionalRules(prev => prev.filter(r => r.id !== id))
190
- }, [])
191
- const updateConditionalRule = React.useCallback((id: string, patch: Partial<ConditionalRule>) => {
192
- setConditionalRules(prev => prev.map(r => (r.id === id ? { ...r, ...patch } : r)))
193
- }, [])
194
-
195
- const tableState = useTableState<SiteDirectoryRow>(sites, columns, { key: "name", dir: "asc" })
196
-
197
- // Persist this hub's table lifecycle (sort / search / filters / column
198
- // visibility / etc.) to localStorage. See `lib/table-state-lifecycle`.
199
- const lifecycleColumnKeys = React.useMemo(
200
- () => new Set(columns.map(c => c.key)),
201
- [columns],
202
- )
203
- useTableStateLifecycle({
204
- namespace: "sites",
205
- tabId: "main",
206
- tableState,
207
- columnKeys: lifecycleColumnKeys,
208
- extras: { conditionalRules },
209
- onLoadExtras: e => {
210
- if (e && Array.isArray(e.conditionalRules)) {
211
- setConditionalRules(e.conditionalRules as ConditionalRule[])
212
- }
213
- },
214
- })
215
-
216
- React.useImperativeHandle(
217
- ref,
218
- () => ({
219
- openPropertiesDrawer: () => {
220
- tableState.setSheetOpen(true)
221
- },
222
- }),
223
- // `tableState` is freshly returned each render by useTableState; depending
224
- // on it would re-create the imperative handle on every render. Only the
225
- // React setter is needed (and is referentially stable).
226
- // eslint-disable-next-line react-hooks/exhaustive-deps
227
- [tableState.setSheetOpen],
228
- )
229
-
230
- const dashMetrics = React.useMemo(
231
- () => sitesKpiMetrics(tableState.rows.length),
232
- [tableState.rows.length],
233
- )
234
-
235
- // Generic panel view rendering for sites
236
- const panelGroupsBuilder = (rows: SiteDirectoryRow[]): FinderGroup[] => [
237
- {
238
- id: "all",
239
- label: `All sites (${rows.length})`,
240
- count: rows.length,
241
- },
242
- ]
243
-
244
- const panelRenderListRow = (row: SiteDirectoryRow, _isSelected: boolean) => (
245
- <div className="flex-1 min-w-0 flex items-center gap-2">
246
- <Avatar size="sm" className="size-6 shrink-0">
247
- <AvatarFallback className="bg-brand/10 p-0 text-brand text-xs">
248
- <i className="fa-light fa-hospital text-xs" aria-hidden="true" />
249
- </AvatarFallback>
250
- </Avatar>
251
- <div className="flex-1 min-w-0">
252
- <p className="text-sm font-medium text-foreground truncate">{row.name}</p>
253
- <p className="text-xs text-muted-foreground truncate">{row.url}</p>
254
- </div>
255
- </div>
256
- )
257
-
258
- const panelRenderDetail = (row: SiteDirectoryRow) => (
259
- <div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4">
260
- <div>
261
- <h3 className="text-sm font-semibold text-foreground mb-2">Site</h3>
262
- <p className="text-sm text-foreground">{row.name}</p>
263
- </div>
264
- <div className="flex flex-col gap-2">
265
- <div>
266
- <span className="text-xs font-medium text-muted-foreground">Key</span>
267
- <p className="text-sm text-foreground font-mono">{row.id}</p>
268
- </div>
269
- <div>
270
- <span className="text-xs font-medium text-muted-foreground">Path</span>
271
- <p className="text-sm text-foreground break-all">{row.url}</p>
272
- </div>
273
- </div>
274
- </div>
275
- )
276
-
277
- const drawerToolbarProps = {
278
- state: tableState,
279
- totalRows: sites.length,
280
- filterFields,
281
- fieldDefinitions: fieldDefinitionsForDrawer,
282
- resolveColumnLabel,
283
- displayOptions,
284
- onDisplayOptionsChange: patchDisplay,
285
- conditionalRules,
286
- onAddConditionalRule: addConditionalRule,
287
- onRemoveConditionalRule: removeConditionalRule,
288
- onUpdateConditionalRule: updateConditionalRule,
289
- currentView: view,
290
- onViewChange,
291
- lifecycleTabLabel: "Sites",
292
- }
293
-
294
- const tableProps = {
295
- data: sites,
296
- columns,
297
- getRowId: (row: SiteDirectoryRow) => row.id,
298
- getRowSelectionLabel: (row: SiteDirectoryRow) => row.name,
299
- selectable: true,
300
- searchable: displayOptions.showToolbarSearch,
301
- showColumnHeaders: displayOptions.showColumnLabels,
302
- groupable: true,
303
- defaultSort: { key: "name", dir: "asc" as const },
304
- emptyState: <p className="text-sm text-muted-foreground">No sites match your filters.</p>,
305
- conditionalRules,
306
- state: tableState,
307
- toolbarSlot: (s: ReturnType<typeof useTableState<SiteDirectoryRow>>) => (
308
- <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />
309
- ),
310
- bulkActionsSlot: (selected: Set<string | number>) => {
311
- const n = selected.size
312
- if (n === 0) return null
313
- return (
314
- <>
315
- <span className="sr-only">{n} selected</span>
316
- <Tip label="Export selection (demo)">
317
- <Button size="sm" variant="outline" type="button">
318
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
319
- Export
320
- </Button>
321
- </Tip>
322
- </>
323
- )
324
- },
325
- }
326
-
327
- if (view === "table") {
328
- return (
329
- <div className="pb-6">
330
- <DataTable<SiteDirectoryRow> {...tableProps} />
331
- </div>
332
- )
333
- }
334
-
335
- const sharedToolbar = (
336
- <DataTableToolbar
337
- state={tableState}
338
- columns={columns}
339
- searchable={displayOptions.showToolbarSearch}
340
- searchAriaLabel="Search sites"
341
- toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
342
- />
343
- )
344
-
345
- if (view === "list") {
346
- return (
347
- <div className="flex min-h-0 flex-1 flex-col">
348
- {sharedToolbar}
349
- <SitesListView rows={tableState.rows} />
350
- </div>
351
- )
352
- }
353
-
354
- if (view === "board") {
355
- return (
356
- <div className="flex min-h-0 flex-1 flex-col">
357
- {sharedToolbar}
358
- <SitesCardGrid rows={tableState.rows} />
359
- </div>
360
- )
361
- }
362
-
363
- if (view === "panel") {
364
- return (
365
- <div className="flex min-h-0 flex-1 flex-col">
366
- {sharedToolbar}
367
- <ListPageSplitHubChrome aria-label="Sites directory panel view">
368
- <FinderPanelView<SiteDirectoryRow>
369
- embedded
370
- groupsColumnTitle="Sites"
371
- groups={panelGroupsBuilder(tableState.rows)}
372
- rows={tableState.rows}
373
- getRowId={(row) => row.id}
374
- getRowGroupId={() => "all"}
375
- autoSaveId="sites-panel-view"
376
- renderListRow={panelRenderListRow}
377
- renderDetail={panelRenderDetail}
378
- emptyList={<p className="text-sm text-muted-foreground">No sites found.</p>}
379
- />
380
- </ListPageSplitHubChrome>
381
- </div>
382
- )
383
- }
384
-
385
- return (
386
- <div className="flex min-h-0 flex-1 flex-col gap-4">
387
- {sharedToolbar}
388
- <div className="px-4 pb-2 lg:px-6">
389
- <KeyMetrics
390
- variant="flat"
391
- metrics={dashMetrics}
392
- insight={SITES_KPI_INSIGHT}
393
- showHeader={false}
394
- metricsSingleRow
395
- />
396
- </div>
397
- <SitesCardGrid rows={tableState.rows} />
398
- </div>
399
- )
400
- })
401
-
402
- SitesTable.displayName = "SitesTable"
@@ -1,122 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Team board — kanban by member status or role. Column layout from `ListPageBoardTemplate`.
5
- */
6
-
7
- import * as React from "react"
8
- import {
9
- TEAM_MEMBER_STATUS_BADGE_CLASS,
10
- TEAM_MEMBER_STATUS_ICON,
11
- TEAM_MEMBER_STATUS_LABEL,
12
- } from "@/lib/list-status-badges"
13
- import type { TeamMember } from "@/lib/mock/team"
14
- import { BoardCardTwoLineBlock } from "@/components/data-views/board-card-primitives"
15
- import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
16
- import {
17
- ListPageBoardCard,
18
- ListPageBoardCardAvatar,
19
- ListPageBoardCardBadgeRow,
20
- ListPageBoardCardBody,
21
- ListPageBoardCardHeader,
22
- ListPageBoardCardTitleRow,
23
- } from "@/components/data-views/list-page-board-card"
24
- import {
25
- ListPageBoardTemplate,
26
- type ListPageBoardColumnDef,
27
- } from "@/components/data-views/list-page-board-template"
28
-
29
- const NEUTRAL_COUNT_BADGE = "bg-muted/90 text-foreground"
30
-
31
- const STATUS_BOARD_COLUMNS: ListPageBoardColumnDef<TeamMember>[] = [
32
- { id: "active", label: "Active", description: "On the team", filter: m => m.status === "active" },
33
- { id: "away", label: "Away", description: "Temporarily away", filter: m => m.status === "away" },
34
- { id: "invited", label: "Invited", description: "Pending acceptance", filter: m => m.status === "invited" },
35
- ]
36
-
37
- function roleBoardColumns(members: TeamMember[]): {
38
- columns: ListPageBoardColumnDef<TeamMember>[]
39
- badgeMap: Record<string, string>
40
- } {
41
- const roles = [...new Set(members.map(m => m.role))].sort((a, b) => a.localeCompare(b))
42
- const columns: ListPageBoardColumnDef<TeamMember>[] = roles.map(role => ({
43
- id: `role:${role}`,
44
- label: role,
45
- filter: (m: TeamMember) => m.role === role,
46
- }))
47
- const badgeMap = Object.fromEntries(roles.map(r => [`role:${r}`, NEUTRAL_COUNT_BADGE]))
48
- return { columns, badgeMap }
49
- }
50
-
51
- function useTeamBoardModel(members: TeamMember[], groupByColumnKey: string) {
52
- return React.useMemo(() => {
53
- if (groupByColumnKey === "role") {
54
- const { columns, badgeMap } = roleBoardColumns(members)
55
- return { columns, badgeMap }
56
- }
57
- return {
58
- columns: STATUS_BOARD_COLUMNS,
59
- badgeMap: TEAM_MEMBER_STATUS_BADGE_CLASS as Record<string, string>,
60
- }
61
- }, [members, groupByColumnKey])
62
- }
63
-
64
- function TeamBoardCard({
65
- member,
66
- onRowActivate,
67
- }: {
68
- member: TeamMember
69
- onRowActivate?: (member: TeamMember) => void
70
- }) {
71
- return (
72
- <ListPageBoardCard className="w-full" onClick={onRowActivate ? () => onRowActivate(member) : undefined}>
73
- <ListPageBoardCardHeader>
74
- <ListPageBoardCardTitleRow
75
- title={member.name}
76
- titleClassName="truncate"
77
- trailing={<ListPageBoardCardAvatar initials={member.initials} />}
78
- />
79
- <ListPageBoardCardBadgeRow>
80
- <ListHubStatusBadge
81
- surface="board"
82
- label={TEAM_MEMBER_STATUS_LABEL[member.status]}
83
- tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
84
- icon={TEAM_MEMBER_STATUS_ICON[member.status]}
85
- />
86
- </ListPageBoardCardBadgeRow>
87
- <ListPageBoardCardBody>
88
- <BoardCardTwoLineBlock iconClass="fa-briefcase" line1={member.role} line2={member.email} />
89
- </ListPageBoardCardBody>
90
- </ListPageBoardCardHeader>
91
- </ListPageBoardCard>
92
- )
93
- }
94
-
95
- export const TEAM_BOARD_GROUP_OPTIONS = [
96
- { key: "status", label: "Status" },
97
- { key: "role", label: "Role" },
98
- ] as const
99
-
100
- export function TeamBoardView({
101
- members,
102
- groupByColumnKey,
103
- onRowActivate,
104
- }: {
105
- members: TeamMember[]
106
- groupByColumnKey: string
107
- onRowActivate?: (member: TeamMember) => void
108
- }) {
109
- const key = groupByColumnKey === "role" ? "role" : "status"
110
- const { columns, badgeMap } = useTeamBoardModel(members, key)
111
-
112
- return (
113
- <ListPageBoardTemplate
114
- columns={columns}
115
- rows={members}
116
- getRowKey={m => m.id}
117
- columnCountBadgeClassName={badgeMap}
118
- emptyColumnLabel="No members"
119
- renderCard={member => <TeamBoardCard member={member} onRowActivate={onRowActivate} />}
120
- />
121
- )
122
- }
@@ -1,100 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Team page — primary list template: ListPageTemplate + KeyMetrics + TeamTable (same composition as PlacementsClient).
5
- * Imports from `@/components/data-views` for shared list-page + view types.
6
- */
7
-
8
- import * as React from "react"
9
- import {
10
- ListPageTemplate,
11
- type ViewTab,
12
- dataListViewIcon,
13
- type DataListViewType,
14
- } from "@/components/data-views"
15
- import { TeamPageHeader } from "@/components/team-page-header"
16
- import { TeamTable, type TeamTableHandle } from "@/components/team-table"
17
- import { KeyMetrics } from "@/components/key-metrics"
18
- import { useAskLeoPageContext } from "@/components/ask-leo-sidebar"
19
- import { TEAM_MEMBERS } from "@/lib/mock/team"
20
- import { teamKpiInsight, teamKpiMetrics } from "@/lib/mock/team-kpi"
21
-
22
- const DEFAULT_TEAM_TABS: ViewTab[] = [
23
- {
24
- id: "members",
25
- label: "Members",
26
- viewType: "table",
27
- icon: "fa-table",
28
- filterId: "all",
29
- },
30
- {
31
- id: "panel",
32
- label: "Panel",
33
- viewType: "panel",
34
- icon: "fa-sidebar",
35
- filterId: "all",
36
- },
37
- ]
38
-
39
- export function TeamClient() {
40
- const [exportOpen, setExportOpen] = React.useState(false)
41
- const [showMetrics, setShowMetrics] = React.useState(true)
42
- const tableRef = React.useRef<TeamTableHandle>(null)
43
- const memberCount = TEAM_MEMBERS.length
44
-
45
- const metrics = React.useMemo(() => teamKpiMetrics(TEAM_MEMBERS), [])
46
- const insight = React.useMemo(() => teamKpiInsight(TEAM_MEMBERS), [])
47
-
48
- useAskLeoPageContext(
49
- React.useMemo(
50
- () => ({
51
- title: "Team",
52
- description: `${memberCount} members in this directory.`,
53
- suggestions: [
54
- "Who owns the most active placements?",
55
- "Summarize workload by program",
56
- ],
57
- }),
58
- [memberCount],
59
- ),
60
- )
61
-
62
- return (
63
- <ListPageTemplate
64
- defaultTabs={DEFAULT_TEAM_TABS}
65
- getTabCount={() => memberCount}
66
- tablePropertiesRef={tableRef}
67
- header={
68
- <TeamPageHeader
69
- memberCount={memberCount}
70
- onInvite={() => {}}
71
- onExport={() => setExportOpen(true)}
72
- showMetrics={showMetrics}
73
- onToggleMetrics={() => setShowMetrics(v => !v)}
74
- />
75
- }
76
- metrics={
77
- <KeyMetrics
78
- variant="flat"
79
- metrics={metrics}
80
- insight={insight}
81
- showHeader={false}
82
- metricsSingleRow
83
- />
84
- }
85
- showMetrics={showMetrics}
86
- exportOpen={exportOpen}
87
- onExportOpenChange={setExportOpen}
88
- exportTotalRows={memberCount}
89
- renderContent={(tab, updateTab) => (
90
- <TeamTable
91
- key={tab.id}
92
- ref={tableRef}
93
- members={TEAM_MEMBERS}
94
- view={tab.viewType}
95
- onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
96
- />
97
- )}
98
- />
99
- )
100
- }
@@ -1,59 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * TeamListView — full-width rows for team roster (same data as DataTable / board).
5
- * Shell from generic `DataRowList`; row body stays team-specific (avatar,
6
- * name, role, email, status badge).
7
- */
8
-
9
- import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
10
- import { ListPageBoardCard, ListPageBoardCardAvatar } from "@/components/data-views/list-page-board-card"
11
- import { DataRowList } from "@/components/data-views/data-row-list"
12
- import {
13
- TEAM_MEMBER_STATUS_BADGE_CLASS,
14
- TEAM_MEMBER_STATUS_ICON,
15
- TEAM_MEMBER_STATUS_LABEL,
16
- } from "@/lib/list-status-badges"
17
- import type { TeamMember } from "@/lib/mock/team"
18
-
19
- export function TeamListView({
20
- members,
21
- onRowActivate,
22
- }: {
23
- members: TeamMember[]
24
- onRowActivate?: (member: TeamMember) => void
25
- }) {
26
- return (
27
- <DataRowList<TeamMember>
28
- rows={members}
29
- getRowId={m => m.id}
30
- emptyState="No team members match your filters."
31
- ariaLabel="Team members"
32
- renderRow={member => (
33
- <ListPageBoardCard
34
- layout="row"
35
- rowContainerClassName="flex flex-row items-center gap-3"
36
- onClick={onRowActivate ? () => onRowActivate(member) : undefined}
37
- leading={<ListPageBoardCardAvatar initials={member.initials} className="size-9" />}
38
- rowEnd={
39
- <div className="flex shrink-0 items-center gap-2">
40
- <ListHubStatusBadge
41
- surface="board"
42
- label={TEAM_MEMBER_STATUS_LABEL[member.status]}
43
- tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
44
- icon={TEAM_MEMBER_STATUS_ICON[member.status]}
45
- />
46
- <i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden="true" />
47
- </div>
48
- }
49
- >
50
- <div className="space-y-0.5">
51
- <p className="truncate text-sm font-semibold text-foreground">{member.name}</p>
52
- <p className="text-xs text-muted-foreground">{member.role}</p>
53
- <p className="truncate text-xs text-muted-foreground">{member.email}</p>
54
- </div>
55
- </ListPageBoardCard>
56
- )}
57
- />
58
- )
59
- }