@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.
- package/CHANGELOG.md +15 -0
- package/consumer-extras/AGENTS.md +76 -0
- package/consumer-extras/README.md +5 -1
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
- package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +21 -6
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
- package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
- package/consumer-extras/patterns/data-views-pattern.md +40 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/globals.css +7 -1858
- package/src/theme.css +10 -1126
- package/src/tokens/README.md +15 -0
- package/src/tokens/base.css +337 -0
- package/src/tokens/high-contrast.css +1195 -0
- package/src/tokens/layers.css +224 -0
- package/src/tokens/tailwind-bridge.css +118 -0
- package/src/tokens/themes.css +201 -0
- package/template/AGENTS.md +60 -22
- package/template/app/(app)/dashboard/loading.tsx +3 -15
- package/template/app/(app)/dashboard/page.tsx +2 -14
- package/template/app/(app)/data-list/layout.tsx +43 -0
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/loading.tsx +1 -18
- package/template/app/(app)/question-bank/find/page.tsx +2 -1
- package/template/app/(app)/question-bank/library/page.tsx +2 -1
- package/template/app/(app)/question-bank/list/page.tsx +2 -1
- package/template/app/(app)/question-bank/new/page.tsx +15 -23
- package/template/app/(app)/question-bank/page.tsx +2 -1
- package/template/app/(app)/settings/page.tsx +4 -5
- package/template/app/globals.css +7 -1964
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +70 -55
- package/template/components/data-views/index.ts +37 -9
- package/template/components/data-views/list-page-calendar-view.tsx +593 -0
- package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/list-hub-board-view.tsx +68 -0
- package/template/components/list-hub-client.tsx +186 -0
- package/template/components/list-hub-list-view.tsx +36 -0
- package/template/components/list-hub-panel-activator.tsx +8 -0
- package/template/components/list-hub-secondary-nav.tsx +121 -0
- package/template/components/list-hub-table.tsx +336 -0
- package/template/components/new-question-composer.tsx +6 -24
- package/template/components/product-switcher.tsx +3 -2
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-folder-columns-panel.tsx +104 -0
- package/template/components/question-bank-table.tsx +143 -485
- package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
- package/template/components/secondary-panel.tsx +4 -44
- package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
- package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
- package/template/components/secondary-panels/registry.tsx +15 -0
- package/template/components/settings-appearance-card.tsx +3 -2
- package/template/components/settings-client.tsx +59 -15
- package/template/components/settings-form-row.tsx +9 -4
- package/template/components/table-properties/drawer-button.tsx +13 -0
- package/template/components/table-properties/drawer.tsx +65 -4
- package/template/components/templates/focused-workflow-layouts.tsx +448 -0
- package/template/components/templates/focused-workflow-page-template.tsx +69 -0
- package/template/components/templates/list-page.tsx +29 -5
- package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +40 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/shell-surface-elevation-pattern.md +5 -3
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/data-list-display-options.ts +16 -2
- package/template/lib/data-list-view-registry.ts +104 -0
- package/template/lib/data-list-view-surface.ts +15 -1
- package/template/lib/data-list-view.ts +10 -1
- package/template/lib/data-view-dashboard-storage.ts +38 -35
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-nav.ts +121 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +3 -7
- package/template/lib/list-status-badges.ts +4 -97
- package/template/lib/mock/list-hub-directory.ts +27 -0
- package/template/lib/mock/list-hub-kpi.ts +27 -0
- package/template/lib/mock/navigation.tsx +1 -0
- package/template/lib/page-loading-variant.ts +40 -0
- package/template/lib/question-bank-supported-views.ts +13 -0
- package/template/lib/table-state-lifecycle.ts +2 -2
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -612
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -1068
- package/template/components/placement-board-card.tsx +0 -262
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -404
- package/template/components/placements-client.tsx +0 -252
- package/template/components/placements-list-view.tsx +0 -171
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -640
- package/template/components/placements-table.tsx +0 -1642
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-all-client.tsx +0 -154
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/sites-table.tsx +0 -382
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-list-view.tsx +0 -59
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -693
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -183
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- 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
|
-
}
|