@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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/question-bank/new` — full-page authoring composer.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors focused flows: `SiteHeader` back nav (← Question hub) + `PageHeader` title
|
|
5
|
+
* • `SidebarAutoCollapse` mounted in `beforeSiteHeader` so the main sidebar
|
|
6
|
+
* collapses on enter and restores to its prior state on leave
|
|
7
|
+
* • A wider max-width than the placement form (the composer carries a
|
|
8
|
+
* two-column document + metadata-rail layout)
|
|
9
|
+
*
|
|
10
|
+
* The body is `NewQuestionComposer` — see `components/new-question-composer.tsx`.
|
|
11
|
+
*
|
|
12
|
+
* Folder pre-selection: callers may pass `?folderId=<id>` so users dropped into
|
|
13
|
+
* the composer from a folder-scoped library land with that folder pre-selected
|
|
14
|
+
* in the metadata rail.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { NewQuestionComposer } from "@/components/new-question-composer"
|
|
18
|
+
import { SidebarAutoCollapse } from "@/components/sidebar-auto-collapse"
|
|
19
|
+
import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_QUESTION_BANK_FOLDERS,
|
|
22
|
+
type QuestionBankFolder,
|
|
23
|
+
} from "@/lib/mock/question-bank-folders"
|
|
24
|
+
import { generateDraftQuestionId } from "@/lib/question-bank-authoring"
|
|
25
|
+
import { newQuestionBackNav } from "@/lib/question-bank-nav"
|
|
26
|
+
|
|
27
|
+
interface NewQuestionPageProps {
|
|
28
|
+
searchParams: Promise<{ folderId?: string }>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default async function NewQuestionPage({ searchParams }: NewQuestionPageProps) {
|
|
32
|
+
const params = await searchParams
|
|
33
|
+
const folders: QuestionBankFolder[] = DEFAULT_QUESTION_BANK_FOLDERS
|
|
34
|
+
|
|
35
|
+
const requested = typeof params.folderId === "string" ? params.folderId : undefined
|
|
36
|
+
const matched = requested ? folders.find(f => f.id === requested) : undefined
|
|
37
|
+
const defaultFolderId = matched?.id
|
|
38
|
+
|
|
39
|
+
const back = newQuestionBackNav(folders, defaultFolderId)
|
|
40
|
+
const draftQuestionId = generateDraftQuestionId()
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<PrimaryPageTemplate
|
|
44
|
+
beforeSiteHeader={<SidebarAutoCollapse />}
|
|
45
|
+
siteHeader={{ back }}
|
|
46
|
+
bodyClassName="min-h-0 flex-1 overflow-y-auto overscroll-y-contain"
|
|
47
|
+
maxWidthClassName="mx-auto w-full max-w-[1100px]"
|
|
48
|
+
contentClassName="px-8 py-4 pb-16"
|
|
49
|
+
>
|
|
50
|
+
<NewQuestionComposer
|
|
51
|
+
draftQuestionId={draftQuestionId}
|
|
52
|
+
defaultFolderId={defaultFolderId}
|
|
53
|
+
backHref={back.href}
|
|
54
|
+
folders={folders}
|
|
55
|
+
/>
|
|
56
|
+
</PrimaryPageTemplate>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import "./globals.css"
|
|
6
|
+
import { isChunkLoadError } from "@/lib/chunk-load-error"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Root error boundary — catches failures outside the (app) segment layout.
|
|
10
|
+
*/
|
|
11
|
+
export default function GlobalError({
|
|
12
|
+
error,
|
|
13
|
+
reset,
|
|
14
|
+
}: {
|
|
15
|
+
error: Error & { digest?: string }
|
|
16
|
+
reset: () => void
|
|
17
|
+
}) {
|
|
18
|
+
const chunkStale = isChunkLoadError(error)
|
|
19
|
+
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
if (process.env.NODE_ENV === "development") {
|
|
22
|
+
console.error(error)
|
|
23
|
+
}
|
|
24
|
+
}, [error])
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<body className="bg-background font-sans text-foreground">
|
|
29
|
+
<div
|
|
30
|
+
className="flex min-h-svh flex-col items-center justify-center gap-4 px-4 py-12 text-center"
|
|
31
|
+
role="alert"
|
|
32
|
+
>
|
|
33
|
+
<h1 className="text-lg font-semibold">Something went wrong</h1>
|
|
34
|
+
<p className="max-w-md text-sm text-muted-foreground">
|
|
35
|
+
{chunkStale
|
|
36
|
+
? "The app loaded an outdated script bundle (common after a dev-server rebuild). Reload the page to fetch the latest chunks."
|
|
37
|
+
: process.env.NODE_ENV === "development"
|
|
38
|
+
? error.message
|
|
39
|
+
: "Please try again. If the problem continues, contact support."}
|
|
40
|
+
</p>
|
|
41
|
+
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
42
|
+
{chunkStale ? (
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground"
|
|
46
|
+
onClick={() => window.location.reload()}
|
|
47
|
+
>
|
|
48
|
+
Reload page
|
|
49
|
+
</button>
|
|
50
|
+
) : null}
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
className="inline-flex h-9 items-center justify-center rounded-md border border-input bg-background px-4 text-sm font-medium"
|
|
54
|
+
onClick={() => reset()}
|
|
55
|
+
>
|
|
56
|
+
Try again
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</body>
|
|
61
|
+
</html>
|
|
62
|
+
)
|
|
63
|
+
}
|
package/template/app/globals.css
CHANGED
|
@@ -187,10 +187,21 @@ html[data-text-size="large"] {
|
|
|
187
187
|
--leo-surface-tint-b: color-mix(in oklch, var(--brand-color) 8%, var(--background));
|
|
188
188
|
--leo-surface-gradient: linear-gradient(180deg, var(--leo-surface-tint-a) 0%, var(--leo-surface-tint-b) 100%);
|
|
189
189
|
|
|
190
|
-
/* KeyMetrics `variant="flat"` —
|
|
191
|
-
--key-metrics-flat-
|
|
192
|
-
--key-metrics-flat-
|
|
193
|
-
--key-metrics-flat-
|
|
190
|
+
/* KeyMetrics `variant="flat"` — no band surface; bottom brand glow only (OKLCH). */
|
|
191
|
+
--key-metrics-flat-cell-bg: transparent;
|
|
192
|
+
--key-metrics-flat-divider: color-mix(in oklch, var(--sidebar-border) 55%, transparent);
|
|
193
|
+
--key-metrics-flat-band-radial: radial-gradient(
|
|
194
|
+
ellipse 120% 68% at 50% 100%,
|
|
195
|
+
color-mix(in oklch, var(--brand-color) 20%, transparent) 0%,
|
|
196
|
+
color-mix(in oklch, var(--brand-color) 8%, transparent) 42%,
|
|
197
|
+
transparent 72%
|
|
198
|
+
);
|
|
199
|
+
--key-metrics-flat-band-shadow: none;
|
|
200
|
+
--key-metrics-card-glow-radial: radial-gradient(
|
|
201
|
+
ellipse 110% 90% at 50% 100%,
|
|
202
|
+
color-mix(in oklch, var(--brand-color) 18%, transparent) 0%,
|
|
203
|
+
transparent 65%
|
|
204
|
+
);
|
|
194
205
|
|
|
195
206
|
/* ── Surfaces ────────────────────────────────────────────────── */
|
|
196
207
|
--background: oklch(1 0 0);
|
|
@@ -270,8 +281,8 @@ html[data-text-size="large"] {
|
|
|
270
281
|
--sidebar-ring: oklch(0.25 0 0);
|
|
271
282
|
/* Nav section titles — ≥4.5:1 vs --sidebar (not vs page white) */
|
|
272
283
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 58%, var(--sidebar));
|
|
273
|
-
/* Nested secondary rail —
|
|
274
|
-
--secondary-panel-bg: color-mix(in oklch, var(--
|
|
284
|
+
/* Nested secondary rail — elevation 1: brand wash, lighter than `--sidebar` / `--brand-tint`. */
|
|
285
|
+
--secondary-panel-bg: color-mix(in oklch, var(--background) 40%, var(--brand-tint-light) 60%);
|
|
275
286
|
|
|
276
287
|
/* Browser UI (meta theme-color) — aligned with --brand-tint */
|
|
277
288
|
--theme-color-chrome: #f3f2f8;
|
|
@@ -379,9 +390,21 @@ html[data-text-size="large"] {
|
|
|
379
390
|
--destructive: oklch(0.65 0.20 25); /* brighter for dark bg */
|
|
380
391
|
--destructive-foreground: oklch(0.10 0 0);
|
|
381
392
|
|
|
382
|
-
|
|
383
|
-
--key-metrics-flat-
|
|
384
|
-
--key-metrics-flat-
|
|
393
|
+
/* KeyMetrics flat band — no surface; bottom brand glow only (OKLCH). */
|
|
394
|
+
--key-metrics-flat-cell-bg: transparent;
|
|
395
|
+
--key-metrics-flat-divider: color-mix(in oklch, var(--sidebar-border) 55%, transparent);
|
|
396
|
+
--key-metrics-flat-band-radial: radial-gradient(
|
|
397
|
+
ellipse 120% 68% at 50% 100%,
|
|
398
|
+
color-mix(in oklch, var(--brand-color) 26%, transparent) 0%,
|
|
399
|
+
color-mix(in oklch, var(--brand-color) 10%, transparent) 42%,
|
|
400
|
+
transparent 72%
|
|
401
|
+
);
|
|
402
|
+
--key-metrics-flat-band-shadow: none;
|
|
403
|
+
--key-metrics-card-glow-radial: radial-gradient(
|
|
404
|
+
ellipse 110% 90% at 50% 100%,
|
|
405
|
+
color-mix(in oklch, var(--brand-color) 22%, transparent) 0%,
|
|
406
|
+
transparent 62%
|
|
407
|
+
);
|
|
385
408
|
|
|
386
409
|
/* Borders — visible but not washed out on dark surfaces */
|
|
387
410
|
--border: oklch(0.38 0.008 270);
|
|
@@ -431,8 +454,8 @@ html[data-text-size="large"] {
|
|
|
431
454
|
--sidebar-border: oklch(0.38 0.010 270);
|
|
432
455
|
--sidebar-ring: oklch(0.85 0 0);
|
|
433
456
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 48%, var(--sidebar));
|
|
434
|
-
/* Nested secondary rail —
|
|
435
|
-
--secondary-panel-bg: color-mix(in oklch, var(--
|
|
457
|
+
/* Nested secondary rail — elevation 1: brand stack, lifted toward `--card` / page. */
|
|
458
|
+
--secondary-panel-bg: color-mix(in oklch, var(--card) 32%, var(--brand-tint) 68%);
|
|
436
459
|
--theme-color-chrome: #2f2d36;
|
|
437
460
|
|
|
438
461
|
/* Lifted scrim on dark — white-tinted veil, not heavy black */
|
|
@@ -493,6 +516,7 @@ html[data-text-size="large"] {
|
|
|
493
516
|
--secondary: oklch(0.31 0.04 286.1);
|
|
494
517
|
--muted: oklch(0.31 0.04 286.1);
|
|
495
518
|
--accent: oklch(0.33 0.06 286.1);
|
|
519
|
+
--brand-tint-light: oklch(0.30 0.014 286.1);
|
|
496
520
|
}
|
|
497
521
|
|
|
498
522
|
/* ==========================================================================
|
|
@@ -501,9 +525,14 @@ html[data-text-size="large"] {
|
|
|
501
525
|
========================================================================== */
|
|
502
526
|
.theme-prism,
|
|
503
527
|
.theme-rose {
|
|
504
|
-
--brand-
|
|
505
|
-
--brand-
|
|
506
|
-
--
|
|
528
|
+
--brand-tint: oklch(0.97 0.02 343);
|
|
529
|
+
--brand-tint-light: oklch(0.992 0.01 343);
|
|
530
|
+
--brand-tint-subtle: oklch(0.93 0.028 343);
|
|
531
|
+
--brand-color: oklch(0.57 0.24 342); /* Prism rose */
|
|
532
|
+
--brand-color-light: oklch(0.78 0.14 342);
|
|
533
|
+
--brand-color-dark: oklch(0.42 0.24 342);
|
|
534
|
+
--brand-color-deep: oklch(0.32 0.20 342);
|
|
535
|
+
--ring: var(--brand-color-dark);
|
|
507
536
|
}
|
|
508
537
|
|
|
509
538
|
.theme-prism:not(.dark),
|
|
@@ -531,6 +560,100 @@ html[data-text-size="large"] {
|
|
|
531
560
|
--muted: oklch(0.31 0.04 342);
|
|
532
561
|
--accent: oklch(0.33 0.06 342);
|
|
533
562
|
--theme-color-chrome: #2a2428;
|
|
563
|
+
--brand-tint-light: oklch(0.30 0.014 342);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/* ==========================================================================
|
|
567
|
+
Theme: Exxat Assessment · Green · hue 159.88
|
|
568
|
+
Usage: <html class="theme-assessment">
|
|
569
|
+
========================================================================== */
|
|
570
|
+
.theme-assessment {
|
|
571
|
+
--brand-tint: oklch(0.965 0.018 159.88);
|
|
572
|
+
--brand-tint-light: oklch(0.992 0.008 159.88);
|
|
573
|
+
--brand-tint-subtle: oklch(0.93 0.028 159.88);
|
|
574
|
+
--brand-color: oklch(0.7 0.0913 159.88);
|
|
575
|
+
--brand-color-light: oklch(0.82 0.072 159.88);
|
|
576
|
+
--brand-color-dark: oklch(0.48 0.0913 159.88);
|
|
577
|
+
--brand-color-deep: oklch(0.34 0.075 159.88);
|
|
578
|
+
--ring: var(--brand-color-dark);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.theme-assessment:not(.dark) {
|
|
582
|
+
--sidebar: var(--brand-tint);
|
|
583
|
+
--sidebar-accent: oklch(0.945 0.026 159.88);
|
|
584
|
+
--sidebar-border: oklch(0.90 0.026 159.88);
|
|
585
|
+
--secondary: oklch(0.95 0.012 159.88);
|
|
586
|
+
--accent: oklch(0.925 0.016 159.88);
|
|
587
|
+
--muted: oklch(0.945 0.008 159.88);
|
|
588
|
+
--theme-color-chrome: #f1faf5;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.theme-assessment.dark,
|
|
592
|
+
.dark.theme-assessment {
|
|
593
|
+
--ring: var(--brand-color);
|
|
594
|
+
--background: oklch(0.20 0.008 159.88);
|
|
595
|
+
--sidebar: oklch(0.245 0.015 159.88);
|
|
596
|
+
--sidebar-accent: oklch(0.30 0.012 159.88);
|
|
597
|
+
--sidebar-border: oklch(0.38 0.010 159.88);
|
|
598
|
+
--secondary: oklch(0.31 0.04 159.88);
|
|
599
|
+
--muted: oklch(0.31 0.04 159.88);
|
|
600
|
+
--accent: oklch(0.33 0.06 159.88);
|
|
601
|
+
--theme-color-chrome: #242a27;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/* ==========================================================================
|
|
605
|
+
Theme: Custom product · user-selected hue
|
|
606
|
+
Usage: <html class="theme-custom" style="--custom-product-brand-color: …">
|
|
607
|
+
========================================================================== */
|
|
608
|
+
/*
|
|
609
|
+
* Tint chroma is **derived from the source's chroma** so neighbouring hues
|
|
610
|
+
* (e.g. Blue h=252 vs Indigo h=280 vs Purple h=286) read as distinct pale
|
|
611
|
+
* tints instead of collapsing to the same near-white. The earlier formula
|
|
612
|
+
* pinned chroma to a fixed `0.018`, which is below the hue-discrimination
|
|
613
|
+
* threshold at L≈0.96 for closely-spaced hues — that's why Blue / Indigo /
|
|
614
|
+
* Purple looked identical in the sidebar.
|
|
615
|
+
*
|
|
616
|
+
* `max(<floor>, calc(c * <fraction>))` rules:
|
|
617
|
+
* - vibrant brand colours (c ≈ 0.10–0.23) get a meaningfully tinted
|
|
618
|
+
* sidebar / accent / muted scale → product chrome visibly changes per
|
|
619
|
+
* pick
|
|
620
|
+
* - low-chroma custom-hex picks (greys) fall back to the floor so they
|
|
621
|
+
* don't render as pure white
|
|
622
|
+
* - lightness + hue stay pinned to the original mock-up values, so the
|
|
623
|
+
* overall "very pale background" feel is unchanged
|
|
624
|
+
*/
|
|
625
|
+
.theme-custom {
|
|
626
|
+
--brand-tint: oklch(from var(--custom-product-brand-color) 0.965 max(0.018, calc(c * 0.20)) h);
|
|
627
|
+
--brand-tint-light: oklch(from var(--custom-product-brand-color) 0.992 max(0.008, calc(c * 0.08)) h);
|
|
628
|
+
--brand-tint-subtle: oklch(from var(--custom-product-brand-color) 0.93 max(0.028, calc(c * 0.30)) h);
|
|
629
|
+
--brand-color: var(--custom-product-brand-color);
|
|
630
|
+
--brand-color-light: oklch(from var(--custom-product-brand-color) 0.82 c h);
|
|
631
|
+
--brand-color-dark: oklch(from var(--custom-product-brand-color) 0.48 c h);
|
|
632
|
+
--brand-color-deep: oklch(from var(--custom-product-brand-color) 0.34 c h);
|
|
633
|
+
--ring: var(--brand-color-dark);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.theme-custom:not(.dark) {
|
|
637
|
+
--sidebar: var(--brand-tint);
|
|
638
|
+
--sidebar-accent: oklch(from var(--custom-product-brand-color) 0.945 max(0.026, calc(c * 0.28)) h);
|
|
639
|
+
--sidebar-border: oklch(from var(--custom-product-brand-color) 0.90 max(0.026, calc(c * 0.28)) h);
|
|
640
|
+
--secondary: oklch(from var(--custom-product-brand-color) 0.95 max(0.012, calc(c * 0.14)) h);
|
|
641
|
+
--accent: oklch(from var(--custom-product-brand-color) 0.925 max(0.016, calc(c * 0.18)) h);
|
|
642
|
+
--muted: oklch(from var(--custom-product-brand-color) 0.945 max(0.008, calc(c * 0.10)) h);
|
|
643
|
+
--theme-color-chrome: color-mix(in oklch, var(--custom-product-brand-color) 10%, white);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.theme-custom.dark,
|
|
647
|
+
.dark.theme-custom {
|
|
648
|
+
--ring: var(--brand-color);
|
|
649
|
+
--background: oklch(from var(--custom-product-brand-color) 0.20 max(0.008, calc(c * 0.10)) h);
|
|
650
|
+
--sidebar: oklch(from var(--custom-product-brand-color) 0.245 max(0.015, calc(c * 0.18)) h);
|
|
651
|
+
--sidebar-accent: oklch(from var(--custom-product-brand-color) 0.30 max(0.012, calc(c * 0.14)) h);
|
|
652
|
+
--sidebar-border: oklch(from var(--custom-product-brand-color) 0.38 max(0.010, calc(c * 0.12)) h);
|
|
653
|
+
--secondary: oklch(from var(--custom-product-brand-color) 0.31 max(0.04, calc(c * 0.40)) h);
|
|
654
|
+
--muted: oklch(from var(--custom-product-brand-color) 0.31 max(0.04, calc(c * 0.40)) h);
|
|
655
|
+
--accent: oklch(from var(--custom-product-brand-color) 0.33 max(0.06, calc(c * 0.50)) h);
|
|
656
|
+
--theme-color-chrome: color-mix(in oklch, var(--custom-product-brand-color) 12%, black);
|
|
534
657
|
}
|
|
535
658
|
|
|
536
659
|
/* ==========================================================================
|
|
@@ -1804,6 +1927,20 @@ html:is([data-contrast="high"], [data-contrast="windows"]) [data-slot="coach-mar
|
|
|
1804
1927
|
100% { opacity: 1; transform: scale(1) rotate(0); }
|
|
1805
1928
|
}
|
|
1806
1929
|
|
|
1930
|
+
/* Radix Collapsible / Accordion — slide the children open/closed using the
|
|
1931
|
+
`--radix-collapsible-content-height` CSS var that Radix sets on the
|
|
1932
|
+
content element. Mirrors the shadcn accordion pattern (Tailwind v3
|
|
1933
|
+
"accordion-down" / "accordion-up"); we keep our own name so it can be
|
|
1934
|
+
reused by both Collapsible and Accordion primitives. */
|
|
1935
|
+
@keyframes collapsible-down {
|
|
1936
|
+
from { height: 0; }
|
|
1937
|
+
to { height: var(--radix-collapsible-content-height); }
|
|
1938
|
+
}
|
|
1939
|
+
@keyframes collapsible-up {
|
|
1940
|
+
from { height: var(--radix-collapsible-content-height); }
|
|
1941
|
+
to { height: 0; }
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1807
1944
|
/* Ask Leo suggestion chips — staggered fade-in + lift on first render. */
|
|
1808
1945
|
@keyframes leo-chip-in {
|
|
1809
1946
|
0% { opacity: 0; transform: translateY(8px) scale(0.96); }
|
package/template/app/layout.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import "./globals.css"
|
|
|
6
6
|
import { ThemeProvider } from "@/components/theme-provider"
|
|
7
7
|
import { TooltipProvider } from "@/components/ui/tooltip"
|
|
8
8
|
import { ProductProvider } from "@/contexts/product-context"
|
|
9
|
+
import { DevChunkLoadRecovery } from "@/components/dev-chunk-load-recovery"
|
|
9
10
|
import { ThemeColorSync } from "@/components/theme-color-sync"
|
|
10
11
|
import { cn } from "@/lib/utils"
|
|
11
12
|
|
|
@@ -59,14 +60,51 @@ export default function RootLayout({
|
|
|
59
60
|
<head>
|
|
60
61
|
{/* Default until ThemeColorSync hydrates (brand + mode override client-side) */}
|
|
61
62
|
<meta name="theme-color" content="#f6f3ff" />
|
|
62
|
-
{/*
|
|
63
|
-
|
|
63
|
+
{/*
|
|
64
|
+
* Adobe Fonts — preconnect + preload Ivy Presto · Kit ID: wuk5wqn.
|
|
65
|
+
*
|
|
66
|
+
* Trust model & Subresource Integrity (SRI):
|
|
67
|
+
* - Adobe Typekit serves *dynamically subsetted* CSS that hashes
|
|
68
|
+
* differently per response, so SRI cannot be applied — the kit URL
|
|
69
|
+
* is locked to a single Adobe-owned origin instead.
|
|
70
|
+
* - `crossOrigin=""` (anonymous CORS) is set on both the preconnect
|
|
71
|
+
* and the stylesheet so the browser uses one CORS-correct
|
|
72
|
+
* connection for preconnect + preload + fetch, and so styles
|
|
73
|
+
* cannot read first-party cookies or credentials.
|
|
74
|
+
* - The same origins are pinned in `style-src` / `font-src` /
|
|
75
|
+
* `connect-src` of the Content-Security-Policy declared in
|
|
76
|
+
* `next.config.mjs`, which is what actually prevents arbitrary
|
|
77
|
+
* third-party CSS/fonts from loading.
|
|
78
|
+
*/}
|
|
79
|
+
<link rel="preconnect" href="https://use.typekit.net" crossOrigin="" />
|
|
64
80
|
<link rel="preconnect" href="https://p.typekit.net" crossOrigin="" />
|
|
65
|
-
<link
|
|
66
|
-
|
|
81
|
+
<link
|
|
82
|
+
rel="preload"
|
|
83
|
+
href="https://use.typekit.net/wuk5wqn.css"
|
|
84
|
+
as="style"
|
|
85
|
+
crossOrigin=""
|
|
86
|
+
/>
|
|
87
|
+
<link
|
|
88
|
+
rel="stylesheet"
|
|
89
|
+
href="https://use.typekit.net/wuk5wqn.css"
|
|
90
|
+
crossOrigin=""
|
|
91
|
+
/>
|
|
67
92
|
</head>
|
|
68
93
|
<body className="bg-sidebar text-foreground font-sans">
|
|
69
|
-
|
|
94
|
+
<DevChunkLoadRecovery />
|
|
95
|
+
{/*
|
|
96
|
+
* Font Awesome Pro Kit — subset via fontawesome-subset.manifest.json +
|
|
97
|
+
* fontawesome.com/kits (Icon Selection).
|
|
98
|
+
*
|
|
99
|
+
* Trust model & SRI: the kit loader URL is content-versioned by
|
|
100
|
+
* fontawesome.com and rotates whenever the subset changes, so SRI
|
|
101
|
+
* cannot be pinned. We instead:
|
|
102
|
+
* - Restrict the script to kit.fontawesome.com (and font/data fetch
|
|
103
|
+
* to ka-f.fontawesome.com) via CSP `script-src` / `font-src` /
|
|
104
|
+
* `connect-src` in `next.config.mjs`.
|
|
105
|
+
* - Load with `crossOrigin="anonymous"` so the script runs with
|
|
106
|
+
* CORS semantics and no credentials.
|
|
107
|
+
*/}
|
|
70
108
|
<Script
|
|
71
109
|
src="https://kit.fontawesome.com/d9bd5774e0.js"
|
|
72
110
|
crossOrigin="anonymous"
|
|
@@ -71,6 +71,7 @@ import { NavUser } from "@/components/nav-user"
|
|
|
71
71
|
import { useSecondaryPanel } from "@/components/secondary-panel"
|
|
72
72
|
import { ExxatProductLogo, ExxatProductMark } from "@/components/exxat-product-logo"
|
|
73
73
|
import { motionHeaderEnter } from "@/lib/motion-ui"
|
|
74
|
+
import { customProductBrandConfig, productBrandLabel } from "@/lib/product-brand"
|
|
74
75
|
import {
|
|
75
76
|
NAV_DOCUMENTS,
|
|
76
77
|
NAV_DOCUMENTS_LABEL,
|
|
@@ -86,8 +87,6 @@ import {
|
|
|
86
87
|
type NavSchool,
|
|
87
88
|
type NavProgram,
|
|
88
89
|
} from "@/lib/mock/navigation"
|
|
89
|
-
import { QUESTION_BANK_ENTRY_PATH } from "@/lib/question-bank-nav"
|
|
90
|
-
|
|
91
90
|
/** Path segment of a nav URL (strip `#fragment` for matching). */
|
|
92
91
|
function navUrlPath(url: string): string {
|
|
93
92
|
if (!url || url === "#") return ""
|
|
@@ -190,11 +189,16 @@ function isCollapsibleChildActive(
|
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
/**
|
|
193
|
-
* “Selected” styling on a collapsible **parent** row
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
192
|
+
* “Selected” styling on a collapsible **parent** row in the **expanded** sidebar.
|
|
193
|
+
*
|
|
194
|
+
* Rule: when any descendant child is the current destination, the parent stays
|
|
195
|
+
* visually NEUTRAL — the active child carries `data-active` on its own. The
|
|
196
|
+
* parent is only highlighted when no child matches but the parent URL still
|
|
197
|
+
* matches (edge case: route that isn't represented in the sub-list).
|
|
198
|
+
*
|
|
199
|
+
* Note: this is for the expanded view only. The collapsed icon rail uses
|
|
200
|
+
* `iconRailActive = isAnyChildActive` because the parent icon is the only
|
|
201
|
+
* visible affordance there (see `CollapsibleNavItem`).
|
|
198
202
|
*/
|
|
199
203
|
function isCollapsibleParentMenuButtonActive(
|
|
200
204
|
pathname: string,
|
|
@@ -204,15 +208,11 @@ function isCollapsibleParentMenuButtonActive(
|
|
|
204
208
|
const children = item.children
|
|
205
209
|
if (!children?.length) return isNavActive(pathname, item.url, locationHash)
|
|
206
210
|
|
|
207
|
-
const
|
|
211
|
+
const anyChildActive = children.some(c =>
|
|
208
212
|
isCollapsibleChildActive(pathname, item, c, locationHash),
|
|
209
213
|
)
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
if (activeChildren.length !== 1) return false
|
|
214
|
-
const [child] = activeChildren
|
|
215
|
-
return navUrlPath(child.url) === navUrlPath(item.url)
|
|
214
|
+
if (anyChildActive) return false
|
|
215
|
+
return isNavActive(pathname, item.url, locationHash)
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/** Accessible suffix for sidebar badges (badge is rendered outside the link node). */
|
|
@@ -296,8 +296,17 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
296
296
|
const [flyoutOpen, setFlyoutOpen] = React.useState(false)
|
|
297
297
|
const flyoutTitleId = React.useId()
|
|
298
298
|
const iconRailCollapsed = state === "collapsed" && !isMobile
|
|
299
|
+
// In the icon rail the parent icon is the ONLY visible thing for this item
|
|
300
|
+
// (no sub-list, no labels) — so it must reflect "I'm somewhere inside this
|
|
301
|
+
// section" by lighting up on any descendant route (e.g. `/question-bank/library`),
|
|
302
|
+
// not only on the parent URL itself. In the expanded view we keep the
|
|
303
|
+
// parent neutral and let the active child row carry `data-active` (see
|
|
304
|
+
// `isCollapsibleParentMenuButtonActive`).
|
|
305
|
+
const iconRailActive = isAnyChildActive
|
|
299
306
|
const triggerIcon =
|
|
300
|
-
|
|
307
|
+
(iconRailCollapsed ? iconRailActive : parentMenuButtonActive) && item.iconActive
|
|
308
|
+
? item.iconActive
|
|
309
|
+
: item.icon
|
|
301
310
|
|
|
302
311
|
React.useEffect(() => {
|
|
303
312
|
setOpen(isAnyChildActive)
|
|
@@ -323,14 +332,16 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
323
332
|
<TooltipTrigger asChild>
|
|
324
333
|
<PopoverTrigger asChild>
|
|
325
334
|
<SidebarMenuButton
|
|
326
|
-
isActive={
|
|
335
|
+
isActive={iconRailActive}
|
|
336
|
+
aria-current={iconRailActive ? "page" : undefined}
|
|
327
337
|
aria-haspopup="dialog"
|
|
328
338
|
aria-label={`${item.title} — open subpages`}
|
|
329
339
|
>
|
|
330
340
|
<span
|
|
341
|
+
key={iconRailActive ? "active" : "idle"}
|
|
331
342
|
className={cn(
|
|
332
343
|
"size-4 shrink-0 flex items-center justify-center",
|
|
333
|
-
|
|
344
|
+
iconRailActive &&
|
|
334
345
|
"[animation:sidebar-icon-pop_380ms_cubic-bezier(0.34,1.56,0.64,1)_both]",
|
|
335
346
|
)}
|
|
336
347
|
aria-hidden="true"
|
|
@@ -391,7 +402,10 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
391
402
|
}}
|
|
392
403
|
asChild
|
|
393
404
|
>
|
|
394
|
-
|
|
405
|
+
{/* `group/collapsible` lets descendant utilities react to the
|
|
406
|
+
Radix `data-state` (e.g. chevron rotate, content slide). Radix's
|
|
407
|
+
asChild merges the data-state onto this `<SidebarMenuItem>`. */}
|
|
408
|
+
<SidebarMenuItem className="group/collapsible">
|
|
395
409
|
<Tooltip>
|
|
396
410
|
<TooltipTrigger asChild>
|
|
397
411
|
<CollapsibleTrigger asChild>
|
|
@@ -409,7 +423,7 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
409
423
|
</span>
|
|
410
424
|
<span>{item.title}</span>
|
|
411
425
|
<i
|
|
412
|
-
className="fa-light fa-chevron-right ml-auto text-xs text-current transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
|
426
|
+
className="fa-light fa-chevron-right ml-auto text-xs text-current transition-transform duration-200 ease-out group-data-[state=open]/collapsible:rotate-90 motion-reduce:transition-none"
|
|
413
427
|
aria-hidden="true"
|
|
414
428
|
/>
|
|
415
429
|
</SidebarMenuButton>
|
|
@@ -419,7 +433,11 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
419
433
|
{item.title}
|
|
420
434
|
</TooltipContent>
|
|
421
435
|
</Tooltip>
|
|
422
|
-
|
|
436
|
+
{/* Slide the children open/closed using Radix's
|
|
437
|
+
`--radix-collapsible-content-height` CSS variable. `overflow-hidden`
|
|
438
|
+
is required so the height clip is visible during the animation.
|
|
439
|
+
Keyframes defined in `app/globals.css` (`collapsible-down/up`). */}
|
|
440
|
+
<CollapsibleContent className="overflow-hidden group-data-[collapsible=icon]:hidden data-[state=open]:[animation:collapsible-down_200ms_ease-out] data-[state=closed]:[animation:collapsible-up_200ms_ease-out] motion-reduce:animate-none">
|
|
423
441
|
<SidebarMenuSub>
|
|
424
442
|
{item.children.map(child => {
|
|
425
443
|
const childActive = isCollapsibleChildActive(pathname, item, child, locationHash)
|
|
@@ -444,7 +462,7 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
444
462
|
}
|
|
445
463
|
|
|
446
464
|
function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: string }) {
|
|
447
|
-
const { openPanel
|
|
465
|
+
const { openPanel } = useSecondaryPanel()
|
|
448
466
|
const locationHash = useLocationHash()
|
|
449
467
|
return (
|
|
450
468
|
<>
|
|
@@ -470,6 +488,12 @@ function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: str
|
|
|
470
488
|
: undefined
|
|
471
489
|
}
|
|
472
490
|
onClick={e => {
|
|
491
|
+
// Reopen the panel when the user clicks a panel-driving row
|
|
492
|
+
// while ALREADY on its route — Next.js `<Link>` does not
|
|
493
|
+
// navigate to the same URL, so without this the panel could
|
|
494
|
+
// stay closed (e.g. after the user collapsed it manually).
|
|
495
|
+
// On first click (different route), default navigation runs
|
|
496
|
+
// and the route's `useAutoPanel` opens the panel itself.
|
|
473
497
|
if (
|
|
474
498
|
item.secondaryPanel &&
|
|
475
499
|
itemPath &&
|
|
@@ -477,11 +501,7 @@ function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: str
|
|
|
477
501
|
!item.url.includes("#")
|
|
478
502
|
) {
|
|
479
503
|
e.preventDefault()
|
|
480
|
-
|
|
481
|
-
closePanel({ mainSidebar: "leave" })
|
|
482
|
-
} else {
|
|
483
|
-
openPanel(item.secondaryPanel)
|
|
484
|
-
}
|
|
504
|
+
openPanel(item.secondaryPanel)
|
|
485
505
|
}
|
|
486
506
|
}}
|
|
487
507
|
>
|
|
@@ -782,14 +802,26 @@ function TeamSwitcher() {
|
|
|
782
802
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
783
803
|
|
|
784
804
|
const PRODUCTS: { id: Product; label: string }[] = [
|
|
785
|
-
{ id: "exxat-one",
|
|
786
|
-
{ id: "exxat-prism",
|
|
805
|
+
{ id: "exxat-one", label: "Exxat One" },
|
|
806
|
+
{ id: "exxat-prism", label: "Exxat Prism" },
|
|
807
|
+
{ id: "exxat-assessment", label: "Exxat Assessment" },
|
|
808
|
+
{ id: "exxat-custom", label: "Custom product" },
|
|
787
809
|
]
|
|
788
810
|
|
|
789
811
|
function ProductLogoButton() {
|
|
790
|
-
const { product, setProduct } = useProduct()
|
|
812
|
+
const { product, setProduct, customProductBrand, hiddenProductIds } = useProduct()
|
|
791
813
|
const { state, isMobile } = useSidebar()
|
|
792
|
-
const
|
|
814
|
+
const products = React.useMemo(
|
|
815
|
+
() => PRODUCTS.flatMap(p => {
|
|
816
|
+
if (hiddenProductIds.includes(p.id)) return []
|
|
817
|
+
if (p.id !== "exxat-custom") return [p]
|
|
818
|
+
return customProductBrand
|
|
819
|
+
? [{ ...p, label: productBrandLabel(customProductBrandConfig(customProductBrand)) }]
|
|
820
|
+
: []
|
|
821
|
+
}),
|
|
822
|
+
[customProductBrand, hiddenProductIds],
|
|
823
|
+
)
|
|
824
|
+
const current = products.find(p => p.id === product) ?? products[0]
|
|
793
825
|
const iconRail = state === "collapsed" && !isMobile
|
|
794
826
|
const expandedOrMobile = state === "expanded" || isMobile
|
|
795
827
|
|
|
@@ -812,6 +844,8 @@ function ProductLogoButton() {
|
|
|
812
844
|
suppressHydrationWarning
|
|
813
845
|
>
|
|
814
846
|
{iconRail ? (
|
|
847
|
+
// Match the school selector footprint in the icon rail (32px frame,
|
|
848
|
+
// 28px mark — same visual weight as the avatar with inset padding).
|
|
815
849
|
<span className="flex size-8 shrink-0 items-center justify-center">
|
|
816
850
|
<ExxatProductMark product={current.id} className="size-7" />
|
|
817
851
|
</span>
|
|
@@ -824,7 +858,7 @@ function ProductLogoButton() {
|
|
|
824
858
|
<ExxatProductLogo
|
|
825
859
|
product={current.id}
|
|
826
860
|
variant="mutedSuffix"
|
|
827
|
-
className="
|
|
861
|
+
className="w-auto max-w-[min(100%,280px)] object-left object-contain"
|
|
828
862
|
/>
|
|
829
863
|
</span>
|
|
830
864
|
<span
|
|
@@ -851,7 +885,7 @@ function ProductLogoButton() {
|
|
|
851
885
|
Switch product
|
|
852
886
|
</DropdownMenuLabel>
|
|
853
887
|
<DropdownMenuSeparator />
|
|
854
|
-
{
|
|
888
|
+
{products.map(p => (
|
|
855
889
|
<DropdownMenuItem
|
|
856
890
|
key={p.id}
|
|
857
891
|
onClick={() => setProduct(p.id)}
|
|
@@ -861,7 +895,7 @@ function ProductLogoButton() {
|
|
|
861
895
|
<ExxatProductLogo
|
|
862
896
|
product={p.id}
|
|
863
897
|
variant="mutedSuffix"
|
|
864
|
-
className="
|
|
898
|
+
className="w-auto shrink-0 max-w-[min(100%,260px)]"
|
|
865
899
|
/>
|
|
866
900
|
{p.id === product && (
|
|
867
901
|
<i className="fa-solid fa-check ml-auto text-brand text-xs" aria-hidden="true" />
|
|
@@ -970,6 +1004,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
|
970
1004
|
<SidebarGroupLabel
|
|
971
1005
|
id="sidebar-documents-heading"
|
|
972
1006
|
className="text-xs font-medium uppercase tracking-wide px-2 text-sidebar-section-label"
|
|
1007
|
+
suppressHydrationWarning
|
|
973
1008
|
>
|
|
974
1009
|
{NAV_DOCUMENTS_LABEL}
|
|
975
1010
|
</SidebarGroupLabel>
|
|
@@ -188,8 +188,6 @@ export function AskLeoSidebar() {
|
|
|
188
188
|
const routeContext = React.useMemo(() => getAskLeoRouteContext(pathname), [pathname])
|
|
189
189
|
const isThinking = threadMessages.some((m) => m.pending)
|
|
190
190
|
|
|
191
|
-
const pageTitle = pageContext?.title ?? routeContext.title
|
|
192
|
-
const pageDescription = pageContext?.description ?? routeContext.description
|
|
193
191
|
const suggestions =
|
|
194
192
|
pageContext?.suggestions && pageContext.suggestions.length > 0
|
|
195
193
|
? pageContext.suggestions
|