@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,448 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Body layouts for `FocusedWorkflowPageTemplate` — single column, stepped wizard,
5
+ * sectioned sidebar (settings-style), and empty placeholder.
6
+ */
7
+
8
+ import * as React from "react"
9
+
10
+ import { Button } from "@/components/ui/button"
11
+ import { Kbd, KbdGroup } from "@/components/ui/kbd"
12
+ import { Shortcut } from "@/components/ui/dropdown-menu"
13
+ import { cn } from "@/lib/utils"
14
+ import { useModKeyLabel } from "@/hooks/use-mod-key-label"
15
+ import { useAltKeyLabel } from "@/hooks/use-mod-key-label"
16
+
17
+ export interface FocusedWorkflowStep {
18
+ id: string
19
+ label: string
20
+ description?: string
21
+ }
22
+
23
+ export interface FocusedWorkflowSingleColumnProps {
24
+ children: React.ReactNode
25
+ className?: string
26
+ }
27
+
28
+ /** Default body — stack header, form sections, and actions in one column. */
29
+ export function FocusedWorkflowSingleColumn({
30
+ children,
31
+ className,
32
+ }: FocusedWorkflowSingleColumnProps) {
33
+ return <div className={cn("flex min-h-0 flex-1 flex-col gap-6", className)}>{children}</div>
34
+ }
35
+
36
+ export interface FocusedWorkflowStepIndicatorProps {
37
+ steps: readonly FocusedWorkflowStep[]
38
+ currentIndex: number
39
+ className?: string
40
+ /** When set, step buttons call this instead of being decorative only. */
41
+ onStepSelect?: (index: number) => void
42
+ }
43
+
44
+ export function FocusedWorkflowStepIndicator({
45
+ steps,
46
+ currentIndex,
47
+ className,
48
+ onStepSelect,
49
+ }: FocusedWorkflowStepIndicatorProps) {
50
+ const progress =
51
+ steps.length > 0 ? ((currentIndex + 1) / steps.length) * 100 : 0
52
+
53
+ return (
54
+ <nav
55
+ className={cn("flex flex-col gap-3", className)}
56
+ aria-label="Workflow progress"
57
+ >
58
+ <div className="flex flex-col gap-1">
59
+ <p className="text-xs font-medium text-muted-foreground">
60
+ Step{" "}
61
+ <span className="tabular-nums text-foreground">{currentIndex + 1}</span>{" "}
62
+ of <span className="tabular-nums">{steps.length}</span>
63
+ </p>
64
+ <div
65
+ className="h-2 w-full overflow-hidden rounded-full bg-muted"
66
+ aria-hidden="true"
67
+ >
68
+ <div
69
+ className="h-full rounded-full transition-all duration-300"
70
+ style={{ width: `${progress}%`, background: "var(--brand-color)" }}
71
+ />
72
+ </div>
73
+ </div>
74
+ <ol className="flex flex-col gap-2 sm:gap-1.5">
75
+ {steps.map((step, index) => {
76
+ const isComplete = index < currentIndex
77
+ const isCurrent = index === currentIndex
78
+ const rowClass = cn(
79
+ "flex w-full items-start gap-3 rounded-lg border px-3 py-2.5 text-left transition-colors",
80
+ isCurrent
81
+ ? "border-[var(--brand-color)]/40 bg-muted/50"
82
+ : "border-border bg-card",
83
+ onStepSelect && !isCurrent && "hover:bg-muted/30",
84
+ )
85
+ const inner = (
86
+ <>
87
+ {isComplete ? (
88
+ <span
89
+ className="mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-emerald-300/70 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/15 dark:text-emerald-300"
90
+ aria-hidden="true"
91
+ >
92
+ <i className="fa-light fa-check text-xs" aria-hidden="true" />
93
+ </span>
94
+ ) : (
95
+ <span
96
+ className={cn(
97
+ "mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full border text-xs font-semibold tabular-nums",
98
+ isCurrent
99
+ ? "border-transparent bg-[var(--brand-color)] text-primary-foreground"
100
+ : "border-border bg-background text-muted-foreground",
101
+ )}
102
+ aria-hidden="true"
103
+ >
104
+ {index + 1}
105
+ </span>
106
+ )}
107
+ <span className="min-w-0 flex-1">
108
+ <span className="block text-sm font-semibold text-foreground">
109
+ {step.label}
110
+ </span>
111
+ {step.description && isCurrent ? (
112
+ <span className="mt-1 block text-xs leading-snug text-muted-foreground sm:text-sm">
113
+ {step.description}
114
+ </span>
115
+ ) : null}
116
+ </span>
117
+ </>
118
+ )
119
+ return (
120
+ <li key={step.id} aria-current={isCurrent ? "step" : undefined}>
121
+ {onStepSelect ? (
122
+ <button
123
+ type="button"
124
+ onClick={() => onStepSelect(index)}
125
+ className={cn(
126
+ rowClass,
127
+ "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
128
+ )}
129
+ >
130
+ {inner}
131
+ </button>
132
+ ) : (
133
+ <div className={rowClass}>{inner}</div>
134
+ )}
135
+ </li>
136
+ )
137
+ })}
138
+ </ol>
139
+ </nav>
140
+ )
141
+ }
142
+
143
+ export interface FocusedWorkflowStepFormProps {
144
+ steps: readonly FocusedWorkflowStep[]
145
+ currentIndex: number
146
+ onStepSelect?: (index: number) => void
147
+ children: React.ReactNode
148
+ footer: React.ReactNode
149
+ className?: string
150
+ }
151
+
152
+ /** Multi-step wizard body — step list, active panel, sticky action footer. */
153
+ export function FocusedWorkflowStepForm({
154
+ steps,
155
+ currentIndex,
156
+ onStepSelect,
157
+ children,
158
+ footer,
159
+ className,
160
+ }: FocusedWorkflowStepFormProps) {
161
+ return (
162
+ <div className={cn("flex min-h-0 flex-1 flex-col gap-8", className)}>
163
+ <FocusedWorkflowStepIndicator
164
+ steps={steps}
165
+ currentIndex={currentIndex}
166
+ onStepSelect={onStepSelect}
167
+ />
168
+ <div className="min-h-0 flex-1">{children}</div>
169
+ <div className="sticky bottom-0 z-10 -mx-4 border-t border-border bg-background/95 px-4 py-4 backdrop-blur-sm sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
170
+ {footer}
171
+ </div>
172
+ </div>
173
+ )
174
+ }
175
+
176
+ export interface FocusedWorkflowSidebarSection {
177
+ id: string
178
+ label: string
179
+ }
180
+
181
+ export interface FocusedWorkflowSidebarSectionsProps {
182
+ sections: readonly FocusedWorkflowSidebarSection[]
183
+ activeSectionId?: string
184
+ onSectionSelect?: (id: string) => void
185
+ /** Full-width block above the nav + content grid (e.g. `PageHeader`). */
186
+ header?: React.ReactNode
187
+ children: React.ReactNode
188
+ className?: string
189
+ navLabel?: string
190
+ }
191
+
192
+ /** Grid for settings-style section nav + body (shared with route loading skeleton). */
193
+ export const FOCUSED_WORKFLOW_SIDEBAR_SECTIONS_GRID_CLASS =
194
+ "lg:grid lg:grid-cols-[minmax(10rem,12rem)_minmax(0,1fr)] lg:gap-x-12 lg:gap-y-8 lg:items-start"
195
+
196
+ /**
197
+ * Sectioned form with a **left nav rail** (settings-style). Use `id` on each
198
+ * `<section>` in `children` matching `sections[].id` for in-page anchors.
199
+ */
200
+ export function FocusedWorkflowSidebarSections({
201
+ sections,
202
+ activeSectionId,
203
+ onSectionSelect,
204
+ header,
205
+ children,
206
+ className,
207
+ navLabel = "Sections",
208
+ }: FocusedWorkflowSidebarSectionsProps) {
209
+ return (
210
+ <div
211
+ className={cn(
212
+ "flex min-h-0 flex-1 flex-col gap-8",
213
+ FOCUSED_WORKFLOW_SIDEBAR_SECTIONS_GRID_CLASS,
214
+ className,
215
+ )}
216
+ >
217
+ {header ? <div className="min-w-0 lg:col-span-2">{header}</div> : null}
218
+ <nav
219
+ className="flex shrink-0 flex-col gap-0.5 lg:sticky lg:top-6 lg:self-start"
220
+ aria-label={navLabel}
221
+ >
222
+ {sections.map(section => {
223
+ const isActive = section.id === activeSectionId
224
+ return (
225
+ <button
226
+ key={section.id}
227
+ type="button"
228
+ onClick={() => onSectionSelect?.(section.id)}
229
+ className={cn(
230
+ "rounded-md py-2 text-left text-sm transition-colors",
231
+ "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
232
+ isActive
233
+ ? "bg-muted font-medium text-foreground"
234
+ : "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
235
+ )}
236
+ aria-current={isActive ? "true" : undefined}
237
+ >
238
+ {section.label}
239
+ </button>
240
+ )
241
+ })}
242
+ </nav>
243
+ <div className="min-w-0 flex-1 flex flex-col gap-16">{children}</div>
244
+ </div>
245
+ )
246
+ }
247
+
248
+ export interface FocusedWorkflowEmptyStateProps {
249
+ iconClass?: string
250
+ title: string
251
+ description?: string
252
+ action?: React.ReactNode
253
+ className?: string
254
+ }
255
+
256
+ export function FocusedWorkflowEmptyState({
257
+ iconClass = "fa-layer-group",
258
+ title,
259
+ description,
260
+ action,
261
+ className,
262
+ }: FocusedWorkflowEmptyStateProps) {
263
+ return (
264
+ <div
265
+ className={cn(
266
+ "flex min-h-[min(24rem,50vh)] flex-col items-center justify-center gap-4 px-4 text-center",
267
+ className,
268
+ )}
269
+ role="status"
270
+ >
271
+ <span
272
+ className="flex size-14 items-center justify-center rounded-xl bg-muted text-muted-foreground"
273
+ aria-hidden="true"
274
+ >
275
+ <i className={cn("fa-light text-xl", iconClass)} aria-hidden="true" />
276
+ </span>
277
+ <div className="max-w-md space-y-2">
278
+ <h2 className="text-lg font-semibold text-foreground">{title}</h2>
279
+ {description ? (
280
+ <p className="text-sm text-muted-foreground">{description}</p>
281
+ ) : null}
282
+ </div>
283
+ {action}
284
+ </div>
285
+ )
286
+ }
287
+
288
+ export interface FocusedWorkflowActionFooterProps {
289
+ onCancel: () => void
290
+ cancelLabel?: string
291
+ cancelDisabled?: boolean
292
+ primary: React.ReactNode
293
+ secondary?: React.ReactNode
294
+ className?: string
295
+ }
296
+
297
+ /** Workflow footer — Cancel (Esc) + primary slot (usually submit with ⏎ Kbd). */
298
+ export function FocusedWorkflowActionFooter({
299
+ onCancel,
300
+ cancelLabel = "Cancel",
301
+ cancelDisabled,
302
+ primary,
303
+ secondary,
304
+ className,
305
+ }: FocusedWorkflowActionFooterProps) {
306
+ return (
307
+ <>
308
+ <Shortcut keys="Escape" disabled={cancelDisabled} onInvoke={onCancel} />
309
+ <div className={cn("flex flex-wrap items-center gap-2", className)}>
310
+ <Button
311
+ type="button"
312
+ variant="outline"
313
+ className="flex-1 min-w-[8rem] sm:flex-none"
314
+ disabled={cancelDisabled}
315
+ onClick={onCancel}
316
+ >
317
+ {cancelLabel}
318
+ <KbdGroup className="ml-1.5">
319
+ <Kbd variant="bare">Esc</Kbd>
320
+ </KbdGroup>
321
+ </Button>
322
+ {secondary}
323
+ <div className="flex flex-1 min-w-[8rem] justify-end sm:flex-none">{primary}</div>
324
+ </div>
325
+ </>
326
+ )
327
+ }
328
+
329
+ export interface FocusedWorkflowWizardFooterProps {
330
+ stepIndex: number
331
+ stepCount: number
332
+ onBack: () => void
333
+ onCancel: () => void
334
+ onNext: () => void
335
+ onSubmit?: () => void
336
+ isFirstStep?: boolean
337
+ isLastStep?: boolean
338
+ nextLabel?: string
339
+ submitLabel?: string
340
+ cancelLabel?: string
341
+ disabled?: boolean
342
+ submitting?: boolean
343
+ }
344
+
345
+ /** Step wizard footer — Back (⌘⌥←), Cancel (Esc), Next (⌘⏎) or Submit (⏎). */
346
+ export function FocusedWorkflowWizardFooter({
347
+ stepIndex,
348
+ stepCount,
349
+ onBack,
350
+ onCancel,
351
+ onNext,
352
+ onSubmit,
353
+ isFirstStep,
354
+ isLastStep,
355
+ nextLabel = "Next",
356
+ submitLabel = "Submit",
357
+ cancelLabel = "Cancel",
358
+ disabled,
359
+ submitting,
360
+ }: FocusedWorkflowWizardFooterProps) {
361
+ const mod = useModKeyLabel()
362
+ const alt = useAltKeyLabel()
363
+ const first = isFirstStep ?? stepIndex === 0
364
+ const last = isLastStep ?? stepIndex >= stepCount - 1
365
+
366
+ return (
367
+ <>
368
+ <Shortcut keys="Escape" disabled={disabled || submitting} onInvoke={onCancel} />
369
+ {!first ? (
370
+ <Shortcut
371
+ keys={`${mod}${alt}←`}
372
+ disabled={disabled || submitting}
373
+ onInvoke={onBack}
374
+ />
375
+ ) : null}
376
+ {last ? (
377
+ <Shortcut keys="Enter" disabled={disabled || submitting} onInvoke={() => onSubmit?.()} />
378
+ ) : (
379
+ <Shortcut keys={`${mod}Enter`} disabled={disabled || submitting} onInvoke={onNext} />
380
+ )}
381
+ <div className="flex flex-wrap items-center gap-2">
382
+ <Button
383
+ type="button"
384
+ variant="outline"
385
+ disabled={disabled || submitting}
386
+ onClick={onCancel}
387
+ >
388
+ {cancelLabel}
389
+ <KbdGroup className="ml-1.5">
390
+ <Kbd variant="bare">Esc</Kbd>
391
+ </KbdGroup>
392
+ </Button>
393
+ {!first ? (
394
+ <Button
395
+ type="button"
396
+ variant="outline"
397
+ disabled={disabled || submitting}
398
+ onClick={onBack}
399
+ >
400
+ Back
401
+ <KbdGroup className="ml-1.5">
402
+ <Kbd variant="bare">
403
+ {mod}
404
+ {alt}←
405
+ </Kbd>
406
+ </KbdGroup>
407
+ </Button>
408
+ ) : null}
409
+ <div className="ms-auto flex flex-1 min-w-[8rem] justify-end sm:flex-none">
410
+ {last ? (
411
+ <Button
412
+ type="button"
413
+ disabled={disabled || submitting}
414
+ aria-busy={submitting}
415
+ onClick={() => onSubmit?.()}
416
+ >
417
+ {submitting ? (
418
+ <>
419
+ <i
420
+ className="fa-light fa-spinner-third fa-spin text-[13px]"
421
+ aria-hidden="true"
422
+ />
423
+ Saving…
424
+ </>
425
+ ) : (
426
+ <>
427
+ {submitLabel}
428
+ <KbdGroup className="ml-1.5">
429
+ <Kbd variant="bare">⏎</Kbd>
430
+ </KbdGroup>
431
+ </>
432
+ )}
433
+ </Button>
434
+ ) : (
435
+ <Button type="button" disabled={disabled || submitting} onClick={onNext}>
436
+ {nextLabel}
437
+ <KbdGroup className="ml-1.5">
438
+ <Kbd variant="bare">
439
+ {mod}⏎
440
+ </Kbd>
441
+ </KbdGroup>
442
+ </Button>
443
+ )}
444
+ </div>
445
+ </div>
446
+ </>
447
+ )
448
+ }
@@ -0,0 +1,69 @@
1
+ import * as React from "react"
2
+
3
+ import { SiteHeader, type SiteHeaderProps } from "@/components/site-header"
4
+ import { SidebarInset } from "@/components/ui/sidebar"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ /** Default horizontal padding for focused workflow routes (forms, wizards, authoring). */
8
+ export const FOCUSED_WORKFLOW_CONTENT_PADDING_CLASS =
9
+ "px-4 pt-6 pb-32 sm:px-6 lg:px-8"
10
+
11
+ /** Max-width presets — narrower than primary list hubs (`max-w-[1440px]`). */
12
+ export const FOCUSED_WORKFLOW_MAX_WIDTH = {
13
+ md: "max-w-3xl",
14
+ lg: "max-w-4xl",
15
+ xl: "max-w-5xl",
16
+ } as const
17
+
18
+ export type FocusedWorkflowMaxWidth = keyof typeof FOCUSED_WORKFLOW_MAX_WIDTH
19
+
20
+ export interface FocusedWorkflowPageTemplateProps {
21
+ /** e.g. `SidebarAutoCollapse` on long-form routes. */
22
+ beforeSiteHeader?: React.ReactNode
23
+ /** Breadcrumb back link + title; parent context stays in `SiteHeader`. */
24
+ siteHeader: SiteHeaderProps
25
+ children: React.ReactNode
26
+ maxWidth?: FocusedWorkflowMaxWidth
27
+ /** Merged with default content padding. */
28
+ contentClassName?: string
29
+ bodyClassName?: string
30
+ }
31
+
32
+ /**
33
+ * Dedicated-route shell for **large or multi-step work** — create/edit flows, wizards,
34
+ * and sectioned settings. **Not** for list hubs (use `PrimaryPageTemplate`) and **not**
35
+ * for Miller-column / split-panel explorers (use `ListPageSplitHubChrome`).
36
+ *
37
+ * Pair body layouts with `FocusedWorkflowSingleColumn`, `FocusedWorkflowStepForm`,
38
+ * `FocusedWorkflowSidebarSections`, or `FocusedWorkflowEmptyState`.
39
+ *
40
+ * @see `docs/focused-workflow-page-pattern.md`
41
+ */
42
+ export function FocusedWorkflowPageTemplate({
43
+ beforeSiteHeader,
44
+ siteHeader,
45
+ children,
46
+ maxWidth = "md",
47
+ contentClassName,
48
+ bodyClassName,
49
+ }: FocusedWorkflowPageTemplateProps) {
50
+ return (
51
+ <SidebarInset id="main-content" tabIndex={-1}>
52
+ {beforeSiteHeader}
53
+ <SiteHeader {...siteHeader} />
54
+ <div className={cn("flex min-h-0 flex-1 flex-col outline-none", bodyClassName)}>
55
+ <div
56
+ className={cn(
57
+ "@container/main mx-auto flex min-h-0 w-full min-w-0 flex-1 flex-col",
58
+ FOCUSED_WORKFLOW_MAX_WIDTH[maxWidth],
59
+ FOCUSED_WORKFLOW_CONTENT_PADDING_CLASS,
60
+ contentClassName,
61
+ )}
62
+ >
63
+ {children}
64
+ </div>
65
+ </div>
66
+ </SidebarInset>
67
+ )
68
+ }
69
+
@@ -47,6 +47,10 @@ import {
47
47
  Shortcut,
48
48
  } from "@/components/ui/dropdown-menu"
