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

43 KiB
Raw Permalink Blame History

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.

Reproduction commands
# 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

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

Reproduction commands
# 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

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 @forwards @core/scss/template/libs/vuetify/variables (vite-plugin-vuetify styles.configFile)

3. Vuetify Configuration & Theme

Reproduction commands
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
  • 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.configFilesrc/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 @forwards 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-outlinecheckbox-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

Reproduction commands
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

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.

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

Reproduction commands
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

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)

Reproduction commands
rg -l '<VDataTable\b|<VDataTableServer\b' --glob '*.vue' apps/app/src | sort
# Per file: server vs client, slots, special features, column count

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

Reproduction commands
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

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

Reproduction commands
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

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

Reproduction commands
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
  • 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

Reproduction commands
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}'

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.