chore(primevue): F3 — PrimeVue foundation with parallel-mode operation #24

Merged
bert.hausmans merged 10 commits from chore/f3-primevue-foundation into main 2026-05-11 20:07:58 +02:00

⚠️ Reviewer-attentie: F3 lands the PrimeVue foundation in parallel mode with Vuetify. Pages still use Vuetify; layouts use the new PrimeVue AppShell. F3.5 follows immediately to bring AppShell to mockup-parity (visual quality); F4 migrates pages.

Implements F3 of RFC-WS-FRONTEND-PRIMEVUE (with three RFC amendments landing in this branch). 10 commits, ~2 days work.

Quality gates

Check Result
TypeScript (vue-tsc) ✓ clean
Vitest ✓ 402/402 (57 files)
Build (Vite) ✓ 14.88s
JS bundle 2.29 MB (target 2.4 MB) ✓
CSS bundle 3.26 MB (target 3.5 MB) ✓

What this delivers

Infrastructure

  • PrimeVue 4.5.5 + @primeuix/themes 2.0.3 + @primevue/forms 4.5.5 + primelocale 1.6.0 (RFC substitution: @primevue/themes → @primeuix/themes per ecosystem state)
  • Tailwind v4.3.0 + @tailwindcss/vite + tailwindcss-primeui
  • Aura preset with Crewli teal #0D9394 primary palette per RFC Appendix B

Components & composables

  • FormField.vue — wrapper around @primevue/forms with label + required asterisk + error Message + hint
  • useFormError.ts — provide/inject-based API 422 bridge with applyApiErrors + clearApiErrors
  • Icon.vue — thin wrapper over @iconify/vue rendering <Icon name="tabler-X" /> as real SVG
  • <Toast /> + <ConfirmDialog /> mounted at App.vue top level

Layout shell (R-10 isolated commit 43915501)

  • AppShell.vue — PrimeVue-only (~210 lines). Hard constraint honored: no Vuetify imports.
  • All 5 top-level layouts rewritten to delegate; filenames preserved per AD-3.

Sample

  • login.vue migrated to FormField + Zod + PrimeVue inputs as F3 sample. Validates FormField API end-to-end with 422 routing via useFormError.

RFC drift corrections (commit d5c9cf19)

