@exxatdesignux/ui 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/consumer-extras/AGENTS.md +76 -0
  3. package/consumer-extras/README.md +5 -1
  4. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
  5. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
  9. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +10 -3
  10. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  11. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  12. package/consumer-extras/patterns/data-views-pattern.md +42 -3
  13. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  14. package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
  15. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
  16. package/package.json +2 -1
  17. package/src/components/ui/button-group.tsx +81 -0
  18. package/src/components/ui/button.tsx +4 -4
  19. package/src/components/ui/sidebar.tsx +2 -2
  20. package/src/globals.css +7 -1807
  21. package/src/theme.css +10 -1126
  22. package/src/tokens/README.md +15 -0
  23. package/src/tokens/base.css +337 -0
  24. package/src/tokens/high-contrast.css +1195 -0
  25. package/src/tokens/layers.css +224 -0
  26. package/src/tokens/tailwind-bridge.css +118 -0
  27. package/src/tokens/themes.css +201 -0
  28. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
  29. package/template/AGENTS.md +66 -21
  30. package/template/app/(app)/dashboard/loading.tsx +3 -15
  31. package/template/app/(app)/dashboard/page.tsx +2 -14
  32. package/template/app/(app)/data-list/layout.tsx +43 -0
  33. package/template/app/(app)/data-list/page.tsx +2 -2
  34. package/template/app/(app)/error.tsx +22 -6
  35. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  36. package/template/app/(app)/examples/page.tsx +1 -0
  37. package/template/app/(app)/layout.tsx +13 -6
  38. package/template/app/(app)/loading.tsx +1 -18
  39. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  40. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  41. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  42. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  43. package/template/app/(app)/question-bank/page.tsx +2 -1
  44. package/template/app/(app)/settings/page.tsx +4 -5
  45. package/template/app/global-error.tsx +63 -0
  46. package/template/app/globals.css +7 -1934
  47. package/template/app/layout.tsx +2 -0
  48. package/template/components/app-route-loading.tsx +14 -0
  49. package/template/components/app-sidebar.tsx +71 -55
  50. package/template/components/data-table/index.tsx +31 -67
  51. package/template/components/data-table/use-table-state.ts +33 -6
  52. package/template/components/data-views/index.ts +37 -9
  53. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  54. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  55. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  56. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  57. package/template/components/dev-chunk-load-recovery.tsx +41 -0
  58. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  59. package/template/components/exxat-product-logo.tsx +2 -6
  60. package/template/components/key-metrics.tsx +54 -22
  61. package/template/components/list-hub-board-view.tsx +68 -0
  62. package/template/components/list-hub-client.tsx +186 -0
  63. package/template/components/list-hub-list-view.tsx +36 -0
  64. package/template/components/list-hub-panel-activator.tsx +8 -0
  65. package/template/components/list-hub-secondary-nav.tsx +121 -0
  66. package/template/components/list-hub-table.tsx +336 -0
  67. package/template/components/new-question-composer.tsx +6 -24
  68. package/template/components/product-switcher.tsx +5 -5
  69. package/template/components/product-wordmark.tsx +4 -7
  70. package/template/components/question-bank-client.tsx +4 -1
  71. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  72. package/template/components/question-bank-hub-client.tsx +2 -5
  73. package/template/components/question-bank-table.tsx +155 -509
  74. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  75. package/template/components/secondary-panel.tsx +4 -44
  76. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  77. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  78. package/template/components/secondary-panels/registry.tsx +15 -0
  79. package/template/components/settings-appearance-card.tsx +3 -2
  80. package/template/components/settings-client.tsx +59 -15
  81. package/template/components/settings-form-row.tsx +9 -4
  82. package/template/components/sidebar-shell.tsx +2 -1
  83. package/template/components/table-properties/drawer-button.tsx +51 -20
  84. package/template/components/table-properties/drawer.tsx +81 -17
  85. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  86. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  87. package/template/components/templates/list-page.tsx +40 -13
  88. package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
  89. package/template/components/templates/page-loading-shell.tsx +262 -0
  90. package/template/components/ui/button-group.tsx +1 -0
  91. package/template/contexts/product-context.tsx +21 -2
  92. package/template/docs/consumer-app-pattern.md +39 -0
  93. package/template/docs/data-views-pattern.md +42 -3
  94. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  95. package/template/docs/focused-workflow-page-pattern.md +84 -0
  96. package/template/docs/kpi-flat-band-pattern.md +57 -0
  97. package/template/docs/kpi-strip-max-four-pattern.md +1 -0
  98. package/template/docs/shell-surface-elevation-pattern.md +54 -0
  99. package/template/lib/chunk-load-error.ts +13 -0
  100. package/template/lib/command-menu-search-data.ts +11 -27
  101. package/template/lib/conditional-rule-match.ts +87 -22
  102. package/template/lib/data-list-display-options.ts +16 -2
  103. package/template/lib/data-list-view-registry.ts +104 -0
  104. package/template/lib/data-list-view-surface.ts +15 -1
  105. package/template/lib/data-list-view.ts +16 -1
  106. package/template/lib/data-view-dashboard-storage.ts +38 -35
  107. package/template/lib/hub-connected-view-renderers.ts +58 -0
  108. package/template/lib/list-hub-nav.ts +121 -0
  109. package/template/lib/list-hub-supported-views.ts +10 -0
  110. package/template/lib/list-page-table-properties.ts +3 -7
  111. package/template/lib/list-status-badges.ts +4 -97
  112. package/template/lib/mock/list-hub-directory.ts +27 -0
  113. package/template/lib/mock/list-hub-kpi.ts +27 -0
  114. package/template/lib/mock/navigation.tsx +1 -0
  115. package/template/lib/page-loading-variant.ts +40 -0
  116. package/template/lib/question-bank-supported-views.ts +13 -0
  117. package/template/lib/sidebar-state-cookie.ts +9 -0
  118. package/template/lib/table-state-lifecycle.ts +60 -13
  119. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  120. package/template/app/(app)/data-list/new/page.tsx +0 -34
  121. package/template/components/compliance-board-view.tsx +0 -142
  122. package/template/components/compliance-client.tsx +0 -92
  123. package/template/components/compliance-list-view.tsx +0 -54
  124. package/template/components/compliance-page-header.tsx +0 -89
  125. package/template/components/compliance-table.tsx +0 -632
  126. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  127. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  128. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  129. package/template/components/new-placement-back-btn.tsx +0 -28
  130. package/template/components/new-placement-form.tsx +0 -1068
  131. package/template/components/placement-board-card.tsx +0 -262
  132. package/template/components/placement-detail.tsx +0 -438
  133. package/template/components/placements-board-view.tsx +0 -404
  134. package/template/components/placements-client.tsx +0 -252
  135. package/template/components/placements-list-view.tsx +0 -171
  136. package/template/components/placements-page-header.tsx +0 -166
  137. package/template/components/placements-table-cells.test.tsx +0 -22
  138. package/template/components/placements-table-cells.tsx +0 -173
  139. package/template/components/placements-table-columns.tsx +0 -640
  140. package/template/components/placements-table.tsx +0 -1675
  141. package/template/components/rotations-empty-state.tsx +0 -50
  142. package/template/components/rotations-panel-activator.tsx +0 -8
  143. package/template/components/sites-all-client.tsx +0 -154
  144. package/template/components/sites-board-view.tsx +0 -67
  145. package/template/components/sites-list-view.tsx +0 -42
  146. package/template/components/sites-table.tsx +0 -402
  147. package/template/components/team-board-view.tsx +0 -122
  148. package/template/components/team-client.tsx +0 -100
  149. package/template/components/team-list-view.tsx +0 -59
  150. package/template/components/team-page-header.tsx +0 -92
  151. package/template/components/team-table.tsx +0 -714
  152. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  153. package/template/lib/mock/compliance-kpi.ts +0 -61
  154. package/template/lib/mock/compliance.ts +0 -146
  155. package/template/lib/mock/placements-kpi.ts +0 -134
  156. package/template/lib/mock/placements.ts +0 -183
  157. package/template/lib/mock/sites-directory.ts +0 -16
  158. package/template/lib/mock/sites-kpi.ts +0 -25
  159. package/template/lib/mock/team-kpi.ts +0 -60
  160. package/template/lib/mock/team.ts +0 -118
  161. package/template/lib/placement-board-card-layout.ts +0 -79
  162. package/template/lib/placement-lifecycle.ts +0 -5
