@exxatdesignux/ui 0.2.18 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/consumer-extras/AGENTS.md +76 -0
  3. package/consumer-extras/README.md +5 -1
  4. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
  5. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +21 -6
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
  9. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  10. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  11. package/consumer-extras/patterns/data-views-pattern.md +40 -3
  12. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  13. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
  14. package/package.json +2 -1
  15. package/src/components/ui/button-group.tsx +81 -0
  16. package/src/components/ui/button.tsx +4 -4
  17. package/src/globals.css +7 -1858
  18. package/src/theme.css +10 -1126
  19. package/src/tokens/README.md +15 -0
  20. package/src/tokens/base.css +337 -0
  21. package/src/tokens/high-contrast.css +1195 -0
  22. package/src/tokens/layers.css +224 -0
  23. package/src/tokens/tailwind-bridge.css +118 -0
  24. package/src/tokens/themes.css +201 -0
  25. package/template/AGENTS.md +60 -22
  26. package/template/app/(app)/dashboard/loading.tsx +3 -15
  27. package/template/app/(app)/dashboard/page.tsx +2 -14
  28. package/template/app/(app)/data-list/layout.tsx +43 -0
  29. package/template/app/(app)/data-list/page.tsx +2 -2
  30. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  31. package/template/app/(app)/examples/page.tsx +1 -0
  32. package/template/app/(app)/loading.tsx +1 -18
  33. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  34. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  35. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  36. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  37. package/template/app/(app)/question-bank/page.tsx +2 -1
  38. package/template/app/(app)/settings/page.tsx +4 -5
  39. package/template/app/globals.css +7 -1964
  40. package/template/components/app-route-loading.tsx +14 -0
  41. package/template/components/app-sidebar.tsx +70 -55
  42. package/template/components/data-views/index.ts +37 -9
  43. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  44. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  45. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  46. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  47. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  48. package/template/components/list-hub-board-view.tsx +68 -0
  49. package/template/components/list-hub-client.tsx +186 -0
  50. package/template/components/list-hub-list-view.tsx +36 -0
  51. package/template/components/list-hub-panel-activator.tsx +8 -0
  52. package/template/components/list-hub-secondary-nav.tsx +121 -0
  53. package/template/components/list-hub-table.tsx +336 -0
  54. package/template/components/new-question-composer.tsx +6 -24
  55. package/template/components/product-switcher.tsx +3 -2
  56. package/template/components/question-bank-client.tsx +4 -1
  57. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  58. package/template/components/question-bank-table.tsx +143 -485
  59. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  60. package/template/components/secondary-panel.tsx +4 -44
  61. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  62. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  63. package/template/components/secondary-panels/registry.tsx +15 -0
  64. package/template/components/settings-appearance-card.tsx +3 -2
  65. package/template/components/settings-client.tsx +59 -15
  66. package/template/components/settings-form-row.tsx +9 -4
  67. package/template/components/table-properties/drawer-button.tsx +13 -0
  68. package/template/components/table-properties/drawer.tsx +65 -4
  69. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  70. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  71. package/template/components/templates/list-page.tsx +29 -5
  72. package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
  73. package/template/components/templates/page-loading-shell.tsx +262 -0
  74. package/template/components/ui/button-group.tsx +1 -0
  75. package/template/docs/consumer-app-pattern.md +39 -0
  76. package/template/docs/data-views-pattern.md +40 -3
  77. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  78. package/template/docs/focused-workflow-page-pattern.md +84 -0
  79. package/template/docs/shell-surface-elevation-pattern.md +5 -3
  80. package/template/lib/command-menu-search-data.ts +11 -27
  81. package/template/lib/data-list-display-options.ts +16 -2
  82. package/template/lib/data-list-view-registry.ts +104 -0
  83. package/template/lib/data-list-view-surface.ts +15 -1
  84. package/template/lib/data-list-view.ts +10 -1
  85. package/template/lib/data-view-dashboard-storage.ts +38 -35
  86. package/template/lib/hub-connected-view-renderers.ts +58 -0
  87. package/template/lib/list-hub-nav.ts +121 -0
  88. package/template/lib/list-hub-supported-views.ts +10 -0
  89. package/template/lib/list-page-table-properties.ts +3 -7
  90. package/template/lib/list-status-badges.ts +4 -97
  91. package/template/lib/mock/list-hub-directory.ts +27 -0
  92. package/template/lib/mock/list-hub-kpi.ts +27 -0
  93. package/template/lib/mock/navigation.tsx +1 -0
  94. package/template/lib/page-loading-variant.ts +40 -0
  95. package/template/lib/question-bank-supported-views.ts +13 -0
  96. package/template/lib/table-state-lifecycle.ts +2 -2
  97. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  98. package/template/app/(app)/data-list/new/page.tsx +0 -34
  99. package/template/components/compliance-board-view.tsx +0 -142
  100. package/template/components/compliance-client.tsx +0 -92
  101. package/template/components/compliance-list-view.tsx +0 -54
  102. package/template/components/compliance-page-header.tsx +0 -89
  103. package/template/components/compliance-table.tsx +0 -612
  104. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  105. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  106. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  107. package/template/components/new-placement-back-btn.tsx +0 -28
  108. package/template/components/new-placement-form.tsx +0 -1068
  109. package/template/components/placement-board-card.tsx +0 -262
  110. package/template/components/placement-detail.tsx +0 -438
  111. package/template/components/placements-board-view.tsx +0 -404
  112. package/template/components/placements-client.tsx +0 -252
  113. package/template/components/placements-list-view.tsx +0 -171
  114. package/template/components/placements-page-header.tsx +0 -166
  115. package/template/components/placements-table-cells.test.tsx +0 -22
  116. package/template/components/placements-table-cells.tsx +0 -173
  117. package/template/components/placements-table-columns.tsx +0 -640
  118. package/template/components/placements-table.tsx +0 -1642
  119. package/template/components/rotations-empty-state.tsx +0 -50
  120. package/template/components/rotations-panel-activator.tsx +0 -8
  121. package/template/components/sites-all-client.tsx +0 -154
  122. package/template/components/sites-board-view.tsx +0 -67
  123. package/template/components/sites-list-view.tsx +0 -42
  124. package/template/components/sites-table.tsx +0 -382
  125. package/template/components/team-board-view.tsx +0 -122
  126. package/template/components/team-client.tsx +0 -100
  127. package/template/components/team-list-view.tsx +0 -59
  128. package/template/components/team-page-header.tsx +0 -92
  129. package/template/components/team-table.tsx +0 -693
  130. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  131. package/template/lib/mock/compliance-kpi.ts +0 -61
  132. package/template/lib/mock/compliance.ts +0 -146
  133. package/template/lib/mock/placements-kpi.ts +0 -134
  134. package/template/lib/mock/placements.ts +0 -183
  135. package/template/lib/mock/sites-directory.ts +0 -16
  136. package/template/lib/mock/sites-kpi.ts +0 -25
  137. package/template/lib/mock/team-kpi.ts +0 -60
  138. package/template/lib/mock/team.ts +0 -118
  139. package/template/lib/placement-board-card-layout.ts +0 -79
  140. package/template/lib/placement-lifecycle.ts +0 -5
