@exxatdesignux/ui 0.2.15 → 0.2.16
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 +12 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -1
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +3 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +3 -1
- package/consumer-extras/patterns/collaboration-access-pattern.md +2 -0
- package/package.json +1 -1
- package/src/globals.css +21 -2
- package/src/theme.css +4 -2
- package/template/AGENTS.md +6 -4
- package/template/app/(app)/question-bank/layout.tsx +11 -4
- package/template/app/globals.css +29 -2
- package/template/components/app-sidebar.tsx +89 -41
- package/template/components/ask-leo-sidebar.tsx +1 -2
- package/template/components/data-views/finder-panel-view.tsx +2 -2
- package/template/components/data-views/index.ts +19 -0
- package/template/components/data-views/list-page-split-details-placeholder.tsx +3 -3
- package/template/components/data-views/list-page-split-hub-tokens.ts +1 -1
- package/template/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/template/components/data-views/outline-tree-menu.tsx +157 -0
- package/template/components/data-views/question-bank-folder-tree-branch.tsx +210 -0
- package/template/components/exxat-product-logo.tsx +11 -72
- package/template/components/folder-details-shell.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +88 -80
- package/template/components/key-metrics.tsx +50 -13
- package/template/components/page-header.tsx +19 -10
- package/template/components/product-switcher.tsx +1 -4
- package/template/components/question-bank-client.tsx +111 -69
- package/template/components/question-bank-page-header.tsx +18 -2
- package/template/components/question-bank-secondary-nav.tsx +12 -225
- package/template/components/secondary-panel.tsx +1 -1
- package/template/components/site-header.tsx +21 -2
- package/template/components/templates/dedicated-search-landing-template.tsx +86 -20
- package/template/components/templates/list-page.tsx +1 -3
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -4
- package/template/docs/collaboration-access-pattern.md +2 -0
- package/template/docs/question-bank-hub-header-pattern.md +25 -0
- package/template/hooks/use-secondary-panel-hub-nav.ts +17 -1
- package/template/lib/mock/navigation.tsx +30 -1
- package/template/lib/question-bank-nav.ts +26 -0
- package/template/package.json +3 -3
- package/template/components/command-menu-01.tsx +0 -133
- package/template/components/command-menu-02.tsx +0 -386
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,18 @@ After the user bumps `@exxatdesignux/ui`, do this in order:
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
+
## [0.2.16] - 2026-05-15
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Tokens**: `globals.css` / `theme.css` refinements and starter **`template/`** parity with the web app (layout, Question bank hub chrome, navigation).
|
|
23
|
+
- **Consumer extras**: Cursor skills + pattern docs refreshed for collaboration / Question bank hub header.
|
|
24
|
+
|
|
25
|
+
### Chore (monorepo)
|
|
26
|
+
|
|
27
|
+
- **Dependabot**: version updates for `apps/web`, `packages/ui`, workspace root, and grouped Next.js + GitHub Actions bumps (see repo `.github/dependabot.yml`).
|
|
28
|
+
- **Web app**: Next.js **16.2.6** (security patches), primary shell scroll fixes, `SiteHeader` chrome alignment.
|
|
29
|
+
|
|
18
30
|
## [0.2.15] - 2026-05-13
|
|
19
31
|
|
|
20
32
|
### Fixed
|
|
@@ -8,7 +8,8 @@ user-invocable: true
|
|
|
8
8
|
|
|
9
9
|
**Handbook:** `apps/web/AGENTS.md` §4.7
|
|
10
10
|
**Narrative:** `apps/web/docs/collaboration-access-pattern.md`
|
|
11
|
-
**Cursor rule:** `.cursor/rules/exxat-collaboration-access.mdc`
|
|
11
|
+
**Cursor rule:** `.cursor/rules/exxat-collaboration-access.mdc`
|
|
12
|
+
**Related (Question bank folder scope + ⋯ Customize folder):** `.cursor/rules/exxat-question-bank-hub-header.mdc` · `docs/question-bank-hub-header-pattern.md`
|
|
12
13
|
|
|
13
14
|
## Wiring checklist
|
|
14
15
|
|
|
@@ -19,6 +20,7 @@ user-invocable: true
|
|
|
19
20
|
5. **Access maps** — `lib/collaborator-access.ts` for Owner / Editor / Commenter / Viewer, invite options, and **`COLLABORATION_HEADER_ADD_LABEL`**.
|
|
20
21
|
6. **Header** — empty roster → outline **Add collaborator**; non-empty → face rail; both open the invite sheet.
|
|
21
22
|
7. **Invite sheet** — `InviteCollaboratorsDrawer`: export-style **`Sheet`**, combined email + access menu, grouped roster (name → email → role tags → access badge).
|
|
23
|
+
8. **Question bank — folder URL scope** — When **`?scope=folder`**, **`QuestionBankPageHeader`** **⋯** also lists **Customize folder**; **`QuestionBankNewFolderSheet`** on **`QuestionBankClient`** — **`.cursor/rules/exxat-question-bank-hub-header.mdc`**, **`docs/question-bank-hub-header-pattern.md`**.
|
|
22
24
|
|
|
23
25
|
## MUST NOT
|
|
24
26
|
|
|
@@ -22,6 +22,7 @@ description: >
|
|
|
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
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`**.
|
|
25
|
+
- **Question bank folder-scoped header (rule + doc):** **`.cursor/rules/exxat-question-bank-hub-header.mdc`** and **`docs/question-bank-hub-header-pattern.md`** — pair with **`exxat-primary-nav-secondary-panel`** when URL **`scope=folder`** drives the hub title.
|
|
25
26
|
- **Consumer repos (npm install of `@exxatdesignux/ui`):** After a version bump, read **`node_modules/@exxatdesignux/ui/CHANGELOG.md`**, run **`npx --package=@exxatdesignux/ui@latest exxat-ui sync-extras`** so **`docs/exxat-ds/consumer-upgrade-checklist.md`** and Cursor skills match the tarball, and diff the host app against **`node_modules/@exxatdesignux/ui/template/`** for anything new to port (routes, re-exports, AGENTS). Use **`exxat-ui changelog`**, **`exxat-ui update`**, and **`exxat-ui doctor`** for CLI guidance.
|
|
26
27
|
|
|
27
28
|
---
|
|
@@ -255,6 +256,8 @@ Use `PageHeader` from `@/components/page-header` for the content-area header (be
|
|
|
255
256
|
|
|
256
257
|
When a hub is **shared**, use **`PageHeader` `variant="collaboration"`**: **empty roster** → outline **Add collaborator**; **non-empty** → face rail (faces / **`+N`** open the invite sheet). **Invite people** also lives under the entity header **⋯ More** and opens **`InviteCollaboratorsDrawer`** via **`CollaborationAccessFlow`** when possible. Library access (Owner / Editor / Commenter / Viewer) comes from **`lib/collaborator-access.ts`**; directory tags (Faculty, Program coordinator, Director) use **`PageHeaderCollaborator.roles`**.
|
|
257
258
|
|
|
259
|
+
**Question bank library — folder URL scope:** When **`?scope=folder&folderId=`** applies, **⋯ More** must also offer **Customize folder** (**`QuestionBankPageHeader`** **`onCustomizeFolder`**) and the **`QuestionBankNewFolderSheet`** must be mounted on **`QuestionBankClient`** so it works on every **`ListPageTemplate`** view tab. **`.cursor/rules/exxat-question-bank-hub-header.mdc`** · **`docs/question-bank-hub-header-pattern.md`** (app: **`apps/web/docs/...`**).
|
|
260
|
+
|
|
258
261
|
**Handbook:** `apps/web/AGENTS.md` §4.7 · **Doc:** `docs/collaboration-access-pattern.md` · **Skill:** `.cursor/skills/exxat-collaboration-access/SKILL.md` · **Reference:** Question bank header + client.
|
|
259
262
|
|
|
260
263
|
---
|
|
@@ -15,7 +15,8 @@ user-invocable: true
|
|
|
15
15
|
2. **`components/secondary-panel.tsx`** — add **`PANELS["<id>"]`** → panel shell (title, optional search) + secondary nav component.
|
|
16
16
|
3. **Hub client** — mount **`*PanelActivator`** with **`useAutoPanel("<id>")`** (same id) for the lifetime of the route (e.g. `QuestionBankPanelActivator`).
|
|
17
17
|
4. **Data** — keep **one** **`useTableState`** / **`tableState.rows`**; drive scope from **URL** + small helpers (see **`lib/question-bank-nav.ts`**).
|
|
18
|
-
5. **
|
|
18
|
+
5. **Folder-scoped hub header (Question bank library)** — When **`scope === "folder"`** in the URL, **`QuestionBankPageHeader`** **⋯ More** includes **Customize folder**; mount **`QuestionBankNewFolderSheet`** on **`QuestionBankClient`** so it works on **all** **`ListPageTemplate`** view tabs — **`.cursor/rules/exxat-question-bank-hub-header.mdc`**, **`docs/question-bank-hub-header-pattern.md`**.
|
|
19
|
+
6. **Collapse control** — the nested rail header uses **`collapseActiveSecondaryPanel()`** (angles-left icon), not “close”, so the panel stays dismissed until **`openPanel`** runs again (nav, scope hook, or hub re-entry). Layout effects that auto-call **`openPanel`** must respect **`secondaryPanelAutoReopenSuppressed`** (see **`app/(app)/question-bank/layout.tsx`** + **`SecondaryPanelProvider`**).
|
|
19
20
|
|
|
20
21
|
## MUST NOT
|
|
21
22
|
|
|
@@ -26,3 +27,4 @@ user-invocable: true
|
|
|
26
27
|
|
|
27
28
|
- `components/app-sidebar.tsx` — `openPanel` on same-route primary click.
|
|
28
29
|
- `components/question-bank-secondary-nav.tsx` + `lib/question-bank-nav.ts`.
|
|
30
|
+
- **Folder-scoped header customize:** `components/question-bank-page-header.tsx`, `components/question-bank-client.tsx` — **`docs/question-bank-hub-header-pattern.md`**, **`.cursor/rules/exxat-question-bank-hub-header.mdc`**.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Shared UI for **who can access a hub** (face stack in the header) and **inviting people** (floating sheet). **Reference:** Question bank — `QuestionBankPageHeader`, `QuestionBankClient`, `InviteCollaboratorsDrawer`.
|
|
4
4
|
|
|
5
|
+
**Folder-scoped question bank:** When the library URL selects a folder (`?scope=folder&folderId=`), the same header **⋯ More** menu also exposes **Customize folder** (name / color / icon) via **`QuestionBankNewFolderSheet`** mounted on **`QuestionBankClient`** so it works on every view tab. See **`docs/question-bank-hub-header-pattern.md`** and **`.cursor/rules/exxat-question-bank-hub-header.mdc`**.
|
|
6
|
+
|
|
5
7
|
## When to use
|
|
6
8
|
|
|
7
9
|
- A list hub or library is **shared** across people (not a private directory).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exxatdesignux/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Exxat shared design system (components, hooks, tokens). Monorepo setup: clone repo then pnpm bootstrap at workspace root — see github.com/ExxatDesign/Exxat-DS-Workspace README.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
package/src/globals.css
CHANGED
|
@@ -165,6 +165,19 @@ html[data-text-size="large"] {
|
|
|
165
165
|
--brand-preview-one: var(--brand-tint);
|
|
166
166
|
--brand-preview-prism: oklch(0.57 0.24 342);
|
|
167
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Ask Leo panel tints (same mixes as the vertical wash). Use for blobs, cards, etc.
|
|
170
|
+
* `--leo-surface-gradient` is the full linear wash on `AskLeoSidebar`.
|
|
171
|
+
*/
|
|
172
|
+
--leo-surface-tint-a: color-mix(in oklch, var(--brand-color) 4%, var(--background));
|
|
173
|
+
--leo-surface-tint-b: color-mix(in oklch, var(--brand-color) 8%, var(--background));
|
|
174
|
+
--leo-surface-gradient: linear-gradient(180deg, var(--leo-surface-tint-a) 0%, var(--leo-surface-tint-b) 100%);
|
|
175
|
+
|
|
176
|
+
/* KeyMetrics `variant="flat"` — soft KPI band (lavender wash → canvas; dark: subtle brand lift) */
|
|
177
|
+
--key-metrics-flat-grad-top: color-mix(in oklch, var(--brand-tint-light) 90%, var(--background));
|
|
178
|
+
--key-metrics-flat-grad-mid: color-mix(in oklch, var(--brand-tint) 34%, var(--background));
|
|
179
|
+
--key-metrics-flat-cell-bg: color-mix(in oklch, var(--background) 80%, var(--brand-tint-light));
|
|
180
|
+
|
|
168
181
|
/* ── Surfaces ────────────────────────────────────────────────── */
|
|
169
182
|
--background: oklch(1 0 0);
|
|
170
183
|
--foreground: oklch(0.145 0 0); /* ≈ #1A1A1A — 17:1 on white ✓ */
|
|
@@ -243,7 +256,8 @@ html[data-text-size="large"] {
|
|
|
243
256
|
--sidebar-ring: oklch(0.25 0 0);
|
|
244
257
|
/* Nav section titles — ≥4.5:1 vs --sidebar (not vs page white) */
|
|
245
258
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 58%, var(--sidebar));
|
|
246
|
-
|
|
259
|
+
/* Nested secondary rail — soft brand wash on canvas (not pure `--background`, not full `--sidebar`). */
|
|
260
|
+
--secondary-panel-bg: color-mix(in oklch, var(--brand-tint) 34%, var(--background));
|
|
247
261
|
|
|
248
262
|
/* Browser UI (meta theme-color) — aligned with --brand-tint */
|
|
249
263
|
--theme-color-chrome: #f3f2f8;
|
|
@@ -351,6 +365,10 @@ html[data-text-size="large"] {
|
|
|
351
365
|
--destructive: oklch(0.65 0.20 25); /* brighter for dark bg */
|
|
352
366
|
--destructive-foreground: oklch(0.10 0 0);
|
|
353
367
|
|
|
368
|
+
--key-metrics-flat-grad-top: color-mix(in oklch, var(--brand-color) 14%, var(--background));
|
|
369
|
+
--key-metrics-flat-grad-mid: color-mix(in oklch, var(--muted) 42%, var(--background));
|
|
370
|
+
--key-metrics-flat-cell-bg: color-mix(in oklch, var(--background) 88%, var(--brand-color));
|
|
371
|
+
|
|
354
372
|
/* Borders — visible but not washed out on dark surfaces */
|
|
355
373
|
--border: oklch(0.38 0.008 270);
|
|
356
374
|
--border-control: oklch(0.72 0.012 270);
|
|
@@ -399,7 +417,8 @@ html[data-text-size="large"] {
|
|
|
399
417
|
--sidebar-border: oklch(0.38 0.010 270);
|
|
400
418
|
--sidebar-ring: oklch(0.85 0 0);
|
|
401
419
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 48%, var(--sidebar));
|
|
402
|
-
|
|
420
|
+
/* Nested secondary rail — dark: subtle brand lift on canvas (not flat `--background` only). */
|
|
421
|
+
--secondary-panel-bg: color-mix(in oklch, var(--background) 82%, var(--brand-color) 18%);
|
|
403
422
|
--theme-color-chrome: #2f2d36;
|
|
404
423
|
|
|
405
424
|
/* Lifted scrim on dark — white-tinted veil, not heavy black */
|
package/src/theme.css
CHANGED
|
@@ -219,7 +219,8 @@
|
|
|
219
219
|
--sidebar-ring: oklch(0.25 0 0);
|
|
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(--brand-tint) 34%, var(--background));
|
|
223
224
|
|
|
224
225
|
/* Browser UI (meta theme-color) — aligned with --brand-tint */
|
|
225
226
|
--theme-color-chrome: #f3f2f8;
|
|
@@ -345,7 +346,8 @@
|
|
|
345
346
|
--sidebar-border: oklch(0.38 0.010 270);
|
|
346
347
|
--sidebar-ring: oklch(0.85 0 0);
|
|
347
348
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 48%, var(--sidebar));
|
|
348
|
-
|
|
349
|
+
/* Nested secondary rail — dark: subtle brand lift on canvas (not flat `--background` only). */
|
|
350
|
+
--secondary-panel-bg: color-mix(in oklch, var(--background) 82%, var(--brand-color) 18%);
|
|
349
351
|
--theme-color-chrome: #2f2d36;
|
|
350
352
|
|
|
351
353
|
/* Lifted scrim on dark — white-tinted veil, not heavy black */
|
package/template/AGENTS.md
CHANGED
|
@@ -162,7 +162,9 @@ Thread **`view`** and **`onViewChange`** from the **client** → **table / toolb
|
|
|
162
162
|
|
|
163
163
|
**MUST NOT:** Set **`secondaryPanel`** without **`PANELS[id]`** and **`useAutoPanel`** — users see **collapsed** main nav with **no** panel body.
|
|
164
164
|
|
|
165
|
-
**
|
|
165
|
+
**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
|
+
|
|
167
|
+
**Cursor rule (panel wiring):** **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. **Icons in panel:** **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
|
|
166
168
|
|
|
167
169
|
### 4.7 Collaboration & access (shared hubs)
|
|
168
170
|
|
|
@@ -534,7 +536,7 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
|
|
|
534
536
|
| Data view charts: **`ChartFigure`** + **`ChartDataTable`**; keyboard highlight via **`chart-keyboard-selection`** (§4.3); layout via **`data-view-dashboard-storage`** | Ad-hoc `localStorage` keys for dashboard layout; opacity-only “selection” without `activeBar`/`activeShape` |
|
|
535
537
|
| Board cards: **`ListPageBoardCard`** shell; status via **`ListHubStatusBadge`** + **`lib/list-status-badges`**; no **`uppercase`** on status chips (§4.4) | One-off board card markup; status as plain body text; duplicated status maps outside **`list-status-badges`**; **empty placeholder** primary hubs (§4.1) |
|
|
536
538
|
| **§4.5** — Non-table view bodies use **`ListPageViewFrame`** (+ **`data-views/`** shells); new grids are generic components, not route-only markup | Duplicated `mx-4` / `max-w-*` per hub; wrapping **`DataTable`** so inset **doubles** (**§5**) |
|
|
537
|
-
| **§4.6** — **`secondaryPanel`** + **`PANELS`** + **`useAutoPanel`** together for nested scope nav | **`secondaryPanel`** id with no panel component or activator |
|
|
539
|
+
| **§4.6** — **`secondaryPanel`** + **`PANELS`** + **`useAutoPanel`** together for nested scope nav; **folder URL scope** → header **⋯** **Customize folder** + client-mounted **`QuestionBankNewFolderSheet`** (**`exxat-question-bank-hub-header.mdc`**) | **`secondaryPanel`** id with no panel component or activator; folder scope with customize **only** inside a single view tab’s subtree |
|
|
538
540
|
| **§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 |
|
|
539
541
|
| **§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 |
|
|
540
542
|
| **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 |
|
|
@@ -570,7 +572,7 @@ Copy and complete when implementing or reviewing:
|
|
|
570
572
|
- [ ] **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.
|
|
571
573
|
- [ ] **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`**.
|
|
572
574
|
- [ ] **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`**.
|
|
573
|
-
- [ ] **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`**.
|
|
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`**.
|
|
574
576
|
- [ ] **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`**.
|
|
575
577
|
- [ ] **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`**.
|
|
576
578
|
- [ ] **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`**.
|
|
@@ -578,4 +580,4 @@ Copy and complete when implementing or reviewing:
|
|
|
578
580
|
|
|
579
581
|
---
|
|
580
582
|
|
|
581
|
-
*Last updated: drawer vs dialog / card vs rows / KPI max-four pattern docs + rules + skills; §6.4 table; §4.8 dedicated search templates; §4.7 collaboration & access; §4.1 centralized dataset + presentation; §4.5–§4.6 view shells + secondary panel; Font Awesome rule; §9.1 application sidebar shell; §4.4 board cards; §6.5 no toast; §7.1 command palette; §13 checklist.*
|
|
583
|
+
*Last updated: question bank folder-scoped header Customize + rule/skill; drawer vs dialog / card vs rows / KPI max-four pattern docs + rules + skills; §6.4 table; §4.8 dedicated search templates; §4.7 collaboration & access; §4.1 centralized dataset + presentation; §4.5–§4.6 view shells + secondary panel; Font Awesome rule; §9.1 application sidebar shell; §4.4 board cards; §6.5 no toast; §7.1 command palette; §13 checklist.*
|
|
@@ -4,11 +4,15 @@ import * as React from "react"
|
|
|
4
4
|
import { usePathname } from "next/navigation"
|
|
5
5
|
|
|
6
6
|
import { useSecondaryPanel } from "@/components/secondary-panel"
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
QUESTION_BANK_ENTRY_PATH,
|
|
9
|
+
QUESTION_BANK_HUB_FIND_PATH,
|
|
10
|
+
QUESTION_BANK_LIST_PATH,
|
|
11
|
+
} from "@/lib/question-bank-nav"
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
|
-
* Keeps the nested secondary panel open across library
|
|
11
|
-
*
|
|
14
|
+
* Keeps the nested secondary panel open across library navigations.
|
|
15
|
+
* **Question hub** (`/question-bank`) and **Search** (`/find`, `/list`) stay full-width — no secondary rail.
|
|
12
16
|
*/
|
|
13
17
|
export default function QuestionBankLayout({ children }: { children: React.ReactNode }) {
|
|
14
18
|
const pathname = usePathname()
|
|
@@ -30,7 +34,10 @@ export default function QuestionBankLayout({ children }: { children: React.React
|
|
|
30
34
|
const isDiscoveryHubRoot =
|
|
31
35
|
pathname === QUESTION_BANK_ENTRY_PATH || pathname === `${QUESTION_BANK_ENTRY_PATH}/`
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
const isDedicatedSearchSurface =
|
|
38
|
+
pathname === QUESTION_BANK_HUB_FIND_PATH || pathname === QUESTION_BANK_LIST_PATH
|
|
39
|
+
|
|
40
|
+
if (isDiscoveryHubRoot || isDedicatedSearchSurface) {
|
|
34
41
|
closePanelRef.current({ mainSidebar: "leave" })
|
|
35
42
|
return undefined
|
|
36
43
|
}
|
package/template/app/globals.css
CHANGED
|
@@ -151,6 +151,14 @@ html[data-text-size="large"] {
|
|
|
151
151
|
via a scoped class, not globally on :root. */
|
|
152
152
|
/* Minimum readable UI copy: 11px (`text-xs` / --text-xs). Do not use smaller arbitrary sizes. */
|
|
153
153
|
|
|
154
|
+
/* ── Layout ─────────────────────────────────────────────────── */
|
|
155
|
+
/* SiteHeader / breadcrumb height. Mirrored on the SidebarProvider for
|
|
156
|
+
descendants; declared here too so JS that reads from documentElement
|
|
157
|
+
(e.g. DataTable sticky-head offset) can resolve it. Plain px so
|
|
158
|
+
`parseFloat` can read it as a number. Keep in sync with `headerHeight`
|
|
159
|
+
in components/sidebar-shell.tsx (currently `calc(var(--spacing) * 12)` = 48px). */
|
|
160
|
+
--header-height: 48px;
|
|
161
|
+
|
|
154
162
|
/* ── Typography ─────────────────────────────────────────────── */
|
|
155
163
|
/* Ivy Presto loaded via Adobe Fonts Kit wuk5wqn (use.typekit.net) */
|
|
156
164
|
--font-heading: "ivypresto-text";
|
|
@@ -171,6 +179,19 @@ html[data-text-size="large"] {
|
|
|
171
179
|
--brand-preview-one: var(--brand-tint);
|
|
172
180
|
--brand-preview-prism: oklch(0.57 0.24 342);
|
|
173
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Ask Leo panel tints (same mixes as the vertical wash). Use for blobs, cards, etc.
|
|
184
|
+
* `--leo-surface-gradient` is the full linear wash on `AskLeoSidebar`.
|
|
185
|
+
*/
|
|
186
|
+
--leo-surface-tint-a: color-mix(in oklch, var(--brand-color) 4%, var(--background));
|
|
187
|
+
--leo-surface-tint-b: color-mix(in oklch, var(--brand-color) 8%, var(--background));
|
|
188
|
+
--leo-surface-gradient: linear-gradient(180deg, var(--leo-surface-tint-a) 0%, var(--leo-surface-tint-b) 100%);
|
|
189
|
+
|
|
190
|
+
/* KeyMetrics `variant="flat"` — soft KPI band (lavender wash → canvas; dark: subtle brand lift) */
|
|
191
|
+
--key-metrics-flat-grad-top: color-mix(in oklch, var(--brand-tint-light) 90%, var(--background));
|
|
192
|
+
--key-metrics-flat-grad-mid: color-mix(in oklch, var(--brand-tint) 34%, var(--background));
|
|
193
|
+
--key-metrics-flat-cell-bg: color-mix(in oklch, var(--background) 80%, var(--brand-tint-light));
|
|
194
|
+
|
|
174
195
|
/* ── Surfaces ────────────────────────────────────────────────── */
|
|
175
196
|
--background: oklch(1 0 0);
|
|
176
197
|
--foreground: oklch(0.145 0 0); /* ≈ #1A1A1A — 17:1 on white ✓ */
|
|
@@ -249,7 +270,8 @@ html[data-text-size="large"] {
|
|
|
249
270
|
--sidebar-ring: oklch(0.25 0 0);
|
|
250
271
|
/* Nav section titles — ≥4.5:1 vs --sidebar (not vs page white) */
|
|
251
272
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 58%, var(--sidebar));
|
|
252
|
-
|
|
273
|
+
/* Nested secondary rail — soft brand wash on canvas (not pure `--background`, not full `--sidebar`). */
|
|
274
|
+
--secondary-panel-bg: color-mix(in oklch, var(--brand-tint) 34%, var(--background));
|
|
253
275
|
|
|
254
276
|
/* Browser UI (meta theme-color) — aligned with --brand-tint */
|
|
255
277
|
--theme-color-chrome: #f3f2f8;
|
|
@@ -357,6 +379,10 @@ html[data-text-size="large"] {
|
|
|
357
379
|
--destructive: oklch(0.65 0.20 25); /* brighter for dark bg */
|
|
358
380
|
--destructive-foreground: oklch(0.10 0 0);
|
|
359
381
|
|
|
382
|
+
--key-metrics-flat-grad-top: color-mix(in oklch, var(--brand-color) 14%, var(--background));
|
|
383
|
+
--key-metrics-flat-grad-mid: color-mix(in oklch, var(--muted) 42%, var(--background));
|
|
384
|
+
--key-metrics-flat-cell-bg: color-mix(in oklch, var(--background) 88%, var(--brand-color));
|
|
385
|
+
|
|
360
386
|
/* Borders — visible but not washed out on dark surfaces */
|
|
361
387
|
--border: oklch(0.38 0.008 270);
|
|
362
388
|
--border-control: oklch(0.72 0.012 270);
|
|
@@ -405,7 +431,8 @@ html[data-text-size="large"] {
|
|
|
405
431
|
--sidebar-border: oklch(0.38 0.010 270);
|
|
406
432
|
--sidebar-ring: oklch(0.85 0 0);
|
|
407
433
|
--sidebar-section-label-foreground: color-mix(in oklch, var(--sidebar-foreground) 48%, var(--sidebar));
|
|
408
|
-
|
|
434
|
+
/* Nested secondary rail — dark: subtle brand lift on canvas (not flat `--background` only). */
|
|
435
|
+
--secondary-panel-bg: color-mix(in oklch, var(--background) 82%, var(--brand-color) 18%);
|
|
409
436
|
--theme-color-chrome: #2f2d36;
|
|
410
437
|
|
|
411
438
|
/* Lifted scrim on dark — white-tinted veil, not heavy black */
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
53
53
|
import { Badge } from "@/components/ui/badge"
|
|
54
54
|
import { StatusBadge } from "@/components/ui/status-badge"
|
|
55
|
+
import { Separator } from "@/components/ui/separator"
|
|
55
56
|
import {
|
|
56
57
|
Tooltip,
|
|
57
58
|
TooltipContent,
|
|
@@ -108,7 +109,8 @@ function normalizedLocationHash(locationHash: string): string {
|
|
|
108
109
|
/**
|
|
109
110
|
* Whether `pathname` (+ optional `location.hash`) matches a sidebar `href`.
|
|
110
111
|
* When several links share the same path (e.g. `/settings`), disambiguate with `#fragment`
|
|
111
|
-
*
|
|
112
|
+
* in each `href` — those rows use the `frag !== null` branch below.
|
|
113
|
+
* For `href` without `#…`, an in-page hash (e.g. QB view tabs) does not clear the match.
|
|
112
114
|
*/
|
|
113
115
|
function isNavActive(pathname: string, url: string, locationHash = ""): boolean {
|
|
114
116
|
const pathOnly = navUrlPath(url)
|
|
@@ -129,7 +131,8 @@ function isNavActive(pathname: string, url: string, locationHash = ""): boolean
|
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
if (pathOnly === "/") return pathname === "/" && h === ""
|
|
132
|
-
|
|
134
|
+
/** Exact path match — ignore `location.hash` when the nav `href` has no `#…` fragment (QB view tabs use hash). */
|
|
135
|
+
if (pathname === pathOnly) return true
|
|
133
136
|
// Design system library — active on hub and detail routes.
|
|
134
137
|
if (pathOnly === "/library") {
|
|
135
138
|
return pathname.startsWith("/library/")
|
|
@@ -165,6 +168,16 @@ function isCollapsibleChildActive(
|
|
|
165
168
|
|
|
166
169
|
if (!isNavActive(pathname, child.url, locationHash)) return false
|
|
167
170
|
|
|
171
|
+
/** Hub entry (`/question-bank`) must not stay “active” on `/question-bank/library` etc. */
|
|
172
|
+
if (parent.primaryHubChildKey && child.key === parent.primaryHubChildKey) {
|
|
173
|
+
const hubPath = navUrlPath(parent.url)
|
|
174
|
+
if (hubPath) {
|
|
175
|
+
const normalized =
|
|
176
|
+
pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
|
177
|
+
if (normalized !== hubPath) return false
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
168
181
|
const urls = children.map(c => c.url)
|
|
169
182
|
const allSameUrl = urls.length > 1 && urls.every(u => u === urls[0])
|
|
170
183
|
if (allSameUrl) {
|
|
@@ -176,6 +189,32 @@ function isCollapsibleChildActive(
|
|
|
176
189
|
return true
|
|
177
190
|
}
|
|
178
191
|
|
|
192
|
+
/**
|
|
193
|
+
* “Selected” styling on a collapsible **parent** row — not the same as “a descendant route is open”.
|
|
194
|
+
* When a child row is the current destination (e.g. Library on `/question-bank/library`), the parent
|
|
195
|
+
* should stay visually neutral while the child carries `data-active`. Only highlight the parent when
|
|
196
|
+
* the active child is the hub row whose `href` matches the parent (e.g. Question hub on `/question-bank`),
|
|
197
|
+
* or when no child matches but the parent URL still matches (edge routes).
|
|
198
|
+
*/
|
|
199
|
+
function isCollapsibleParentMenuButtonActive(
|
|
200
|
+
pathname: string,
|
|
201
|
+
item: NavLinkItem,
|
|
202
|
+
locationHash: string,
|
|
203
|
+
): boolean {
|
|
204
|
+
const children = item.children
|
|
205
|
+
if (!children?.length) return isNavActive(pathname, item.url, locationHash)
|
|
206
|
+
|
|
207
|
+
const activeChildren = children.filter(c =>
|
|
208
|
+
isCollapsibleChildActive(pathname, item, c, locationHash),
|
|
209
|
+
)
|
|
210
|
+
if (activeChildren.length === 0) {
|
|
211
|
+
return isNavActive(pathname, item.url, locationHash)
|
|
212
|
+
}
|
|
213
|
+
if (activeChildren.length !== 1) return false
|
|
214
|
+
const [child] = activeChildren
|
|
215
|
+
return navUrlPath(child.url) === navUrlPath(item.url)
|
|
216
|
+
}
|
|
217
|
+
|
|
179
218
|
/** Accessible suffix for sidebar badges (badge is rendered outside the link node). */
|
|
180
219
|
function badgeAccessibleSuffix(badge: number | string): string {
|
|
181
220
|
if (typeof badge === "number") return `${badge} items`
|
|
@@ -183,30 +222,40 @@ function badgeAccessibleSuffix(badge: number | string): string {
|
|
|
183
222
|
}
|
|
184
223
|
|
|
185
224
|
/** Child row for expandable nav items — shared by inline sub-menu and collapsed-rail popover. */
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
225
|
+
const SidebarNavChildLink = React.forwardRef<
|
|
226
|
+
HTMLAnchorElement,
|
|
227
|
+
{
|
|
228
|
+
parent: NavLinkItem
|
|
229
|
+
child: NavLinkItem
|
|
230
|
+
pathname: string
|
|
231
|
+
locationHash: string
|
|
232
|
+
onNavigate?: () => void
|
|
233
|
+
/** Popover uses surface tokens; inline sub-menu uses `SidebarMenuSubButton`. */
|
|
234
|
+
linkClassName?: string
|
|
235
|
+
} & Omit<React.ComponentPropsWithoutRef<typeof Link>, "href">
|
|
236
|
+
>(function SidebarNavChildLink(
|
|
237
|
+
{
|
|
238
|
+
parent,
|
|
239
|
+
child,
|
|
240
|
+
pathname,
|
|
241
|
+
locationHash,
|
|
242
|
+
onNavigate,
|
|
243
|
+
linkClassName,
|
|
244
|
+
className: incomingClassName,
|
|
245
|
+
onClick,
|
|
246
|
+
...linkRest
|
|
247
|
+
},
|
|
248
|
+
ref,
|
|
249
|
+
) {
|
|
202
250
|
const { openPanel } = useSecondaryPanel()
|
|
203
251
|
const childActive = isCollapsibleChildActive(pathname, parent, child, locationHash)
|
|
204
252
|
const childPath = navUrlPath(child.url)
|
|
205
253
|
|
|
206
254
|
return (
|
|
207
255
|
<Link
|
|
256
|
+
ref={ref}
|
|
208
257
|
href={child.url}
|
|
209
|
-
className={cn("flex min-w-0 items-center gap-2", linkClassName)}
|
|
258
|
+
className={cn("flex min-w-0 items-center gap-2", linkClassName, incomingClassName)}
|
|
210
259
|
aria-current={childActive ? "page" : undefined}
|
|
211
260
|
onClick={e => {
|
|
212
261
|
onNavigate?.()
|
|
@@ -218,15 +267,18 @@ function SidebarNavChildLink({
|
|
|
218
267
|
e.preventDefault()
|
|
219
268
|
openPanel(parent.secondaryPanel)
|
|
220
269
|
}
|
|
270
|
+
onClick?.(e)
|
|
221
271
|
}}
|
|
272
|
+
{...linkRest}
|
|
222
273
|
>
|
|
223
274
|
<span className="size-4 shrink-0 inline-flex items-center justify-center" aria-hidden="true">
|
|
224
|
-
{child.icon}
|
|
275
|
+
{childActive && child.iconActive ? child.iconActive : child.icon}
|
|
225
276
|
</span>
|
|
226
277
|
<span className="min-w-0 flex-1 truncate">{child.title}</span>
|
|
227
278
|
</Link>
|
|
228
279
|
)
|
|
229
|
-
}
|
|
280
|
+
})
|
|
281
|
+
SidebarNavChildLink.displayName = "SidebarNavChildLink"
|
|
230
282
|
|
|
231
283
|
/**
|
|
232
284
|
* CollapsibleNavItem — isolated component so each collapsible has its own
|
|
@@ -235,19 +287,17 @@ function SidebarNavChildLink({
|
|
|
235
287
|
* server (SSR) vs the client (router not yet available).
|
|
236
288
|
*/
|
|
237
289
|
function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: string }) {
|
|
238
|
-
const locationHash
|
|
239
|
-
const isActive = isNavActive(pathname, item.url, locationHash)
|
|
290
|
+
const locationHash = useLocationHash()
|
|
240
291
|
const isAnyChildActive =
|
|
241
292
|
item.children?.some(c => isCollapsibleChildActive(pathname, item, c, locationHash)) ?? false
|
|
293
|
+
const parentMenuButtonActive = isCollapsibleParentMenuButtonActive(pathname, item, locationHash)
|
|
242
294
|
const { state, isMobile } = useSidebar()
|
|
243
|
-
const { openPanel } = useSecondaryPanel()
|
|
244
295
|
const [open, setOpen] = React.useState(false)
|
|
245
296
|
const [flyoutOpen, setFlyoutOpen] = React.useState(false)
|
|
246
297
|
const flyoutTitleId = React.useId()
|
|
247
298
|
const iconRailCollapsed = state === "collapsed" && !isMobile
|
|
248
|
-
const showActiveStyle = isActive || isAnyChildActive
|
|
249
299
|
const triggerIcon =
|
|
250
|
-
|
|
300
|
+
parentMenuButtonActive && item.iconActive ? item.iconActive : item.icon
|
|
251
301
|
|
|
252
302
|
React.useEffect(() => {
|
|
253
303
|
setOpen(isAnyChildActive)
|
|
@@ -267,23 +317,20 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
267
317
|
open={flyoutOpen}
|
|
268
318
|
onOpenChange={next => {
|
|
269
319
|
setFlyoutOpen(next)
|
|
270
|
-
if (next && item.secondaryPanel) {
|
|
271
|
-
openPanel(item.secondaryPanel)
|
|
272
|
-
}
|
|
273
320
|
}}
|
|
274
321
|
>
|
|
275
322
|
<Tooltip>
|
|
276
323
|
<TooltipTrigger asChild>
|
|
277
324
|
<PopoverTrigger asChild>
|
|
278
325
|
<SidebarMenuButton
|
|
279
|
-
isActive={
|
|
326
|
+
isActive={parentMenuButtonActive}
|
|
280
327
|
aria-haspopup="dialog"
|
|
281
328
|
aria-label={`${item.title} — open subpages`}
|
|
282
329
|
>
|
|
283
330
|
<span
|
|
284
331
|
className={cn(
|
|
285
332
|
"size-4 shrink-0 flex items-center justify-center",
|
|
286
|
-
|
|
333
|
+
parentMenuButtonActive &&
|
|
287
334
|
"[animation:sidebar-icon-pop_380ms_cubic-bezier(0.34,1.56,0.64,1)_both]",
|
|
288
335
|
)}
|
|
289
336
|
aria-hidden="true"
|
|
@@ -341,9 +388,6 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
341
388
|
open={open}
|
|
342
389
|
onOpenChange={next => {
|
|
343
390
|
setOpen(next)
|
|
344
|
-
if (next && item.secondaryPanel) {
|
|
345
|
-
openPanel(item.secondaryPanel)
|
|
346
|
-
}
|
|
347
391
|
}}
|
|
348
392
|
asChild
|
|
349
393
|
>
|
|
@@ -351,12 +395,12 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
351
395
|
<Tooltip>
|
|
352
396
|
<TooltipTrigger asChild>
|
|
353
397
|
<CollapsibleTrigger asChild>
|
|
354
|
-
<SidebarMenuButton isActive={
|
|
398
|
+
<SidebarMenuButton isActive={parentMenuButtonActive}>
|
|
355
399
|
<span
|
|
356
|
-
key={
|
|
400
|
+
key={parentMenuButtonActive ? "active" : "idle"}
|
|
357
401
|
className={cn(
|
|
358
402
|
"size-4 shrink-0 flex items-center justify-center",
|
|
359
|
-
|
|
403
|
+
parentMenuButtonActive &&
|
|
360
404
|
"[animation:sidebar-icon-pop_380ms_cubic-bezier(0.34,1.56,0.64,1)_both]",
|
|
361
405
|
)}
|
|
362
406
|
aria-hidden="true"
|
|
@@ -769,10 +813,7 @@ function ProductLogoButton() {
|
|
|
769
813
|
>
|
|
770
814
|
{iconRail ? (
|
|
771
815
|
<span className="flex size-8 shrink-0 items-center justify-center">
|
|
772
|
-
<ExxatProductMark
|
|
773
|
-
product={current.id}
|
|
774
|
-
className="size-7 max-h-none"
|
|
775
|
-
/>
|
|
816
|
+
<ExxatProductMark product={current.id} className="size-7" />
|
|
776
817
|
</span>
|
|
777
818
|
) : (
|
|
778
819
|
<span className="flex min-h-0 min-w-0 flex-1 items-stretch gap-2">
|
|
@@ -901,6 +942,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
|
901
942
|
<ProductLogoButton />
|
|
902
943
|
</SidebarMenuItem>
|
|
903
944
|
</SidebarMenu>
|
|
945
|
+
<div className="flex w-full justify-center px-2">
|
|
946
|
+
<Separator
|
|
947
|
+
orientation="horizontal"
|
|
948
|
+
decorative
|
|
949
|
+
className="my-1.5 h-px w-full max-w-none shrink-0 bg-sidebar-border group-data-[collapsible=icon]:w-8"
|
|
950
|
+
/>
|
|
951
|
+
</div>
|
|
904
952
|
<TeamSwitcher />
|
|
905
953
|
</SidebarHeaderStack>
|
|
906
954
|
</SidebarHeader>
|
|
@@ -286,8 +286,7 @@ export function AskLeoSidebar() {
|
|
|
286
286
|
style={
|
|
287
287
|
open
|
|
288
288
|
? {
|
|
289
|
-
background:
|
|
290
|
-
"linear-gradient(180deg, color-mix(in oklch, var(--brand-color) 4%, var(--background)) 0%, color-mix(in oklch, var(--brand-color) 8%, var(--background)) 100%)",
|
|
289
|
+
background: "var(--leo-surface-gradient)",
|
|
291
290
|
}
|
|
292
291
|
: undefined
|
|
293
292
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* FinderPanelView — Miller-style 3-column split for list-page hubs.
|
|
5
5
|
*
|
|
6
|
-
* Visual shell matches Question bank panel (`ListPageTreeColumnHeader`, `
|
|
6
|
+
* Visual shell matches Question bank panel (`ListPageTreeColumnHeader`, `LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS`,
|
|
7
7
|
* shared resizable handles) — see `list-page-split-hub-tokens.ts`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -142,7 +142,7 @@ export function FinderGroupStrip({
|
|
|
142
142
|
<div
|
|
143
143
|
role="toolbar"
|
|
144
144
|
aria-label={ariaLabel}
|
|
145
|
-
className="flex min-h-10 flex-wrap items-center gap-1.5 border-b border-border bg-
|
|
145
|
+
className="flex min-h-10 flex-wrap items-center gap-1.5 border-b border-border bg-card px-2 py-2"
|
|
146
146
|
>
|
|
147
147
|
{groups.map(group => {
|
|
148
148
|
const isSelected = group.id === selectedGroupId
|
|
@@ -47,6 +47,25 @@ export {
|
|
|
47
47
|
type ListPageTreeColumnHeaderProps,
|
|
48
48
|
} from "@/components/data-views/list-page-tree-column-header"
|
|
49
49
|
|
|
50
|
+
/** VS Code–style outline tree chrome — mirrors shadcn `SidebarMenuSub` (see module doc). */
|
|
51
|
+
export {
|
|
52
|
+
OutlineTreeCollapsibleContentRail,
|
|
53
|
+
OutlineTreeLeafButton,
|
|
54
|
+
OutlineTreeMenu,
|
|
55
|
+
OutlineTreeMenuItem,
|
|
56
|
+
OutlineTreeSub,
|
|
57
|
+
OutlineTreeSubItem,
|
|
58
|
+
OUTLINE_TREE_COLLAPSIBLE_CONTENT_RAIL_CLASS,
|
|
59
|
+
OUTLINE_TREE_CHEVRON_GUIDE_SPACER_CLASS,
|
|
60
|
+
OUTLINE_TREE_SUB_ROW_SHIFT_CLASS,
|
|
61
|
+
type OutlineTreeGuideLayout,
|
|
62
|
+
type OutlineTreeLeafButtonProps,
|
|
63
|
+
type OutlineTreeSurface,
|
|
64
|
+
} from "@/components/data-views/outline-tree-menu"
|
|
65
|
+
|
|
66
|
+
export { QuestionBankFolderTreeBranch } from "@/components/data-views/question-bank-folder-tree-branch"
|
|
67
|
+
export type { QuestionBankFolderTreeBranchProps } from "@/components/data-views/question-bank-folder-tree-branch"
|
|
68
|
+
|
|
50
69
|
export {
|
|
51
70
|
LIST_PAGE_SPLIT_MILLER_COLUMN_PANEL_CLASS,
|
|
52
71
|
LIST_PAGE_SPLIT_MILLER_DETAIL_PANEL_CLASS,
|