Compare commits
6 Commits
1c449ff620
...
834611103e
| Author | SHA1 | Date | |
|---|---|---|---|
| 834611103e | |||
| 99eedb6004 | |||
| 1701e32fdf | |||
| b5765221bb | |||
| 4f07a673a1 | |||
| 9e137cffb9 |
@@ -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
|
||||||
|
|||||||
19
.cursorrules
19
.cursorrules
@@ -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
113
CLAUDE.md
@@ -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 F4a–F4d), 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 13–14 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
|
||||||
|
|||||||
385
dev-docs/PRIMEVUE_COMPONENTS.md
Normal file
385
dev-docs/PRIMEVUE_COMPONENTS.md
Normal 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 (F4a–F4d)
|
||||||
|
|
||||||
|
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 F4a–F4d), 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.
|
||||||
@@ -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 F4a–F4d 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 F4a–F4c, 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).
|
|
||||||
|
|||||||
Reference in New Issue
Block a user