@@ -0,0 +1,345 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Finder-style multi-column folder + item explorer for **panel** view tabs.
5
+ * Generic over folder and item shapes; hubs supply labels, icons, and detail renderers.
6
+ */
7
+
8
+ import * as React from "react"
9
+ import { Button } from "@/components/ui/button"
10
+ import {
11
+ Tooltip,
12
+ TooltipContent,
13
+ TooltipTrigger,
14
+ } from "@/components/ui/tooltip"
15
+ import {
16
+ ResizableHandle,
17
+ ResizablePanel,
18
+ ResizablePanelGroup,
19
+ } from "@/components/ui/resizable"
20
+ import { cn } from "@/lib/utils"
21
+ import { ListPageTreeColumnHeader } from "@/components/data-views/list-page-tree-column-header"
22
+ import {
23
+ LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS,
24
+ LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS,
25
+ LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS,
26
+ } from "@/components/data-views/list-page-split-hub-tokens"
27
+
28
+ export interface ListPageFolderColumnsPanelProps<
29
+ TFolder extends { id: string },
30
+ TItem extends { id: string },
31
+ > {
32
+ folders: TFolder[]
33
+ items: TItem[]
34
+ isFolder: (item: TFolder | TItem) => item is TFolder
35
+ getFolderParentId: (folder: TFolder) => string | null
36
+ getFolderName: (folder: TFolder) => string
37
+ getItemFolderId: (item: TItem) => string | null
38
+ getItemLabel: (item: TItem) => string
39
+ renderItemDetail: (item: TItem) => React.ReactNode
40
+ renderFolderDetail?: (
41
+ folder: TFolder,
42
+ ctx: { folders: TFolder[]; items: TItem[] },
43
+ ) => React.ReactNode
44
+ renderItemMeta?: (item: TItem, ctx: { isSelected: boolean }) => React.ReactNode
45
+ renderFolderIcon?: (
46
+ folder: TFolder,
47
+ ctx: { isSelected: boolean; depth: number },
48
+ ) => React.ReactNode
49
+ renderItemIcon?: (item: TItem, ctx: { isSelected: boolean }) => React.ReactNode
50
+ renderFolderRowClassName?: (
51
+ folder: TFolder,
52
+ ctx: { isSelected: boolean; depth: number },
53
+ ) => string | undefined
54
+ renderFolderActions?: (folder: TFolder) => React.ReactNode
55
+ onAddFolder: (parentId: string | null) => void
56
+ onAddItem?: (parentId: string | null) => void
57
+ rootColumnTitle?: string
58
+ addFolderAriaLabel?: string
59
+ addItemAriaLabel?: string
60
+ }
61
+
62
+ export function ListPageFolderColumnsPanel<
63
+ TFolder extends { id: string },
64
+ TItem extends { id: string },
65
+ >({
66
+ folders,
67
+ items,
68
+ isFolder,
69
+ getFolderParentId,
70
+ getFolderName,
71
+ getItemFolderId,
72
+ getItemLabel,
73
+ renderItemDetail,
74
+ renderFolderDetail,
75
+ renderItemMeta,
76
+ renderFolderIcon,
77
+ renderItemIcon,
78
+ renderFolderRowClassName,
79
+ renderFolderActions,
80
+ onAddFolder,
81
+ onAddItem,
82
+ rootColumnTitle = "Categories",
83
+ addFolderAriaLabel = "Add folder",
84
+ addItemAriaLabel = "Add item",
85
+ }: ListPageFolderColumnsPanelProps<TFolder, TItem>) {
86
+ type HierarchyItem = TFolder | TItem
87
+
88
+ const [selectedPath, setSelectedPath] = React.useState<HierarchyItem[]>(() => {
89
+ const roots = folders
90
+ .filter(f => getFolderParentId(f) === null)
91
+ .sort((a, b) => getFolderName(a).localeCompare(getFolderName(b)))
92
+ return roots.length > 0 ? [roots[0]] : []
93
+ })
94
+
95
+ const isFirstRenderRef = React.useRef(true)
96
+
97
+ const rootFolders = React.useMemo(() => {
98
+ return folders
99
+ .filter(f => getFolderParentId(f) === null)
100
+ .sort((a, b) => getFolderName(a).localeCompare(getFolderName(b)))
101
+ }, [folders, getFolderParentId, getFolderName])
102
+
103
+ const handleSelect = (item: HierarchyItem, depth: number) => {
104
+ setSelectedPath(prev => [...prev.slice(0, depth), item])
105
+ }
106
+
107
+ React.useEffect(() => {
108
+ if (isFirstRenderRef.current && selectedPath.length > 0) {
109
+ const lastItem = selectedPath[selectedPath.length - 1]
110
+ if (isFolder(lastItem)) {
111
+ const subfolders = folders
112
+ .filter(f => getFolderParentId(f) === lastItem.id)
113
+ .sort((a, b) => getFolderName(a).localeCompare(getFolderName(b)))
114
+ const childItems = items.filter(i => getItemFolderId(i) === lastItem.id)
115
+ const next: HierarchyItem[] = [...subfolders, ...childItems]
116
+
117
+ if (next.length > 0 && !selectedPath[selectedPath.length + 1]) {
118
+ setSelectedPath(prev => [...prev, next[0]])
119
+ isFirstRenderRef.current = false
120
+ }
121
+ }
122
+ }
123
+ // eslint-disable-next-line react-hooks/exhaustive-deps
124
+ }, [])
125
+
126
+ const columns: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> =
127
+ React.useMemo(() => {
128
+ const cols: Array<{ items: HierarchyItem[]; depth: number; parentId?: string | null }> = [
129
+ { items: rootFolders, depth: 0, parentId: null },
130
+ ]
131
+
132
+ for (let i = 0; i < selectedPath.length; i++) {
133
+ const item = selectedPath[i]
134
+ if (isFolder(item)) {
135
+ const subfolders = folders
136
+ .filter(f => getFolderParentId(f) === item.id)
137
+ .sort((a, b) => getFolderName(a).localeCompare(getFolderName(b)))
138
+ const childItems = items.filter(i => getItemFolderId(i) === item.id)
139
+ const columnItems: HierarchyItem[] = [...subfolders, ...childItems]
140
+
141
+ if (columnItems.length > 0) {
142
+ cols.push({ items: columnItems, depth: i + 1, parentId: item.id })
143
+ }
144
+ }
145
+ }
146
+
147
+ return cols
148
+ }, [selectedPath, rootFolders, folders, items, isFolder, getFolderParentId, getFolderName, getItemFolderId])
149
+
150
+ const selectedLeaf = selectedPath.length > 0 ? selectedPath.at(-1)! : null
151
+ const selectedItem =
152
+ selectedLeaf && !isFolder(selectedLeaf) ? (selectedLeaf as TItem) : null
153
+ const selectedFolderLeaf =
154
+ selectedLeaf && isFolder(selectedLeaf) ? (selectedLeaf as TFolder) : null
155
+
156
+ const defaultFolderIcon = (selected: boolean) => (
157
+ <i
158
+ className={cn("fa-folder shrink-0 text-sm", selected ? "fa-solid" : "fa-light")}
159
+ aria-hidden="true"
160
+ />
161
+ )
162
+
163
+ const defaultItemIcon = (selected: boolean) => (
164
+ <i
165
+ className={cn("fa-file shrink-0 text-sm", selected ? "fa-solid" : "fa-light")}
166
+ aria-hidden="true"
167
+ />
168
+ )
169
+
170
+ return (
171
+ <ResizablePanelGroup
172
+ direction="horizontal"
173
+ className="flex h-full min-h-0 w-full flex-1 overflow-hidden"
174
+ >
175
+ {columns.map(({ items: columnItems, depth, parentId }, columnIdx) => (
176
+ <React.Fragment key={`col-${depth}`}>
177
+ {columnIdx > 0 && (
178
+ <ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
179
+ )}
180
+ <ResizablePanel
181
+ id={`col-${depth}`}
182
+ defaultSize={columnIdx === 0 ? 35 : columnIdx === 1 ? 35 : 30}
183
+ minSize={15}
184
+ className={LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS}
185
+ >
186
+ <ListPageTreeColumnHeader
187
+ title={
188
+ depth === 0
189
+ ? rootColumnTitle
190
+ : selectedPath[depth - 1] && isFolder(selectedPath[depth - 1])
191
+ ? getFolderName(selectedPath[depth - 1] as TFolder)
192
+ : "Items"
193
+ }
194
+ trailing={
195
+ <>
196
+ <span className="shrink-0 text-xs font-medium text-muted-foreground tabular-nums">
197
+ {columnItems.length}
198
+ </span>
199
+ {depth < columns.length - 1 && columnItems.length > 0 ? (
200
+ <div className="flex shrink-0 items-center gap-0.5">
201
+ <Tooltip>
202
+ <TooltipTrigger asChild>
203
+ <Button
204
+ size="icon-sm"
205
+ variant="ghost"
206
+ onClick={() => onAddFolder(parentId ?? null)}
207
+ aria-label={addFolderAriaLabel}
208
+ >
209
+ <i className="fa-light fa-folder-plus text-xs" aria-hidden="true" />
210
+ </Button>
211
+ </TooltipTrigger>
212
+ <TooltipContent side="top" sideOffset={4}>
213
+ {addFolderAriaLabel}
214
+ </TooltipContent>
215
+ </Tooltip>
216
+ {onAddItem ? (
217
+ <Tooltip>
218
+ <TooltipTrigger asChild>
219
+ <Button
220
+ size="icon-sm"
221
+ variant="ghost"
222
+ onClick={() => onAddItem(parentId ?? null)}
223
+ aria-label={addItemAriaLabel}
224
+ >
225
+ <i className="fa-light fa-plus text-xs" aria-hidden="true" />
226
+ </Button>
227
+ </TooltipTrigger>
228
+ <TooltipContent side="top" sideOffset={4}>
229
+ {addItemAriaLabel}
230
+ </TooltipContent>
231
+ </Tooltip>
232
+ ) : null}
233
+ </div>
234
+ ) : null}
235
+ </>
236
+ }
237
+ />
238
+
239
+ <div className="min-h-0 flex-1 overflow-y-auto py-1">
240
+ {columnItems.map(item => {
241
+ const isSelected = selectedPath[depth]?.id === item.id
242
+ const isFolderRow = isFolder(item)
243
+ const folder = isFolderRow ? item : null
244
+ const rowItem = isFolderRow ? null : (item as TItem)
245
+
246
+ const subfolderCount = isFolderRow
247
+ ? folders.filter(f => getFolderParentId(f) === item.id).length
248
+ : 0
249
+ const childItemCount = isFolderRow
250
+ ? items.filter(i => getItemFolderId(i) === item.id).length
251
+ : 0
252
+ const itemCount = subfolderCount + childItemCount
253
+
254
+ return (
255
+ <div
256
+ key={item.id}
257
+ className="group flex items-center hover:bg-muted/50"
258
+ >
259
+ <button
260
+ type="button"
261
+ onClick={() => handleSelect(item, depth)}
262
+ className={cn(
263
+ "flex flex-1 items-center gap-3 px-3 py-2 text-left text-sm transition-colors duration-75",
264
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
265
+ isSelected
266
+ ? "bg-accent text-accent-foreground"
267
+ : "text-foreground",
268
+ isFolderRow && folder
269
+ ? renderFolderRowClassName?.(folder, { isSelected, depth })
270
+ : undefined,
271
+ )}
272
+ aria-selected={isSelected}
273
+ role="option"
274
+ >
275
+ {isFolderRow && folder ? (
276
+ renderFolderIcon?.(folder, { isSelected, depth }) ??
277
+ defaultFolderIcon(isSelected)
278
+ ) : rowItem ? (
279
+ renderItemIcon?.(rowItem, { isSelected }) ??
280
+ defaultItemIcon(isSelected)
281
+ ) : null}
282
+
283
+ <span
284
+ className={cn(
285
+ "min-w-0 flex-1 truncate leading-tight",
286
+ isSelected && "font-medium",
287
+ )}
288
+ >
289
+ {isFolderRow && folder
290
+ ? getFolderName(folder)
291
+ : rowItem
292
+ ? getItemLabel(rowItem)
293
+ : ""}
294
+ </span>
295
+
296
+ <span
297
+ className={cn(
298
+ "ml-auto shrink-0 text-xs tabular-nums",
299
+ isSelected ? "text-accent-foreground/70" : "text-muted-foreground",
300
+ )}
301
+ >
302
+ {isFolderRow
303
+ ? itemCount
304
+ : rowItem
305
+ ? (renderItemMeta?.(rowItem, { isSelected }) ?? null)
306
+ : null}
307
+ </span>
308
+ </button>
309
+
310
+ {isFolderRow && folder && renderFolderActions?.(folder)}
311
+ </div>
312
+ )
313
+ })}
314
+ </div>
315
+ </ResizablePanel>
316
+ </React.Fragment>
317
+ ))}
318
+
319
+ {(selectedItem || selectedFolderLeaf) && (
320
+ <>
321
+ <ResizableHandle withHandle className={LIST_PAGE_SPLIT_RESIZABLE_HANDLE_CLASS} />
322
+ <ResizablePanel
323
+ id="col-detail"
324
+ defaultSize={30}
325
+ minSize={20}
326
+ className={LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS}
327
+ >
328
+ {selectedItem ? (
329
+ <>
330
+ <ListPageTreeColumnHeader title="Details" className="px-4" />
331
+ <div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
332
+ {renderItemDetail(selectedItem)}
333
+ </div>
334
+ </>
335
+ ) : selectedFolderLeaf && renderFolderDetail ? (
336
+ <div className="min-h-0 flex-1 overflow-hidden">
337
+ {renderFolderDetail(selectedFolderLeaf, { folders, items })}
338
+ </div>
339
+ ) : null}
340
+ </ResizablePanel>
341
+ </>
342
+ )}
343
+ </ResizablePanelGroup>
344
+ )
345
+ }
@@ -21,6 +21,14 @@ export const LIST_PAGE_SPLIT_HUB_HEIGHT_STYLE: React.CSSProperties = {
21
21
  minHeight: 420,
22
22
  }
