55 KiB
RFC — PrimeVue Migration Plan 2.5: Shell Parity + Design Token Foundation
| Field | Value |
|---|---|
| Status | Draft — awaiting approval |
| 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) |
| Successor | Plan 4 (template layer) — explicitly gated on Plan 2.5 closure + parity-batch capture |
| Governing RFC | RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md — this RFC is a Plan 2.5 sub-spec under §10 |
| Source design system | crewli-starter/ (sibling working directory) |
| Sync manifest SHA | 637d77b3 |
0. TL;DR
Visual verification on /v2/dashboard against the crewli-starter design SoT (per AD-G3) surfaced 10 divergences introduced or left unfixed by Plan 2. The Tier-1 primitive parity-batch baselines (4 DEFERRED-HITL, captured by Plan 3) are gated on a correct shell — capturing them against the divergent shell would lock in the wrong visual. In parallel, the typography audit identified Public Sans live in main with no documented rationale, conflicting with PrimeVue's de-facto Inter.
Plan 2.5 closes both gaps in two parallel tracks under one RFC:
- Track A — Shell parity. 10 targeted fixes against the design SoT. No new components, no schema changes, no new stores.
- Track B — Design token foundation. Generate
CREWLI-DESIGN-TOKENS.mdinventory; decide Typography end-to-end (Inter revert); defer remaining token decisions to a follow-up sub-sprint or Plan 4 when more pages exist and visual impact is clear.
Closure of Plan 2.5 unblocks Plan 3 parity-batch baseline capture and establishes the token-decision framework Plan 4 will build on.
1. Context
1.1 Where Plan 2 / Plan 3 landed
Plan 2 (commit 1429abf4) shipped the v2 shell components (AppSidebar, SidebarNav, WorkspaceSwitcher, AppTopbar, RightDrawer, AppDialog, layouts-v2 boundary zone) plus Storybook stories. The shell renders correctly at the structural level — slot regions, store wiring, store-driven sidebar collapse, drawer open/close — and the Vitest mount-test suite passes.
Plan 3 (range 537ec098..637d77b3) shipped 8 Tier-1 primitives (StatusTag, StatCard, PageHead, StateBlock, TagsInput, EnergyDots, EnergyPicker, DraggableBlock) plus statusSeverity.ts SoT, all co-located vue/spec/stories under apps/app/src/components-v2/shared/. Suite is at 564, gzip delta +0.82%. Four Playwright CT baselines are DEFERRED-HITL in the parity-batch pending shell stabilisation (see §1.3). Three backlog items: ENERGYDOTS-NAN, DRAGGABLEBLOCK-POINTERCANCEL (Aria-A2 blocked), AD3-MENUBAR.
1.2 The visual audit finding
A visual diff between /v2/dashboard (current main, post-Plan 3) and the crewli-starter dashboard reference revealed 10 specific divergences. The divergences are not subjective design re-evaluations — each is a deviation from the documented design SoT in RFC-WS-GUI-REDESIGN-CREWLI-STARTER §4 / §7.4 / §13 or from crewli-starter directly. The complete list is in §5.
1.3 Why parity-batch baselines are gated
Plan 3's 4 DEFERRED-HITL baselines (AppTopbar, WorkspaceSwitcher open + closed states, AppShellV2 integrated layout) cannot be captured against the current divergent shell. A baseline locks the current rendered state as the regression target — capturing now would freeze the wrong visual as the contract and force Plan 2.5's fixes to re-baseline (negating the test value).
Parity-batch capture is therefore explicitly not in Plan 2.5 scope — it is the unblocking outcome of Plan 2.5 merging, executed as a separate work item.
1.4 The typography audit trigger
While inventorying the v2 shell's effective :root CSS variables, Public Sans was found as the live font-family on main. No commit message, RFC reference, or design-doc entry justifies the choice. PrimeVue Aura ships with no built-in font (UI components inherit from the application), and PrimeVue's own showcase, Figma UIKit, and Volt commercial templates all use Inter. The Aura preset's spacing, line-height, and x-height ratios are calibrated against Inter-class metrics. Public Sans has marginally different metrics — sub-pixel drift visible in dense data tables and form-field stacks.
This audit finding expanded Plan 2.5 scope to include a first-pass design-token audit before more pages get built on potentially-wrong tokens. The audit framework is in §6.
2. Decisions taken (chat session 2026-05-19)
Five open questions were resolved in chat before this RFC was authored:
| # | Question | Decision | AD |
|---|---|---|---|
| Q1 | WorkspaceSwitcher sub field content |
C4: no sub field in Plan 2.5 (name + initials + gradient only) |
AD-2.5-W1 |
| Q2 | .dark class scope |
PrimeVue canonical class-based selector on <html>, aligned with Tailwind v4 |
AD-2.5-D1 |
| Q3 | Mobile scope in Plan 2.5 | Mobile behaviour follows existing design-doc §4 (Drawer overlay on <lg); no mobile-specific Plan 2.5 fixes |
§3.4 |
| Q4 | Breadcrumb data source | Central navigation registry + useBreadcrumb composable, rendered via PrimeVue Breadcrumb |
AD-2.5-B1 |
| Q5 | Typography | Revert Public Sans → Inter via @fontsource/inter (lokaal package) |
AD-2.5-T1 |
Each is unpacked as a full Architecture Decision in §4.
3. Scope
3.1 In scope
- Track A — 10 shell-parity fixes against the documented design SoT (§5)
- Track B —
CREWLI-DESIGN-TOKENS.mdinventory file + Typography revert end-to-end + regression-lock spec (§6) - Four Architecture Decisions:
AD-2.5-T1(Typography),AD-2.5-D1(Dark-mode selector),AD-2.5-W1(Workspace sub),AD-2.5-B1(Breadcrumb source) - One new composable + config:
apps/app/src/config/navigation.ts,apps/app/src/composables/useBreadcrumb.ts(§4 AD-2.5-B1)
3.2 Track A scope summary
10 fixes against crewli-starter dashboard reference, all desktop-≥lg-viewport, all rooted in design-doc §4 / §7.4 / §13 or in crewli-starter source. No fix introduces a new component, new store, or schema change. Full details in §5.
3.3 Track B scope summary
The token audit is phase-1 only in Plan 2.5: inventory + classification of every :root CSS variable crewli-starter sets, plus end-to-end decision (revert + regression-lock) for Typography only. Remaining token decisions are tagged DEFERRED in CREWLI-DESIGN-TOKENS.md for follow-up. Scope rationale in §6.4.
3.4 Out of scope (explicit)
- Plan 4 template layer. Designed and RFC'd after Plan 2.5 merge + parity-batch capture, not before.
- Parity-batch baseline captures for Plan 3 primitives. The 4 DEFERRED-HITL captures execute as a separate work item after Plan 2.5 merges. They are the unblocking outcome of this RFC, not part of it.
- Backend schema changes. Explicitly: no
organisations.typeenum, no organisation-stats endpoint, nowithCountqueries on the org-list endpoint. Q1's C4 decision eliminates the only Plan 2.5-adjacent need for these. - Mobile-only divergences beyond design-doc §4. Design-doc §4 specifies PrimeVue
Draweroverlay on<lg— that pattern continues. Any mobile visual issues not attributable to one of the 10 shell-parity fixes (which apply at all viewports) are taggedMOBILE-PARITY-DEFERREDand roll up into a future mobile-parity sprint. - Full design-token revert. Phase-1 audit decides Typography only. All other tokens (color scale offsets, surface tones, focus ring, spacing) are inventoried but classification + decision is deferred.
WorkspaceSwitchersubfield implementation beyond removal. Thesubslot in the component template is removed in Plan 2.5; re-introducing it (C1/C2/C3 from chat discussion) requires a separate RFC.
4. Architecture Decisions
AD-2.5-T1 — Typography: Inter via @fontsource/inter
Status: Decided 2026-05-19
Context. apps/app/src/main.css (or the equivalent token application site) sets --p-font-family: 'Public Sans', ... with no documented rationale. PrimeVue Aura preset ships no built-in font; the application controls typography. PrimeVue's own showcase, Figma UIKit, and Volt commercial templates use Inter; Aura's metrics are calibrated against Inter-class fonts.
Decision. Revert to Inter loaded locally via @fontsource/inter package. Apply at the theme layer (apps/app/src/plugins/primevue/theme.ts per AD-2 in the governing RFC), not via ad-hoc main.css overrides.
Rationale.
- Inter is the industry-standard SaaS typography — Linear, Stripe, Vercel, GitHub, Notion. Crewli's competitive set. New users perceive Inter as "professional SaaS" by default; Public Sans reads as "civic / public-sector tooling."
@fontsource/interis a local package — no Google Fonts request, no CDN race condition, no GDPR signing-up-third-parties issue, no FOUT on slow connections.- Metric alignment with Aura. Aura's spacing tokens (
--p-form-field-padding-y,--p-button-padding-y) are sized for Inter's x-height. Public Sans has slightly different metrics — sub-pixel drift visible in dense data tables, form-field stacks, and inline labels. - No documented rationale for Public Sans. The audit found neither a commit message, RFC reference, nor design-doc entry justifying the choice. The default position when there is no rationale is "match the framework's de-facto convention."
Note on "PrimeVue default." PrimeVue Aura technically has no font default — the docs state: "There is no design for fonts as UI components inherit their font settings from the application." The "Inter is PrimeVue default" claim is true in practice (showcase, UIKit, Volt) but not technically in the preset config. This AD documents the distinction so future contributors do not look for a --p-font-family token in the Aura preset and conclude wrongly that "it's there."
Implementation site. apps/app/src/plugins/primevue/theme.ts:
// theme.ts — typography lives here per RFC AD-2, not in main.css
import { definePreset } from "@primeuix/themes";
import Aura from "@primeuix/themes/aura";
import "@fontsource/inter/400.css";
import "@fontsource/inter/500.css";
import "@fontsource/inter/600.css";
import "@fontsource/inter/700.css";
export const CrewliPreset = definePreset(Aura, {
semantic: {
// ... existing teal primary ...
},
extend: {
crewli: {
// ... other Crewli-specific tokens ...
},
},
});
// Font is applied via CSS at the application layer, since Aura
// has no font token. The single application site is below.
apps/app/src/main.css (or the dedicated token site — verify in implementation):
:root {
--crewli-font-family:
"Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
Arial, sans-serif;
}
html,
body {
font-family: var(--crewli-font-family);
}
Removal scope. Every Public Sans reference must be deleted, not commented out:
apps/app/src/main.cssfont-familydeclarationspackage.json@fontsource/public-sansdependency (if present — verify)- Any
<link>tag inindex.htmlloading Public Sans (if present — verify)
Regression lock. New Vitest spec apps/app/tests/unit/styles/typography.spec.ts:
import { describe, expect, it } from "vitest";
describe("Typography regression lock (AD-2.5-T1)", () => {
it("body font-family resolves Inter as first declared family", () => {
document.head.insertAdjacentHTML(
"beforeend",
'<link rel="stylesheet" href="/src/main.css">',
);
const computed = getComputedStyle(document.body).fontFamily;
// First declared family must be Inter (with or without quotes).
expect(computed).toMatch(/^['"]?Inter['"]?[,\s]/);
});
it("Public Sans is not present in the font stack", () => {
const computed = getComputedStyle(document.body).fontFamily;
expect(computed).not.toMatch(/Public Sans/i);
});
});
This spec is mandatory in DoD — its absence means the revert is undefended against silent regression.
AD-2.5-D1 — Dark mode selector: PrimeVue canonical class-based on <html>
Status: Decided 2026-05-19
Context. The current implementation puts a .dark class on AppTopbar only (Bert's visual audit observation, item 6 of 10). The design-doc §4 specifies <html data-theme> mechanism (data-attribute approach). PrimeVue's canonical toggle-able dark mode is class-based on the document root. Tailwind v4's default class-based dark variant is also class-on-<html>. The current state aligns with none of these three patterns.
Decision. Use PrimeVue's canonical class-based selector: darkModeSelector: '.dark' in theme.ts, with the class toggled on document.documentElement (i.e., <html>). This supersedes design-doc §4's <html data-theme> reference, which is now an obsolete pre-implementation note.
Rationale.
- PrimeVue canonical toggle pattern. The PrimeVue docs example:
darkModeSelector: '.my-app-dark'with toggle viadocument.documentElement.classList.toggle('my-app-dark'). We use.darkinstead of.my-app-darkfor convergence (see below). - Convergence with Tailwind v4. Tailwind v4's canonical class-based dark variant uses
.darkon<html>(@custom-variant dark (&:where(.dark, .dark *))). Using a single class for both Tailwind utility-class variants and PrimeVue component tokens means one toggle, two systems react — no out-of-sync windows, no Tailwinddark:utilities that activate while PrimeVue components stay light, no inverse. systemdefault rejected. PrimeVue Aura's actual default isdarkModeSelector: 'system'(matchesprefers-color-scheme). Crewli has an explicit dark-mode toggle button in the topbar — thesystemdefault would prevent the toggle from working.systemis fine for apps that defer entirely to OS preference; Crewli is not one of those apps.- Why
<html data-theme>was wrong. Data-attribute selectors work, but they require Tailwind v4 to be configured to@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *))and PrimeVuedarkModeSelector: '[data-theme="dark"]'. Both can be done — but it's two non-canonical configurations for no benefit over the standard class-on-<html>pattern. Design-doc §4 was authored before implementation; in practice the class-based path is simpler and matches both ecosystems' defaults.
Implementation site. apps/app/src/plugins/primevue/theme.ts:
app.use(PrimeVue, {
theme: {
preset: CrewliPreset,
options: {
prefix: "p",
darkModeSelector: ".dark", // ← AD-2.5-D1
cssLayer: false,
},
},
});
apps/app/src/stores/useShellUiStore.ts, applyDomAttributes():
applyDomAttributes() {
// AD-2.5-D1: dark mode is a single class on <html>.
// Tailwind v4 @custom-variant dark and PrimeVue darkModeSelector
// both react to this class.
const root = document.documentElement
if (this.theme === 'dark') {
root.classList.add('dark')
} else {
root.classList.remove('dark')
}
// density still uses data-attribute (orthogonal axis to colour scheme).
root.setAttribute('data-density', this.density)
}
Removal scope. Every .dark class application not on <html> must be deleted, not adapted. Specifically:
- Audit all v2 components for hardcoded
class="dark"or:class="{ dark: ... }"— these are tactical patches and must be removed - Audit
applyDomAttributesfor any priorsetAttribute('data-theme', ...)writes — replace with the class toggle - Audit
apps/app/src/main.css(and any Tailwind config) for[data-theme="dark"]selectors — replace with.dark
Verification. Two-pronged:
- Manual smoke: open
/v2/dashboard, toggle dark mode, both topbar AND sidebar AND content area transition (not just topbar). - Vitest spec
apps/app/tests/unit/stores/useShellUiStore.spec.tsextended:
it("toggles the .dark class on document.documentElement (AD-2.5-D1)", () => {
const store = useShellUiStore();
store.theme = "dark";
store.applyDomAttributes();
expect(document.documentElement.classList.contains("dark")).toBe(true);
store.theme = "light";
store.applyDomAttributes();
expect(document.documentElement.classList.contains("dark")).toBe(false);
});
it("does not write a data-theme attribute (AD-2.5-D1 supersedes design-doc §4)", () => {
const store = useShellUiStore();
store.theme = "dark";
store.applyDomAttributes();
expect(document.documentElement.hasAttribute("data-theme")).toBe(false);
});
Cross-doc impact. RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md §4 must be updated in Plan 2.5 closure docs to note: "AD-G2.5-D1 supersedes the <html data-theme> mechanism described here; the class-based selector on <html> is the live convention as of Plan 2.5."
AD-2.5-W1 — WorkspaceSwitcher sub field: no sub in Plan 2.5
Status: Decided 2026-05-19
Context. Design-doc §7.4 specifies WorkspaceSwitcher shape { initials, name, sub, gradient }, derived via computed properties over useAuthStore + useOrganisationStore. The sub field is described as "org metadata line" — vague, not committed to a specific data source. Three chat-discussed options:
- C1: ledental (
"12 leden") — requireswithCount('users')on org-list endpoint - C2: plan tier — requires
organisations.subscription_tiercolumn (does not exist perSCHEMA.md) - C3: locatie/city — requires
organisations.city(perSCHEMA.mdnot present at column level) - C4: nothing — only name + initials + gradient
Decision. C4: in Plan 2.5, WorkspaceSwitcher and its dropdown items render name + initials + gradient only. The sub slot in the component template is removed, not made conditional.
Rationale.
- Zero scope creep. C1/C2/C3 each require either backend work or schema additions. None of them is the Plan 2.5 purpose (shell parity). Forcing one in pulls Plan 2.5 into a tenant-data-shape discussion.
- Design-doc compliant. Design-doc §7.4 says
subis"org metadata line"— an optional descriptor, not a load-bearing UX element. Removing it is within design intent. - Visual delta is minimal. In
crewli-starter,subrenders at ~50% opacity undername. Its absence collapses the workspace cell by ~16px vertical — well within the design's visual tolerance for the bottom-of-sidebar workspace switcher. - Re-introducing later is cheap. Removing the slot now and adding it back via a separate RFC (with proper data-source decision) costs ~30 lines of code. The reverse — committing to a half-built C1 and rolling back — costs significantly more.
Implementation site. apps/app/src/components-v2/layout/WorkspaceSwitcher.vue:
<!-- AD-2.5-W1: sub field removed. Re-introducing requires a separate RFC
with explicit data-source decision (organisations.city, member count,
subscription tier — none of these are decided as of 2026-05-19). -->
<template>
<button class="workspace-cell">
<span class="initials-square" :style="{ background: gradient }">
{{ initials }}
</span>
<span class="workspace-name">{{ name }}</span>
<i class="pi pi-chevron-down" />
</button>
</template>
Dropdown items follow the same shape — gradient + name + current-checkmark, no sub line.
Removal scope. Every prior sub reference must be deleted, not commented out:
- Component template: remove the
<span class="workspace-sub">{{ sub }}</span>line - Component props/computed: remove
subfrom the derived computed in §7.4 wiring - Storybook stories: remove
subfrom all WorkspaceSwitcher story arg defaults - Any tests asserting
sub: delete those specs (don't comment-out)
Cross-doc impact. RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md §7.4 must be updated in Plan 2.5 closure docs to note: "AD-G2.5-W1 reduces the WorkspaceSwitcher shape to { initials, name, gradient } for Plan 2.5; sub may be re-introduced under a future RFC with explicit data-source decision."
AD-2.5-B1 — Breadcrumb data source: central navigation registry + useBreadcrumb composable
Status: Decided 2026-05-19
Context. Design-doc §4 specifies PrimeVue Breadcrumb as the primitive for the topbar breadcrumb. Data-source mechanism was unspecified. Chat discussion established the crewli-starter convention is "own setup based on total application navigation (modules, submodules, etc.)" — i.e., the breadcrumb derives from the same navigation tree the sidebar reads.
Decision. Plan 2.5 introduces a central navigation registry as the SoT for both sidebar nav structure AND breadcrumb chain. A new composable useBreadcrumb() reads the current route and walks the registry to produce the breadcrumb item list, which is passed to PrimeVue's Breadcrumb primitive for rendering.
Why a registry, not route meta.
- Single source of truth. Sidebar reads
APP_NAVIGATIONfor nav rendering;useBreadcrumbreads the sameAPP_NAVIGATIONfor breadcrumb derivation. One file changes, two surfaces update. - Avoids meta-drift. Route meta requires every route definition to carry
meta.breadcrumbindependently; nothing enforces consistency between sidebar definition and meta strings. Drift inevitable. - Test-friendly. Walk-tree function is pure —
walkNavTree(registry, routeName)is unit-testable without mounting components or stubbingvue-router. - No PrimeVue Breadcrumb data-model coupling. PrimeVue
Breadcrumbaccepts aMenuItem[]— our composable returns exactly that. No translation layer.
Implementation site.
apps/app/src/config/navigation.ts (new):
// AD-2.5-B1: central navigation registry.
// Single source of truth for sidebar rendering AND breadcrumb derivation.
import type { Component } from "vue";
export interface NavItem {
key: string; // stable identifier (e.g., 'events.volunteers')
label: string; // display string
routeName?: string; // vue-router route name (leaf nodes)
icon?: string; // tabler icon name (e.g., 'tabler:calendar')
children?: NavItem[]; // submodule nesting
hidden?: boolean; // exclude from sidebar but allow breadcrumb walk
}
export const APP_NAVIGATION: NavItem[] = [
{
key: "dashboard",
label: "Dashboard",
routeName: "v2-dashboard",
icon: "tabler:home",
},
{
key: "events",
label: "Events",
icon: "tabler:calendar",
children: [
{ key: "events.list", label: "Events", routeName: "v2-events" },
{
key: "events.volunteers",
label: "Volunteers",
routeName: "v2-event-volunteers",
},
// ...
],
},
// ... etc, see Plan 2.5 implementation for complete registry
];
apps/app/src/composables/useBreadcrumb.ts (new):
import { computed } from "vue";
import { useRoute } from "vue-router";
import { APP_NAVIGATION, type NavItem } from "@/config/navigation";
export interface BreadcrumbItem {
label: string;
routeName?: string;
}
/**
* Walks APP_NAVIGATION to find the path from the root to the leaf
* matching the given route name. Returns an empty array if no match.
*
* AD-2.5-B1: pure function, unit-testable without router or component mount.
*/
export function walkNavTree(
tree: NavItem[],
routeName: string,
acc: BreadcrumbItem[] = [],
): BreadcrumbItem[] {
for (const node of tree) {
const next = [...acc, { label: node.label, routeName: node.routeName }];
if (node.routeName === routeName) return next;
if (node.children) {
const childMatch = walkNavTree(node.children, routeName, next);
if (childMatch.length > 0) return childMatch;
}
}
return [];
}
export function useBreadcrumb() {
const route = useRoute();
return computed<BreadcrumbItem[]>(() => {
if (!route.name) return [];
return walkNavTree(APP_NAVIGATION, String(route.name));
});
}
apps/app/src/components-v2/layout/AppBreadcrumb.vue (new primitive — also addresses Fix 2):
<script setup lang="ts">
import { computed } from "vue";
import Breadcrumb from "primevue/breadcrumb";
import { useBreadcrumb } from "@/composables/useBreadcrumb";
const crumbs = useBreadcrumb();
// PrimeVue Breadcrumb expects MenuItem[] — map our shape.
const items = computed(() =>
crumbs.value.map((c) => ({
label: c.label,
route: c.routeName ? { name: c.routeName } : undefined,
})),
);
</script>
<template>
<Breadcrumb
:model="items"
:pt="{ root: { class: 'border-none p-0 bg-transparent' } }"
>
<template #item="{ item }">
<RouterLink
v-if="item.route"
:to="item.route"
class="text-sm text-surface-600 hover:text-surface-900"
>
{{ item.label }}
</RouterLink>
<span v-else class="text-sm text-surface-900 font-medium">{{
item.label
}}</span>
</template>
</Breadcrumb>
</template>
Sidebar consumes the same registry. SidebarNav is refactored to read APP_NAVIGATION (replacing whatever current source it uses). This is the "single source of truth" payoff — both the sidebar rendering and the breadcrumb derivation read the same file. Adding a new page = adding one entry, both UIs update.
Tests.
apps/app/tests/unit/composables/useBreadcrumb.spec.ts:
import { describe, expect, it } from "vitest";
import { walkNavTree } from "@/composables/useBreadcrumb";
import type { NavItem } from "@/config/navigation";
const FIXTURE: NavItem[] = [
{ key: "a", label: "A", routeName: "a" },
{
key: "b",
label: "B",
children: [{ key: "b.1", label: "B-1", routeName: "b-1" }],
},
];
describe("walkNavTree (AD-2.5-B1)", () => {
it("returns the chain for a leaf route", () => {
expect(walkNavTree(FIXTURE, "b-1")).toEqual([
{ label: "B", routeName: undefined },
{ label: "B-1", routeName: "b-1" },
]);
});
it("returns empty for an unmatched route", () => {
expect(walkNavTree(FIXTURE, "nonexistent")).toEqual([]);
});
it("returns single-entry chain for a top-level leaf", () => {
expect(walkNavTree(FIXTURE, "a")).toEqual([{ label: "A", routeName: "a" }]);
});
});
Future scope. A central navigation registry naturally extends to: role-based filtering (NavItem.requiresPermission), feature-flag gates (NavItem.featureFlag), and dynamic ordering. Plan 2.5 does not implement these — the registry has only the fields shown above. Extensions are explicit follow-up RFCs.
5. Shell-parity fixes (Track A)
Each fix follows the same structure: current state, design SoT reference, fix, regression lock. All fixes apply at desktop ≥lg viewport; mobile follows existing design-doc §4 (Drawer overlay) — no Plan 2.5-specific mobile work.
5.1 Fix 1 — Remove duplicate Crewli brand from topbar
Current state. Both SidebarHeader (sidebar top) and AppTopbar render a Crewli logo + wordmark. The sidebar logo is correct per design-doc §4 (SidebarHeader.vue # logo + collapse toggle). The topbar logo is a Plan 2 implementation artifact not in the design.
Design SoT. crewli-starter dashboard: topbar #start slot contains breadcrumb only, no brand. Brand lives in SidebarHeader exclusively.
Fix. Delete the brand block from AppTopbar.vue. The #start slot is repurposed in Fix 2.
Regression lock. Component test asserting AppTopbar does NOT render an element with data-testid="topbar-brand". Visual: parity-batch captures the brand-free topbar after Plan 2.5 closes.
5.2 Fix 2 — AppTopbar #start: replace brand with AppBreadcrumb
Current state. #start renders the brand block (see Fix 1).
Design SoT. Design-doc §4: "AppTopbar # Breadcrumb (PrimeVue) + actions". crewli-starter confirms breadcrumb left, actions right.
Fix. Replace #start content with <AppBreadcrumb /> (the new primitive from AD-2.5-B1).
<!-- AppTopbar.vue, AFTER Fix 1 + Fix 2 -->
<template>
<header class="app-topbar">
<!-- left: breadcrumb (AD-2.5-B1) -->
<AppBreadcrumb />
<!-- right: actions (Fix 10 specifies exact order/state) -->
<div class="topbar-actions">
<!-- search / dark toggle / notifications / user menu -->
</div>
</header>
</template>
Regression lock. Component test asserting AppTopbar renders an <AppBreadcrumb> child. Unit test for useBreadcrumb already covered in AD-2.5-B1.
5.3 Fix 3 — Move WorkspaceSwitcher from top to bottom of sidebar
Current state. AppSidebar renders WorkspaceSwitcher at the top (between header and nav). Plan 2 regression against design-doc.
Design SoT. Design-doc §4 (literal): "WorkspaceSwitcher.vue # bottom switcher (custom visual + PrimeVue Popover)". The "bottom switcher" phrasing is explicit and is not a stylistic choice.
Fix. Reorder children in AppSidebar.vue template:
<!-- AppSidebar.vue, AFTER Fix 3 + Fix 9 -->
<template>
<aside class="app-sidebar">
<SidebarHeader />
<SidebarNav />
<div class="sidebar-spacer flex-1" />
<!-- pushes switcher to bottom -->
<WorkspaceSwitcher />
</aside>
</template>
The flex-1 spacer is the canonical way to bottom-anchor in a flex-col container — no absolute positioning, no mt-auto hacks.
Regression lock. Component test asserting that within AppSidebar, the DOM order is SidebarHeader → SidebarNav → WorkspaceSwitcher (the spacer is presentational, not a testable target). Visual: parity-batch captures the bottom-anchored switcher.
5.4 Fix 4 — Workspace label scheme: implement AD-2.5-W1 (no sub)
Current state. WorkspaceSwitcher renders name + sub (currently "Stichting Feestfabriek / org_admin" — an internal-tooling-looking label).
Design SoT. AD-2.5-W1 (this RFC).
Fix. Per AD-2.5-W1: remove sub slot entirely. Render initials + name + gradient only.
Regression lock. Component test asserting WorkspaceSwitcher does NOT render an element with data-testid="workspace-sub". Storybook story WithSub is deleted (not commented out). Type definition for the computed shape drops the sub field.
5.5 Fix 5 — Workspace dropdown items per crewli-starter
Current state. Dropdown contents depend on prior implementation; need verification against crewli-starter reference.
Design SoT. Design-doc §7.4: "Panel content (Workspaces header + Manage link, list with gradient logos + current checkmark, footer New workspace / Invite) stays custom markup."
Fix. Restructure dropdown panel:
- Header:
"Workspaces"label +Managelink (right-aligned) - List: each item =
gradient square (initials) + name + current-checkmark-if-active. Nosub(perAD-2.5-W1). - Footer:
New workspace+Inviteactions
Reuses existing org list from useAuthStore().organisations. No new store, no new endpoint.
Regression lock. Component test rendering WorkspaceSwitcher with a 3-org fixture, asserting:
- Header row contains text
"Workspaces"and a link with text"Manage" - List row count equals fixture org count
- Footer row contains both
"New workspace"and"Invite"triggers
Visual: parity-batch captures the dropdown-open state.
5.6 Fix 6 — Dark mode class scope: implement AD-2.5-D1
Current state. .dark class lives on AppTopbar only (per audit observation). Tailwind dark: utilities and PrimeVue darkModeSelector are out of sync.
Design SoT. AD-2.5-D1 (this RFC).
Fix. Per AD-2.5-D1: configure darkModeSelector: '.dark' in theme.ts; rewrite useShellUiStore.applyDomAttributes() to toggle .dark on document.documentElement; delete every .dark class application elsewhere.
Regression lock. Two specs covered in AD-2.5-D1 (applyDomAttributes toggles .dark on <html>; no data-theme attribute is written).
5.7 Fix 7 — Content top-offset: title falls behind topbar
Current state. Page title at the top of the main content scrolls beneath the topbar (no top padding or sticky offset).
Design SoT. crewli-starter: topbar is sticky / positioned, main content has a top offset matching topbar height.
Fix. Two-step:
- Make topbar sticky-or-fixed (whichever matches
crewli-starter). Verify againstcrewli-startersource — most likelyposition: sticky; top: 0with appropriatez-index. - Apply matching top padding to
<main>inAppShellV2:
<!-- AppShellV2.vue -->
<main class="flex-1 overflow-auto p-6">
<!-- p-6 already adds spacing; verify topbar height matches the spec -->
<slot />
</main>
If the topbar is h-14 (current Plan 1 spec) and <main> already has sufficient top padding, this may be a non-fix in code — but must be visually verified at all viewports before claiming closure.
Regression lock. Visual baseline only — no good unit assertion for scroll behaviour. Parity-batch captures the scrolled state.
5.8 Fix 8 — Remove mysterious ▼ arrow at content bottom
Current state. A ▼ chevron renders at the bottom of the content area on /v2/dashboard. Origin unknown — likely a leftover stub or a misplaced Breadcrumb chevron.
Design SoT. Not present in crewli-starter.
Fix. Locate and delete. Implementation prompt instructs Claude Code to grep -rn "pi-chevron-down\|▼\|▼" apps/app/src/components-v2/ apps/app/src/layouts/ to find candidate sites, identify the offending element, and remove it. Most likely candidates: a placeholder in AppShellV2.vue from Plan 1 scaffold; a stray Breadcrumb separator; or a Pinia DevTools indicator (excluded from prod build).
Regression lock. Visual baseline. No specific unit assertion — the element shouldn't have existed in the first place.
5.9 Fix 9 — Sidebar bottom region per crewli-starter
Current state. Sidebar bottom is empty / has only the (mis-positioned) WorkspaceSwitcher.
Design SoT. crewli-starter sidebar bottom region contains: the bottom-anchored WorkspaceSwitcher (Fix 3) plus surrounding spacing/dividers per the design.
Fix. This fix is partially implicit in Fix 3 (WorkspaceSwitcher moves to bottom). The remainder: any divider, padding, or visual structure surrounding the bottom region must match crewli-starter. Implementation prompt requires Claude Code to paste the crewli-starter sidebar source (specifically the sidebar bottom region) into the prompt, then port the structural markup to AppSidebar.vue and WorkspaceSwitcher.vue.
Requires from Bert: the crewli-starter sidebar source file — paste into the implementation-prompt phase.
Regression lock. Visual baseline. Component test for sidebar DOM order from Fix 3 covers structural ordering.
5.10 Fix 10 — Topbar right-side items: order + state per crewli-starter (incl. density toggle)
Current state. Topbar right-side renders some combination of search / dark-toggle / notifications / user menu, with unverified ordering. Density toggle is missing entirely despite useShellUiStore.density state existing since Plan 2 (per governing RFC §4).
Design SoT. crewli-starter topbar (mobile screenshot, matching desktop): search → density toggle → dark-toggle → notifications (with badge) → user avatar. The icon that renders as 0|0 between search and dark-toggle is the density toggle (chat clarification 2026-05-19): switches between compact (less spacing) and regular (more spacing) GUI modes.
Icon caveat.
0|0is Iconify's fallback glyph for an icon name that doesn't resolve in the registered collection. It is almost certainly not the intended icon —crewli-starterreferences a real Tabler (or inline SVG) icon that didn't load in the screenshot context. Implementation prompt must verify the actual icon fromcrewli-startersource rather than reproduce the fallback.
Fix. Reorder AppTopbar.vue right-side actions to match the design SoT order, AND wire the density toggle to useShellUiStore:
-
Verify store API.
useShellUiStore.densityalready exists per design-doc §4 (values:'compact' | 'regular' | 'comfy'). VerifytoggleDensity()action exists; if not, add it as a binary cycle (compact ⇔ regular, ignoringcomfyfor the topbar trigger).comfyremains available for components that opt in explicitly (e.g.,DraggableBlockper design-doc §7.1). -
Add density toggle
<Button>toAppTopbar.vuebetween search trigger and dark-mode toggle. -
Identify the correct icon. Implementation prompt requires Claude Code to read
crewli-starter's topbar source (Bert pastes in) and use the same icon. If it's a Tabler icon: add toaddCollectionat app bootstrap (per the established Iconify discipline — no runtime CDN fetches). If it's an inline SVG: port verbatim. Do not guess the icon name. -
Wire interaction + a11y:
<Button text rounded :aria-label="'Toggle density'" :aria-pressed="shell.density === 'compact'" data-testid="density-toggle" @click="shell.toggleDensity()" > <Icon :icon="DENSITY_ICON" /> <!-- DENSITY_ICON identified per step 3 --> </Button> -
Apply via
applyDomAttributes(). Density continues to writedata-density="<value>"ondocument.documentElement(orthogonal toAD-2.5-D1's.darkclass on<html>— density and colour-scheme are independent axes; both can coexist on the same root element without conflict).
Regression lock. Component test asserting topbar right-side renders, in order:
- Search trigger
- Density toggle (with
data-testid="density-toggle") - Dark-mode toggle
- Notifications (with
OverlayBadge) - User menu avatar
Store spec extension apps/app/tests/unit/stores/useShellUiStore.spec.ts:
it("toggleDensity cycles compact ⇔ regular (comfy is not toggle-reachable)", () => {
const store = useShellUiStore();
store.density = "regular";
store.toggleDensity();
expect(store.density).toBe("compact");
store.toggleDensity();
expect(store.density).toBe("regular");
});
it("toggleDensity from comfy resets to regular (not compact)", () => {
const store = useShellUiStore();
store.density = "comfy";
store.toggleDensity();
expect(store.density).toBe("regular");
});
it("applyDomAttributes writes data-density on documentElement", () => {
const store = useShellUiStore();
store.density = "compact";
store.applyDomAttributes();
expect(document.documentElement.getAttribute("data-density")).toBe("compact");
});
The "comfy → regular" behaviour is deliberate: comfy is opt-in per component, never user-cycled. Resetting to regular (not compact) is the conservative recovery if a stale comfy value persists through hydration.
Visual: parity-batch captures both density states of the topbar after Plan 2.5 closes.
Requires from Bert (P6 prompt phase): the crewli-starter topbar source file, specifically the density-toggle button markup and icon reference.
6. Design token audit (Track B)
6.1 Process
The audit produces dev-docs/CREWLI-DESIGN-TOKENS.md as the inventory and decision register for every :root CSS variable crewli-starter sets. The doc has three sections:
- Inventory — full table of every variable, source value, classification.
- Classification per variable — one of three categories (defined in §6.2).
- Decision — for each token classified as
Generic, an explicit decision: keepcrewli-startervalue (with rationale) or revert to Aura default.
Plan 2.5 fully decides one row (Typography); the rest are inventoried and tagged DEFERRED. The framework is in place for Plan 2.5b or Plan 4 to make the remaining decisions.
6.2 Classification scheme
Each token is one of:
- Brand-essential — token carries Crewli brand identity. Keep
crewli-startervalue. Examples:--p-primary-color(teal), gradient definitions. - Bespoke — token has a non-Aura-default value with deliberate design intent. Keep
crewli-startervalue with rationale. Examples: custom focus-ring width ifcrewli-starterwidened it; custom border-radius scale. - Generic — token has a non-Aura-default value with no documented intent. Default decision: revert to Aura. Override requires explicit rationale in the decision register. Examples: font-family (per
AD-2.5-T1), arbitrary surface-tone shifts.
6.3 CREWLI-DESIGN-TOKENS.md structure (template)
# Crewli Design Tokens — Audit & Decision Register
| Field | Value |
| ---------------------- | ---------------------- |
| Source design system | crewli-starter/ |
| Aura preset version | @primeuix/themes@4.5.x |
| Audit date | 2026-05-19 |
| Phase-1 decided tokens | Typography (AD-2.5-T1) |
## Token inventory
| Token | crewli-starter value | Aura default | Classification | Decision | RFC |
| ---------------------- | -------------------- | ------------------- | ----------------------------------------------- | --------------- | ------------------ |
| `--p-font-family` | `'Public Sans', ...` | n/a (inherited) | Generic | Revert to Inter | AD-2.5-T1 |
| `--p-primary-color` | `#0D9394` | `#10b981` (emerald) | Brand-essential | Keep | governing RFC AD-2 |
| `--p-focus-ring-width` | `2px` (verify) | `2px` (verify) | (DEFERRED — verify both values, classify after) | DEFERRED | — |
| ... | ... | ... | ... | ... | ... |
## Decisions
### Typography — Inter via @fontsource/inter
See AD-2.5-T1 in RFC-WS-PRIMEVUE-PLAN-2-5.md.
### (DEFERRED tokens listed here as section stubs, no decisions yet)
6.4 Why Phase-1 stops at Typography
The token audit is a multi-day effort if done exhaustively. Plan 2.5's purpose is shell parity, not full token reconciliation. Stopping at Typography:
- Lets Plan 2.5 ship. Each token decision can require visual smoke-test at multiple density / dark-mode combinations. End-to-end decisions for ~30+ tokens is its own sprint.
- Establishes the framework. The doc, the classification scheme, the decision-register format — these are durable artifacts. Future decisions append; they don't restart.
- Typography has the largest visual surface impact. Font choice affects every text element in the app. Other tokens (e.g.,
--p-focus-ring-width) are localised. - The regression-lock pattern is now in place.
AD-2.5-T1's computed-style test is the template; future token reverts copy the pattern.
Phase-2 candidates (for Plan 2.5b or absorbed into Plan 4): font-size scale (root rem base), surface tone (--p-surface-*), focus-ring (--p-focus-ring-*), border-radius scale.
7. Test strategy
7.1 Unit / component
All Vitest / Vue Test Utils based:
useBreadcrumbwalkNavTree (4 specs minimum: leaf, no-match, top-level, deep nesting)useShellUiStore.applyDomAttributesdark-mode toggle (2 specs: positive + data-theme-absence)useShellUiStore.toggleDensitydensity toggle (3 specs: compact⇔regular cycle, comfy→regular reset, data-density write)WorkspaceSwitcherno-subregression (1 spec:data-testid="workspace-sub"does not render)AppTopbarFix 1+2 (1 spec: no brand, AppBreadcrumb present)AppSidebarFix 3 (1 spec: DOM order)AppTopbarFix 10 (2 specs: 5-item action order including density toggle, density toggle click callsshell.toggleDensity)- Typography regression lock (
AD-2.5-T1: 2 specs)
Estimated new spec count: ~16 specs, all unit-level. Suite delta: 564 → ~580.
7.2 Visual regression (Playwright CT)
Plan 2.5 does not add new visual baselines. Reason: visual baselines are what's gated. The 4 DEFERRED-HITL captures and any Plan 2.5 shell baselines are taken after Plan 2.5 merges, in the unblocked parity-batch work item.
Plan 2.5 may update the existing AppShellV2 mount-test snapshot (a structural test, not a pixel-diff visual) to reflect the new sidebar DOM order.
7.3 Token regression lock
AD-2.5-T1 mandates the typography computed-style spec. This is the pattern: every decided token in CREWLI-DESIGN-TOKENS.md requires a corresponding computed-style spec. Phase-1 has one such spec; Phase-2 will add more per token decided.
7.4 What's deliberately not tested
crewli-startersource code parity. We do not unit-test that our markup matchescrewli-starterline-for-line — that's what visual baselines are for, after Plan 2.5 merges.- Mobile-specific behaviour. No new mobile specs in Plan 2.5. Existing mobile drawer tests (if any) continue to pass.
- Cross-browser dark-mode. PrimeVue + Tailwind handle the actual style application; we test our toggle logic only.
8. Sequencing
Strict linear order. No parallel work. Each step gates the next.
1. Foundation: AD-2.5-T1 + AD-2.5-D1 + AD-2.5-B1 file scaffolding
├── theme.ts: definePreset + darkModeSelector + Inter import
├── apps/app/src/config/navigation.ts: APP_NAVIGATION
├── apps/app/src/composables/useBreadcrumb.ts: walkNavTree + useBreadcrumb
└── apps/app/src/components-v2/layout/AppBreadcrumb.vue: primitive
2. AD-2.5-T1 implementation
├── @fontsource/inter installed
├── Public Sans deletions (verify package.json, main.css, index.html)
├── typography.spec.ts (regression lock)
└── COMMIT: feat(theme): revert font to Inter per AD-2.5-T1
3. AD-2.5-D1 implementation
├── useShellUiStore.applyDomAttributes() rewrite
├── Stray .dark class removals
├── useShellUiStore.spec.ts extension
└── COMMIT: refactor(theme): dark mode class on <html> per AD-2.5-D1
4. AD-2.5-W1 + AD-2.5-B1 implementations
├── WorkspaceSwitcher.vue: remove sub slot + Storybook story update
├── SidebarNav refactor to read APP_NAVIGATION
├── useBreadcrumb.spec.ts (4 specs)
└── COMMIT: feat(layout): central navigation registry per AD-2.5-B1
5. Shell-parity fixes 1–5 (topbar + sidebar structure)
├── Fix 1: remove topbar brand
├── Fix 2: AppTopbar #start = AppBreadcrumb
├── Fix 3: WorkspaceSwitcher to bottom
├── Fix 4: no sub (implementation already in step 4)
├── Fix 5: dropdown panel per crewli-starter
└── COMMIT: fix(shell): parity fixes 1–5 per RFC-WS-PRIMEVUE-PLAN-2-5 §5
6. Shell-parity fixes 6–10 (remaining)
├── Fix 6: dark mode (implementation already in step 3)
├── Fix 7: content top-offset verification
├── Fix 8: remove ▼ arrow
├── Fix 9: sidebar bottom region (requires crewli-starter source paste)
├── Fix 10: topbar right-side order/state + density toggle wired to useShellUiStore (+ crewli-starter source paste for icon ID)
└── COMMIT: fix(shell): parity fixes 6–10 per RFC-WS-PRIMEVUE-PLAN-2-5 §5
7. CREWLI-DESIGN-TOKENS.md
├── Inventory section: every :root variable from crewli-starter
├── Classification per variable
├── Typography section: full AD-2.5-T1 record
├── DEFERRED section stubs
└── COMMIT: docs(theme): CREWLI-DESIGN-TOKENS.md phase-1 inventory
8. Closure docs
├── Update RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md §4 (dark mode supersession)
├── Update RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md §7.4 (WorkspaceSwitcher sub note)
├── Update BACKLOG.md: close MIGRATION-PRIMEVUE-PLAN-2-5; open MIGRATION-PRIMEVUE-PLAN-2-5-PARITY-BATCH and MIGRATION-PRIMEVUE-PLAN-2-5B (deferred tokens)
└── COMMIT: docs(rfc): close PrimeVue Plan 2.5
9. Merge to main
Gates:
- Steps 1–4 each block the next step (foundation has dependencies).
- Step 5 requires steps 1–4 complete (uses AppBreadcrumb, WorkspaceSwitcher, etc.).
- Step 6 requires step 3 complete (Fix 6 =
AD-2.5-D1). - Step 9 requires
pnpm exec vitest rungreen ANDpnpm exec eslint apps/app/src/components-v2 apps/app/src/composablesgreen (scoped lint, not whole-codebase due to known formatter OOM).
9. Definition of Done
A Plan 2.5 closure is complete only when all of the following hold:
- All 10 shell-parity fixes implemented and visually verified at
≥lg - All 4 ADs (
T1,D1,W1,B1) implemented in code - All ~12 new unit/component specs green
- Full Vitest suite green:
pnpm exec vitest runexits 0 - Scoped lint green:
pnpm exec eslint apps/app/src/components-v2 apps/app/src/composables apps/app/src/config apps/app/src/composables apps/app/src/stores - No new
anytypes introduced;pnpm exec vue-tsc --noEmit -p apps/app/tsconfig.jsonclean CREWLI-DESIGN-TOKENS.mdexists with Typography section fully populatedRFC-WS-GUI-REDESIGN-CREWLI-STARTER.md§4 and §7.4 updated with supersession notesBACKLOG.mdupdated: closeMIGRATION-PRIMEVUE-PLAN-2-5, open follow-ups- Public Sans fully removed from code, dependencies, and HTML (
grep -rn "public.sans\|Public Sans" apps/app/returns no hits) - Every commit references the RFC:
per RFC-WS-PRIMEVUE-PLAN-2-5 §Xorper AD-2.5-X - Drift-check:
.claude-sync/regenerated and uploaded to Project Knowledge git push origin mainafter final closure commit
10. Implementation plan
Plan 2.5 will be executed by Claude Code in sequential phases matching §8. Each phase corresponds to one Claude Code prompt, authored by Claude Chat in a follow-up message:
| Phase | Prompt scope | Estimated complexity |
|---|---|---|
| P1 | Foundation: theme.ts + navigation.ts + useBreadcrumb + AppBreadcrumb | Medium |
| P2 | AD-2.5-T1: Inter revert + regression lock | Small |
| P3 | AD-2.5-D1: dark mode rewrite | Small–Medium (audit-then-edit) |
| P4 | AD-2.5-W1 + sidebar/breadcrumb wiring | Medium |
| P5 | Shell fixes 1–5 | Medium |
| P6 | Shell fixes 6–10 (incl. crewli-starter source paste for Fix 9) | Medium |
| P7 | CREWLI-DESIGN-TOKENS.md authoring | Medium (inventory work) |
| P8 | Closure docs (RFC updates + BACKLOG.md) | Small |
Prompts are written one phase at a time, each in its own chat message, copy-paste-ready for Claude Code. Each prompt begins with Read /CLAUDE.md and /dev-docs/RFC-WS-PRIMEVUE-PLAN-2-5.md before starting.
11. Open items / deferred
Deferred to a follow-up RFC
WorkspaceSwitcher.subdata-source decision — seeAD-2.5-W1rationale; re-introducingsubrequires a separate RFC with explicit C1/C2/C3 choice- CREWLI-DESIGN-TOKENS Phase-2 decisions — surface tones, focus-ring, border-radius, font-size scale, spacing rhythm. Either Plan 2.5b or absorbed into Plan 4.
- Mobile parity sprint — any mobile visual divergences beyond design-doc §4 — separate sprint, post-Plan 4 or in parallel
Deferred to unblocked parity-batch work item
- The 4 DEFERRED-HITL Plan 3 baseline captures (
AppTopbar,WorkspaceSwitcheropen/closed,AppShellV2integrated) - Any new Plan 2.5 shell baselines (post-fix capture)
ENERGYDOTS-NAN,DRAGGABLEBLOCK-POINTERCANCEL,AD3-MENUBARPlan 3 backlog items remain Plan 3 backlog — not absorbed into Plan 2.5
Plan 4 scope (informational, not part of this RFC)
Plan 4 designs and implements the template layer — PageTemplate, DataTablePage, DetailPage, FormPage higher-order layouts that compose Plan 3 primitives + Plan 2 shell. Plan 4 RFC will be authored after Plan 2.5 merges and parity-batch baselines capture cleanly. Plan 4 prerequisites:
- Plan 2.5 closed (this RFC)
- Parity-batch baselines captured against the corrected shell
CREWLI-DESIGN-TOKENS.mdPhase-1 in place (template layer reads from token decisions)
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):
RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md§4 — add supersession note forAD-2.5-D1(dark mode class-on-<html>replaces<html data-theme>)RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md§7.4 — add supersession note forAD-2.5-W1(sub removed in Plan 2.5)BACKLOG.md— closeMIGRATION-PRIMEVUE-PLAN-2-5; openMIGRATION-PRIMEVUE-PLAN-2-5-PARITY-BATCH(visual capture) andMIGRATION-PRIMEVUE-PLAN-2-5B-TOKENS(deferred token decisions)CLAUDE.md— no update required if it doesn't reference dark-mode mechanism or workspace shape; verifyCREWLI-DESIGN-TOKENS.md— newly created, Phase-1 content per §6.3
The .claude-sync/ regeneration runs automatically via the post-commit hook. Manual re-upload to Project Knowledge is required (no public upload API).
Appendix B — Why not absorb Plan 2.5 into Plan 3 closure docs
Considered: extend the Plan 3 closure docs commit (637d77b3) with the 10 shell fixes and call it Plan 3 completion.
Rejected because:
- Scope coherence. Plan 3 is "Tier-1 primitives." The 10 fixes are shell parity. Distinct concerns; mixing them dilutes commit history.
- AD ownership. The four ADs in this RFC are architectural decisions, not primitive bugfixes. They deserve a named RFC for future reference (someone in 2027 finding a
.darkclass on a component-root in a code review can search "AD-2.5-D1" and land here). - Parity-batch gating. Plan 3 closure happened before the shell-parity audit. Backporting fixes into Plan 3 obscures that the audit identified the gap after Plan 3 — the chronology matters for understanding why the DEFERRED-HITL baselines were chosen.
End of RFC.