chore(f3.5): AppShell mockup parity — sidebar, topbar, plugin fixes #26

Merged
bert.hausmans merged 8 commits from chore/f3.5-appshell-mockup-parity into main 2026-05-14 13:38:52 +02:00

Summary

F3.5 sub-package: brings the AppShell layout (sidebar + topbar) in line with the agreed mockup, and fixes several PrimeVue/Iconify integration bugs that surfaced during the parallel-mode window.

Rebased onto current main (which now includes the Storybook setup from #25).

Commits (8, oldest → newest)

SHA Message
8f3a404a feat(appshell): add org-switcher card and bump sidebar width to w-72
4089a14b feat(appshell): refine section label styling for sidebar nav
f8fddc0e feat(appshell): add user-info card to sidebar bottom; remove topbar avatar
3df55b4d feat(appshell): topbar breadcrumb, notification bell, and help icon
29f3fdf2 fix(appshell): explicitly import SidebarHeader and SidebarUserCard
b1443be4 fix(iconify): bootstrap Tabler icon set at runtime for @iconify/vue
f218ac6e fix(primevue): switch installer to named export to stop double-registration
71585e1b fix(appshell): wrap PrimeVue responsive elements to bypass specificity conflict

Files changed (6, +388 / −67)

  • apps/app/src/layouts/components/AppShell.vue — sidebar width, topbar restructure, responsive wrappers
  • apps/app/src/layouts/components/SidebarHeader.vue (new) — org-switcher card
  • apps/app/src/layouts/components/SidebarUserCard.vue (new) — user-info card at sidebar bottom
  • apps/app/src/main.ts — installer import tweak, explicit component imports
  • apps/app/src/plugins/iconify.ts (new) — runtime Tabler icon-set bootstrap
  • apps/app/src/plugins/primevue/index.ts — named-export installer to stop double-registration

Test plan

  • pnpm dev from apps/app/ — confirm SPA boots without console errors and sidebar/topbar render per mockup
  • Toggle sidebar collapse / dark mode — no specificity glitches
  • Click through 2-3 navigation entries — icons load (Tabler set), no missing-icon placeholders
  • pnpm test + pnpm typecheck — green
  • pnpm storybook — AppShell-adjacent stories (if any) still render
## Summary F3.5 sub-package: brings the AppShell layout (sidebar + topbar) in line with the agreed mockup, and fixes several PrimeVue/Iconify integration bugs that surfaced during the parallel-mode window. Rebased onto current `main` (which now includes the Storybook setup from #25). ## Commits (8, oldest → newest) | SHA | Message | |---|---| | `8f3a404a` | feat(appshell): add org-switcher card and bump sidebar width to w-72 | | `4089a14b` | feat(appshell): refine section label styling for sidebar nav | | `f8fddc0e` | feat(appshell): add user-info card to sidebar bottom; remove topbar avatar | | `3df55b4d` | feat(appshell): topbar breadcrumb, notification bell, and help icon | | `29f3fdf2` | fix(appshell): explicitly import SidebarHeader and SidebarUserCard | | `b1443be4` | fix(iconify): bootstrap Tabler icon set at runtime for @iconify/vue | | `f218ac6e` | fix(primevue): switch installer to named export to stop double-registration | | `71585e1b` | fix(appshell): wrap PrimeVue responsive elements to bypass specificity conflict | ## Files changed (6, +388 / −67) - `apps/app/src/layouts/components/AppShell.vue` — sidebar width, topbar restructure, responsive wrappers - `apps/app/src/layouts/components/SidebarHeader.vue` (new) — org-switcher card - `apps/app/src/layouts/components/SidebarUserCard.vue` (new) — user-info card at sidebar bottom - `apps/app/src/main.ts` — installer import tweak, explicit component imports - `apps/app/src/plugins/iconify.ts` (new) — runtime Tabler icon-set bootstrap - `apps/app/src/plugins/primevue/index.ts` — named-export installer to stop double-registration ## Test plan - [ ] `pnpm dev` from `apps/app/` — confirm SPA boots without console errors and sidebar/topbar render per mockup - [ ] Toggle sidebar collapse / dark mode — no specificity glitches - [ ] Click through 2-3 navigation entries — icons load (Tabler set), no missing-icon placeholders - [ ] `pnpm test` + `pnpm typecheck` — green - [ ] `pnpm storybook` — AppShell-adjacent stories (if any) still render
bert.hausmans added 8 commits 2026-05-14 13:38:07 +02:00
Introduces SidebarHeader.vue — a PrimeVue-only org-switcher that
replaces the centered Crewli wordmark at the top of the sidebar. The
component mirrors the legacy Vuetify OrganisationSwitcher (avatar with
org initials, organisation name, plan-tier placeholder, dropdown
chevron, PrimeVue Menu of available orgs) but cannot reuse it
directly per the R-10 layout-shell-isolation invariant.

Plan-tier shows a hardcoded "Pro" placeholder until the backend
Organisation resource exposes a plan field — tracked separately, not
in F3.5 scope. When the user has no active organisation (portal
users, fresh super_admin), the component degrades to the original
title block so PortalLayout continues to read "Crewli Portal".

Desktop sidebar width bumped w-64 → w-72 (256 → 288 px) to give the
org-switcher card breathing room and accommodate the user-info card
arriving in B3. Mobile Drawer width bumped 16rem → 18rem to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Section headings ("Beheer" / organisation name, "Platform") were
already uppercase + muted but read as bold paragraph dividers more
than as quiet group markers. Tighten letter-spacing, drop weight
from semibold to medium, lighten the color one step (surface-500 →
surface-400), and shrink text to 11px so the headings recede and
let the nav items themselves carry the visual weight.

Spacing nudged from mt-4/mb-2/px-2 → mt-6/mb-1/px-3: more breathing
room above each group, less below (the items already have py-2 on
top), and the heading left-edge now lines up with the icons of the
nav items beneath it (both at px-3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consolidates the user menu into a single sidebar-bottom location.
SidebarUserCard.vue shows avatar (initial), full name, role (Dutch
label mapped from org pivot role or 'Super Admin' fallback) and a
chevron-up that opens a PrimeVue Menu with "Mijn Profiel" and
"Uitloggen". The Menu uses popup mode; PrimeVue v4's absolutePosition
logic auto-flips above the trigger when the panel would overflow the
viewport bottom — verify in Phase C.

AppShell loses the topbar avatar Button + Menu and the associated
state (userMenuRef, userInitial, userMenuItems, toggleUserMenu) plus
its imports (Avatar, Menu, useAuthStore, computed). The component is
now a pure layout shell with no auth-store coupling. The topbar's
right side is intentionally empty in this commit; B4 fills it with
breadcrumb / notification bell / help icon.

Layout: nav uses min-h-0 flex-1 overflow-y-auto so it shrinks under
viewport pressure and lets the user card stay pinned at the bottom
of the sidebar. Mobile Drawer's content pt-override sets the same
flex-column behaviour so the user card sits flush at the bottom of
the drawer overlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Left side gains a desktop-only breadcrumb "Organisation / Page title"
using the current organisation from useAuthStore and a page title
resolved by:

  1. route.meta.title (if a page sets it explicitly), then
  2. matching the active route name against the navItems prop, then
  3. humanizing the route name as a last-resort fallback.

The chevron separator is suppressed when either side is empty, so
portal and pre-org users see just the page title. Mobile preserves
the existing hamburger + title text (the breadcrumb is hidden on
<lg to keep the topbar single-row).

Right side gains a notification bell and a help icon. The bell is a
visual placeholder (no badge) — clicking shows a PrimeVue Toast
"Notificaties komen binnenkort beschikbaar" until the notification
framework lands as a separate sprint.

The help icon would normally open https://docs.crewli.app in a new
tab, but the host currently serves with a TLS cert that does not
cover the name (ERR_TLS_CERT_ALTNAME_INVALID), so the click handler
falls back to a Toast. A TODO comment in the source records the
target URL and the one-line switch to make once the cert is fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
unplugin-vue-components' Components({ dirs }) in vite.config.ts only
scans src/components, src/@core/components, and src/views/demos. The
sub-components introduced in B1/B3 live under src/layouts/components/,
which is NOT in the auto-import scan path. Without an explicit script
import, Vue renders <SidebarHeader> and <SidebarUserCard> as unknown
HTML elements (no DOM output, no errors), which is why the topbar and
sidebar-bottom cards looked empty in browser inspection.

Adding the two imports inline with the existing Icon import keeps the
component graph explicit. The post-edit eslint --fix hook preserves
the imports because the template usages (already present from B1 and
B3) make vue-eslint-parser see them as used.

The original B1/B3 commits had the imports stripped by the hook
because the imports were added in a separate Edit *before* the
template usages — eslint --fix correctly removed them as unused at
that moment, and the next Edit added the template usage but not the
import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The PrimeVue side of the parallel-mode app renders icons via
@iconify/vue's <Icon> component (src/components/Icon.vue). At runtime
@iconify/vue resolves an icon name like "tabler-eye" by looking up
its data in the in-memory icon registry; on a miss it falls back to
fetching https://api.iconify.design/tabler/eye.json. The CSP blocks
that origin, so every Tabler icon used in AppShell, SidebarHeader,
SidebarUserCard, and the migrated login form rendered as an empty
<svg viewBox="0 0 16 16"></svg>.

New plugins/iconify.ts loads the full Tabler set
(@iconify-json/tabler/icons.json, already in package.json as 1.2.23)
and registers it via addCollection() at module-load time. main.ts
side-effect-imports it before any other import so the registry is
warm before the first Icon mounts.

This is a NEW concern, separate from the existing plugins/iconify/
(index.ts + icons.css) which generates Vuexy-style i-tabler-* CSS
classes for Vuetify's VIcon adapter. The two systems must coexist
during F3–F6 parallel mode; the legacy directory can be deleted
alongside Vuetify when F6 lands.

Bundle cost: ~1.9 MB uncompressed JSON, ~400 KB gzipped in the main
chunk. Per-icon imports are a future optimisation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
main.ts explicitly calls installPrimeVue(app) AFTER registerPlugins(app)
per the comment in main.ts ("so PrimeVue lives outside the Vuexy @core
machine"). The intent was a single registration site outside the
auto-discovery loop.

Bug: registerPlugins (src/@core/utils/plugins.ts) globs
plugins/*/index.{ts,js} eagerly and invokes the `default` export of
each match. plugins/primevue/index.ts was exporting installPrimeVue
as the default, so registerPlugins also picked it up and called it.
End result: PrimeVue and its three services (Toast, Confirmation,
Dialog) were each registered twice on every app boot. Visible
symptoms: duplicate Toast emissions on a single Toast.add() call,
and ConfirmationService callbacks firing twice for one user
confirmation.

Fix: convert `export default function installPrimeVue` to a NAMED
export, and update main.ts's import to `{ installPrimeVue }`. The
registerPlugins glob still picks up the module path but the
`pluginImportModule.default?.(app)` invocation becomes a no-op via
optional chaining (no default export to call). main.ts remains the
single registration site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tailwind's lg:hidden loses to PrimeVue's .p-button { display: inline-flex }
due to equal specificity but later cascade order. Resulted in the mobile
hamburger remaining visible on desktop, allowing the Drawer to open over
the already-visible permanent sidebar.

Fix: wrap mobile-only cluster (hamburger + title) in a plain <div lg:hidden>
so the wrapper owns the visibility toggle. The wrapper is not a PrimeVue
component, so no specificity competition.

The Drawer itself had the same anti-pattern (class="lg:hidden") and is
worse, because PrimeVue Drawer teleports to body — a wrapping div on the
parent does not isolate the teleported overlay, and a class on the Drawer
root loses to .p-drawer { display: flex } when visible. Converted to
v-if="!isLg" driven by useMediaQuery('(min-width: 1024px)'). Vue simply
does not render the component on lg+, so no display rule competes.

Audited all 5 layouts for the same anti-pattern:
- AppShell.vue — fixed (Button + Drawer described above)
- default.vue / OrganizerLayout.vue / PortalLayout.vue — delegate to
  AppShell; no PrimeVue elements with responsive classes
- blank.vue — plain <div>, no PrimeVue
- PublicLayout.vue — plain <main>, no PrimeVue

useMediaQuery is auto-imported via unplugin-auto-import's @vueuse/core
entry in vite.config.ts; explicit imports get stripped by the post-edit
ESLint --fix hook as redundant.

F3-introduced bug (commit 43915501); surfaced during F3.5 testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bert.hausmans merged commit 524d0ee586 into main 2026-05-14 13:38:52 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bert.hausmans/crewli#26