From 1ba5d5bb9b24226a3a878689d6a023975e13ebf1 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Wed, 3 Jun 2026 02:23:39 +0200 Subject: [PATCH] =?UTF-8?q?docs(plan-2.5):=20closure=20=E2=80=94=20RFC=20s?= =?UTF-8?q?upersession,=20BACKLOG=20landing,=20applyDomAttributes=20dedupe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan 2.5 final phase (P8). Closes the PrimeVue shell-migration workstream. - RFC-WS-PRIMEVUE-PLAN-2-5: added Supersessions section recording the governing-RFC divergences (§4 dark mode ``→`` per AD-2.5-D1; §7.4 workspace sub option A reversed to placeholder after visual review). Added closure summary (phases, ADs, brand-square recipe, suite delta, lessons). Status → COMPLETE. - BACKLOG: landed 8 items surfaced during Plan 2.5 (MOBILE-SHELL-PARITY, WORKSPACE-DROPDOWN-SUB-CONTENT, DENSITY-AWARE-SPACING, TOPBAR-H-VAR- DECLARE, CSP-FONT-SRC-LOCKDOWN, AUTO-IMPORTS-V2-SCAN, PNPM-RESOLUTIONS- ROOT, SHELLUI-STALE-DATA-THEME-CLEANUP). Marked GRADIENT-BRAND- ALIGNMENT as resolved. - useShellUiStore.toggleDensity: removed redundant applyDomAttributes() call (the AppShellV2 watch already covers density changes). Moved the DOM-write assertion to AppShellV2 watcher-coverage specs. Plan 2.5 status: COMPLETE. Co-Authored-By: Claude Opus 4.8 --- .../stores/__tests__/useShellUiStore.spec.ts | 13 +- apps/app/src/stores/useShellUiStore.ts | 14 +- .../component/layouts/AppShellV2.spec.ts | 38 +++++- dev-docs/BACKLOG.md | 124 ++++++++++++++++++ .../RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md | 9 ++ dev-docs/RFC-WS-PRIMEVUE-PLAN-2-5.md | 64 ++++++++- 6 files changed, 248 insertions(+), 14 deletions(-) diff --git a/apps/app/src/stores/__tests__/useShellUiStore.spec.ts b/apps/app/src/stores/__tests__/useShellUiStore.spec.ts index 928bd393..7ac77a97 100644 --- a/apps/app/src/stores/__tests__/useShellUiStore.spec.ts +++ b/apps/app/src/stores/__tests__/useShellUiStore.spec.ts @@ -59,20 +59,19 @@ describe('useShellUiStore', () => { }) // Plan 2.5 P6 Fix 10 — toggleDensity is the binary UI flip exposed by - // the topbar density button. Calls applyDomAttributes() so the - // attribute mutates synchronously (consumers - // like the AppShellV2 watcher would also fire, but the synchronous - // contract is what topbar callers rely on). - it('toggleDensity flips comfortable ⇔ compact and writes data-density', () => { + // the topbar density button. Plan 2.5 P8 dedupe: it is now a pure state + // mutation — the write is owned solely by AppShellV2's + // watch([theme, density]) (the single applyDomAttributes() authority). + // The watcher-coverage assertion lives in + // tests/component/layouts/AppShellV2.spec.ts; here we assert the flip only. + it('toggleDensity flips comfortable ⇔ compact', () => { const s = useShellUiStore() expect(s.density).toBe('comfortable') s.toggleDensity() expect(s.density).toBe('compact') - expect(document.documentElement.getAttribute('data-density')).toBe('compact') s.toggleDensity() expect(s.density).toBe('comfortable') - expect(document.documentElement.getAttribute('data-density')).toBe('comfortable') }) it('toggleDensity from compact returns to comfortable on the second call', () => { diff --git a/apps/app/src/stores/useShellUiStore.ts b/apps/app/src/stores/useShellUiStore.ts index 369e1f3b..e3454fc9 100644 --- a/apps/app/src/stores/useShellUiStore.ts +++ b/apps/app/src/stores/useShellUiStore.ts @@ -46,14 +46,18 @@ export const useShellUiStore = defineStore('shellUi', () => { /** * Plan 2.5 P6 Fix 10: binary UI toggle between the two density extremes - * the topbar exposes. Calls applyDomAttributes() so consumers (PrimeVue - * Aura preset + Tailwind component styles) react to the new - * immediately, mirroring how AppShellV2's watcher - * would update on a setDensity() call but synchronously. + * the topbar exposes. Pure state mutation — the write + * is owned by AppShellV2's `watch([theme, density])`, the single + * applyDomAttributes() authority. Mirrors setTheme(), which has always + * relied on that same watcher. + * + * Plan 2.5 P8 dedupe: this previously also called applyDomAttributes() + * directly, so a density toggle fired it twice (idempotent but redundant — + * once here, once via the watch). The direct call was removed; the watch + * still covers the change. */ function toggleDensity(): void { density.value = density.value === 'compact' ? 'comfortable' : 'compact' - applyDomAttributes() } function applyDomAttributes(): void { diff --git a/apps/app/tests/component/layouts/AppShellV2.spec.ts b/apps/app/tests/component/layouts/AppShellV2.spec.ts index 012db098..5d881ebe 100644 --- a/apps/app/tests/component/layouts/AppShellV2.spec.ts +++ b/apps/app/tests/component/layouts/AppShellV2.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { mount } from '@vue/test-utils' import { createPinia } from 'pinia' import AppShellV2 from '@/layouts/components/AppShellV2.vue' @@ -31,4 +31,40 @@ describe('AppShellV2 (skeleton)', () => { await wrapper.vm.$nextTick() expect(wrapper.find('[data-testid="appshell-v2"]').classes()).toContain('is-collapsed') }) + + // Plan 2.5 P8 dedupe — AppShellV2's watch([theme, density]) is the single + // applyDomAttributes() authority. toggleDensity() (useShellUiStore) no longer + // writes the DOM itself; the watch covers it. These specs lock that contract: + // the density change reaches via the watch, and exactly one + // applyDomAttributes() call fires per toggle (no redundant double-call). + it('watch writes the toggled density to ', async () => { + document.documentElement.removeAttribute('data-density') + + const pinia = createPinia() + const wrapper = mount(AppShellV2, { global: { plugins: [pinia] } }) + const { useShellUiStore } = await import('@/stores/useShellUiStore') + const shell = useShellUiStore() + + // onMounted seeded the comfortable default. + expect(document.documentElement.getAttribute('data-density')).toBe('comfortable') + + shell.toggleDensity() + await wrapper.vm.$nextTick() + expect(document.documentElement.getAttribute('data-density')).toBe('compact') + }) + + it('density toggle fires applyDomAttributes exactly once (via the watch, not toggleDensity)', async () => { + const pinia = createPinia() + const wrapper = mount(AppShellV2, { global: { plugins: [pinia] } }) + const { useShellUiStore } = await import('@/stores/useShellUiStore') + const shell = useShellUiStore() + + // Spy AFTER mount so the onMounted seed call is excluded from the count. + const spy = vi.spyOn(shell, 'applyDomAttributes') + + shell.toggleDensity() + await wrapper.vm.$nextTick() + + expect(spy).toHaveBeenCalledTimes(1) + }) }) diff --git a/dev-docs/BACKLOG.md b/dev-docs/BACKLOG.md index 49efc372..1914532a 100644 --- a/dev-docs/BACKLOG.md +++ b/dev-docs/BACKLOG.md @@ -676,6 +676,130 @@ voor third-party integraties (ticketing, HR, etc.) ## Technische schuld +> Plan 2.5 shell follow-ups (landed at P8 closure, juni 2026). See +> `dev-docs/RFC-WS-PRIMEVUE-PLAN-2-5.md` → Plan 2.5 Closure. + +### MOBILE-SHELL-PARITY — Mobile drawer chrome parity with desktop sidebar + +**Prioriteit:** Middel + +Mobile drawer chrome doesn't match the desktop sidebar polish: logo +placement is incorrect, an extraneous X close button overlaps the drawer, +and the WorkspaceSwitcher is hidden on mobile. A dedicated mobile sprint +after Plan 2.5; deliberately out of scope per Q3. + +**Trigger:** before mobile users onboard / mobile traffic becomes +non-trivial. + +--- + +### WORKSPACE-DROPDOWN-SUB-CONTENT — Real WorkspaceSwitcher sub-line content (type + metrics) + +**Prioriteit:** Middel + +The WorkspaceSwitcher sub line currently shows the placeholder string +'Organisatie' (per AD-2.5-W1 reversal). Real content requires an +`organisations.type` enum + a metrics endpoint (e.g., member count, +festival/event metadata). Needs a small RFC covering: type enum schema, +metrics aggregation, API shape, frontend wiring. + +**Trigger:** when organisation metadata work begins, OR when the +placeholder reads as too empty in production. + +--- + +### DENSITY-AWARE-SPACING — Wire `data-density` to component spacing + +**Prioriteit:** Laag + +`` toggles correctly between `comfortable` and +`compact`, but no component CSS reads the attribute, so the toggle is +visually inert. Plan 4 should pick which spacing surfaces (tables, lists, +cards) respond to density and add the corresponding +`[data-density="compact"]` overrides. + +**Trigger:** Plan 4 template-layer work. + +--- + +### TOPBAR-H-VAR-DECLARE — Declare `--topbar-h` in `:root` (or drop it) + +**Prioriteit:** Laag + +`--topbar-h` is referenced as `var(--topbar-h, 56px)` in component CSS but +never declared in `:root`. Effectively a hardcoded 56px with no override +path. Either declare it in `:root` for real or replace usages with `h-14` +(Tailwind). + +**Trigger:** Plan 4 cleanup, or when topbar height changes. + +--- + +### CSP-FONT-SRC-LOCKDOWN — `Content-Security-Policy: font-src 'self'` lockdown + +**Prioriteit:** Laag + +The P2-followup webfontloader lesson exposed a structural risk: dynamic +font-loading JS can pull from external domains (Google Fonts), bypassing +GDPR/privacy posture. A `Content-Security-Policy: font-src 'self' data:` +directive provides structural defense — even if a stray font-loader is +reintroduced, browsers would block external requests. + +**Trigger:** pre-launch security hardening. + +--- + +### AUTO-IMPORTS-V2-SCAN — `components-v2/` not in the auto-import scan dirs + +**Prioriteit:** Laag + +`vite.config.ts:79-94` `Components({dirs})` doesn't scan `components-v2/`, +so v2 components must be explicitly imported. The P5-followup AppBreadcrumb +auto-import surprise was traced to this. Either add `components-v2/` to the +scan dirs or accept explicit imports (the current trade-off). + +**Trigger:** when explicit imports become tedious, or a new contributor +stumbles. + +--- + +### PNPM-RESOLUTIONS-ROOT — `resolutions` field in app package.json is a no-op + +**Prioriteit:** Laag + +`apps/app/package.json` has a `resolutions` field, but pnpm only honors +resolutions in the workspace root `package.json`. Currently a no-op. Move +to root `package.json` or remove. + +**Trigger:** next dependency conflict requiring a resolution override. + +--- + +### SHELLUI-STALE-DATA-THEME-CLEANUP — Clean up stale legacy `data-theme` on init + +**Prioriteit:** Laag + +`useShellUiStore.applyDomAttributes()` writes `` and +`` but doesn't clean up an old `data-theme` attribute if +it ever existed (from a prior Vuetify-era implementation). Pre-launch this +is a non-issue; for cleanliness, remove the stale attribute on init. + +**Trigger:** cleanup pass before launch, or if anyone hits attribute-related +styling weirdness. + +--- + +### ~~GRADIENT-BRAND-ALIGNMENT — gradient palette off-brand teal anchor~~ ✅ RESOLVED + +~~The `utils/v2/gradient.ts` GRADIENT_PALETTE clustered in the Tailwind +blue-green family with off-brand teal `#0d9488` for the anchor slot.~~ +Resolved in P7-followup-gradient-brand: palette replaced with the +crewli-starter diverse 8-color palette, slot 1 anchored to Crewli +`#0D9394`/`#075F60`. User-avatar fallback also corrected. Regression-spec +locks the brand-teal slot. + +--- + ### TECH-OBSERVER-TEST-CONVERGENCE — Drop `bootstrap_on_org_create` flag once tests converge **Aanleiding:** Session 3 introduceerde `OrganisationObserver` om elke nieuwe diff --git a/dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md b/dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md index e2e51d07..721c3b76 100644 --- a/dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md +++ b/dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md @@ -7,6 +7,15 @@ | **Design spec** | `dev-docs/superpowers/specs/2026-05-15-crewli-starter-gui-redesign-design.md` | | **Impl plans** | `dev-docs/superpowers/plans/2026-05-16-gui-redesign-foundation.md` (Plan 1 of 5) | +> **Superseded in part during Plan 2.5.** Two design-SoT decisions diverged +> at implementation: dark-mode selector (`.dark` on ``, not ``; +> AD-2.5-D1) and the WorkspaceSwitcher sub line (option A reversed to a +> placeholder; AD-2.5-W1). The authoritative record is +> [`RFC-WS-PRIMEVUE-PLAN-2-5.md`](./RFC-WS-PRIMEVUE-PLAN-2-5.md) → +> Supersessions. (No discrete §4 dark-mode / §7.4 workspace-sub sections +> exist in this file — its §4 is "Out of scope" — so the supersession lives +> in the Plan 2.5 RFC rather than as per-section notes here.) + ## Status | Plan | State | Scope | diff --git a/dev-docs/RFC-WS-PRIMEVUE-PLAN-2-5.md b/dev-docs/RFC-WS-PRIMEVUE-PLAN-2-5.md index f1574da9..8fe4cb23 100644 --- a/dev-docs/RFC-WS-PRIMEVUE-PLAN-2-5.md +++ b/dev-docs/RFC-WS-PRIMEVUE-PLAN-2-5.md @@ -2,7 +2,7 @@ | Field | Value | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -| **Status** | Draft — awaiting approval | +| **Status** | ✅ Complete (2026-06-03) | | **Date** | 2026-05-19 | | **Author** | Bert + Claude Chat | | **Predecessors** | Plan 2 (`1429abf4` on `main`), Plan 3 (range `537ec098..637d77b3`, suite 564, 4 DEFERRED-HITL baselines) | @@ -26,6 +26,28 @@ Closure of Plan 2.5 unblocks Plan 3 parity-batch baseline capture and establishe --- +## Supersessions vs the governing RFC (RFC-WS-GUI-REDESIGN-CREWLI-STARTER) + +During implementation, two decisions diverged from the governing RFC. Both are deliberate and documented here: + +### §4 — Dark mode selector + +- Governing RFC specified: `.dark` class on ``. +- Plan 2.5 implemented (AD-2.5-D1, P3 commit `d0dd45c0`): `.dark` on ``. +- Rationale: PrimeVue's `darkModeSelector` and Tailwind v4's `@custom-variant dark (&:where(.dark, .dark *))` both target the document root by convention; placing the class on `` aligns the entire dark-mode stack (PrimeVue + Tailwind + Crewli utilities) and avoids the cascading mismatch where AppTopbar reacted but downstream surfaces did not. +- Regression lock: `useShellUiStore.applyDomAttributes()` writes the class to `document.documentElement`. + +### §7.4 — Workspace switcher sub line + +- Governing RFC specified option A: no sub line under the workspace name. +- Plan 2.5 initially implemented option A (P4–P5). +- After visual review against the crewli-starter SoT, **reversed**: a light-grey placeholder sub line is now rendered in the expanded trigger and dropdown rows; collapsed remains bare. The placeholder is a neutral string ('Organisatie'); real type + metrics data is deferred under backlog `WORKSPACE-DROPDOWN-SUB-CONTENT` (future backend work). +- Rationale: the two-line layout reads materially better; the placeholder honors the no-backend constraint without sacrificing the visual. + +> **Cross-doc note.** Appendix A of this RFC asks for per-section supersession notes in `RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md` §4 / §7.4. That file does not have discrete dark-mode / workspace-sub sections under those numbers (its §4 is "Out of scope"; there is no §7.4) — the §4 / §7.4 numbering above refers to the design SoT, per §1.2 of this RFC. A single top-level pointer was added to the governing RFC's status block instead; **this Supersessions section is authoritative.** + +--- + ## 1. Context ### 1.1 Where Plan 2 / Plan 3 landed @@ -978,6 +1000,46 @@ Plan 4 designs and implements the **template layer** — `PageTemplate`, `DataTa --- +## Plan 2.5 Closure (June 2026) + +### Phases completed + +- **P1** — Navigation registry + breadcrumb refactor +- **P2** — Inter typography via @fontsource/inter (+ P2-followup: removed webfontloader Public Sans JS path) +- **P3** — Dark mode mechanism (`.dark` on `` + Tailwind v4 `@custom-variant dark`) +- **P4** — WorkspaceSwitcher refactor + APP_NAVIGATION as single SoT +- **P5** — Shell parity fixes 1–5 (breadcrumb retirement, dropdown panel structure) +- **P6** — Shell parity fixes 6–10 (sidebar height, density toggle, divider) +- **P6 followups** — brand-square symmetry, logo anchor (no-jump), switcher align/spacing/sub/hover/parity (the inset-shadow unification ended the recurring "not identical" perception) +- **P7** — CREWLI-DESIGN-TOKENS.md (Track B foundation doc) + gradient brand-alignment fix (palette diversification + Crewli teal anchor) +- **P8** — Closure (this phase): RFC supersession notes, BACKLOG landing, applyDomAttributes dedupe + +### Architectural decisions of record + +- **AD-2.5-T1** — Inter via @fontsource/inter (local, weights 400/500/600/700); Public Sans fully removed from BOTH CSS and JS webfontloader paths. Regression-locked. +- **AD-2.5-D1** — `.dark` on `` (supersedes governing RFC §4) +- **AD-2.5-W1** — Option A (no sub) initially implemented, then reversed to placeholder sub after visual review (supersedes governing RFC §7.4) +- **AD-2.5-B1** — APP_NAVIGATION as single source of truth for sidebar + breadcrumb (eliminates legacy meta-based breadcrumb) + +### Brand square recipe (cemented) + +- 32px (`w-8 h-8`), `rounded-lg`, shared `shadow-[inset_0_-2px_0_#00000014]` utility on BOTH the header logo and the workspace avatar (single source — cannot drift). Offset 16px from the rail edge; collapsed rail width 64px = 32 + 2×16 keeps the square stationary across collapse toggle. + +### Suite delta + +Plan 2.5 added ~50 specs across phases (baseline → P8 final = **574 verified**, 84 files). Notable regression locks: typography (Inter on both CSS and JS paths), gradient palette (brand-teal slot pinned to #0D9394), avatar/logo size + shadow parity. P8 itself: the store's `toggleDensity` DOM-write assertion moved to AppShellV2's watcher-coverage specs (−1 assertion in the store spec, +2 specs in `AppShellV2.spec.ts`). + +### Lessons of record + +- **Audit-first beats pixel-math.** The recurring switcher iterations came from prescribing pixel values without first measuring rendered state. The final parity pass succeeded because it produced a precise side-by-side comparison table BEFORE editing. +- **Sub-perceptual divergences need spec locks.** The inset-shadow 8% vs 10% mismatch and the gradient #0D9394 vs #0d9488 mismatch were both invisible to eyeballing; only hex-value specs catch them. +- **Shared definitions prevent drift.** The header logo and workspace avatar now reference the same Tailwind utility classes for size/radius/shadow — structurally impossible to diverge. +- **One DOM-write authority.** P8 collapsed the density toggle's redundant `applyDomAttributes()` call into AppShellV2's `watch([theme, density])`, so `toggleDensity` and `setTheme` are now both pure state mutations — the watcher is the single root-attribute writer. + +### Status: COMPLETE + +--- + ## Appendix A — Cross-doc updates required at closure At Plan 2.5 closure, the following dev-docs receive **content updates** (not just SHA bumps in the sync manifest):