docs(audit): WS-FRONTEND-PRIMEVUE F1 codebase inventory
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
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.
|
||||||
Reference in New Issue
Block a user