@exxatdesignux/ui 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/consumer-extras/AGENTS.md +76 -0
  3. package/consumer-extras/README.md +5 -1
  4. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
  5. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
  9. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
  10. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  11. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  12. package/consumer-extras/patterns/data-views-pattern.md +42 -3
  13. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  14. package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
  15. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
  16. package/package.json +2 -1
  17. package/src/components/ui/button-group.tsx +81 -0
  18. package/src/components/ui/button.tsx +4 -4
  19. package/src/components/ui/sidebar.tsx +2 -2
  20. package/src/globals.css +7 -1807
  21. package/src/theme.css +10 -1126
  22. package/src/tokens/README.md +15 -0
  23. package/src/tokens/base.css +337 -0
  24. package/src/tokens/high-contrast.css +1195 -0
  25. package/src/tokens/layers.css +224 -0
  26. package/src/tokens/tailwind-bridge.css +118 -0
  27. package/src/tokens/themes.css +201 -0
  28. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
  29. package/template/AGENTS.md +66 -21
  30. package/template/app/(app)/dashboard/loading.tsx +3 -15
  31. package/template/app/(app)/dashboard/page.tsx +2 -14
  32. package/template/app/(app)/data-list/layout.tsx +43 -0
  33. package/template/app/(app)/data-list/page.tsx +2 -2
  34. package/template/app/(app)/error.tsx +22 -6
  35. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  36. package/template/app/(app)/examples/page.tsx +1 -0
  37. package/template/app/(app)/layout.tsx +13 -6
  38. package/template/app/(app)/loading.tsx +1 -18
  39. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  40. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  41. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  42. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  43. package/template/app/(app)/question-bank/page.tsx +2 -1
  44. package/template/app/(app)/settings/page.tsx +4 -5
  45. package/template/app/global-error.tsx +63 -0
  46. package/template/app/globals.css +7 -1934
  47. package/template/app/layout.tsx +2 -0
  48. package/template/components/app-route-loading.tsx +14 -0
  49. package/template/components/app-sidebar.tsx +71 -55
  50. package/template/components/data-table/index.tsx +31 -67
  51. package/template/components/data-table/use-table-state.ts +33 -6
  52. package/template/components/data-views/index.ts +37 -9
  53. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  54. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  55. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  56. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  57. package/template/components/dev-chunk-load-recovery.tsx +41 -0
  58. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  59. package/template/components/exxat-product-logo.tsx +2 -6
  60. package/template/components/key-metrics.tsx +54 -22
  61. package/template/components/list-hub-board-view.tsx +68 -0
  62. package/template/components/list-hub-client.tsx +186 -0
  63. package/template/components/list-hub-list-view.tsx +36 -0
  64. package/template/components/list-hub-panel-activator.tsx +8 -0
  65. package/template/components/list-hub-secondary-nav.tsx +121 -0
  66. package/template/components/list-hub-table.tsx +336 -0
  67. package/template/components/new-question-composer.tsx +6 -24
  68. package/template/components/product-switcher.tsx +5 -5
  69. package/template/components/product-wordmark.tsx +4 -7
  70. package/template/components/question-bank-client.tsx +4 -1
  71. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  72. package/template/components/question-bank-hub-client.tsx +2 -5
  73. package/template/components/question-bank-table.tsx +155 -509
  74. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  75. package/template/components/secondary-panel.tsx +4 -44
  76. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  77. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  78. package/template/components/secondary-panels/registry.tsx +15 -0
  79. package/template/components/settings-appearance-card.tsx +3 -2
  80. package/template/components/settings-client.tsx +59 -15
  81. package/template/components/settings-form-row.tsx +9 -4
  82. package/template/components/sidebar-shell.tsx +2 -1
  83. package/template/components/table-properties/drawer-button.tsx +51 -20
  84. package/template/components/table-properties/drawer.tsx +81 -17
  85. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  86. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  87. package/template/components/templates/list-page.tsx +40 -13
  88. package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
  89. package/template/components/templates/page-loading-shell.tsx +262 -0
  90. package/template/components/ui/button-group.tsx +1 -0
  91. package/template/contexts/product-context.tsx +21 -2
  92. package/template/docs/consumer-app-pattern.md +39 -0
  93. package/template/docs/data-views-pattern.md +42 -3
  94. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  95. package/template/docs/focused-workflow-page-pattern.md +84 -0
  96. package/template/docs/kpi-flat-band-pattern.md +57 -0
  97. package/template/docs/kpi-strip-max-four-pattern.md +1 -0
  98. package/template/docs/shell-surface-elevation-pattern.md +54 -0
  99. package/template/lib/chunk-load-error.ts +13 -0
  100. package/template/lib/command-menu-search-data.ts +11 -27
  101. package/template/lib/conditional-rule-match.ts +87 -22
  102. package/template/lib/data-list-display-options.ts +16 -2
  103. package/template/lib/data-list-view-registry.ts +104 -0
  104. package/template/lib/data-list-view-surface.ts +15 -1
  105. package/template/lib/data-list-view.ts +16 -1
  106. package/template/lib/data-view-dashboard-storage.ts +38 -35
  107. package/template/lib/hub-connected-view-renderers.ts +58 -0
  108. package/template/lib/list-hub-nav.ts +121 -0
  109. package/template/lib/list-hub-supported-views.ts +10 -0
  110. package/template/lib/list-page-table-properties.ts +3 -7
  111. package/template/lib/list-status-badges.ts +4 -97
  112. package/template/lib/mock/list-hub-directory.ts +27 -0
  113. package/template/lib/mock/list-hub-kpi.ts +27 -0
  114. package/template/lib/mock/navigation.tsx +1 -0
  115. package/template/lib/page-loading-variant.ts +40 -0
  116. package/template/lib/question-bank-supported-views.ts +13 -0
  117. package/template/lib/sidebar-state-cookie.ts +9 -0
  118. package/template/lib/table-state-lifecycle.ts +60 -13
  119. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  120. package/template/app/(app)/data-list/new/page.tsx +0 -34
  121. package/template/components/compliance-board-view.tsx +0 -142
  122. package/template/components/compliance-client.tsx +0 -92
  123. package/template/components/compliance-list-view.tsx +0 -54
  124. package/template/components/compliance-page-header.tsx +0 -89
  125. package/template/components/compliance-table.tsx +0 -632
  126. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  127. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  128. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  129. package/template/components/new-placement-back-btn.tsx +0 -28
  130. package/template/components/new-placement-form.tsx +0 -1068
  131. package/template/components/placement-board-card.tsx +0 -262
  132. package/template/components/placement-detail.tsx +0 -438
  133. package/template/components/placements-board-view.tsx +0 -404
  134. package/template/components/placements-client.tsx +0 -252
  135. package/template/components/placements-list-view.tsx +0 -171
  136. package/template/components/placements-page-header.tsx +0 -166
  137. package/template/components/placements-table-cells.test.tsx +0 -22
  138. package/template/components/placements-table-cells.tsx +0 -173
  139. package/template/components/placements-table-columns.tsx +0 -640
  140. package/template/components/placements-table.tsx +0 -1675
  141. package/template/components/rotations-empty-state.tsx +0 -50
  142. package/template/components/rotations-panel-activator.tsx +0 -8
  143. package/template/components/sites-all-client.tsx +0 -154
  144. package/template/components/sites-board-view.tsx +0 -67
  145. package/template/components/sites-list-view.tsx +0 -42
  146. package/template/components/sites-table.tsx +0 -402
  147. package/template/components/team-board-view.tsx +0 -122
  148. package/template/components/team-client.tsx +0 -100
  149. package/template/components/team-list-view.tsx +0 -59
  150. package/template/components/team-page-header.tsx +0 -92
  151. package/template/components/team-table.tsx +0 -714
  152. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  153. package/template/lib/mock/compliance-kpi.ts +0 -61
  154. package/template/lib/mock/compliance.ts +0 -146
  155. package/template/lib/mock/placements-kpi.ts +0 -134
  156. package/template/lib/mock/placements.ts +0 -183
  157. package/template/lib/mock/sites-directory.ts +0 -16
  158. package/template/lib/mock/sites-kpi.ts +0 -25
  159. package/template/lib/mock/team-kpi.ts +0 -60
  160. package/template/lib/mock/team.ts +0 -118
  161. package/template/lib/placement-board-card-layout.ts +0 -79
  162. package/template/lib/placement-lifecycle.ts +0 -5