49
49
  import type { DataListViewType } from "@/lib/data-list-view"
50
+ import {
51
+ dataListViewTilesForHub,
52
+ showsListPageHubMetricsStrip,
53
+ } from "@/lib/data-list-view-registry"
50
54
  import { DATA_LIST_VIEW_TILES, dataListViewAddShortcut } from "@/lib/data-list-view"
51
55
  import {
52
56
  createListPageEditViewHandler,
@@ -126,6 +130,11 @@ export interface ListPageTemplateProps {
126
130
  tablePropertiesRef?: React.RefObject<OpenTablePropertiesHandle | null>
127
131
  /** When true, hide the views tab strip (tabs + add view) — e.g. search landing with a single table surface. */
128
132
  hideViewsToolbar?: boolean
133
+ /**
134
+ * View types this hub can render. Limits **Add view** and documents intent; table components
135
+ * should still implement each kind via `ListPageConnectedViewBody`. Defaults to all registry views.
136
+ */
137
+ supportedViewTypes?: readonly DataListViewType[]
129
138
  }
130
139
 
131
140
  /** Collision-proof id for a dynamically-added tab. Module-level counters reset
@@ -180,6 +189,7 @@ export function ListPageTemplate({
180
189
  onEditView,
181
190
  tablePropertiesRef,
182
191
  hideViewsToolbar = false,
192
+ supportedViewTypes,
183
193
  }: ListPageTemplateProps) {
184
194
  const controlled =
185
195
  tabsProp !== undefined &&
@@ -212,6 +222,20 @@ export function ListPageTemplate({
212
222
 
213
223
  const activeTab = tabs.find(t => t.id === activeTabId) ?? tabs[0]
214
224
 
225
+ const addableViewTypes = React.useMemo(
226
+ () =>
227
+ supportedViewTypes != null
228
+ ? dataListViewTilesForHub(supportedViewTypes)
229
+ : VIEW_TYPES,
230
+ [supportedViewTypes],
231
+ )
232
+
233
+ const metricsVisible =
234
+ showMetrics
235
+ && metrics != null
236
+ && activeTab != null
237
+ && showsListPageHubMetricsStrip(activeTab.viewType)
238
+
215
239
  const editViewFromRef = React.useMemo(
216
240
  () => (tablePropertiesRef ? createListPageEditViewHandler(tablePropertiesRef) : undefined),
217
241
  [tablePropertiesRef]
@@ -272,8 +296,8 @@ export function ListPageTemplate({
272
296
  }
273
297
 
274
298
  return (
275
- <>
276
- {!hideViewsToolbar && VIEW_TYPES.slice(0, 9).map((v, i) => {
299
+ <div className="flex min-h-0 w-full min-w-0 flex-1 flex-col">
300
+ {!hideViewsToolbar && addableViewTypes.slice(0, 9).map((v, i) => {
277
301
  const keys = dataListViewAddShortcut(i)
278
302
  return keys ? (
279
303
  <Shortcut
@@ -302,7 +326,7 @@ export function ListPageTemplate({
302
326
  )}
303
327
  {header}
304
328
 
305
- {showMetrics && metrics}
329
+ {metricsVisible ? metrics : null}
306
330
 
307
331
  {/* ── Views toolbar (not tablist: settings/close are not tabs — WCAG 1.3.1 / ARIA) ── */}
