43 KiB
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 branchaudit/primevue-migration(cut frommainat62afbde). Stack snapshot: Vue 3.5.22 · Vuetify 3.10.8 · Vuexy template v9.5.0 (perapps/app/package.json"version") on disk template ref v10.11.1 inresources/· 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 fromrg/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 inVUEXY_COMPONENTS.md§7).VCard: untyped default slot only.VTab: bound#defaultfor 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/AppBarSearchare auto-imported byunplugin-vue-components(Vite config registerssrc/@core/componentsas a scan dir), so they appear as bare tags in templates withoutimportstatements. Tag counts:AppTextField153,AppSelect45,DialogCloseBtn9,AppKpiCard8,AppAutocomplete8,AppTextarea5,AppLoadingIndicator3,AppDateTimePicker3,TheCustomizer2,Notifications2,AppStepper2,AppCombobox2,ThemeSwitcher1,Shortcuts1,ScrollToTop1,CustomRadios1,CustomRadiosWithIcon1,AppBarSearch1.
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— registerscreateVuetify({ aliases: { IconBtn: VBtn }, components: { VVideo }, defaults, icons, theme }). Auto-imports come fromvite-plugin-vuetify(configured inapps/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 Vuexygrey-50…grey-900ramp, and Vuexy variables forborder-color,overlay-scrim-background,tooltip-background, opacity tokens (hover/focus/pressed/etc.), shadow tokens. - Primary colour is also overridable per cookie (
lightThemePrimaryColor,darkThemePrimaryColor) viacookieReffrom@layouts/stores/config. Static defaults:staticPrimaryColor = '#0D9394',staticPrimaryDarkenColor = '#0B7F80'. (Note: the publicVUEXY_COMPONENTS.mdlists#7367F0as 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/tabler1.2.23 (primary),@iconify-json/mdi1.2.3,@iconify-json/fa1.2.2. - 40+ Vuetify icon
aliasesusetabler-*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.vuefiles;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:
ref({ ... })for form state.VFormref + per-field:rules="[requiredValidator, emailValidator, …]"from@core/utils/validators.- Separate
errors: Ref<Record<string, string>>populated from API 422 responses, fed via:error-messages="errors.field". - 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
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 |
– | 10s–100s |
components/organisation/RegistrationFieldTemplatesTab.vue |
client | 4 | item.actions, item.field_type, item.is_system, item.label |
– | 10s–100s |
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 |
– | 10s–100s |
pages/events/[id]/persons/index.vue |
client | 6 | item.actions, item.created_at, item.crowd_type, item.full_name, item.status |
– | 100s–1000s (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 |
– | 10s–100s |
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 |
– | 10s–100s |
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.tsfiles + 4.test.tsfiles = 30 inapps/app/src/. The remaining ≈27 covered by Vitest live outsidesrc/— likely the timetable tests under feature folders or in the@corevendored 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
-
Layout selection mechanism. Only
meta.layout: 'blank'shows up as an explicit override in route files. TheOrganizerLayout/PortalLayout/PublicLayoutselection documented inVUEXY_COMPONENTS.md§2 is presumably done via filename matching byvite-plugin-vue-meta-layouts, but nometa.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. -
Direct
vuetify/components/VFormdeep imports. 27 files doimport { 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 withvuetify/components/VListinAppBarSearch.vue. -
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'sCalendar/DatePicker, or take a third option. The current SPA has no@vuetify/labs/date-fns-style adapter. -
Iconify vs PrimeIcons. Crewli has 593
tabler-*references in templates plus an Iconify-based custom Vuetify icon component inplugins/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.svgetc.) only matter if the PrimeVue equivalent has its own theming for these. -
AppKpiCard(8 uses). This component appears as a tag but the file was not enumerated inapps/app/src/@core/components/. It is presumably a Crewli-custom card (likely underapps/app/src/components/somewhere), but the audit grep treated it as a VuexyApp*component because of theAppprefix. The RFC should confirm whether it's Crewli-custom (migration: rewrite as part of the dashboard pattern in F4) or vendored (migration: replace). -
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-NLfrom day one (PrimeVue has a locale system), or whether the project keeps its current "hard-coded Dutch + Vue I18n loaded but unused" stance. -
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 asetupFilesconfig) so it can be swapped to PrimeVue in a single point during F5. -
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.scsscannot 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.