6 Commits

Author SHA1 Message Date
834611103e Merge pull request 'chore(docs): F2 — PrimeVue documentation foundation' (#22) from chore/f2-primevue-docs into main
Reviewed-on: #22
2026-05-10 23:23:52 +02:00
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-TIMETABLE-Artist-Timetable-Module.md
dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md
dev-docs/MIGRATION-AUDIT-PRIMEVUE.md dev-docs/MIGRATION-AUDIT-PRIMEVUE.md
dev-docs/PRIMEVUE_COMPONENTS.md
dev-docs/CLAUDE_CODE_TOOLING.md dev-docs/CLAUDE_CODE_TOOLING.md
dev-docs/FRONTEND-TOOLING.md dev-docs/FRONTEND-TOOLING.md
dev-docs/LARASTAN.md dev-docs/LARASTAN.md

View File

@@ -1,7 +1,11 @@
# Crewli Cursor Rules # 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 ## 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 ## Laravel
- Resource controllers, form requests, API resources — always - 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 - Global scope `OrganisationScope` on event-related models
- Policies for authorization — never inline role checks - Policies for authorization — never inline role checks
## Vuexy reference path (mandatory) ## UI framework strategy (migration-aware)
- When looking at Vuexy examples, demos, or patterns, ALWAYS reference: - 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/` `resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/`
- Never reference `javascript-version/` or `starter-kit/` variants - Never reference `javascript-version/` or `starter-kit/` variants
## Vue 3 ## Vue 3
- `<script setup lang="ts">` always - `<script setup lang="ts">` always
- TanStack Query for API state, Pinia for UI state - 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 ## Naming
- snake_case DB | camelCase JS | PascalCase Vue | `use*` composables | `use*Store` Pinia - 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 ## Tech stack
- Backend: PHP 8.2+, Laravel 12, Sanctum, Spatie Permission, MySQL 8, Redis - 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) - Testing: PHPUnit (backend), Vitest (frontend)
## Quality gates ## Quality gates
@@ -166,54 +166,47 @@ right tier per the decision tree there before adding new tests.
## Frontend rules (strict) ## 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.
``` **Always read [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md)
resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/ 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: #### On migrated surfaces (target state)
- `javascript-version/` — wrong language
- `starter-kit/` — incomplete, missing components
- Any other variant or version
Before implementing any Vuexy-based page or component, read the reference implementation from this path first: PrimeVue is the framework. Follow [`PRIMEVUE_COMPONENTS.md`](./dev-docs/PRIMEVUE_COMPONENTS.md):
```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"
```
### 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. #### On un-migrated surfaces (legacy, transient)
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.
Concrete component rules: Vuetify + Vuexy `@core/` components remain in use until the surface's F4
- Tables: `v-data-table-server` with server-side pagination — never client-side for API data sub-package lands. When extending these surfaces during the transition:
- 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
**Before ANY frontend task:** read `/dev-docs/VUEXY_COMPONENTS.md` to verify - Match the surrounding code (`<VBtn>`, `<VTextField>`, `<v-data-table-server>`, etc.)
you are using available components rather than building custom ones. - 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 ### Vue components
@@ -240,7 +233,16 @@ you are using available components rather than building custom ones.
### Forms ### 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 - `ref({ field: ... })` for form state
- `VForm` ref + per-field rules drawn from `@core/utils/validators` - `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 - A separate `errors: Ref<Record<string, string>>` for server-validation
feedback (mapped from 422 responses) feedback (mapped from 422 responses)
- **Zod** for runtime validation of API payloads/responses (in - **Zod** for runtime validation of API payloads/responses (in
`apps/app/src/schemas/*.ts`) — Zod schemas mirror backend Form Requests `apps/app/src/schemas/*.ts`) — schemas already mirror backend Form
(field names, required/optional, types) and are the canonical contract Requests and carry forward unchanged into the target state
- No inline validation logic in components - No inline validation logic in components
VeeValidate is **NOT** the form library here. It was previously listed A single form is either fully Zod-resolver-validated (target) or fully
but never actually adopted in any page; it was removed in commit `:rules`-validated (legacy) — never a hybrid. VeeValidate is **NOT** in
`<sha>` (Session 4 follow-up). Reference forms: `apps/app/src/components/sections/CreateShiftDialog.vue`, 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/components/timetable/AddPerformanceDialog.vue`,
`apps/app/src/pages/register/[public_token].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 ### UI
- Always use Vuexy/Vuetify for layout, forms, tables, dialogs - 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 Vuetify utility class exists - 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 - 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) - **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
- Use Vuetify responsive props (`cols`, `sm`, `md`, `lg`) — no fixed pixel widths - 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 Vuetify utility exists - Custom CSS via `<style scoped>` only as last resort when no framework utility / `pt` API / Aura token can do the job
## Forbidden patterns ## 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]/` 13. Vue page component in `src/pages/[module]/`
14. Add route in Vue Router 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 ## Diagnostic discipline: audit before assume
When debugging or fixing any bug, the first action is to verify the 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. > This document is deprecated. The Crewli SPA is migrating from Vuetify
Generated from Vuexy template v10.11.1 (`resources/vuexy-admin-v10.11.1/vue-version/`) and Crewli codebase scan. > 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/` - **For PrimeVue conventions** (target state): see [`PRIMEVUE_COMPONENTS.md`](./PRIMEVUE_COMPONENTS.md)
Copied into: `apps/app/src/@core/components/` - **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.
| 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 |
All wrappers forward **all slots and events** from the underlying Vuetify component.
### 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
}
})
``` ```
--- ## End of life
## 3. Established Page Patterns This file is deleted entirely in F6 cleanup once F4aF4d have migrated
all surfaces to PrimeVue and F5 has validated the migration.
### 3.1 List Page Pattern (Table + Filters + KPI Cards + Detail Drawer) ## Why a stub instead of immediate deletion
**Structure:** KPI metric cards → filter row → VDataTableServer → detail drawer + form dialogs Per project decision 2026-05-10: during F4aF4c, some F4 developers
**Crewli reference:** `apps/app/src/pages/events/[id]/persons/index.vue` will need to look up Vuetify conventions for components on un-migrated
**Vuexy template reference:** `resources/.../src/pages/apps/user/list/index.vue` surfaces. Git-history archaeology
(`git show 1c449ff6204cae6371da08c34ea8934d6b2ffcb8:dev-docs/VUEXY_COMPONENTS.md`)
**Components used:** is workable but inconvenient. This stub provides an explicit forwarding
- VRow/VCol (cols="12" sm="6" md="3") for 4 metric cards at top marker, with the actual content recoverable via git history per the
- VCard + VAvatar (variant="tonal", rounded, size=44) + VIcon (size=28) for each metric explicit SHA above.
- 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).