Files
crewli/dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md
2026-05-16 01:10:02 +02:00

731 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `@primeuix/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.
> **Drift correction (F3 B9):** RFC v1.0 referenced `@primevue/themes@^4.5`. That package was deprecated by its maintainers (PrimeFaces) during the F3 sprint with explicit guidance to migrate to `@primeuix/themes`. PrimeVue 4's official install documentation at primevue.org/vite/ now specifies `@primeuix/themes` as the theme package. Same maintainers, same API surface (Aura preset, `definePreset`, semantic tokens). F3 commit B1 installed `@primeuix/themes@^2` directly; this paragraph and Appendix B are aligned with that ecosystem state.
**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 content. The custom Vuetify icon adapter in `apps/app/src/plugins/vuetify/icons.ts` is replaced by a tiny generic `<Icon>` component (at `apps/app/src/components/Icon.vue`) that wraps `@iconify/vue`'s `<Icon>` component and emits real `<svg>` markup. Call-site migration substitutes `<VIcon icon="tabler-X" />` with `<Icon name="tabler-X" />`. The 5 SVG checkbox/radio overrides in `@images/svg/` become unused (PrimeVue's Checkbox/RadioButton ship their own styling) and are deleted in F6.
> **Drift correction (F3 B9):** RFC v1.0 described the new `<Icon>` component as rendering `<i :class="i-tabler-X">` utility classes. That approach would require UnoCSS to resolve `i-tabler-*` into real icon output, and UnoCSS is not in the Crewli stack — the project already uses `@iconify/vue` (installed at 4.1.2) for SVG icon rendering through Vuetify's icon adapter. F3 commit B6 implements `Icon.vue` as a thin wrapper around `@iconify/vue` accordingly. The substitution preserves existing icon naming (`tabler-eye`, `tabler-user`, etc.) and produces equivalent SVG output to the current Vuetify path. UnoCSS-style utility-class icon rendering was considered but not adopted; the wrapper retains Crewli's continuity.
**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`, `@primeuix/themes@^2` [^themes-pkg], `@primevue/forms@^4.5`, `primelocale@^1`, `tailwindcss@^4`, `@tailwindcss/vite@^4`.
[^themes-pkg]: RFC v1.0 specified `@primevue/themes@^4.5`. That package was deprecated during F3 with explicit guidance to migrate to `@primeuix/themes` (same maintainers, same API surface). PrimeVue 4's official install documentation now prescribes `@primeuix/themes`. See F3 commit B1 for the substitution rationale and B9 for the AD-2 + Appendix B alignment.
- 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.
> **⚠️ SUPERSEDED (2026-05-16):** F4aF4d below are superseded by
> `dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md`. The page-migration
> strategy changed (crewli-starter is now the design source, parallel
> `/v2/*` routes, page-by-page cutover). AD-1..AD-12 remain binding.
> F2/F3/F5/F6 are unaffected.
### 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.**~~ **Landed post-RFC** (Storybook 10, commit ebb8e3bc). See `dev-docs/FRONTEND-TOOLING.md` § Storybook.
---
## 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
// Drift correction (F3 B9): imports use @primeuix/themes (the
// maintained successor that PrimeVue 4's install docs now prescribe).
import { definePreset } from '@primeuix/themes'
import Aura from '@primeuix/themes/aura'
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/forms`: pin to **same exact patch version** (e.g., `4.5.5`) in F3. They must always upgrade together to avoid token-mismatch bugs.
- `@primeuix/themes`: pin to `^2` (own release cadence, decoupled from PrimeVue's 4.x patch line — same maintainers, separate package per the F3 B9 drift correction).
- `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.**
---
## Amendment 2026-05-10: TEST-INFRA-001 inserted before F2
| Field | Value |
|---|---|
| **Amendment ID** | A-1 |
| **Status** | Approved |
| **Date** | 2026-05-10 |
| **Author** | Architecture (Claude Chat) |
| **Trigger** | Timetable stabilization sprint (PR #18, #19) surfaced three diagnostic incidents that the RFC v1.0 sequencing did not anticipate |
| **Scope of change** | Sequencing addition + F5 scope extension + Risk register + DoD. **No changes to F2F6 internal architecture, no changes to Aura preset, no changes to `<FormField>` API, no changes to Tailwind v4, no changes to bundle size targets.** |
### A.1 Background
This RFC v1.0 was approved on 2026-05-10. On the same day, the closing PR of the timetable stabilization sprint (`fix/timetable-stabilization`, PR #19) merged to main. That sprint formalized a new project principle in `CLAUDE.md` titled "Diagnostic discipline: audit before assume", anchored to three empirical incidents:
- **B1** — controller assumed buggy; schema-verify gate against `SCHEMA.md:1285` proved the seeder was wrong, controllers correct
- **B5** — enum-shape assumed drifted; field-by-field response audit proved decimal-as-string was the actual drift
- **UX divergence** — 397 jsdom-tests green, but manual browser test against the prototype revealed substantial divergence (PerformanceBlock missing genre tag, drag broken, resize handles missing, etc.) — tracked as `ART-S4-UX-PARITY` in BACKLOG.md
The common pattern across all three: **the existing Vitest+jsdom test layer did not detect what the product actually does in a real browser**. Tests asserted against mocks that agreed with schemas; schemas didn't agree with the backend wire format; layouts didn't match the prototype; jsdom couldn't compute layout cascade, sticky positioning, or visual regressions.
This RFC's F4 sub-packages (a/b/c/d) constitute a structurally larger refactor than the timetable module — every `.vue` page in the SPA changes, 75 distinct Vuetify components are replaced, 38 form-bearing components migrate to `<FormField>`. The probability that the same pattern of incidents will recur during F4 — without test infrastructure capable of detecting them — is high enough to warrant addressing pre-emptively.
The RFC v1.0 author did not have access to the timetable-stabilization context (the work was done in a separate Claude Chat that focused exclusively on the migration audit). Hence this amendment, not a v1.1 rewrite.
### A.2 What changes
| Aspect | RFC v1.0 | After amendment |
|---|---|---|
| Sequence | F1 → F2 → F3 → F4 → F5 → F6 | F1 → **TEST-INFRA-001** → F2 → F3 → F4 → F5 → F6 |
| F5 scope | axe-core + 22 mount-tests + perf benchmark | All of v1.0 **plus visual regression baselines (intermediate for Artist Management, first-capture for other surfaces)** |
| Risk register | 10 risks (R-1 through R-10) | 10 + **R-11** (visual regression coverage gap) |
| Definition of Done | 15 items | 15 + **5 new items** (DoD-16 through DoD-20) |
| Sprint effort | 1012 working days | **1519 working days** (+57 for TEST-INFRA-001) |
| Concurrent Artist Management work | Continues on Vuetify until F4b absorbs it | Same — TEST-INFRA-001 does not modify application code, only test infrastructure |
### A.3 New sprint: TEST-INFRA-001 (57 working days)
This sprint is **prerequisite to F2**. F2 cannot begin until TEST-INFRA-001 closes. Sprint scope at high level — full sprint prompt is authored separately and is the next concrete action after this amendment is committed.
**Goals:**
1. **Playwright Component Testing foundation**: install `@playwright/test` + `@playwright/experimental-ct-vue` + axe-core dependencies. Configure component-test runner with Vuetify+Pinia+TanStack+router providers. Demonstrate one working component test that mounts a current Vuetify component (proves the foundation) and one that mounts a prototype HTML region (proves visual baseline pipeline).
2. **Visual regression infrastructure**: pixel-diff tooling, baseline storage in Git LFS (self-hosted per Bert's preference), prototype-HTML rendering pipeline (the prototype at `./resources/Crewli - Artist Timetable Management/` is React+Babel via unpkg — Playwright renders it once, captures baselines, stores them as authoritative). CI integration with PR-comment diff visibility.
3. **Real-backend e2e setup**: Playwright e2e configuration that spins up Laravel test-server, authenticates via Sanctum, runs against seeded fixtures. First flow: the 409 conflict contract test from `TEST-CONTRACT-001`.
4. **Architecture documentation**: `dev-docs/ARCH-TESTING.md` codifying the test pyramid (unit / component / integration / e2e / visual), scope per layer, when something belongs in which layer, conventions for mock-vs-real-backend choices, baseline update workflow.
5. **No migration of existing Vitest tests**. Existing 402 Vitest+jsdom tests remain unchanged. They will be naturally replaced during F4 component migration (each migrated component gets a fresh Playwright Component Test, the corresponding Vitest+jsdom test is removed at that point). This is a deliberate scope decision — migrating tests now and again during F4 is duplicate work.
**Baseline scope:**
Visual regression baselines in this sprint are limited to **Artist Management surfaces** (PerformanceBlock, PerformancePopover, AddPerformanceDialog, Wachtrij, StageRow, plus the timetable canvas at three states: empty / partial / full). Reason: these are the only surfaces with a canonical prototype to baseline against. Other modules (Volunteers, Events, Forms, Platform Admin) get baselines progressively as they're migrated through F4 — and those baselines are taken against the **PrimeVue implementation post-migration**, not against the current Vuetify implementation that we are about to discard.
**Definition of Done:**
- All five goals delivered, branch merged to main, `.claude-sync/` re-uploaded
- Existing 402 Vitest+jsdom tests still pass unchanged
- At least one Playwright component test passing in CI (smoke proof)
- At least one visual regression baseline committed (smoke proof)
- One real-backend e2e test passing (TEST-CONTRACT-001 first flow)
- `ARCH-TESTING.md` reviewed and committed
**BACKLOG consolidation:**
This sprint consumes and closes three pre-existing BACKLOG entries from the timetable-stabilization PR:
- `TEST-INFRA-001` — closed by goal 1+2 above
- `TEST-CONTRACT-001` — closed by goal 3 above
- `TEST-VISUAL-001` — closed by goal 2 above (with explicit prototype-HTML baseline source)
### A.4 Updated sequencing
F1 ─► TEST-INFRA-001 ─► F2 ─► F3 ─► F4a ─► F4b ─► F4c ─► F4d ─► F5 ─► F6
╰──── 5-7 d ────╯ ╰1d╯ ╰2d╯ ╰1d╯ ╰3d╯ ╰1d╯ ╰1.5d╯ ╰1.5d╯ ╰0.5d╯
╰── visual & e2e ──╯
╰── infra only ────╯
After F6: ART-S4-UX-PARITY (Artist Management UX parity on PrimeVue, with visual regression baselines as objective measure)
After ART-S4-UX-PARITY: Sessie 5 (Engagement Detail), Accreditation Engine, etc.
Total estimated effort: 1519 working days for TEST-INFRA-001 + F2F6.
### A.5 F5 scope extension
RFC v1.0 §6, F5 (Tests, accessibility, performance) is extended with one additional output. The handling depends on whether a prototype-baseline exists for the surface being validated:
**Artist Management surfaces** (PerformanceBlock, PerformancePopover, AddPerformanceDialog, Wachtrij, StageRow, timetable canvas — originally baselined against prototype HTML in TEST-INFRA-001):
- F5 captures the PrimeVue-implementation baselines as a **separate intermediate baseline set**. These are explicitly NOT expected to match the prototype baselines.
- Reason: UX parity with the prototype is out of scope for F4 (F4b absorbs the Artist Management module "as it stands at F4b start" per RFC §6, which is its current Vuetify implementation including all gaps documented in `ART-S4-UX-PARITY`). PrimeVue migration converts those Vuetify components to PrimeVue equivalents but does not close the UX gaps.
- F5 acceptance for these surfaces requires only that the PrimeVue implementations render **structurally correctly** (no missing components, no broken layouts, no console errors, no a11y blockers) — not pixel-equivalence with the prototype.
- `ART-S4-UX-PARITY` (post-F6 sprint) closes the gap by refactoring the PrimeVue Artist Management surfaces to match the prototype, and at that point updates the intermediate baselines to align with the prototype baselines.
**Surfaces outside Artist Management** (Volunteers, Events, Forms, Platform Admin, etc. — no prototype baseline exists):
- F5 captures the **first PrimeVue-implementation baseline** for each surface. This becomes the regression anchor going forward.
- F5 acceptance requires the baseline to be reproducible (re-running the capture produces an identical baseline within configured pixel tolerance).
The existing F5 outputs (axe-core audit, mount-test verification, perf benchmark) are unchanged.
### A.6 Risk register addition
| ID | Risk | Probability | Impact | Mitigation |
|---|---|---|---|---|
| R-11 | F4 component migration introduces visual regressions that jsdom-tests do not detect, mirroring the timetable-stabilization pattern | High (without TEST-INFRA-001) / Low (with) | High | TEST-INFRA-001 sprint inserted before F2 establishes Playwright + visual regression baselines against prototype HTML. F4 sub-packages must show passing baselines before sub-package closure. F5 re-validates all baselines against PrimeVue final state with the dual-tier scope defined in §A.5. |
### A.7 Definition of Done — additional items
The sprint-level DoD in RFC v1.0 §10 is extended with:
16. `dev-docs/ARCH-TESTING.md` exists and is referenced from `CLAUDE.md`.
17. Playwright Component Testing infrastructure is operational; `pnpm test:component` (or equivalent) runs in CI.
18. Visual regression baselines exist for all Artist Management surfaces; `pnpm test:visual` (or equivalent) passes against them.
19. At least one real-backend e2e test (the 409 conflict contract test from TEST-CONTRACT-001) passes against a Laravel test-server in CI.
20. BACKLOG entries `TEST-INFRA-001`, `TEST-CONTRACT-001`, and `TEST-VISUAL-001` are marked closed in `BACKLOG.md`, with references to the sprint commits.
### A.8 Effort impact
| Phase | RFC v1.0 estimate | After amendment |
|---|---|---|
| F1 | already complete | already complete |
| **TEST-INFRA-001** | | **57 days** (new) |
| F2 | 1 day | 1 day |
| F3 | 2 days | 2 days |
| F4a | 1 day | 1 day |
| F4b | 3 days | 3 days |
| F4c | 1 day | 1 day |
| F4d | 1.5 days | 1.5 days |
| F5 | 1 day | 1.5 days (visual regression validation extends scope per §A.5) |
| F6 | 0.5 day | 0.5 day |
| **Total** | **1012 days** | **1519 days** |
No launch-pressure exists per current planning (Bert confirmation 2026-05-10: minimum 6 months). The 57 day investment is justified against the high probability that R-11 materializes during F4 without it.
### A.9 What this amendment does NOT change
To prevent ambiguity in future review:
- **F2 documentation rewrite scope** is unchanged (`PRIMEVUE_COMPONENTS.md`, CLAUDE.md update, `.cursorrules` update, `VUEXY_COMPONENTS.md` deletion).
- **F3 foundation outputs** are unchanged. Tailwind v4 still installs, `<FormField>` wrapper still gets built per Appendix A, layout shells still get rewritten, Aura preset still gets configured with Crewli teal.
- **F4 sub-package boundaries and order** are unchanged. F4a (`/portal/*`) → F4b (organizer root + Artist Management) → F4c (`/platform/*`) → F4d (public registration + Form Builder).
- **F6 cleanup deletions** are unchanged.
- **Bundle size targets** in §11 are unchanged.
- **Architectural decisions** AD-1 through AD-12 are unchanged.
- **`<FormField>` API specification** in Appendix A is unchanged.
### A.10 Approval & next action
This amendment is approved on **2026-05-10**. The next concrete action is to commit this amendment to `dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md` on the `audit/primevue-migration` branch, regenerate the sync manifest, upload `.claude-sync/` to Project Knowledge, then issue the **TEST-INFRA-001 sprint prompt** (authored separately).
After TEST-INFRA-001 closes and merges to main, F2 begins per RFC v1.0 §6.