@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
@@ -26,4 +26,5 @@ On **primary list hubs** (`ListPageTemplate` metrics slot) and on **dashboard
26
26
  ## See also
27
27
 
28
28
  - **`docs/kpi-trend-pattern.md`** — deltas, arrows, **`trendPolarity`**.
29
+ - **`docs/kpi-flat-band-pattern.md`** — **`variant="flat"`** presentation (orthogonal to tile count).
29
30
  - **`.cursor/rules/exxat-kpi-max-four.mdc`**, **`.cursor/skills/exxat-kpi-max-four/SKILL.md`**
@@ -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
@@ -0,0 +1,13 @@
1
+ /** Detect stale Turbopack / webpack chunk failures after dev-server rebuilds. */
2
+ export function isChunkLoadError(error: unknown): boolean {
3
+ if (!error || typeof error !== "object") return false
4
+ const err = error as { name?: string; message?: string }
5
+ const name = err.name ?? ""
6
+ const msg = err.message ?? ""
7
+ return (
8
+ name === "ChunkLoadError" ||
9
+ msg.includes("Failed to load chunk") ||
10
+ msg.includes("Loading chunk") ||
11
+ msg.includes("ChunkLoadError")
12
+ )
13
+ }
@@ -3,36 +3,21 @@
3
3
  */
4
4
 
5
5
  import type { CommandMenuGroup, CommandMenuItem } from "@/lib/command-menu-config"
6
- import { ALL_PLACEMENTS } from "@/lib/mock/placements"
6
+ import { LIST_HUB_DIRECTORY } from "@/lib/mock/list-hub-directory"
7
7
 
8
8
  function sampleRowSearchItems(): CommandMenuItem[] {
9
- return ALL_PLACEMENTS.map((p) => {
10
- const nameParts = p.student.trim().split(/\s+/)
11
- return {
12
- id: `sample-row-${p.id}`,
13
- label: `Row ${p.id} — ${p.student}`,
14
- icon: "fa-light fa-table",
15
- href: `/data-list/${p.id}`,
16
- keywords: [
17
- `row ${p.id}`,
18
- p.student,
19
- ...nameParts,
20
- p.program,
21
- p.site,
22
- p.internship,
23
- p.specialization,
24
- p.email,
25
- p.supervisor,
26
- p.status,
27
- p.compliance,
28
- ]
29
- .filter(Boolean)
30
- .join(" "),
31
- }
32
- })
9
+ return LIST_HUB_DIRECTORY.map(row => ({
10
+ id: `sample-row-${row.id}`,
11
+ label: row.title,
12
+ icon: "fa-light fa-calendar-days",
13
+ href: "/data-list",
14
+ keywords: [row.id, row.title, row.category, row.eventDate, "list hub", "calendar"]
15
+ .filter(Boolean)
16
+ .join(" "),
17
+ }))
33
18
  }
34
19
 
