@exxatdesignux/ui 0.2.17 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -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 +22 -7
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
- 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 +42 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/components/ui/sidebar.tsx +2 -2
- package/src/globals.css +7 -1807
- 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/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/AGENTS.md +66 -21
- 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)/error.tsx +22 -6
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/layout.tsx +13 -6
- 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/global-error.tsx +63 -0
- package/template/app/globals.css +7 -1934
- package/template/app/layout.tsx +2 -0
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +71 -55
- package/template/components/data-table/index.tsx +31 -67
- package/template/components/data-table/use-table-state.ts +33 -6
- 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/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/exxat-product-logo.tsx +2 -6
- package/template/components/key-metrics.tsx +54 -22
- 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 +5 -5
- package/template/components/product-wordmark.tsx +4 -7
- 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-hub-client.tsx +2 -5
- package/template/components/question-bank-table.tsx +155 -509
- 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/sidebar-shell.tsx +2 -1
- package/template/components/table-properties/drawer-button.tsx +51 -20
- package/template/components/table-properties/drawer.tsx +81 -17
- 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 +40 -13
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/contexts/product-context.tsx +21 -2
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +42 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/kpi-flat-band-pattern.md +57 -0
- package/template/docs/kpi-strip-max-four-pattern.md +1 -0
- package/template/docs/shell-surface-elevation-pattern.md +54 -0
- package/template/lib/chunk-load-error.ts +13 -0
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/conditional-rule-match.ts +87 -22
- 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 +16 -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/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +60 -13
- 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 -632
- 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 -1675
- 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 -402
- 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 -714
- 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,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,7 +47,11 @@ import {
|
|
|
47
47
|
Shortcut,
|
|
48
48
|
} from "@/components/ui/dropdown-menu"
|
|
49
49
|
import type { DataListViewType } from "@/lib/data-list-view"
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
dataListViewTilesForHub,
|
|
52
|
+
showsListPageHubMetricsStrip,
|
|
53
|
+
} from "@/lib/data-list-view-registry"
|
|
54
|
+
import { DATA_LIST_VIEW_TILES, dataListViewAddShortcut } from "@/lib/data-list-view"
|
|
51
55
|
import {
|
|
52
56
|
createListPageEditViewHandler,
|
|
53
57
|
type OpenTablePropertiesHandle,
|
|
@@ -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,14 +296,17 @@ export function ListPageTemplate({
|
|
|
272
296
|
}
|
|
273
297
|
|
|
274
298
|
return (
|
|
275
|
-
|
|
276
|
-
{!hideViewsToolbar &&
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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) => {
|
|
301
|
+
const keys = dataListViewAddShortcut(i)
|
|
302
|
+
return keys ? (
|
|
303
|
+
<Shortcut
|
|
304
|
+
key={v.type}
|
|
305
|
+
keys={keys}
|
|
306
|
+
onInvoke={() => addView(v.type)}
|
|
307
|
+
/>
|
|
308
|
+
) : null
|
|
309
|
+
})}
|
|
283
310
|
{activeTab && !hideViewsToolbar && (
|
|
284
311
|
<>
|
|
285
312
|
<Shortcut keys="F2" onInvoke={() => openRename(activeTab)} />
|
|
@@ -299,7 +326,7 @@ export function ListPageTemplate({
|
|
|
299
326
|
)}
|
|
300
327
|
{header}
|
|
301
328
|
|
|
302
|
-
{
|
|
329
|
+
{metricsVisible ? metrics : null}
|
|
303
330
|
|
|
304
331
|
{/* ── Views toolbar (not tablist: settings/close are not tabs — WCAG 1.3.1 / ARIA) ── */}
|
|
305
332
|
{!hideViewsToolbar && (
|
|
@@ -477,10 +504,10 @@ export function ListPageTemplate({
|
|
|
477
504
|
<DropdownMenuContent align="start">
|
|
478
505
|
<DropdownMenuLabel className="text-xs">Add a view</DropdownMenuLabel>
|
|
479
506
|
<DropdownMenuSeparator />
|
|
480
|
-
{
|
|
507
|
+
{addableViewTypes.map((v, i) => (
|
|
481
508
|
<DropdownMenuItem
|
|
482
509
|
key={v.type}
|
|
483
|
-
shortcut={i
|
|
510
|
+
shortcut={dataListViewAddShortcut(i)}
|
|
484
511
|
onSelect={() => addView(v.type)}
|
|
485
512
|
>
|
|
486
513
|
<i className={`fa-light ${v.icon}`} aria-hidden="true" />
|
|
@@ -576,6 +603,6 @@ export function ListPageTemplate({
|
|
|
576
603
|
</DialogFooter>
|
|
577
604
|
</DialogContent>
|
|
578
605
|
</Dialog>
|
|
579
|
-
|
|
606
|
+
</div>
|
|
580
607
|
)
|
|
581
608
|
}
|
|
@@ -15,7 +15,7 @@ export interface NestedSecondaryPanelShellProps {
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Shared chrome for a nested hub rail — full width vs icon rail.
|
|
18
|
-
* Fill uses `--secondary-panel-bg`
|
|
18
|
+
* Fill uses `--secondary-panel-bg` — one step lighter than `--sidebar` (elevation 1).
|
|
19
19
|
*/
|
|
20
20
|
export function NestedSecondaryPanelShell({
|
|
21
21
|
open,
|
|
@@ -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)]",
|
|
@@ -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"
|
|
@@ -10,7 +10,26 @@
|
|
|
10
10
|
|
|
11
11
|
import * as React from "react"
|
|
12
12
|
import { useAppStore, type Product } from "@/stores/app-store"
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
brandForProduct,
|
|
15
|
+
EXXAT_ASSESSMENT_BRAND,
|
|
16
|
+
EXXAT_ONE_BRAND,
|
|
17
|
+
EXXAT_PRISM_BRAND,
|
|
18
|
+
} from "@/lib/product-brand"
|
|
19
|
+
|
|
20
|
+
const DEFAULT_PRODUCT_ACCENT: Record<Product, string> = {
|
|
21
|
+
"exxat-one": EXXAT_ONE_BRAND.brandColor,
|
|
22
|
+
"exxat-prism": EXXAT_PRISM_BRAND.brandColor,
|
|
23
|
+
"exxat-assessment": EXXAT_ASSESSMENT_BRAND.brandColor,
|
|
24
|
+
"exxat-custom": "",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function accentOverrideActive(product: Product, override: string | undefined): boolean {
|
|
28
|
+
if (!override?.trim()) return false
|
|
29
|
+
const defaultAccent = DEFAULT_PRODUCT_ACCENT[product]?.trim()
|
|
30
|
+
if (!defaultAccent) return true
|
|
31
|
+
return override.trim().toLowerCase() !== defaultAccent.toLowerCase()
|
|
32
|
+
}
|
|
14
33
|
|
|
15
34
|
export type { Product }
|
|
16
35
|
|
|
@@ -62,7 +81,7 @@ export function ProductProvider({ children }: { children: React.ReactNode }) {
|
|
|
62
81
|
// / theme-assessment` classes (with bespoke hue formulas in
|
|
63
82
|
// `globals.css`) are still used for the **default** look of each
|
|
64
83
|
// built-in.
|
|
65
|
-
const hasAccentOverride =
|
|
84
|
+
const hasAccentOverride = accentOverrideActive(product, productBrandColors[product])
|
|
66
85
|
let themeClass: "theme-one" | "theme-prism" | "theme-assessment" | "theme-custom"
|
|
67
86
|
if (hasAccentOverride) {
|
|
68
87
|
themeClass = "theme-custom"
|
|
@@ -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)
|
|
@@ -18,6 +18,8 @@ This document describes how list pages combine **views**, **toolbar** behavior,
|
|
|
18
18
|
| **Team page (primary template)** | `TeamClient` = `ListPageTemplate` + `KeyMetrics` + `TeamPageHeader` + `TeamTable` (same composition as `DataListClient`) | `components/team-client.tsx`, `lib/mock/team-kpi.ts` |
|
|
19
19
|
| **Team roster** | `TeamTable` — `DataTable` + `useTableState` + `TablePropertiesDrawer`; list/board/dashboard read **`tableState.rows`** | `components/team-table.tsx` |
|
|
20
20
|
| **Dashboard view (list tab)** | **`KeyMetrics`** (`variant="flat"` or `"card"`) — same KPI system as the template metrics strip; **do not** add ad-hoc `Card` grids for entity summaries | `TeamTable` dashboard branch, `lib/mock/team-kpi.ts` |
|
|
21
|
+
| **List hub metrics strip** | **`KeyMetrics variant="flat"`** — transparent cells, OKLCH brand glow only, border hairlines (**no** grey panel) | **`docs/kpi-flat-band-pattern.md`**, Placements / Team / Question bank clients |
|
|
22
|
+
| **Secondary panel chrome** | **`--secondary-panel-bg`** on **`NestedSecondaryPanelShell`** (lighter than sidebar, follows active product) | **`docs/shell-surface-elevation-pattern.md`**, Question bank |
|
|
21
23
|
| **Export** | `ExportDrawer` | `ListPageTemplate` export props; `DataListClient` |
|
|
22
24
|
| **View body layout** (gutter + centered max-width for folder / icon / panel-style content) | **`ListPageViewFrame`** (`components/data-views/list-page-view-frame.tsx`, re-exported from `components/data-views`) | **`FolderGridView`** (uses the frame); **`QuestionBankOsFolderView`** — see **`AGENTS.md` §4.5** |
|
|
23
25
|
|
|
@@ -36,6 +38,41 @@ Non-table view branches (e.g. **folder** icon grid, **panel** finder, OS-style f
|
|
|
36
38
|
|
|
37
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**).
|
|
38
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
|
+
|
|
39
76
|
## Architecture
|
|
40
77
|
|
|
41
78
|
- **Page shell** — `ListPageTemplate` owns the views toolbar (tabs), optional metrics, and export drawer. Content for the active tab is rendered via `renderContent`.
|
|
@@ -144,11 +181,13 @@ Reference: `components/placements-page-header.tsx`, `components/team-page-header
|
|
|
144
181
|
|
|
145
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.
|
|
146
183
|
|
|
147
|
-
**
|
|
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).
|
|
148
187
|
|
|
149
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.
|
|
150
189
|
|
|
151
|
-
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`**.
|
|
152
191
|
|
|
153
192
|
---
|
|
154
193
|
|
|
@@ -168,7 +207,7 @@ When a route is a **primary** destination in nav (main hub for an entity) **and*
|
|
|
168
207
|
- [ ] **>10 items** → search, filter, sort, properties (per surface type above).
|
|
169
208
|
- [ ] **Has data to export** → **More** menu with **Export** + shared `ExportDrawer` pattern.
|
|
170
209
|
- [ ] **Primary + large / main hub** → `ListPageTemplate`-style shell where applicable.
|
|
171
|
-
- [ ] **Page vs drawer vs dialog (§6.4)** — Quick with parent **context** → drawer/sheet; **blocking** confirm → **dialog**; primary or long flows → **
|
|
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`).
|
|
172
211
|
- [ ] **Primary button** → `Button` default variant (`size="lg"` for parity with Placements), not outline.
|
|
173
212
|
- [ ] **Dashboard view tab** → `KeyMetrics` + shared KPI helpers from **`tableState.rows`**; no duplicate one-off metric cards.
|
|
174
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
|