@exxatdesignux/ui 0.2.17 → 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 +15 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +6 -1
- 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 +1 -1
- package/src/components/ui/sidebar.tsx +2 -2
- package/src/globals.css +65 -14
- package/src/theme.css +3 -3
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/AGENTS.md +11 -4
- package/template/app/(app)/error.tsx +22 -6
- package/template/app/(app)/layout.tsx +13 -6
- package/template/app/global-error.tsx +63 -0
- package/template/app/globals.css +44 -14
- package/template/app/layout.tsx +2 -0
- package/template/components/app-sidebar.tsx +4 -3
- package/template/components/compliance-table.tsx +0 -20
- package/template/components/data-table/index.tsx +31 -67
- package/template/components/data-table/use-table-state.ts +33 -6
- package/template/components/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/exxat-product-logo.tsx +2 -6
- package/template/components/key-metrics.tsx +54 -22
- package/template/components/placement-board-card.tsx +1 -1
- package/template/components/placements-list-view.tsx +1 -1
- package/template/components/placements-table.tsx +3 -36
- package/template/components/product-switcher.tsx +2 -3
- package/template/components/product-wordmark.tsx +4 -7
- package/template/components/question-bank-hub-client.tsx +2 -5
- package/template/components/question-bank-table.tsx +12 -24
- package/template/components/sidebar-shell.tsx +2 -1
- package/template/components/sites-table.tsx +0 -20
- package/template/components/table-properties/drawer-button.tsx +38 -20
- package/template/components/table-properties/drawer.tsx +16 -13
- package/template/components/team-table.tsx +0 -21
- package/template/components/templates/list-page.tsx +12 -9
- package/template/components/templates/nested-secondary-panel-shell.tsx +2 -2
- package/template/contexts/product-context.tsx +21 -2
- 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/lib/chunk-load-error.ts +13 -0
- package/template/lib/conditional-rule-match.ts +87 -22
- package/template/lib/data-list-view.ts +6 -0
- package/template/lib/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +58 -11
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,21 @@ After the user bumps `@exxatdesignux/ui`, do this in order:
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
+
## [0.2.18] - 2026-05-19
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`Sidebar`**: Read `sidebar_state` on the server for `defaultOpen` so the documents **Resources** heading and rail chrome match the first client paint (fixes hydration warnings). Skip redundant cookie restore when state already matches.
|
|
23
|
+
- **`TablePropertiesDrawer`**: Portaled **Add filter / sort / rule** menus use `z-[90]` above the properties sheet (`z-[80]`); `Sheet` and dropdowns use `modal={false}`; filter updates apply synchronously (no `startTransition` deferral). Drawer button routes column/display handlers through refs for the portaled sheet.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Starter `template/`** and **consumer extras**: KPI flat band + shell surface elevation patterns/skills; table-properties and hub parity with `apps/web`.
|
|
28
|
+
|
|
29
|
+
### Chore (monorepo)
|
|
30
|
+
|
|
31
|
+
- Package **`version`** set to **0.2.18** for npm publish (tag **`ui-v0.2.18`**).
|
|
32
|
+
|
|
18
33
|
## [0.2.17] - 2026-05-18
|
|
19
34
|
|
|
20
35
|
### Changed
|
|
@@ -551,7 +551,7 @@ import { DropdownMenuItem, Shortcut } from "@/components/ui/dropdown-menu"
|
|
|
551
551
|
| Duplicate | ⌘/Ctrl + D |
|
|
552
552
|
| Review / Info | ⌘/Ctrl + I |
|
|
553
553
|
| Remove / Delete item | ⌘/Ctrl + ⌫ |
|
|
554
|
-
| Add view (1..n) |
|
|
554
|
+
| Add view (1..n) | **1..9** (plain digit; `dataListViewAddShortcut`) |
|
|
555
555
|
| **Submit a workflow** (Create, Save, Export, Apply) | **Enter** ⏎ — scoped to the open form/drawer/dialog |
|
|
556
556
|
| **Cancel / dismiss** a workflow | **Esc** (Radix Dialog/Sheet/AlertDialog already bind this) |
|
|
557
557
|
| **Advance a multi-step wizard** | ⌘/Ctrl + Enter (plain Enter must not submit mid-flow) |
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: exxat-kpi-flat-band
|
|
3
|
+
description: KeyMetrics variant flat — transparent KPI strip, OKLCH brand glow only, cell-border hairlines (no grid surface). Use when wiring ListPageTemplate metrics, dashboard mix KPIs, or fixing flat band looking like a grey box.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Exxat DS — KPI flat band
|
|
8
|
+
|
|
9
|
+
**Rule:** `.cursor/rules/exxat-kpi-flat-band.mdc`
|
|
10
|
+
**Doc:** `apps/web/docs/kpi-flat-band-pattern.md`
|
|
11
|
+
|
|
12
|
+
## Checklist
|
|
13
|
+
|
|
14
|
+
- [ ] `variant="flat"` on hub / mix view — not `card` for list-page strip.
|
|
15
|
+
- [ ] `flatBandStyle` = **only** `var(--key-metrics-flat-band-radial)`; shadow **`none`**.
|
|
16
|
+
- [ ] No `--key-metrics-flat-band-linear` in component or hub inline `style`.
|
|
17
|
+
- [ ] Cells **`bg-transparent`**; grid uses **`flatMetricsHairlineClass(count, halfLayout)`** — borders only, **no** `gap-px` fill.
|
|
18
|
+
- [ ] **4 KPIs:** verticals between 1|2|3|4 when wide; 2×2 dividers only below `@[max-width:29.99rem]` container.
|
|
19
|
+
- [ ] Divider + glow tokens stay **OKLCH** (`--key-metrics-flat-divider`, `color-mix(in oklch, var(--brand-color) …)`).
|
|
20
|
+
- [ ] **≤ 4** `MetricItem` — `docs/kpi-strip-max-four-pattern.md`.
|
|
21
|
+
- [ ] KPI helpers use **`tableState.rows`** on connected hubs.
|
|
22
|
+
|
|
23
|
+
## MUST NOT
|
|
24
|
+
|
|
25
|
+
- Grey/lavender **panel** behind metrics (removed linear wash + gap fill).
|
|
26
|
+
- Duplicate KPI **`Card`** wall for same numbers.
|
|
27
|
+
- Mute product suffix to grey in dark (`mutedSuffix` does **not** change `wordmarkColor`).
|
|
28
|
+
|
|
29
|
+
## Code pointers
|
|
30
|
+
|
|
31
|
+
- `apps/web/components/key-metrics.tsx` — `flatMetricsHairlineClass`, `flatBandStyle`
|
|
32
|
+
- `apps/web/app/globals.css` — `--key-metrics-flat-*`
|
|
33
|
+
- `question-bank-client.tsx`, `dashboard-tabs.tsx` — reference usage
|
|
34
|
+
|
|
35
|
+
## Pair with
|
|
36
|
+
|
|
37
|
+
- `exxat-kpi-max-four`, `exxat-kpi-trends`, `exxat-list-page-connected-views`
|
|
38
|
+
- `docs/shell-surface-elevation-pattern.md` — sidebar vs page (not the KPI band)
|
|
@@ -17,12 +17,14 @@ user-invocable: true
|
|
|
17
17
|
4. **Data** — keep **one** **`useTableState`** / **`tableState.rows`**; drive scope from **URL** + small helpers (see **`lib/question-bank-nav.ts`**).
|
|
18
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
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`**).
|
|
20
|
+
7. **Surface elevation** — secondary panel = **level 1** (lighter than sidebar, darker than page). Use **`--secondary-panel-bg`** on **`NestedSecondaryPanelShell`**; derive from **`--brand-tint*`** per active product (**One** indigo, **Prism** rose). See **`docs/shell-surface-elevation-pattern.md`**.
|
|
20
21
|
|
|
21
22
|
## MUST NOT
|
|
22
23
|
|
|
23
24
|
- Set **`secondaryPanel`** without **`PANELS[id]`** + **`useAutoPanel`** — broken empty rail.
|
|
24
25
|
- Use this for full product areas that belong as **primary** or **collapsible child** rows.
|
|
25
26
|
- Invent a parallel zoom / reflow hook for the secondary rail — reuse **`useSidebarReflowZoom`** (the provider already wires it; see §"High-zoom auto-collapse" below).
|
|
27
|
+
- Set secondary panel **`bg-sidebar`** or a fixed rose mix for every product — breaks **One** indigo chrome.
|
|
26
28
|
|
|
27
29
|
## High-zoom auto-collapse
|
|
28
30
|
|
|
@@ -39,6 +41,9 @@ Custom panel content (anything you register under `PANELS[id]`) should **read `s
|
|
|
39
41
|
- `components/app-sidebar.tsx` — `openPanel` on same-route primary click.
|
|
40
42
|
- `components/secondary-panel.tsx` — `SecondaryPanelProvider`, `PANELS` registry, **high-zoom auto-collapse**.
|
|
41
43
|
- `hooks/use-sidebar-reflow-zoom.ts` — shared zoom / reflow signal.
|
|
42
|
-
- `components/templates/nested-secondary-panel-shell.tsx` — expanded vs `compact` (icon rail) widths
|
|
44
|
+
- `components/templates/nested-secondary-panel-shell.tsx` — expanded vs `compact` (icon rail) widths; **`bg-[var(--secondary-panel-bg)]`**.
|
|
45
|
+
- `app/globals.css` — `--secondary-panel-bg`, `--sidebar`, product **`theme-one`** / **`theme-prism`** blocks.
|
|
46
|
+
- `contexts/product-context.tsx` — `accentOverrideActive`, theme class on `<html>`.
|
|
47
|
+
- **`docs/shell-surface-elevation-pattern.md`**
|
|
43
48
|
- `components/question-bank-secondary-nav.tsx` + `lib/question-bank-nav.ts`.
|
|
44
49
|
- **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`**.
|
|
@@ -18,6 +18,8 @@ This document describes how list pages combine **views**, **toolbar** behavior,
|
|
|
18
18
|
| **Team page (primary template)** | `TeamClient` = `ListPageTemplate` + `KeyMetrics` + `TeamPageHeader` + `TeamTable` (same composition as `DataListClient`) | `components/team-client.tsx`, `lib/mock/team-kpi.ts` |
|
|
19
19
|
| **Team roster** | `TeamTable` — `DataTable` + `useTableState` + `TablePropertiesDrawer`; list/board/dashboard read **`tableState.rows`** | `components/team-table.tsx` |
|
|
20
20
|
| **Dashboard view (list tab)** | **`KeyMetrics`** (`variant="flat"` or `"card"`) — same KPI system as the template metrics strip; **do not** add ad-hoc `Card` grids for entity summaries | `TeamTable` dashboard branch, `lib/mock/team-kpi.ts` |
|
|
21
|
+
| **List hub metrics strip** | **`KeyMetrics variant="flat"`** — transparent cells, OKLCH brand glow only, border hairlines (**no** grey panel) | **`docs/kpi-flat-band-pattern.md`**, Placements / Team / Question bank clients |
|
|
22
|
+
| **Secondary panel chrome** | **`--secondary-panel-bg`** on **`NestedSecondaryPanelShell`** (lighter than sidebar, follows active product) | **`docs/shell-surface-elevation-pattern.md`**, Question bank |
|
|
21
23
|
| **Export** | `ExportDrawer` | `ListPageTemplate` export props; `DataListClient` |
|
|
22
24
|
| **View body layout** (gutter + centered max-width for folder / icon / panel-style content) | **`ListPageViewFrame`** (`components/data-views/list-page-view-frame.tsx`, re-exported from `components/data-views`) | **`FolderGridView`** (uses the frame); **`QuestionBankOsFolderView`** — see **`AGENTS.md` §4.5** |
|
|
23
25
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# KPI flat band (`KeyMetrics` `variant="flat"`)
|
|
2
|
+
|
|
3
|
+
> **Component:** `components/key-metrics.tsx` — **`flatMetricsHairlineClass`**, **`flatBandStyle`**.
|
|
4
|
+
> **Tokens:** `app/globals.css` — `--key-metrics-flat-*`.
|
|
5
|
+
> **Cursor:** `.cursor/rules/exxat-kpi-flat-band.mdc` · `.cursor/skills/exxat-kpi-flat-band/SKILL.md`
|
|
6
|
+
> **Related:** `docs/kpi-strip-max-four-pattern.md`, `docs/kpi-trend-pattern.md`
|
|
7
|
+
|
|
8
|
+
## Intent
|
|
9
|
+
|
|
10
|
+
List hubs and the main dashboard mix view use **`KeyMetrics variant="flat"`** as a **metrics strip without a surface**: users see KPI copy and deltas on the **page canvas**, with a **brand-colored glow** under the band only. This is **not** a card, tinted panel, or `gap-px` grid fill.
|
|
11
|
+
|
|
12
|
+
## MUST
|
|
13
|
+
|
|
14
|
+
1. **No band surface** — The `<section>` background is **only** `var(--key-metrics-flat-band-radial)`. **Do not** stack `--key-metrics-flat-band-linear`, opaque gradients, or `box-shadow` fills that read as a grey/lavender box.
|
|
15
|
+
2. **Transparent cells** — `metricsCellSurfaceClassName` is **`bg-transparent`** for `variant="flat"`. **Do not** use `bg-background`, `bg-card`, or `gap-px` + `bg-border` / `bg-foreground/*` on the grid (that paints tile surfaces).
|
|
16
|
+
3. **Hairlines = borders only** — Use **`flatMetricsHairlineClass(itemCount, metricsHalfWidthLayout)`** in `key-metrics.tsx`:
|
|
17
|
+
- **2 tiles:** `border-r` on the first cell only.
|
|
18
|
+
- **4 tiles, wide strip (default):** `border-r` on cells 1–3 (verticals between all columns); **no** horizontal rule.
|
|
19
|
+
- **4 tiles, narrow `@container` (< 30rem, 2×2 grid):** odd-column `border-r` + `border-b` on the top row only (via `@[max-width:29.99rem]` overrides).
|
|
20
|
+
4. **Divider color (OKLCH)** — `--key-metrics-flat-divider: color-mix(in oklch, var(--sidebar-border) 55%, transparent)`; apply on children with `[&>*]:border-[color:var(--key-metrics-flat-divider)]`. Dividers follow **active product** hue (`--sidebar-border`), not neutral grey alone.
|
|
21
|
+
5. **Glow (OKLCH)** — Radial stops use `color-mix(in oklch, var(--brand-color) …%, transparent)` so **Exxat One / Prism / Assessment / `theme-custom`** each tint correctly. **Do not** hardcode rose/indigo literals on theme blocks unless documenting a one-off.
|
|
22
|
+
6. **List page usage** — Prefer **`showHeader={false}`**, **`metricsSingleRow`** when four KPIs share one row; pass **`insight`** only when the insight rail is product-required (same row uses `lg:grid-cols-[3fr_2fr]`).
|
|
23
|
+
7. **Cap at four tiles** — See **`docs/kpi-strip-max-four-pattern.md`**.
|
|
24
|
+
|
|
25
|
+
## MUST NOT
|
|
26
|
+
|
|
27
|
+
- Add **`--key-metrics-flat-band-linear`** back into `flatBandStyle` or hub inline styles (e.g. question-bank hub hero).
|
|
28
|
+
- Use **`variant="card"`** on **`ListPageTemplate`** metrics when the design calls for a **flat strip** on the page background.
|
|
29
|
+
- Duplicate KPI numbers in ad-hoc **`Card`** grids on the same hub.
|
|
30
|
+
- Set **`variant="mutedSuffix"`** on product wordmarks to grey out the **suffix** in dark mode — suffix stays **Exxat pink** (`wordmarkColor`); see **`lib/product-brand.ts`**.
|
|
31
|
+
|
|
32
|
+
## Tokens (`app/globals.css`)
|
|
33
|
+
|
|
34
|
+
| Token | Role |
|
|
35
|
+
|--------|------|
|
|
36
|
+
| `--key-metrics-flat-band-radial` | Bottom brand glow (only layer on flat `<section>`) |
|
|
37
|
+
| `--key-metrics-flat-band-shadow` | **`none`** for flat band (no faux surface lift) |
|
|
38
|
+
| `--key-metrics-flat-cell-bg` | **`transparent`** |
|
|
39
|
+
| `--key-metrics-flat-divider` | OKLCH hairline between cells |
|
|
40
|
+
|
|
41
|
+
Dark mode (`.dark`): same rules — transparent cells, radial glow only, no linear fill to `--background`.
|
|
42
|
+
|
|
43
|
+
## Reference implementations
|
|
44
|
+
|
|
45
|
+
- `components/question-bank-client.tsx` — `KeyMetrics variant="flat" metricsSingleRow`
|
|
46
|
+
- `components/dashboard-tabs.tsx` — mix view flat band + insight
|
|
47
|
+
- `components/placements-client.tsx`, `team-client.tsx`, `compliance-client.tsx` — list hub metrics slot
|
|
48
|
+
|
|
49
|
+
## Insight rail (flat + side-by-side)
|
|
50
|
+
|
|
51
|
+
When **`insight`** is shown beside KPIs, the insight **`Card`** may keep its own surface; the **KPI grid** stays transparent. **Do not** add `lg:border-l` on the insight column for flat band — the insight card ring is the separator (`key-metrics.tsx`).
|
|
52
|
+
|
|
53
|
+
## See also
|
|
54
|
+
|
|
55
|
+
- **`docs/kpi-strip-max-four-pattern.md`**
|
|
56
|
+
- **`docs/kpi-trend-pattern.md`**
|
|
57
|
+
- **`docs/shell-surface-elevation-pattern.md`** — sidebar / secondary panel / page stack
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Shell surface elevation (sidebar · secondary panel · page)
|
|
2
|
+
|
|
3
|
+
> **Tokens:** `app/globals.css` — `--sidebar`, `--secondary-panel-bg`, `--background`, `--brand-tint*`.
|
|
4
|
+
> **Shell:** `components/templates/nested-secondary-panel-shell.tsx` — `bg-[var(--secondary-panel-bg)]`.
|
|
5
|
+
> **Cursor:** `.cursor/rules/exxat-primary-nav-secondary-panel.mdc` · `.cursor/skills/exxat-primary-nav-secondary-panel/SKILL.md`
|
|
6
|
+
|
|
7
|
+
## Stack (back → front)
|
|
8
|
+
|
|
9
|
+
| Level | Surface | Token / class | Notes |
|
|
10
|
+
|-------|---------|---------------|--------|
|
|
11
|
+
| **0** | Primary icon rail + app chrome | `--sidebar` (= `--brand-tint` on light product themes) | Darkest brand wash in the shell |
|
|
12
|
+
| **1** | Nested secondary panel (Library, etc.) | `--secondary-panel-bg` | **Lighter** than level 0; **same product hue** |
|
|
13
|
+
| **2** | Main page / inset content | `--background` | Lightest (white canvas light; dark charcoal dark) |
|
|
14
|
+
|
|
15
|
+
**MUST** derive secondary panel fill from **`--brand-tint` / `--brand-tint-light`**, not a fixed rose or neutral grey. When the user selects **Exxat One**, both levels use **indigo hue ~286**; **Prism** uses **rose ~342**; **`theme-custom`** follows `--custom-product-brand-color` via `ProductProvider`.
|
|
16
|
+
|
|
17
|
+
## OKLCH formulas (light)
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
--sidebar: var(--brand-tint);
|
|
21
|
+
--secondary-panel-bg: color-mix(in oklch, var(--background) 40%, var(--brand-tint-light) 60%);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## OKLCH formulas (dark)
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
--secondary-panel-bg: color-mix(in oklch, var(--card) 32%, var(--brand-tint) 68%);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Per-product **dark** theme blocks (`.theme-one.dark`, `.theme-prism.dark`, …) set **`--brand-tint-light`** where needed so mixes stay on-hue.
|
|
31
|
+
|
|
32
|
+
## Implementation
|
|
33
|
+
|
|
34
|
+
- **`NestedSecondaryPanelShell`** — `bg-[var(--secondary-panel-bg)]`, `ring-sidebar-border` (not generic `ring-border` alone).
|
|
35
|
+
- **Do not** set secondary panel to `bg-sidebar` (same as level 0 — loses elevation).
|
|
36
|
+
- **Do not** use `color-mix(… var(--sidebar) …)` without brand tokens if it drifts from active product theme.
|
|
37
|
+
|
|
38
|
+
## Product theme classes
|
|
39
|
+
|
|
40
|
+
- **`theme-one`** / **`theme-prism`** / **`theme-assessment`** — built-in OKLCH brand scales in `globals.css`.
|
|
41
|
+
- **`theme-custom`** — when user picks an accent in Settings; driven by `--custom-product-brand-color`.
|
|
42
|
+
- **`ProductProvider`** — applies `theme-one` vs `theme-prism` vs `theme-custom`; accent override only when it **differs** from the product default (see `accentOverrideActive` in `contexts/product-context.tsx`).
|
|
43
|
+
|
|
44
|
+
## Logo vs chrome
|
|
45
|
+
|
|
46
|
+
- **Chrome** (sidebar, secondary panel, KPI glow) follows **`--brand-tint` / `--brand-color`** per product.
|
|
47
|
+
- **Logo art** (mark + suffix) stays **Exxat pink** via `wordmarkColor` / `markGradient` in `lib/product-brand.ts` — recolouring a product in Settings changes **theme accent**, not corporate logo pink.
|
|
48
|
+
|
|
49
|
+
## See also
|
|
50
|
+
|
|
51
|
+
- **`docs/kpi-flat-band-pattern.md`** — flat KPI strip uses brand glow only, no surface
|
|
52
|
+
- **`apps/web/AGENTS.md` §4.6** — secondary panel wiring
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exxatdesignux/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
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": {
|
|
@@ -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)) => {
|
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 */
|
|
@@ -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) |
|
package/template/AGENTS.md
CHANGED
|
@@ -33,9 +33,11 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
|
|
|
33
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
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
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**
|
|
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.
|
|
37
39
|
|
|
38
|
-
**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).
|
|
39
41
|
|
|
40
42
|
---
|
|
41
43
|
|
|
@@ -165,6 +167,8 @@ Thread **`view`** and **`onViewChange`** from the **client** → **table / toolb
|
|
|
165
167
|
|
|
166
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`**.
|
|
167
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
|
+
|
|
168
172
|
**Cursor rule (panel wiring):** **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. **Icons in panel:** **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
|
|
169
173
|
|
|
170
174
|
### 4.7 Collaboration & access (shared hubs)
|
|
@@ -510,6 +514,8 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
|
|
|
510
514
|
- **Drawer vs dialog (same route):** `docs/drawer-vs-dialog-pattern.md` — **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**
|
|
511
515
|
- **Cards vs table rows:** `docs/card-vs-rows-pattern.md` — **`.cursor/rules/exxat-card-vs-list-rows.mdc`**
|
|
512
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`
|
|
513
519
|
- **KPI deltas & trend arrows:** `docs/kpi-trend-pattern.md` (`MetricItem.trendPolarity`, `KeyMetrics`, chart mini-metrics)
|
|
514
520
|
- **Global command palette (⌘K):** `docs/command-menu-pattern.md`
|
|
515
521
|
- **No toast / snackbars:** **§6.5**, root **`.cursor/rules/exxat-no-toast.mdc`**
|
|
@@ -574,7 +580,8 @@ Copy and complete when implementing or reviewing:
|
|
|
574
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.
|
|
575
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`**.
|
|
576
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`**.
|
|
577
|
-
- [ ] **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`**.
|
|
578
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`**.
|
|
579
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`**.
|
|
580
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`**.
|
|
@@ -583,4 +590,4 @@ Copy and complete when implementing or reviewing:
|
|
|
583
590
|
|
|
584
591
|
---
|
|
585
592
|
|
|
586
|
-
*Last updated: monospace system IDs
|
|
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.*
|
|
@@ -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 />
|
|
@@ -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
|
+
}
|