781 lines
38 KiB
Markdown
781 lines
38 KiB
Markdown
# 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<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/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<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/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
|
|
<!-- 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).
|