@exxatdesignux/ui 0.2.18 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/consumer-extras/AGENTS.md +76 -0
  3. package/consumer-extras/README.md +5 -1
  4. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
  5. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +21 -6
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
  9. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  10. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  11. package/consumer-extras/patterns/data-views-pattern.md +40 -3
  12. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  13. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
  14. package/package.json +2 -1
  15. package/src/components/ui/button-group.tsx +81 -0
  16. package/src/components/ui/button.tsx +4 -4
  17. package/src/globals.css +7 -1858
  18. package/src/theme.css +10 -1126
  19. package/src/tokens/README.md +15 -0
  20. package/src/tokens/base.css +337 -0
  21. package/src/tokens/high-contrast.css +1195 -0
  22. package/src/tokens/layers.css +224 -0
  23. package/src/tokens/tailwind-bridge.css +118 -0
  24. package/src/tokens/themes.css +201 -0
  25. package/template/AGENTS.md +60 -22
  26. package/template/app/(app)/dashboard/loading.tsx +3 -15
  27. package/template/app/(app)/dashboard/page.tsx +2 -14
  28. package/template/app/(app)/data-list/layout.tsx +43 -0
  29. package/template/app/(app)/data-list/page.tsx +2 -2
  30. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  31. package/template/app/(app)/examples/page.tsx +1 -0
  32. package/template/app/(app)/loading.tsx +1 -18
  33. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  34. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  35. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  36. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  37. package/template/app/(app)/question-bank/page.tsx +2 -1
  38. package/template/app/(app)/settings/page.tsx +4 -5
  39. package/template/app/globals.css +7 -1964
  40. package/template/components/app-route-loading.tsx +14 -0
  41. package/template/components/app-sidebar.tsx +70 -55
  42. package/template/components/data-views/index.ts +37 -9
  43. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  44. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  45. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  46. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  47. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  48. package/template/components/list-hub-board-view.tsx +68 -0
  49. package/template/components/list-hub-client.tsx +186 -0
  50. package/template/components/list-hub-list-view.tsx +36 -0
  51. package/template/components/list-hub-panel-activator.tsx +8 -0
  52. package/template/components/list-hub-secondary-nav.tsx +121 -0
  53. package/template/components/list-hub-table.tsx +336 -0
  54. package/template/components/new-question-composer.tsx +6 -24
  55. package/template/components/product-switcher.tsx +3 -2
  56. package/template/components/question-bank-client.tsx +4 -1
  57. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  58. package/template/components/question-bank-table.tsx +143 -485
  59. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  60. package/template/components/secondary-panel.tsx +4 -44
  61. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  62. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  63. package/template/components/secondary-panels/registry.tsx +15 -0
  64. package/template/components/settings-appearance-card.tsx +3 -2
  65. package/template/components/settings-client.tsx +59 -15
  66. package/template/components/settings-form-row.tsx +9 -4
  67. package/template/components/table-properties/drawer-button.tsx +13 -0
  68. package/template/components/table-properties/drawer.tsx +65 -4
  69. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  70. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  71. package/template/components/templates/list-page.tsx +29 -5
  72. package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
  73. package/template/components/templates/page-loading-shell.tsx +262 -0
  74. package/template/components/ui/button-group.tsx +1 -0
  75. package/template/docs/consumer-app-pattern.md +39 -0
  76. package/template/docs/data-views-pattern.md +40 -3
  77. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  78. package/template/docs/focused-workflow-page-pattern.md +84 -0
  79. package/template/docs/shell-surface-elevation-pattern.md +5 -3
  80. package/template/lib/command-menu-search-data.ts +11 -27
  81. package/template/lib/data-list-display-options.ts +16 -2
  82. package/template/lib/data-list-view-registry.ts +104 -0
  83. package/template/lib/data-list-view-surface.ts +15 -1
  84. package/template/lib/data-list-view.ts +10 -1
  85. package/template/lib/data-view-dashboard-storage.ts +38 -35
  86. package/template/lib/hub-connected-view-renderers.ts +58 -0
  87. package/template/lib/list-hub-nav.ts +121 -0
  88. package/template/lib/list-hub-supported-views.ts +10 -0
  89. package/template/lib/list-page-table-properties.ts +3 -7
  90. package/template/lib/list-status-badges.ts +4 -97
  91. package/template/lib/mock/list-hub-directory.ts +27 -0
  92. package/template/lib/mock/list-hub-kpi.ts +27 -0
  93. package/template/lib/mock/navigation.tsx +1 -0
  94. package/template/lib/page-loading-variant.ts +40 -0
  95. package/template/lib/question-bank-supported-views.ts +13 -0
  96. package/template/lib/table-state-lifecycle.ts +2 -2
  97. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  98. package/template/app/(app)/data-list/new/page.tsx +0 -34
  99. package/template/components/compliance-board-view.tsx +0 -142
  100. package/template/components/compliance-client.tsx +0 -92
  101. package/template/components/compliance-list-view.tsx +0 -54
  102. package/template/components/compliance-page-header.tsx +0 -89
  103. package/template/components/compliance-table.tsx +0 -612
  104. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  105. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  106. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  107. package/template/components/new-placement-back-btn.tsx +0 -28
  108. package/template/components/new-placement-form.tsx +0 -1068
  109. package/template/components/placement-board-card.tsx +0 -262
  110. package/template/components/placement-detail.tsx +0 -438
  111. package/template/components/placements-board-view.tsx +0 -404
  112. package/template/components/placements-client.tsx +0 -252
  113. package/template/components/placements-list-view.tsx +0 -171
  114. package/template/components/placements-page-header.tsx +0 -166
  115. package/template/components/placements-table-cells.test.tsx +0 -22
  116. package/template/components/placements-table-cells.tsx +0 -173
  117. package/template/components/placements-table-columns.tsx +0 -640
  118. package/template/components/placements-table.tsx +0 -1642
  119. package/template/components/rotations-empty-state.tsx +0 -50
  120. package/template/components/rotations-panel-activator.tsx +0 -8
  121. package/template/components/sites-all-client.tsx +0 -154
  122. package/template/components/sites-board-view.tsx +0 -67
  123. package/template/components/sites-list-view.tsx +0 -42
  124. package/template/components/sites-table.tsx +0 -382
  125. package/template/components/team-board-view.tsx +0 -122
  126. package/template/components/team-client.tsx +0 -100
  127. package/template/components/team-list-view.tsx +0 -59
  128. package/template/components/team-page-header.tsx +0 -92
  129. package/template/components/team-table.tsx +0 -693
  130. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  131. package/template/lib/mock/compliance-kpi.ts +0 -61
  132. package/template/lib/mock/compliance.ts +0 -146
  133. package/template/lib/mock/placements-kpi.ts +0 -134
  134. package/template/lib/mock/placements.ts +0 -183
  135. package/template/lib/mock/sites-directory.ts +0 -16
  136. package/template/lib/mock/sites-kpi.ts +0 -25
  137. package/template/lib/mock/team-kpi.ts +0 -60
  138. package/template/lib/mock/team.ts +0 -118
  139. package/template/lib/placement-board-card-layout.ts +0 -79
  140. package/template/lib/placement-lifecycle.ts +0 -5
