@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
|
@@ -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 &&
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
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)]",
|