@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,262 @@
1
+ import * as React from "react"
2
+
3
+ import { Skeleton } from "@/components/ui/skeleton"
4
+ import { SidebarInset } from "@/components/ui/sidebar"
5
+ import {
6
+ FOCUSED_WORKFLOW_CONTENT_PADDING_CLASS,
7
+ FOCUSED_WORKFLOW_MAX_WIDTH,
8
+ } from "@/components/templates/focused-workflow-page-template"
9
+ import { FOCUSED_WORKFLOW_SIDEBAR_SECTIONS_GRID_CLASS } from "@/components/templates/focused-workflow-layouts"
10
+ import { PRIMARY_PAGE_MAX_WIDTH_CLASS } from "@/components/templates/primary-page-template"
11
+ import type { PageLoadingVariant } from "@/lib/page-loading-variant"
12
+ import { cn } from "@/lib/utils"
13
+
14
+ /** Breadcrumb bar placeholder — matches `SiteHeader` footprint without client hooks. */
15
+ function SiteHeaderSkeleton() {
16
+ return (
17
+ <div className="sticky top-0 z-30 bg-transparent">
18
+ <header
19
+ role="presentation"
20
+ className="flex h-(--header-height) shrink-0 items-center gap-2 rounded-t-xl bg-background ps-4 pe-2 lg:gap-2 lg:ps-6 lg:pe-2"
21
+ >
22
+ <Skeleton className="size-8 shrink-0 rounded-md" />
23
+ <Skeleton className="h-4 w-36 max-w-[50vw] rounded-md" />
24
+ <div className="ms-auto flex items-center gap-2">
25
+ <Skeleton className="h-8 w-16 rounded-md" />
26
+ <Skeleton className="size-8 rounded-md" />
27
+ </div>
28
+ </header>
29
+ </div>
30
+ )
31
+ }
32
+
33
+ interface PageLoadingChromeProps {
34
+ children: React.ReactNode
35
+ maxWidthClassName?: string
36
+ contentClassName?: string
37
+ paddingClassName?: string
38
+ }
39
+
40
+ function PageLoadingChrome({
41
+ children,
42
+ maxWidthClassName = PRIMARY_PAGE_MAX_WIDTH_CLASS,
43
+ contentClassName,
44
+ paddingClassName = "px-4 pt-2 pb-32 sm:px-6 lg:px-8",
45
+ }: PageLoadingChromeProps) {
46
+ return (
47
+ <SidebarInset id="main-content" tabIndex={-1} aria-busy="true" aria-label="Loading page">
48
+ <SiteHeaderSkeleton />
49
+ <div className="flex min-h-0 flex-1 flex-col outline-none">
50
+ <div
51
+ className={cn(
52
+ "@container/main mx-auto flex min-h-0 w-full min-w-0 flex-1 flex-col",
53
+ maxWidthClassName,
54
+ paddingClassName,
55
+ contentClassName,
56
+ )}
57
+ >
58
+ {children}
59
+ </div>
60
+ </div>
61
+ </SidebarInset>
62
+ )
63
+ }
64
+
65
+ /** List hub — header, KPI band, toolbar, table rows. */
66
+ export function PrimaryListHubLoadingBody() {
67
+ return (
68
+ <div className="flex flex-col gap-6">
69
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
70
+ <div className="space-y-2">
71
+ <Skeleton className="h-9 w-56 max-w-full rounded-md" />
72
+ <Skeleton className="h-4 w-72 max-w-full rounded-md" />
73
+ </div>
74
+ <div className="flex shrink-0 gap-2">
75
+ <Skeleton className="h-9 w-28 rounded-md" />
76
+ <Skeleton className="h-9 w-9 rounded-md" />
77
+ </div>
78
+ </div>
79
+ <div className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-border bg-border sm:grid-cols-4">
80
+ {Array.from({ length: 4 }).map((_, i) => (
81
+ <Skeleton key={i} className="h-[4.75rem] rounded-none bg-card" />
82
+ ))}
83
+ </div>
84
+ <div className="flex flex-wrap items-center gap-2">
85
+ <Skeleton className="h-9 min-w-[12rem] flex-1 rounded-md sm:max-w-xs" />
86
+ <Skeleton className="h-9 w-24 rounded-md" />
87
+ <Skeleton className="h-9 w-9 rounded-md" />
88
+ </div>
89
+ <div className="space-y-2 rounded-xl border border-border bg-card p-2">
90
+ <Skeleton className="h-10 w-full rounded-md" />
91
+ {Array.from({ length: 8 }).map((_, i) => (
92
+ <Skeleton key={i} className="h-11 w-full rounded-md" />
93
+ ))}
94
+ </div>
95
+ </div>
96
+ )
97
+ }
98
+
99
+ export function PrimaryListHubLoadingFallback() {
100
+ return (
101
+ <PageLoadingChrome>
102
+ <PrimaryListHubLoadingBody />
103
+ </PageLoadingChrome>
104
+ )
105
+ }
106
+
107
+ /** Question bank discovery hub — composer + folder grid. */
108
+ export function QuestionBankHubLoadingBody() {
109
+ return (
110
+ <div className="flex flex-col gap-10">
111
+ <div className="mx-auto flex w-full max-w-3xl flex-col gap-4 py-6">
112
+ <Skeleton className="mx-auto h-10 w-64 max-w-full rounded-md" />
113
+ <Skeleton className="h-12 w-full rounded-xl" />
114
+ <Skeleton className="h-4 w-48 rounded-md" />
115
+ </div>
116
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
117
+ {Array.from({ length: 6 }).map((_, i) => (
118
+ <Skeleton key={i} className="h-28 rounded-xl" />
119
+ ))}
120
+ </div>
121
+ </div>
122
+ )
123
+ }
124
+
125
+ export function QuestionBankHubLoadingFallback() {
126
+ return (
127
+ <PageLoadingChrome>
128
+ <QuestionBankHubLoadingBody />
129
+ </PageLoadingChrome>
130
+ )
131
+ }
132
+
133
+ /** Dedicated search landing / results (`?q=`). */
134
+ export function DedicatedSearchLoadingBody() {
135
+ return (
136
+ <div className="mx-auto flex min-h-[min(52vh,28rem)] w-full max-w-3xl flex-col justify-center gap-6 py-10">
137
+ <Skeleton className="h-10 w-72 max-w-full rounded-md" />
138
+ <Skeleton className="h-12 w-full rounded-xl" />
139
+ <div className="flex flex-wrap gap-2">
140
+ <Skeleton className="h-8 w-24 rounded-full" />
141
+ <Skeleton className="h-8 w-32 rounded-full" />
142
+ <Skeleton className="h-8 w-20 rounded-full" />
143
+ </div>
144
+ </div>
145
+ )
146
+ }
147
+
148
+ export function DedicatedSearchLoadingFallback() {
149
+ return (
150
+ <PageLoadingChrome maxWidthClassName="max-w-5xl">
151
+ <DedicatedSearchLoadingBody />
152
+ </PageLoadingChrome>
153
+ )
154
+ }
155
+
156
+ /** Dashboard tabs — KPI row + chart block. */
157
+ export function DashboardLoadingBody() {
158
+ return (
159
+ <div className="flex flex-col gap-6">
160
+ <div className="space-y-2">
161
+ <Skeleton className="h-9 w-56 max-w-full rounded-md" />
162
+ <Skeleton className="h-4 w-80 max-w-full rounded-md" />
163
+ </div>
164
+ <Skeleton className="h-11 w-full max-w-xl rounded-md" />
165
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
166
+ {Array.from({ length: 4 }).map((_, i) => (
167
+ <Skeleton key={i} className="h-24 rounded-xl" />
168
+ ))}
169
+ </div>
170
+ <Skeleton className="min-h-[320px] w-full rounded-xl" />
171
+ </div>
172
+ )
173
+ }
174
+
175
+ export function DashboardLoadingFallback() {
176
+ return (
177
+ <PageLoadingChrome maxWidthClassName={PRIMARY_PAGE_MAX_WIDTH_CLASS}>
178
+ <DashboardLoadingBody />
179
+ </PageLoadingChrome>
180
+ )
181
+ }
182
+
183
+ /** Focused workflow — single column form/settings sections. */
184
+ export function FocusedWorkflowLoadingBody({ withSidebar = false }: { withSidebar?: boolean }) {
185
+ if (withSidebar) {
186
+ return (
187
+ <div className={cn("flex flex-col gap-8", FOCUSED_WORKFLOW_SIDEBAR_SECTIONS_GRID_CLASS)}>
188
+ <div className="space-y-2 lg:col-span-2">
189
+ <Skeleton className="h-9 w-40 max-w-full rounded-md" />
190
+ <Skeleton className="h-4 w-full max-w-xl rounded-md" />
191
+ </div>
192
+ <div className="flex flex-col gap-1">
193
+ {Array.from({ length: 5 }).map((_, i) => (
194
+ <Skeleton key={i} className="h-9 w-full rounded-md" />
195
+ ))}
196
+ </div>
197
+ <div className="flex flex-col gap-8">
198
+ <Skeleton className="h-24 w-full rounded-xl" />
199
+ <Skeleton className="h-48 w-full rounded-xl" />
200
+ <Skeleton className="h-32 w-full rounded-xl" />
201
+ </div>
202
+ </div>
203
+ )
204
+ }
205
+
206
+ return (
207
+ <div className="flex flex-col gap-8">
208
+ <div className="space-y-2">
209
+ <Skeleton className="h-9 w-48 max-w-full rounded-md" />
210
+ <Skeleton className="h-4 w-full max-w-lg rounded-md" />
211
+ </div>
212
+ <Skeleton className="h-40 w-full rounded-xl" />
213
+ <Skeleton className="h-56 w-full rounded-xl" />
214
+ </div>
215
+ )
216
+ }
217
+
218
+ export function FocusedWorkflowLoadingFallback({ withSidebar = false }: { withSidebar?: boolean }) {
219
+ return (
220
+ <PageLoadingChrome
221
+ maxWidthClassName={FOCUSED_WORKFLOW_MAX_WIDTH.lg}
222
+ paddingClassName={FOCUSED_WORKFLOW_CONTENT_PADDING_CLASS}
223
+ >
224
+ <FocusedWorkflowLoadingBody withSidebar={withSidebar} />
225
+ </PageLoadingChrome>
226
+ )
227
+ }
228
+
229
+ export function SimplePageLoadingBody() {
230
+ return (
231
+ <div className="flex max-w-xl flex-col gap-4 py-4">
232
+ <Skeleton className="h-4 w-full rounded-md" />
233
+ <Skeleton className="h-4 w-5/6 rounded-md" />
234
+ <div className="flex gap-3 pt-2">
235
+ <Skeleton className="h-9 w-36 rounded-md" />
236
+ <Skeleton className="h-9 w-28 rounded-md" />
237
+ </div>
238
+ </div>
239
+ )
240
+ }
241
+
242
+ export function SimplePageLoadingFallback() {
243
+ return (
244
+ <PageLoadingChrome maxWidthClassName="max-w-3xl">
245
+ <SimplePageLoadingBody />
246
+ </PageLoadingChrome>
247
+ )
248
+ }
249
+
250
+ const FALLBACK_BY_VARIANT: Record<PageLoadingVariant, React.ReactNode> = {
251
+ dashboard: <DashboardLoadingFallback />,
252
+ "primary-list-hub": <PrimaryListHubLoadingFallback />,
253
+ "question-bank-hub": <QuestionBankHubLoadingFallback />,
254
+ "dedicated-search": <DedicatedSearchLoadingFallback />,
255
+ "focused-workflow": <FocusedWorkflowLoadingFallback />,
256
+ "focused-workflow-sidebar": <FocusedWorkflowLoadingFallback withSidebar />,
257
+ simple: <SimplePageLoadingFallback />,
258
+ }
259
+
260
+ export function PageLoadingByVariant({ variant }: { variant: PageLoadingVariant }) {
261
+ return FALLBACK_BY_VARIANT[variant]
262
+ }
@@ -0,0 +1 @@
1
+ export * from "@exxatdesignux/ui/components/button-group"
@@ -0,0 +1,39 @@
1
+ # Consumer app pattern (`@exxatdesignux/ui`)
2
+
3
+ > **Audience:** Product repos that **install** the design system from npm — not the `apps/web` monorepo reference app.
4
+ > **Handbook:** `packages/ui/consumer-extras/AGENTS.md` · **Upgrade:** `consumer-upgrade-checklist.md` · **Skill:** `.cursor/skills/exxat-consumer-app/SKILL.md`
5
+
6
+ ## Intent
7
+
8
+ A **consumer app** composes **`@exxatdesignux/ui`** primitives and copies **patterns** from the published **`template/`** tree. It does **not** fork parallel table stacks, view routers, or shell tokens.
9
+
10
+ ## MUST
11
+
12
+ 1. **Install** — `@exxatdesignux/ui` + peers; import DS CSS once (`@exxatdesignux/ui/globals.css` or documented entry).
13
+ 2. **Sync extras** — After version bumps: `npx --package=@exxatdesignux/ui@latest exxat-ui sync-extras` → `.cursor/skills/exxat-*` + `docs/exxat-ds/*.md`.
14
+ 3. **Diff template** — Compare `node_modules/@exxatdesignux/ui/template/` for new files (layouts, `ListPageConnectedViewBody`, `FocusedWorkflowPageTemplate`, mocks).
15
+ 4. **List hubs** — One **`useTableState`** row bag; **`ListPageConnectedViewBody`** + **`defineHubViewRenderers`**; same **`supportedViewTypes`** on **`ListPageTemplate`** and **`TablePropertiesDrawer`**.
16
+ 5. **Shell tokens** — Sidebar / secondary panel / page elevation via **`--sidebar`**, **`--secondary-panel-bg`**, **`--background`** — see **`shell-surface-elevation-pattern.md`**.
17
+ 6. **Focused workflows** — Dedicated create/edit/settings routes use **`FocusedWorkflowPageTemplate`** — **`focused-workflow-page-pattern.md`**.
18
+
19
+ ## MUST NOT
20
+
21
+ - Copy only components without porting **AGENTS**, **rules**, and **pattern docs** when behavior changes.
22
+ - Ship hub view tabs with long **`if (view === "table")`** chains instead of **`ListPageConnectedViewBody`**.
23
+ - Keep a second mock array per view while the grid uses **`useTableState`**.
24
+ - Set secondary panel to **`bg-sidebar`** or light **`--brand-tint`** mixes in dark mode.
25
+
26
+ ## Checklist (new consumer feature)
27
+
28
+ - [ ] Read **`consumer-upgrade-checklist.md`** for the installed UI version.
29
+ - [ ] Run **`exxat-ui sync-extras`** if skills/patterns are stale.
30
+ - [ ] Find the closest **`template/`** hub or page; port file names and imports to your app paths.
31
+ - [ ] **List hub:** `lib/<hub>-supported-views.ts`, `lib/data-list-view-registry.ts`, `ListPageConnectedViewBody`, centralized **`tableState.rows`**.
32
+ - [ ] **Form route:** `FocusedWorkflowPageTemplate` + one body layout from **`focused-workflow-layouts.tsx`**.
33
+ - [ ] **Nav + secondary panel:** `secondaryPanel` on nav item, `PANELS` registry, `useAutoPanel` — **`exxat-primary-nav-secondary-panel`** skill.
34
+ - [ ] Re-run **`fa:subset-audit`** when adding Font Awesome glyphs.
35
+
36
+ ## See also
37
+
38
+ - Monorepo reference: `apps/web/` (full product demo)
39
+ - **`.cursor/rules/exxat-consumer-app.mdc`** (when working inside a consumer repo that synced rules)
@@ -38,6 +38,41 @@ Non-table view branches (e.g. **folder** icon grid, **panel** finder, OS-style f
38
38
 
39
39
  **Handbook:** **`AGENTS.md` §4.5**. **Cursor:** **`.cursor/rules/exxat-list-page-view-shells.mdc`**. **Skill:** **`.cursor/skills/exxat-list-page-view-shells/SKILL.md`**. **Do not** wrap **`DataTable`** in the frame if that stacks padding with the table toolbar (**`AGENTS.md` §5**).
40
40
 
41
+ ## View registry and connected bodies (extensibility)
42
+
43
+ To add a **new view type** without breaking existing hubs:
44
+
45
+ 1. **Register once** — Add a tile in `lib/data-list-view.ts` (`DATA_LIST_VIEW_TILES`). Capabilities (hub KPI strip, render kind) derive automatically in `lib/data-list-view-registry.ts`.
46
+ 2. **Build the body once** — Add or extend a generic surface under `components/data-views/` (e.g. `ListPageCalendarView`). Entity-specific wiring stays in props (`getEventDate`, board `renderCard`), not a second calendar implementation per hub.
47
+ 3. **Declare what each hub supports** — `lib/<hub>-supported-views.ts` exports a `const` array; pass the same array to **`ListPageTemplate`** (`supportedViewTypes`), the hub table (`supportedViewTypes` → `TablePropertiesDrawerButton`), and rely on defaults if omitted. **Add view**, **⌘1–9** shortcuts, and **Properties → View type** tiles all use `dataListViewTilesForHub` / `dataListViewSelectionTilesForHub` so users cannot pick a view the hub never implemented.
48
+ 4. **Route in the hub table with `ListPageConnectedViewBody`** — Switch on **`getDataListViewRenderKind(view)`**, not raw `view === "…"` chains. Pass one renderer per kind the hub supports via **`defineHubViewRenderers(MY_HUB_SUPPORTED_VIEWS, { … })`** (`lib/hub-connected-view-renderers.ts`) so dev builds warn when a supported view has no body. **Do not** use a default branch that renders dashboard/KPIs; missing renderers show `ListPageViewNotConfigured`.
49
+ 5. **Let the template own hub chrome** — `ListPageTemplate` hides the metrics strip on calendar/dashboard via `showsListPageHubMetricsStrip(activeTab.viewType)`. Hub clients pass `showMetrics` only; they do not reimplement per-tab KPI visibility.
50
+ 6. **Panel / Miller columns** — Reuse **`ListPageFolderColumnsPanel`** (`components/data-views/list-page-folder-columns-panel.tsx`) for folder + record columns; hub-specific chrome (folder colors, actions) lives in a thin wrapper (e.g. `QuestionBankFolderColumnsPanel`). **Do not** export question-bank-only tree/folder nav from the generic `data-views` barrel.
51
+
52
+ ```tsx
53
+ // lib/my-hub-supported-views.ts
54
+ export const MY_HUB_SUPPORTED_VIEWS = ["table", "list", "calendar"] as const satisfies readonly DataListViewType[]
55
+
56
+ // my-hub-client.tsx
57
+ <ListPageTemplate supportedViewTypes={MY_HUB_SUPPORTED_VIEWS} showMetrics={showMetrics} … />
58
+
59
+ // my-hub-table.tsx
60
+ import { defineHubViewRenderers } from "@/lib/hub-connected-view-renderers"
61
+
62
+ return (
63
+ <ListPageConnectedViewBody
64
+ view={view}
65
+ hubLabel="My hub"
66
+ renderers={defineHubViewRenderers(MY_HUB_SUPPORTED_VIEWS, {
67
+ "data-table": <DataTable … />,
68
+ "calendar-with-toolbar": toolbarShell(<ListPageCalendarView rows={tableState.rows} getEventDate={…} />),
69
+ })}
70
+ />
71
+ )
72
+ ```
73
+
74
+ **Checklist for a new view type:** registry tile → `data-views/` component → update each `*-supported-views.ts` that should expose it → add renderer in each `*-table.tsx` (via `defineHubViewRenderers`) → Properties drawer copy in `table-properties/drawer.tsx` if needed → `table-state-lifecycle` picks up types via `DATA_LIST_VIEW_TILES` (no separate allowlist).
75
+
41
76
  ## Architecture
42
77
 
43
78
  - **Page shell** — `ListPageTemplate` owns the views toolbar (tabs), optional metrics, and export drawer. Content for the active tab is rendered via `renderContent`.
@@ -146,11 +181,13 @@ Reference: `components/placements-page-header.tsx`, `components/team-page-header
146
181
 
147
182
  **When to use a new page (route):** The flow is **primary**, **long-form**, **multi-step**, or should have its **own URL** / bookmark / history **without** the parent page behind it — e.g. full create/edit, wizards, or detail that *is* the task.
148
183
 
149
- **Rule of thumb:** **Context + quick** **drawer**; **blocking short choice** **dialog**; **otherwise** **new page**.
184
+ **Focused workflow shell:** For dedicated create/edit, wizards, and sectioned settings, use **`FocusedWorkflowPageTemplate`** + layouts in **`focused-workflow-layouts.tsx`** see **`docs/focused-workflow-page-pattern.md`** and **`AGENTS.md` §14**. **Not** for list hubs (**`ListPageTemplate`**) or Miller-column explorers.
185
+
186
+ **Rule of thumb:** **Context + quick** → **drawer**; **blocking short choice** → **dialog**; **primary / long / wizard / settings** → **focused workflow route** (or other dedicated page).
150
187
 
151
188
  **Modal vs side panel (same route):** When the overlay stays on the same URL, prefer **`docs/drawer-vs-dialog-pattern.md`** and **`.cursor/rules/exxat-drawer-vs-dialog.mdc`** — drawers keep the hub visible; dialogs trap focus for confirms.
152
189
 
153
- Canonical rules: **`AGENTS.md` §6.4**, root **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
190
+ Canonical rules: **`AGENTS.md` §6.4**, **§14**, root **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**, **`.cursor/rules/exxat-focused-workflow-page.mdc`**.
154
191
 
155
192
  ---
156
193
 
@@ -170,7 +207,7 @@ When a route is a **primary** destination in nav (main hub for an entity) **and*
170
207
  - [ ] **>10 items** → search, filter, sort, properties (per surface type above).
171
208
  - [ ] **Has data to export** → **More** menu with **Export** + shared `ExportDrawer` pattern.
172
209
  - [ ] **Primary + large / main hub** → `ListPageTemplate`-style shell where applicable.
173
- - [ ] **Page vs drawer vs dialog (§6.4)** — Quick with parent **context** → drawer/sheet; **blocking** confirm → **dialog**; primary or long flows → **new route** (`docs/drawer-vs-dialog-pattern.md`).
210
+ - [ ] **Page vs drawer vs dialog (§6.4)** — Quick with parent **context** → drawer/sheet; **blocking** confirm → **dialog**; primary or long flows → **focused workflow route** (`docs/focused-workflow-page-pattern.md`, **§14**) or other dedicated page (`docs/drawer-vs-dialog-pattern.md`).
174
211
  - [ ] **Primary button** → `Button` default variant (`size="lg"` for parity with Placements), not outline.
175
212
  - [ ] **Dashboard view tab** → `KeyMetrics` + shared KPI helpers from **`tableState.rows`**; no duplicate one-off metric cards.
176
213
  - [ ] **Data view charts** → `ChartFigure` + `chart-keyboard-selection`; layout persistence via **`data-view-dashboard-storage`** (see `AGENTS.md` §4.3).
@@ -30,13 +30,15 @@
30
30
 
31
31
  Use when the work is **primary**, **long**, **multi-step**, or deserves its **own URL** — see **`exxat-page-vs-drawer.mdc`** and **`AGENTS.md` §6.4**.
32
32
 
33
+ For **large forms, wizards, and sectioned settings**, use **`FocusedWorkflowPageTemplate`** and body layouts — **`docs/focused-workflow-page-pattern.md`**, **`AGENTS.md` §14**, **`.cursor/skills/exxat-focused-workflow-page/SKILL.md`**.
34
+
33
35
  ## Quick matrix
34
36
 
35
37
  | Need | Drawer | Dialog | Route |
36
38
  | --- | --- | --- | --- |
37
39
  | Keep hub visible | Yes | No (blocks) | No |
38
40
  | Short confirm / alert | Rare | Yes | Overkill |
39
- | Long form / wizard | Cramped | No | Yes |
41
+ | Long form / wizard | Cramped | No | Yes — **`FocusedWorkflowPageTemplate`** |
40
42
  | Properties tied to a table | Yes | Too small | Optional |
41
43
 
42
44
  ## Accessibility
@@ -0,0 +1,84 @@
1
+ # Focused workflow page (dedicated routes)
2
+
3
+ > **Related:** **`AGENTS.md` §6.4** (page vs drawer vs dialog), **§14** (AI checklist), **`docs/drawer-vs-dialog-pattern.md`**, **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-focused-workflow-page.mdc`**, **`.cursor/skills/exxat-focused-workflow-page/SKILL.md`**.
4
+
5
+ ## Intent
6
+
7
+ Use **`FocusedWorkflowPageTemplate`** for **large or multi-step work** on its **own route** — create/edit forms, wizards, and sectioned settings. The shell is **narrower** than list hubs and **does not** use Miller-column / split-panel explorers.
8
+
9
+ | Surface | Use instead |
10
+ | --- | --- |
11
+ | Browsable record hubs (table, board, dashboard tabs) | **`PrimaryPageTemplate`** + **`ListPageTemplate`** |
12
+ | Finder / folder columns / split hub chrome | **`ListPageSplitHubChrome`**, **`ListPageFolderColumnsPanel`** |
13
+ | Quick properties or export beside a grid | **Drawer** (`TablePropertiesDrawer`, `ExportDrawer`) |
14
+ | Blocking confirm on the same route | **Dialog** |
15
+
16
+ ## Surface matrix (§6.4)
17
+
18
+ | Need | Drawer | Dialog | **Focused workflow route** |
19
+ | --- | --- | --- | --- |
20
+ | Keep hub visible while acting | Yes | No | No |
21
+ | Own URL / bookmark / history | Rare | No | **Yes** |
22
+ | Multi-step wizard | Cramped | No | **Yes** |
23
+ | Sectioned settings (left nav) | Awkward | No | **Yes** |
24
+ | Short delete confirm | No | **Yes** | Overkill |
25
+
26
+ ## Shell
27
+
28
+ **`FocusedWorkflowPageTemplate`** (`components/templates/focused-workflow-page-template.tsx`):
29
+
30
+ - **`SidebarInset`** + **`SiteHeader`** (breadcrumb back link + title).
31
+ - Centered column: **`max-w-3xl` / `max-w-4xl` / `max-w-5xl`** via **`maxWidth`** (`md` | `lg` | `xl`).
32
+ - Default padding: **`FOCUSED_WORKFLOW_CONTENT_PADDING_CLASS`**.
33
+
34
+ Optional **`beforeSiteHeader`** (e.g. **`SidebarAutoCollapse`** on long forms).
35
+
36
+ ## Body layouts
37
+
38
+ Import from **`components/templates/focused-workflow-layouts.tsx`**:
39
+
40
+ | Layout | When |
41
+ | --- | --- |
42
+ | **`FocusedWorkflowSingleColumn`** | Default stack — header, form sections, footer actions (e.g. question authoring). |
43
+ | **`FocusedWorkflowStepForm`** + **`FocusedWorkflowWizardFooter`** | Multi-step wizard with progress list and sticky footer (placement-style flows). |
44
+ | **`FocusedWorkflowSidebarSections`** | Sectioned form with **left nav rail** (settings-style); put **`id`** on each `<section>` matching **`sections[].id`**. |
45
+ | **`FocusedWorkflowEmptyState`** | Placeholder / not-yet-configured route body. |
46
+ | **`FocusedWorkflowActionFooter`** | Single-step Cancel (Esc) + primary (Enter) without step chrome. |
47
+
48
+ Keyboard: wizard and action footers pair **`Shortcut`** with inline **`<Kbd variant="bare">`** per **`.cursor/rules/exxat-kbd-shortcuts.mdc`**.
49
+
50
+ ## Golden references
51
+
52
+ | Route | Variant |
53
+ | --- | --- |
54
+ | **`/question-bank/new`** | Shell + **`FocusedWorkflowSingleColumn`** + domain composer |
55
+ | **`/settings`** | Shell (`maxWidth="lg"`) + **`FocusedWorkflowSidebarSections`** |
56
+ | **`/examples/focused-workflow`** | Showcase: empty, steps, sidebar (toggle) |
57
+
58
+ ## Wiring checklist (implementers)
59
+
60
+ 1. **Route** under **`app/(app)/…/page.tsx`** — thin server page; heavy UI in a **client** component.
61
+ 2. **`siteHeader`**: **`back`** or **`breadcrumbs`** + **`title`**; avoid duplicating the trail in the body.
62
+ 3. Pick **`maxWidth`**: **`md`** for simple forms, **`lg`** for settings / wide fields.
63
+ 4. Choose **one** body layout; do **not** nest Miller columns or **`ListPageTemplate`** view tabs inside this shell.
64
+ 5. Domain logic stays in **`*-composer.tsx`** / **`*-client.tsx`**; templates stay generic **`FocusedWorkflow*`**.
65
+ 6. Run **§14** in **`AGENTS.md`** when reviewing.
66
+
67
+ ## AI execution checklist (copy for PRs)
68
+
69
+ - [ ] **`FocusedWorkflowPageTemplate`** on the route — not ad-hoc **`SidebarInset`** / list-hub shell.
70
+ - [ ] Correct body variant: **single column** | **step form** | **sidebar sections** | **empty**.
71
+ - [ ] Wizard/action footers use **`Shortcut`** + bare **`Kbd`** in buttons.
72
+ - [ ] **No** list-hub view tabs, **no** folder-column explorer inside the page.
73
+ - [ ] Template/component names remain **generic** (not tied to one entity).
74
+ - [ ] **§6.5** — no toast for product feedback.
75
+
76
+ ## Pair with
77
+
78
+ - **`exxat-page-vs-drawer.mdc`**, **`exxat-drawer-vs-dialog.mdc`**, **`exxat-kbd-shortcuts.mdc`**
79
+ - **`exxat-reuse-before-custom.mdc`** — extend **`focused-workflow-layouts.tsx`** before forking a second shell
80
+
81
+ ## See also
82
+
83
+ - **`components/examples/focused-workflow-showcase.tsx`**
84
+ - **`packages/ui/consumer-extras/patterns/focused-workflow-page-pattern.md`** (npm consumers)
@@ -1,7 +1,7 @@
1
1
  # Shell surface elevation (sidebar · secondary panel · page)
2
2
 
3
3
  > **Tokens:** `app/globals.css` — `--sidebar`, `--secondary-panel-bg`, `--background`, `--brand-tint*`.
4
- > **Shell:** `components/templates/nested-secondary-panel-shell.tsx` — `bg-[var(--secondary-panel-bg)]`.
4
+ > **Shell:** `components/templates/nested-secondary-panel-shell.tsx` — `bg-secondary-panel-bg` + `[data-slot="secondary-panel"]` rule in `globals.css`.
5
5
  > **Cursor:** `.cursor/rules/exxat-primary-nav-secondary-panel.mdc` · `.cursor/skills/exxat-primary-nav-secondary-panel/SKILL.md`
6
6
 
7
7
  ## Stack (back → front)
@@ -24,14 +24,16 @@
24
24
  ## OKLCH formulas (dark)
25
25
 
26
26
  ```css
27
- --secondary-panel-bg: color-mix(in oklch, var(--card) 32%, var(--brand-tint) 68%);
27
+ --secondary-panel-bg: color-mix(in oklch, var(--background) 32%, var(--sidebar-accent) 68%);
28
28
  ```
29
29
 
30
+ **Do not** mix with light-mode **`--brand-tint`** in dark — product theme classes keep a light **`--brand-tint`** for logos/KPI glow; the secondary rail must use **`--sidebar-accent`** so the Library panel stays on-hue and dark.
31
+
30
32
  Per-product **dark** theme blocks (`.theme-one.dark`, `.theme-prism.dark`, …) set **`--brand-tint-light`** where needed so mixes stay on-hue.
31
33
 
32
34
  ## Implementation
33
35
 
34
- - **`NestedSecondaryPanelShell`** — `bg-[var(--secondary-panel-bg)]`, `ring-sidebar-border` (not generic `ring-border` alone).
36
+ - **`NestedSecondaryPanelShell`** — `bg-secondary-panel-bg` (token + `[data-slot="secondary-panel"][data-state="open"]` in `globals.css`), `border-sidebar-border` (not generic `ring-border` alone).
35
37
  - **Do not** set secondary panel to `bg-sidebar` (same as level 0 — loses elevation).
36
38
  - **Do not** use `color-mix(… var(--sidebar) …)` without brand tokens if it drifts from active product theme.
37
39
 
@@ -3,36 +3,21 @@
3
3
  */
4
4
 
5
5
  import type { CommandMenuGroup, CommandMenuItem } from "@/lib/command-menu-config"
6
- import { ALL_PLACEMENTS } from "@/lib/mock/placements"
6
+ import { LIST_HUB_DIRECTORY } from "@/lib/mock/list-hub-directory"
7
7
 
8
8
  function sampleRowSearchItems(): CommandMenuItem[] {
9
- return ALL_PLACEMENTS.map((p) => {
10
- const nameParts = p.student.trim().split(/\s+/)
11
- return {
12
- id: `sample-row-${p.id}`,
13
- label: `Row ${p.id} — ${p.student}`,
14
- icon: "fa-light fa-table",
15
- href: `/data-list/${p.id}`,
16
- keywords: [
17
- `row ${p.id}`,
18
- p.student,
19
- ...nameParts,
20
- p.program,
21
- p.site,
22
- p.internship,
23
- p.specialization,
24
- p.email,
25
- p.supervisor,
26
- p.status,
27
- p.compliance,
28
- ]
29
- .filter(Boolean)
30
- .join(" "),
31
- }
32
- })
9
+ return LIST_HUB_DIRECTORY.map(row => ({
10
+ id: `sample-row-${row.id}`,
11
+ label: row.title,
12
+ icon: "fa-light fa-calendar-days",
13
+ href: "/data-list",
14
+ keywords: [row.id, row.title, row.category, row.eventDate, "list hub", "calendar"]
15
+ .filter(Boolean)
16
+ .join(" "),
17
+ }))
33
18
  }
34
19
 
35
- /** Built once at module load — avoids remapping all placement rows on every layout render. */
20
+ /** Built once at module load — avoids remapping rows on every layout render. */
36
21
  export const COMMAND_MENU_SEARCH_DATA_GROUPS: CommandMenuGroup[] = [
37
22
  {
38
23
  id: "sample-rows",
@@ -42,7 +27,6 @@ export const COMMAND_MENU_SEARCH_DATA_GROUPS: CommandMenuGroup[] = [
42
27
  },
43
28
  ]
44
29
 
45
- /** Demo rows for the list hub — search-only so the palette stays lightweight on open. */
46
30
  export function getCommandMenuSearchDataGroups(): CommandMenuGroup[] {
47
31
  return COMMAND_MENU_SEARCH_DATA_GROUPS
48
32
  }
@@ -1,10 +1,13 @@
1
1
  /**
2
- * Display options for Data list (table / board / etc.) — shared across view types
2
+ * Display options for Data list (table / board / calendar / etc.) — shared across view types
3
3
  * so hide/show preferences persist when switching views.
4
4
  */
5
5
 
6
6
  export type BoardLineCount = 1 | 2 | 3
7
7
 
8
+ /** Right-hand calendar body: month grid vs week rows (scroll stack uses the same months). */
9
+ export type CalendarMainView = "month" | "week"
10
+
8
11
  export interface DataListDisplayOptions {
9
12
  /**
10
13
  * Board swimlanes: dataset field (table column key) used to split cards into columns.
@@ -13,7 +16,7 @@ export interface DataListDisplayOptions {
13
16
  boardGroupByColumnKey: string
14
17
  /** Max lines for primary text blocks on board cards */
15
18
  boardLineCount: BoardLineCount
16
- /** Page title block (Placements + subtitle) */
19
+ /** Page title block */
17
20
  showViewTitle: boolean
18
21
  /** Board: phase column titles + descriptions. Table: column header row. */
19
22
  showColumnLabels: boolean
@@ -22,6 +25,10 @@ export interface DataListDisplayOptions {
22
25
  boardNewCardAbove: boolean
23
26
  /** Toolbar search control (table view) */
24
27
  showToolbarSearch: boolean
28
+ /** Calendar: left column — mini month, event list, layout tiles */
29
+ showCalendarSummaryPanel: boolean
30
+ /** Calendar: main scrollable body layout */
31
+ calendarMainView: CalendarMainView
25
32
  }
26
33
 
27
34
  export const DEFAULT_DATA_LIST_DISPLAY_OPTIONS: DataListDisplayOptions = {
@@ -32,4 +39,11 @@ export const DEFAULT_DATA_LIST_DISPLAY_OPTIONS: DataListDisplayOptions = {
32
39
  showBoardColumnCounts: true,
33
40
  boardNewCardAbove: true,
34
41
  showToolbarSearch: true,
42
+ showCalendarSummaryPanel: false,
43
+ calendarMainView: "month",
35
44
  }
45
+
46
+ export const CALENDAR_MAIN_VIEW_TILES = [
47
+ { value: "month" as const, label: "Month", icon: "fa-calendar-days" },
48
+ { value: "week" as const, label: "Week", icon: "fa-calendar-week" },
49
+ ]