23
23
 
24
+ /**
25
+ * Viewport band for calendar under `ListPageTemplate` + hub toolbar (+ optional metrics).
26
+ * Uses a fixed height (not min-height) so the week strip scrolls inside the card, not the page.
27
+ */
28
+ export const LIST_PAGE_CALENDAR_HEIGHT_STYLE: React.CSSProperties = {
29
+ height: "clamp(420px, calc(100dvh - 22rem), 880px)",
30
+ }
31
+
24
32
  const SURFACE_CLASS =
25
33
  "flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card"
26
34
 
@@ -0,0 +1,183 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Patterns / examples — `FocusedWorkflowPageTemplate` layout variants.
5
+ */
6
+
7
+ import * as React from "react"
8
+ import Link from "next/link"
9
+
10
+ import { PageHeader } from "@/components/page-header"
11
+ import { Button } from "@/components/ui/button"
12
+ import { Input } from "@/components/ui/input"
13
+ import { Label } from "@/components/ui/label"
14
+ import { ViewSegmentedControl } from "@/components/ui/view-segmented-control"
15
+ import {
16
+ FocusedWorkflowEmptyState,
17
+ FocusedWorkflowSingleColumn,
18
+ FocusedWorkflowStepForm,
19
+ FocusedWorkflowSidebarSections,
20
+ FocusedWorkflowWizardFooter,
21
+ type FocusedWorkflowStep,
22
+ } from "@/components/templates/focused-workflow-layouts"
23
+ import {
24
+ FocusedWorkflowPageTemplate,
25
+ FOCUSED_WORKFLOW_CONTENT_PADDING_CLASS,
26
+ } from "@/components/templates/focused-workflow-page-template"
27
+
28
+ const DEMO_STEPS: FocusedWorkflowStep[] = [
29
+ {
30
+ id: "basics",
31
+ label: "Basics",
32
+ description: "Name, owner, and schedule for this record.",
33
+ },
34
+ {
35
+ id: "details",
36
+ label: "Details",
37
+ description: "Optional fields and attachments.",
38
+ },
39
+ {
40
+ id: "review",
41
+ label: "Review",
42
+ description: "Confirm before you submit.",
43
+ },
44
+ ]
45
+
46
+ const DEMO_SECTIONS = [
47
+ { id: "general", label: "General" },
48
+ { id: "notifications", label: "Notifications" },
49
+ { id: "security", label: "Security" },
50
+ ] as const
51
+
52
+ type DemoVariant = "empty" | "steps" | "sidebar"
53
+
54
+ const VARIANT_OPTIONS: { value: DemoVariant; label: string }[] = [
55
+ { value: "empty", label: "Empty" },
56
+ { value: "steps", label: "Steps" },
57
+ { value: "sidebar", label: "Sidebar" },
58
+ ]
59
+
60
+ export function FocusedWorkflowShowcase() {
61
+ const [variant, setVariant] = React.useState<DemoVariant>("steps")
62
+ const [stepIndex, setStepIndex] = React.useState(0)
63
+ const [activeSection, setActiveSection] = React.useState<string>("general")
64
+
65
+ const siteHeader = {
66
+ back: { label: "Patterns", href: "/examples" },
67
+ title: "Focused workflow layouts",
68
+ }
69
+
70
+ const shell = (body: React.ReactNode, maxWidth: "md" | "lg" = "lg") => (
71
+ <FocusedWorkflowPageTemplate
72
+ siteHeader={siteHeader}
73
+ maxWidth={maxWidth}
74
+ contentClassName={FOCUSED_WORKFLOW_CONTENT_PADDING_CLASS}
75
+ >
76
+ <FocusedWorkflowSingleColumn>
77
+ <PageHeader
78
+ title="Focused workflow"
79
+ subtitle="Dedicated routes for large forms and multi-step actions — not list hubs or split-column explorers."
80
+ actions={
81
+ <ViewSegmentedControl
82
+ value={variant}
83
+ onValueChange={v => setVariant(v as DemoVariant)}
84
+ options={VARIANT_OPTIONS}
85
+ aria-label="Layout variant"
86
+ />
87
+ }
88
+ />
89
+ {body}
90
+ </FocusedWorkflowSingleColumn>
91
+ </FocusedWorkflowPageTemplate>
92
+ )
93
+
94
+ if (variant === "empty") {
95
+ return shell(
96
+ <FocusedWorkflowEmptyState
97
+ title="No draft yet"
98
+ description="Start a new record from the hub, or pick a template to pre-fill common fields."
99
+ action={
100
+ <Button type="button" size="lg">
101
+ Create record
102
+ </Button>
103
+ }
104
+ />,
105
+ )
106
+ }
107
+
108
+ if (variant === "sidebar") {
109
+ return shell(
110
+ <FocusedWorkflowSidebarSections
111
+ sections={DEMO_SECTIONS}
112
+ activeSectionId={activeSection}
113
+ onSectionSelect={id => {
114
+ setActiveSection(id)
115
+ document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" })
116
+ }}
117
+ >
118
+ <section id="general" className="scroll-mt-20 space-y-4">
119
+ <header className="space-y-1">
120
+ <h2 className="text-lg font-semibold text-foreground">General</h2>
121
+ <p className="text-sm text-muted-foreground">
122
+ Settings-style section nav — same shell as long preference forms.
123
+ </p>
124
+ </header>
125
+ <div className="space-y-2">
126
+ <Label htmlFor="fw-name">Display name</Label>
127
+ <Input id="fw-name" defaultValue="Clinical education workspace" />
128
+ </div>
129
+ </section>
130
+ <section id="notifications" className="scroll-mt-20 space-y-4">
131
+ <header className="space-y-1">
132
+ <h2 className="text-lg font-semibold text-foreground">Notifications</h2>
133
+ <p className="text-sm text-muted-foreground">Email and in-app alerts.</p>
134
+ </header>
135
+ <p className="text-sm text-muted-foreground">Wire toggles and checkboxes here.</p>
136
+ </section>
137
+ <section id="security" className="scroll-mt-20 space-y-4">
138
+ <header className="space-y-1">
139
+ <h2 className="text-lg font-semibold text-foreground">Security</h2>
140
+ <p className="text-sm text-muted-foreground">Sessions and access.</p>
141
+ </header>
142
+ <Button type="button" variant="outline" asChild>
143
+ <Link href="/settings">Open full settings</Link>
144
+ </Button>
145
+ </section>
146
+ </FocusedWorkflowSidebarSections>,
147
+ "lg",
148
+ )
149
+ }
150
+
151
+ return shell(
152
+ <FocusedWorkflowStepForm
153
+ steps={DEMO_STEPS}
154
+ currentIndex={stepIndex}
155
+ onStepSelect={setStepIndex}
156
+ footer={
157
+ <FocusedWorkflowWizardFooter
158
+ stepIndex={stepIndex}
159
+ stepCount={DEMO_STEPS.length}
160
+ onBack={() => setStepIndex(i => Math.max(0, i - 1))}
161
+ onCancel={() => setStepIndex(0)}
162
+ onNext={() => setStepIndex(i => Math.min(DEMO_STEPS.length - 1, i + 1))}
163
+ onSubmit={() => setStepIndex(0)}
164
+ submitLabel="Create record"
165
+ />
166
+ }
167
+ >
168
+ <div className="rounded-lg border border-border bg-card p-6">
169
+ <h2 className="text-base font-semibold text-foreground">
170
+ {DEMO_STEPS[stepIndex]?.label}
171
+ </h2>
172
+ <p className="mt-2 text-sm text-muted-foreground">
173
+ {DEMO_STEPS[stepIndex]?.description} Replace this panel with your step fields.
174
+ </p>
175
+ <div className="mt-6 space-y-2">
176
+ <Label htmlFor="fw-demo-field">Example field</Label>
177
+ <Input id="fw-demo-field" placeholder="Required on this step" />
178
+ </div>
179
+ </div>
180
+ </FocusedWorkflowStepForm>,
181
+ "md",
182
+ )
183
+ }
@@ -0,0 +1,68 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import type { ListHubRecord } from "@/lib/mock/list-hub-directory"
5
+ import { formatDateUS } from "@/lib/date-filter"
6
+ import { BoardCardIconRow, BoardCardTwoLineBlock } from "@/components/data-views/board-card-primitives"
7
+ import {
8
+ HubRecordCard,
9
+ ListPageBoardCardBody,
10
+ ListPageBoardCardHeader,
11
+ ListPageBoardCardTitleRow,
12
+ } from "@/components/data-views/list-page-board-card"
13
+ import {
14
+ ListPageBoardTemplate,
15
+ type ListPageBoardColumnDef,
16
+ } from "@/components/data-views/list-page-board-template"
17
+
18
+ const CATEGORY_COUNT_BADGE = "bg-muted/90 text-foreground"
19
+
20
+ function ListHubBoardCard({ row }: { row: ListHubRecord }) {
21
+ return (
22
+ <HubRecordCard interactive className="h-full w-full">
23
+ <ListPageBoardCardHeader>
24
+ <ListPageBoardCardTitleRow title={row.title} titleClassName="truncate" />
25
+ <ListPageBoardCardBody>
26
+ <BoardCardTwoLineBlock
27
+ iconClass="fa-tag"
28
+ line1={row.category}
29
+ line2={formatDateUS(row.eventDate)}
30
+ />
31
+ <BoardCardIconRow iconClass="fa-hashtag">
32
+ <span className="font-mono tabular-nums">{row.id}</span>
33
+ </BoardCardIconRow>
34
+ </ListPageBoardCardBody>
35
+ </ListPageBoardCardHeader>
36
+ </HubRecordCard>
37
+ )
38
+ }
39
+
40
+ function useListHubBoardColumns(rows: ListHubRecord[]) {
41
+ return React.useMemo(() => {
42
+ const categories = [...new Set(rows.map(r => r.category))].sort((a, b) => a.localeCompare(b))
43
+ const columns: ListPageBoardColumnDef<ListHubRecord>[] = categories.map(category => ({
44
+ id: category,
45
+ label: category,
46
+ filter: (r: ListHubRecord) => r.category === category,
47
+ }))
48
+ const badgeMap = Object.fromEntries(
49
+ categories.map(c => [c, CATEGORY_COUNT_BADGE]),
50
+ )
51
+ return { columns, badgeMap }
52
+ }, [rows])
53
+ }
54
+
55
+ export function ListHubCardGrid({ rows }: { rows: ListHubRecord[] }) {
56
+ const { columns, badgeMap } = useListHubBoardColumns(rows)
57
+
58
+ return (
59
+ <ListPageBoardTemplate
60
+ columns={columns}
61
+ rows={rows}
62
+ getRowKey={r => r.id}
63
+ columnCountBadgeClassName={badgeMap}
64
+ emptyColumnLabel="No records"
65
+ renderCard={row => <ListHubBoardCard row={row} />}
66
+ />
67
+ )
68
+ }