@@ -1,382 +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 type { ColumnDef } from "@/components/data-table/types"
14
- import type { DataListViewType } from "@/lib/data-list-view"
15
- import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
16
- import { TablePropertiesDrawerButton } from "@/components/table-properties"
17
- import type { ConditionalRule, FilterFieldDef, FilterOperator } from "@/components/table-properties/types"
18
- import { Button } from "@/components/ui/button"
19
- import {
20
- DropdownMenu,
21
- DropdownMenuContent,
22
- DropdownMenuItem,
23
- DropdownMenuTrigger,
24
- } from "@/components/ui/dropdown-menu"
25
- import { Tip } from "@/components/ui/tip"
26
- import { Avatar, AvatarFallback } from "@/components/ui/avatar"
27
- import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
28
- import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
29
- import { SitesCardGrid } from "@/components/sites-board-view"
30
- import { SitesListView } from "@/components/sites-list-view"
31
- import { KeyMetrics } from "@/components/key-metrics"
32
- import { SITES_KPI_INSIGHT, sitesKpiMetrics } from "@/lib/mock/sites-kpi"
33
- import {
34
- DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
35
- type DataListDisplayOptions,
36
- } from "@/lib/data-list-display-options"
37
-
38
- function columnToFilterFieldDef(c: ColumnDef<SiteDirectoryRow>): FilterFieldDef | null {
39
- if (!c.filter) return null
40
- const f = c.filter
41
- const defaultOps: FilterOperator[] =
42
- f.type === "select" || f.type === "date" ? ["is", "is_not"] : ["contains", "not_contains"]
43
- return {
44
- key: c.key,
45
- label: c.label,
46
- icon: f.icon ?? "fa-filter",
47
- type: f.type,
48
- operators: (f.operators ?? defaultOps) as FilterOperator[],
49
- options: f.options,
50
- ...(f.textMask ? { textMask: f.textMask } : {}),
51
- }
52
- }
53
-
54
- function columnsToFilterFields(cols: ColumnDef<SiteDirectoryRow>[]) {
55
- return cols.map(columnToFilterFieldDef).filter((x): x is FilterFieldDef => x !== null)
56
- }
57
-
58
- function buildSitesColumns(): ColumnDef<SiteDirectoryRow>[] {
59
- const COLUMN_SELECT: ColumnDef<SiteDirectoryRow> = {
60
- key: "select",
61
- label: "",
62
- width: 40,
63
- minWidth: 40,
64
- defaultPin: "left",
65
- lockPin: true,
66
- }
67
-
68
- return [
69
- COLUMN_SELECT,
70
- {
71
- key: "name",
72
- label: "Site",
73
- width: 260,
74
- minWidth: 160,
75
- sortable: true,
76
- sortKey: "name",
77
- filter: {
78
- type: "text",
79
- icon: "fa-hospital",
80
- operators: ["contains", "not_contains"],
81
- },
82
- cell: row => (
83
- <div className="flex min-w-0 items-center gap-2">
84
- <Avatar size="sm" className="size-8 shrink-0">
85
- <AvatarFallback className="bg-brand/10 p-0 text-brand">
86
- <i className="fa-light fa-hospital text-sm" aria-hidden="true" />
87
- </AvatarFallback>
88
- </Avatar>
89
- <span className="truncate text-sm font-medium text-foreground">{row.name}</span>
90
- </div>
91
- ),
92
- },
93
- {
94
- key: "id",
95
- label: "Key",
96
- width: 160,
97
- minWidth: 120,
98
- sortable: true,
99
- sortKey: "id",
100
- filter: {
101
- type: "text",
102
- icon: "fa-hashtag",
103
- operators: ["contains", "not_contains"],
104
- },
105
- cell: row => <span className="text-sm text-foreground/90">{row.id}</span>,
106
- },
107
- {
108
- key: "url",
109
- label: "Path",
110
- width: 220,
111
- minWidth: 140,
112
- sortable: true,
113
- sortKey: "url",
114
- filter: {
115
- type: "text",
116
- icon: "fa-link",
117
- operators: ["contains", "not_contains"],
118
- },
119
- cell: row => (
120
- <span className="truncate text-sm text-muted-foreground" title={row.url}>
121
- {row.url}
122
- </span>
123
- ),
124
- },
125
- {
126
- key: "actions",
127
- label: "",
128
- width: 48,
129
- minWidth: 48,
130
- defaultPin: "right",
131
- lockPin: true,
132
- cell: row => (
133
- <div className="flex items-center justify-center">
134
- <DropdownMenu>
135
- <DropdownMenuTrigger asChild>
136
- <Button size="icon-sm" variant="ghost" aria-label={`Actions for ${row.name}`}>
137
- <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
138
- </Button>
139
- </DropdownMenuTrigger>
140
- <DropdownMenuContent align="end">
141
- <DropdownMenuItem asChild>
142
- <Link href={row.url} className="flex cursor-pointer items-center gap-2">
143
- <i className="fa-light fa-arrow-up-right-from-square" aria-hidden="true" />
144
- Open site
145
- </Link>
146
- </DropdownMenuItem>
147
- </DropdownMenuContent>
148
- </DropdownMenu>
149
- </div>
150
- ),
151
- },
152
- ]
153
- }
154
-
155
-
156
- export type SitesTableHandle = OpenTablePropertiesHandle
157
-
158
- export const SitesTable = React.forwardRef<
159
- SitesTableHandle,
160
- { sites: SiteDirectoryRow[]; view?: DataListViewType; onViewChange?: (v: DataListViewType) => void }
161
- >(function SitesTable({ sites, view = "board", onViewChange }, ref) {
162
- const columns = React.useMemo(() => buildSitesColumns(), [])
163
- const filterFields = React.useMemo(() => columnsToFilterFields(columns), [columns])
164
- const fieldDefinitionsForDrawer = React.useMemo(
165
- () =>
166
- columns
167
- .filter(c => c.key !== "select" && c.key !== "actions")
168
- .map(c => ({ key: c.key, label: c.label, sortable: !!(c.sortable && (c.sortKey ?? c.key)) })),
169
- [columns],
170
- )
171
-
172
- const resolveColumnLabel = React.useCallback(
173
- (key: string) => columns.find(c => c.key === key)?.label ?? key,
174
- [columns],
175
- )
176
-
177
- const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
178
- const patchDisplay = React.useCallback((patch: Partial<DataListDisplayOptions>) => {
179
- setDisplayOptions(prev => ({ ...prev, ...patch }))
180
- }, [])
181
-
182
- const [conditionalRules, setConditionalRules] = React.useState<ConditionalRule[]>([])
183
-
184
- const addConditionalRule = React.useCallback((rule: Omit<ConditionalRule, "id">) => {
185
- setConditionalRules(prev => [...prev, { ...rule, id: `cr-${Date.now()}` }])
186
- }, [])
187
- const removeConditionalRule = React.useCallback((id: string) => {
188
- setConditionalRules(prev => prev.filter(r => r.id !== id))
189
- }, [])
190
- const updateConditionalRule = React.useCallback((id: string, patch: Partial<ConditionalRule>) => {
191
- setConditionalRules(prev => prev.map(r => (r.id === id ? { ...r, ...patch } : r)))
192
- }, [])
193
-
194
- const tableState = useTableState<SiteDirectoryRow>(sites, columns, { key: "name", dir: "asc" })
195
-
196
- React.useImperativeHandle(
197
- ref,
198
- () => ({
199
- openPropertiesDrawer: () => {
200
- tableState.setSheetOpen(true)
201
- },
202
- }),
203
- // `tableState` is freshly returned each render by useTableState; depending
204
- // on it would re-create the imperative handle on every render. Only the
205
- // React setter is needed (and is referentially stable).
206
- // eslint-disable-next-line react-hooks/exhaustive-deps
207
- [tableState.setSheetOpen],
208
- )
209
-
210
- const dashMetrics = React.useMemo(
211
- () => sitesKpiMetrics(tableState.rows.length),
212
- [tableState.rows.length],
213
- )
214
-
215
- // Generic panel view rendering for sites
216
- const panelGroupsBuilder = (rows: SiteDirectoryRow[]): FinderGroup[] => [
217
- {
218
- id: "all",
219
- label: `All sites (${rows.length})`,
220
- count: rows.length,
221
- },
222
- ]
223
-
224
- const panelRenderListRow = (row: SiteDirectoryRow, _isSelected: boolean) => (
225
- <div className="flex-1 min-w-0 flex items-center gap-2">
226
- <Avatar size="sm" className="size-6 shrink-0">
227
- <AvatarFallback className="bg-brand/10 p-0 text-brand text-xs">
228
- <i className="fa-light fa-hospital text-xs" aria-hidden="true" />
229
- </AvatarFallback>
230
- </Avatar>
231
- <div className="flex-1 min-w-0">
232
- <p className="text-sm font-medium text-foreground truncate">{row.name}</p>
233
- <p className="text-xs text-muted-foreground truncate">{row.url}</p>
234
- </div>
235
- </div>
236
- )
237
-
238
- const panelRenderDetail = (row: SiteDirectoryRow) => (
239
- <div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4">
240
- <div>
241
- <h3 className="text-sm font-semibold text-foreground mb-2">Site</h3>
242
- <p className="text-sm text-foreground">{row.name}</p>
243
- </div>
244
- <div className="flex flex-col gap-2">
245
- <div>
246
- <span className="text-xs font-medium text-muted-foreground">Key</span>
247
- <p className="text-sm text-foreground font-mono">{row.id}</p>
248
- </div>
249
- <div>
250
- <span className="text-xs font-medium text-muted-foreground">Path</span>
251
- <p className="text-sm text-foreground break-all">{row.url}</p>
252
- </div>
253
- </div>
254
- </div>
255
- )
256
-
257
- const drawerToolbarProps = {
258
- state: tableState,
259
- totalRows: sites.length,
260
- filterFields,
261
- fieldDefinitions: fieldDefinitionsForDrawer,
262
- resolveColumnLabel,
263
- displayOptions,
264
- onDisplayOptionsChange: patchDisplay,
265
- conditionalRules,
266
- onAddConditionalRule: addConditionalRule,
267
- onRemoveConditionalRule: removeConditionalRule,
268
- onUpdateConditionalRule: updateConditionalRule,
269
- currentView: view,
270
- onViewChange,
271
- lifecycleTabLabel: "Sites",
272
- }
273
-
274
- const tableProps = {
275
- data: sites,
276
- columns,
277
- getRowId: (row: SiteDirectoryRow) => row.id,
278
- getRowSelectionLabel: (row: SiteDirectoryRow) => row.name,
279
- selectable: true,
280
- searchable: displayOptions.showToolbarSearch,
281
- showColumnHeaders: displayOptions.showColumnLabels,
282
- groupable: true,
283
- defaultSort: { key: "name", dir: "asc" as const },
284
- emptyState: <p className="text-sm text-muted-foreground">No sites match your filters.</p>,
285
- conditionalRules,
286
- state: tableState,
287
- toolbarSlot: (s: ReturnType<typeof useTableState<SiteDirectoryRow>>) => (
288
- <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />
289
- ),
290
- bulkActionsSlot: (selected: Set<string | number>) => {
291
- const n = selected.size
292
- if (n === 0) return null
293
- return (
294
- <>
295
- <span className="sr-only">{n} selected</span>
296
- <Tip label="Export selection (demo)">
297
- <Button size="sm" variant="outline" type="button">
298
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
299
- Export
300
- </Button>
301
- </Tip>
302
- </>
303
- )
304
- },
305
- }
306
-
307
- if (view === "table") {
308
- return (
309
- <div className="pb-6">
310
- <DataTable<SiteDirectoryRow> {...tableProps} />
311
- </div>
312
- )
313
- }
314
-
315
- const sharedToolbar = (
316
- <DataTableToolbar
317
- state={tableState}
318
- columns={columns}
319
- searchable={displayOptions.showToolbarSearch}
320
- searchAriaLabel="Search sites"
321
- toolbarSlot={s => <TablePropertiesDrawerButton {...drawerToolbarProps} state={s} />}
322
- />
323
- )
324
-
325
- if (view === "list") {
326
- return (
327
- <div className="flex min-h-0 flex-1 flex-col">
328
- {sharedToolbar}
329
- <SitesListView rows={tableState.rows} />
330
- </div>
331
- )
332
- }
333
-
334
- if (view === "board") {
335
- return (
336
- <div className="flex min-h-0 flex-1 flex-col">
337
- {sharedToolbar}
338
- <SitesCardGrid rows={tableState.rows} />
339
- </div>
340
- )
341
- }
342
-
343
- if (view === "panel") {
344
- return (
345
- <div className="flex min-h-0 flex-1 flex-col">
346
- {sharedToolbar}
347
- <ListPageSplitHubChrome aria-label="Sites directory panel view">
348
- <FinderPanelView<SiteDirectoryRow>
349
- embedded
350
- groupsColumnTitle="Sites"
351
- groups={panelGroupsBuilder(tableState.rows)}
352
- rows={tableState.rows}
353
- getRowId={(row) => row.id}
354
- getRowGroupId={() => "all"}
355
- autoSaveId="sites-panel-view"
356
- renderListRow={panelRenderListRow}
357
- renderDetail={panelRenderDetail}
358
- emptyList={<p className="text-sm text-muted-foreground">No sites found.</p>}
359
- />
360
- </ListPageSplitHubChrome>
361
- </div>
362
- )
363
- }
364
-
365
- return (
366
- <div className="flex min-h-0 flex-1 flex-col gap-4">
367
- {sharedToolbar}
368
- <div className="px-4 pb-2 lg:px-6">
369
- <KeyMetrics
370
- variant="flat"
371
- metrics={dashMetrics}
372
- insight={SITES_KPI_INSIGHT}
373
- showHeader={false}
374
- metricsSingleRow
375
- />
376
- </div>
377
- <SitesCardGrid rows={tableState.rows} />
378
- </div>
379
- )
380
- })
381
-
382
- 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
- }