# 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. > **GUI redesign:** the page-migration phase is governed by > `dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md` (supersedes F4a–F4d). > This component reference still applies to all v2 work. **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). --- ## v2 primitives registry (Plan 3) > Landed by Plan 3 of [RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md](./RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md) > (commits `537ec098..0b19e785` on `main`). All eight live under > `apps/app/src/components-v2/shared/`; each has a co-located > `.stories.ts` (Storybook title `Shared/`) and a co-located > `__tests__/.spec.ts`. PrimeVue-first per AD-G3; scoped CSS only > where the spec justifies it (EnergyDots/EnergyPicker §8, DraggableBlock §7.1). | Component | Purpose | Props (summary) | |---|---|---| | `StatusTag` | PrimeVue `Tag` whose `severity` is resolved from a status string via the SoT map (never inlined) | `status`, `label?`, `dot?` | | `StatCard` | KPI tile (PrimeVue `Card` + `Icon` + optional trend) — replaces v1 `AppKpiCard` | `icon`, `label`, `value`, `trend?`, `trendDir?` | | `PageHead` | Thin Tailwind flex page header (title / sub / `#actions` slot); no PrimeVue dependency | `title`, `sub?` (+ `#actions` slot) | | `StateBlock` | Mandatory loading / error / empty three-state wrapper + success passthrough | `state`, `errorMessage?`, `emptyMessage?`, `actionLabel?`, `retryLabel?` (emits `retry`, `action`) | | `TagsInput` | Multi-tag input — re-implementation on PrimeVue `AutoComplete` (multiple + typeahead); lowercase-dedupe, comma/Enter split, 5-suggestion cap | `modelValue?: string[]`, `suggestions?: string[]`, `placeholder?` | | `EnergyDots` | Read-only 5-dot energy meter (scoped CSS, §8-justified) | `value?: number`, `lg?: boolean` | | `EnergyPicker` | Interactive 5-step energy picker (clicking the current value resets to 0) | `modelValue?: number` | | `DraggableBlock` | §7.1 canonical 2-line draggable block (PointerEvent drag; parent owns all positioning) | `line1Left`, `line1Right?`, `line2Left?`, `line2Right?`, `selected?`, `dragging?`, `density?` (emits `click`, `dragstart`, `dragend`) | **`statusSeverity` — single source of truth.** `apps/app/src/components-v2/shared/statusSeverity.ts` is the canonical map from the five backend-mirrored status enums (`ShiftAssignmentStatus`, `ArtistEngagementStatus`, `PaymentStatus`, `PersonStatus`, `MatchStatus`) to PrimeVue `Tag` severity literals. `StatusTag` resolves through it and never inlines a severity. The map is bidirectionally test-locked by `apps/app/tests/unit/utils/statusSeverity.consistency.spec.ts` (every enum value maps to a severity; every map key is reachable from an enum). **Changes to the map are RFC-level** (design spec §8) — not an ad-hoc per-component decision. **AppTopbar `` chrome-wrap (Plan 3 Task 11, RFC AD-3).** Convention: *wrap, don't rewrite*. The hand-rolled top-bar chrome is wrapped in `` with the breadcrumb in `#start` and the search / notifications / user cluster in `#end`. Menubar carries no menu items — it is the AD-3-mandated chrome shell only; its `pt` slots inherit the original layout classes verbatim so the wrap is visually identical to the pre-wrap (Plan-2) bar (human pixel-parity check at the HITL gate). **Caveat:** `:model="[]"` renders an empty `
    ` — tracked as `A11Y-AD3-MENUBAR-EMPTY-MODEL` in [`BACKLOG.md`](./BACKLOG.md) § Technische schuld; resolve at the F5 a11y batch before reusing this pattern broadly. **DraggableBlock drag-model contract.** The canonical PointerEvent drag model — reconciled from the two crewli-starter consumers (TimetableGrid + CueTimelineEditor) — is fixed by the A2 contract doc `dev-docs/superpowers/plans/2026-05-17-gui-redesign-tier1-primitives-DRAGGABLEBLOCK-CONTRACT.md` (commit `dd45e899`). Component is presentational; the parent owns all snap/lane/px math. **Known gap:** no `@pointercancel` handler — a system-cancelled drag emits a spurious `dragend`. Tracked as `FRONTEND-DRAGGABLEBLOCK-POINTERCANCEL` (blocked on an A2-contract amendment first). --- ## 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 + `` 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 `` or `` | §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 `` 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 `` | **Migration spirit.** Form inputs are the highest-coverage area; nearly every input flows through `` (§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 ``. See §5 for the canonical pattern. ### 3.2 Layout | Vuetify | Replacement | Notes | |---|---|---| | `VContainer` | `
    ` | Tailwind utility composition; no PrimeVue equivalent | | `VRow` | `
    ` | Tailwind grid | | `VCol cols=N md=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 `
    ` | 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` | `
    ` with Tailwind | No equivalent component | | `VSpacer` | `
    ` 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 `` (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 `
    ` 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 `
  • ` (in list) | — | Inside a list, just compose the row markup directly | | `VListSubheader` | `
  • ` | — | 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` | `` (native) | — | No wrapper needed; use `loading="lazy"` | | `VIcon` | `` | — | **Iconify-Tabler retained** per [RFC AD-5](./RFC-WS-FRONTEND-PRIMEVUE.md#ad-5-icons--iconify-tabler-retained-primeicons-not-installed); thin wrapper at `apps/app/src/components/Icon.vue` over `@iconify/vue`'s ``. PrimeIcons is **not installed** | | `VLabel` | `