@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.
- package/CHANGELOG.md +30 -0
- package/consumer-extras/AGENTS.md +76 -0
- package/consumer-extras/README.md +5 -1
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
- package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +22 -7
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- 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 +10 -3
- package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
- package/consumer-extras/patterns/data-views-pattern.md +42 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +54 -0
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/components/ui/sidebar.tsx +2 -2
- package/src/globals.css +7 -1807
- package/src/theme.css +10 -1126
- package/src/tokens/README.md +15 -0
- package/src/tokens/base.css +337 -0
- package/src/tokens/high-contrast.css +1195 -0
- package/src/tokens/layers.css +224 -0
- package/src/tokens/tailwind-bridge.css +118 -0
- package/src/tokens/themes.css +201 -0
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/AGENTS.md +66 -21
- package/template/app/(app)/dashboard/loading.tsx +3 -15
- package/template/app/(app)/dashboard/page.tsx +2 -14
- package/template/app/(app)/data-list/layout.tsx +43 -0
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/error.tsx +22 -6
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/layout.tsx +13 -6
- package/template/app/(app)/loading.tsx +1 -18
- package/template/app/(app)/question-bank/find/page.tsx +2 -1
- package/template/app/(app)/question-bank/library/page.tsx +2 -1
- package/template/app/(app)/question-bank/list/page.tsx +2 -1
- package/template/app/(app)/question-bank/new/page.tsx +15 -23
- package/template/app/(app)/question-bank/page.tsx +2 -1
- package/template/app/(app)/settings/page.tsx +4 -5
- package/template/app/global-error.tsx +63 -0
- package/template/app/globals.css +7 -1934
- package/template/app/layout.tsx +2 -0
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +71 -55
- package/template/components/data-table/index.tsx +31 -67
- package/template/components/data-table/use-table-state.ts +33 -6
- package/template/components/data-views/index.ts +37 -9
- package/template/components/data-views/list-page-calendar-view.tsx +593 -0
- package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
- package/template/components/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/exxat-product-logo.tsx +2 -6
- package/template/components/key-metrics.tsx +54 -22
- package/template/components/list-hub-board-view.tsx +68 -0
- package/template/components/list-hub-client.tsx +186 -0
- package/template/components/list-hub-list-view.tsx +36 -0
- package/template/components/list-hub-panel-activator.tsx +8 -0
- package/template/components/list-hub-secondary-nav.tsx +121 -0
- package/template/components/list-hub-table.tsx +336 -0
- package/template/components/new-question-composer.tsx +6 -24
- package/template/components/product-switcher.tsx +5 -5
- package/template/components/product-wordmark.tsx +4 -7
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-folder-columns-panel.tsx +104 -0
- package/template/components/question-bank-hub-client.tsx +2 -5
- package/template/components/question-bank-table.tsx +155 -509
- package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
- package/template/components/secondary-panel.tsx +4 -44
- package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
- package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
- package/template/components/secondary-panels/registry.tsx +15 -0
- package/template/components/settings-appearance-card.tsx +3 -2
- package/template/components/settings-client.tsx +59 -15
- package/template/components/settings-form-row.tsx +9 -4
- package/template/components/sidebar-shell.tsx +2 -1
- package/template/components/table-properties/drawer-button.tsx +51 -20
- package/template/components/table-properties/drawer.tsx +81 -17
- package/template/components/templates/focused-workflow-layouts.tsx +448 -0
- package/template/components/templates/focused-workflow-page-template.tsx +69 -0
- package/template/components/templates/list-page.tsx +40 -13
- package/template/components/templates/nested-secondary-panel-shell.tsx +3 -2
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/contexts/product-context.tsx +21 -2
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +42 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -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 +54 -0
- package/template/lib/chunk-load-error.ts +13 -0
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/conditional-rule-match.ts +87 -22
- package/template/lib/data-list-display-options.ts +16 -2
- package/template/lib/data-list-view-registry.ts +104 -0
- package/template/lib/data-list-view-surface.ts +15 -1
- package/template/lib/data-list-view.ts +16 -1
- package/template/lib/data-view-dashboard-storage.ts +38 -35
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-nav.ts +121 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +3 -7
- package/template/lib/list-status-badges.ts +4 -97
- package/template/lib/mock/list-hub-directory.ts +27 -0
- package/template/lib/mock/list-hub-kpi.ts +27 -0
- package/template/lib/mock/navigation.tsx +1 -0
- package/template/lib/page-loading-variant.ts +40 -0
- package/template/lib/question-bank-supported-views.ts +13 -0
- package/template/lib/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +60 -13
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -632
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -1068
- package/template/components/placement-board-card.tsx +0 -262
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -404
- package/template/components/placements-client.tsx +0 -252
- package/template/components/placements-list-view.tsx +0 -171
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -640
- package/template/components/placements-table.tsx +0 -1675
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-all-client.tsx +0 -154
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/sites-table.tsx +0 -402
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-list-view.tsx +0 -59
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -714
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -183
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- 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` (< 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 {
|
|
6
|
+
import { LIST_HUB_DIRECTORY } from "@/lib/mock/list-hub-directory"
|
|
7
7
|
|
|
8
8
|
function sampleRowSearchItems(): CommandMenuItem[] {
|
|
9
|
-
return
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
+
}
|