feat(layouts): rewrite layout shells with PrimeVue Drawer + Menubar + Avatar

Layout-shell rewrite per RFC AD-3, B7-option-B. R-10 isolation invariant
honored — this single commit is revertible to roll back the layout
change without losing B1–B6 progress.

New component (PrimeVue-only, no Vuetify imports per F3 hard constraint):
- apps/app/src/layouts/components/AppShell.vue (~210 lines)
  - Desktop sidebar (Tailwind grid, lg+ breakpoint) renders nav items
    as PrimeVue Buttons + Icons. Mobile (<lg) hides sidebar; PrimeVue
    Drawer slides in on hamburger toggle.
  - Top bar (Tailwind) has hamburger + title (mobile) and an Avatar +
    Menu (PrimeVue) for the user dropdown with "Mijn Profiel" and
    "Uitloggen" actions.
  - Nav items accept the existing { title, to: { name }, icon: { icon } }
    shape from src/navigation/vertical so call-sites stay terse.

Five top-level layouts delegate to AppShell (filename preserved per
AD-3 so vite-plugin-vue-meta-layouts continues to resolve routes
unchanged):
- default.vue       — org + (super-admin) platform nav
- OrganizerLayout   — same nav as default; matches authenticated org UX
- PortalLayout      — portal-specific 2-item nav ("Mijn evenementen",
                       "Mijn Profiel")
- blank.vue         — minimal chrome-less wrapper for login etc.
- PublicLayout      — minimal wrapper for public form-fill routes;
                       uses <main> for semantic structure

F3 functional regressions (intentional — F4 sub-packages reintroduce
each item through PrimeVue):
- NavSearchBar (Vuetify-heavy combobox/overlay) — absent from top bar
- ContextSwitcher (Vuetify VBtn + VMenu) — absent
- NavbarThemeSwitcher (Vuetify IconBtn) — absent; dark mode driven by
  PrimeVue's darkModeSelector: '.dark' continues to work via the
  existing @core skin classes until F6 cleanup
- NavbarShortcuts (Vuetify-heavy) — absent
- NavBarNotifications (Vuetify-heavy) — absent
- UserProfile from @/layouts/components/ (Vuetify-heavy menu) — replaced
  with the minimal Avatar + Menu dropdown described above; rich profile
  panel returns in F4
- ImpersonationBanner — absent; super-admin impersonation UX is F4 work
- PortalLayout event-mode vs platform-mode topbar (route.meta.navMode
  driven) — absent; F4 reintroduces via AppShell prop or slot
- Suspense + AppLoadingIndicator wrapping pages — dropped; pages handle
  their own loading via PrimeVue ProgressSpinner

VApp at App.vue level still wraps everything, so Vuetify components
inside still-Vuetify pages continue to render correctly during the
parallel-mode window.

Test updates (no Vuetify in layout structure to assert against anymore):
- OrganizerLayout.spec.ts — mocks AppShell instead of the deleted
  DefaultLayoutWithVerticalNav reference; provides Pinia.
- PortalLayout.spec.ts — same mock pattern; new structural assertions
  go through AppShell stub; the new third test verifies
  PortalLayout forwards portal nav items + title to AppShell.
- PublicLayout.vue — uses <main> for semantics; PublicLayout.spec.ts
  still passes unchanged.

Auto-generated component/auto-import dts files refreshed for the new
AppShell component (committed for stable dev workflow).

Verification:
- pnpm typecheck — clean.
- pnpm test — 402 tests pass (test count unchanged after spec rewrites).
- pnpm build — succeeds in 14.05s; AppShell chunk is ~57 KB raw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 01:12:06 +02:00
parent f5a9e491ce
commit 4391550140
10 changed files with 415 additions and 407 deletions

View File

@@ -96,9 +96,11 @@ declare module 'vue' {
FormErrorState: typeof import('./src/components/shared/public-form/FormErrorState.vue')['default']
FormFailureDetail: typeof import('./src/components/form-failures/FormFailureDetail.vue')['default']
FormFailuresTable: typeof import('./src/components/form-failures/FormFailuresTable.vue')['default']
FormField: typeof import('./src/components/forms/FormField.vue')['default']
FormStepper: typeof import('./src/components/shared/public-form/FormStepper.vue')['default']
GridBg: typeof import('./src/components/timetable/GridBg.vue')['default']
I18n: typeof import('./src/@core/components/I18n.vue')['default']
Icon: typeof import('./src/components/Icon.vue')['default']
IdentityMatchBanner: typeof import('./src/components/shared/public-form/IdentityMatchBanner.vue')['default']
ImageUploadField: typeof import('./src/components/common/ImageUploadField.vue')['default']
ImpersonateDialog: typeof import('./src/components/platform/ImpersonateDialog.vue')['default']