@exxatdesignux/ui 0.2.16 → 0.2.18
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 +26 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +149 -4
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +142 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/coach-marks.md +169 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +382 -0
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +56 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +19 -0
- package/consumer-extras/patterns/data-views-pattern.md +2 -0
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +52 -0
- package/package.json +3 -3
- package/src/components/ui/banner.tsx +2 -0
- package/src/components/ui/chart.tsx +57 -2
- package/src/components/ui/sidebar.tsx +3 -2
- package/src/globals.css +65 -14
- package/src/theme.css +3 -3
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
- package/template/AGENTS.md +27 -17
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/error.tsx +22 -6
- package/template/app/(app)/layout.tsx +13 -6
- package/template/app/(app)/question-bank/layout.tsx +18 -5
- package/template/app/(app)/question-bank/new/page.tsx +58 -0
- package/template/app/global-error.tsx +63 -0
- package/template/app/globals.css +151 -14
- package/template/app/layout.tsx +43 -5
- package/template/components/app-sidebar.tsx +68 -33
- package/template/components/ask-leo-sidebar.tsx +0 -2
- package/template/components/brand-color-picker.tsx +344 -0
- package/template/components/compliance-list-view.tsx +33 -51
- package/template/components/compliance-table.tsx +4 -0
- package/template/components/data-table/index.tsx +99 -91
- package/template/components/data-table/pagination.tsx +0 -1
- package/template/components/data-table/types.ts +4 -1
- package/template/components/data-table/use-table-state.ts +276 -100
- package/template/components/data-views/data-row-list.tsx +183 -0
- package/template/components/data-views/index.ts +7 -3
- package/template/components/data-views/os-folder-glyph.tsx +8 -0
- package/template/components/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/export-drawer.tsx +1 -1
- package/template/components/exxat-product-logo.tsx +168 -317
- package/template/components/invite-collaborators-drawer.tsx +5 -3
- package/template/components/key-metrics.tsx +122 -62
- package/template/components/new-placement-form.tsx +4 -2
- package/template/components/new-question-composer.tsx +2208 -0
- package/template/components/page-breadcrumb-trail.tsx +131 -0
- package/template/components/page-header.tsx +2 -1
- package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx} +2 -2
- package/template/components/placement-detail.tsx +1 -1
- package/template/components/placements-board-view.tsx +1 -1
- package/template/components/{data-list-client.tsx → placements-client.tsx} +9 -7
- package/template/components/placements-list-view.tsx +19 -133
- package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx} +2 -2
- package/template/components/{data-list-table-cells.tsx → placements-table-cells.tsx} +1 -1
- package/template/components/placements-table-columns.tsx +2 -2
- package/template/components/{data-list-table.tsx → placements-table.tsx} +42 -66
- package/template/components/product-switcher.tsx +24 -7
- package/template/components/product-wordmark.tsx +282 -0
- package/template/components/question-bank-client.tsx +20 -2
- package/template/components/question-bank-hub-client.tsx +105 -115
- package/template/components/question-bank-list-view.tsx +30 -54
- package/template/components/question-bank-new-folder-sheet.tsx +1 -1
- package/template/components/question-bank-secondary-nav.tsx +0 -3
- package/template/components/question-bank-table.tsx +19 -6
- package/template/components/rotations-empty-state.tsx +3 -0
- package/template/components/secondary-panel.tsx +23 -3
- package/template/components/settings-appearance-card.tsx +584 -141
- package/template/components/sidebar-shell.tsx +2 -1
- package/template/components/site-header.tsx +36 -31
- package/template/components/sites-list-view.tsx +31 -36
- package/template/components/sites-table.tsx +4 -0
- package/template/components/table-properties/drawer-button.tsx +38 -20
- package/template/components/table-properties/drawer.tsx +17 -14
- package/template/components/team-client.tsx +1 -1
- package/template/components/team-list-view.tsx +34 -50
- package/template/components/team-table.tsx +8 -3
- package/template/components/templates/list-page.tsx +12 -9
- package/template/components/templates/nested-secondary-panel-shell.tsx +10 -4
- package/template/components/ui/dot-pattern.tsx +50 -26
- package/template/components/ui/leo-icon.tsx +23 -3
- package/template/contexts/product-context.tsx +70 -7
- package/template/contexts/system-banner-context.tsx +112 -4
- package/template/docs/data-views-pattern.md +2 -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 +52 -0
- package/template/eslint.config.mjs +18 -0
- package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
- package/template/lib/chunk-load-error.ts +13 -0
- package/template/lib/conditional-rule-match.ts +87 -22
- package/template/lib/data-list-persistence.ts +57 -257
- package/template/lib/data-list-view.ts +6 -0
- package/template/lib/dev-log.test.ts +6 -5
- package/template/lib/exxat-palette.json +1462 -0
- package/template/lib/exxat-palette.ts +136 -0
- package/template/lib/list-page-table-properties.ts +1 -1
- package/template/lib/list-status-badges.ts +1 -1
- package/template/lib/mailto.ts +29 -0
- package/template/lib/placement-board-card-layout.ts +1 -1
- package/template/lib/product-brand.ts +268 -0
- package/template/lib/question-bank-authoring.ts +308 -0
- package/template/lib/question-bank-nav.ts +44 -0
- package/template/lib/raf-throttle.ts +45 -0
- package/template/lib/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +521 -0
- package/template/next.config.mjs +156 -0
- package/template/package.json +3 -3
- package/template/stores/app-store.ts +46 -1
|
@@ -153,6 +153,7 @@ export function SystemBanner({
|
|
|
153
153
|
<a
|
|
154
154
|
href={action.href}
|
|
155
155
|
className="inline-flex shrink-0 items-center gap-1 text-xs font-semibold underline underline-offset-2 hover:no-underline"
|
|
156
|
+
suppressHydrationWarning
|
|
156
157
|
>
|
|
157
158
|
{action.label}
|
|
158
159
|
<i className="fa-light fa-arrow-right text-xs" aria-hidden="true" />
|
|
@@ -185,6 +186,7 @@ export function SystemBanner({
|
|
|
185
186
|
...(variant === "promo" ? { boxShadow: promoOuterShadow } : null),
|
|
186
187
|
...style,
|
|
187
188
|
}}
|
|
189
|
+
suppressHydrationWarning
|
|
188
190
|
{...props}
|
|
189
191
|
>
|
|
190
192
|
{decorativeOverlay ? (
|
|
@@ -69,6 +69,50 @@ function ChartContainer({
|
|
|
69
69
|
)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Conservative validators for the two values interpolated into a `<style>`
|
|
74
|
+
* block via `dangerouslySetInnerHTML`.
|
|
75
|
+
*
|
|
76
|
+
* `ChartConfig.color` is typed as a free `string` and may be authored by
|
|
77
|
+
* downstream consumers of `@exxatdesignux/ui` who could pass user-controlled
|
|
78
|
+
* data. To prevent CSS injection (escaping the property value, closing the
|
|
79
|
+
* block, or injecting `</style>`) we accept only a documented allowlist of
|
|
80
|
+
* CSS color syntaxes and reject anything that contains rule-terminating or
|
|
81
|
+
* markup-sensitive characters.
|
|
82
|
+
*
|
|
83
|
+
* Keys come from `ChartConfig` and become CSS custom-property names, so they
|
|
84
|
+
* are restricted to a safe identifier alphabet.
|
|
85
|
+
*/
|
|
86
|
+
const CSS_KEY_PATTERN = /^[A-Za-z0-9_-]+$/
|
|
87
|
+
|
|
88
|
+
const SAFE_COLOR_PATTERN = new RegExp(
|
|
89
|
+
[
|
|
90
|
+
/^#[0-9a-fA-F]{3,8}$/, // #rgb / #rrggbb / #rrggbbaa
|
|
91
|
+
/^rgba?\([^;{}<>"'\\]*\)$/, // rgb()/rgba()
|
|
92
|
+
/^hsla?\([^;{}<>"'\\]*\)$/, // hsl()/hsla()
|
|
93
|
+
/^hwb\([^;{}<>"'\\]*\)$/, // hwb()
|
|
94
|
+
/^lab\([^;{}<>"'\\]*\)$/, // lab()
|
|
95
|
+
/^lch\([^;{}<>"'\\]*\)$/, // lch()
|
|
96
|
+
/^oklab\([^;{}<>"'\\]*\)$/, // oklab()
|
|
97
|
+
/^oklch\([^;{}<>"'\\]*\)$/, // oklch()
|
|
98
|
+
/^color\([^;{}<>"'\\]*\)$/, // color()
|
|
99
|
+
/^color-mix\([^;{}<>"'\\]*\)$/, // color-mix()
|
|
100
|
+
/^var\(--[A-Za-z0-9_-]+(?:\s*,[^;{}<>"'\\]+)?\)$/, // var(--token[, fallback])
|
|
101
|
+
/^[a-zA-Z]+$/, // named colors + currentColor/transparent
|
|
102
|
+
]
|
|
103
|
+
.map((re) => re.source)
|
|
104
|
+
.join("|"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
function sanitizeChartColor(color: string): string | null {
|
|
108
|
+
const trimmed = color.trim()
|
|
109
|
+
if (!trimmed) return null
|
|
110
|
+
// Defence-in-depth: any of these characters could break out of the value
|
|
111
|
+
// and turn the inline `<style>` block into an injection sink.
|
|
112
|
+
if (/[;{}<>"'\\]/.test(trimmed)) return null
|
|
113
|
+
return SAFE_COLOR_PATTERN.test(trimmed) ? trimmed : null
|
|
114
|
+
}
|
|
115
|
+
|
|
72
116
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
73
117
|
const colorConfig = Object.entries(config).filter(
|
|
74
118
|
([, config]) => config.theme || config.color
|
|
@@ -78,6 +122,13 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
|
78
122
|
return null
|
|
79
123
|
}
|
|
80
124
|
|
|
125
|
+
// `id` is generated from `React.useId()` in `ChartContainer`, but consumers
|
|
126
|
+
// can override it via the `id` prop, so we still verify the shape before
|
|
127
|
+
// interpolating it into a CSS selector.
|
|
128
|
+
if (!CSS_KEY_PATTERN.test(id)) {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
81
132
|
return (
|
|
82
133
|
<style
|
|
83
134
|
dangerouslySetInnerHTML={{
|
|
@@ -87,11 +138,15 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
|
87
138
|
${prefix} [data-chart=${id}] {
|
|
88
139
|
${colorConfig
|
|
89
140
|
.map(([key, itemConfig]) => {
|
|
90
|
-
|
|
141
|
+
if (!CSS_KEY_PATTERN.test(key)) return null
|
|
142
|
+
const rawColor =
|
|
91
143
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
92
144
|
itemConfig.color
|
|
93
|
-
|
|
145
|
+
if (!rawColor) return null
|
|
146
|
+
const safeColor = sanitizeChartColor(rawColor)
|
|
147
|
+
return safeColor ? ` --color-${key}: ${safeColor};` : null
|
|
94
148
|
})
|
|
149
|
+
.filter(Boolean)
|
|
95
150
|
.join("\n")}
|
|
96
151
|
}
|
|
97
152
|
`
|
|
@@ -80,9 +80,9 @@ function SidebarProvider({
|
|
|
80
80
|
if (typeof window === "undefined") return
|
|
81
81
|
if (window.matchMedia(SIDEBAR_COOKIE_VIEWPORT_MQ).matches) return
|
|
82
82
|
const fromCookie = readSidebarStateCookie()
|
|
83
|
-
if (fromCookie === undefined) return
|
|
83
|
+
if (fromCookie === undefined || fromCookie === open) return
|
|
84
84
|
_setOpen(fromCookie)
|
|
85
|
-
}, [openProp])
|
|
85
|
+
}, [openProp, open])
|
|
86
86
|
|
|
87
87
|
const setOpen = React.useCallback(
|
|
88
88
|
(value: boolean | ((value: boolean) => boolean)) => {
|
|
@@ -490,6 +490,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
|
490
490
|
<li
|
|
491
491
|
data-slot="sidebar-menu-item"
|
|
492
492
|
data-sidebar="menu-item"
|
|
493
|
+
suppressHydrationWarning
|
|
493
494
|
className={cn(
|
|
494
495
|
"group/menu-item relative",
|
|
495
496
|
/* Icon rail: center the square menu control in the column (footer + primary). */
|
package/src/globals.css
CHANGED
|
@@ -173,10 +173,21 @@ html[data-text-size="large"] {
|
|
|
173
173
|
--leo-surface-tint-b: color-mix(in oklch, var(--brand-color) 8%, var(--background));
|
|
174
174
|
--leo-surface-gradient: linear-gradient(180deg, var(--leo-surface-tint-a) 0%, var(--leo-surface-tint-b) 100%);
|
|
175
175
|
|
|
176
|
-
/* KeyMetrics `variant="flat"` —
|
|
177
|
-
--key-metrics-flat-
|
|
178
|
-
--key-metrics-flat-
|
|
179
|
-
--key-metrics-flat-
|
|
176
|
+
/* KeyMetrics `variant="flat"` — no band surface; bottom brand glow only (OKLCH). */
|
|
177
|
+
--key-metrics-flat-cell-bg: transparent;
|
|
178
|
+
--key-metrics-flat-divider: color-mix(in oklch, var(--sidebar-border) 55%, transparent);
|
|
179
|
+
--key-metrics-flat-band-radial: radial-gradient(
|
|
180
|
+
ellipse 120% 68% at 50% 100%,
|
|
181
|
+
color-mix(in oklch, var(--brand-color) 20%, transparent) 0%,
|
|
182
|
+
color-mix(in oklch, var(--brand-color) 8%, transparent) 42%,
|
|
183
|
+
transparent 72%
|
|
184
|
+
);
|
|
185
|
+
--key-metrics-flat-band-shadow: none;
|
|
186
|
+
--key-metrics-card-glow-radial: radial-gradient(
|
|
187
|
+
ellipse 110% 90% at 50% 100%,
|
|
188
|
+
color-mix(in oklch, var(--brand-color) 18%, transparent) 0%,
|
|
189
|
+
transparent 65%
|
|
190
|
+
);
|
|
180
191
|
|
|
181
192
|
/* ── Surfaces ────────────────────────────────────────────────── */
|
|
182
193
|
--background: oklch(1 0 0);
|
|
@@ -256,8 +267,8 @@ html[data-text-size="large"] {
|
|
|
256
267
|
--sidebar-ring: oklch(0.25 0 0);
|
|
257
268
|
/* Nav section titles — ≥4.5:1 vs --sidebar (not vs page white) */
|
|
258
269
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 58%, var(--sidebar));
|
|
259
|
-
/* Nested secondary rail —
|
|
260
|
-
--secondary-panel-bg: color-mix(in oklch, var(--
|
|
270
|
+
/* Nested secondary rail — elevation 1: brand wash, lighter than `--brand-tint` / sidebar. */
|
|
271
|
+
--secondary-panel-bg: color-mix(in oklch, var(--background) 40%, var(--brand-tint-light) 60%);
|
|
261
272
|
|
|
262
273
|
/* Browser UI (meta theme-color) — aligned with --brand-tint */
|
|
263
274
|
--theme-color-chrome: #f3f2f8;
|
|
@@ -365,9 +376,20 @@ html[data-text-size="large"] {
|
|
|
365
376
|
--destructive: oklch(0.65 0.20 25); /* brighter for dark bg */
|
|
366
377
|
--destructive-foreground: oklch(0.10 0 0);
|
|
367
378
|
|
|
368
|
-
|
|
369
|
-
--key-metrics-flat-
|
|
370
|
-
--key-metrics-flat-
|
|
379
|
+
/* KeyMetrics flat band — no surface; bottom brand glow only (OKLCH). */
|
|
380
|
+
--key-metrics-flat-cell-bg: transparent;
|
|
381
|
+
--key-metrics-flat-band-radial: radial-gradient(
|
|
382
|
+
ellipse 120% 68% at 50% 100%,
|
|
383
|
+
color-mix(in oklch, var(--brand-color) 26%, transparent) 0%,
|
|
384
|
+
color-mix(in oklch, var(--brand-color) 10%, transparent) 42%,
|
|
385
|
+
transparent 72%
|
|
386
|
+
);
|
|
387
|
+
--key-metrics-flat-band-shadow: none;
|
|
388
|
+
--key-metrics-card-glow-radial: radial-gradient(
|
|
389
|
+
ellipse 110% 90% at 50% 100%,
|
|
390
|
+
color-mix(in oklch, var(--brand-color) 22%, transparent) 0%,
|
|
391
|
+
transparent 62%
|
|
392
|
+
);
|
|
371
393
|
|
|
372
394
|
/* Borders — visible but not washed out on dark surfaces */
|
|
373
395
|
--border: oklch(0.38 0.008 270);
|
|
@@ -417,8 +439,8 @@ html[data-text-size="large"] {
|
|
|
417
439
|
--sidebar-border: oklch(0.38 0.010 270);
|
|
418
440
|
--sidebar-ring: oklch(0.85 0 0);
|
|
419
441
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 48%, var(--sidebar));
|
|
420
|
-
/* Nested secondary rail —
|
|
421
|
-
--secondary-panel-bg: color-mix(in oklch, var(--
|
|
442
|
+
/* Nested secondary rail — elevation 1: brand stack, lifted toward `--card` / page. */
|
|
443
|
+
--secondary-panel-bg: color-mix(in oklch, var(--card) 32%, var(--brand-tint) 68%);
|
|
422
444
|
--theme-color-chrome: #2f2d36;
|
|
423
445
|
|
|
424
446
|
/* Lifted scrim on dark — white-tinted veil, not heavy black */
|
|
@@ -464,6 +486,12 @@ html[data-text-size="large"] {
|
|
|
464
486
|
--secondary: oklch(0.95 0.012 286.1);
|
|
465
487
|
--accent: oklch(0.925 0.015 286.1);
|
|
466
488
|
--muted: oklch(0.945 0.008 286.1);
|
|
489
|
+
--key-metrics-flat-band-radial: radial-gradient(
|
|
490
|
+
ellipse 120% 68% at 50% 100%,
|
|
491
|
+
oklch(0.50 0.14 286.1 / 0.22) 0%,
|
|
492
|
+
oklch(0.50 0.14 286.1 / 0.09) 42%,
|
|
493
|
+
transparent 72%
|
|
494
|
+
);
|
|
467
495
|
}
|
|
468
496
|
|
|
469
497
|
.theme-one.dark,
|
|
@@ -479,6 +507,12 @@ html[data-text-size="large"] {
|
|
|
479
507
|
--secondary: oklch(0.31 0.04 286.1);
|
|
480
508
|
--muted: oklch(0.31 0.04 286.1);
|
|
481
509
|
--accent: oklch(0.33 0.06 286.1);
|
|
510
|
+
--key-metrics-flat-band-radial: radial-gradient(
|
|
511
|
+
ellipse 120% 68% at 50% 100%,
|
|
512
|
+
oklch(0.50 0.14 286.1 / 0.28) 0%,
|
|
513
|
+
oklch(0.50 0.14 286.1 / 0.1) 42%,
|
|
514
|
+
transparent 72%
|
|
515
|
+
);
|
|
482
516
|
}
|
|
483
517
|
|
|
484
518
|
/* ==========================================================================
|
|
@@ -487,9 +521,14 @@ html[data-text-size="large"] {
|
|
|
487
521
|
========================================================================== */
|
|
488
522
|
.theme-prism,
|
|
489
523
|
.theme-rose {
|
|
490
|
-
--brand-
|
|
491
|
-
--brand-
|
|
492
|
-
--
|
|
524
|
+
--brand-tint: oklch(0.97 0.02 343);
|
|
525
|
+
--brand-tint-light: oklch(0.992 0.01 343);
|
|
526
|
+
--brand-tint-subtle: oklch(0.93 0.028 343);
|
|
527
|
+
--brand-color: oklch(0.57 0.24 342); /* Prism rose */
|
|
528
|
+
--brand-color-light: oklch(0.78 0.14 342);
|
|
529
|
+
--brand-color-dark: oklch(0.42 0.24 342);
|
|
530
|
+
--brand-color-deep: oklch(0.32 0.20 342);
|
|
531
|
+
--ring: var(--brand-color-dark);
|
|
493
532
|
}
|
|
494
533
|
|
|
495
534
|
.theme-prism:not(.dark),
|
|
@@ -502,6 +541,12 @@ html[data-text-size="large"] {
|
|
|
502
541
|
--muted: oklch(0.945 0.008 343);
|
|
503
542
|
--banner-prism-bg: oklch(0.97 0.02 343);
|
|
504
543
|
--theme-color-chrome: #fff5f9;
|
|
544
|
+
--key-metrics-flat-band-radial: radial-gradient(
|
|
545
|
+
ellipse 120% 68% at 50% 100%,
|
|
546
|
+
oklch(0.57 0.24 342 / 0.22) 0%,
|
|
547
|
+
oklch(0.57 0.24 342 / 0.09) 42%,
|
|
548
|
+
transparent 72%
|
|
549
|
+
);
|
|
505
550
|
}
|
|
506
551
|
|
|
507
552
|
.theme-prism.dark,
|
|
@@ -517,6 +562,12 @@ html[data-text-size="large"] {
|
|
|
517
562
|
--muted: oklch(0.31 0.04 342);
|
|
518
563
|
--accent: oklch(0.33 0.06 342);
|
|
519
564
|
--theme-color-chrome: #2a2428;
|
|
565
|
+
--key-metrics-flat-band-radial: radial-gradient(
|
|
566
|
+
ellipse 120% 68% at 50% 100%,
|
|
567
|
+
oklch(0.57 0.24 342 / 0.28) 0%,
|
|
568
|
+
oklch(0.57 0.24 342 / 0.1) 42%,
|
|
569
|
+
transparent 72%
|
|
570
|
+
);
|
|
520
571
|
}
|
|
521
572
|
|
|
522
573
|
/* ==========================================================================
|
package/src/theme.css
CHANGED
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
/* Nav section titles — ≥4.5:1 vs --sidebar (not vs page white) */
|
|
221
221
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 58%, var(--sidebar));
|
|
222
222
|
/* Nested secondary rail — soft brand wash on canvas (not pure `--background`, not full `--sidebar`). */
|
|
223
|
-
--secondary-panel-bg: color-mix(in oklch, var(--
|
|
223
|
+
--secondary-panel-bg: color-mix(in oklch, var(--background) 50%, var(--sidebar) 50%);
|
|
224
224
|
|
|
225
225
|
/* Browser UI (meta theme-color) — aligned with --brand-tint */
|
|
226
226
|
--theme-color-chrome: #f3f2f8;
|
|
@@ -346,8 +346,8 @@
|
|
|
346
346
|
--sidebar-border: oklch(0.38 0.010 270);
|
|
347
347
|
--sidebar-ring: oklch(0.85 0 0);
|
|
348
348
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 48%, var(--sidebar));
|
|
349
|
-
/* Nested secondary rail — dark:
|
|
350
|
-
--secondary-panel-bg: color-mix(in oklch, var(--background)
|
|
349
|
+
/* Nested secondary rail — dark: neutral step between page canvas and sidebar (no per-product brand wash). */
|
|
350
|
+
--secondary-panel-bg: color-mix(in oklch, var(--background) 58%, var(--sidebar) 42%);
|
|
351
351
|
--theme-color-chrome: #2f2d36;
|
|
352
352
|
|
|
353
353
|
/* Lifted scrim on dark — white-tinted veil, not heavy black */
|
|
@@ -21,7 +21,7 @@ description: >
|
|
|
21
21
|
- **Stack:** Next.js 16 (App Router), React, TypeScript, Tailwind CSS, shadcn/ui primitives, Font Awesome icons
|
|
22
22
|
- **App root:** `exxat-ds/app/(app)/` — route group that wraps all authenticated pages
|
|
23
23
|
- **Single source of truth:** `exxat-ds/AGENTS.md` for full prose explanations; this skill is the actionable summary
|
|
24
|
-
- **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-dedicated-search-surfaces`, `exxat-accessibility`, `exxat-board-cards`, `exxat-collaboration-access` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
|
|
24
|
+
- **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-mono-ids`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-dedicated-search-surfaces`, `exxat-accessibility`, `exxat-board-cards`, `exxat-collaboration-access` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
@@ -66,7 +66,7 @@ Use `@/components/ui/kbd` (`Kbd` + `KbdGroup`) anywhere users discover actions b
|
|
|
66
66
|
| Duplicate | ⌘/Ctrl + **D** |
|
|
67
67
|
| Review / Info | ⌘/Ctrl + **I** |
|
|
68
68
|
| Remove / Delete | ⌘/Ctrl + **⌫** |
|
|
69
|
-
| Add view (1..n) |
|
|
69
|
+
| Add view (1..n) | **1..9** (plain digit; skipped in inputs / open dialogs) |
|
|
70
70
|
| **Submit a workflow** (Create, Save, Export, Apply) | **Enter** (⏎) — scoped to the form/drawer/dialog |
|
|
71
71
|
| **Cancel / dismiss** a workflow | **Esc** (Radix handles for Dialog/Sheet) |
|
|
72
72
|
| **Advance a multi-step wizard** | ⌘/Ctrl + **Enter** (plain Enter stays in the input) |
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Exxat DS — monospace typography for record IDs, question IDs, and other system identifiers
|
|
3
|
+
globs: apps/web/**/*.tsx
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Exxat DS — monospace IDs
|
|
8
|
+
|
|
9
|
+
Use this when rendering **system identifiers** — values a user copies, searches, or matches in APIs and tables (not human-readable names or prose).
|
|
10
|
+
|
|
11
|
+
## MUST
|
|
12
|
+
|
|
13
|
+
1. **Class** — Wrap identifier text in **`font-mono tabular-nums`**. Add size/color from context: typically **`text-xs text-muted-foreground`** (secondary line, table meta) or **`text-sm`** when the ID is the primary label in a narrow cell.
|
|
14
|
+
2. **What counts as an ID** — Question IDs (`questionId`, `Q-YYMM-XXXX`), record/entity keys shown in UI, folder/surface technical keys when displayed as identifiers, hex tokens in pickers, audit/log principals, site/row **`id`** columns meant for lookup.
|
|
15
|
+
3. **Mixed lines** — When an ID sits beside prose (e.g. page subtitle), only the ID segment is mono; keep separators and labels in the default sans stack.
|
|
16
|
+
|
|
17
|
+
## SHOULD
|
|
18
|
+
|
|
19
|
+
- Match existing hubs: **`question-bank-table.tsx`**, **`question-bank-list-view.tsx`**, **`new-question-composer.tsx`** (header subtitle), **`sites-table.tsx`** (`row.id`).
|
|
20
|
+
- Prefer **`truncate`** / **`min-w-0`** on mono IDs in tight layouts so long tokens do not blow out columns.
|
|
21
|
+
|
|
22
|
+
## MUST NOT
|
|
23
|
+
|
|
24
|
+
- Apply **`font-mono`** to **person names**, **folder display names**, **status labels**, **dates**, **counts**, **currency**, or **body copy** — only the identifier token.
|
|
25
|
+
- Use mono for **option letters** (A/B/C) or **step numbers** unless they are literal system IDs.
|
|
26
|
+
|
|
27
|
+
## See also
|
|
28
|
+
|
|
29
|
+
- **`.cursor/skills/exxat-mono-ids/SKILL.md`**
|
|
30
|
+
- **`apps/web/AGENTS.md`** — §1 item on IDs, §13 checklist
|
package/template/AGENTS.md
CHANGED
|
@@ -18,23 +18,26 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
|
|
|
18
18
|
6. **Before** adding or changing **board (kanban) cards** on list hubs, read **§4.4** and the **`exxat-board-cards`** skill (**`.cursor/skills/`** or **`.claude/skills/`** at repo root — same content).
|
|
19
19
|
7. **Before** adding **folder, panel, or other non-table view bodies** (centered grids, reusable shells), read **§4.5** and **`.cursor/rules/exxat-list-page-view-shells.mdc`** / **`.cursor/skills/exxat-list-page-view-shells/SKILL.md`**.
|
|
20
20
|
8. **Before** adding or changing **Font Awesome** icons in app UI, read **`.cursor/rules/exxat-fontawesome-icons.mdc`** (Kit subsetting, weights, **`aria-hidden`** on **`<i>`**).
|
|
21
|
-
9. **Before**
|
|
22
|
-
10. **Before** adding **
|
|
23
|
-
11. **Before** adding **
|
|
24
|
-
12. **Before**
|
|
25
|
-
13. **Before**
|
|
26
|
-
14. **Before**
|
|
27
|
-
15.
|
|
21
|
+
9. **Before** rendering **record IDs, question IDs, or other system identifiers**, read **`.cursor/rules/exxat-mono-ids.mdc`** and **`.cursor/skills/exxat-mono-ids/SKILL.md`** (**`font-mono tabular-nums`**).
|
|
22
|
+
10. **Before** adding a **primary nav row** that opens a **nested secondary nav panel** (Question bank style), read **§4.6** and **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**.
|
|
23
|
+
11. **Before** adding **shared access / invite collaborators** on a hub (face stack + invite sheet), read **§4.7** and **`.cursor/rules/exxat-collaboration-access.mdc`** / **`.cursor/skills/exxat-collaboration-access/SKILL.md`**.
|
|
24
|
+
12. **Before** adding **onboarding tours, feature walkthroughs, or coach marks**, read **§11** and `references/coach-marks.md`.
|
|
25
|
+
13. **Before** changing the **global command palette (⌘K)** or search/AI entry UX, read **§7.1** and **`docs/command-menu-pattern.md`**.
|
|
26
|
+
14. **Before** choosing **drawer vs dialog vs new page** for a task flow, read **§6.4**, **`docs/data-views-pattern.md`** (Page vs drawer), and **`docs/drawer-vs-dialog-pattern.md`** (modal vs side panel on the same route).
|
|
27
|
+
15. **Before** adding **success/error/confirmation feedback**, read **§6.5** and **`.cursor/rules/exxat-no-toast.mdc`** (no toast or snackbars).
|
|
28
|
+
16. Prefer **composing existing components** over new one-off UI. If something is missing, **extend** shared components under `components/`, not a single page file.
|
|
28
29
|
- **MUST** scan `components/` (especially `components/ui/`, `components/data-views/`, `components/templates/`, `components/key-metrics.tsx`, `components/page-header.tsx`, and the charts/banner/dot-pattern surfaces) **before** writing any new UI. If a primitive or composition already exists, **use it** — don't build a parallel one.
|
|
29
30
|
- **Examples of existing surfaces to reuse:** card grid → `ListPageBoardCard` + `BoardCardIconRow` / `BoardCardTwoLineBlock`; AI / dot animation → `AiThinkingOverlay` + `DotPattern`; search input → `InputGroup` + `InputGroupAddon` + `InputGroupInput`; page title → `PageHeader` (serif via `font-heading`); list hub shell → `ListPageTemplate` (`metrics`, `defaultTabs`, `renderContent`); metrics strip → `KeyMetrics`; **view body gutter + centered max-width** → **`ListPageViewFrame`** (**§4.5**); **shared access / invite** → **`PageHeader` `variant="collaboration"`** + **`InviteCollaboratorsDrawer`** (**§4.7**).
|
|
30
31
|
- **If** nothing fits and you would add a **new shared primitive or large bespoke widget**: **ask the user** for direction first — **`.cursor/rules/exxat-reuse-before-custom.mdc`** (unless the task already explicitly approved a greenfield build).
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
17. **Match** naming, imports, and patterns of the nearest reference implementation (usually Placements).
|
|
33
|
+
18. **Before** adding entity **mock data**, a **new view tab**, or **detail/inspector** panels on a list hub, read **`.cursor/rules/exxat-centralized-list-dataset.mdc`** and **`.cursor/skills/exxat-centralized-list-dataset/SKILL.md`** (single **`useTableState`** row bag for every view; **no** parallel mock arrays per view).
|
|
34
|
+
19. **Before** choosing **cards vs table rows vs simple lists** for a hub, read **`docs/card-vs-rows-pattern.md`** and **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
|
|
35
|
+
20. **Before** adding **`KeyMetrics`** strips on list hubs or dashboard key-metrics cards, read **`docs/kpi-strip-max-four-pattern.md`** and **`.cursor/rules/exxat-kpi-max-four.mdc`** (at most **four** tiles).
|
|
36
|
+
21. **Before** styling **`KeyMetrics variant="flat"`** (list hub metrics strip, dashboard mix KPI band), read **`docs/kpi-flat-band-pattern.md`** and **`.cursor/rules/exxat-kpi-flat-band.mdc`** / **`.cursor/skills/exxat-kpi-flat-band/SKILL.md`** (transparent band, OKLCH glow, border hairlines only).
|
|
37
|
+
22. **Before** changing **secondary panel** or **sidebar** brand chrome, read **`docs/shell-surface-elevation-pattern.md`** and **§4.6** ( **`--secondary-panel-bg`**, active product theme).
|
|
38
|
+
23. **Before** adding **new shared UI primitives** or bespoke widgets when nothing in **`components/`** fits after scanning, follow **`.cursor/rules/exxat-reuse-before-custom.mdc`** — **ask the user** for direction unless the task already approved a greenfield build.
|
|
36
39
|
|
|
37
|
-
**Longer narrative and architecture:** `docs/data-views-pattern.md`, `docs/drawer-vs-dialog-pattern.md`, `docs/card-vs-rows-pattern.md`, `docs/kpi-strip-max-four-pattern.md`, `docs/command-menu-pattern.md`, `docs/collaboration-access-pattern.md` (keep in sync with this handbook for big refactors).
|
|
40
|
+
**Longer narrative and architecture:** `docs/data-views-pattern.md`, `docs/drawer-vs-dialog-pattern.md`, `docs/card-vs-rows-pattern.md`, `docs/kpi-strip-max-four-pattern.md`, **`docs/kpi-flat-band-pattern.md`**, **`docs/shell-surface-elevation-pattern.md`**, `docs/command-menu-pattern.md`, `docs/collaboration-access-pattern.md` (keep in sync with this handbook for big refactors).
|
|
38
41
|
|
|
39
42
|
---
|
|
40
43
|
|
|
@@ -42,8 +45,8 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
|
|
|
42
45
|
|
|
43
46
|
1. **User / task instructions** in the current session (highest).
|
|
44
47
|
2. This **`AGENTS.md`** for Exxat DS product patterns.
|
|
45
|
-
3. **`.cursor/rules/*.mdc`** at repo root (`exxat-data-tables`, `exxat-list-page-connected-views`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-table-properties-drawer`, `exxat-board-cards`, `exxat-page-vs-drawer`, `exxat-drawer-vs-dialog`, `exxat-card-vs-list-rows`, `exxat-kpi-max-four`, `exxat-reuse-before-custom`, `exxat-no-toast`, `exxat-kbd-shortcuts`, `exxat-accessibility`, `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-collaboration-access`, `exxat-ds-agents`) and any rules under **`exxat-ds/.cursor/rules/`** (including **`exxat-dashboard-view-charts`** for Data view charts).
|
|
46
|
-
4. Project **skills** under `.cursor/skills/` when relevant — e.g. **shadcn**, **exxat-accessibility** (WCAG / ARIA / touch / contrast), **exxat-board-cards** (kanban card shell, status badges, primitives), **exxat-list-page-view-shells** (centered view bodies, **`ListPageViewFrame`**), **exxat-centralized-list-dataset** (one **`useTableState`** row bag + shared maps across all list-hub views and **`TablePropertiesDrawer`**), **exxat-collaboration-access** (face rail + invite sheet + library access), **exxat-dedicated-search-surfaces** (landing vs results split, **`DedicatedSearch*`** templates + recents without hydration drift), **exxat-drawer-vs-dialog**, **exxat-card-vs-list-rows**, **exxat-kpi-max-four
|
|
48
|
+
3. **`.cursor/rules/*.mdc`** at repo root (`exxat-data-tables`, `exxat-list-page-connected-views`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-table-properties-drawer`, `exxat-board-cards`, `exxat-page-vs-drawer`, `exxat-drawer-vs-dialog`, `exxat-card-vs-list-rows`, `exxat-kpi-max-four`, `exxat-reuse-before-custom`, `exxat-no-toast`, `exxat-kbd-shortcuts`, `exxat-accessibility`, `exxat-fontawesome-icons`, **`exxat-mono-ids`**, `exxat-primary-nav-secondary-panel`, `exxat-collaboration-access`, `exxat-ds-agents`) and any rules under **`exxat-ds/.cursor/rules/`** (including **`exxat-dashboard-view-charts`** for Data view charts).
|
|
49
|
+
4. Project **skills** under `.cursor/skills/` when relevant — e.g. **shadcn**, **exxat-accessibility** (WCAG / ARIA / touch / contrast), **exxat-board-cards** (kanban card shell, status badges, primitives), **exxat-list-page-view-shells** (centered view bodies, **`ListPageViewFrame`**), **exxat-centralized-list-dataset** (one **`useTableState`** row bag + shared maps across all list-hub views and **`TablePropertiesDrawer`**), **exxat-collaboration-access** (face rail + invite sheet + library access), **exxat-dedicated-search-surfaces** (landing vs results split, **`DedicatedSearch*`** templates + recents without hydration drift), **exxat-drawer-vs-dialog**, **exxat-card-vs-list-rows**, **exxat-kpi-max-four**, **`exxat-mono-ids`** (monospace system identifiers).
|
|
47
50
|
|
|
48
51
|
If two documents conflict, prefer the **more specific** rule for the file type, then **newer** team decisions captured in `AGENTS.md`.
|
|
49
52
|
|
|
@@ -164,6 +167,8 @@ Thread **`view`** and **`onViewChange`** from the **client** → **table / toolb
|
|
|
164
167
|
|
|
165
168
|
**Folder-scoped library (Question bank):** When the URL is scoped to a folder (**`scope === "folder"`** + **`folderId`** via **`lib/question-bank-nav.ts`**), the hub **`QuestionBankPageHeader`** **⋯ More** menu **MUST** include **Customize folder** and open **`QuestionBankNewFolderSheet`** from the **hub client** so the action works on **every** **`ListPageTemplate`** view tab — not only inside **`QuestionBankTable`** branches that mount their own sheet. **Pattern:** **`docs/question-bank-hub-header-pattern.md`**. **Cursor rule:** **`.cursor/rules/exxat-question-bank-hub-header.mdc`**.
|
|
166
169
|
|
|
170
|
+
**Surface elevation:** Secondary panel = **level 1** between primary sidebar (**`--sidebar`**, level 0) and page canvas (**`--background`**, level 2). **`NestedSecondaryPanelShell`** uses **`bg-[var(--secondary-panel-bg)]`** — OKLCH mix from **`--brand-tint*`** per active product (**One** indigo, **Prism** rose, **`theme-custom`** when accent differs from default). **MUST NOT** set panel to **`bg-sidebar`** or a fixed rose fill for all products. **`docs/shell-surface-elevation-pattern.md`**.
|
|
171
|
+
|
|
167
172
|
**Cursor rule (panel wiring):** **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. **Icons in panel:** **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
|
|
168
173
|
|
|
169
174
|
### 4.7 Collaboration & access (shared hubs)
|
|
@@ -509,6 +514,8 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
|
|
|
509
514
|
- **Drawer vs dialog (same route):** `docs/drawer-vs-dialog-pattern.md` — **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**
|
|
510
515
|
- **Cards vs table rows:** `docs/card-vs-rows-pattern.md` — **`.cursor/rules/exxat-card-vs-list-rows.mdc`**
|
|
511
516
|
- **KPI strip (max four tiles):** `docs/kpi-strip-max-four-pattern.md` — **`.cursor/rules/exxat-kpi-max-four.mdc`**
|
|
517
|
+
- **KPI flat band (list hubs):** `docs/kpi-flat-band-pattern.md` — **`.cursor/rules/exxat-kpi-flat-band.mdc`**
|
|
518
|
+
- **Shell surfaces (sidebar · secondary panel · page):** `docs/shell-surface-elevation-pattern.md`
|
|
512
519
|
- **KPI deltas & trend arrows:** `docs/kpi-trend-pattern.md` (`MetricItem.trendPolarity`, `KeyMetrics`, chart mini-metrics)
|
|
513
520
|
- **Global command palette (⌘K):** `docs/command-menu-pattern.md`
|
|
514
521
|
- **No toast / snackbars:** **§6.5**, root **`.cursor/rules/exxat-no-toast.mdc`**
|
|
@@ -540,6 +547,7 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
|
|
|
540
547
|
| **§4.7** — **`PageHeader` `variant="collaboration"`** + **`CollaborationAccessFlow`** / **`InviteCollaboratorsDrawer`**; empty **Add collaborator** + non-empty face rail; roster + invite from **`collaborator-access.ts`** | Extra invite beside a populated face rail; per-person roster cards; forked access enums; toast on invite |
|
|
541
548
|
| **§4.8** — **`DedicatedSearch*`** templates + composer + recents; **no** `localStorage` in **`useState`** initial paint; hub-specific **`patchSearchParams`** only | Forked `*QuestionBank*SearchLanding*` shells for another entity; hydration mismatch on recents |
|
|
542
549
|
| **Font Awesome** — Kit in **`app/layout.tsx`**; **`fa-light` / `fa-solid`** conventions; **`aria-hidden`** on decorative **`<i>`**; run **`fa:subset-audit`** when adding glyphs (**`exxat-fontawesome-icons.mdc`**) | Parallel icon libraries for the same product chrome |
|
|
550
|
+
| **System IDs** — **`font-mono tabular-nums`** on question/record keys; mono **only** the ID token in mixed subtitles (**`exxat-mono-ids.mdc`**) | Mono on names, statuses, dates, or whole subtitle lines |
|
|
543
551
|
|
|
544
552
|
---
|
|
545
553
|
|
|
@@ -572,12 +580,14 @@ Copy and complete when implementing or reviewing:
|
|
|
572
580
|
- [ ] **Accessibility:** §8 — tablist/toolbar patterns, **≥24px** targets for icon-only controls, contrast on tinted surfaces, dialog/sheet/drawer **titles**; **every icon that communicates info has a text alternative** — adjacent label (preferred) OR `aria-label` + `Tooltip` (§8.6 Case A/B/C, covers informational icons like calendar-for-date, status dots, AND icon-only buttons); **kbd inside a button uses `<Kbd variant="bare">`** (§8.7); re-run **axe** on Placements when changing views toolbar.
|
|
573
581
|
- [ ] **Coach marks (§11):** `CoachMark` + `useCoachMark`; register in **`coach-mark-registry`**; use **`enabled`** / **`dependsOnDismissedFlowId`** when a tour must wait for another flow or a specific view (e.g. **dashboard**); customize-dashboard flows use **`lib/dashboard-customize-coach-mark.ts`**.
|
|
574
582
|
- [ ] **Application sidebar (§9.1):** **`ExxatProductLogo`** for product; **`logoDevUrl`** for schools; team switcher **`DropdownMenuContent`** keeps the explicit wide school/program surface (**`!w-max`** + min/max width); expanded switcher **`h-auto min-h-12`**; no **`CollapsibleTrigger` → `SidebarMenuButton` with `tooltip` prop**; child links **popover** on icon rail; profile **`stockPortraitUrl`** + **`referrerPolicy="no-referrer"`** on **`AvatarImage`**.
|
|
575
|
-
- [ ] **Secondary panel (§4.6):** If **`NavLinkItem.secondaryPanel`** is set — **`PANELS[id]`** in **`secondary-panel.tsx`**, hub mounts **`useAutoPanel(id)`**, scope syncs to URL + **`tableState.rows`** — **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. **Question bank folder scope:** header **⋯** → **Customize folder** + **`QuestionBankNewFolderSheet`** on **`QuestionBankClient`** — **`docs/question-bank-hub-header-pattern.md`**, **`.cursor/rules/exxat-question-bank-hub-header.mdc`**.
|
|
583
|
+
- [ ] **Secondary panel (§4.6):** If **`NavLinkItem.secondaryPanel`** is set — **`PANELS[id]`** in **`secondary-panel.tsx`**, hub mounts **`useAutoPanel(id)`**, scope syncs to URL + **`tableState.rows`** — **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. Panel shell uses **`--secondary-panel-bg`** (brand OKLCH, not **`bg-sidebar`**) — **`docs/shell-surface-elevation-pattern.md`**. **Question bank folder scope:** header **⋯** → **Customize folder** + **`QuestionBankNewFolderSheet`** on **`QuestionBankClient`** — **`docs/question-bank-hub-header-pattern.md`**, **`.cursor/rules/exxat-question-bank-hub-header.mdc`**.
|
|
584
|
+
- [ ] **Flat KPI strip:** **`KeyMetrics variant="flat"`** — transparent cells, radial glow only, **`flatMetricsHairlineClass`** borders — **`docs/kpi-flat-band-pattern.md`**, **`.cursor/rules/exxat-kpi-flat-band.mdc`**.
|
|
576
585
|
- [ ] **Collaboration & access (§4.7):** Shared hubs use **`variant="collaboration"`**, empty **Add collaborator** / non-empty face rail, **⋯ → Invite people**, **`CollaborationAccessFlow`** or **`InviteCollaboratorsDrawer`**, **`lib/collaborator-access.ts`**, roster **name → email → role tags** — **`.cursor/rules/exxat-collaboration-access.mdc`**.
|
|
577
586
|
- [ ] **Dedicated search (§4.8):** Landing uses **`DedicatedSearchLandingTemplate`**; results use **`DedicatedSearchResultsHeaderChrome`** + outer **`DEDICATED_SEARCH_RESULTS_OUTER_CONTENT_CLASSNAME`**; **`DedicatedSearchUrlComposer`** + **`DedicatedSearchRecents`** with **`createDedicatedSearchRecentsController`** — **`.cursor/rules/exxat-dedicated-search-surfaces.mdc`**.
|
|
578
587
|
- [ ] **KPI trends:** **`MetricItem.trend`** matches the delta direction; **`trendPolarity`** set for “more is worse” metrics (flags, defects, overdue) — **`docs/kpi-trend-pattern.md`**, **`.cursor/rules/exxat-kpi-trends.mdc`**.
|
|
579
588
|
- [ ] **Font Awesome:** New glyphs covered by **`fa:subset-audit`** / Kit subset; decorative **`<i>`** has **`aria-hidden`**; icon-only controls follow **§8.6** — **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
|
|
589
|
+
- [ ] **System IDs:** Visible **`questionId`**, record keys, and copy-pasteable identifiers use **`font-mono tabular-nums`**; mixed lines mono-wrap **only** the ID — **`.cursor/rules/exxat-mono-ids.mdc`**, **`.cursor/skills/exxat-mono-ids/SKILL.md`**.
|
|
580
590
|
|
|
581
591
|
---
|
|
582
592
|
|
|
583
|
-
*Last updated:
|
|
593
|
+
*Last updated: KPI flat band + shell surface elevation pattern docs/rules/skills; §4.6 secondary panel OKLCH; monospace system IDs; question bank folder header; drawer vs dialog / card vs rows / KPI max-four; §4.8 dedicated search; §4.7 collaboration; §4.1 centralized dataset; §4.5 view shells; Font Awesome; §9.1 sidebar; §4.4 board cards; §6.5 no toast; §7.1 command palette; §13 checklist.*
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PlacementsClient } from "@/components/placements-client"
|
|
2
2
|
import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
|
|
3
3
|
|
|
4
4
|
export default function DataListPage() {
|
|
5
5
|
return (
|
|
6
6
|
<PrimaryPageTemplate siteHeader={{ title: "List hub" }}>
|
|
7
|
-
<
|
|
7
|
+
<PlacementsClient />
|
|
8
8
|
</PrimaryPageTemplate>
|
|
9
9
|
)
|
|
10
10
|
}
|
|
@@ -4,6 +4,7 @@ import * as React from "react"
|
|
|
4
4
|
import { AlertCircle } from "lucide-react"
|
|
5
5
|
|
|
6
6
|
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { isChunkLoadError } from "@/lib/chunk-load-error"
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Route error boundary for the signed-in app shell. Lets users retry without a full reload.
|
|
@@ -15,6 +16,8 @@ export default function AppRouteError({
|
|
|
15
16
|
error: Error & { digest?: string }
|
|
16
17
|
reset: () => void
|
|
17
18
|
}) {
|
|
19
|
+
const chunkStale = isChunkLoadError(error)
|
|
20
|
+
|
|
18
21
|
React.useEffect(() => {
|
|
19
22
|
if (process.env.NODE_ENV === "development") {
|
|
20
23
|
console.error(error)
|
|
@@ -30,14 +33,27 @@ export default function AppRouteError({
|
|
|
30
33
|
<div className="space-y-2">
|
|
31
34
|
<h1 className="text-lg font-semibold text-foreground">Something went wrong</h1>
|
|
32
35
|
<p className="max-w-md text-sm text-muted-foreground">
|
|
33
|
-
{
|
|
34
|
-
?
|
|
35
|
-
:
|
|
36
|
+
{chunkStale
|
|
37
|
+
? "The app loaded an outdated script bundle (common after a dev-server rebuild). Reload the page to fetch the latest chunks."
|
|
38
|
+
: process.env.NODE_ENV === "development"
|
|
39
|
+
? error.message
|
|
40
|
+
: "Please try again. If the problem continues, contact support."}
|
|
36
41
|
</p>
|
|
37
42
|
</div>
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
44
|
+
{chunkStale ? (
|
|
45
|
+
<Button type="button" onClick={() => window.location.reload()}>
|
|
46
|
+
Reload page
|
|
47
|
+
</Button>
|
|
48
|
+
) : null}
|
|
49
|
+
<Button
|
|
50
|
+
type="button"
|
|
51
|
+
variant={chunkStale ? "outline" : "default"}
|
|
52
|
+
onClick={() => reset()}
|
|
53
|
+
>
|
|
54
|
+
Try again
|
|
55
|
+
</Button>
|
|
56
|
+
</div>
|
|
41
57
|
</div>
|
|
42
58
|
)
|
|
43
59
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { cookies } from "next/headers"
|
|
2
2
|
import { AppSidebar } from "@/components/app-sidebar"
|
|
3
3
|
import { SidebarShell } from "@/components/sidebar-shell"
|
|
4
|
+
import {
|
|
5
|
+
SIDEBAR_STATE_COOKIE_NAME,
|
|
6
|
+
sidebarDefaultOpenFromCookie,
|
|
7
|
+
} from "@/lib/sidebar-state-cookie"
|
|
4
8
|
import { DashboardViewProvider } from "@/contexts/dashboard-view-context"
|
|
5
9
|
import { ChartVariantProvider } from "@/contexts/chart-variant-context"
|
|
6
10
|
import { AskLeoProvider, AskLeoSidebar } from "@/components/ask-leo-sidebar"
|
|
@@ -20,11 +24,14 @@ import { COMMAND_MENU_SEARCH_DATA_GROUPS } from "@/lib/command-menu-search-data"
|
|
|
20
24
|
* The SystemBanner is configured from Settings (persisted to localStorage
|
|
21
25
|
* via SystemBannerProvider) — no hardcoded copy here.
|
|
22
26
|
*/
|
|
23
|
-
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
export default async function AppLayout({ children }: { children: React.ReactNode }) {
|
|
28
|
+
const cookieStore = await cookies()
|
|
29
|
+
const sidebarDefaultOpen = sidebarDefaultOpenFromCookie(
|
|
30
|
+
cookieStore.get(SIDEBAR_STATE_COOKIE_NAME)?.value,
|
|
27
31
|
)
|
|
32
|
+
const commandMenuConfig = buildCommandMenuConfig({
|
|
33
|
+
dataGroups: COMMAND_MENU_SEARCH_DATA_GROUPS,
|
|
34
|
+
})
|
|
28
35
|
|
|
29
36
|
return (
|
|
30
37
|
<DashboardViewProvider>
|
|
@@ -33,7 +40,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|
|
33
40
|
<SystemBannerProvider>
|
|
34
41
|
<CommandMenuProvider value={commandMenuConfig}>
|
|
35
42
|
|
|
36
|
-
<SidebarShell wrapperClassName="flex min-h-svh flex-col">
|
|
43
|
+
<SidebarShell defaultOpen={sidebarDefaultOpen} wrapperClassName="flex min-h-svh flex-col">
|
|
37
44
|
{/* ⌘K command palette */}
|
|
38
45
|
<CommandMenu />
|
|
39
46
|
<SystemBannerSlot />
|
|
@@ -10,17 +10,31 @@ import {
|
|
|
10
10
|
QUESTION_BANK_LIST_PATH,
|
|
11
11
|
} from "@/lib/question-bank-nav"
|
|
12
12
|
|
|
13
|
+
/** Full-page focused flows under `/question-bank/*` that suppress the secondary panel. */
|
|
14
|
+
const QUESTION_BANK_FOCUSED_FLOW_PATHS: readonly string[] = ["/question-bank/new"]
|
|
15
|
+
|
|
16
|
+
function isQuestionBankFocusedFlow(pathname: string): boolean {
|
|
17
|
+
return QUESTION_BANK_FOCUSED_FLOW_PATHS.some(p => pathname === p || pathname.startsWith(`${p}/`))
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
21
|
* Keeps the nested secondary panel open across library navigations.
|
|
15
|
-
* **Question hub** (`/question-bank`)
|
|
22
|
+
* **Question hub** (`/question-bank`), **Search** (`/find`, `/list`), and
|
|
23
|
+
* focused authoring routes (`/new`) stay full-width — no secondary rail.
|
|
16
24
|
*/
|
|
17
25
|
export default function QuestionBankLayout({ children }: { children: React.ReactNode }) {
|
|
18
26
|
const pathname = usePathname()
|
|
19
27
|
const { openPanel, closePanel, activePanel } = useSecondaryPanel()
|
|
20
28
|
const closePanelRef = React.useRef(closePanel)
|
|
21
29
|
const openPanelRef = React.useRef(openPanel)
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
// "Latest ref" pattern — keep callbacks current without re-running the
|
|
31
|
+
// route effect below on every render. `useEffect` (no deps) updates the
|
|
32
|
+
// refs after each render; React's `refs` rule disallows direct ref writes
|
|
33
|
+
// during render so the assignment lives here instead.
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
closePanelRef.current = closePanel
|
|
36
|
+
openPanelRef.current = openPanel
|
|
37
|
+
})
|
|
24
38
|
|
|
25
39
|
/** Leaving `/question-bank/*` entirely — close nested panel without forcing primary sidebar open (⌘B / cookie). */
|
|
26
40
|
React.useEffect(() => {
|
|
@@ -37,7 +51,7 @@ export default function QuestionBankLayout({ children }: { children: React.React
|
|
|
37
51
|
const isDedicatedSearchSurface =
|
|
38
52
|
pathname === QUESTION_BANK_HUB_FIND_PATH || pathname === QUESTION_BANK_LIST_PATH
|
|
39
53
|
|
|
40
|
-
if (isDiscoveryHubRoot || isDedicatedSearchSurface) {
|
|
54
|
+
if (isDiscoveryHubRoot || isDedicatedSearchSurface || isQuestionBankFocusedFlow(pathname)) {
|
|
41
55
|
closePanelRef.current({ mainSidebar: "leave" })
|
|
42
56
|
return undefined
|
|
43
57
|
}
|
|
@@ -46,7 +60,6 @@ export default function QuestionBankLayout({ children }: { children: React.React
|
|
|
46
60
|
openPanelRef.current("question-bank")
|
|
47
61
|
}
|
|
48
62
|
return undefined
|
|
49
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- pathname + activePanel drive when to ensure panel id
|
|
50
63
|
}, [pathname, activePanel])
|
|
51
64
|
|
|
52
65
|
return children
|