From 971adc040a981356fd07ad001446ee4af229784d Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Tue, 14 Apr 2026 01:23:47 +0200 Subject: [PATCH] docs: generate Vuexy component registry for frontend reference Co-Authored-By: Claude Opus 4.6 (1M context) --- dev-docs/VUEXY_COMPONENTS.md | 780 +++++++++++++++++++++++++++++++++++ 1 file changed, 780 insertions(+) create mode 100644 dev-docs/VUEXY_COMPONENTS.md diff --git a/dev-docs/VUEXY_COMPONENTS.md b/dev-docs/VUEXY_COMPONENTS.md new file mode 100644 index 00000000..f7ecf511 --- /dev/null +++ b/dev-docs/VUEXY_COMPONENTS.md @@ -0,0 +1,780 @@ +# Vuexy Component Registry + +Reference document for all frontend work. Consult BEFORE writing any new component. +Generated from Vuexy template v10.11.1 (`resources/vuexy-admin-v10.11.1/vue-version/`) and Crewli codebase scan. + +--- + +## 1. @core Components + +Source: `resources/vuexy-admin-v10.11.1/vue-version/typescript-version/full-version/src/@core/components/` +Copied into: `apps/app/src/@core/components/` and `apps/portal/src/@core/components/` + +### Form Element Wrappers (`@core/components/app-form-elements/`) + +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 | + +**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 + +### Portal Layouts (`apps/portal/src/layouts/`) + +| Layout | File | Used In | Purpose | +|--------|------|---------|---------| +| **portal** | `layouts/portal.vue` | All authenticated portal pages | Custom navbar with two modes + mobile drawer | +| **blank** | `layouts/blank.vue` | Login, registration, password reset | Minimal layout | +| **default** | `layouts/default.vue` | Fallback (delegates to portal) | Wrapper | + +**Portal layout** is a **custom implementation** — does NOT use @layouts VerticalNavLayout. Features: +- **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 +- Container max-width: 1440px, navbar height: 64px + +### Route Meta for Layout Selection + +```typescript +definePage({ + meta: { + layout: 'blank' | 'default' | 'portal', + public: true, // skip auth guard + requiresAuth: false, // optional auth + navMode: 'event' | 'platform', // portal only + navTitle: 'Page Title', // portal platform mode only + navActiveLink: 'events', // highlight nav item in app + } +}) +``` + +--- + +## 3. Established Page Patterns + +### 3.1 List Page Pattern (Table + Filters + KPI Cards + Detail Drawer) + +**Structure:** KPI metric cards → filter row → VDataTableServer → detail drawer + form dialogs +**Crewli reference:** `apps/app/src/pages/events/[id]/persons/index.vue` +**Vuexy template reference:** `resources/.../src/pages/apps/user/list/index.vue` + +**Components used:** +- VRow/VCol (cols="12" sm="6" md="3") for 4 metric cards at top +- VCard + VAvatar (variant="tonal", rounded, size=44) + VIcon (size=28) for each metric +- AppSelect for filter dropdowns in a row +- VDataTableServer (or VDataTable) with custom column templates via `#item.[column]` +- VChip for status badges and type labels (with `:color` from status color map) +- VBtn for row actions (approve, edit, delete) +- PersonDetailPanel (VNavigationDrawer) for row-click detail view +- CreatePersonDialog / EditPersonDialog (VDialog) for CRUD +- VSnackbar for success/error notifications +- VSkeletonLoader for loading state +- VAlert (type="error") with retry button for error state + +**KPI tiles pattern:** +```typescript +const tiles = computed(() => [ + { title: 'Totaal', value: meta.value?.total ?? 0, icon: 'tabler-users', color: 'primary' }, + { title: 'Goedgekeurd', value: meta.value?.total_approved ?? 0, icon: 'tabler-circle-check', color: 'success' }, + { title: 'Wachtend', value: meta.value?.pending ?? 0, icon: 'tabler-clock', color: 'warning' }, + { title: 'Afgewezen', value: meta.value?.rejected ?? 0, icon: 'tabler-x', color: 'error' }, +]) +``` + +**Data fetching pattern:** +```typescript +const { data, isLoading, isError, refetch } = usePersonList(eventId, filters) +const { mutate: approve } = useApprovePerson(eventId) +const { mutate: deletePerson } = useDeletePerson(eventId) +``` + +--- + +### 3.2 Detail Panel Pattern (Right Drawer with Tabs) + +**Structure:** VNavigationDrawer (temporary, right) → header with avatar + status chips → VTabs → tabbed content +**Crewli reference:** `apps/app/src/components/persons/PersonDetailPanel.vue` +**Vuexy template reference:** `resources/.../src/views/apps/user/list/AddNewUserDrawer.vue` (drawer pattern) + +**Components used:** +- VNavigationDrawer (`location="end"`, `temporary`, `width=480`) +- VAvatar (size=56, variant="tonal", color="primary") with initials +- VChip for status/type badges +- VBtn icon variant (variant="text", size="small") for edit/close actions +- VTabs + VTabsWindow + VTabsWindowItem for tabbed content +- VList + VListItem with `#prepend` slot (VIcon) for data display +- VTextarea for inline-editable admin notes (save on blur) +- VSwitch for toggle fields +- VSkeletonLoader (type="article") for loading state +- VDivider between sections + +**Props pattern:** +```typescript +defineProps<{ eventId: string; orgId: string; personId: string | null }>() +const modelValue = defineModel({ 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>({}) +const refVForm = ref() +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 + +``` + +**v-model pattern for dialog visibility:** +```typescript +const modelValue = defineModel({ 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 + + + +
+ + + +

{{ value }}

+
+

{{ title }}

+

+ {{ secondary }} + {{ subtitle }} +

+
+
+
+``` + +--- + +### 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 + + + Status wijzigen + + Weet je zeker dat je de status wilt wijzigen naar {{ target }}? + + + + Annuleren + Bevestigen + + + +``` + +**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/portal/src/pages/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/portal/src/pages/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 Form with VeeValidate + Zod) +**Reference:** `apps/portal/src/pages/register/[eventSlug].vue` +- VForm with VeeValidate field binding + Zod schemas from `@/schemas/` +- Conditional form fields based on event configuration +- Real-time email duplicate checking +- Password creation for new users + +#### Auth Pages (Centered Card) +**Reference:** `apps/portal/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` + API error mapping | (VeeValidate + Zod only in portal registration) | +| 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` 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/portal/src/components/`) + +| 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 + + + + + + Kon data niet laden. + + + + + + +

Geen resultaten gevonden.

+ Toevoegen +
+ + + +``` + +**Loading skeleton for grids** (match the data layout): +```vue + + + + + +``` + +--- + +## 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 + + + + + +``` + +### 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).