Three RFC v1.0 inaccuracies discovered during F3 implementation:

  1. AD-2: @primevue/themes@^4.5@primeuix/themes@^2 (PrimeVue 4's official install docs prescribe this path; @primevue/themes was deprecated by maintainers)
  2. AD-5: <i class="i-tabler-X"><Icon name="tabler-X" /> wrapping @iconify/vue (UnoCSS not in stack; existing Crewli pattern retained)
  3. Appendix B: import paths aligned to @primeuix/themes/aura

Known F3 functional regressions — all returning in F3.5 or F4

Per B7 commit body: NavSearchBar, ContextSwitcher, NavbarThemeSwitcher, NavbarShortcuts, NavBarNotifications, rich UserProfile menu, ImpersonationBanner, Portal event-mode topbar. F3.5 (immediately after this PR) addresses notifications/user-info/search visually; F4 sub-packages restore full functionality.

What this does NOT change

  • 402 Vitest tests pass unchanged (Vuetify test runtime preserved per RFC F5 deferral)
  • Vuetify components on pages continue to render (parallel-mode invariant)
  • No application page migrated yet (login is the F3 sample, not full page migration)

Post-merge action required

Re-upload .claude-sync/ to Project Knowledge (manifest SHA changed; RFC and PRIMEVUE_COMPONENTS.md amended in commit d5c9cf19).

🤖 Generated with Claude Code

> ⚠️ **Reviewer-attentie**: F3 lands the PrimeVue foundation in parallel mode with Vuetify. Pages still use Vuetify; layouts use the new PrimeVue AppShell. F3.5 follows immediately to bring AppShell to mockup-parity (visual quality); F4 migrates pages. Implements F3 of RFC-WS-FRONTEND-PRIMEVUE (with three RFC amendments landing in this branch). 10 commits, ~2 days work. ## Quality gates | Check | Result | |---|---| | TypeScript (vue-tsc) | ✓ clean | | Vitest | ✓ 402/402 (57 files) | | Build (Vite) | ✓ 14.88s | | JS bundle | 2.29 MB (target 2.4 MB) ✓ | | CSS bundle | 3.26 MB (target 3.5 MB) ✓ | ## What this delivers **Infrastructure** - PrimeVue 4.5.5 + @primeuix/themes 2.0.3 + @primevue/forms 4.5.5 + primelocale 1.6.0 (RFC substitution: @primevue/themes → @primeuix/themes per ecosystem state) - Tailwind v4.3.0 + @tailwindcss/vite + tailwindcss-primeui - Aura preset with Crewli teal #0D9394 primary palette per RFC Appendix B **Components & composables** - `FormField.vue` — wrapper around @primevue/forms with label + required asterisk + error Message + hint - `useFormError.ts` — provide/inject-based API 422 bridge with `applyApiErrors` + `clearApiErrors` - `Icon.vue` — thin wrapper over @iconify/vue rendering `<Icon name="tabler-X" />` as real SVG - `<Toast />` + `<ConfirmDialog />` mounted at App.vue top level **Layout shell (R-10 isolated commit `43915501`)** - `AppShell.vue` — PrimeVue-only (~210 lines). Hard constraint honored: no Vuetify imports. - All 5 top-level layouts rewritten to delegate; filenames preserved per AD-3. **Sample** - `login.vue` migrated to FormField + Zod + PrimeVue inputs as F3 sample. Validates FormField API end-to-end with 422 routing via useFormError. ## RFC drift corrections (commit `d5c9cf19`) Three RFC v1.0 inaccuracies discovered during F3 implementation: 1. **AD-2**: `@primevue/themes@^4.5` → `@primeuix/themes@^2` (PrimeVue 4's official install docs prescribe this path; @primevue/themes was deprecated by maintainers) 2. **AD-5**: `<i class="i-tabler-X">` → `<Icon name="tabler-X" />` wrapping @iconify/vue (UnoCSS not in stack; existing Crewli pattern retained) 3. **Appendix B**: import paths aligned to @primeuix/themes/aura ## Known F3 functional regressions — all returning in F3.5 or F4 Per B7 commit body: NavSearchBar, ContextSwitcher, NavbarThemeSwitcher, NavbarShortcuts, NavBarNotifications, rich UserProfile menu, ImpersonationBanner, Portal event-mode topbar. F3.5 (immediately after this PR) addresses notifications/user-info/search visually; F4 sub-packages restore full functionality. ## What this does NOT change - 402 Vitest tests pass unchanged (Vuetify test runtime preserved per RFC F5 deferral) - Vuetify components on pages continue to render (parallel-mode invariant) - No application page migrated yet (login is the F3 sample, not full page migration) ## Post-merge action required Re-upload `.claude-sync/` to Project Knowledge (manifest SHA changed; RFC and PRIMEVUE_COMPONENTS.md amended in commit d5c9cf19). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bert.hausmans added 10 commits 2026-05-11 20:00:55 +02:00
Both comments in apps/app/playwright/index.ts (header block lines 38-45
and inline at line 66) state that the Vuetify provider gets replaced by
PrimeVue in F3. This predates the RFC clarification that test-runtime
flip is F5, not F3 (per ARCH-TESTING.md §7).

F3 builds the PrimeVue runtime in main.ts but keeps the test runtime
on Vuetify. Component tests continue to mount with the Vuetify provider
until F5 deliberately swaps it. This commit aligns the comments with
that decision so no future contributor wonders whether the F3 sprint
should have touched this file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Packages installed:
- primevue@4.5.5
- @primeuix/themes@2.0.3 (substitutes @primevue/themes per ecosystem
  state — see rationale below)
- @primevue/forms@4.5.5
- primelocale@1.6.0 (pinned to ^1 per RFC)
- tailwindcss@4.3.0
- @tailwindcss/vite@4.3.0
- tailwindcss-primeui@0.6.1

Package substitution: @primevue/themes → @primeuix/themes

RFC v1.0 §6 F3 specifies @primevue/themes@^4.5, but during install pnpm
reported this package as deprecated by its maintainers (PrimeFaces) with
explicit guidance to migrate to @primeuix/themes. Web verification confirms
that the official PrimeVue 4 install documentation at primevue.org/vite/
now specifies `@primeuix/themes` directly, not the deprecated path:

  pnpm add primevue @primeuix/themes
  import Aura from '@primeuix/themes/aura';

@primeuix/themes is maintained by the same maintainers (mert.sincan,
cagatay.civici), has the same API surface (Aura preset, definePreset,
semantic tokens), and is the path PrimeVue 4's documentation now
prescribes. The substitution is not a deviation from PrimeVue v4
conventions — it IS the current PrimeVue v4 convention.

The RFC will be amended in B9 to align AD-2 and Appendix B with this
ecosystem state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scaffolds apps/app/src/plugins/primevue/ as three files mirroring the
Vuetify plugin structure at apps/app/src/plugins/vuetify/:

- theme.ts — CrewliPreset extends Aura via definePreset(). Primary
  palette (50–950) is the exact token plan from RFC Appendix B,
  centered on Crewli teal #0D9394 (light primary, primary.500) and
  #0B7F80 (dark primary, primary.600). Surface tokens use Aura
  defaults. colorScheme.light/dark map primary.color, hover, active,
  and contrastColor per Appendix B.

- defaults.ts — empty pt (PassThrough) defaults object. F3 ships this
  scaffold; F4 sub-packages populate component-level defaults as each
  Vuetify surface migrates.

- index.ts — installPrimeVue(app) registers PrimeVue with the preset,
  Dutch locale (primelocale/nl.json → nl.nl), darkModeSelector: '.dark'
  (matches Vuexy convention per AD-2), and the three services Toast,
  Confirmation, Dialog.

Theme imports use @primeuix/themes (the maintained successor PrimeVue's
official docs prescribe), not RFC v1.0's @primevue/themes. See B1 commit
for substitution rationale. RFC will be aligned in B9.

The plugin is not yet registered in main.ts — that lands in B4 after
Tailwind v4 wiring (B3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three additions wire Tailwind v4 into the SPA without disturbing the
existing Vuetify pipeline:

- apps/app/src/assets/styles/tailwind.css — Tailwind v4 CSS-first entry.
  Uses @import "tailwindcss"; @plugin "tailwindcss-primeui"; and
  @source pointing at apps/app/src/ to scan template content.

- apps/app/vite.config.ts — adds the @tailwindcss/vite plugin between
  vue() and vuetify(). After vue() so it sees compiled template
  content; before vuetify() so Vuetify's SCSS pipeline runs unimpeded.

- apps/app/src/main.ts — imports tailwind.css before the Vuetify/Vuexy
  SCSS so utility classes are available alongside Vuetify's cascade.

optimizeDeps.exclude remains ['vuetify'] (no PrimeVue addition) — HMR
behaves correctly in dev with the current config; revisit if needed.

Verification:
- pnpm typecheck — clean.
- pnpm build — succeeds in 13.97s; CSS emitted per-route as expected.
- pnpm test — 402 tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
installPrimeVue(app) runs AFTER registerPlugins(app) (which registers
Vuetify + router + Pinia via the Vuexy @core machine). Placing the
PrimeVue install outside @core/utils/plugins is deliberate — it keeps
PrimeVue free of the Vuexy plugin loader so F6 can remove @core/
without disturbing PrimeVue registration.

Both frameworks are now active at runtime. Existing Vuetify pages
continue to render unchanged; PrimeVue components become available
for the layout-shell rewrite (B7) and the FormField wrapper (B5).

Verification:
- pnpm typecheck — clean.
- pnpm build — succeeds in 14.26s, no PrimeVue or theme-related errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new artifacts that together provide the F4 form-migration target:

apps/app/src/components/forms/FormField.vue — project-owned wrapper
around @primevue/forms' built-in FormField. The default slot accepts
the actual input (e.g. <InputText name="email" />); the wrapper renders
label (with optional required asterisk), error Message, and hint chrome
around it. Reads field state from the parent <Form> via the built-in
FormField's scoped slot, so call-sites do not need to thread $form
manually.

apps/app/src/composables/useFormError.ts — the API 422 bridge. Parent
component calls useFormError() once; the composable provides an
apiErrors ref through Vue inject. Each FormField in the component
reads its own field name from that map. applyApiErrors() reads the
Crewli backend's { errors: { field: string[] } } shape and surfaces
the first message per field; clearApiErrors() resets between submits.

Error precedence per RFC Appendix A: explicit apiError prop > inject
apiErrors map > Zod resolver error from $field.

Signature note: RFC's useFormError(formRef) is implemented as
useFormError() — the formRef parameter is unused in the provide/inject
implementation, and Crewli convention avoids unused parameters. RFC
will be aligned in B9 if it remains a meaningful spec gap during F4.

Verification:
- pnpm typecheck — clean.
- pnpm test — 402 tests pass unchanged.
- B8 will exercise the components end-to-end on the login page; F4d
  validates against the public-registration multi-step form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two additions complete the F3 runtime scaffolding:

apps/app/src/components/Icon.vue — generic Iconify renderer wrapping
@iconify/vue's <Icon> component. F4 migration substitutes
<VIcon icon="tabler-X" /> with <Icon name="tabler-X" /> at call-sites,
producing real SVG output and using the existing Crewli "tabler-*"
naming convention. Props: name (required, e.g. "tabler-eye"), optional
size. The component avoids @iconify/vue's auto-import for clarity at
call-sites.

apps/app/src/App.vue — mounts <Toast /> and <ConfirmDialog /> at the
template root inside VLocaleProvider. Both render alongside the
existing VSnackbar and VDialog confirm patterns during the F3–F4
parallel-mode window. F4 sub-packages migrate call-sites to PrimeVue's
useToast() / useConfirm() composables.

UnoCSS-style i-tabler-* utility-class rendering (RFC AD-5 v1.0 wording)
is not adopted — UnoCSS is not installed in the Crewli stack. The
RFC will be aligned in B9.

Verification:
- pnpm typecheck — clean.
- pnpm test — 402 tests pass unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Replaces the Vuetify VForm + AppTextField + VBtn stack with the F3
form pattern: @primevue/forms' <Form> with a Zod resolver, the
project-owned <FormField> wrapper from B5, and PrimeVue InputText /
Password / Checkbox / Button at the input layer. Surrounding chrome
(VRow / VCol illustration column, VCard, VAlert reset-success banner,
auth-logo link, MfaChallengeCard) stays Vuetify until F4b migrates
the auth surface in full.

Zod schema:
- email: required, valid email format
- password: required

Both messages are Dutch (per F3 sprint plan convention).

422 error handling routes through useFormError() from B5. The Laravel
response shape (errors.<field>: string[]) feeds applyApiErrors directly.
rate_limited and other reason-only failures are synthesized into the
email field's error map so they surface visually under the email input,
preserving the existing UX.

The remember-me checkbox is rendered with PrimeVue Checkbox (no schema
coverage — it's UI state, not validated input). The password visibility
toggle is delegated to PrimeVue's Password component's built-in
toggle-mask prop (replaces the previous manual isPasswordVisible ref
and append-inner-icon plumbing).

Verification:
- pnpm typecheck — clean.
- pnpm test — 402 tests pass unchanged.
- pnpm build — succeeds; login chunk grew from ~21 KB to ~84 KB raw
  due to @primevue/forms + Password/Checkbox component code (gzip 22 KB).
  Will normalize during F4 as more pages share these modules.
- Manual browser test deferred to Phase C brand-review screenshot
  capture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three RFC drift corrections discovered during F3 implementation:

1. AD-5 icon rendering: corrected from "<i :class='i-tabler-X'>"
   utility-class pattern (which would require UnoCSS, not installed)
   to "@iconify/vue's <Icon> component with name='tabler-X' prop"
   (existing Crewli pattern producing real SVG output). The thin wrapper
   shipped in F3 B6 as apps/app/src/components/Icon.vue accordingly.

2. AD-2 theme architecture: corrected package reference from
   @primevue/themes@^4.5 (deprecated by PrimeFaces) to
   @primeuix/themes@^2 (the path now prescribed by PrimeVue 4's
   official install docs at primevue.org/vite/). Same maintainers,
   same API surface (definePreset, Aura preset, semantic tokens).
   F3 commit B1 already uses the corrected package.

3. Appendix B Aura theme token plan: updated import-path examples to
   @primeuix/themes and @primeuix/themes/aura accordingly.

Also updated:
- §6 F3 deliverables list: dependency line now reads @primeuix/themes@^2
  with a footnote linking to the B1 rationale.
- Appendix C Version Pinning Policy: separated @primeuix/themes from
  the primevue/^primevue/forms lockstep pin (independent release cadence).
- dev-docs/PRIMEVUE_COMPONENTS.md §3 (Data display): VIcon row updated
  to <Icon name="tabler-..." />; surrounding migration-spirit paragraph
  rewritten; §10 external-resources link relabeled to @primeuix/themes.

These are RFC drift corrections — the implementation in F3 (commits
B1, B2, B6 of this sprint) already uses the corrected packages and
import paths. This commit aligns the spec with reality so future
contributors don't reach for the deprecated/inaccurate documentation.

.claude-sync/ regenerates automatically post-commit via lefthook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bert.hausmans merged commit e36f57b8e1 into main 2026-05-11 20:07:58 +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#24