308
332
  {!hideViewsToolbar && (
@@ -480,7 +504,7 @@ export function ListPageTemplate({
480
504
  <DropdownMenuContent align="start">
481
505
  <DropdownMenuLabel className="text-xs">Add a view</DropdownMenuLabel>
482
506
  <DropdownMenuSeparator />
483
- {VIEW_TYPES.map((v, i) => (
507
+ {addableViewTypes.map((v, i) => (
484
508
  <DropdownMenuItem
485
509
  key={v.type}
486
510
  shortcut={dataListViewAddShortcut(i)}
@@ -579,6 +603,6 @@ export function ListPageTemplate({
579
603
  </DialogFooter>
580
604
  </DialogContent>
581
605
  </Dialog>
582
- </>
606
+ </div>
583
607
  )
584
608
  }
@@ -27,6 +27,7 @@ export function NestedSecondaryPanelShell({
27
27
  return (
28
28
  <nav
29
29
  aria-label={ariaLabel}
30
+ data-slot="secondary-panel"
30
31
  data-state={open ? "open" : "closed"}
31
32
  data-layout={open ? (compact ? "icon" : "expanded") : "closed"}
32
33
  className={cn(
@@ -40,7 +41,7 @@ export function NestedSecondaryPanelShell({
40
41
  // 2rem on mobile where the panel scrolls inline and we leave
41
42
  // a little more breathing room). No upper cap so tall screens
42
43
  // get a fully-extended rail.
43
- "shrink-0 m-2 mx-2 rounded-xl ring-1 ring-sidebar-border shadow-sm relative md:sticky md:top-2 bg-[var(--secondary-panel-bg)]",
44
+ "shrink-0 m-2 mx-2 rounded-xl border border-sidebar-border bg-secondary-panel-bg shadow-sm relative md:sticky md:top-2",
44
45
  compact
45
46
  ? "w-12 min-w-12 max-w-12 h-[calc(100svh-2rem)] md:h-[calc(100svh-1rem)]"
46
47
  : "w-64 min-w-64 max-w-64 h-[calc(100svh-2rem)] md:h-[calc(100svh-1rem)]",