docs(rfc): WS-FRONTEND-PRIMEVUE planning — F1 audit, RFC v1.0, Amendment A-1, sync conf expansion #20

Merged
bert.hausmans merged 4 commits from audit/primevue-migration into main 2026-05-10 21:20:50 +02:00
Showing only changes of commit e2d9797de3 - Show all commits

View File

@@ -0,0 +1,560 @@
# RFC-WS-FRONTEND-PRIMEVUE — Vuetify+Vuexy → PrimeVue Migration
| Field | Value |
|---|---|
| **Status** | Approved (decision date: 2026-05-10) |
| **Version** | 1.0 |
| **Author** | Architecture (Claude Chat) |
| **Audit input** | `dev-docs/MIGRATION-AUDIT-PRIMEVUE.md` (commit `5d9399b`, branch `audit/primevue-migration`) |
| **Audit base SHA** | `main @ 62afbde` (audit cutoff) |
| **Decision basis** | Strategic analysis 2026-05-08 + F1 codebase audit 2026-05-10 |
| **Sprint identifier** | `WS-FRONTEND-PRIMEVUE` |
| **Estimated effort** | 1012 working days, serial |
| **Concurrent work** | Artist Management module continues on Vuetify+Vuexy stack; migrated as part of F4b |
| **Related docs** | `CLAUDE.md`, `dev-docs/VUEXY_COMPONENTS.md` (to be replaced), `AUTH_ARCHITECTURE.md`, `BACKLOG.md` |
---
## 1. Decision Summary
Migrate the `apps/app/` SPA from **Vuetify 3.10.8 + Vuexy template 9.5/10.11** to **PrimeVue 4.5 (Aura preset) + Tailwind CSS v4**, before public SaaS launch. Form layer adopts **`@primevue/forms` + Zod resolver** as the new canonical pattern (no VeeValidate — never adopted in this codebase). Flatpickr-based date input, Iconify-Tabler icon system, Pinia stores, TanStack Query composables, and route file structure remain unchanged. Total scope is six work packages (F2 through F6, F1 already complete) executed serially, with each package gated by a Claude Chat review before the next begins. Out of scope: design system overhaul, vue-i18n adoption, Form Builder organizer UI (S3b backlog), notification framework.
---
## 2. Background & Rationale
The 2026-05-08 strategic analysis concluded that Vuetify+Vuexy carries three structural risks for Crewli's enterprise SaaS profile: documented v-data-table performance degradation at festival-scale row counts, the Vuetify project's publicly disclosed funding crisis (OpenCollective exhausted, contributor compensation suspended), and Vuexy template lock-in to a Material-Design aesthetic that reads as dated against 2026 SaaS norms. PrimeVue 4 with the Aura preset addresses all three: DataTable handles 1000+ rows natively with frozen columns and lazy virtual scroll, PrimeTek's commercial backing is materially stronger, and the design-token architecture supports future evolution without component rewrites.
The F1 audit confirmed that the migration cost is bounded and predictable: 46 pages, 75 distinct Vuetify components, 14 datatables, 38 form-bearing components, zero UI-framework coupling in stores or query composables, and zero VeeValidate to preserve. The single largest CSS chunk is 3.14 MB (the entire Vuexy template); reducing this is the most concrete bundle win available. The pre-launch window is the cheapest possible moment to migrate — switching Vuetify→PrimeVue post-launch was estimated at 35 weeks against ~10 days now.
The Vuexy template Extended-license has not been purchased and will not be. There is no sunk cost in the template ecosystem.
---
## 3. Audit-Derived Facts (F1 Reference)
The following facts are anchored to the F1 audit and drive every decision in this RFC. Re-validate if migrating against a later `main` SHA.
| Fact | Value | Implication |
|---|---|---|
| Total pages | 46 (30 organizer / 8 platform / 7 portal / 2 register / -1 nesting) | F4 sub-package sizing |
| Vuetify components in use | 75 distinct, 6 use ≥ 100× | Mapping table required (§8) |
| Top 5 components by usage | VBtn (420), VCol (331), VIcon (267), VCard (250), VCardText (208) | Highest mapping leverage |
| DataTables | 14 (5 server, 9 client) | F4 per-tree distribution |
| Forms (`<VForm>`) | 64 occurrences across 38 components | Largest single migration cost |
| Form library | None (no VeeValidate, no FormKit) | New form layer = greenfield |
| Zod schemas | 2 (registration, timetable) | Expand opportunity, not constraint |
| Pinia stores | 8, zero UI-framework imports | Migration risk = 0 in this layer |
| TanStack Query composables | 29, zero UI-framework imports | Migration risk = 0 in this layer |
| Vuetify imports in tests | 0 | Single setup-file flip in F5 |
| Mount-rendering tests | 22 | Validation surface for F5 |
| Main JS chunk (gzip) | 213 KB | Target post-migration: ≤ 180 KB |
| Single largest CSS chunk | 3.14 MB | Target post-migration: ≤ 400 KB |
| Iconify-Tabler references | 593 | Retain (PrimeVue is icon-agnostic) |
| AppDateTimePicker | 548 lines, Flatpickr-based, framework-agnostic | Retain in fase 1 |
| Layout selection | Filename match via `vite-plugin-vue-meta-layouts` | Retain mechanism |
| Portal route prefix | `/portal/*` (not `/p/*` as docs claim) | CLAUDE.md correction in F2 |
---
## 4. Scope
### 4.1 In scope
- Replacement of every Vuetify component (`V*`) used in `apps/app/src/` with PrimeVue equivalents.
- Replacement of all Vuexy `@core` and `@layouts` files with project-owned equivalents or PrimeVue-native solutions.
- Introduction of `@primevue/forms` + Zod as the canonical form layer, including a `<FormField>` wrapper component.
- Introduction of Tailwind CSS v4 for layout utilities (replacing VRow/VCol/VSpacer).
- Aura theme preset configured with Crewli teal (`#0D9394`) and dark mode support.
- Locale wired to `nl-NL` via `primelocale`.
- Layout shell rewrites for `default.vue`, `blank.vue`, `OrganizerLayout.vue`, `PortalLayout.vue`, `PublicLayout.vue`.
- Toast service (replacing VSnackbar) and ConfirmDialog service.
- Documentation rewrite: `dev-docs/PRIMEVUE_COMPONENTS.md` (replaces VUEXY_COMPONENTS.md), `CLAUDE.md` frontend section, `.cursorrules` frontend rules.
- Test runtime registration flip from Vuetify to PrimeVue (single setupFile).
- Removal of Vuetify, vite-plugin-vuetify, Vuexy SCSS tree, and Vuexy `@core/composable/useSkins.ts` from the bundle.
- Bundle size verification against targets in §11.
### 4.2 Out of scope
- Adoption of vue-i18n with locale message files. UI strings remain hardcoded Dutch in templates. Multi-language is a separate future sprint.
- Migration of `AppDateTimePicker` from Flatpickr to PrimeVue's native DatePicker. Tracked as `FRONTEND-DATEPICKER-PRIMEVUE-NATIVE` in BACKLOG.md (post-launch).
- Form Builder organizer UI (drag-drop schema builder). Tracked as S3b in BACKLOG.md.
- Notification framework (Laravel Echo + Soketi) — separate planned sprint.
- Design system token expansion beyond what the Aura preset offers out-of-the-box.
- White-label per-tenant theming beyond primary-color and logo overrides.
- Any backend changes. This sprint is frontend-only.
- Visual redesign of any page. Migration preserves existing UX and information architecture; only the rendering layer changes.
### 4.3 Concurrent work tolerance
The Artist Management module is currently being developed on the existing Vuetify+Vuexy stack. This RFC does not block that work. Artist Management's UI will be migrated to PrimeVue as part of F4b (organizer tree). The maintainer of Artist Management should commit working Vuetify code as if no migration were happening; the F4b prompt will translate it.
---
## 5. Architectural Decisions
Each decision below is binding for this sprint. Deviations require an RFC amendment.
### AD-1: Form layer = `@primevue/forms` + Zod resolver
The current `<VForm>` ref + per-field `:rules` array pattern is replaced by `<Form>` from `@primevue/forms` with a `zodResolver(schema)` configuration. API 422 errors are merged into the form's error state via a documented hook. A project-owned `<FormField>` wrapper (spec in Appendix A) abstracts the row template (label + input slot + validation message) so call-sites stay terse.
**Rationale:** PrimeVue v4 ships `@primevue/forms` as the official form integration with built-in resolvers for Zod, Yup, Joi, Valibot, Superstruct. Crewli already uses Zod for the two existing schemas. There is no VeeValidate to preserve. This decision unifies form validation under one library that owns both the rendering and the validation contract.
**Implication:** The 27 deep `vuetify/components/VForm` imports become a single `<Form>` import per page, often eliminated entirely by the `<FormField>` wrapper. The 11 validators in `@core/utils/validators.ts` (`requiredValidator`, `emailValidator`, etc.) are replaced by Zod schema methods (`.min(1)`, `.email()`, `.regex()`, custom `.refine()`). New Zod schemas are added under `apps/app/src/schemas/` per feature module.
### AD-2: Theme = Aura preset, Crewli teal primary, dark mode preserved
The Aura preset is configured via `@primevue/themes` `definePreset(Aura, { semantic: { primary: { ... } } })`. The Crewli teal (`#0D9394` light, `#0B7F80` dark) maps to `primary.500` and `primary.600` semantic tokens. Surface, success, info, warning, error tokens use Aura defaults unless explicit Crewli equivalents exist. Dark mode is wired via PrimeVue's `darkModeSelector: '.dark'` config, integrated with the existing `@core/composable/useSkins.ts` until F6 cleanup.
**Per-tenant overrides:** primary color and logo only (per project-level decision). Implementation: a Pinia computed reads `useOrganisationStore().branding.primaryColor` and patches the active preset via `usePrimeVue().config.theme.preset` at organisation-switch time. Full white-labeling is explicitly out of scope.
**Rationale:** Aura is PrimeTek's flagship modern preset, design-token-based, and matches the "tighter spacing, subtle borders, emerald-adjacent accent" aesthetic identified as more contemporary than Vuetify Material in the strategic analysis. The teal already brand-identifies Crewli; Aura's token system makes per-tenant primary swaps trivial.
### AD-3: Layout shell = filename-match preserved, contents rewritten
`vite-plugin-vue-meta-layouts` continues to do filename-based layout selection. The five layout files (`default.vue`, `blank.vue`, `OrganizerLayout.vue`, `PortalLayout.vue`, `PublicLayout.vue`) are rewritten to use PrimeVue components: `Drawer` for the side navigation, `Menubar` for the top bar, `Avatar`+`Menu` for the user-profile dropdown. The `meta.layout: 'blank'` override remains the only explicit layout selector. The 12 `@layouts/components/*` files (VerticalNav, VerticalNavLayout, etc.) are deleted in F6 — they are replaced inline within the new shells.
**Rationale:** Routing-mechanism preservation minimises blast radius. The shell rewrite is bounded (~5 files, ~600 lines new code) and contained to F3 Foundation. No route definition changes.
### AD-4: Date layer = Flatpickr retained in fase 1
`AppDateTimePicker.vue` (548 lines, Flatpickr-based) is retained as-is. Its dependency on `<VTextField>` is replaced with `<InputText>` (one-line change at the trigger element). Flatpickr itself is framework-agnostic and survives the migration.
**Rationale:** AppDateTimePicker encodes the Crewli-specific Dutch UX for date+time selection (locale, formatting, range constraints). Rewriting it during the framework migration is unnecessary risk. PrimeVue's native `DatePicker` is excellent and we will likely migrate to it post-launch (tracked as `FRONTEND-DATEPICKER-PRIMEVUE-NATIVE`), but that is polish work, not launch-blocker.
### AD-5: Icons = Iconify-Tabler retained, PrimeIcons not installed
The 593 `tabler-*` icon references in templates remain unchanged. PrimeVue's components accept any element in icon slots and `:icon` props can render arbitrary class names. The custom Vuetify icon component in `apps/app/src/plugins/vuetify/icons.ts` is replaced by a tiny generic `<Icon>` component that renders `<i :class="...">` for Iconify class strings — or, where call-sites already use `<VIcon icon="tabler-X">`, the migration substitutes `<i class="iconify tabler-X">` directly. The 5 SVG checkbox/radio overrides in `@images/svg/` become unused (PrimeVue's Checkbox/RadioButton ship their own styling) and are deleted in F6.
**Rationale:** Throwing away 593 icon references is needless cost; PrimeVue does not require its own icon set; the project's Tabler aesthetic is preserved.
### AD-6: Locale = `nl-NL` via primelocale, vue-i18n unused
PrimeVue is configured with `locale: nlLocale` where `nlLocale` is imported from `primelocale/nl.json`. This drives DatePicker month names, "no records" text, ARIA labels, aria-live announcements, and PaginatorReportTemplate text. UI strings in templates remain hardcoded Dutch. `vue-i18n` (already installed at 11.1.12) stays installed but unused — its removal is out of scope.
**Rationale:** Day-one Dutch locale costs ~5 lines of config and prevents English UI noise from leaking into the Crewli interface. Vue-i18n adoption is a separate decision (multi-language sprint) unrelated to this migration.
### AD-7: DataTable strategy
All 14 existing DataTables migrate to PrimeVue's `<DataTable>` + `<Column>` API. Server-paginated tables (5) use `:lazy="true"` + `@page` + `@sort` + `@filter` events wired to TanStack Query parameters. Tables expected to display ≥ 200 rows simultaneously enable virtual scroll via `:scrollable="true"` `:scroll-height="..."` `:virtual-scroller-options="{ itemSize: 44 }"`. The `#expanded-row` slot pattern (used by 2 audit/log tables) maps to PrimeVue's `:expandedRows` v-model + `#expansion` template. Per-cell `#item.<col>` slots map to PrimeVue's `#body` template per `<Column>`.
**Frozen columns and column reordering:** introduced opportunistically where UX benefits (e.g., the festival-roster Persons page), not blanket-applied.
**Rationale:** The strategic analysis identified DataTable as the highest-leverage component upgrade. PrimeVue's DataTable handles Crewli's 1000+ row scenarios (festival rosters, cross-tenant audit logs, user lists) with documented enterprise references and no degradation patterns matching Vuetify's GH #20335 / #20601.
### AD-8: Stores and queries unchanged
The 8 Pinia stores and 29 TanStack Query composables under `apps/app/src/stores/` and `apps/app/src/composables/api/` are not touched by this sprint, except where they happen to import from `vuetify` (zero today, per audit) or `@core` (zero today, per audit). The audit confirmed zero coupling. Any incidental `useToast()` usages currently going through Vuetify's snackbar pattern migrate to PrimeVue's `useToast()` from `primevue/usetoast` — this is the only API change in this layer.
### AD-9: SCSS strip = three-stage
**Stage F3 (introduction):** PrimeVue + Tailwind load alongside Vuetify. Both stylesheets present. Temporary CSS bloat (~3.5 MB) is accepted. New code uses PrimeVue.
**Stage F4ad (per-tree migration):** Vuetify components removed per route tree. Per-component SCSS in `@core/scss/template/libs/vuetify/components/` (24 files) becomes dead but stays imported. Incremental SCSS stripping is forbidden — too risky, too easy to break unrelated pages.
**Stage F6 (cleanup):** Three entry imports removed in one commit (`@core/scss/template/index.scss` from `main.ts:11`, `@core/scss/template/libs/vuetify/index.scss` from `plugins/vuetify/index.ts:13`, `vuetify/styles` from `plugins/vuetify/index.ts:14`). The entire `@core/scss/template/` tree (146 files) deleted. Bundle size measured against §11 targets as acceptance criterion.
**Rationale:** Single-direction strips; no per-component CSS archaeology; one measurable cleanup commit at the end.
### AD-10: Test runtime = single setupFile flip
The Vitest setupFile that registers Vuetify globally is identified during F3 (likely `apps/app/vitest.config.ts` setupFiles or `apps/app/src/testing/setup.ts`). In F3 it is updated to register PrimeVue alongside Vuetify (transitional). In F5 the Vuetify registration is removed and the 22 mount-tests are validated to pass against PrimeVue-only registration. Component-level test changes are made only where tests assert on Vuetify-specific class names or DOM structure — those assertions are rewritten to PrimeVue equivalents on a per-test basis.
### AD-11: Toast and ConfirmDialog services
VSnackbar usage (39 occurrences across 35 files) migrates to PrimeVue's `<Toast>` mounted once in `App.vue` plus `useToast()` composable at call-sites. Existing `useNotificationStore` continues to drive the API; only the underlying renderer changes.
VDialog confirm-pattern usage (subset of 86 VDialog occurrences) where the dialog is a yes/no confirmation migrates to PrimeVue's `<ConfirmDialog>` + `useConfirm()` service. Complex dialogs (forms in modals, multi-step) migrate to plain `<Dialog>` components.
### AD-12: Tailwind CSS v4 added for layout utilities
Tailwind CSS v4 is installed in F3 (`tailwindcss@^4`, `@tailwindcss/vite@^4`). The Aura preset is configured with `cssLayer: { name: 'primevue', order: 'tailwind-base, primevue, tailwind-utilities' }` so utility classes win over component defaults. VRow / VCol / VSpacer occurrences (524 total) migrate to Tailwind grid utilities (`grid grid-cols-12 gap-4`, `flex items-center justify-between`, etc.). PrimeFlex is not installed (deprecated by PrimeTek).
**Rationale:** Tailwind v4 is the layout substrate PrimeVue 4 documents and Aura examples assume. Without it we would write ~30 lines of custom layout CSS plus lose the utility-class vocabulary that the broader ecosystem now uses. Cost: one additional dev dependency, ~10 KB to the production bundle, and a learning curve for `grid-cols-N` / `flex` utilities. Benefit: future styling work moves at ecosystem-standard speed.
---
## 6. Sprint Breakdown
Six work packages, executed strictly serially. Each package ends with: (a) all tests green, (b) Bert review and approval, (c) commit pushed, (d) Claude Chat re-syncs to confirm before issuing the next prompt.
### F2 — Documentation rewrite (1 day)
**Output:**
- New: `dev-docs/PRIMEVUE_COMPONENTS.md` (~700 lines, 9 sections matching VUEXY_COMPONENTS.md structure but for PrimeVue). Includes component mapping reference, page patterns, `<FormField>` API, theme tokens, three-state pattern (loading/empty/error) in PrimeVue idioms, DataTable lazy + virtual scroll cookbook.
- Updated: `CLAUDE.md` frontend section — 4-step decision tree retargeted to PrimeVue, 11 component rules updated, mandatory pre-task check now references `PRIMEVUE_COMPONENTS.md`. The `/p/*` shorthand corrected to `/portal/*` (matches reality per audit).
- Updated: `.cursorrules` frontend rules.
- Deleted: `dev-docs/VUEXY_COMPONENTS.md` (replaced by `PRIMEVUE_COMPONENTS.md`).
- Updated: `dev-docs/SYNC_MANIFEST.md` (post-commit hook).
**Definition of Done:** all docs committed, `npm run sync:docs` regenerates `.claude-sync/`, Bert uploads new manifest to Project Knowledge.
**Note:** F2 produces no code changes. Documentation lands first so subsequent prompts can reference the canonical component map.
### F3 — Foundation (2 days)
**Output:**
- Dependencies installed: `primevue@^4.5`, `@primevue/themes@^4.5`, `@primevue/forms@^4.5`, `primelocale@^1`, `tailwindcss@^4`, `@tailwindcss/vite@^4`.
- Dependencies retained: `flatpickr`, `vue-flatpickr-component`, `@iconify-json/tabler`, `@iconify-json/mdi`, `@iconify-json/fa`, all Pinia/TanStack/Vue Query/Zod packages.
- New: `apps/app/src/plugins/primevue/index.ts` — registers PrimeVue plugin with Aura preset, theme tokens, nl-NL locale.
- New: `apps/app/src/plugins/primevue/theme.ts` — Aura `definePreset` with Crewli teal semantic tokens.
- New: `apps/app/src/plugins/primevue/defaults.ts` — global `pt` (PassThrough) defaults replacing Vuetify component defaults from `defaults.ts`.
- New: `apps/app/src/components/forms/FormField.vue` — wrapper component (spec in Appendix A).
- New: `apps/app/src/components/Icon.vue` — generic Iconify renderer (~20 lines).
- Rewritten: `apps/app/src/layouts/default.vue`, `blank.vue`, `OrganizerLayout.vue`, `PortalLayout.vue`, `PublicLayout.vue`.
- New: `apps/app/tailwind.config.js` (Tailwind v4 minimal config).
- Updated: `apps/app/vite.config.ts` (add Tailwind Vite plugin; vite-plugin-vuetify retained until F6).
- Updated: `apps/app/src/main.ts` (register PrimeVue plugin, import `tailwindcss/index.css`, retain Vuetify imports until F6).
- Updated: `apps/app/src/App.vue` (mount `<Toast>` and `<ConfirmDialog>`).
- New: `apps/app/src/composables/useFormError.ts` — merges Zod resolver errors with API 422 responses.
- Identified: Vitest setupFile location documented in F3 closing summary (no flip yet — that's F5).
**Definition of Done:**
- App boots, login page renders with PrimeVue OrganizerLayout but old pages still use Vuetify (parallel mode).
- `<FormField>` wrapper has at least one usage in a sample page (e.g., login).
- Tailwind utility classes work (`<div class="flex gap-4">` renders).
- All 402 existing tests still pass.
- Bundle size measured (expected: temporary increase to ~2.4 MB JS, ~3.5 MB CSS — both stylesheets loaded).
**Risk:** Layout-shell rewrite is the largest single discrete change. Recommend committing the shell change in isolation before any component migration, so rollback is one-commit-revert if needed.
### F4 — Component migration (56 days, 4 sub-packages)
Each sub-package migrates one route tree. Within a sub-package: pages migrate one at a time, smallest pages first, dialogs/modals/components used by those pages migrate alongside their parent page.
#### F4a — `/portal/*` (1 day)
7 pages, 2 with forms. Smallest blast radius — proves the migration pattern end-to-end. Includes the dropdown UX from `UX_SPEC_FESTIVAL_HIERARCHY.md` (4 scenarios). At F4a closure, the pattern (component mappings, FormField usage, Toast invocations, layout integration) is locked and re-applied uniformly in F4bd.
#### F4b — Organizer root (3 days)
30 pages, 7 with forms. Includes 2 datatables (members, organisation/companies) and the Artist Management module (whatever state it is in at F4b start — committed-Vuetify or partially-built). Highest absolute page count. Sub-divided into:
- F4b.1: top-level pages (login, account-settings, dashboard, members, organisation/*) — ~10 pages
- F4b.2: events/* tree (12 pages) including event-detail tabs
- F4b.3: dialogs and components consumed across organizer pages
#### F4c — `/platform/*` (1 day)
8 pages, 4 with datatables (organisations index, users index, activity-log, form-failures). Three of these are 1000+ row tables — perfect testing ground for DataTable lazy + virtual scroll patterns. Server-side filtering wired to existing TanStack Query params.
#### F4d — Public registration + Form Builder (1.5 days)
`register/[public_token].vue`, `register/success.vue`, plus the public form renderer composables under `apps/app/src/composables/forms/`. Most complex due to dynamic field rendering (the form-renderer reads form_schema, renders field-by-field based on field_type enum). Form-renderer field components map case-by-case to PrimeVue equivalents. Form Builder organizer UI (S3b) is explicitly NOT part of F4d — that work begins post-launch on the new PrimeVue foundation.
**Definition of Done (per sub-package):**
- All pages in the sub-package render with zero Vuetify components in the visible tree.
- All forms in the sub-package use `<Form>` + `<FormField>` + Zod.
- Tests for migrated components pass (failures investigated; assertion-on-Vuetify-class-name updates allowed).
- No new TypeScript errors.
- Bundle size measured at sub-package end, recorded in commit message.
### F5 — Tests, accessibility, performance (1 day)
**Output:**
- Vitest setupFile flipped: PrimeVue-only registration, Vuetify registration removed.
- 22 mount-tests verified passing.
- axe-core audit run on top 10 screens (login, dashboard, events list, event detail, members, organisation settings, platform users, platform activity-log, public registration, portal events). Issues triaged: blocker bugs fixed, non-blockers logged in BACKLOG.md as `A11Y-PRIMEVUE-*` items.
- DataTable performance benchmark: 5000-row mock dataset rendered in `pages/platform/users/index.vue` configuration (lazy, virtual scroll, sortable). Acceptance: < 800 ms initial render on M-class laptop, < 100 ms scroll-frame time.
- Bundle size measured: targets in §11.
**Definition of Done:** all tests pass, axe-core blockers fixed, perf benchmark meets targets, commit message documents measurements.
### F6 — Cleanup (0.5 day)
**Output:**
- Removed dependencies: `vuetify`, `vite-plugin-vuetify`, `@mdi/js` (if present), `vue-flatpickr-component` (only if Flatpickr replaced — not in this sprint, so retained).
- Removed: `apps/app/src/@core/` (entire tree, 137 files).
- Removed: `apps/app/src/@layouts/` (entire tree, 29 files).
- Removed: `apps/app/src/plugins/vuetify/` (entire directory).
- Removed from `apps/app/src/main.ts`: Vuetify plugin registration, Vuetify CSS imports.
- Removed from `apps/app/vite.config.ts`: `vite-plugin-vuetify` plugin.
- Removed: 5 SVG form-control overrides under `@images/svg/`.
- Updated: `tsconfig.json` paths (remove `@core/*`, `@layouts/*` aliases).
- Updated: `.cursorrules` if it still references Vuetify.
- Verified: `pnpm build` succeeds; `pnpm test --run` shows 402 tests passing; bundle size measured against §11 targets.
**Definition of Done:** clean `package.json`, no orphaned imports, no orphaned SCSS, bundle size at or below targets, closure-PR opened with summary commit.
---
## 7. Component Mapping Reference (top 25 by usage)
The full 75-component mapping lives in `dev-docs/PRIMEVUE_COMPONENTS.md` after F2. The table below is the binding subset for sprint planning.
| Vuetify | Uses | PrimeVue replacement | Notes |
|---|---:|---|---|
| VBtn | 420 | `Button` | `variant='tonal'``severity='secondary' outlined`; `variant='text'``text` prop; `:loading``:loading` |
| VCol | 331 | Tailwind `col-span-N` | `<div class="md:col-span-6">` etc. |
| VIcon | 267 | `<Icon name="tabler-X">` (custom 20-line wrapper) | Iconify class output |
| VCard | 250 | `Card` | Slot mapping: `#title`, `#subtitle`, `#content`, `#footer` |
| VCardText | 208 | `<div class="p-6">` (or Card `#content` slot) | Just padding |
| VAlert | 116 | `Message` | `type='success/info/warning/error'``severity` |
| VChip | 102 | `Tag` (preferred) or `Chip` | `Tag` for status badges; `Chip` for removable filters |
| VRow | 102 | Tailwind `grid grid-cols-12 gap-4` | |
| VSpacer | 91 | Tailwind `flex-1` | Or surrounding `justify-between` |
| VDialog | 86 | `Dialog` | `:max-width``:style="{ width: '500px' }"` |
| VCardActions | 82 | `<template #footer>` (in Card) | Footer slot of Card |
| VListItem | 82 | `MenuItem` (in menu) or `<li>` (in list) | Context-dependent |
| VForm | 64 | `Form` from `@primevue/forms` + `<FormField>` | See Appendix A |
| VAvatar | 59 | `Avatar` | `variant='tonal'``:style="{ background: ... }"` |
| VCardTitle | 56 | `<template #title>` (in Card) | |
| VDivider | 56 | `Divider` | `:vertical``layout='vertical'` |
| VSkeletonLoader | 52 | `Skeleton` | `type='card'` → manual structure |
| VList | 51 | `Listbox` (selection) or `DataView` (cards) | Context-dependent |
| VSnackbar | 39 | `useToast()` + `<Toast>` mounted in App.vue | Notification store unchanged |
| VSwitch | 28 | `ToggleSwitch` | |
| VTextField | 27 | `InputText` + `FloatLabel` + `Message` (via `<FormField>`) | `density='compact'``size='small'` |
| VMenu | 21 | `Menu` (anchor) or `Popover` (rich content) | |
| VTabs / VTab / VWindow | 11 / 17 / 6 | `Tabs` + `TabList` + `Tab` + `TabPanels` + `TabPanel` | |
| VDataTable / VDataTableServer | 9 / 5 | `DataTable` + `Column` (`:lazy="true"` for server) | See AD-7 |
| VAutocomplete | 4 | `AutoComplete` | |
| VSelect | 5 | `Select` | |
**Long-tail (≤ 11 uses) treated case-by-case during F4** — full mapping in `PRIMEVUE_COMPONENTS.md`.
---
## 8. Risk Register
| ID | Risk | Probability | Impact | Mitigation |
|---|---|---|---|---|
| R-1 | DataTable virtual scroll regressions on specific column types (custom slots) | Medium | High | Benchmark in F5 with the heaviest real table (`pages/platform/users/index.vue`); rollback to non-virtual mode as Plan B |
| R-2 | `<FormField>` wrapper API doesn't compose well for complex forms (multi-section, conditional fields) | Medium | High | Build the wrapper in F3 against the most complex form in F4d (public registration with sections); revise API before F4a if signs of strain |
| R-3 | Tailwind utility class name collisions with existing custom classes | Low | Medium | Tailwind v4 prefix config (`tw-` if needed) — decide in F3 based on namespace audit |
| R-4 | Mount-tests fail in F5 due to PrimeVue-specific DOM structure | High | Low | Expected. Rewrite assertions per-test; this is anticipated work, not a defect |
| R-5 | Concurrent Artist Management work creates merge conflicts during F4b | Medium | Medium | F4b explicitly absorbs Artist Management code as it stands at F4b start; communication before F4b begins |
| R-6 | Bundle size targets (§11) not met | Low | Medium | Iterate in F6: tree-shake harder, defer non-critical imports, consider PrimeVue per-component imports vs. global registration |
| R-7 | Aura preset doesn't match Crewli brand once seen at scale | Medium | Medium | F3 includes a brand-review checkpoint: render the dashboard + 2 representative pages, get Bert sign-off, before F4 starts |
| R-8 | `useToast()` API differs enough from VSnackbar that `useNotificationStore` needs API changes | Low | Low | Wrap in `useNotificationStore` to keep call-site API stable |
| R-9 | Flatpickr CSS conflicts with PrimeVue tokens (z-index, color) | Low | Low | Verify in F3 with a date-input render; scope CSS overrides if needed |
| R-10 | Layout shell rewrites break responsive behavior on mobile | Medium | Medium | Manual mobile testing in F3 closure; layout shells are < 600 lines so issues are bounded |
---
## 9. Rollback Plan
The migration is committed on a feature branch (`migration/primevue`). Each F-package commits incrementally with descriptive messages. Rollback strategies by stage:
- **During F2:** revert documentation commits; no code touched.
- **During F3:** revert the foundation commit and dependency installs. Vuetify still registered, app reverts to current state.
- **During F4 (sub-package level):** revert the sub-package commit. Other migrated trees keep working because parallel-mode (Vuetify + PrimeVue both registered) is in force until F6.
- **During F5:** test fixes are reversible per-test commit.
- **After F6:** rollback requires reinstalling Vuetify + Vuexy SCSS. Practical but expensive (~1 day to undo). F6 is the point of no return.
The migration branch is squash-merged to `main` only after F6 closure and full test pass. Production deploy follows the standard process. If a critical bug surfaces in production within 7 days of deploy, the rollback is a `main` revert of the squash commit and a re-deploy.
---
## 10. Definition of Done (Sprint-Level)
The sprint is closed when all of the following are true:
1. `apps/app/package.json` does not list `vuetify`, `vite-plugin-vuetify`, or any Vuexy template-related dev dependency.
2. `apps/app/src/@core/`, `apps/app/src/@layouts/`, `apps/app/src/plugins/vuetify/` directories do not exist.
3. `rg '<V[A-Z]' apps/app/src --glob '*.vue'` returns 0 matches.
4. `rg "from ['\"]vuetify" apps/app/src` returns 0 matches.
5. `rg "from ['\"]@core/" apps/app/src` returns 0 matches.
6. `rg "from ['\"]@layouts/" apps/app/src` returns 0 matches.
7. `pnpm test --run` reports ≥ 402 tests passing (count may grow if new tests added).
8. `pnpm build` succeeds.
9. Bundle size measurement: main JS chunk ≤ 180 KB gzip, total CSS ≤ 400 KB uncompressed.
10. Manual smoke test of all 4 route trees passes (login → dashboard, organisation switch, /platform/* admin tasks, /portal/* event flow, /register/[token] flow).
11. axe-core audit of top 10 screens shows 0 critical violations.
12. `dev-docs/PRIMEVUE_COMPONENTS.md` exists; `dev-docs/VUEXY_COMPONENTS.md` does not.
13. `CLAUDE.md` and `.cursorrules` reference PrimeVue patterns; no remaining Vuetify or Vuexy references.
14. `dev-docs/SYNC_MANIFEST.md` regenerated, `.claude-sync/` uploaded to Project Knowledge.
15. Memory updated to reflect completion (this RFC moved from "Approved" to "Implemented").
---
## 11. Bundle Size Targets
| Metric | F1 baseline | F6 target | F6 stretch |
|---|---|---|---|
| Main JS chunk (gzip) | 213 KB | ≤ 180 KB | ≤ 150 KB |
| Total JS uncompressed | 1.90 MB | ≤ 1.40 MB | ≤ 1.20 MB |
| Total CSS uncompressed | 3.32 MB | ≤ 400 KB | ≤ 300 KB |
| Largest single CSS chunk | 3.14 MB | ≤ 300 KB | ≤ 200 KB |
| Initial paint resources | 597 KB JS + 3.14 MB CSS | < 1 MB combined | < 700 KB combined |
Targets are advisory; F6 closure is approved if F6-target column is met. Stretch targets identify opportunities for post-launch optimization sprints.
---
## 12. Backlog Items Created by This RFC
These are added to `BACKLOG.md` during F2 and tracked independently:
- `FRONTEND-DATEPICKER-PRIMEVUE-NATIVE` — replace Flatpickr with PrimeVue DatePicker. Post-launch, low priority.
- `FRONTEND-VUE-I18N-ADOPTION` — extract Dutch UI strings to vue-i18n message files for future multi-language support. Medium priority, separate sprint.
- `A11Y-PRIMEVUE-*` — placeholder for any non-blocker axe-core findings during F5.
- `FRONTEND-DESIGN-SYSTEM-EXPANSION` — extend Aura semantic tokens for Crewli-specific spacing, type ramp, motion. Low priority, post-launch.
- `FRONTEND-VOLT-EVALUATION` — assess PrimeVue Volt (Tailwind v4 unstyled wrapper) as a future style-management approach. Low priority.
---
## 13. Decisions That Were NOT Made
To prevent ambiguity, decisions deliberately deferred:
- **PrimeVue Pro (PrimeBlocks) license purchase.** Free tier is sufficient for sprint scope. Re-evaluate after launch if dashboard pages need premium block components.
- **Sakai or other PrimeVue admin templates.** None will be adopted; layout shells are hand-rolled to avoid template lock-in.
- **CSS-in-JS migration (e.g., Pinceau, Vanilla Extract).** Out of scope; Aura tokens + Tailwind utilities cover all cases.
- **State machine for form wizards** (e.g., XState). Out of scope; existing `useFormSteps.ts` remains.
- **Storybook or component playground.** Useful but out of scope; revisit post-launch.
---
## 14. Approval & Next Action
This RFC is approved on **2026-05-10**. The next concrete action is to commit the RFC to `dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md`, regenerate the sync manifest, upload to Project Knowledge, then issue the **F2 prompt to Claude Code** (documentation rewrite). Architectural decisions in this RFC are binding for the duration of the sprint; deviations require RFC amendment with explicit rationale.
---
## Appendix A — `<FormField>` API Specification
Project-owned wrapper at `apps/app/src/components/forms/FormField.vue`. Goal: terse call-site syntax that abstracts the @primevue/forms boilerplate.
### Usage example
```vue
<script setup lang="ts">
import { Form } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import { z } from 'zod'
import FormField from '@/components/forms/FormField.vue'
import InputText from 'primevue/inputtext'
import Select from 'primevue/select'
import Button from 'primevue/button'
const schema = z.object({
email: z.string().email('Geen geldig e-mailadres'),
role: z.enum(['organizer', 'volunteer'], { message: 'Selecteer een rol' }),
})
const onSubmit = async ({ valid, values }: any) => {
if (valid) await api.invite(values)
}
</script>
<template>
<Form :resolver="zodResolver(schema)" @submit="onSubmit" v-slot="$form">
<FormField name="email" label="E-mailadres" required>
<InputText name="email" />
</FormField>
<FormField name="role" label="Rol" required>
<Select name="role" :options="roleOptions" />
</FormField>
<Button type="submit" label="Uitnodigen" :loading="$form.submitting" />
</Form>
</template>
```
### Component contract
```ts
// FormField.vue props
interface FormFieldProps {
name: string // matches Zod schema key
label?: string // optional — rendered as <label>
required?: boolean // shows asterisk; does NOT add validation (Zod owns that)
hint?: string // grey helper text below input
apiError?: string | null // overrides validation message (for 422 surfacing)
}
// Slots:
// default — the actual PrimeVue input (must have :name matching `name`)
//
// Renders:
// <div class="field">
// <label>{{ label }}<span v-if="required">*</span></label>
// <slot />
// <Message v-if="error" severity="error" size="small">{{ error }}</Message>
// <small v-else-if="hint">{{ hint }}</small>
// </div>
//
// Error precedence: apiError > Zod resolver error from $form context
```
### API 422 integration
`useFormError(formRef)` composable merges API 422 responses into the form state:
```ts
import { useFormError } from '@/composables/useFormError'
const formRef = ref()
const { applyApiErrors, clearApiErrors } = useFormError(formRef)
const onSubmit = async ({ valid, values }: any) => {
if (!valid) return
clearApiErrors()
try {
await api.invite(values)
} catch (e) {
if (e.response?.status === 422) applyApiErrors(e.response.data.errors)
else throw e
}
}
```
`applyApiErrors` injects per-field error messages into `<FormField>` via the `apiError` prop pattern — no manual error refs needed.
---
## Appendix B — Aura Theme Token Plan
```ts
// apps/app/src/plugins/primevue/theme.ts
import Aura from '@primevue/themes/aura'
import { definePreset } from '@primevue/themes'
export const CrewliPreset = definePreset(Aura, {
semantic: {
primary: {
50: '#E6F4F4',
100: '#CCE9EA',
200: '#99D3D4',
300: '#66BDBE',
400: '#33A7A8',
500: '#0D9394', // Crewli teal
600: '#0B7F80', // Crewli teal dark
700: '#086B6C',
800: '#055758',
900: '#034344',
950: '#012F30',
},
colorScheme: {
light: {
primary: { color: '{primary.500}', contrastColor: '#ffffff', hoverColor: '{primary.600}', activeColor: '{primary.700}' },
// surface, formField, list, navigation, overlay, content tokens use Aura defaults
},
dark: {
primary: { color: '{primary.400}', contrastColor: '{surface.900}', hoverColor: '{primary.300}', activeColor: '{primary.200}' },
},
},
},
})
```
Component-level overrides use the `pt` (PassThrough) prop globally configured in `defaults.ts`, replacing Vuetify's `defaults: { VBtn: { ... } }` pattern.
---
## Appendix C — Version Pinning Policy
PrimeVue moves fast (4.5.0 → 4.5.5 in OctDec 2025). Sprint pins:
- `primevue` and `@primevue/themes` and `@primevue/forms`: pin to **same exact patch version** (e.g., `4.5.5`) in F3. They must always upgrade together to avoid token-mismatch bugs.
- `tailwindcss` and `@tailwindcss/vite`: pin to same exact version.
- `primelocale`: caret range fine (`^1`).
Post-launch upgrade policy: monthly review, upgrade in lockstep, regression-test against the F5 axe-core + perf benchmarks before merging.
---
**End of RFC.**