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