@@ -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,41 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { isChunkLoadError } from "@/lib/chunk-load-error"
6
+
7
+ const RELOAD_FLAG = "exxat-ds:chunk-reload-attempted"
8
+
9
+ /**
10
+ * Dev-only: auto-reload once when Turbopack serves a stale chunk hash so users
11
+ * are not stuck on a blank shell before the route error boundary mounts.
12
+ */
13
+ export function DevChunkLoadRecovery() {
14
+ React.useEffect(() => {
15
+ if (process.env.NODE_ENV !== "development") return
16
+
17
+ function maybeReload(error: unknown) {
18
+ if (!isChunkLoadError(error)) return
19
+ if (typeof window === "undefined") return
20
+ if (window.sessionStorage.getItem(RELOAD_FLAG) === "1") return
21
+ window.sessionStorage.setItem(RELOAD_FLAG, "1")
22
+ window.location.reload()
23
+ }
24
+
25
+ const onError = (event: ErrorEvent) => {
26
+ maybeReload(event.error ?? event.message)
27
+ }
28
+ const onRejection = (event: PromiseRejectionEvent) => {
29
+ maybeReload(event.reason)
30
+ }
31
+
32
+ window.addEventListener("error", onError)
33
+ window.addEventListener("unhandledrejection", onRejection)
34
+ return () => {
35
+ window.removeEventListener("error", onError)
36
+ window.removeEventListener("unhandledrejection", onRejection)
37
+ }
38
+ }, [])
39
+
40
+ return null
41
+ }
@@ -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
+ }
@@ -38,7 +38,7 @@ export type ExxatProductLogoVariant = "default" | "mutedSuffix"
38
38
  export interface ExxatProductLogoProps {
39
39
  product: Product
40
40
  className?: string
41
- /** Sidebar / switcher: muted wordmark in dark mode only; light keeps brand color. */
41
+ /** Reserved for switcher chrome; suffix stays Exxat pink in all modes. */
42
42
  variant?: ExxatProductLogoVariant
43
43
  }
44
44
 
@@ -191,7 +191,6 @@ export function ExxatProductLogo({
191
191
  const customProductBrand = useAppStore(s => s.customProductBrand)
192
192
  const productBrandColors = useAppStore(s => s.productBrandColors)
193
193
  const config = brandForProduct(product, customProductBrand, productBrandColors)
194
- const muted = variant === "mutedSuffix"
195
194
  const suffixColor = config.wordmarkColor ?? config.brandColor
196
195
 
197
196
  return (
@@ -213,10 +212,7 @@ export function ExxatProductLogo({
213
212
  {/* HTML suffix — IvyPresto Text SemiBold per Figma brand spec. */}
214
213
  <span
215
214
  data-product-wordmark-suffix
216
- className={cn(
217
- "ms-[0.18em] text-[1.55em] font-semibold tracking-[-0.03em] -translate-y-[3px]",
218
- muted && "dark:!text-[var(--muted-foreground)]",
219
- )}
215
+ className="ms-[0.18em] text-[1.55em] font-semibold tracking-[-0.03em] -translate-y-[3px]"
220
216
  style={{
221
217
  fontFamily: "var(--font-heading), 'ivypresto-text', Georgia, serif",
222
218
  color: suffixColor,