Files
crewli/dev-docs/MIGRATION-AUDIT-PRIMEVUE.md
2026-05-10 01:57:12 +02:00

762 lines
43 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# MIGRATION-AUDIT-PRIMEVUE — WS-FRONTEND-PRIMEVUE F1
> **Status:** F1 inventory output. Pure facts only — no opinions, no proposals.
> **Scope:** `apps/app/` (the single SPA). Read-only audit; no application code modified.
> **Generated:** 2026-05-10 against branch `audit/primevue-migration` (cut from `main` at `62afbde`).
> **Stack snapshot:** Vue 3.5.22 · Vuetify 3.10.8 · Vuexy template v9.5.0 (per `apps/app/package.json` "version") on disk template ref v10.11.1 in `resources/` · TypeScript 5.9.3 · Vite 7.1.12 · Vitest 4.1.5 · TanStack Query 5.95 · Pinia 3.0.3.
>
> Reproduction commands are inlined per section in `<details>` blocks. All counts come from `rg` / `find`. Tables sort deterministically (count desc, then alpha). Run all commands from repo root unless noted.
---
## 1. Vuetify Component Usage Inventory
The codebase uses **PascalCase** Vuetify tags exclusively (`<VBtn>`, not `<v-btn>`); a kebab-case scan returned 0 matches. Counts below are **opening-tag occurrences** across all `.vue` files under `apps/app/src/`. The "Files" column is the deduplicated number of `.vue` files containing at least one such tag.
<details>
<summary>Reproduction commands</summary>
```bash
# Tag counts
rg --no-filename -o '<[Vv][A-Z][A-Za-z]+|<v-[a-z][a-z-]+' --glob '*.vue' apps/app/src \
| sort | uniq -c | sort -rn
# Files per tag
rg -l '<VBtn\b' --glob '*.vue' apps/app/src | wc -l # repeat per tag
```
</details>
### 1.1 Component frequency table (all components used ≥ 3 times)
| Component | Uses | Files |
|---|---:|---:|
| VBtn | 420 | 109 |
| VCol | 331 | 72 |
| VIcon | 267 | 99 |
| VCard | 250 | 119 |
| VCardText | 208 | 109 |
| VAlert | 116 | 69 |
| VChip | 102 | 49 |
| VRow | 102 | 71 |
| VSpacer | 91 | 62 |
| VDialog | 86 | 65 |
| VCardActions | 82 | 55 |
| VListItem | 82 | 41 |
| VForm | 64 | 38 |
| VAvatar | 59 | 34 |
| VCardTitle | 56 | 37 |
| VDivider | 56 | 32 |
| VSkeletonLoader | 52 | 39 |
| VList | 51 | 39 |
| VListItemTitle | 49 | 29 |
| VSnackbar | 39 | 35 |
| VSwitch | 28 | 15 |
| VTextField | 27 | 9 |
| VListItemSubtitle | 24 | 16 |
| VMenu | 21 | 20 |
| VCardItem | 20 | 13 |
| VWindowItem | 19 | 6 |
| VCheckbox | 18 | 14 |
| VTab | 17 | 11 |
| VLabel | 15 | 13 |
| VTooltip | 15 | 11 |
| VCardSubtitle | 12 | 10 |
| VImg | 12 | 12 |
| VNodeRenderer | 11 | 9 |
| VTabs | 11 | 11 |
| VTextarea | 11 | 11 |
| VDataTable | 9 | 9 |
| VProgressLinear | 8 | 8 |
| VRadio | 8 | 6 |
| VRadioGroup | 8 | 6 |
| VProgressCircular | 7 | 7 |
| VTimelineItem | 6 | 1 |
| VWindow | 6 | 6 |
| VContainer | 5 | 4 |
| VDataTableServer | 5 | 5 |
| VExpandTransition | 5 | 4 |
| VSelect | 5 | 4 |
| VAutocomplete | 4 | 4 |
| VNavigationDrawer | 4 | 4 |
| VApp | 3 | 3 |
| VBtnToggle | 3 | 3 |
| VCombobox | 3 | 3 |
| VListSubheader | 3 | 3 |
| VOtpInput | 3 | 3 |
| VTable | 3 | 3 |
| VTabsWindowItem | 3 | 3 |
### 1.2 Long tail (used < 3 times)
`VAppBar (1)`, `VAppBarNavIcon (1)`, `VBadge (1)`, `VColorPicker (2)`, `VExpansionPanel (1)`, `VExpansionPanelText (1)`, `VExpansionPanels (1)`, `VField (1)`, `VFooter (1)`, `VInput (1)`, `VLocaleProvider (1)`, `VMain (2)`, `VOverlay (1)`, `VPagination (1)`, `VScaleTransition (1)`, `VSheet (2)`, `VSlideGroup (1)`, `VSlideGroupItem (1)`, `VTabsWindow (1)`, `VTimeline (2)`.
### 1.3 Distinct prop combinations (top components)
Sampled by extracting attribute names from each component's opening tag across the codebase. Counts are **distinct attribute occurrences**, not unique combinations — high-frequency props show how the component is shaped in practice.
**VBtn** (420 uses): `variant=` 218 (203 literal + 15 bound), `color=` 149, `:loading=` 94, `size=` 85, `prepend-icon=` 61, `:disabled=` 38, `type=` 37, `class=` 19. Pattern: variant + color + optional loading is canonical.
**VChip** (102 uses): `size=` 92 (typically `"small"`), `:color=` 53, `variant=` 46, `color=` 31, `prepend-icon=` 10. Always uses `:label=true` from defaults (no per-instance override).
**VAlert** (116 uses): `type=` 114, `class=` 93, `variant=` 89, `density=` 48. Almost all alerts carry `type` + `variant`; common `variant` values are `tonal` and `outlined`.
**VCard** (250 uses): `class=` 143 (126 literal + 17 bound), `variant=` 46, `color=` 9, `:max-width=` 8. The `card-border-shadow-{color}` Vuexy class shows up here a lot (KPI tiles).
**VTextField** (27 uses, but most form inputs go through `AppTextField`): `:model-value=` 5, `density=` 4, `type=` 3, `label=` 3, `variant=` 2, `hide-details=` 1, `class=` 1. Heavy reliance on Vuetify defaults (density=comfortable, variant=outlined) so most call-sites pass minimal props.
**VDialog** (86 uses): `max-width=` 78, `:model-value=` 28, `:width=` 8, `:fullscreen=` 2. Width strategy is fixed `max-width="500"` / `"600"` literals + a few responsive `:fullscreen` cases.
**VAvatar** (59 uses): `size=` 49, `variant=` 47 (45 literal + 2 bound), `color=` 52 (35 literal + 17 bound), `class=` 12. Canonical: `tonal` variant + bound `color`.
**VCol** (331 uses): `cols=` 321, `md=` 134, `sm=` 56, `lg=` 11. Responsive grids use `cols sm md` consistently; `lg/xl` are rare.
**VList** (51 uses): `density=` 25, `class=` 17. `density="compact"` from defaults.
**VSnackbar** (39 uses): `color=` 39 (32 literal + 7 bound), `location=` 5. Always coloured per outcome.
**VDataTable / VDataTableServer** — see Section 6 for full detail.
**VAutocomplete** (4 uses, raw): `label=` 2, `:items=` 2, `:error-messages=` 2, `:rules=` 1, `hide-details=` 1, `:loading=` 1. Most autocompletes go through `AppAutocomplete`.
**VTextarea** (11 uses, raw, with 5 going through `AppTextarea`): minimal — `variant=`, `:rules=`, `:label=` each appear once on raw VTextarea.
### 1.4 Slot usage patterns (by component)
Captured via `rg -o '#item\.[a-zA-Z._]+|#expanded-row|#top|#no-data|#header\.[a-zA-Z._]+|#prepend|#append'`. Most slots cluster on data tables and list items. See Section 6 for per-DataTable slot lists. Other notable slot usage:
- `VListItem`: `#prepend` (icons in nav), `#append` (badges, action buttons).
- `VTextField` / `AppTextField`: `#append-inner` (copy-to-clipboard, password reveal toggles).
- `VAlert`: `#append` (retry button — see three-state pattern in `VUEXY_COMPONENTS.md` §7).
- `VCard`: untyped default slot only.
- `VTab`: bound `#default` for label; some pages use icon-only tabs.
---
## 2. Vuexy `@core` and `@layouts` Surface Area
<details>
<summary>Reproduction commands</summary>
```bash
# Distinct @core / @layouts / @images imports
rg --no-filename -o "from\s+['\"]@core/[^'\"]+['\"]" --glob '*.{vue,ts}' apps/app/src \
| sed -E "s/from\s+['\"]([^'\"]+)['\"]/\1/" | sort | uniq -c | sort -rn
# File counts per import path
rg -l "from ['\"]@core/utils/validators['\"]" --glob '*.{vue,ts}' apps/app/src | wc -l
# Enumerate vendored directories
find apps/app/src/@core -type f | sort
find apps/app/src/@layouts -type f | sort
```
</details>
### 2.1 Imports from `@core/*`
| Import path | Files | Description |
|---|---:|---|
| `@core/utils/validators` | 27 | `requiredValidator`, `emailValidator`, etc. |
| `@core/stores/config` | 8 | Theme/skin/layout cookies-backed store |
| `@core/types` | 6 | Shared Vuexy types (`UserThemeConfig`, etc.) |
| `@core/composable/useGenerateImageVariant` | 4 | Theme-aware image picker |
| `@core/utils/colorConverter` | 3 | `hexToRgb`, `rgbaToHex` |
| `@core/utils/plugins` | 2 | `registerPlugins` plugin auto-loader |
| `@core/utils/formatters` | 1 | `avatarText`, `kFormatter`, `formatDate` |
| `@core/initCore` | 1 | App.vue init hook |
| `@core/components/AppBarSearch.vue` | 1 | Used in `NavSearchBar.vue` |
| `@core/components/ScrollToTop.vue` | 1 | Used in `App.vue` |
> Note: `AppTextField`/`AppSelect`/`AppAutocomplete`/`AppTextarea`/`AppCombobox`/`AppDateTimePicker`/`DialogCloseBtn`/`AppStepper`/`Notifications`/`TheCustomizer`/`ThemeSwitcher`/`Shortcuts`/`AppBarSearch` are auto-imported by `unplugin-vue-components` (Vite config registers `src/@core/components` as a scan dir), so they appear as bare tags in templates without `import` statements. Tag counts: `AppTextField` 153, `AppSelect` 45, `DialogCloseBtn` 9, `AppKpiCard` 8, `AppAutocomplete` 8, `AppTextarea` 5, `AppLoadingIndicator` 3, `AppDateTimePicker` 3, `TheCustomizer` 2, `Notifications` 2, `AppStepper` 2, `AppCombobox` 2, `ThemeSwitcher` 1, `Shortcuts` 1, `ScrollToTop` 1, `CustomRadios` 1, `CustomRadiosWithIcon` 1, `AppBarSearch` 1.
### 2.2 Imports from `@layouts/*`
| Import path | Files | Description |
|---|---:|---|
| `@layouts/types` | 20 | `NavLink`, `NavGroup`, etc. |
| `@layouts/stores/config` | 14 | Layout-config Pinia store + `cookieRef` helper |
| `@layouts/components/VNodeRenderer` | 8 | Generic VNode renderer (`.tsx`) |
| `@layouts/utils` | 8 | `switchToVerticalNavOnLtOverlayNavBreakpoint`, etc. |
| `@layouts/components` | 6 | Barrel — `VerticalNavLayout`, etc. |
| `@layouts/enums` | 6 | `AppContentLayoutNav`, `NavbarType`, etc. |
| `@layouts/plugins/casl` | 5 | `can`, `canViewNavMenuGroup`, `canNavigate` |
| `@layouts/symbols` | 3 | DI inject keys |
| `@layouts/config` | 2 | Layout config defaults |
### 2.3 Imports from `@images/*`
44 distinct image asset imports across the SPA. Top duplicated assets: `@images/pages/misc-mask-light.png` (4 files), `@images/pages/misc-mask-dark.png` (4 files), `@images/avatars/avatar-3.png` / `avatar-4.png` / `avatar-5.png` (2 files each). Remaining 39 assets imported by 1 file each. Includes 5 SVG form-control overrides (`checkbox-checked.svg`, `checkbox-unchecked.svg`, `checkbox-indeterminate.svg`, `radio-checked.svg`, `radio-unchecked.svg`) referenced by `apps/app/src/plugins/vuetify/icons.ts`.
### 2.4 `apps/app/src/@core/` enumeration (137 files)
```
components/ (16 files)
AppBarSearch.vue, AppDrawerHeaderSection.vue, AppStepper.vue,
CardStatisticsVerticalSimple.vue, CustomizerSection.vue,
DialogCloseBtn.vue, DropZone.vue, I18n.vue, MoreBtn.vue,
Notifications.vue, ProductDescriptionEditor.vue, ScrollToTop.vue,
Shortcuts.vue, TablePagination.vue, ThemeSwitcher.vue, TiptapEditor.vue
components/app-form-elements/ (12 files)
AppAutocomplete.vue, AppCombobox.vue, AppDateTimePicker.vue,
AppSelect.vue, AppTextField.vue, AppTextarea.vue,
CustomCheckboxes.vue, CustomCheckboxesWithIcon.vue, CustomCheckboxesWithImage.vue,
CustomRadios.vue, CustomRadiosWithIcon.vue, CustomRadiosWithImage.vue
components/cards/ (4 files)
AppCardActions.vue, AppCardCode.vue,
CardStatisticsHorizontal.vue, CardStatisticsVertical.vue
composable/ (5 files)
createUrl.ts, useCookie.ts, useGenerateImageVariant.ts,
useResponsiveSidebar.ts, useSkins.ts
libs/apex-chart/ (1 file) apexCharConfig.ts
libs/chartjs/ (8 files) chartjsConfig.ts + 7 chart wrappers
scss/base/ (16 files) base layout/nav/utility/skin SCSS partials
scss/base/libs/ (1 file) perfect-scrollbar
scss/base/libs/vuetify/ (3 files) Vuetify SASS variable bridge
scss/base/placeholders/ (8 files) %placeholder mixins
scss/base/skins/ (2 files) bordered skin
scss/template/ (8 files) template entry SCSS partials
scss/template/libs/ (3 files) ApexCharts / FullCalendar / Shepherd overrides
scss/template/libs/vuetify/ (3 files) Vuetify variable + index entry
scss/template/libs/vuetify/components/ (24 files) per-Vuetify-component SCSS overrides
scss/template/pages/ (2 files) page-auth.scss, misc.scss
scss/template/placeholders/ (7 files) layout placeholders
scss/template/skins/ (2 files) bordered skin (template layer)
stores/config.ts (1 file)
types.ts, enums.ts, index.ts, initCore.ts (4 files)
utils/ (6 files) validators, formatters, helpers, vuetify, plugins, colorConverter
```
### 2.5 `apps/app/src/@layouts/` enumeration (29 files)
```
components.ts (1 file - barrel export)
components/ (12 files)
HorizontalNav.vue, HorizontalNavGroup.vue, HorizontalNavLayout.vue,
HorizontalNavLink.vue, HorizontalNavPopper.vue, TransitionExpand.vue,
VNodeRenderer.tsx, VerticalNav.vue, VerticalNavGroup.vue,
VerticalNavLayout.vue, VerticalNavLink.vue, VerticalNavSectionTitle.vue
config.ts, enums.ts, index.ts, symbols.ts, types.ts, utils.ts (6 files)
plugins/casl.ts (1 file)
stores/config.ts (1 file)
styles/ (8 SCSS files)
_classes.scss, _default-layout.scss, _global.scss, _mixins.scss,
_placeholders.scss, _rtl.scss, _variables.scss, index.scss
```
### 2.6 Global SCSS / template-style imports
Loaded at app startup:
| File | Import | Source |
|---|---|---|
| `apps/app/src/main.ts:11` | `import '@core/scss/template/index.scss'` | Vuexy SCSS template entrypoint |
| `apps/app/src/main.ts:12` | `import '@styles/styles.scss'` | App-level overrides (currently 1 line: `@import "@/styles/tokens/_timetable.css"`) |
| `apps/app/src/plugins/vuetify/index.ts:13` | `import '@core/scss/template/libs/vuetify/index.scss'` | Vuexy's per-Vuetify-component overrides |
| `apps/app/src/plugins/vuetify/index.ts:14` | `import 'vuetify/styles'` | Vuetify base styles |
| `apps/app/src/styles/settings.scss` | `@forward "../assets/styles/variables/vuetify"` | bridges into `apps/app/src/assets/styles/variables/_vuetify.scss` which `@forward`s `@core/scss/template/libs/vuetify/variables` (vite-plugin-vuetify `styles.configFile`) |
---
## 3. Vuetify Configuration & Theme
<details>
<summary>Reproduction commands</summary>
```bash
ls apps/app/src/plugins/vuetify
cat apps/app/src/plugins/vuetify/{index,defaults,theme,icons}.ts
cat apps/app/src/styles/settings.scss
cat apps/app/src/assets/styles/variables/_vuetify.scss
```
</details>
- **Plugin entry:** `apps/app/src/plugins/vuetify/index.ts` — registers `createVuetify({ aliases: { IconBtn: VBtn }, components: { VVideo }, defaults, icons, theme })`. Auto-imports come from `vite-plugin-vuetify` (configured in `apps/app/vite.config.ts`, `styles.configFile``src/styles/settings.scss`).
- **Sibling files:** `defaults.ts`, `theme.ts`, `icons.ts` (one folder, no sub-trees).
### 3.1 Themes defined
Defined in `apps/app/src/plugins/vuetify/theme.ts`:
- `light`: `dark: false`, full Crewli colour palette (primary `#0D9394`).
- `dark`: `dark: true`, dark variants of the same palette.
- Light & dark share semantic tokens (`primary`, `secondary`, `success`, `info`, `warning`, `error`) plus the Vuexy `grey-50…grey-900` ramp, and Vuexy variables for `border-color`, `overlay-scrim-background`, `tooltip-background`, opacity tokens (`hover`/`focus`/`pressed`/etc.), shadow tokens.
- Primary colour is also overridable per cookie (`lightThemePrimaryColor`, `darkThemePrimaryColor`) via `cookieRef` from `@layouts/stores/config`. Static defaults: `staticPrimaryColor = '#0D9394'`, `staticPrimaryDarkenColor = '#0B7F80'`. (Note: the public `VUEXY_COMPONENTS.md` lists `#7367F0` as primary — this is the upstream Vuexy default; the Crewli theme has been re-skinned to teal.)
### 3.2 Component defaults (`defaults.ts`)
Configured per-component defaults (excerpt; full list in source file):
| Component | Defaults |
|---|---|
| `IconBtn` (alias of VBtn) | `icon: true`, `color: 'default'`, `variant: 'text'` |
| `VAlert` | `density: 'comfortable'`, nested `VBtn { color: undefined }` |
| `VAvatar` | `variant: 'flat'` (with `// Remove after next release` comment) |
| `VBadge`, `VBtn` | `color: 'primary'` |
| `VChip` | `label: true` |
| `VDataTable`, `VDataTableServer` | nested `VPagination` icons (tabler-chevrons-left/right) |
| `VExpansionPanel`, `VExpansionPanelTitle` | `expandIcon`/`collapseIcon: 'tabler-chevron-right'` |
| `VList` | `color: 'primary'`, `density: 'compact'`, nested defaults for `VCheckboxBtn`, `VListItem`, `VListItem.VAvatar { size: 40 }` |
| `VMenu` | `offset: '2px'` |
| `VPagination` | `density: 'comfortable'`, `variant: 'tonal'` |
| `VTabs` | `color: 'primary'`, `density: 'comfortable'`, nested `VSlideGroup { showArrows: true }` |
| `VTooltip` | `location: 'top'` |
| `VCheckbox`, `VRadioGroup`, `VRadio` | `color: 'primary'`, `density: 'comfortable'`, `hideDetails: 'auto'` |
| `VSelect`, `VTextField`, `VAutocomplete`, `VCombobox`, `VFileInput`, `VTextarea` | `variant: 'outlined'`, `color: 'primary'`, `density: 'comfortable'`, `hideDetails: 'auto'`; `VSelect`/`VAutocomplete`/`VCombobox` further set nested `VChip { label: true }`; `VAutocomplete` adds `menuProps.contentClass: 'app-autocomplete__content v-autocomplete__content'` |
| `VRangeSlider`, `VSlider` | `color: 'primary'`, `thumbLabel: true`, `thumbSize: 22`, `trackSize: 6`, `hideDetails: 'auto'` |
| `VRating` | `color: 'warning'` |
| `VProgressLinear` | `height: 6`, `roundedBar: true`, `rounded: true`, `bgColor: 'rgba(var(--v-track-bg))'` |
| `VSnackbar` | nested `VBtn { density: 'comfortable' }` |
| `VSwitch` | `inset: true`, `color: 'primary'`, `hideDetails: 'auto'`, `ripple: false` |
| `VNavigationDrawer` | `touchless: true` |
| `VVideo` | nested `VSlider { thumbLabel: false }` |
### 3.3 SCSS variable overrides
`apps/app/src/styles/settings.scss` is the vite-plugin-vuetify `configFile`. It `@forward`s `apps/app/src/assets/styles/variables/_vuetify.scss`, which itself is currently a passthrough to `@core/scss/template/libs/vuetify/variables` (no project-level Vuetify SASS variables overridden — comments document how to override). `apps/app/src/assets/styles/variables/_template.scss` is similarly a passthrough to `@core/scss/template/variables`. **Net:** zero project overrides on Vuetify SASS variables; all customisation flows through theme colours + component defaults in TypeScript.
`apps/app/src/styles/styles.scss` contains a single `@import "@/styles/tokens/_timetable.css"` (RFC-TIMETABLE v0.2 D21 status palette + geometry custom properties; plain CSS so jsdom/vitest can also load it). `apps/app/src/assets/styles/styles.scss` is empty other than that import comment.
### 3.4 Locale configuration
No explicit `locale`/`fallbackLocale` config is passed to `createVuetify`. Vuetify falls back to its English default. App content is Dutch (Crewli is a Dutch-market product per CLAUDE.md "User Documentation"); UI strings are hard-coded Dutch in templates rather than going through Vuetify's `$vuetify.locale` system. `vue-i18n` is installed (11.1.12) and registered via auto-imports, but there are no locale message files under `apps/app/src/` (no `i18n/`, no `locales/` directory found).
### 3.5 Date adapter
No Vuetify-specific date adapter (no `import { aliases } from 'vuetify/labs/...'`-style adapter, no `date: { adapter }` block in `createVuetify`). Date inputs go through `AppDateTimePicker.vue` (548 lines), which is a **Flatpickr**-based picker (`flatpickr@4.6.13` + `vue-flatpickr-component@11.0.5`) styled to look like VTextField. Vue Flatpickr is therefore the project's de-facto date layer — this is a Vuexy template choice that *will not* survive a switch to PrimeVue out of the box.
### 3.6 Icon set
Configured in `apps/app/src/plugins/vuetify/icons.ts`:
- `defaultSet: 'iconify'` — custom Vuetify icon component that renders Iconify class names.
- Iconify packs in `package.json`: `@iconify-json/tabler` 1.2.23 (primary), `@iconify-json/mdi` 1.2.3, `@iconify-json/fa` 1.2.2.
- 40+ Vuetify icon `aliases` use `tabler-*` names (e.g. `calendar: 'tabler-calendar'`, `expand: 'tabler-chevron-down'`).
- 5 SVG overrides for form controls override MDI checkbox/radio names (`mdi-checkbox-blank-outline``checkbox-unchecked.svg`, etc.).
- Direct usage scan: `tabler-*` icon strings appear **593 times** in `.vue` files; `mdi-*` strings appear **2 times**. Tabler is the de-facto icon set; MDI is residual.
---
## 4. Form Layer Inventory
<details>
<summary>Reproduction commands</summary>
```bash
ls apps/app/src/@core/components/app-form-elements/
find apps/app/src/schemas -type f
rg -l "from ['\"]zod['\"]" --glob '*.{vue,ts}' apps/app/src
rg -l 'vee-validate|useField|useForm.*vee' --glob '*.{vue,ts}' apps/app/src
rg -l "from ['\"]@core/utils/validators['\"]" --glob '*.{vue,ts}' apps/app/src | wc -l
find apps/app/src -name 'useForm*' -type f
```
</details>
### 4.1 Wrapper components around Vuetify form inputs
All in `apps/app/src/@core/components/app-form-elements/`. These are the canonical form inputs (auto-imported, used in templates without explicit imports). Each wraps a Vuetify component with a separated label row.
| File | Lines | Wraps |
|---|---:|---|
| AppAutocomplete.vue | 58 | VAutocomplete |
| AppCombobox.vue | 58 | VCombobox |
| AppDateTimePicker.vue | 548 | VTextField + Flatpickr (largest — full date/time UX) |
| AppSelect.vue | 51 | VSelect |
| AppTextField.vue | 50 | VTextField |
| AppTextarea.vue | 51 | VTextarea |
| CustomCheckboxes.vue | 81 | VCheckbox + custom card UI |
| CustomCheckboxesWithIcon.vue | 95 | (variant) |
| CustomCheckboxesWithImage.vue | 93 | (variant) |
| CustomRadios.vue | 84 | VRadio + custom card UI |
| CustomRadiosWithIcon.vue | 88 | (variant) |
| CustomRadiosWithImage.vue | 100 | (variant) |
There is **only one** wrapper family — `App*` (Vuexy stock) — no Crewli-specific form-field wrapper has been added. Form components use these wrappers + `@core/utils/validators` per-field rule arrays.
### 4.2 VeeValidate integration pattern
**None.** A repo-wide grep for `vee-validate`, `useField`, and `useForm.*vee` returns **zero matches**. CLAUDE.md and `VUEXY_COMPONENTS.md` §4 explicitly state VeeValidate was removed (Session 4 follow-up) and was never adopted.
The actual pattern is:
1. `ref({ ... })` for form state.
2. `VForm` ref + per-field `:rules="[requiredValidator, emailValidator, …]"` from `@core/utils/validators`.
3. Separate `errors: Ref<Record<string, string>>` populated from API 422 responses, fed via `:error-messages="errors.field"`.
4. **Zod** (3.x) for runtime validation of API payloads/responses where the contract matters.
### 4.3 Zod schemas
3 files import `zod`:
| Path | Subject | Lines |
|---|---|---:|
| `apps/app/src/schemas/registrationSchema.ts` | `step1Schema` + `fullRegistrationSchema` for the public registration form (first_name/last_name/email/dob/phone) | 11 |
| `apps/app/src/schemas/timetable.ts` | Timetable performance create/update payloads, sub-event type, decimal-position parsing | 240 |
| `apps/app/src/composables/api/useTimetable.ts` | Inline schema usage for timetable API responses | (consumer) |
There is no top-level `apps/app/src/schemas/` index file. Coverage is limited: 2 of ~30 API composables have a Zod-validated contract.
### 4.4 Custom validation rules registered globally
None. `@core/utils/validators.ts` exports plain functions (`requiredValidator`, `emailValidator`, `passwordValidator`, `confirmedValidator`, `betweenValidator`, `integerValidator`, `regexValidator`, `alphaValidator`, `urlValidator`, `lengthValidator`, `alphaDashValidator`); no rule registry, no `defineRule`/`createRule`. The 27 importing files cherry-pick the validators they need.
### 4.5 Form-related composables
| Path | Purpose |
|---|---|
| `apps/app/src/composables/useFormDraft.ts` (370 lines) | Public-form draft autosave + idempotency-key-driven submit; the largest form composable |
| `apps/app/src/composables/forms/composables/useFormSteps.ts` | Splits a public form schema into wizard steps (submitter / section / heading_group / flat / review) |
| `apps/app/src/composables/forms/composables/formatFieldValue.ts` | Field-value formatter used by the public form renderer |
| `apps/app/src/composables/publicFormInjection.ts` | DI symbols for sharing schema + values down the public-form tree |
| `apps/app/src/composables/api/usePublicForm.ts` | Draft create/save/submit mutations |
| `apps/app/src/composables/api/useFormSchemas.ts`, `useFormFailures.ts`, `useRegistrationFormFields.ts`, `usePublicFormSections.ts`, `usePublicFormTimeSlots.ts` | TanStack Query composables for form-builder data |
`useForm` from VeeValidate or any other library is not present.
---
## 5. Page & Route Inventory
<details>
<summary>Reproduction commands</summary>
```bash
find apps/app/src/pages -name '*.vue' -type f | wc -l
find apps/app/src/pages -maxdepth 1 -mindepth 1 -type d | sort
find apps/app/src/pages/platform -name '*.vue' -type f | sort
find apps/app/src/pages/portal -name '*.vue' -type f | sort
find apps/app/src/pages/register -name '*.vue' -type f | sort
rg -l '<VForm\b|<AppTextField\b|<VTextField\b|<AppSelect\b|<VSelect\b|<AppAutocomplete\b' \
apps/app/src/pages/<tree> | wc -l
```
</details>
Total `.vue` page files under `apps/app/src/pages/`: **46**. Routes are file-based via `unplugin-vue-router` (no separate `router/` directory beyond the plugin shim — `apps/app/src/router/` does not exist; `apps/app/src/plugins/1.router/index.ts` only configures the auto-routes plus guards).
### 5.1 Organizer (root) — pages NOT under `/platform/`, `/portal/`, `/register/`
Total: **30** `.vue` files. (The remainder are auth + index-redirect at `pages/` root.)
| Top-level dir | Pages |
|---|---:|
| `events/` | 12 (event detail tabs: index, persons, sections, time-slots, programmaonderdelen, briefings, artists, crowd-lists, settings/index, settings/registration-fields, timetable; plus events list) |
| `platform/` | 8 (treated separately — see 5.2) |
| `portal/` | 7 (treated separately — see 5.3) |
| `organisation/` | 5 (`companies.vue`, `index.vue`, `settings.vue`, `form-failures/index.vue`, `form-failures/[id].vue`) |
| `register/` | 2 (treated separately — see 5.4) |
| `pages/` (top-level) | 8 (`[...error].vue`, `forbidden.vue`, `forgot-password.vue`, `index.vue`, `login.vue`, `reset-password.vue`, `select-organisation.vue`, `verify-email-change.vue`) |
| `account-settings/` | 1 (`index.vue`) |
| `dashboard/` | 1 (`index.vue`) |
| `invitations/` | 1 (`[token].vue`) |
| `members/` | 1 (`index.vue`) |
Organizer-tree pages (everything except `platform/`, `portal/`, `register/`) = **30**.
### 5.2 Platform admin (`/platform/*`)
8 files:
```
platform/index.vue
platform/activity-log/index.vue
platform/form-failures/index.vue
platform/form-failures/[id].vue
platform/organisations/index.vue
platform/organisations/[id].vue
platform/users/index.vue
platform/users/[id].vue
```
### 5.3 Portal (`/portal/*`)
7 files (token-based routes are **not** under `/p/` despite the design-doc convention — the actual path is `/portal/*`; CLAUDE.md uses `/p/*` shorthand):
```
portal/advance/[token].vue
portal/evenementen/index.vue
portal/evenementen/[eventId].vue
portal/profiel.vue
portal/registreren/index.vue
portal/shifts/index.vue
portal/wachtwoord-instellen.vue
```
### 5.4 Public registration (`/register/*`)
2 files: `register/[public_token].vue`, `register/success.vue`. These are token-based public form-fill flows.
### 5.5 Forms per route tree
Pages containing at least one `<VForm>`, `<AppTextField>`, `<VTextField>`, `<AppSelect>`, `<VSelect>`, or `<AppAutocomplete>`:
| Tree | Pages w/ forms |
|---|---:|
| Organizer root (incl. top-level auth pages) | 7 |
| Platform admin | 4 |
| Portal | 2 |
| Public register | 1 |
| (Total pages with forms) | 14 |
Many additional forms live in `components/` dialogs (`CreateEventDialog`, `EditPersonDialog`, etc.) that are mounted from these pages — `<VForm>` appears in **38** components total (31 of them in `apps/app/src/components/`, the rest in pages and `@core` wrappers).
---
## 6. DataTable Inventory (Critical for Migration)
<details>
<summary>Reproduction commands</summary>
```bash
rg -l '<VDataTable\b|<VDataTableServer\b' --glob '*.vue' apps/app/src | sort
# Per file: server vs client, slots, special features, column count
```
</details>
14 files use `<VDataTable>` or `<VDataTableServer>`. None use `<VDataTableVirtual>`.
| File | Mode | Approx. cols | Slots in use | Special features | Approx. row count |
|---|---|---:|---|---|---|
| `components/form-failures/FormFailuresTable.vue` | server | 7 | `item.actions`, `item.exception_class`, `item.failed_at`, `item.form_submission_id`, `item.listener_class`, `item.retry_count`, `item.state`, `no-data` | | 100s (per organisation) |
| `components/organisation/EmailLogTab.vue` | server | 6 | `expanded-row`, `item.recipient_email`, `item.sent_at`, `item.status`, `item.subject`, `item.template_label`, `item.triggered_by` | `expand` (single-row expand) | 1000s (audit log) |
| `components/organisation/EmailTemplatesTab.vue` | client | 3 | `item.actions`, `item.is_custom`, `item.label` | | < 50 |
| `components/organisation/PersonTagsTab.vue` | client | 5 | `item.actions`, `item.category`, `item.color`, `item.icon`, `item.name` | | 10s100s |
| `components/organisation/RegistrationFieldTemplatesTab.vue` | client | 4 | `item.actions`, `item.field_type`, `item.is_system`, `item.label` | | 10s100s |
| `components/organisations/CrowdTypesManager.vue` | client | 5 | `item.actions`, `item.color`, `item.icon`, `item.name`, `item.system_type` | | < 50 |
| `pages/events/[id]/crowd-lists/index.vue` | client | 7 | `item.actions`, `item.auto_approve`, `item.crowd_type_id`, `item.name`, `item.persons_count`, `item.recipient_company_id`, `item.type` | | 10s100s |
| `pages/events/[id]/persons/index.vue` | client | 6 | `item.actions`, `item.created_at`, `item.crowd_type`, `item.full_name`, `item.status` | | 100s1000s (festival roster) |
| `pages/members/index.vue` | client | 5 | `item.actions`, `item.full_name`, `item.role`, `no-data` | `search=`, `sort-by` | 10s |
| `pages/organisation/companies.vue` | client | 6 | `item.actions`, `item.contact_email`, `item.contact_full_name`, `item.contact_phone`, `item.type` | | 10s100s |
| `pages/platform/activity-log/index.vue` | server | 5 | `expanded-row`, `item.causer`, `item.created_at`, `item.expand`, `item.subject_type`, `no-data` | `expand` | 10000s (cross-tenant audit) |
| `pages/platform/organisations/[id].vue` | client | 5 | `item.actions`, `item.full_name`, `item.role`, `no-data` | `search=`, `sort-by` | 10s |
| `pages/platform/organisations/index.vue` | server | 6 | `item.billing_status`, `item.created_at`, `no-data` | | 10s100s |
| `pages/platform/users/index.vue` | server | 7 | `item.actions`, `item.created_at`, `item.email_verified_at`, `item.full_name`, `item.organisations`, `item.roles`, `no-data` | | 1000s (cross-tenant) |
**Summary:** 5 server-paginated, 9 client-side. Heavy use of `#item.<col>` per-cell slots (every table uses 3+); `expand` (row-expand to detail) used in 2 audit/log tables. No `select` (checkbox selection), no `group-by`, no `custom-filter`. The `<VTable>` component (3 uses, simpler grid) appears in `account-settings/NotificationsTab.vue`, `pages/platform/activity-log/index.vue` (as a sub-element), and `dialogs/AddEditRoleDialog.vue` — these are not data tables proper. No `v-data-iterator` usage.
---
## 7. Pinia Stores & TanStack Query
<details>
<summary>Reproduction commands</summary>
```bash
find apps/app/src/stores -type f -name '*.ts' | sort
find apps/app/src/composables/api -type f -name '*.ts' | grep -v __tests__ | sort
rg -l 'from .vuetify|from .@core|from .@layouts' apps/app/src/stores
rg -l 'from .vuetify|from .@core|from .@layouts' apps/app/src/composables/api
```
</details>
### 7.1 Pinia stores (8 production stores; 2 spec files excluded)
```
apps/app/src/stores/portal/usePortalStore.ts
apps/app/src/stores/useAuthStore.ts
apps/app/src/stores/useImpersonationStore.ts
apps/app/src/stores/useNotificationStore.ts
apps/app/src/stores/useOrganisationStore.ts
apps/app/src/stores/useSectionsUiStore.ts
apps/app/src/stores/useShiftDetailStore.ts
apps/app/src/stores/useTimetableStore.ts
```
### 7.2 TanStack Query composables
29 production files under `apps/app/src/composables/api/` (excluding `__tests__/`). Listed alphabetically:
```
portal/usePortalProfile.ts
portal/usePortalShifts.ts
portal/useVolunteerRegistration.ts
useAccount.ts
useAdmin.ts
useAuth.ts
useCompanies.ts
useCrowdLists.ts
useCrowdTypes.ts
useEmail.ts
useEvents.ts
useFormFailures.ts
useFormSchemas.ts
useIdentityMatches.ts
useMembers.ts
useMfa.ts
useOrganisations.ts
usePersonTags.ts
usePersons.ts
usePublicForm.ts
usePublicFormSections.ts
usePublicFormTimeSlots.ts
useRegistrationFieldTemplates.ts
useRegistrationFormFields.ts
useSections.ts
useShiftAssignments.ts
useShifts.ts
useTimeSlots.ts
useTimetable.ts
useTimetableMutations.ts
```
### 7.3 UI-framework purity
`rg -l 'from .vuetify|from .@core|from .@layouts'` against `apps/app/src/stores/` returns **0 files**. Same query against `apps/app/src/composables/api/` returns **0 files**. Stores and query composables are framework-agnostic — migration risk in this layer is **zero**.
---
## 8. Layout Shell Inventory
<details>
<summary>Reproduction commands</summary>
```bash
find apps/app/src/layouts -type f | sort
rg --no-filename -o "layout: '[a-z-]+'" --glob '*.vue' apps/app/src/pages | sort -u
rg --no-filename -o "navMode: '[a-z-]+'" --glob '*.vue' apps/app/src/pages | sort -u
ls apps/app/src/navigation/vertical apps/app/src/navigation/horizontal
```
</details>
### 8.1 Layout files
```
apps/app/src/layouts/
default.vue — auth shell; switches vertical/horizontal nav via useConfigStore
blank.vue — minimal shell (login, error pages)
OrganizerLayout.vue — thin wrapper around DefaultLayoutWithVerticalNav
PortalLayout.vue — custom navbar (platform/event modes), 1440px max-width
PublicLayout.vue — unauthenticated scaffold (registration flows)
components/
DefaultLayoutWithHorizontalNav.vue
DefaultLayoutWithVerticalNav.vue
Footer.vue
NavBarNotifications.vue
NavSearchBar.vue
NavbarShortcuts.vue
NavbarThemeSwitcher.vue
UserProfile.vue
__tests__/
OrganizerLayout.spec.ts
PortalLayout.spec.ts
PublicLayout.spec.ts
```
### 8.2 Layouts in use
`vite-plugin-vue-meta-layouts` is configured with `target: './src/layouts'`, `defaultLayout: 'default'`. So the implicit layout for any page without `meta.layout` is `default.vue`.
Distinct `meta.layout` overrides found in route metadata:
```
layout: 'blank' — appears (e.g. login, error pages)
```
(Only `'blank'` appears as an explicit override in `definePage({ meta })` blocks. Other layouts — Organizer/Portal/Public — are **not** selected via `meta.layout`; they are name-based files matched by `vite-plugin-vue-meta-layouts` to route folders. The mapping documented in `VUEXY_COMPONENTS.md` table — Organizer for `events/members/...`, Portal for `portal/**`, Public for `register/**` — is the de-facto contract; verify in the open questions.)
Distinct `meta.navMode` values (Portal-only):
```
navMode: 'event'
navMode: 'platform'
```
### 8.3 Navigation source
```
apps/app/src/navigation/vertical/index.ts 73 lines
apps/app/src/navigation/horizontal/index.ts 17 lines
```
The vertical nav is the canonical source for the sidebar menu structure consumed by `VerticalNavLayout` from `@layouts`. The horizontal variant (17 lines) is a stub — present because Vuexy supports horizontal nav, but Crewli has not built it out.
---
## 9. Test Surface
<details>
<summary>Reproduction commands</summary>
```bash
cd apps/app && pnpm test --run # captures the summary line only
find apps/app/src -type f -name '*.spec.ts' | wc -l
find apps/app/src -type f -name '*.test.ts' | wc -l
rg -l "from ['\"]vuetify|from ['\"]@core|from ['\"]@layouts" \
apps/app/src --glob '*.spec.ts' --glob '*.test.ts' | wc -l
rg -l 'mount\(' apps/app/src --glob '*.spec.ts' --glob '*.test.ts' | wc -l
```
</details>
- **Vitest summary (run on this branch):** `Test Files 57 passed (57) — Tests 402 passed (402) — Duration 4.69s`. (The 57 file count includes `.spec.ts`, `.test.ts`, and any other patterns Vitest's default config picks up; the on-disk counts are 26 `.spec.ts` files + 4 `.test.ts` files = 30 in `apps/app/src/`. The remaining ≈27 covered by Vitest live outside `src/` — likely the timetable tests under feature folders or in the `@core` vendored tree.)
- **Tests importing `vuetify` / `@core` / `@layouts`:** **0** files (neither raw nor via barrels). Test isolation is excellent — the migration won't ripple into the test layer through imports.
- **Tests that mount components (use `@vue/test-utils` / `mount(`):** **22** files.
**Implication for migration risk:** the 22 mount-tests will break at the moment Vuetify is removed if they render real components rather than stubbing. Most of those today resolve Vuetify components implicitly through `unplugin-vue-components` — they don't import Vuetify directly, so the import-import surface is 0 but the runtime-render surface is 22.
---
## 10. Bundle Size Baseline
<details>
<summary>Reproduction commands</summary>
```bash
cd apps/app && pnpm build
du -sh apps/app/dist/assets
find apps/app/dist/assets -name '*.js' -not -name '*.map' \
-exec stat -f "%z %N" {} \; | sort -rn | head -10
find apps/app/dist/assets -name 'V*.js' -not -name '*.map' \
-exec stat -f "%z %N" {} \; | awk '{sum+=$1} END {printf "%.1f KB\n", sum/1024}'
```
</details>
Build run on `audit/primevue-migration` branch, `pnpm build` succeeded in 14.52s. Sizes below are uncompressed JS unless marked.
| Metric | Value |
|---|---|
| Total JS (uncompressed) | **~1.90 MB** across all chunks |
| Main entry chunk (gzip) | **213.16 KB** (`index-BkGc0pfd.js`, 597.4 KB raw) |
| Public-form bundle (gzip) | **79.55 KB** (`_public_token_-C7-qZZqM.js`, 233.1 KB raw — registration page) |
| All Vuetify component chunks (`V*.js`, raw) | **~157.5 KB** (29 chunks) |
| Total CSS (uncompressed) | **~3.32 MB** |
| Single largest CSS chunk (uncompressed) | **3.14 MB** (`index-DCIrkDWf.css`) |
### 10.1 Top 10 largest JS chunks (uncompressed)
| KB raw | Chunk | Notes |
|---:|---|---|
| 597.4 | `index-BkGc0pfd.js` | Main entry — Vue + Vuetify core + router + Pinia + TanStack Query + auth + nav |
| 233.1 | `_public_token_-C7-qZZqM.js` | Public registration page (incl. form-renderer + Tiptap editor in some configs) |
| 106.0 | `index-VHmHOd5d.js` | (anonymous shared chunk) |
| 73.7 | `index-DHwLxN4g.js` | (anonymous shared chunk) |
| 70.6 | `settings-BqN6Rluh.js` | Organisation settings page |
| 59.4 | `NavSearchBar.vue_vue_type_style_index_0_lang-CDorXaGZ.js` | Global search (Cmd-K) — pulled in by `default.vue` layout |
| 55.6 | `idempotencyKey-VF0QPxMW.js` | Idempotency-key utilities (incl. crypto-randomUUID polyfill bundle) |
| 41.6 | `MfaDisableDialog.vue_vue_type_script_setup_true_lang-PUuOhlbq.js` | MFA flow — TOTP + OTP input |
| 34.9 | `VDataTable-CWn3CXVV.js` | Vuetify DataTable component chunk (lazy) |
| 34.7 | `index-BIwP0EOg.js` | (anonymous shared chunk) |
### 10.2 Vuetify-specific chunks (lazy-loaded `V*.js`)
29 chunks totaling **157.5 KB uncompressed** when summed. Largest: `VDataTable` (34.9 KB), `VList` (22.3 KB), `VSelect` (13.5 KB), `VTabs` (10.7 KB), `VChip` (10.5 KB), `VNavigationDrawer` (9.3 KB), `VAutocomplete` (8.6 KB). These are the components Vite split out from the main bundle — i.e. they are **on top of** whatever Vuetify code is already inlined in the 597 KB main entry. The actual Vuetify contribution to the bundle is therefore larger than 157 KB; a precise split would need `rollup-plugin-visualizer` (not currently configured), which is out of scope for this audit.
### 10.3 CSS
The single dominant CSS chunk is **3.14 MB uncompressed** — Vuexy's full SCSS template plus all per-component overrides. This is the biggest single migration win available: PrimeVue's Aura preset is CSS-in-JS-ish (style tokens) and ships dramatically less hand-written SCSS.
---
## 11. Open Questions for the RFC
1. **Layout selection mechanism.** Only `meta.layout: 'blank'` shows up as an explicit override in route files. The `OrganizerLayout` / `PortalLayout` / `PublicLayout` selection documented in `VUEXY_COMPONENTS.md` §2 is presumably done via filename matching by `vite-plugin-vue-meta-layouts`, but no `meta.layout: 'organizer' | 'portal' | 'public'` strings appear in any page file. The RFC should pin down which mechanism is canonical (filename match vs. explicit meta) before the layout shell is rewritten.
2. **Direct `vuetify/components/VForm` deep imports.** 27 files do `import { VForm } from 'vuetify/components/VForm'` (the rest of the Vuetify components come via auto-import). The RFC should decide whether the migration converts these to a single canonical FormField wrapper or leaves them as deep PrimeVue equivalents. Same pattern with `vuetify/components/VList` in `AppBarSearch.vue`.
3. **Date layer.** `AppDateTimePicker.vue` (548 lines) is Flatpickr-based, not Vuetify-native — Vuexy never wired up a Vuetify date adapter. The RFC needs to decide whether to keep Flatpickr (decoupled from Vuetify, would survive the migration as-is), swap to PrimeVue's `Calendar` / `DatePicker`, or take a third option. The current SPA has no `@vuetify/labs/date-fns`-style adapter.
4. **Iconify vs PrimeIcons.** Crewli has 593 `tabler-*` references in templates plus an Iconify-based custom Vuetify icon component in `plugins/vuetify/icons.ts`. The RFC should decide whether the migration keeps the Tabler-via-Iconify pipeline (works with any framework) or moves to PrimeIcons. The 5 SVG form-control overrides for checkbox/radio (`@images/svg/checkbox-checked.svg` etc.) only matter if the PrimeVue equivalent has its own theming for these.
5. **`AppKpiCard` (8 uses).** This component appears as a tag but the file was not enumerated in `apps/app/src/@core/components/`. It is presumably a Crewli-custom card (likely under `apps/app/src/components/` somewhere), but the audit grep treated it as a Vuexy `App*` component because of the `App` prefix. The RFC should confirm whether it's Crewli-custom (migration: rewrite as part of the dashboard pattern in F4) or vendored (migration: replace).
6. **Locale + date formatting.** No Vuetify locale config is present, but UI strings are Dutch. The RFC should make explicit whether PrimeVue gets configured with `nl-NL` from day one (PrimeVue has a locale system), or whether the project keeps its current "hard-coded Dutch + Vue I18n loaded but unused" stance.
7. **Test runtime contract.** 22 test files mount real components but 0 import from `vuetify` / `@core` / `@layouts`. The Vitest setup must be implicitly registering Vuetify globally. The RFC should confirm where this registration lives (likely a `setupFiles` config) so it can be swapped to PrimeVue in a single point during F5.
8. **Vuexy SCSS stripping at scale.** ~3.14 MB single-CSS-chunk indicates the Vuexy SCSS template is fully active. The RFC should sequence which SCSS imports get removed first vs. last (`@core/scss/template/index.scss` cannot drop until the layout shell is on PrimeVue, but the per-Vuetify-component SCSS in `@core/scss/template/libs/vuetify/components/` becomes dead the moment a given Vuetify component is removed). A staged removal path is non-trivial and likely belongs in the RFC.