35
- /** Built once at module load — avoids remapping all placement rows on every layout render. */
20
+ /** Built once at module load — avoids remapping rows on every layout render. */
36
21
  export const COMMAND_MENU_SEARCH_DATA_GROUPS: CommandMenuGroup[] = [
37
22
  {
38
23
  id: "sample-rows",
@@ -42,7 +27,6 @@ export const COMMAND_MENU_SEARCH_DATA_GROUPS: CommandMenuGroup[] = [
42
27
  },
43
28
  ]
44
29
 
45
- /** Demo rows for the list hub — search-only so the palette stays lightweight on open. */
46
30
  export function getCommandMenuSearchDataGroups(): CommandMenuGroup[] {
47
31
  return COMMAND_MENU_SEARCH_DATA_GROUPS
48
32
  }
@@ -1,32 +1,97 @@
1
- import type { ConditionalRule } from "@/components/table-properties/types"
1
+ import type { ConditionalRule, FilterTextMask } from "@/components/table-properties/types"
2
2
 
3
- /** First matching conditional rule background for a row (same logic as DataTable cells). */
3
+ export type ConditionalColumnHint = {
4
+ key: string
5
+ sortKey?: string
6
+ filter?: { type?: string; textMask?: FilterTextMask }
7
+ }
8
+
9
+ function rowValueForRule<T extends Record<string, unknown>>(
10
+ row: T,
11
+ rule: ConditionalRule,
12
+ columns?: ConditionalColumnHint[],
13
+ ): string {
14
+ const col = columns?.find(c => c.key === rule.fieldKey)
15
+ const dataKey = (col?.sortKey ?? rule.fieldKey) as keyof T
16
+ return String(row[dataKey] ?? "")
17
+ }
18
+
19
+ function ruleHasActiveValues(
20
+ rule: ConditionalRule,
21
+ columns?: ConditionalColumnHint[],
22
+ ): boolean {
23
+ if (rule.values.length === 0) return false
24
+ const col = columns?.find(c => c.key === rule.fieldKey)
25
+ if (col?.filter?.type === "text") return (rule.values[0] ?? "").trim().length > 0
26
+ return true
27
+ }
28
+
29
+ function conditionalTextMatches(
30
+ cellVal: string,
31
+ needle: string,
32
+ op: "contains" | "not_contains",
33
+ textMask: FilterTextMask | undefined,
34
+ ) {
35
+ const v = cellVal.trim()
36
+ const n = needle.trim()
37
+ if (!n) return op === "not_contains"
38
+ if (textMask === "phone" || textMask === "zip") {
39
+ const nd = n.replace(/\D/g, "")
40
+ const hay = v.replace(/\D/g, "")
41
+ if (!nd) return op === "not_contains"
42
+ const hit = hay.includes(nd)
43
+ return op === "contains" ? hit : !hit
44
+ }
45
+ const hit = v.toLowerCase().includes(n.toLowerCase())
46
+ return op === "contains" ? hit : !hit
47
+ }
48
+
49
+ /** Whether a conditional rule matches a row (same logic as DataTable cells). */
50
+ export function conditionalRuleMatchesRow<T extends Record<string, unknown>>(
51
+ row: T,
52
+ rule: ConditionalRule,
53
+ columns?: ConditionalColumnHint[],
54
+ ): boolean {
55
+ if (!ruleHasActiveValues(rule, columns)) return false
56
+ const v = rowValueForRule(row, rule, columns).trim()
57
+ const col = columns?.find(c => c.key === rule.fieldKey)
58
+ const textMask = col?.filter?.type === "text" ? col.filter.textMask : undefined
59
+ switch (rule.operator) {
60
+ case "is":
61
+ return rule.values.includes(v)
62
+ case "is_not":
63
+ return !rule.values.includes(v)
64
+ case "contains":
65
+ return rule.values.some(val => conditionalTextMatches(v, val, "contains", textMask))
66
+ case "not_contains":
67
+ return !rule.values.some(val => conditionalTextMatches(v, val, "contains", textMask))
68
+ default:
69
+ return false
70
+ }
71
+ }
72
+
73
+ /** First matching conditional rule background for a row (list/board row tint). */
4
74
  export function getConditionalRowBackground<T extends Record<string, unknown>>(
5
75
  row: T,
6
76
  rules: ConditionalRule[] | undefined,
77
+ columns?: ConditionalColumnHint[],
7
78
  ): string | undefined {
8
79
  if (!rules?.length) return undefined
9
80
  for (const rule of rules) {
10
- const cellVal = String(row[rule.fieldKey as keyof T] ?? "")
11
- const v = cellVal.trim()
12
- switch (rule.operator) {
13
- case "is":
14
- if (rule.values.length > 0 && rule.values.includes(v)) return rule.bgColor
15
- break
16
- case "is_not":
17
- if (rule.values.length > 0 && !rule.values.includes(v)) return rule.bgColor
18
- break
19
- case "contains":
20
- if (rule.values.length > 0 && rule.values.some(val => v.toLowerCase().includes(val.toLowerCase())))
21
- return rule.bgColor
22
- break
23
- case "not_contains":
24
- if (rule.values.length > 0 && !rule.values.some(val => v.toLowerCase().includes(val.toLowerCase())))
25
- return rule.bgColor
26
- break
27
- default:
28
- break
29
- }
81
+ if (conditionalRuleMatchesRow(row, rule, columns)) return rule.bgColor
30
82
  }
31
83
  return undefined
32
84
  }
85
+
86
+ /** Background for one table cell from conditional rules on that column. */
87
+ export function getConditionalCellBackground<T extends Record<string, unknown>>(
88
+ row: T,
89
+ colKey: string,
90
+ rules: ConditionalRule[] | undefined,
91
+ columns?: ConditionalColumnHint[],
92
+ ): string | undefined {
93
+ if (!rules?.length) return undefined
94
+ const rule = rules.find(r => r.fieldKey === colKey)
95
+ if (!rule || !conditionalRuleMatchesRow(row, rule, columns)) return undefined
96
+ return rule.bgColor
97
+ }
@@ -1,10 +1,13 @@
1
1
  /**
2
- * Display options for Data list (table / board / etc.) — shared across view types
2
+ * Display options for Data list (table / board / calendar / etc.) — shared across view types
3
3
  * so hide/show preferences persist when switching views.
4
4
  */
5
5
 
6
6
  export type BoardLineCount = 1 | 2 | 3
7
7
 
8
+ /** Right-hand calendar body: month grid vs week rows (scroll stack uses the same months). */
9
+ export type CalendarMainView = "month" | "week"
10
+
8
11
  export interface DataListDisplayOptions {
9
12
  /**
10
13
  * Board swimlanes: dataset field (table column key) used to split cards into columns.
@@ -13,7 +16,7 @@ export interface DataListDisplayOptions {
13
16
  boardGroupByColumnKey: string
14
17
  /** Max lines for primary text blocks on board cards */
15
18
  boardLineCount: BoardLineCount
16
- /** Page title block (Placements + subtitle) */
19
+ /** Page title block */
17
20
  showViewTitle: boolean
18
21
  /** Board: phase column titles + descriptions. Table: column header row. */
19
22
  showColumnLabels: boolean
@@ -22,6 +25,10 @@ export interface DataListDisplayOptions {
22
25
  boardNewCardAbove: boolean
23
26
  /** Toolbar search control (table view) */
24
27
  showToolbarSearch: boolean
28
+ /** Calendar: left column — mini month, event list, layout tiles */
29
+ showCalendarSummaryPanel: boolean
30
+ /** Calendar: main scrollable body layout */
31
+ calendarMainView: CalendarMainView
25
32
  }
26
33
 
27
34
  export const DEFAULT_DATA_LIST_DISPLAY_OPTIONS: DataListDisplayOptions = {
@@ -32,4 +39,11 @@ export const DEFAULT_DATA_LIST_DISPLAY_OPTIONS: DataListDisplayOptions = {
32
39
  showBoardColumnCounts: true,
33
40
  boardNewCardAbove: true,
34
41
  showToolbarSearch: true,
42
+ showCalendarSummaryPanel: false,
43
+ calendarMainView: "month",
35
44
  }
45
+
46
+ export const CALENDAR_MAIN_VIEW_TILES = [
47
+ { value: "month" as const, label: "Month", icon: "fa-calendar-days" },
48
+ { value: "week" as const, label: "Week", icon: "fa-calendar-week" },
49
+ ]
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Central registry for list-page view types — labels, render kinds, and hub chrome rules.
3
+ *
4
+ * **Add a new view once here** (plus a body in `components/data-views/`). Hubs declare
5
+ * `supportedViewTypes` on `ListPageTemplate`; table components branch with
6
+ * `getDataListViewRenderKind` + `ListPageConnectedViewBody` (never a dashboard fallback).
7
+ *
8
+ * @see `docs/data-views-pattern.md` — "View registry and connected bodies"
9
+ */
10
+
11
+ import {
12
+ DATA_LIST_VIEW_TILES,
13
+ type DataListViewType,
14
+ dataListViewAddShortcut,
15
+ dataListViewIcon,
16
+ dataListViewLabel,
17
+ } from "@/lib/data-list-view"
18
+ import {
19
+ getDataListViewRenderKind,
20
+ type DataListViewRenderKind,
21
+ } from "@/lib/data-list-view-surface"
22
+
23
+ export interface DataListViewDefinition {
24
+ value: DataListViewType
25
+ label: string
26
+ icon: string
27
+ renderKind: DataListViewRenderKind
28
+ /** `ListPageTemplate` metrics slot above the views toolbar. */
29
+ hubMetricsStrip: boolean
30
+ }
31
+
32
+ const DEFINITIONS: DataListViewDefinition[] = DATA_LIST_VIEW_TILES.map(tile => {
33
+ const renderKind = getDataListViewRenderKind(tile.value)
34
+ const hubMetricsStrip = renderKind !== "calendar-with-toolbar" && renderKind !== "dashboard-with-toolbar"
35
+ return {
36
+ value: tile.value,
37
+ label: tile.label,
38
+ icon: tile.icon,
39
+ renderKind,
40
+ hubMetricsStrip,
41
+ }
42
+ })
43
+
44
+ const BY_VALUE = new Map<DataListViewType, DataListViewDefinition>(
45
+ DEFINITIONS.map(d => [d.value, d]),
46
+ )
47
+
48
+ export const DATA_LIST_VIEW_REGISTRY: readonly DataListViewDefinition[] = DEFINITIONS
49
+
50
+ export function dataListViewDefinition(view: DataListViewType): DataListViewDefinition {
51
+ const def = BY_VALUE.get(view)
52
+ if (!def) {
53
+ throw new Error(`Unknown DataListViewType: ${view}`)
54
+ }
55
+ return def
56
+ }
57
+
58
+ /** `ListPageTemplate` hub KPI strip — false for calendar and dashboard (inline KPIs). */
59
+ export function showsListPageHubMetricsStrip(view: DataListViewType): boolean {
60
+ return dataListViewDefinition(view).hubMetricsStrip
61
+ }
62
+
63
+ /** Tiles for Add view + Properties when a hub only supports a subset of views. */
64
+ export function dataListViewTilesForHub(supported: readonly DataListViewType[]) {
65
+ const allowed = new Set(supported)
66
+ return DATA_LIST_VIEW_REGISTRY.filter(d => allowed.has(d.value)).map(d => ({
67
+ type: d.value,
68
+ label: d.label,
69
+ icon: d.icon,
70
+ }))
71
+ }
72
+
73
+ /** `SelectionTileGrid` options for Properties when a hub supports a subset of views. */
74
+ export function dataListViewSelectionTilesForHub(supported: readonly DataListViewType[]) {
75
+ return dataListViewTilesForHub(supported).map(t => ({
76
+ value: t.type,
77
+ label: t.label,
78
+ icon: t.icon,
79
+ }))
80
+ }
81
+
82
+ /** View types that expose Table Properties (all registered `DataListViewType` values). */
83
+ export const DATA_LIST_SURFACE_VIEW_TYPES: ReadonlySet<DataListViewType> = new Set(
84
+ DATA_LIST_VIEW_REGISTRY.map(d => d.value),
85
+ )
86
+
87
+ export function isDataListSurfaceViewType(viewType: string): viewType is DataListViewType {
88
+ return DATA_LIST_SURFACE_VIEW_TYPES.has(viewType as DataListViewType)
89
+ }
90
+
91
+ export function isDataListViewTypeSupported(
92
+ view: DataListViewType,
93
+ supported: readonly DataListViewType[],
94
+ ): boolean {
95
+ return supported.includes(view)
96
+ }
97
+
98
+ export {
99
+ dataListViewAddShortcut,
100
+ dataListViewIcon,
101
+ dataListViewLabel,
102
+ getDataListViewRenderKind,
103
+ type DataListViewRenderKind,
104
+ }
@@ -11,18 +11,22 @@
11
11
  * | `list` | `DataTableToolbar` + list layout |
12
12
  * | `board` | `DataTableToolbar` + board / kanban |
13
13
  * | `dashboard`| `DataTableToolbar` + KPI (`KeyMetrics`) + optional charts (`ChartCard`, Recharts, etc.) |
14
+ * | `calendar` | `DataTableToolbar` + `ListPageCalendarView` (month grid + day detail) |
14
15
  * | `folder` | `DataTableToolbar` + icon grid (macOS-Finder-style) |
15
16
  * | `panel` | `DataTableToolbar` + resizable split (list / tree column + detail inspector) |
16
17
  */
17
18
 
18
19
  import type { DataListViewType } from "@/lib/data-list-view"
19
20
 
21
+ export { showsListPageHubMetricsStrip } from "@/lib/data-list-view-registry"
22
+
20
23
  /** What to render for the active view tab (routing / branching). */
21
24
  export type DataListViewRenderKind =
22
25
  | "data-table"
23
26
  | "list-with-toolbar"
24
27
  | "board-with-toolbar"
25
28
  | "dashboard-with-toolbar"
29
+ | "calendar-with-toolbar"
26
30
  | "folder-with-toolbar"
27
31
  | "panel-with-toolbar"
28
32
  | "tree-panel-with-toolbar"
@@ -41,6 +45,8 @@ export function getDataListViewRenderKind(view: DataListViewType): DataListViewR
41
45
  return "board-with-toolbar"
42
46
  case "dashboard":
43
47
  return "dashboard-with-toolbar"
48
+ case "calendar":
49
+ return "calendar-with-toolbar"
44
50
  case "folder":
45
51
  return "folder-with-toolbar"
46
52
  case "panel":
@@ -65,5 +71,13 @@ export function usesDashboardSurface(view: DataListViewType): boolean {
65
71
 
66
72
  /** Shared toolbar (search, filters, properties); body differs by view. */
67
73
  export function usesToolbarWithFilteredRows(view: DataListViewType): boolean {
68
- return view === "list" || view === "board" || view === "dashboard" || view === "folder" || view === "panel" || view === "tree-panel"
74
+ return (
75
+ view === "list" ||
76
+ view === "board" ||
77
+ view === "dashboard" ||
78
+ view === "calendar" ||
79
+ view === "folder" ||
80
+ view === "panel" ||
81
+ view === "tree-panel"
82
+ )
69
83
  }
@@ -5,7 +5,15 @@
5
5
  * `dataListViewLabel` / `dataListViewIcon` on every page so Table / List / Board / Dashboard
6
6
  * stay consistent and stay wired to the same `useTableState` dataset (see `docs/data-views-pattern.md`).
7
7
  */
8
- export type DataListViewType = "table" | "list" | "board" | "dashboard" | "folder" | "panel" | "tree-panel"
8
+ export type DataListViewType =
9
+ | "table"
10
+ | "list"
11
+ | "board"
12
+ | "dashboard"
13
+ | "calendar"
14
+ | "folder"
15
+ | "panel"
16
+ | "tree-panel"
9
17
 
10
18
  export const DATA_LIST_VIEW_TILES: readonly {
11
19
  value: DataListViewType
@@ -16,6 +24,7 @@ export const DATA_LIST_VIEW_TILES: readonly {
16
24
  { value: "list", icon: "fa-list", label: "List view" },
17
25
  { value: "board", icon: "fa-table-columns", label: "Board view" },
18
26
  { value: "dashboard", icon: "fa-chart-mixed", label: "Dashboard view" },
27
+ { value: "calendar", icon: "fa-calendar-days", label: "Calendar view" },
19
28
  { value: "folder", icon: "fa-grid-2", label: "Folder view" },
20
29
  { value: "panel", icon: "fa-sidebar", label: "List & details" },
21
30
  { value: "tree-panel", icon: "fa-sitemap", label: "Tree & details" },
@@ -30,3 +39,9 @@ export function dataListViewLabel(view: DataListViewType): string {
30
39
  export function dataListViewIcon(view: DataListViewType): string {
31
40
  return DATA_LIST_VIEW_TILES.find(t => t.value === view)?.icon ?? "fa-table"
32
41
  }
42
+
43
+ /** Add-view menu hint + `<Shortcut>` keys (1–9). Skipped in inputs via `useShortcut`. */
44
+ export function dataListViewAddShortcut(index: number): string | undefined {
45
+ if (index < 0 || index > 8) return undefined
46
+ return String(index + 1)
47
+ }