@@ -0,0 +1,84 @@
1
+ # Focused workflow page (dedicated routes)
2
+
3
+ > **Related:** **`AGENTS.md` §6.4** (page vs drawer vs dialog), **§14** (AI checklist), **`docs/drawer-vs-dialog-pattern.md`**, **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-focused-workflow-page.mdc`**, **`.cursor/skills/exxat-focused-workflow-page/SKILL.md`**.
4
+
5
+ ## Intent
6
+
7
+ Use **`FocusedWorkflowPageTemplate`** for **large or multi-step work** on its **own route** — create/edit forms, wizards, and sectioned settings. The shell is **narrower** than list hubs and **does not** use Miller-column / split-panel explorers.
8
+
9
+ | Surface | Use instead |
10
+ | --- | --- |
11
+ | Browsable record hubs (table, board, dashboard tabs) | **`PrimaryPageTemplate`** + **`ListPageTemplate`** |
12
+ | Finder / folder columns / split hub chrome | **`ListPageSplitHubChrome`**, **`ListPageFolderColumnsPanel`** |
13
+ | Quick properties or export beside a grid | **Drawer** (`TablePropertiesDrawer`, `ExportDrawer`) |
14
+ | Blocking confirm on the same route | **Dialog** |
15
+
16
+ ## Surface matrix (§6.4)
17
+
18
+ | Need | Drawer | Dialog | **Focused workflow route** |
19
+ | --- | --- | --- | --- |
20
+ | Keep hub visible while acting | Yes | No | No |
21
+ | Own URL / bookmark / history | Rare | No | **Yes** |
22
+ | Multi-step wizard | Cramped | No | **Yes** |
23
+ | Sectioned settings (left nav) | Awkward | No | **Yes** |
24
+ | Short delete confirm | No | **Yes** | Overkill |
25
+
26
+ ## Shell
27
+
28
+ **`FocusedWorkflowPageTemplate`** (`components/templates/focused-workflow-page-template.tsx`):
29
+
30
+ - **`SidebarInset`** + **`SiteHeader`** (breadcrumb back link + title).
31
+ - Centered column: **`max-w-3xl` / `max-w-4xl` / `max-w-5xl`** via **`maxWidth`** (`md` | `lg` | `xl`).
32
+ - Default padding: **`FOCUSED_WORKFLOW_CONTENT_PADDING_CLASS`**.
33
+
34
+ Optional **`beforeSiteHeader`** (e.g. **`SidebarAutoCollapse`** on long forms).
35
+
36
+ ## Body layouts
37
+
38
+ Import from **`components/templates/focused-workflow-layouts.tsx`**:
39
+
40
+ | Layout | When |
41
+ | --- | --- |
42
+ | **`FocusedWorkflowSingleColumn`** | Default stack — header, form sections, footer actions (e.g. question authoring). |
43
+ | **`FocusedWorkflowStepForm`** + **`FocusedWorkflowWizardFooter`** | Multi-step wizard with progress list and sticky footer (placement-style flows). |
44
+ | **`FocusedWorkflowSidebarSections`** | Sectioned form with **left nav rail** (settings-style); put **`id`** on each `<section>` matching **`sections[].id`**. |
45
+ | **`FocusedWorkflowEmptyState`** | Placeholder / not-yet-configured route body. |
46
+ | **`FocusedWorkflowActionFooter`** | Single-step Cancel (Esc) + primary (Enter) without step chrome. |
47
+
48
+ Keyboard: wizard and action footers pair **`Shortcut`** with inline **`<Kbd variant="bare">`** per **`.cursor/rules/exxat-kbd-shortcuts.mdc`**.
49
+
50
+ ## Golden references
51
+
52
+ | Route | Variant |
53
+ | --- | --- |
54
+ | **`/question-bank/new`** | Shell + **`FocusedWorkflowSingleColumn`** + domain composer |
55
+ | **`/settings`** | Shell (`maxWidth="lg"`) + **`FocusedWorkflowSidebarSections`** |
56
+ | **`/examples/focused-workflow`** | Showcase: empty, steps, sidebar (toggle) |
57
+
58
+ ## Wiring checklist (implementers)
59
+
60
+ 1. **Route** under **`app/(app)/…/page.tsx`** — thin server page; heavy UI in a **client** component.
61
+ 2. **`siteHeader`**: **`back`** or **`breadcrumbs`** + **`title`**; avoid duplicating the trail in the body.
62
+ 3. Pick **`maxWidth`**: **`md`** for simple forms, **`lg`** for settings / wide fields.
63
+ 4. Choose **one** body layout; do **not** nest Miller columns or **`ListPageTemplate`** view tabs inside this shell.
64
+ 5. Domain logic stays in **`*-composer.tsx`** / **`*-client.tsx`**; templates stay generic **`FocusedWorkflow*`**.
65
+ 6. Run **§14** in **`AGENTS.md`** when reviewing.
66
+
67
+ ## AI execution checklist (copy for PRs)
68
+
69
+ - [ ] **`FocusedWorkflowPageTemplate`** on the route — not ad-hoc **`SidebarInset`** / list-hub shell.
70
+ - [ ] Correct body variant: **single column** | **step form** | **sidebar sections** | **empty**.
71
+ - [ ] Wizard/action footers use **`Shortcut`** + bare **`Kbd`** in buttons.
72
+ - [ ] **No** list-hub view tabs, **no** folder-column explorer inside the page.
73
+ - [ ] Template/component names remain **generic** (not tied to one entity).
74
+ - [ ] **§6.5** — no toast for product feedback.
75
+
76
+ ## Pair with
77
+
78
+ - **`exxat-page-vs-drawer.mdc`**, **`exxat-drawer-vs-dialog.mdc`**, **`exxat-kbd-shortcuts.mdc`**
79
+ - **`exxat-reuse-before-custom.mdc`** — extend **`focused-workflow-layouts.tsx`** before forking a second shell
80
+
81
+ ## See also
82
+
83
+ - **`components/examples/focused-workflow-showcase.tsx`**
84
+ - **`packages/ui/consumer-extras/patterns/focused-workflow-page-pattern.md`** (npm consumers)
@@ -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` (&lt; 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,54 @@
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-secondary-panel-bg` + `[data-slot="secondary-panel"]` rule in `globals.css`.
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(--background) 32%, var(--sidebar-accent) 68%);
28
+ ```
29
+
30
+ **Do not** mix with light-mode **`--brand-tint`** in dark — product theme classes keep a light **`--brand-tint`** for logos/KPI glow; the secondary rail must use **`--sidebar-accent`** so the Library panel stays on-hue and dark.
31
+
32
+ Per-product **dark** theme blocks (`.theme-one.dark`, `.theme-prism.dark`, …) set **`--brand-tint-light`** where needed so mixes stay on-hue.
33
+
34
+ ## Implementation
35
+
36
+ - **`NestedSecondaryPanelShell`** — `bg-secondary-panel-bg` (token + `[data-slot="secondary-panel"][data-state="open"]` in `globals.css`), `border-sidebar-border` (not generic `ring-border` alone).
37
+ - **Do not** set secondary panel to `bg-sidebar` (same as level 0 — loses elevation).
38
+ - **Do not** use `color-mix(… var(--sidebar) …)` without brand tokens if it drifts from active product theme.
39
+
40
+ ## Product theme classes
41
+
42
+ - **`theme-one`** / **`theme-prism`** / **`theme-assessment`** — built-in OKLCH brand scales in `globals.css`.
43
+ - **`theme-custom`** — when user picks an accent in Settings; driven by `--custom-product-brand-color`.
44
+ - **`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`).
45
+
46
+ ## Logo vs chrome
47
+
48
+ - **Chrome** (sidebar, secondary panel, KPI glow) follows **`--brand-tint` / `--brand-color`** per product.
49
+ - **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.
50
+
51
+ ## See also
52
+
53
+ - **`docs/kpi-flat-band-pattern.md`** — flat KPI strip uses brand glow only, no surface
54
+ - **`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.17",
3
+ "version": "0.2.19",
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": {
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "./globals.css": "./src/globals.css",
29
29
  "./theme.css": "./src/theme.css",
30
+ "./tokens/*": "./src/tokens/*",
30
31
  "./components/*": {
31
32
  "types": "./src/components/ui/*.tsx",
32
33
  "import": "./src/components/ui/*.tsx",
@@ -0,0 +1,81 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+ import { Slot } from "radix-ui"
6
+
7
+ import { cn } from "../../lib/utils"
8
+ import { Separator } from "./separator"
9
+
10
+ const buttonGroupVariants = cva(
11
+ "group/button-group flex w-fit items-stretch overflow-hidden rounded-md *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
12
+ {
13
+ variants: {
14
+ orientation: {
15
+ horizontal:
16
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md!",
17
+ vertical:
18
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md!",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ orientation: "horizontal",
23
+ },
24
+ },
25
+ )
26
+
27
+ function ButtonGroup({
28
+ className,
29
+ orientation,
30
+ ...props
31
+ }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
32
+ return (
33
+ <div
34
+ role="group"
35
+ data-slot="button-group"
36
+ data-orientation={orientation}
37
+ className={cn(buttonGroupVariants({ orientation }), className)}
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
43
+ function ButtonGroupText({
44
+ className,
45
+ asChild = false,
46
+ ...props
47
+ }: React.ComponentProps<"div"> & {
48
+ asChild?: boolean
49
+ }) {
50
+ const Comp = asChild ? Slot.Root : "div"
51
+
52
+ return (
53
+ <Comp
54
+ className={cn(
55
+ "flex items-center gap-2 rounded-lg border bg-muted px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
56
+ className,
57
+ )}
58
+ {...props}
59
+ />
60
+ )
61
+ }
62
+
63
+ function ButtonGroupSeparator({
64
+ className,
65
+ orientation = "vertical",
66
+ ...props
67
+ }: React.ComponentProps<typeof Separator>) {
68
+ return (
69
+ <Separator
70
+ data-slot="button-group-separator"
71
+ orientation={orientation}
72
+ className={cn(
73
+ "relative self-stretch bg-input data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:h-auto data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto",
74
+ className,
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants }
@@ -23,14 +23,14 @@ const buttonVariants = cva(
23
23
  size: {
24
24
  default:
25
25
  "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pe-2.5 has-data-[icon=inline-start]:ps-2.5",
26
- xs: "h-6 gap-1 px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
27
- sm: "h-8 gap-1 px-3 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2 [&_svg:not([class*='size-'])]:size-3.5",
26
+ xs: "h-6 gap-1 px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ sm: "h-8 gap-1 px-3 text-[0.8rem] in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2 [&_svg:not([class*='size-'])]:size-3.5",
28
28
  lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pe-3.5 has-data-[icon=inline-start]:ps-3.5",
29
29
  icon: "size-9",
30
30
  "icon-xs":
31
- "size-6 in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
31
+ "size-6 in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
32
32
  "icon-sm":
33
- "size-8 in-data-[slot=button-group]:rounded-lg",
33
+ "size-8 in-data-[slot=button-group]:rounded-md",
34
34
  "icon-lg": "size-10",
35
35
  },
36
36
  },
@@ -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)) => {