Manual smoke showed the expanded trigger flush against the rail edges:
the wrapper used px-4 when collapsed but only p-2 when expanded.
Unified the expanded wrapper to `px-4 py-2`, matching the collapsed
state and the header brand row, so the rounded trigger card is inset
16px from both rail edges with symmetric breathing room consistent
with the header's px-4 rhythm.
Trade-off documented in the wrapper comment: with px-4 wrapper + p-2
internal trigger padding, the expanded avatar sits ~24px from the
rail edge — ~8px deeper than the bare header logo at 16px. Accepted
as deliberate (the expanded trigger reads as a distinct rounded
button card). The bare-square / logo mirror lives in the collapsed
state, which is unchanged.
No spec changes — the prior assertions cover the collapsed wrapper
(px-4 + h-[56px]) and the expanded trigger's rounded-lg shape, both
still hold. Suite delta: 564 -> 564.
Desktop only. vue-tsc clean. Scoped ESLint clean (0 errors).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Manual smoke after the logo-anchor fix (63300e5f) showed the footer
WorkspaceSwitcher used different spacing and a boxed avatar vs the
header logo. Applied the identical alignment recipe so the workspace
avatar mirrors the brand logo in both states (Option B from the
brief — bare square when collapsed, padded trigger only when
expanded).
Changes (WorkspaceSwitcher.vue only):
- Wrapper toggles `h-[56px] flex items-center px-4` when collapsed
(mirrors the SidebarHeader brand row) and stays `p-2` when
expanded (room for the padded trigger).
- Collapsed: a BARE rounded-lg avatar button at the same 16px left
offset as the header logo. No `.trigger` container, no rounded
hover-bg box wider than the avatar — the button IS the visible
square (`.ws-logo .ws-logo-square w-8 h-8 rounded-lg`). True
top/bottom mirror of the brand square.
- Expanded: unchanged padded `.trigger` button with avatar + name +
chevron + hover bg. Avatar's left offset stays at 16px from the
rail (wrapper p-2 + trigger p-2) so the expanded avatar also
lines up vertically with the header logo.
Same alignment equation as the header recipe:
rail_collapsed (64px) = square (32px) + 2 × px-4 (2 × 16px)
In both states the avatar's left edge sits at x=16px from the
rail's left — identical to the brand logo above. Vertical line
down the left side now reads as a single column of squares.
Desktop only. Mobile drawer chrome stays as MOBILE-SHELL-PARITY.
Tests adapted:
- `expanded trigger uses rounded-lg` (was tested in both states; the
collapsed render no longer has a `.trigger` container).
- `expanded trigger has no justify-center` (split from the
prior two-state assertion).
- New: `collapsed renders a bare avatar button (no .trigger
container, just .ws-logo)` — locks the bare-square contract.
- New: `collapsed wrapper uses px-4` — locks the
centring-equation invariant (rail=square+2×px-4) against
accidental wrapper-padding regressions.
Suite delta: 563 → 564 (+1 net: +2 new collapsed-shape asserts,
−1 redundant two-state assert).
vue-tsc clean. Scoped ESLint clean (0 errors, pre-existing
warnings only). Manual smoke pending Bert — draw a vertical line
down the rail's left edge and verify the brand square and the
workspace square left edges sit on it in both states; in collapsed
mode verify the avatar is a bare square (no boxed button), same
visual treatment as the bare logo above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refines the prior sidebar styling commit (8e166512) after manual smoke:
- Brand logo + workspace avatar: rounded-xl -> rounded-lg (crisper
square per design review; both stay unified at the same radius).
- Logo no longer jumps on collapse. The previous code toggled
`justify-between` ⇄ `justify-center` on the header row, which
re-centred the logo against the parent's width — and the parent
width animates from 256px to 64px over 200ms, so the logo slid
from x≈112px (centred in the expanded rail) to x=16px (centred in
the collapsed rail). Visible jump.
Fix: the brand row is now ALWAYS `px-4` and left-aligned. The
logo's horizontal offset (16px from the rail's left edge) is
identical in expanded and collapsed states. Why this still looks
centred when the rail collapses:
rail_collapsed (64px) = logo (32px) + 2 × px-4 (2 × 16px)
With those numbers aligned, a stationary left-aligned logo IS
visually centred in the 64px-wide collapsed rail. The width
transition then "slides the rail closed around" the anchored
logo. Wordmark + Beta badge sit to the RIGHT of the logo and
v-if-disappear on collapse; their absence doesn't shift the logo
because they were never to its left.
- Toggle chevron placement:
- Expanded: collapse chevron (◀) inline at the right of the brand
row, pushed by `ms-auto` (NOT by justify-between forcing the
layout to recentre the logo).
- Collapsed: a SECOND row below the brand row holds a centred
expand chevron (▶) button. Replaces the prior tucked-chip that
overlapped the logo. No overlap, no overhang needed against the
aside's `overflow-hidden`.
- WorkspaceSwitcher trigger: same anchor-on-the-left treatment
applied. The `justify-center` switch on collapse is gone (it
caused an identical avatar slide). Wrapper padding `p-[10px]` ->
`p-2` so the avatar's left offset (wrapper 8 + trigger 8 = 16px)
matches the SidebarHeader logo (px-4 = 16px) — the brand square
and the workspace square are now vertically aligned in the
collapsed rail.
Desktop only. Mobile drawer chrome tracked separately as
MOBILE-SHELL-PARITY.
Tests adapted:
- WorkspaceSwitcher.spec.ts: trigger-rounded assertion bumped
rounded-xl -> rounded-lg; +1 spec locks "trigger never carries
justify-center" (avatar-anchored invariant).
- SidebarHeader.spec.ts: collapsed-behaviour spec rewritten — was
asserting `justify-center`, now asserts the row carries `px-4`
WITHOUT either justify-center OR justify-between (the actual
anchor contract); +1 spec confirms the expand chevron lives in a
SIBLING row of the brand mark (no overlap).
Suite delta: 561 -> 563 (+2). vue-tsc clean. Scoped ESLint clean
(0 errors, pre-existing warnings only).
Manual smoke pending Bert: collapse/expand slowly and watch the
logo — must NOT move horizontally. Confirm rounded-lg looks crisp
(not pebble-soft). Confirm expand chevron sits in its own row
below the logo, no overlap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan 2.5 P6 follow-up. Closes two desktop shell-parity gaps from P6
manual smoke against crewli-starter SoT:
- Sidebar logo decoupled from the collapse toggle. Expanded layout is
now justify-between (brand group left, collapse chevron right) and
collapsed layout is justify-center with the logo alone. The expand
affordance becomes a small absolute-positioned circular button at
the rail's right edge — solid background + border so the slight
overlap with the centred logo reads as a tucked-in chip rather than
a collision. Toggling collapsed no longer shifts the logo.
- Brand square (SidebarHeader logo) and workspace avatar
(WorkspaceSwitcher trigger) unified to the same rounded square
(h-8 w-8 rounded-xl). Existing sizes were already consistent at
32px — radius bumped from rounded-lg (8px) / var(--p-border-radius)
(~6px) to rounded-xl (12px) per the design direction. Collapsed
rail now reads as a vertical mirror: brand square at the top,
avatar square at the bottom, bracketing the nav icons.
- WorkspaceSwitcher trigger restyled: rounded-xl (was the sharper
var-radius), p-2 (was px-[10px] py-[8px]), hover background. The
collapsed-variant gating of name + chevron is unchanged from P5.
Edge-mounted overhang past the rail edge was not possible: the aside
carries `overflow-hidden` (intentional, for the w-64 ⇄ w-16 width
transition) which clips anything past the rail edge. The tucked-chip
pattern (24px circle at end-0, solid bg) is the visual compromise —
the affordance stays inside the rail, discoverable, and visually
decoupled from the logo.
Desktop only. Mobile drawer chrome (logo placement, drawer X button,
missing switcher) tracked separately as MOBILE-SHELL-PARITY.
Tests:
- +2 WorkspaceSwitcher.spec.ts: trigger uses rounded-xl; collapsed
trigger renders avatar only (hides .meta).
- +1 SidebarHeader.spec.ts: collapsed row hides the .brand-name
wordmark and toggles to justify-center (logo-stays-centred lock).
Suite delta: 558 → 561 (+3). vue-tsc clean. Scoped ESLint clean
(0 errors, pre-existing warnings only).
Manual smoke pending Bert: collapse the rail, verify logo stays put
and the expand chip appears at the right edge; verify the trigger
shows rounded corners + hover bg; verify the collapsed avatar
mirrors the brand square size/radius.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per RFC-WS-PRIMEVUE-PLAN-2-5 §5.6–§5.10. Final code phase of Plan 2.5
before closure docs (P7 tokens, P8 closure).
Changes:
- Fix 9: sidebar full-height. The desktop <aside> now carries
`h-screen sticky top-0` so it fills the viewport vertically and
pins to top on body scroll. Without this the aside sized to its
children's intrinsic heights (~250-400px) and ended mid-viewport
even though the surrounding grid row stretched to 100vh. With
h-screen, SidebarNav's `flex-1` claims the remaining column space
and WorkspaceSwitcher anchors to the true viewport bottom — its
`border-t` (existing from P5) is now the divider above the
switcher per crewli-starter. Mobile Drawer untouched (PrimeVue's
internal pt classes already give it 100% panel height).
- Fix 10: density toggle promoted to the store. New
useShellUiStore.toggleDensity() flips comfortable ⇔ compact and
calls applyDomAttributes() synchronously. AppTopbar's local
toggleDensity wrapper deleted; the button now invokes
shell.toggleDensity() directly and carries a stable
data-testid="density-toggle" plus a `title` matching its
aria-label. Density icons swapped from generic flex-alignment
glyphs (tabler-layout-distribute-{vertical,horizontal}) to the
literal density metaphor (tabler-baseline-density-{small,medium}).
Both new icons verified present in the loaded
@iconify-json/tabler set. Topbar right-side order
(search → density → dark → notifications → user) was already
correct from P5; locked with a new ordering spec.
Verified (no code change):
- Fix 6 (§5.6): dark mode `.dark` on <html> confirmed in
useShellUiStore.applyDomAttributes (AD-2.5-D1, P3 complete).
Component-level dark coverage remains a separate backlog item
(DARKMODE-V2-COVERAGE).
- Fix 8 (§5.8): the ▼ arrow is the Vue DevTools v8.0.2 dev-only
toggle button injected by the devtools vite plugin, not Crewli
code — diagnosed, no action.
- Fix 7 (§5.7): non-reproducible at code level. Topbar is
`sticky top-0` and is a SIBLING flex item of <main> inside the
shell's flex-col right column; normal flow stacks <main> below
the topbar at first paint, so the title cannot fall behind a
sticky topbar in this composition. Documented as no-op; if
Bert reproduces it after Fix 9 lands, the symptom is something
else (likely a per-page negative margin or a separate scroll-
container interaction worth its own ticket).
Density enum corrected against runtime data-density: 'comfortable'
(not 'comfy' — the earlier RFC assumption is wrong; the store has
always typed `'comfortable' | 'compact'`).
Tests:
- +2 useShellUiStore.spec.ts: toggleDensity flips comfortable ⇔
compact AND writes data-density via applyDomAttributes;
toggleDensity from compact returns to comfortable on call 2.
- +2 AppTopbar.spec.ts: density button reachable by
data-testid="density-toggle"; topbar right-side order locked
via HTML index comparison (search → density → dark → notif →
user). Existing density-flip specs adapted to spy on
toggleDensity (the new direct call site).
Suite delta: 554 → 558 (+4). vue-tsc clean. Scoped ESLint clean
(0 errors, pre-existing warnings only).
Manual smoke pending Bert:
1. Sidebar full-height, switcher pinned to viewport bottom (Fix 9)
2. Page title clears topbar (Fix 7 — expected no change needed)
3. Density toggle visible between search and dark with the
density icon (Fix 10)
4. Click density toggle → spacing visibly changes, <html
data-density> flips between comfortable and compact (Fix 10)
5. Topbar order: search → density → dark → notifications →
avatar (Fix 10)
6. Dark mode still toggles (Fix 6 regression)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P2 reverted CSS font-family to Inter but missed the JS font-loading
path: src/plugins/webfontloader.ts loaded Public Sans from Google
Fonts via WebFont.load(). The wf-publicsans-n4-active class on <html>
(found during P5 manual smoke) proved Public Sans was still loaded at
runtime, plus an external Google Fonts CDN request — both contrary to
AD-2.5-T1 (local @fontsource/inter, no CDN).
Audit context: the plugin was auto-registered via the Vuexy
registerPlugins() glob (src/@core/utils/plugins.ts walks
plugins/*.{ts,js}). No explicit import / call site to delete — file
removal is enough. The plugin only ever loaded Public Sans (no other
families), so full deletion is correct.
Changes:
- Removed src/plugins/webfontloader.ts (auto-registration falls away
with the file itself; no manual unregister needed).
- Removed webfontloader (1.6.28) + @types/webfontloader (1.6.38) from
package.json / pnpm-lock.yaml.
- Strengthened tests/unit/styles/typography.spec.ts with a new
describe block that scans every src/plugins/*.ts for: any
webfontloader reference, any WebFont.load call, any "Public Sans"
spelling, any fonts.googleapis.com URL. Plus a regression-lock
spec asserting webfontloader.ts itself stays deleted.
Suite delta: 552 → 554 (+2 new JS-path specs). vue-tsc clean.
Scoped ESLint clean (0 errors).
Manual smoke pending (Bert): hard-reload /v2/dashboard, confirm
- wf-publicsans-* and wf-active classes are gone from <html>
- computed font-family on body text starts with "Inter"
- Network tab has no fonts.googleapis.com request
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to P5 (commit ac36dfe9). Vue warned about unresolved
AppBreadcrumb component in AppTopbar's #start slot — auto-import via
unplugin-vue-components did not register it because
components-v2/ is outside the scan path (Components({ dirs: [...] })
covers src/@core/components, src/views/demos, src/components only).
The original P5 edit did include this import line, but a formatter
pass appears to have pruned it as "unused" before runtime parsed the
template; the symbol was unresolved at render and the warning
surfaced. Restored explicitly so any future formatter pass keeps it.
Fix 2 (AppTopbar #start = AppBreadcrumb) now functions visually,
not just structurally. Manual smoke pending (Bert).
Follow-up backlog: AUTO-IMPORTS-V2-SCAN — extend Components({ dirs })
to include src/components-v2/ so the v2 chrome can rely on the same
auto-import ergonomics as v1. Not done here to keep the fix surgical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per RFC-WS-PRIMEVUE-PLAN-2-5 §5.1–§5.5 plus the AD-2.5-W1 option-A
supersession (no sub on dropdown items either, accepted divergence).
Atomic changes:
- AppTopbar: brand block (gradient "C" mark + Crewli wordmark) removed
per Fix 1; the #start slot now renders <AppBreadcrumb /> per Fix 2.
Legacy meta-based useBreadcrumb consumption (breadcrumbModel computed,
vue-router useRouter import, command-based PrimeVue Breadcrumb model)
is gone; AppBreadcrumb owns the registry-driven path. Dead
topbar-mark-shadow scoped CSS rule deleted.
- AppBreadcrumb: import updated to the renamed useBreadcrumb.
- AppSidebar: docstring updated to make the Fix 3 vertical order
(Header → Nav → Switcher, switcher bottom-anchored) explicit. No
template change needed: SidebarNav's root <nav class="flex-1"> already
fills available column space, naturally pushing WorkspaceSwitcher to
the bottom (two flex-1 siblings would split the column 50/50 and
compress the nav — a separate spacer element is structurally wrong).
- WorkspaceSwitcher: dropdown panel restructured per crewli-starter
reference. Semantic class markers (.popover-head/.title/.link/.list/
.opt/.is-current/.ws-logo/.name/.check-mark/.foot) added alongside
Tailwind utilities so specs assert structure with stable selectors.
Footer buttons wired to placeholder createWorkspace / inviteUser
handlers (console.warn + TODO) until the flows ship. Manage link
stays a non-navigating label (no v2-workspaces-manage route yet).
No sub line on any dropdown row (AD-2.5-W1 option A).
Atomic legacy useBreadcrumb retirement (planned since P1):
- Legacy route-meta-driven useBreadcrumb + toBreadcrumbItems +
BreadcrumbRouteRecord types deleted entirely (only AppTopbar
consumed it, and that consumption is gone after Fix 2).
- useNavBreadcrumb → useBreadcrumb (single SoT for breadcrumb chain).
- NavBreadcrumbItem → BreadcrumbItem.
- AppBreadcrumb.vue import updated to the new name.
- SidebarNav.vue docstring reference scrubbed to the new name.
- useBreadcrumb.spec.ts: 10 legacy toBreadcrumbItems specs removed;
4 walkNavTree specs retained.
AppTopbar.spec.ts:
- vue-router mock simplified (route.matched no longer relevant).
- AppBreadcrumb stubbed in #start; legacy command-vs-route assertion
removed; new spec verifies AppBreadcrumb is rendered.
WorkspaceSwitcher.spec.ts: 5 new dropdown specs (header / row count /
current-row checkmark / footer buttons / no-sub on rows).
Suite delta: 557 → 552 (−5 net: −10 legacy toBreadcrumbItems specs,
+5 Fix 5 dropdown specs, −1 obsolete AppTopbar breadcrumb-model spec,
+1 new AppTopbar AppBreadcrumb-presence spec).
vue-tsc clean. Scoped ESLint clean (0 errors). All 3 re-grep checks
returned 0 hits (useNavBreadcrumb/NavBreadcrumbItem, topbar brand
selectors, standalone "sub" identifier in WorkspaceSwitcher — only
documentation comments referencing the no-sub state remain, which
describe absence by design).
Manual smoke skipped (Auto Mode); coverage from the post-edit specs
includes AppBreadcrumb-in-#start, dropdown structure, and trigger
no-sub. Recommend Bert run `pnpm --filter crewli-app dev` and verify
the 6 checks listed in the prompt before merging.
Known divergence from crewli-starter (accepted):
- Dropdown rows are ~16px shorter than crewli-starter (no sub line).
Tracked as WORKSPACE-DROPDOWN-SUB-CONTENT for a future RFC with
the required backend scope (organisations.type enum + metrics).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P4 (Plan 2.5, AD-2.5-W1 + AD-2.5-B1) refactored SidebarNav to read
APP_NAVIGATION directly, retiring the OrganizerLayoutV2 → useV2Nav →
AppSidebar :groups → SidebarNav :groups props chain. Five artifacts
were deliberately left in place to keep the P4 diff focused — this
commit removes them.
Deleted:
- src/composables/useV2Nav.ts (+ spec) — v1→v2 nav fold adapter, no
production consumer post-P4
- src/types/v2/nav.ts — V2NavGroup / V2NavItem types, only consumed
by the deleted composables above. types/v2/ directory removed (empty)
- src/components-v2/layout/sidebarNavActive.ts (+ spec) — pure helper,
SidebarNav now uses inlined active check against NavItem.routeName
- navFixture export + V2NavGroup import from stories/v2/_helpers.ts
Also: stale "useV2Nav(orgNavItems)" reference scrubbed from
OrganizerLayoutV2.vue docstring (the function no longer exists; the
comment now describes the retired plumbing generically).
Suite delta: 575 → 557 (−18 specs). The drop is correct — the removed
specs tested deleted dead code (sidebarNavActive: 8 specs, useV2Nav:
10 specs), not contract behaviour.
vue-tsc clean. Scoped ESLint clean (0 errors). Final re-grep on all
deleted symbols (useV2Nav, V2NavGroup, V2NavItem, sidebarNavActive,
navFixture) returns zero hits across apps/app/src/.
Per zero-compromise gap 5 (delete > adapt): orphans don't stay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per RFC-WS-PRIMEVUE-PLAN-2-5 §4 AD-2.5-W1 and AD-2.5-B1, §5.4 Fix 4.
Changes:
- WorkspaceSwitcher: sub field removed from template, WorkspaceDisplay
type, and buildDisplay derivation. Stories did not carry sub args
(auto-derived from seeded org.role); no WithSub story existed. New
regression spec (WorkspaceSwitcher.spec.ts) locks the no-sub render.
- SidebarNav: now consumes APP_NAVIGATION from src/config/navigation.ts
as the single source of truth (shared with breadcrumb derivation in
useNavBreadcrumb). The groups: V2NavGroup[] prop is removed; render
walks top-level NavItems (branch nodes render label-heading + children;
leaf nodes render as rows; items without routeName render as
non-clickable dormant placeholders). Previous nav data source:
groups prop fed by useV2Nav(orgNavItems) in OrganizerLayoutV2.
- APP_NAVIGATION expanded with 7 entries to preserve visual sidebar
continuity (Evenementen at top-level + Beheer branch with 5 children).
All new entries use routeName: undefined until the corresponding v2
page lands (TODOs noted per entry); only Dashboard maps to v2-dashboard.
- AppSidebar: groups prop removed; passes only :collapsed to SidebarNav.
- OrganizerLayoutV2: useV2Nav(orgNavItems) plumbing retired; the layout
now renders <AppSidebar /> with no nav-data wiring.
- Tests: AppSidebar.spec drops the "passes groups prop to SidebarNav"
assertion; OrganizerLayoutV2.spec drops the "forwards orgNavItems"
assertion. New WorkspaceSwitcher no-sub regression spec (+2 tests).
- Storybook: SidebarNav.stories and AppSidebar.stories updated to no
longer thread navFixture/groups; WithActiveItem pushes v2-dashboard.
Position of WorkspaceSwitcher (Fix 3), workspace dropdown panel (Fix 5),
and AppBreadcrumb wiring (Fix 2) remain unchanged in P4 — both lands in
P5. The legacy useBreadcrumb composable also remains untouched until P5
(atomic with AppTopbar refactor).
Orphans flagged for follow-up cleanup (intentionally not deleted in P4):
useV2Nav composable + spec, V2NavGroup/V2NavItem types, sidebarNavActive
helper + spec, navFixture in stories/v2/_helpers.ts.
Suite delta: 575 → 575 (+2 WorkspaceSwitcher no-sub spec, -1 AppSidebar
groups-prop assertion, -1 OrganizerLayoutV2 groups-forward assertion).
vue-tsc clean. Scoped ESLint clean (0 errors).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per RFC-WS-PRIMEVUE-PLAN-2-5 §4 AD-2.5-D1 and §5.6 Fix 6. Single class
on <html> drives both PrimeVue darkModeSelector and Tailwind v4
@custom-variant dark — one toggle, two ecosystems react.
Audit findings (pre-change):
- applyDomAttributes was writing BOTH data-theme="dark" AND .dark on
documentElement. The historic data-theme write is the design-doc §4
mechanism that AD-2.5-D1 supersedes; the .dark toggle was already
correct (and is already paired with PrimeVue darkModeSelector: '.dark'
in plugins/primevue/index.ts:31, verified in P1).
- tailwind.css had NO @custom-variant dark directive — Tailwind v4
default is `prefers-color-scheme` (OS-controlled), so utility
`dark:` variants would have ignored the topbar toggle entirely.
- One stray .dark subtree wrapper in AppTopbar.stories.ts:56
(DarkTheme story) — deliberate Storybook isolation per its comment,
but in violation of AD-2.5-D1's single-source-of-truth rule.
Changes:
- useShellUiStore.applyDomAttributes(): removed data-theme write,
kept .dark class toggle on document.documentElement, kept
data-density (P6 wires density-toggle UI; density is an
orthogonal axis and unaffected). File-header comment updated to
cite AD-2.5-D1 + reference the Tailwind & PrimeVue mirror sites.
- assets/styles/tailwind.css: added
`@custom-variant dark (&:where(.dark, .dark *))` so utility
`dark:` classes resolve via the same .dark trigger.
- components-v2/layout/AppTopbar.stories.ts: stripped class="dark"
from the DarkTheme story's render wrapper. Story comment updated
to flag that visual confirmation now comes via parity-batch
Playwright (after Plan 2.5 closes), not Storybook autodocs. A
proper documentElement-mutating decorator is a backlog item.
- stores/__tests__/useShellUiStore.spec.ts: updated the existing
applyDomAttributes assertion to drop the data-theme expectation
(the write is gone); added a new `describe('applyDomAttributes
— dark mode (AD-2.5-D1)', …)` block with 2 specs (class toggle
reactive, no data-theme attribute written).
Re-grep verification — all three return 0 hits:
- stray .dark in v2 (excluding `dark:` utility prefixes)
- data-theme setAttribute calls in stores/
- [data-theme=…] CSS selectors anywhere
Suite delta: 573 → 575 (+2). vue-tsc clean. Scoped ESLint clean.
Note: darkModeSelector: '.dark' was already set in
plugins/primevue/index.ts:31 (verified in P1 audit) — the config
dimension of AD-2.5-D1 was satisfied before this commit; P3 closes
the store-side, Tailwind-side, and stray-class dimensions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per RFC-WS-PRIMEVUE-PLAN-2-5 §4 AD-2.5-T1. Establishes Inter as the
canonical Crewli body font via @fontsource/inter (local package, no
Google Fonts CDN).
Audit findings (pre-change):
- No @fontsource/public-sans package was installed.
- No <link> tag in index.html loaded Public Sans.
- Only one Public Sans reference existed in source: the vendored
Vuexy SCSS variable $font-family-custom at
src/@core/scss/template/libs/vuetify/_variables.scss, which drives
Vuetify's $body-font-family on legacy surfaces during F4.
- No src/main.css exists; the Tailwind v4 entry lives at
src/assets/styles/tailwind.css with no @theme block yet.
Changes:
- @fontsource/inter@^5.2.8 added to dependencies; weights
400/500/600/700 imported at main.ts ahead of tailwind.css.
- src/assets/styles/tailwind.css: new @theme block declaring
--font-sans Inter-first, plus :root --crewli-font-family and
html/body font-family applying that variable cascade-wide.
- src/@core/scss/template/libs/vuetify/_variables.scss:
$font-family-custom switched from the historical body font to
Inter (vendored edit, narrowly scoped, F6 removes @core/ entirely).
- tests/unit/styles/typography.spec.ts: 3-spec regression lock
(Tailwind direct stacks, Vuexy SCSS variable, zero historical
references in either file). File-content inspection — jsdom does
not cascade from imported stylesheets, so getComputedStyle would
always pass.
Suite delta: 570 → 573 (+3; the prompt's template was +2 but the
audit revealed two distinct font-config files, so each gets its own
assertion per the prompt's "cover all sites" rule). vue-tsc clean.
Scoped ESLint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per RFC-WS-PRIMEVUE-PLAN-2-5 §8 step 1. Foundation scaffolding only —
no shell fixes, no Public Sans removal, no useShellUiStore changes
(P2–P6 scope).
Implements:
- theme darkModeSelector verified at '.dark' (already correct in
plugins/primevue/index.ts — config site is here, not theme.ts).
- src/config/navigation.ts: APP_NAVIGATION registry per AD-2.5-B1
(Dashboard entry only — v2-dashboard is the only v2 route today).
- src/composables/useBreadcrumb.ts: walkNavTree pure helper +
useNavBreadcrumb composable per AD-2.5-B1. The legacy meta-based
useBreadcrumb is preserved (consumed by AppTopbar, P1 may not
touch AppTopbar); P4 retires it and renames useNavBreadcrumb.
- src/components-v2/layout/AppBreadcrumb.vue: layout primitive
wrapping PrimeVue Breadcrumb, consuming useNavBreadcrumb.
- Tests: walkNavTree (4 specs, co-located), AppBreadcrumb mount
(2 specs, tests/component/layouts/).
Suite 564 → 570 (+6, all new specs green). vue-tsc clean. Scoped
ESLint clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Vitest hoists vi.mock()/vi.hoisted() above all imports, so the
component import can sit with the other imports (import/first satisfied)
without the eslint-disable-next-line directives — the mock factories
only deref their refs at mount time. Honors the no-eslint-disable rule.
28/28 affected specs green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The project's no-unused-vars only ignores all-underscore names (/^_+$/u);
`_bp` in the @vueuse/core useBreakpoints mock failed it. Latent since
Task 3 — masked because the whole-codebase `pnpm lint` stylish formatter
OOMs (RangeError on the legacy-code message volume) and emitted no
results. Scoped errors-only lint surfaced it. 21/21 specs still green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Code-review follow-up. AppTopbar DarkTheme mutated <html>.dark which
leaked into Default/CompactDensity stacked on the same autodocs page;
scope dark to the story subtree via a `.dark` wrapper (Aura
darkModeSelector is the `.dark` class) — verified isolated on the docs
page. Also factor the duplicated render scaffolds in AppDialog (shared
dialogStory factory) and WorkspaceSwitcher (meta-level render).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the Plan-1 skeleton stubs: OrganizerLayoutV2 now fills
AppShellV2's #sidebar/#topbar/#drawer slots with the ported AppSidebar /
AppTopbar / RightDrawer and sources orgNavItems via useV2Nav() (legal
now that OrganizerLayoutV2.vue is the layouts-v2 zone). AppShellV2 is
unchanged; its contract test stays green. New component test locks the
composition (right component per slot, :groups forwarded, no skeleton).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AD-G2 ("OrganizerLayoutV2 wraps AppShellV2") was in tension with AD-G5:
src/layouts/OrganizerLayoutV2.vue classified as the v1 `layouts` zone,
which is deliberately barred from components-v2. New `layouts-v2` zone
(src/layouts/*V2*.vue, mode:file) gets pages-v2-equivalent v2 capability;
the v1 `layouts` zone is unchanged so v2 isolation is preserved. RFC
AD-G5 amended; locked by 3 boundaries-v2.spec.ts regression tests (7/7).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
vue/no-reserved-component-names is error-level; <dialog> is native HTML.
Matching is via the stubs key + findComponent reference, not the stub's
own name, so the rename is behaviour-neutral (14/14 tests still pass).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces crewli-starter's hand-rolled Teleport/scrim/keydown Escape
pattern with a typed PrimeVue Dialog wrapper. v-model:open via writable
computed; slots #tabs/#footer gated; 12 unit tests all passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds :key="component" to <component :is="Body"> so opening drawer B
while A is open fully remounts (A's instance/state can't leak into B) —
a real defect for a primary shell overlay. Same-name reopen still
relies on the body reacting to prop changes (documented inline).
Companion test asserts the cross-component switch swaps the body
cleanly (A unmounts, B mounts). Addresses the Task 5 code-review
Important finding.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
unplugin-auto-import scans src/composables/; the new drawerRegistry
exports added global + vue-module declarations. auto-imports.d.ts is
tracked — keep it in sync (same precedent as prior composable syncs).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds composables/drawerRegistry.ts (boundary-safe register-by-call map:
register/resolve, zero static component imports — composables zone may
not import components, RFC-WS-GUI-REDESIGN AD-G5). Extends useRightDrawer
with resolveDrawerComponent (thin facade, prior API/tests preserved).
RightDrawer.vue: PrimeVue <Drawer position=right>, v-model:visible via a
writable computed ↔ useRightDrawer isOpen/close; title/flush read from the
open() props object (A4); dynamic <component :is> via resolveDrawerComponent
with a graceful empty state on null; #actions header slot retained. 18
unit/component tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- FIX A (IMPORTANT): PrimeVue Breadcrumb ignores `route` key; map non-last
items with `command: () => router.push(item.to)` for real client-side nav
- FIX B: add type="button" to all 6 native <button> chrome elements
- FIX C: authStore.logout() bare call matches project no-void pattern
- FIX D: document param-route edge case in toBreadcrumbItems
- FIX E: regression test asserts command+push on non-last, no command on last,
no `route` key on any item
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- useBreadcrumb composable: pure toBreadcrumbItems() helper + thin
useRoute() wrapper; route-driven, no prop coupling
- AppTopbar: hamburger→setMobileOpen, theme/density toggles→shell store,
PrimeVue Breadcrumb/OverlayBadge/Popover/Avatar/Menu; replaces all
manual document.mousedown listeners with PrimeVue built-in dismissal;
notifications stubbed (useNotificationStore is a toast queue, not a
feed — TODO TECH-WS-GUI-REDESIGN); sign-out→authStore.logout()
- Unit tests: 10 breadcrumb + 6 AppTopbar assertions (16 total, all pass)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CRITICAL: replace `lg:hidden` on PrimeVue Drawer with `v-if="isMobile"` so the
teleported portal/overlay is never created on desktop viewports regardless of
mobileOpen state. Replace useMediaQuery raw string in SidebarHeader with
useBreakpoints(breakpointsTailwind).smaller('lg') shared by both components.
Add desktop/mobile comments; adapt tests to useBreakpoints mock; add
Drawer-absent-on-desktop and aside w-16/w-64 width-class assertions (21 tests).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ports crewli-starter's monolithic AppSidebar.vue into two typed production
components: SidebarHeader (the .brand block) and AppSidebar (composing
SidebarHeader + SidebarNav + WorkspaceSwitcher). AppSidebar renders a
permanent <aside> on desktop (lg+) and a PrimeVue Drawer on mobile, both
wired to useShellUiStore for collapse/mobile state.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ports crewli-starter WorkspaceSwitcher into the Crewli SPA as production
TypeScript: PrimeVue Popover replaces the manual click-outside listener,
data is derived from useAuthStore/useOrganisationStore (no new store), gradient
pairs are deterministic via a new pure util with full Vitest coverage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- FIX 1: Replace <button @click="router.push"> with <RouterLink custom>
+ <a> for real link semantics (middle-click, ⌘-click, screen-reader);
custom isNavItemActive prefix-match stays the active source of truth;
adds :aria-current="page" on active items; drops useRouter/router.push.
RouterLink to prop cast via itemTo() helper (RouteLocationRaw from
unplugin-vue-router) to satisfy typed RouterLinkTyped<RouteNamedMap>.
- FIX 2: Align .nav-item comment to actual template values (py-[9px]
rounded-md, not CSS vars); replace inaccurate Tailwind v3/v4 before:
composability justification in <style scoped> with the real reason
(accent bar at left:-10px is clipped by the overflow-y-auto nav).
- FIX 3: text-left → text-start (logical property, RTL-safe).
- FIX 4: Document id=route-name assumption in useV2Nav.ts with a
one-line comment at the id: assignment.
- FIX 5: Reword misleading "dotted names" spec description to state
the real invariant (id = v1 route name, already kebab-case).
- FIX 6: Add 2 tests — useV2Nav wrapper .value equality, and
consecutive-headings edge case (empty-items group produced).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
unplugin-auto-import scans src/composables/, so the new useV2Nav added
a global + vue-module declaration. auto-imports.d.ts is tracked; keep
it in sync (same precedent as Plan 1's useRightDrawer sync).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ports crewli-starter's sidebar nav into the SPA as production TS:
V2NavGroup/V2NavItem types, a pure toV2NavGroups adapter wrapped by
useV2Nav(items) (composables zone can't import @/navigation, so the
v1 nav array is passed in — the layout supplies orgNavItems in Task 7),
a pure isNavItemActive helper, and SidebarNav.vue (props-only,
router-driven nav, route-based active state, collapsed mode, main.css
translated to Tailwind inline). 16 unit tests. Icon import is
allowed via the components-foundation bridge (no eslint-disable).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>