5 Commits

Author SHA1 Message Date
99eedb6004 chore(sync): add PRIMEVUE_COMPONENTS.md to .claude-sync.conf
Closes B5 of F2 (RFC-WS-FRONTEND-PRIMEVUE). PRIMEVUE_COMPONENTS.md
joins the synced doc set so Claude Project Knowledge picks it up on
next upload of .claude-sync/.

Sync output:
- .claude-sync.conf: 34 → 35 entries (+1: PRIMEVUE_COMPONENTS.md)
- .claude-sync/*.md: 34 → 35 files (sync script output;
  SYNC_MANIFEST.md auto-regenerated, not counted as net-new)

VUEXY_COMPONENTS.md kept in conf (deprecation stub still useful as a
forwarding marker during F4); removed in F6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:51:54 +02:00
1701e32fdf docs(cursor): update .cursorrules for PrimeVue migration phase
Mirrors CLAUDE.md changes in B3:

- Stack line notes PrimeVue + Tailwind v4 as target, Vuetify as legacy
- New "UI framework strategy (migration-aware)" section forwards to
  PRIMEVUE_COMPONENTS.md with surface-level guidance
- Vuexy reference-path section retained but scoped to legacy surfaces
- Vue 3 section split: Tailwind + pt on migrated, Vuetify-first on
  legacy
- Top blockquote signals this file evolves as F4 progresses

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:40 +02:00
b5765221bb docs(claude): point UI-framework conventions to PRIMEVUE_COMPONENTS.md; document migration-phase guidance
CLAUDE.md updated for the Vuetify→PrimeVue migration phase per
RFC-WS-FRONTEND-PRIMEVUE F2:

- Stack line: notes PrimeVue + Tailwind v4 as target, Vuetify still
  present on un-migrated surfaces
- Replaced "Vuexy reference source" + "Vuexy-first strategy" sections
  with a single "UI framework strategy (migration-aware)" section that
  splits guidance into migrated / un-migrated / new surfaces and
  forwards to PRIMEVUE_COMPONENTS.md
- Forms section now documents both target (@primevue/forms + Zod
  resolver via FormField) and legacy (ref + VForm + :rules) patterns,
  with the surface-level-consistency rule
- UI section reframed: PrimeVue + Tailwind on migrated surfaces,
  Vuetify utilities on legacy surfaces, three-state pattern preserved
  on both
- Order of work: framework note added for new pages during F4

Framework-agnostic sections (database, multi-tenancy, ULID,
controllers, models, security, testing) untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:50:10 +02:00
4f07a673a1 docs(vuetify): replace VUEXY_COMPONENTS.md with deprecation stub (F6 deletion target)
Vuexy/Vuetify component reference is superseded by PRIMEVUE_COMPONENTS.md
per RFC-WS-FRONTEND-PRIMEVUE. Stub forwards readers to the new doc and
provides the explicit pre-F2 SHA (1c449ff620)
for retrieving the original 777-line content during F4a–F4c on
un-migrated surfaces.

File deleted entirely in F6 cleanup. Stub-not-delete decision per
2026-05-10 project chat (Bert): explicit forwarding marker beats
git-history archaeology while parallel-mode is in force.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:48:08 +02:00
9e137cffb9 docs(primevue): add PRIMEVUE_COMPONENTS.md — component mapping, forms pattern, Aura theming, Tailwind integration
Foundation document for F2 of RFC-WS-FRONTEND-PRIMEVUE. Encodes
Crewli-specific conventions for the Vuetify→PrimeVue migration:

- Component mapping by category (form / layout / data display /
  feedback / navigation / overlays), each with a paragraph on
  migration spirit; cross-references PrimeVue docs rather than
  duplicating reference material
- Aura theme + Crewli teal primary token plan (full token list in
  RFC Appendix B; F3 implements)
- Canonical forms pattern: @primevue/forms + Zod resolver +
  <FormField> wrapper (full API spec lives in RFC Appendix A —
  cross-referenced, not duplicated)
- DataTable conventions: lazy / virtual / column-template, with a
  slot translation cheat sheet from VDataTable
- pt API + Tailwind v4 + Aura tokens decision matrix
- Migration phase guidance (surface-level consistency rule, no
  back-porting, F6 cliff)
- VIcon stays Iconify-Tabler per RFC AD-5; PrimeIcons not installed

Length: 385 lines. F4 sub-packages will extend §3 as surfaces migrate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:47:43 +02:00
5 changed files with 487 additions and 823 deletions

View File

@@ -23,6 +23,7 @@ dev-docs/RFC-WS-6.md
dev-docs/RFC-TIMETABLE-Artist-Timetable-Module.md
dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md
dev-docs/MIGRATION-AUDIT-PRIMEVUE.md
dev-docs/PRIMEVUE_COMPONENTS.md
dev-docs/CLAUDE_CODE_TOOLING.md
dev-docs/FRONTEND-TOOLING.md
dev-docs/LARASTAN.md

View File

@@ -1,7 +1,11 @@
# Crewli Cursor Rules
> Migration phase: this file is updated in F2 of RFC-WS-FRONTEND-PRIMEVUE
> and continues to evolve as F4 progresses. Authoritative UI-framework
> reference is `dev-docs/PRIMEVUE_COMPONENTS.md`.
## Stack
PHP 8.2 + Laravel 12 | TypeScript + Vue 3 + Vuexy/Vuetify | Pinia + TanStack Query
PHP 8.2 + Laravel 12 | TypeScript + Vue 3 + PrimeVue + Tailwind v4 (target) / Vuetify (legacy, un-migrated surfaces) | Pinia + TanStack Query
## Laravel
- Resource controllers, form requests, API resources — always
@@ -9,15 +13,22 @@ PHP 8.2 + Laravel 12 | TypeScript + Vue 3 + Vuexy/Vuetify | Pinia + TanStack Que
- Global scope `OrganisationScope` on event-related models
- Policies for authorization — never inline role checks
## Vuexy reference path (mandatory)
- When looking at Vuexy examples, demos, or patterns, ALWAYS reference:
## UI framework strategy (migration-aware)
- Always read `dev-docs/PRIMEVUE_COMPONENTS.md` first — it's the authoritative reference for component selection, theming, forms, and DataTable conventions
- On migrated / new surfaces: PrimeVue + Tailwind v4; forms via `@primevue/forms` + Zod resolver through `<FormField>` (RFC-WS-FRONTEND-PRIMEVUE Appendix A)
- On un-migrated surfaces (during F4): match surrounding Vuetify code; pre-F2 Vuexy reference recoverable via `git show 1c449ff6204cae6371da08c34ea8934d6b2ffcb8:dev-docs/VUEXY_COMPONENTS.md`
- Never mix PrimeVue and Vuetify inside one form or one surface ("no back-porting")
## Vuexy reference path (legacy surfaces only)
- When extending an un-migrated surface, the Vuexy template reference is at:
`resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/`
- Never reference `javascript-version/` or `starter-kit/` variants
## Vue 3
- `<script setup lang="ts">` always
- TanStack Query for API state, Pinia for UI state
- Vuetify components first; custom CSS only as a last resort
- On migrated surfaces: Tailwind utilities for layout, PrimeVue components, `pt` API for component-internal styling
- On legacy surfaces: Vuetify components first; custom CSS only as a last resort
## Naming
- snake_case DB | camelCase JS | PascalCase Vue | `use*` composables | `use*Store` Pinia

113
CLAUDE.md
View File

@@ -11,7 +11,7 @@ Design document: `/dev-docs/design-document.md`
## Tech stack
- Backend: PHP 8.2+, Laravel 12, Sanctum, Spatie Permission, MySQL 8, Redis
- Frontend: TypeScript, Vue 3 (Composition API), Vuexy/Vuetify, Pinia, TanStack Query
- Frontend: TypeScript, Vue 3 (Composition API), PrimeVue + Tailwind v4 (target state, migration in progress per [RFC-WS-FRONTEND-PRIMEVUE](./dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md)) — Vuetify/Vuexy still present on un-migrated surfaces during F4; see [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md). Pinia, TanStack Query.
- Testing: PHPUnit (backend), Vitest (frontend)
## Quality gates
@@ -166,54 +166,47 @@ right tier per the decision tree there before adding new tests.
## Frontend rules (strict)
### Vuexy reference source (mandatory)
### UI framework strategy (migration-aware)
When referencing Vuexy demo pages, components, or patterns, ALWAYS use the TypeScript Vue version at:
The SPA is migrating Vuetify/Vuexy → PrimeVue + Tailwind v4 per
[RFC-WS-FRONTEND-PRIMEVUE](./dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md).
During F4 (sub-packages F4aF4d), both frameworks ship in the same build
on different surfaces. The component-selection rules depend on which side
of the migration the surface is on.
```
resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/
```
**Always read [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md)
before any frontend task** — it is the authoritative reference for
component selection, theming, forms, and DataTable conventions across
both phases.
This is the **ONLY** valid reference path. Never use:
- `javascript-version/` — wrong language
- `starter-kit/` — incomplete, missing components
- Any other variant or version
#### On migrated surfaces (target state)
Before implementing any Vuexy-based page or component, read the reference implementation from this path first:
```bash
# Example: find auth page references
find resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/src/pages -name "*.vue" | grep -i "login\|auth"
```
PrimeVue is the framework. Follow [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md):
### Vuexy-first strategy
1. **Can a Tailwind utility do this?** (layout, spacing, typography) → use it.
2. **Does PrimeVue provide a component?** → use it (see §3 component mapping).
3. **Forms**`@primevue/forms` + Zod resolver via `<FormField>` (§5; full API in [RFC Appendix A](./dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md#appendix-a--formfield-api-specification)).
4. **DataTables**`<DataTable>` with `:lazy="true"` for server-side (§6).
5. **None of the above?** → cross-reference https://primevue.org/ for the closest match. Add a note in `PRIMEVUE_COMPONENTS.md` §3 if it's a recurring need.
Before writing ANY frontend component, consult `/dev-docs/VUEXY_COMPONENTS.md` and follow this decision tree:
Customization order: Tailwind utilities (layout) → `pt` API (component-internal) → Aura preset extension (brand-wide) → `<style scoped>` (last resort, with comment).
1. **Can a standard Vuetify component do this?** → Use it with default props.
Do not wrap it in a custom component.
2. **Does Vuexy provide an @core component for this?** → Use it. Check
`/dev-docs/VUEXY_COMPONENTS.md` section 1 for the full registry.
3. **Does an existing Crewli page already solve a similar UI pattern?**
Copy that pattern exactly. Check `/dev-docs/VUEXY_COMPONENTS.md` section 3
for established patterns and their reference implementations.
4. **None of the above?** → Only then write custom code. Add `<style scoped>`
with a comment explaining why Vuexy/Vuetify couldn't handle it.
#### On un-migrated surfaces (legacy, transient)
Concrete component rules:
- Tables: `v-data-table-server` with server-side pagination — never client-side for API data
- Cards: `v-card` directly, or `AppCardActions` when collapse/refresh/remove is needed
- Forms in dialogs: `v-dialog` + `v-card` + `v-form` — follow the established dialog pattern
- Detail panels: `v-navigation-drawer` with `temporary` and `location="end"` — follow ShiftDetailPanel pattern
- Date/time pickers: `AppDateTimePicker` from @core — never raw input[type=date]
- Status indicators: `v-chip` with color prop — never custom styled spans
- Loading states: `v-skeleton-loader` — never custom spinners
- Error states: `v-alert` with retry button — never custom error divs
- Empty states: `v-card` with icon + message + action button
- Notifications: `v-snackbar` — never custom toast components
- Page layout: `v-row` + `v-col` with Vuetify breakpoint props — never CSS grid or custom flexbox
Vuetify + Vuexy `@core/` components remain in use until the surface's F4
sub-package lands. When extending these surfaces during the transition:
**Before ANY frontend task:** read `/dev-docs/VUEXY_COMPONENTS.md` to verify
you are using available components rather than building custom ones.
- Match the surrounding code (`<VBtn>`, `<VTextField>`, `<v-data-table-server>`, etc.)
- Reference the pre-F2 Vuexy registry via git: `git show 1c449ff6204cae6371da08c34ea8934d6b2ffcb8:dev-docs/VUEXY_COMPONENTS.md`
- Vuexy template reference (when needed): `resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/` — TypeScript Vue version is the only valid path
Do **not** introduce PrimeVue components inside an un-migrated surface
("no back-porting" — see `PRIMEVUE_COMPONENTS.md` §9).
#### On new surfaces (created during or after F4)
Start in PrimeVue. The migration phase is not a license to add new
Vuetify code.
### Vue components
@@ -240,7 +233,16 @@ you are using available components rather than building custom ones.
### Forms
Canonical form pattern (used everywhere in the SPA):
The canonical form pattern depends on the migration phase of the surface:
**Target state (migrated surfaces, new surfaces):** `@primevue/forms` +
Zod resolver via the `<FormField>` wrapper. Full API specification in
[RFC-WS-FRONTEND-PRIMEVUE Appendix A](./dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md#appendix-a--formfield-api-specification);
Crewli conventions in [`PRIMEVUE_COMPONENTS.md` §5](./dev-docs/PRIMEVUE_COMPONENTS.md).
One Zod schema per form, field names mirror backend Form Request keys
(snake_case), 422 errors merge via `useFormError(formRef)`.
**Legacy state (un-migrated surfaces, transient until each F4 sub-package):**
- `ref({ field: ... })` for form state
- `VForm` ref + per-field rules drawn from `@core/utils/validators`
@@ -248,13 +250,16 @@ Canonical form pattern (used everywhere in the SPA):
- A separate `errors: Ref<Record<string, string>>` for server-validation
feedback (mapped from 422 responses)
- **Zod** for runtime validation of API payloads/responses (in
`apps/app/src/schemas/*.ts`) — Zod schemas mirror backend Form Requests
(field names, required/optional, types) and are the canonical contract
`apps/app/src/schemas/*.ts`) — schemas already mirror backend Form
Requests and carry forward unchanged into the target state
- No inline validation logic in components
VeeValidate is **NOT** the form library here. It was previously listed
but never actually adopted in any page; it was removed in commit
`<sha>` (Session 4 follow-up). Reference forms: `apps/app/src/components/sections/CreateShiftDialog.vue`,
A single form is either fully Zod-resolver-validated (target) or fully
`:rules`-validated (legacy) — never a hybrid. VeeValidate is **NOT** in
the stack on either side of the migration.
Reference forms (legacy pattern, will migrate during F4):
`apps/app/src/components/sections/CreateShiftDialog.vue`,
`apps/app/src/components/timetable/AddPerformanceDialog.vue`,
`apps/app/src/pages/register/[public_token].vue`.
@@ -268,12 +273,12 @@ but never actually adopted in any page; it was removed in commit
### UI
- Always use Vuexy/Vuetify for layout, forms, tables, dialogs
- Do not write custom CSS when a Vuetify utility class exists
- Component framework selection: see "UI framework strategy" above and [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md). PrimeVue + Tailwind v4 on migrated/new surfaces; Vuetify on un-migrated surfaces during F4
- Do not write custom CSS when a framework utility (Tailwind on migrated surfaces, Vuetify utilities on legacy surfaces) exists
- Responsive: mobile-first, usable from 375px width
- **Three states per page:** every data-driven view must handle loading (skeleton/spinner), error (`v-alert` with retry button), and empty (helpful message with action button)
- Use Vuetify responsive props (`cols`, `sm`, `md`, `lg`) — no fixed pixel widths
- Custom CSS via `<style scoped>` only as last resort when no Vuetify utility exists
- **Three states per page:** every data-driven view must handle loading (skeleton), error (`Message` / `v-alert` with retry button), and empty (helpful message with action button) — both frameworks support this pattern
- Responsive layout: Tailwind grid (`grid grid-cols-12 gap-4` + `col-span-N md:col-span-M`) on migrated surfaces; Vuetify `v-row` + `v-col` with breakpoint props on legacy surfaces — no fixed pixel widths
- Custom CSS via `<style scoped>` only as last resort when no framework utility / `pt` API / Aura token can do the job
## Forbidden patterns
@@ -321,6 +326,12 @@ allowed only with a `TODO TECH-*` reference to a backlog item.
13. Vue page component in `src/pages/[module]/`
14. Add route in Vue Router
> **Framework note for steps 1314 during F4 migration:** new pages
> follow the PrimeVue + Tailwind conventions in [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md).
> If the new module is grafted onto a not-yet-migrated surface (rare),
> match the surrounding Vuetify style and let the surface's F4
> sub-package migrate it later.
## Diagnostic discipline: audit before assume
When debugging or fixing any bug, the first action is to verify the

View File

@@ -0,0 +1,385 @@
# PRIMEVUE_COMPONENTS — PrimeVue Component Conventions for Crewli
> Authoritative reference for PrimeVue component selection, theming, forms,
> and DataTable conventions in the Crewli SPA. Read this before adding or
> migrating a UI component during F4 of [RFC-WS-FRONTEND-PRIMEVUE](./RFC-WS-FRONTEND-PRIMEVUE.md).
> This document encodes Crewli-specific conventions; for component
> reference (props, slots, events) cross-reference https://primevue.org/.
**Status:** Foundation (F2). Refined incrementally by F4 sub-packages as
real migration experience surfaces gaps.
**Aligned to:** PrimeVue 4.5.x with the Aura preset and `@primevue/forms`.
PrimeVue is installed in F3; F2 documents intent only.
**Replaces:** [`VUEXY_COMPONENTS.md`](./VUEXY_COMPONENTS.md) (now a
deprecation stub; deletion in F6).
---
## 1. Purpose and scope
This document defines:
1. Which PrimeVue component (or Tailwind / native equivalent) replaces each Vuetify component used in the SPA today
2. The Aura theme tokens that carry Crewli's brand (teal primary, dark mode)
3. The canonical form pattern (`@primevue/forms` + Zod resolver + `<FormField>` wrapper)
4. The canonical DataTable pattern (lazy / virtual / column-template conventions)
5. When to use the `pt` (pass-through) API, Tailwind utilities, or Aura tokens for customization
6. How to navigate the migration phase where Vuetify and PrimeVue both ship in the SPA simultaneously (F4aF4d)
Out of scope here:
- Dependency installation steps (see [F3](./RFC-WS-FRONTEND-PRIMEVUE.md#f3--foundation-2-days))
- Per-component props/slot reference (see https://primevue.org/)
- The full 75-component Vuetify usage inventory (see [MIGRATION-AUDIT-PRIMEVUE.md §1](./MIGRATION-AUDIT-PRIMEVUE.md))
---
## 2. When to read this document
| Goal | Read |
|---|---|
| Picking a PrimeVue equivalent for a Vuetify usage during F4 | §3 |
| Customizing Crewli's brand colors / dark mode | §4 |
| Writing or migrating a form | §5 |
| Migrating a `<VDataTable>` or `<VDataTableServer>` | §6 |
| Deciding between `pt`, Tailwind utility, or Aura token override | §7, §8 |
| Working on a not-yet-migrated surface during F4 | §9 |
| Understanding the test-runtime story during migration | [ARCH-TESTING.md §7](./ARCH-TESTING.md) |
Cross-document relations:
- [RFC-WS-FRONTEND-PRIMEVUE.md](./RFC-WS-FRONTEND-PRIMEVUE.md) — full migration plan, sprint breakdown, architectural decisions (AD-1 through AD-12), risk register
- [MIGRATION-AUDIT-PRIMEVUE.md](./MIGRATION-AUDIT-PRIMEVUE.md) — Vuetify usage inventory (§1), `@core` / `@layouts` surface area (§2), form-layer inventory (§4), DataTable inventory (§6)
- [VUEXY_COMPONENTS.md](./VUEXY_COMPONENTS.md) — deprecation stub; its pre-F2 content is reachable via git history (commit `1c449ff6204cae6371da08c34ea8934d6b2ffcb8`) for reference on un-migrated surfaces
- [ARCH-TESTING.md](./ARCH-TESTING.md) §7 — explains why Vuetify lives in test infrastructure during the migration
---
## 3. Component mapping by category
The Crewli SPA uses ~73 distinct Vuetify components today (52 with ≥3 uses,
20 long-tail with <3 uses). The tables below group them into six functional
categories. Each category is followed by one paragraph on the migration
spirit — direct prop translation is rarely possible, since PrimeVue's
customization model is `pt` + Tailwind, not Vuetify's deep-customization
slots.
For component-level prop and slot reference, follow the linked PrimeVue
docs URL — this document does not duplicate them.
### 3.1 Form inputs
| Vuetify | PrimeVue | Reference | Notes |
|---|---|---|---|
| `VTextField` | `InputText` | https://primevue.org/inputtext/ | `density='compact'``size='small'`; wrapped via `<FormField>` for label + error |
| `VTextarea` | `Textarea` | https://primevue.org/textarea/ | Auto-resize via `autoResize` prop |
| `VSelect` | `Select` | https://primevue.org/select/ | Renamed from `Dropdown` in v4; `:items``:options`, `item-title``optionLabel`, `item-value``optionValue` |
| `VAutocomplete` | `AutoComplete` | https://primevue.org/autocomplete/ | Server-side: bind `@complete` instead of `:items` |
| `VCombobox` | `AutoComplete` with `:multiple` | https://primevue.org/autocomplete/ | Free-text + dropdown; PrimeVue uses one component for both |
| `VCheckbox` | `Checkbox` | https://primevue.org/checkbox/ | `binary` prop for single-value checkboxes |
| `VRadio` / `VRadioGroup` | `RadioButton` | https://primevue.org/radiobutton/ | No group component; bind `v-model` directly to each `RadioButton` |
| `VSwitch` | `ToggleSwitch` | https://primevue.org/toggleswitch/ | Renamed from `InputSwitch` in v4 |
| `VOtpInput` | `InputOtp` | https://primevue.org/inputotp/ | |
| `VFileInput` (rare) | `FileUpload` | https://primevue.org/fileupload/ | `mode='basic'` for inline; default for full uploader |
| `VColorPicker` (2 uses) | `ColorPicker` | https://primevue.org/colorpicker/ | |
| `AppDateTimePicker` (Flatpickr) | unchanged | — | Flatpickr is retained per [RFC AD-4](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-4-date-layer--flatpickr-retained-in-fase-1); a thin `DateTimePicker.vue` wrapper composes Flatpickr inside `<FormField>` |
**Migration spirit.** Form inputs are the highest-coverage area; nearly
every input flows through `<FormField>` (§5). Most prop renames are
trivial (`density``size`, `:items``:options`). The bigger shift is
the validation contract: Vuetify's `:rules` array per field is replaced
by a single Zod schema at the form level, with field-level errors
surfaced through `<FormField>`. See §5 for the canonical pattern.
### 3.2 Layout
| Vuetify | Replacement | Notes |
|---|---|---|
| `VContainer` | `<div class="max-w-screen-xl mx-auto px-4">` | Tailwind utility composition; no PrimeVue equivalent |
| `VRow` | `<div class="grid grid-cols-12 gap-4">` | Tailwind grid |
| `VCol cols=N md=M` | `<div class="col-span-N md:col-span-M">` | Tailwind responsive prefixes |
| `VCard` | `Card` | https://primevue.org/card/ — slots: `#title`, `#subtitle`, `#content`, `#footer` |
| `VCardTitle` | Card `#title` slot | Compose inside `Card` |
| `VCardSubtitle` | Card `#subtitle` slot | |
| `VCardText` | Card `#content` slot or `<div class="p-6">` | Most uses are pure padding |
| `VCardActions` | Card `#footer` slot | |
| `VCardItem` | manual flex layout inside `Card #content` | No direct equivalent |
| `VDivider` | `Divider` | https://primevue.org/divider/ — `:vertical``layout='vertical'` |
| `VSheet` | `<div>` with Tailwind | No equivalent component |
| `VSpacer` | `<div class="flex-1">` or surrounding `justify-between` | |
| `VApp`, `VMain`, `VAppBar`, `VFooter` | layout shell rewrites | See [RFC AD-3](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-3-layout-shell--filename-match-preserved-contents-rewritten); filenames preserved, contents replaced |
| `VLocaleProvider` | PrimeVue `<ConfigProvider>` (or app-level `app.use(PrimeVue, { locale })`) | See [RFC AD-6](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-6-locale--nl-nl-via-primelocale-vue-i18n-unused) |
**Migration spirit.** Most layout in PrimeVue is Tailwind, not components.
`VRow`/`VCol` translates 1:1 to a 12-column Tailwind grid. `VCard` is the
biggest semantic preservation — its slot model maps to PrimeVue's `Card`
slot model with minor renames. Plain `<div>` with Tailwind utilities
replaces `VSheet`, `VSpacer`, and most uses of `VContainer`.
### 3.3 Data display
| Vuetify | PrimeVue / Replacement | Reference | Notes |
|---|---|---|---|
| `VDataTable` | `DataTable` + `Column` | https://primevue.org/datatable/ | See §6 |
| `VDataTableServer` | `DataTable` with `:lazy="true"` | https://primevue.org/datatable/#lazy_load | See §6 |
| `VTable` | `DataTable` (no lazy) | — | For static / pre-loaded tables |
| `VList` | `Listbox` (selectable) or `DataView` (cards) | https://primevue.org/listbox/ | Context-dependent; selection lists use `Listbox`, card grids use `DataView` |
| `VListItem` / `VListItemTitle` / `VListItemSubtitle` | `MenuItem` (in menu) or `<li>` (in list) | — | Inside a list, just compose the row markup directly |
| `VListSubheader` | `<li class="text-sm text-surface-500">` | — | Plain markup |
| `VChip` | `Tag` (preferred) or `Chip` | https://primevue.org/tag/ | `Tag` for status badges; `Chip` for removable filters |
| `VAvatar` | `Avatar` | https://primevue.org/avatar/ | `variant='tonal'``:style="{ background, color }"` (no built-in tonal) |
| `VImg` | `<img>` (native) | — | No wrapper needed; use `loading="lazy"` |
| `VIcon` | `<i class="i-tabler-..." />` | — | **Iconify-Tabler retained** per [RFC AD-5](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-5-icons--iconify-tabler-retained-primeicons-not-installed); PrimeIcons is **not installed** |
| `VLabel` | `<label>` (native) or `<FormField label="...">` | — | |
| `VTimeline` / `VTimelineItem` | `Timeline` | https://primevue.org/timeline/ | |
| `VBadge` (1 use) | `Badge` or `OverlayBadge` | https://primevue.org/badge/ | |
**Migration spirit.** `VIcon` is the most widespread component (267 uses)
and it does **not** become a PrimeVue component. The Iconify-Tabler
class-based pattern (`<i class="i-tabler-arrow-right" />`) is
stack-agnostic and survives the migration intact. PrimeIcons is not
installed; do not introduce `pi pi-*` icon classes during F4 — they will
not render. Lists are the most context-dependent area: a selection list
uses `Listbox`, a card grid uses `DataView`, a navigation list uses
`MenuItem`/`PanelMenu`, and a generic display list is just `<ul><li>`.
### 3.4 Feedback
| Vuetify | PrimeVue | Reference | Notes |
|---|---|---|---|
| `VAlert` | `Message` | https://primevue.org/message/ | `type``severity`; inline alerts only |
| `VSnackbar` | `useToast()` + `<Toast>` | https://primevue.org/toast/ | One `<Toast>` mounted in `App.vue`; `useNotificationStore` wraps `useToast()` so call-site API stays stable per [RFC AD-11](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-11-toast-and-confirmdialog-services) |
| `VProgressLinear` | `ProgressBar` | https://primevue.org/progressbar/ | `:indeterminate``mode='indeterminate'` |
| `VProgressCircular` | `ProgressSpinner` | https://primevue.org/progressspinner/ | |
| `VSkeletonLoader` | `Skeleton` | https://primevue.org/skeleton/ | `type='card'` has no preset; compose with explicit `width`, `height`, `shape` |
**Migration spirit.** `Message` is for inline alerts (rendered in the
page). Toasts are global, mounted once in `App.vue`, and triggered via
`useToast()` (or the existing `useNotificationStore` which now wraps it).
`Skeleton` is more primitive than `VSkeletonLoader` — Vuexy's preset
shapes (`type='card'`, `type='article'`) need to be reconstructed from
explicit `Skeleton` elements; do this once as a project-owned
`<SkeletonCard>` utility if a shape is reused.
### 3.5 Navigation
| Vuetify | PrimeVue | Reference | Notes |
|---|---|---|---|
| `VTabs` / `VTab` / `VWindow` / `VWindowItem` | `Tabs` + `TabList` + `Tab` + `TabPanels` + `TabPanel` | https://primevue.org/tabs/ | Single component family; controlled via `:value` |
| `VBtnToggle` | `SelectButton` | https://primevue.org/selectbutton/ | |
| `VPagination` (1 use) | `Paginator` | https://primevue.org/paginator/ | DataTable has built-in paginator; standalone use is rare |
| `VBreadcrumbs` (none today) | `Breadcrumb` | https://primevue.org/breadcrumb/ | Reserved for future use |
| `VNavigationDrawer` | layout-shell custom + `PanelMenu` (or `Drawer` for mobile) | https://primevue.org/panelmenu/, https://primevue.org/drawer/ | Sidebar nav is part of the shell rewrite per [RFC AD-3](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-3-layout-shell--filename-match-preserved-contents-rewritten); `PanelMenu` provides accordion behavior equivalent to `VVerticalNavGroup` |
| Vuexy `AppStepper` | `Stepper` + `StepList` + `Step` + `StepPanels` + `StepPanel` | https://primevue.org/stepper/ | Multi-step wizards (e.g., public registration) |
| Vuexy `VerticalNavLayout` | layout shell rewrite | — | See [RFC AD-3](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-3-layout-shell--filename-match-preserved-contents-rewritten) |
**Migration spirit.** PrimeVue's `Tabs` family expands one Vuetify component
into five elements (`Tabs` / `TabList` / `Tab` / `TabPanels` / `TabPanel`).
This is more verbose at call sites but more flexible (e.g., tab list can
live separately from panels). The sidebar / nav shell is a rewrite, not
a component swap — Vuexy's `@layouts/` system is replaced wholesale per
RFC AD-3, but route-level nav semantics (Pinia stores, route meta) stay
the same.
### 3.6 Overlays
| Vuetify | PrimeVue | Reference | Notes |
|---|---|---|---|
| `VDialog` | `Dialog` | https://primevue.org/dialog/ | `:max-width='500'``:style="{ width: '500px' }"`; `:fullscreen``:maximizable` |
| `VMenu` | `Menu` (action lists) or `Popover` (rich content) | https://primevue.org/menu/, https://primevue.org/popover/ | Choose `Menu` for `MenuItem` lists, `Popover` for arbitrary slot content |
| `VTooltip` | `v-tooltip` directive | https://primevue.org/tooltip/ | Directive form (`v-tooltip="'Hint'"`) is preferred over a wrapping component |
| `VOverlay` | `Dialog` with `modal` prop, or custom `<div>` | — | Most overlay uses are modal dialogs |
| `VBottomSheet` (none today) | `Drawer` with `position='bottom'` | https://primevue.org/drawer/ | Reserved; mobile bottom-sheets if needed |
| `VExpansionPanel(s)` | `Accordion` + `AccordionPanel` + `AccordionHeader` + `AccordionContent` | https://primevue.org/accordion/ | Renamed in v4 from `AccordionTab` |
| `VExpandTransition` / `VScaleTransition` | Vue `<Transition>` (native) | — | Native transitions; Vuetify-specific transitions removed |
| Native `confirm()` / Vuexy `ConfirmDialog` | `useConfirm()` + `<ConfirmDialog>` | https://primevue.org/confirmdialog/ | Mounted once in `App.vue` per [RFC AD-11](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-11-toast-and-confirmdialog-services) |
**Migration spirit.** Overlays are services in PrimeVue: `useToast()`,
`useConfirm()`, `useDialog()`. The pattern is one mount in `App.vue`,
many calls from anywhere. `Dialog` itself is still a component (not a
service) for non-confirm modals — keep `<Dialog v-model:visible="...">`
for create/edit forms.
---
## 4. Aura theme and Crewli tokens
PrimeVue 4 uses CSS custom properties under the `--p-*` prefix, generated
from a JS preset. Crewli's preset extends Aura and overrides only the
primary color (teal) and dark-mode contrast colors. Full token plan is in
[RFC Appendix B](./RFC-WS-FRONTEND-PRIMEVUE.md#appendix-b--aura-theme-token-plan); F3 implements
`apps/app/src/plugins/primevue/theme.ts`.
Key conventions:
- **Primary color** is Crewli teal (`#0D9394`), defined as `primary.500` in the preset
- **Dark mode** uses `darkModeSelector: '.dark'` (matches Vuexy's existing dark-class strategy on `<html>`); the same toggle that flips Vuetify's dark theme today flips PrimeVue's, so no UI change for users
- **Surface, formField, list, navigation, overlay, content** tokens use Aura defaults (no override needed)
- **Brand-color customization** at the component level happens via the `pt` API (§7), not via CSS overrides
Do **not** introduce `--p-*` overrides directly in SCSS during F4. If a
brand adjustment is needed, extend the preset in `theme.ts` so the
override is centralized and dark-mode-aware.
---
## 5. Forms pattern (canonical)
The form layer is `@primevue/forms` plus a Zod resolver, called through a
Crewli-owned `<FormField>` wrapper at
`apps/app/src/components/forms/FormField.vue`.
**The full API specification — usage example, prop contract, slot
contract, `useFormError` 422 integration, error precedence — lives in
[RFC-WS-FRONTEND-PRIMEVUE Appendix A](./RFC-WS-FRONTEND-PRIMEVUE.md#appendix-a--formfield-api-specification).**
That is the single source of truth; it is intentionally not duplicated
here so that revisions to the API only need to land in one place.
What this document encodes (Crewli conventions on top of the API):
1. **One Zod schema per form**, defined at the top of the component or hoisted to `apps/app/src/schemas/[module].ts` if reused. Schema field names mirror the backend Form Request field names exactly (snake_case) so 422 errors map back without translation.
2. **`<FormField name="..." label="..." required>`** wraps every PrimeVue input. The wrapper's `name` prop matches the Zod schema key and the input's `:name` prop. Labels are Dutch by Crewli convention.
3. **Server validation (422)**: catch in the `onSubmit` handler, call `applyApiErrors(e.response.data.errors)` from `useFormError(formRef)`. Field-level messages are injected via `<FormField :apiError="...">`; manual `errors` refs are not used.
4. **No mixed validation**. A single form is either fully Zod-validated (target) or fully `:rules`-validated (legacy Vuetify). Do not introduce a hybrid.
5. **Migration phase**: existing forms keep their current `ref({}) + VForm + :rules + errors` pattern until the surrounding surface is migrated. Per F4 sub-package, all forms on that surface flip to PrimeVue + `<FormField>` + Zod resolver in one commit. See §9.
VeeValidate is **not** the form library. It was previously listed in
`CLAUDE.md` but never adopted; the form layer is `@primevue/forms` + Zod
resolver per [RFC AD-1](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-1-form-layer--primevueforms--zod-resolver).
---
## 6. DataTable pattern
`<DataTable>` replaces both `VDataTable` (client-side) and
`VDataTableServer` (server-side). The full DataTable inventory is in
[MIGRATION-AUDIT-PRIMEVUE §6](./MIGRATION-AUDIT-PRIMEVUE.md#6-datatable-inventory-critical-for-migration);
the strategy is in [RFC AD-7](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-7-datatable-strategy).
Conventions:
| Need | PrimeVue API | Notes |
|---|---|---|
| Server-side pagination/sort/filter | `:lazy="true"`, `:totalRecords`, `@page` / `@sort` / `@filter` event handlers | Replaces `VDataTableServer`'s `:items-length` + `@update:options` |
| Custom cell rendering | `<Column field="..."><template #body="slotProps">...</template></Column>` | Replaces Vuetify's `#item.fieldName` slot |
| Custom header rendering | `<Column><template #header>...</template></Column>` | Replaces `#header.fieldName` |
| Expandable rows | `expandedRows` v-model + `<template #expansion>` | https://primevue.org/datatable/#row_expand |
| Row selection | `selection` v-model + `selectionMode="single"` / `"multiple"` | |
| Virtual scrolling (>1000 rows) | `:virtualScrollerOptions="{ itemSize: 50 }"` | Replaces `v-data-table-virtual` |
| Empty / loading / error states | `<template #empty>`, `:loading`, surrounding error wrapper | The three-state pattern (loading / error / empty) per [`CLAUDE.md` UI rules](../CLAUDE.md) is preserved |
**Slot translation cheat sheet:**
| `VDataTable` slot | `DataTable` equivalent |
|---|---|
| `#item.email` | `<Column field="email"><template #body="{ data }">{{ data.email }}</template></Column>` |
| `#header.actions` | `<Column><template #header>Acties</template></Column>` |
| `#expanded-row` | `<template #expansion="{ data }">...</template>` |
| `#no-data` | `<template #empty>...</template>` |
| `#loading` | `:loading` prop + `loadingIcon` |
| `#top` | sibling `<div>` above `<DataTable>`; not a built-in slot |
**Per-page conventions:**
- One `<DataTable>` instance per page; no nested data tables
- Server-side tables wire to a TanStack Query composable that returns `{ data, isLoading, isError, refetch }`; the composable's params (`page`, `perPage`, `sortBy`, `filters`) are driven by reactive refs that PrimeVue's `@page` / `@sort` / `@filter` handlers update
- Column widths are **not** set in props for the common case; use `pt` on `<Column>` only when a fixed width is structurally needed
- Selection state is a Pinia store **only** if it crosses components (e.g., bulk-action toolbar); inline selection stays local
---
## 7. Pass-through (`pt`) API and slot conventions
PrimeVue's primary customization mechanism is `pt` (pass-through), which
targets named DOM nodes inside a component and applies attributes/classes
to them.
```vue
<Button label="Opslaan" :pt="{
root: { class: 'shadow-md' },
label: { class: 'font-semibold' }
}" />
```
Decision matrix — when to reach for which tool:
| Need | Use |
|---|---|
| Layout, spacing, typography around a component | Tailwind utility classes on the wrapping `<div>` |
| Internal styling of a component's DOM (e.g., the `Button`'s root `<button>`) | `pt` |
| Brand color, dark-mode-aware token | Aura preset extension in `theme.ts` (§4) |
| One-off override that doesn't fit the above | `<style scoped>` as last resort, with a comment explaining why |
Slot conventions vs. Vuetify:
- Most named slots map 1:1 in name (`#default`, `#footer`), but slot **prop shapes** differ — always check the PrimeVue docs page for the exact slot signature
- PrimeVue's slot system is generally narrower than Vuetify's; deep customization that Vuetify did via slots is done in PrimeVue via `pt`
- Default slots receive no implicit data in most components — read the doc page if a slot returns nothing useful
---
## 8. Tailwind v4 + PrimeVue integration
Tailwind v4 with the `tailwindcss-primeui` plugin is installed alongside
PrimeVue per [RFC AD-12](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-12-tailwind-css-v4-added-for-layout-utilities).
Configuration lives in F3.
Source-order conventions (CSS cascade matters):
1. PrimeVue base styles (auto-injected by the plugin)
2. Aura theme tokens (from `theme.ts`)
3. `tailwindcss-primeui` exposes Aura tokens as Tailwind utilities (`bg-primary`, `text-surface-500`, etc.)
4. Tailwind utility classes in templates
5. Project-scoped CSS (last resort)
When to reach for what:
| Need | Tool |
|---|---|
| Margin, padding, flex, grid, gap | Tailwind utility class |
| Text color, background, border using brand tokens | Tailwind utility from `tailwindcss-primeui` (`bg-primary-500`, `text-surface-700`) |
| Component-internal restyle | `pt` on the component |
| Brand-wide override | Aura preset extension |
Avoid mixing arbitrary HEX values into templates. If a color is reused
twice, it belongs in the Aura preset; reference it via the surface /
primary scale token instead of literal HEX.
---
## 9. Migration phase guidance
During F4 (sub-packages F4aF4d), the SPA contains both Vuetify and
PrimeVue components in the same build. The discipline that keeps this
sane:
1. **Surface-level consistency.** Within one route or component tree, use one framework. Never mix `<VTextField>` and `<InputText>` in the same form, never mix `<VBtn>` and `<Button>` in the same toolbar. The unit of migration is a surface (a route or a feature folder), committed as a sub-package.
2. **No back-porting.** Do not migrate "just one component" inside a not-yet-migrated surface. The sub-package is the smallest migration unit.
3. **Pre-migration: follow Vuetify.** If you are extending an un-migrated surface during F4 (e.g., bug fix on `/portal/*` while F4b is in progress on the organizer root), follow the conventions visible in the surrounding code. The pre-F2 `VUEXY_COMPONENTS.md` content is reachable at git commit `1c449ff6204cae6371da08c34ea8934d6b2ffcb8` for reference.
4. **Post-migration: follow PrimeVue.** Any new code on a migrated surface follows this document. New surfaces (created during or after F4) start in PrimeVue.
5. **Tests during migration.** Vuetify stays in test infrastructure for surfaces that haven't migrated yet. See [ARCH-TESTING.md §7](./ARCH-TESTING.md) for the test-runtime story; `setupFile` flips per [RFC AD-10](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-10-test-runtime--single-setupfile-flip).
6. **When in doubt, prefer PrimeVue conventions.** If a form on a migrated surface needs a component this document doesn't yet cover, find the closest PrimeVue equivalent on https://primevue.org/, implement it, and append a note in §3 (or open a tightly scoped PR to update this document). F4 sub-packages are expected to refine §3 — that is the explicit growth path.
7. **F6 is the cliff.** After F6 (cleanup, ~0.5 day), Vuetify is removed from `package.json` and `<V*>` tags fail to compile. No more parallel mode. Per [RFC §9](./RFC-WS-FRONTEND-PRIMEVUE.md#9-rollback-plan), F6 is the point of no return for rollback.
---
## 10. Cross-references and resources
External (PrimeVue ecosystem):
- PrimeVue components: https://primevue.org/
- `@primevue/forms` (form layer): https://primevue.org/forms/
- `@primevue/themes` (Aura preset): https://primevue.org/theming/styled/
- `tailwindcss-primeui` (token bridge): https://github.com/primefaces/tailwindcss-primeui
Internal (Crewli docs):
- [RFC-WS-FRONTEND-PRIMEVUE.md](./RFC-WS-FRONTEND-PRIMEVUE.md) — full migration plan, AD-1 to AD-12, sprint breakdown, risk register, FormField API (Appendix A), Aura theme tokens (Appendix B), version pinning policy (Appendix C)
- [MIGRATION-AUDIT-PRIMEVUE.md](./MIGRATION-AUDIT-PRIMEVUE.md) — F1 audit: Vuetify usage inventory (§1), `@core` / `@layouts` surface area (§2), Vuetify config and theme (§3), form-layer inventory (§4), page and route inventory (§5), DataTable inventory (§6)
- [ARCH-TESTING.md](./ARCH-TESTING.md) — test-tier decision tree; §7 covers the Vuetify-in-test-infra temporary state during the migration
- [VUEXY_COMPONENTS.md](./VUEXY_COMPONENTS.md) — deprecation stub; pre-F2 content recoverable via git commit `1c449ff6204cae6371da08c34ea8934d6b2ffcb8`
- [CLAUDE.md](../CLAUDE.md) — project-wide conventions; UI section now points here
This document is a foundation. F4 sub-packages will extend §3 with
component-level migration notes as real surfaces are converted; expect
diff PRs against this file alongside each sub-package commit.

View File

@@ -1,777 +1,33 @@
# Vuexy Component Registry
# Vuexy Components — DEPRECATED
Reference document for all frontend work. Consult BEFORE writing any new component.
Generated from Vuexy template v10.11.1 (`resources/vuexy-admin-v10.11.1/vue-version/`) and Crewli codebase scan.
> This document is deprecated. The Crewli SPA is migrating from Vuetify
> to PrimeVue per [RFC-WS-FRONTEND-PRIMEVUE](./RFC-WS-FRONTEND-PRIMEVUE.md).
---
## During F4 component migration
## 1. @core Components
The SPA contains both Vuetify and PrimeVue components simultaneously.
Reference the active framework per surface:
Source: `resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/src/@core/components/`
Copied into: `apps/app/src/@core/components/`
- **For PrimeVue conventions** (target state): see [`PRIMEVUE_COMPONENTS.md`](./PRIMEVUE_COMPONENTS.md)
- **For Vuetify conventions on un-migrated surfaces**: see git history
at commit `1c449ff6204cae6371da08c34ea8934d6b2ffcb8` (HEAD of `main`
immediately before F2):
### Form Element Wrappers (`@core/components/app-form-elements/`)
```bash
git show 1c449ff6204cae6371da08c34ea8934d6b2ffcb8:dev-docs/VUEXY_COMPONENTS.md
```
These wrap Vuetify form components with a **separate label row** for consistent styling. Use these instead of raw Vuetify form components in all forms.
## End of life
| Component | Purpose | Key Props | When to Use |
|-----------|---------|-----------|-------------|
| **AppTextField** | VTextField wrapper with separate label | All VTextField props via `$attrs` + `label` | Text input fields |
| **AppTextarea** | VTextarea wrapper with separate label | All VTextarea props via `$attrs` + `label` | Multi-line text input |
| **AppSelect** | VSelect wrapper with separate label | All VSelect props via `$attrs` + `label` | Dropdown select fields |
| **AppAutocomplete** | VAutocomplete wrapper with separate label | All VAutocomplete props via `$attrs` + `label` | Searchable dropdowns |
| **AppCombobox** | VCombobox wrapper with separate label | All VCombobox props via `$attrs` + `label` | Free-text + dropdown combo |
| **AppDateTimePicker** | Flatpickr-based date/time picker with Vuetify styling | `modelValue`, `placeholder`, `autofocus`, `counter`, `prefix`, `suffix`, all VInput/VField props | Date and time selection |
This file is deleted entirely in F6 cleanup once F4aF4d have migrated
all surfaces to PrimeVue and F5 has validated the migration.
All wrappers forward **all slots and events** from the underlying Vuetify component.
## Why a stub instead of immediate deletion
### Custom Form Controls (`@core/components/app-form-elements/`)
Styled card-based selection controls with title, description, and optional media.
| Component | Purpose | Key Props | Emits |
|-----------|---------|-----------|-------|
| **CustomCheckboxes** | Styled checkboxes with title + description | `selectedCheckbox: string[]`, `checkboxContent: CustomInputContent[]`, `gridColumn?` | `update:selectedCheckbox` |
| **CustomCheckboxesWithIcon** | Checkboxes with icon display | Same as above (content includes `icon`) | `update:selectedCheckbox` |
| **CustomCheckboxesWithImage** | Checkboxes with image backgrounds | `selectedCheckbox: string[]`, `checkboxContent: { bgImage, value, label? }[]` | `update:selectedCheckbox` |
| **CustomRadios** | Styled radio buttons with title + description | `selectedRadio: string`, `radioContent: CustomInputContent[]`, `gridColumn?` | `update:selectedRadio` |
| **CustomRadiosWithIcon** | Radio buttons with icon display | Same as above (content includes `icon`) | `update:selectedRadio` |
| **CustomRadiosWithImage** | Radio buttons with image backgrounds | `selectedRadio: string`, `radioContent: { bgImage, value, label? }[]` | `update:selectedRadio` |
### Card Components (`@core/components/cards/`)
| Component | Purpose | Key Props | When to Use |
|-----------|---------|-----------|-------------|
| **CardStatisticsHorizontal** | Stat card — icon right, text left | `title`, `icon`, `stats`, `color?` (default: primary) | Dashboard KPI cards (horizontal layout) |
| **CardStatisticsVertical** | Stat card with embedded ApexChart | `title`, `icon`, `stats`, `height`, `series`, `chartOptions`, `color?` | Stat cards with trend charts |
| **CardStatisticsVerticalSimple** | Simple stat card — icon + value | `title`, `icon`, `stats`, `color?` | Simple stat display without charts |
| **AppCardActions** | Card with collapse/refresh/remove action buttons | `collapsed?`, `noActions?`, `actionCollapsed?`, `actionRefresh?`, `actionRemove?`, `loading?`, `title?` | Cards needing user-togglable actions |
| **AppCardCode** | Code display with syntax highlighting + TS/JS toggle | `title`, `code: Record<'ts'\|'js', string>`, `codeLanguage?`, `noPadding?` | Documentation / code examples |
### Utility Components (`@core/components/`)
| Component | Purpose | Key Props / Events | When to Use |
|-----------|---------|-------------------|-------------|
| **AppStepper** | Multi-step wizard (horizontal/vertical) | `items: Item[]`, `currentStep?`, `direction?`, `iconSize?`, `isActiveStepValid?`, `align?`; emits `update:currentStep` | Multi-step forms, wizards, processes |
| **AppDrawerHeaderSection** | Drawer header with title + close button | `title`; emits `cancel`; slot: `beforeClose` | Header section in side drawers |
| **AppBarSearch** | Full-screen search dialog (Ctrl+K / Cmd+K) | `isDialogVisible`, `searchResults: T[]`, `isLoading?`; emits `search`, `update:isDialogVisible`; slots: `suggestions`, `searchResult`, `noData`, `noDataSuggestion` | Global search in app bar |
| **DialogCloseBtn** | Styled close button (X icon) for dialogs | `icon?` (default: tabler-x), `iconSize?` (default: 20) | Close button in dialog/modal headers |
| **TablePagination** | Table pagination with item count display | `page`, `itemsPerPage`, `totalItems`; emits `update:page` | Custom pagination for non-VDataTable tables |
| **MoreBtn** | Three-dot context menu button | `menuList?`, `itemProps?`, `iconSize?`, `class?` | More actions menu in cards/table rows |
| **ScrollToTop** | Fixed scroll-to-top button (appears after 200px scroll) | None | Add to layout for long pages |
| **TiptapEditor** | Rich text editor (Tiptap) with formatting toolbar | `modelValue`, `placeholder?`; emits `update:modelValue` | Rich text editing (notes, descriptions) |
| **ProductDescriptionEditor** | Rich text editor variant with toolbar | Same as TiptapEditor | Product/content description editing |
| **DropZone** | Drag-and-drop file upload with image preview cards | None (internal state) | Image file uploads |
| **TheCustomizer** | Theme customizer drawer (primary color, mode, skin, layout, width, direction) | None (reads/writes config store) | App-level theme customization panel |
| **ThemeSwitcher** | Light/Dark/System theme switcher dropdown | `themes: ThemeSwitcherTheme[]` | Theme switching in app bar |
| **Notifications** | Notification bell with dropdown panel | `notifications`, `badgeProps?`, `location?`; emits `read`, `unread`, `remove`, `click:notification` | User notifications in app bar |
| **Shortcuts** | Grid menu of quick-access shortcut links | `shortcuts: Shortcut[]`, `togglerIcon?` | Quick navigation in app bar |
| **I18n** | Language/locale switcher menu | `languages: I18nLanguage[]`, `location?` | Language switching in app bar |
| **CustomizerSection** | Section wrapper for theme customizer | `title`, `divider?` (default: true) | Organize customizer sections |
---
## 2. Layouts
### @layouts System (`@layouts/`)
The layout system is a Vuexy plugin providing navigation infrastructure, state management, and responsive behavior.
#### Layout Components (`@layouts/components/`)
| Component | Purpose | Key Props / Slots |
|-----------|---------|-------------------|
| **VerticalNavLayout** | Complete vertical nav layout wrapper | Props: `navItems`, `verticalNavAttrs`; Slots: `vertical-nav-header`, `before-vertical-nav-items`, `navbar` (receives toggleVerticalOverlayNavActive), default (page content), `footer` |
| **VerticalNav** | Sidebar navigation with PerfectScrollbar | Props: `tag`, `navItems`, `isOverlayNavActive`, `toggleIsOverlayNavActive`; Slots: `nav-header`, `before-nav-items`, `nav-items`, `after-nav-items` |
| **VerticalNavLink** | Individual nav link with ACL support | Props: `item: NavLink` (title, icon, to/href, badgeContent, disable) |
| **VerticalNavGroup** | Expandable nav group with accordion behavior | Props: `item: NavGroup` (title, icon, children[]) |
| **VerticalNavSectionTitle** | Section divider/header in nav | Props: `item: NavSectionTitle` (heading) |
| **HorizontalNavLayout** | Complete horizontal nav layout wrapper | Props: `navItems`; Slots: `navbar`, default, `footer` |
| **HorizontalNav** | Horizontal navigation bar | Props: `navItems` |
| **HorizontalNavLink** | Horizontal nav link item | Props: `item: NavLink`, `isSubItem` |
| **HorizontalNavGroup** | Dropdown group for horizontal nav | Props: `item: NavGroup`, `childrenAtEnd`, `isSubItem` |
| **HorizontalNavPopper** | Floating UI dropdown for horizontal nav | Props: `popperInlineEnd`, `tag`, `contentContainerTag`, `isRtl`; Slots: default (trigger), `content` |
| **TransitionExpand** | Smooth height expand/collapse animation | Used by VerticalNavGroup |
| **VNodeRenderer** | Generic VNode rendering (TSX) | Props: `nodes: VNode \| VNode[]` |
#### Layout Configuration (`@layouts/config.ts`)
| Setting | Default | Options |
|---------|---------|---------|
| Content width | Boxed | `ContentWidth.Fluid`, `ContentWidth.Boxed` |
| Nav type | Vertical | `AppContentLayoutNav.Vertical`, `AppContentLayoutNav.Horizontal` |
| Overlay nav breakpoint | md (768px) | Any Vuetify breakpoint |
| Navbar | Sticky | `NavbarType.Sticky`, `NavbarType.Static`, `NavbarType.Hidden` |
| Footer | Static | `FooterType.Sticky`, `FooterType.Static`, `FooterType.Hidden` |
| Horizontal nav | Sticky | Same as NavbarType |
| Navbar blur | true | boolean |
| Vertical nav collapsed | false | boolean |
#### Layout Store (`useLayoutConfigStore`)
Pinia store managing all layout state. Values persisted via cookies:
- `navbarType`, `isNavbarBlurEnabled`, `isVerticalNavCollapsed`
- `appContentWidth`, `appContentLayoutNav`
- `horizontalNavType`, `footerType`, `isAppRTL`
Computed: `_layoutClasses`, `isLessThanOverlayNavBreakpoint`, `isVerticalNavMini(isVerticalNavHovered)`
#### ACL Integration (`@layouts/plugins/casl.ts`)
- `can(action, subject)` — Check user ability (returns true if ACL disabled)
- `canViewNavMenuGroup(item)` — Determines if group renders based on children abilities
- `canNavigate(to)` — Route-level access control using `route.meta.action/subject`
### App Layouts (`apps/app/src/layouts/`)
| Layout | File | Used In | Purpose |
|--------|------|---------|---------|
| **default** | `layouts/default.vue` | All authenticated pages | Switches between vertical/horizontal nav based on config store |
| **blank** | `layouts/blank.vue` | Login, error pages | Minimal layout — no navigation |
| **OrganizerLayout** | `layouts/OrganizerLayout.vue` | All pages under `pages/{events,members,organisation,account-settings,dashboard,invitations}/**` | Organizer experience — thin wrapper around `DefaultLayoutWithVerticalNav` |
| **PortalLayout** | `layouts/PortalLayout.vue` | All pages under `pages/portal/**` | Volunteer/crew portal — custom navbar with two modes (platform / event) + mobile drawer; container max-width 1440px, navbar height 64px |
| **PublicLayout** | `layouts/PublicLayout.vue` | All pages under `pages/register/**` | Unauthenticated scaffold for public form viewer / registration flow |
`PortalLayout` is a **custom implementation** — it does NOT use @layouts VerticalNavLayout. Modes (set via route `meta.navMode`):
- **Platform mode** (`navMode: 'platform'`): Crewli logo + optional page title + UserAvatarMenu
- **Event mode** (`navMode: 'event'`): Org name + event name + back link
- Mobile: VNavigationDrawer hamburger menu with user info + nav links + logout
- Desktop: UserAvatarMenu component in navbar
**default** delegates to:
- `layouts/components/DefaultLayoutWithVerticalNav.vue` — Vertical sidebar (default)
- `layouts/components/DefaultLayoutWithHorizontalNav.vue` — Horizontal top nav (alternative)
**Navbar components** (in `layouts/components/`):
- `UserProfile.vue` — Avatar menu with logout
- `NavSearchBar.vue` — Search with grouped suggestions
- `NavBarNotifications.vue` — Notification bell + panel
- `NavbarShortcuts.vue` — Quick access buttons
- `NavbarThemeSwitcher.vue` — Light/Dark/System switcher
- `Footer.vue` — Copyright footer
### Route Meta for Layout Selection
```typescript
definePage({
meta: {
layout: 'blank' | 'default' | 'portal',
public: true, // skip auth guard
requiresAuth: false, // optional auth
navMode: 'event' | 'platform', // portal only
navTitle: 'Page Title', // portal platform mode only
navActiveLink: 'events', // highlight nav item in app
}
})
```
---
## 3. Established Page Patterns
### 3.1 List Page Pattern (Table + Filters + KPI Cards + Detail Drawer)
**Structure:** KPI metric cards → filter row → VDataTableServer → detail drawer + form dialogs
**Crewli reference:** `apps/app/src/pages/events/[id]/persons/index.vue`
**Vuexy template reference:** `resources/.../src/pages/apps/user/list/index.vue`
**Components used:**
- VRow/VCol (cols="12" sm="6" md="3") for 4 metric cards at top
- VCard + VAvatar (variant="tonal", rounded, size=44) + VIcon (size=28) for each metric
- AppSelect for filter dropdowns in a row
- VDataTableServer (or VDataTable) with custom column templates via `#item.[column]`
- VChip for status badges and type labels (with `:color` from status color map)
- VBtn for row actions (approve, edit, delete)
- PersonDetailPanel (VNavigationDrawer) for row-click detail view
- CreatePersonDialog / EditPersonDialog (VDialog) for CRUD
- VSnackbar for success/error notifications
- VSkeletonLoader for loading state
- VAlert (type="error") with retry button for error state
**KPI tiles pattern:**
```typescript
const tiles = computed(() => [
{ title: 'Totaal', value: meta.value?.total ?? 0, icon: 'tabler-users', color: 'primary' },
{ title: 'Goedgekeurd', value: meta.value?.total_approved ?? 0, icon: 'tabler-circle-check', color: 'success' },
{ title: 'Wachtend', value: meta.value?.pending ?? 0, icon: 'tabler-clock', color: 'warning' },
{ title: 'Afgewezen', value: meta.value?.rejected ?? 0, icon: 'tabler-x', color: 'error' },
])
```
**Data fetching pattern:**
```typescript
const { data, isLoading, isError, refetch } = usePersonList(eventId, filters)
const { mutate: approve } = useApprovePerson(eventId)
const { mutate: deletePerson } = useDeletePerson(eventId)
```
---
### 3.2 Detail Panel Pattern (Right Drawer with Tabs)
**Structure:** VNavigationDrawer (temporary, right) → header with avatar + status chips → VTabs → tabbed content
**Crewli reference:** `apps/app/src/components/persons/PersonDetailPanel.vue`
**Vuexy template reference:** `resources/.../src/views/apps/user/list/AddNewUserDrawer.vue` (drawer pattern)
**Components used:**
- VNavigationDrawer (`location="end"`, `temporary`, `width=480`)
- VAvatar (size=56, variant="tonal", color="primary") with initials
- VChip for status/type badges
- VBtn icon variant (variant="text", size="small") for edit/close actions
- VTabs + VTabsWindow + VTabsWindowItem for tabbed content
- VList + VListItem with `#prepend` slot (VIcon) for data display
- VTextarea for inline-editable admin notes (save on blur)
- VSwitch for toggle fields
- VSkeletonLoader (type="article") for loading state
- VDivider between sections
**Props pattern:**
```typescript
defineProps<{ eventId: string; orgId: string; personId: string | null }>()
const modelValue = defineModel<boolean>({ required: true })
defineEmits<{ edit: [person: Person] }>()
```
**Interaction flow:** Row click → `selectedPersonId` set → drawer opens → edit button emits `edit` → parent opens edit dialog.
**Drawer scroll fix** (custom CSS required for scrollable content):
```css
.detail-drawer :deep(.v-navigation-drawer__content) { min-height: 0; }
```
---
### 3.3 Form Dialog Pattern (Create/Edit)
**Structure:** VDialog → VCard → VCardTitle → VForm → form fields → VCardActions
**Crewli reference:** `apps/app/src/components/persons/CreatePersonDialog.vue`
**Vuexy template reference:** `resources/.../src/components/dialogs/UserInfoEditDialog.vue`
**Components used:**
- VDialog (`max-width="500"` or responsive: `$vuetify.display.smAndDown ? 'auto' : 600`)
- VCard with VCardTitle, VCardText, VCardActions
- VForm (ref for validation)
- AppTextField / AppSelect / AppDateTimePicker / AppAutocomplete for form fields
- VBtn with `:loading="isPending"` for submit
- DialogCloseBtn in card header (Vuexy pattern) or VBtn icon for close
- `@core/utils/validators` for field validation (requiredValidator, emailValidator)
**Error handling pattern:**
```typescript
const errors = ref<Record<string, string>>({})
const refVForm = ref<VForm>()
const { mutate, isPending } = useCreatePerson(eventIdRef)
function onSubmit() {
refVForm.value?.validate().then(({ valid }) => {
if (!valid) return
mutate(payload, {
onSuccess: () => { modelValue.value = false },
onError: (err) => { errors.value = err.response.data.errors },
})
})
}
```
```vue
<AppTextField :error-messages="errors.email" :rules="[requiredValidator, emailValidator]" />
```
**v-model pattern for dialog visibility:**
```typescript
const modelValue = defineModel<boolean>({ required: true })
```
---
### 3.4 Dashboard / Stat Cards Pattern
**Structure:** VRow/VCol grid with color-coded metric cards → conditional content sections
**Crewli reference:** `apps/app/src/components/events/EventMetricCards.vue`
**Vuexy template reference:** `resources/.../src/pages/dashboards/analytics.vue`
**Components used:**
- VRow + VCol (cols="12" sm="6" md="3") for responsive 4-column grid
- VCard with `card-border-shadow-{color}` custom class and `cursor-pointer h-100`
- VAvatar (`:color`, `variant="tonal"`, `size=44`, `rounded`) + VIcon (`size=28`)
- VCardText with `d-flex align-center mb-1` for icon + value row
- VSkeletonLoader (type="heading") in matching grid for loading state
- VAlert (variant="tonal", type="error") with retry for error state
**Color logic pattern:**
```typescript
const shiftFillColor = computed(() => {
if (!stats.value || stats.value.shifts_total === 0) return 'success'
const rate = stats.value.shifts_filled / stats.value.shifts_total
if (rate >= 0.9) return 'success'
if (rate >= 0.6) return 'warning'
return 'error'
})
```
**Card template:**
```vue
<VCol cols="12" sm="6" md="3">
<VCard :class="['cursor-pointer h-100', `card-border-shadow-${color}`]" @click="navigate">
<VCardText>
<div class="d-flex align-center mb-1">
<VAvatar :color="color" variant="tonal" size="44" rounded class="me-4">
<VIcon :icon="icon" size="28" />
</VAvatar>
<h4 class="text-h4 mb-0">{{ value }}</h4>
</div>
<p class="mb-1">{{ title }}</p>
<p class="mb-0">
<span class="text-heading fw-medium me-2">{{ secondary }}</span>
<span class="text-body-secondary text-sm">{{ subtitle }}</span>
</p>
</VCardText>
</VCard>
</VCol>
```
---
### 3.5 Tab Navigation Pattern (Event Tabs Wrapper)
**Structure:** Header (back + title + status chips + actions) → VTabs → slot for child page content
**Crewli reference:** `apps/app/src/components/events/EventTabsNav.vue`
**Vuexy template reference:** `resources/.../src/pages/pages/account-settings/[tab].vue`
**Components used:**
- VBtn (icon="tabler-arrow-left", variant="text") for back navigation
- VChip for status/type badges with color maps
- VTabs with VTab using `:to` prop for route-based tab switching (no VWindow needed)
- Slot for tab content — child page renders inside
- VSkeletonLoader (type="card") for loading
- VAlert (type="error") with retry for errors
**Tab definition:**
```typescript
const tabs = [
{ label: 'Overzicht', icon: 'tabler-layout-dashboard', route: 'events-id' },
{ label: 'Personen', icon: 'tabler-users', route: 'events-id-persons' },
{ label: 'Secties & Shifts', icon: 'tabler-layout-grid', route: 'events-id-sections' },
{ label: 'Instellingen', icon: 'tabler-settings', route: 'events-id-settings' },
]
const activeTab = computed(() =>
tabs.find(t => route.name === t.route || route.name?.startsWith(`${t.route}-`))?.route
)
```
---
### 3.6 Settings Page Pattern (Nested Tabs)
**Structure:** VTabs (route/query-param based) → VWindow + VWindowItem → delegated tab components
**Crewli reference:** `apps/app/src/pages/organisation/settings.vue`
**Vuexy template reference:** `resources/.../src/pages/pages/account-settings/[tab].vue`
Two approaches from Vuexy:
- **Horizontal tabs:** `VTabs` (class="v-tabs-pill") with VTab `:to` route binding + VWindow
- **Vertical tabs:** VRow → VCol md="4" (VTabs direction="vertical") + VCol md="8" (VWindow)
**Key patterns:**
- `v-tabs-pill` class for pill-style tabs (Vuexy custom class)
- `disable-tab-transition` on VWindow to prevent animation delays
- Route params or query params for tab state persistence
---
### 3.7 Sections & Shifts Pattern (Draggable + Table + Drawer)
**Structure:** Draggable section list (sidebar) → shift table (main area) → shift detail drawer
**Crewli reference:** `apps/app/src/components/sections/SectionsShiftsPanel.vue`
**Components used:**
- `vuedraggable` library for reorderable section list
- VCard per section with click-to-activate
- VDataTable for shifts within active section
- ShiftDetailPanel (VNavigationDrawer) for shift details
- Custom CSS for drag ghost states (necessary — no Vuetify equivalent)
---
### 3.8 Confirm Dialog Pattern
**Inline confirm** (preferred for new code):
```vue
<VDialog v-model="confirmOpen" max-width="440">
<VCard>
<VCardTitle class="text-h6">Status wijzigen</VCardTitle>
<VCardText class="text-body-1">
Weet je zeker dat je de status wilt wijzigen naar <strong>{{ target }}</strong>?
</VCardText>
<VCardActions>
<VSpacer />
<VBtn variant="text" :disabled="isPending" @click="cancel">Annuleren</VBtn>
<VBtn color="primary" :loading="isPending" @click="confirm">Bevestigen</VBtn>
</VCardActions>
</VCard>
</VDialog>
```
**Reusable ConfirmDialog** component also available at `components/dialogs/ConfirmDialog.vue` with success/cancel result dialogs.
---
### 3.9 Portal Page Patterns
#### Event List (Card Grid)
**Reference:** `apps/app/src/pages/portal/evenementen/index.vue`
- VRow + VCol (cols="12" sm="6" md="4") responsive grid
- Custom EventCard component per event (gradient banner, status chip, hover animation)
- VSkeletonLoader for loading, VAlert for empty state
#### Event Detail (Hash-based Tabs)
**Reference:** `apps/app/src/pages/portal/evenementen/[eventId].vue`
- VTabs with hash-based routing (#overzicht, #rooster, #claimen, #informatie)
- VWindow + VWindowItem per tab with dedicated tab components
- StatusCard shows different UI per approval status (pending/approved/rejected)
- Conditional tab visibility based on approval status
#### Registration (Multi-step Public Form)
**Reference:** `apps/app/src/pages/register/[public_token].vue`
- VForm + `useFormDraft` composable for state, autosave, idempotency-key drafts
- Per-field validators from `@core/utils/validators` (`emailValidator`, `requiredValidator`)
- Zod schemas in `apps/app/src/schemas/registrationSchema.ts` validate the
outgoing payload at submit time
- Conditional form fields based on event configuration
- Real-time email duplicate checking
- Password creation for new users
#### Auth Pages (Centered Card)
**Reference:** `apps/app/src/pages/login.vue`
- VCard centered on blank layout
- VTextField with password visibility toggle
- VAlert for error messages, VBtn with loading state
---
## 4. Vuetify Component Quick Reference
Preferred Vuetify components for common needs. Use these, not custom solutions.
| Need | Use | NOT |
|------|-----|-----|
| Data tables (server pagination) | `VDataTableServer` | VDataTable or custom table |
| Data tables (client-side) | `VDataTable` | Custom table markup |
| Form text input | `AppTextField` (@core wrapper) | Raw VTextField or custom input |
| Form textarea | `AppTextarea` (@core wrapper) | Raw VTextarea |
| Select / dropdown | `AppSelect` or `AppAutocomplete` (@core) | Raw VSelect or custom dropdown |
| Date/time picker | `AppDateTimePicker` (@core, Flatpickr) | `input[type=date]` or custom picker |
| Confirmation dialog | VDialog + VCard (inline pattern, §3.8) | `window.confirm()` |
| Side panel / drawer | VNavigationDrawer (`location="end"`, `temporary`) | Custom sidebar |
| Tabs (route-based) | VTabs + VTab (with `:to` for routing) | Custom tab logic |
| Tabs (local state) | VTabs + VWindow + VWindowItem | Manual `v-if` switching |
| Loading: page/card | VSkeletonLoader (`type="card"`, `"article"`, `"heading"`) | Custom spinner |
| Loading: button | VBtn `:loading="isPending"` | Disabled + spinner |
| Loading: global bar | VProgressLinear (indeterminate) | Custom loading bar |
| Error state | VAlert (`type="error"`) with retry VBtn in `#append` slot | Custom error div |
| Empty state | VCard (`variant="outlined"`) + VIcon + message + VBtn | Blank area |
| Status / type badge | VChip (`:color` from color map, `size="small"`) | Custom styled span |
| Icon buttons | VBtn (`icon` variant, `variant="text"`, `size="small"`) | Custom icon wrapper |
| Tooltips | VTooltip (default location: top via defaults) | `title` attribute |
| Toast / notification | VSnackbar | Custom notification |
| User avatar | VAvatar (`variant="tonal"`) with initials or VImg | Custom circle div |
| Stat metric cards | VCard + VAvatar (tonal, rounded) + VIcon layout | CardStatisticsHorizontal (unless you need chart) |
| Progress indicator | VProgressLinear | Custom progress bar |
| Responsive grid | VRow + VCol with `cols`/`sm`/`md`/`lg` props | CSS Grid or fixed widths |
| List display | VList + VListItem with `#prepend`/`#append` slots | Custom list markup |
| Dividers | VDivider | Custom hr or border |
| Spacer in card actions | VSpacer | Custom margin/flex |
| Menu / dropdown actions | VMenu + VList + VListItem | Custom popover |
| Drag-and-drop lists | `vuedraggable` (external library) | Custom drag logic |
| Rich text editing | TiptapEditor (@core) | Custom editor |
| Form validation | VForm ref + `@core/utils/validators` + Zod schema for payload + API 422 error map | VeeValidate (removed; was never actually adopted) |
| Multi-step wizard | AppStepper (@core) | Custom step logic |
| Drawer header | AppDrawerHeaderSection (@core) | Custom header |
| Code display | AppCardCode (@core) | Custom code block |
---
## 5. Plugins & Configuration
### Vuetify Defaults (`plugins/vuetify/defaults.ts`)
Pre-configured component defaults — no need to repeat these props in your code:
| Component | Default Props |
|-----------|---------------|
| VBtn | `color: 'primary'` |
| VChip | `label: true` |
| VList | `density: 'compact'`, `color: 'primary'` |
| VTabs | `color: 'primary'`, `density: 'comfortable'` |
| VSelect, VTextField, VAutocomplete | `variant: 'outlined'`, `color: 'primary'`, `density: 'comfortable'`, `hideDetails: 'auto'` |
| VCheckbox, VRadio, VSwitch | `color: 'primary'`, `density: 'comfortable'` |
| VTooltip | `location: 'top'` |
| VMenu | `offset: '2px'` |
| VExpansionPanel | `expandIcon: 'tabler-chevron-right'`, `collapseIcon: 'tabler-chevron-right'` |
| VDataTable | Pagination icons: `tabler-chevrons-left/right`, `tabler-chevron-left/right` |
| VPagination | First/last: `tabler-chevrons-left/right`, prev/next: `tabler-chevron-left/right` |
### Theme Colors (`plugins/vuetify/theme.ts`)
| Color | Hex | Usage |
|-------|-----|-------|
| Primary | `#7367F0` | Buttons, links, active states |
| Secondary | `#808390` | Muted text, secondary actions |
| Success | `#28C76F` | Approved, positive states |
| Info | `#00BAD1` | Informational states |
| Warning | `#FF9F43` | Pending, attention-needed states |
| Error | `#FF4C51` | Rejected, destructive actions |
Light theme: background `#F8F7FA`, surface `#FFFFFF`
Dark theme: background `#25293C`, surface `#2F3349`
Full grey scale: grey-50 through grey-900. Semantic on-colors: on-primary, on-secondary, etc.
### Icon System (`plugins/vuetify/icons.ts`)
- **Tabler icons** via Iconify (prefix: `tabler-`)
- Custom SVG overrides for checkbox and radio form controls
- 40+ Vuetify icon aliases configured (expand, collapse, sort, check, clear, edit, delete, calendar, etc.)
### Plugin Registration System (`@core/utils/plugins.ts`)
Auto-discovers and registers plugins from `plugins/*.ts` and `plugins/*/index.ts` using `import.meta.glob`. Numbered prefixes control load order:
- `1.router/` — loaded first
- `2.pinia.ts` — loaded second
- Everything else — alphabetical
### @core Utilities
| Utility | Path | Key Exports |
|---------|------|-------------|
| **validators** | `@core/utils/validators.ts` | `requiredValidator`, `emailValidator`, `passwordValidator`, `confirmedValidator`, `betweenValidator`, `integerValidator`, `regexValidator`, `alphaValidator`, `urlValidator`, `lengthValidator`, `alphaDashValidator` |
| **formatters** | `@core/utils/formatters.ts` | `avatarText(name)` → initials, `kFormatter(num)` → "1.5k", `formatDate(value, options?)`, `formatDateToMonthShort(value)`, `prefixWithPlus(value)` |
| **helpers** | `@core/utils/helpers.ts` | `isEmpty(value)`, `isNullOrUndefined(value)`, `isEmptyArray(arr)`, `isObject(obj)`, `isToday(date)` |
| **colorConverter** | `@core/utils/colorConverter.ts` | `hexToRgb(hex)`, `rgbaToHex(rgba, forceRemoveAlpha?)` |
| **vuetify** | `@core/utils/vuetify.ts` | `resolveVuetifyTheme(defaultTheme)` — resolves from cookie/system preference |
### @core Composables
| Composable | Path | Purpose |
|------------|------|---------|
| **useCookie** | `@core/composable/useCookie.ts` | Reactive cookie management — returns `Ref<T>` synced to `document.cookie`, JSON serialization, 30-day default expiry |
| **useResponsiveSidebar** | `@core/composable/useResponsiveSidebar.ts` | Returns `isLeftSidebarOpen` ref, auto-hides on mobile (mdAndDown) |
| **useSkins** | `@core/composable/useSkins.ts` | `injectSkinClasses()` — adds `skin--{value}` to body; `layoutAttrs` — computed for semi-dark vertical nav |
| **useGenerateImageVariant** | `@core/composable/useGenerateImageVariant.ts` | Returns correct image for current theme/skin (light, dark, light-bordered, dark-bordered) |
| **createUrl** | `@core/composable/createUrl.ts` | Reactive URL builder with query params from `MaybeRefOrGetter` sources |
### @core Enums & Types (`@core/enums.ts`, `@core/types.ts`)
```typescript
// Enums
Skins: 'default' | 'bordered'
Theme: 'light' | 'dark' | 'system'
Layout: 'vertical' | 'horizontal' | 'collapsed'
Direction: 'ltr' | 'rtl'
// Key types
UserThemeConfig // extends LayoutConfig with i18n, theme, skin, isVerticalNavSemiDark
CustomInputContent // { title, desc, value, subtitle?, icon? } for Custom Checkboxes/Radios
GridColumn // Vuetify grid column props
SortItem // { key, order } for data table sorting
Options // { page, itemsPerPage, sortBy, groupBy, search } for data table
```
### @core Chart Libraries
| Library | Path | Components |
|---------|------|------------|
| **ApexCharts** | `@core/libs/apex-chart/apexCharConfig.ts` | Configuration utilities for ApexCharts |
| **Chart.js** | `@core/libs/chartjs/` | 7 wrappers: BarChart, BubbleChart, DoughnutChart, LineChart, PolarAreaChart, RadarChart, ScatterChart |
---
## 6. Crewli Custom Components
### Event Components (`apps/app/src/components/events/`)
| Component | Purpose | Used By |
|-----------|---------|---------|
| **EventTabsNav** | Event header + horizontal tab navigation wrapper | All event sub-pages |
| **EventMetricCards** | 4-column KPI stat card grid with color logic | Event overview page |
| **CreateEventDialog** | Create event form dialog | Events list page |
| **EditEventDialog** | Edit event form dialog | EventTabsNav |
| **CreateSubEventDialog** | Create sub-event for festivals | Programmaonderdelen page |
| **DeleteSubEventDialog** | Delete sub-event confirmation | Programmaonderdelen page |
| **RegistrationLinkCard** | Copyable registration link display | EventTabsNav |
### Person Components (`apps/app/src/components/persons/`)
| Component | Purpose | Used By |
|-----------|---------|---------|
| **PersonDetailPanel** | Right drawer — avatar + status + tabs (Info/Shifts/Accreditatie) | Persons list page |
| **CreatePersonDialog** | Create person form with crowd type + company selection | Persons list page |
| **EditPersonDialog** | Edit person form | Persons list page, PersonDetailPanel |
### Section & Shift Components (`apps/app/src/components/sections/`, `shifts/`)
| Component | Purpose | Used By |
|-----------|---------|---------|
| **SectionsShiftsPanel** | Draggable section list + shift table + shift detail drawer | Sections page |
| **CreateSectionDialog** | Create section form | SectionsShiftsPanel |
| **EditSectionDialog** | Edit section form | SectionsShiftsPanel |
| **CreateShiftDialog** | Create shift form | SectionsShiftsPanel |
| **CreateTimeSlotDialog** | Create time slot form | Time slots page |
| **ShiftDetailPanel** | Right drawer — shift details + assignments | SectionsShiftsPanel |
| **AssignShiftDialog** | Assign person to shift | SectionsShiftsPanel |
| **AssignPersonDialog** | Assign person to shift (from person context) | ShiftDetailPanel |
### Crowd List Components (`apps/app/src/components/crowd-lists/`)
| Component | Purpose | Used By |
|-----------|---------|---------|
| **CrowdListFormDialog** | Create/edit crowd list | Crowd lists page |
| **CrowdListDetailPanel** | Right drawer — crowd list details + members | Crowd lists page |
| **AddPersonToCrowdListDialog** | Add person to crowd list | CrowdListDetailPanel |
### Organisation Components (`apps/app/src/components/organisation/`, `organisations/`)
| Component | Purpose | Used By |
|-----------|---------|---------|
| **EditOrganisationDialog** | Edit organisation details | Organisation page |
| **CompanyDialog** | Create/edit company | Companies page |
| **CrowdTypesManager** | Manage crowd types (table + CRUD) | Organisation settings |
| **PersonTagsTab** | Manage person tags | Organisation settings |
| **RegistrationFieldTemplatesTab** | Manage field templates | Organisation settings |
| **EmailBrandingTab** | Email branding settings (colors, logo) | Organisation settings |
### Member Components (`apps/app/src/components/members/`)
| Component | Purpose | Used By |
|-----------|---------|---------|
| **InviteMemberDialog** | Invite member form | Members page |
| **EditMemberRoleDialog** | Change member role | Members page |
### Registration Field Components (`apps/app/src/components/event/`)
| Component | Purpose | Used By |
|-----------|---------|---------|
| **RegistrationFieldCard** | Draggable field card with edit/delete | Registration fields page |
| **RegistrationFieldFormDialog** | Create/edit registration field | Registration fields page |
| **TemplatePickerDialog** | Select from predefined field templates | Registration fields page |
| **ImportFromEventDialog** | Import fields from another event | Registration fields page |
### Layout Components (`apps/app/src/components/layout/`)
| Component | Purpose | Used By |
|-----------|---------|---------|
| **OrganisationSwitcher** | Switch between organisations | Navigation sidebar |
### Portal Components (`apps/app/src/components/portal/`)
| Component | Purpose | Used By |
|-----------|---------|---------|
| **portal/EventCard** | Event card for grid (gradient banner, status chip, hover) | Events list |
| **portal/StatusCard** | Approval status display with quick action cards (pending/approved/rejected variants) | Event overview tab |
| **portal/UserAvatarMenu** | User menu in portal navbar | Portal layout |
| **portal/AppLoadingIndicator** | Global loading bar (Suspense fallback, VProgressLinear) | Portal layouts |
| **event/OverzichtTab** | Event overview tab (StatusCard + shift summary) | Event detail |
| **event/RoosterTab** | My shifts schedule tab (cancel dialog) | Event detail |
| **event/ClaimenTab** | Claim available shifts tab (day-grouped, confirm dialog) | Event detail |
| **event/InformatieTab** | Event info display tab (VList read-only) | Event detail |
---
## 7. Three-State Pattern (Loading / Error / Empty)
Every data-driven view MUST handle three states. Follow this pattern:
```vue
<!-- Loading -->
<VSkeletonLoader v-if="isLoading" type="card" />
<!-- Error -->
<VAlert v-else-if="isError" type="error" class="mb-4">
Kon data niet laden.
<template #append>
<VBtn variant="text" @click="refetch()">Opnieuw proberen</VBtn>
</template>
</VAlert>
<!-- Empty -->
<VCard v-else-if="!items?.length" variant="outlined" class="text-center pa-8">
<VIcon icon="tabler-inbox" size="48" class="mb-4 text-disabled" />
<p class="text-body-1 text-disabled">Geen resultaten gevonden.</p>
<VBtn @click="openCreate">Toevoegen</VBtn>
</VCard>
<!-- Data -->
<template v-else>
<!-- Actual content -->
</template>
```
**Loading skeleton for grids** (match the data layout):
```vue
<VRow v-if="isLoading">
<VCol v-for="n in 4" :key="n" cols="12" sm="6" md="3">
<VCard><VCardText><VSkeletonLoader type="heading" /></VCardText></VCard>
</VCol>
</VRow>
```
---
## 8. SCSS & Styling Reference
### Available SCSS Imports
From the Vuexy template (`@core/scss/`):
| Category | Key Files | Purpose |
|----------|-----------|---------|
| **Base** | `_components.scss`, `_dark.scss`, `_misc.scss` | Core component overrides |
| **Layout** | `_default-layout.scss`, `_default-layout-w-vertical-nav.scss` | Layout structure styles |
| **Navigation** | `_vertical-nav.scss`, `_horizontal-nav.scss` | Nav-specific styles |
| **Utilities** | `_utilities.scss`, `_mixins.scss`, `_variables.scss` | Helper classes and mixins |
| **Skins** | `skins/` directory | Bordered skin variant styles |
| **Template pages** | `template/pages/` | Page-specific styles (auth, misc, etc.) |
| **Component overrides** | `template/components/` | Per-component SCSS (alert, avatar, button, card, chip, dialog, table, tabs, tooltip, etc.) |
| **Library styles** | `template/libs/` | ApexChart, FullCalendar, Shepherd, Swiper overrides |
### Page-Level SCSS Usage
```vue
<!-- Auth pages -->
<style lang="scss">
@use "@core/scss/template/pages/page-auth";
</style>
<!-- Error pages -->
<style lang="scss">
@use "@core/scss/template/pages/misc.scss";
</style>
```
### Custom CSS Classes from Vuexy
| Class | Purpose |
|-------|---------|
| `card-border-shadow-{color}` | Colored left border shadow on cards (primary, success, warning, error, info) |
| `v-tabs-pill` | Pill-style tabs |
| `skin--default`, `skin--bordered` | Skin system classes (injected on body) |
| `match-height` | Equal height cards in a row |
| `disable-tab-transition` | Prevent VWindow tab animation |
---
## 9. Cleanup Opportunities
Most custom CSS in the codebase is justified. Identified areas:
| File | CSS | Status |
|------|-----|--------|
| `SectionsShiftsPanel.vue` | `.section-item { cursor: grab }`, `.section-ghost` | **Justified** — vuedraggable states, no Vuetify equivalent |
| `registration-fields.vue` | `.field-ghost` | **Justified** — vuedraggable styling |
| `RegistrationFieldCard.vue` | `.cursor-grab`, `.min-width-0` | **Justified** — no Vuetify utility for cursor/min-width |
| `CrowdListDetailPanel.vue` | `:deep(.v-navigation-drawer__content) { min-height: 0 }` | **Justified** — drawer scroll fix |
| `ShiftDetailPanel.vue` | Same drawer override | **Justified** — same fix |
| `EmailBrandingTab.vue` | `.color-swatch` sizing | **Justified** — custom color preview element |
| `AddEditPermissionDialog.vue` | `.permission-table td` styling | **Minor** — template code, custom padding intentional |
| `AppSearchHeader.vue` | `.search-header` padding/background | **Template code** — not actively used in Crewli |
**Overall:** Excellent Vuetify adoption. Custom CSS is minimal and justified for cases without Vuetify equivalents (drag states, drawer scroll fixes, specialized visual elements).
Per project decision 2026-05-10: during F4aF4c, some F4 developers
will need to look up Vuetify conventions for components on un-migrated
surfaces. Git-history archaeology
(`git show 1c449ff6204cae6371da08c34ea8934d6b2ffcb8:dev-docs/VUEXY_COMPONENTS.md`)
is workable but inconvenient. This stub provides an explicit forwarding
marker, with the actual content recoverable via git history per the
explicit SHA above.