- BACKLOG: add 3 spawned follow-ups (EnergyDots NaN, DraggableBlock pointercancel, AD-3 Menubar a11y) - RFC-WS-GUI-REDESIGN-CREWLI-STARTER: mark Plan 3 complete with commit refs + DoD ledger - PRIMEVUE_COMPONENTS: v2 primitives registry (8 components), statusSeverity SoT, Menubar-wrap pattern - ARCH-TESTING: mount-helper type convention (Plan 3 codified, Plan 4 carry-over) - FRONTEND-TOOLING: scoped lint invocation note (DoD #13 root cause) - AppDialog.stories.ts: rename title to 'Shared/AppDialog' for sibling consistency
30 KiB
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. 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 (now a
deprecation stub; deletion in F6).
v2 primitives registry (Plan 3)
Landed by Plan 3 of RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md (commits
537ec098..0b19e785onmain). All eight live underapps/app/src/components-v2/shared/; each has a co-located<Name>.stories.ts(Storybook titleShared/<Name>) and a co-located__tests__/<Name>.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 <Menubar> chrome-wrap (Plan 3 Task 11, RFC AD-3).
Convention: wrap, don't rewrite. The hand-rolled top-bar chrome is
wrapped in <Menubar :model="[]"> 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 <ul role="menubar"> — tracked as A11Y-AD3-MENUBAR-EMPTY-MODEL in
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(commitdd45e899). Component is presentational; the parent owns all snap/lane/px math. Known gap: no@pointercancelhandler — a system-cancelled drag emits a spuriousdragend. Tracked asFRONTEND-DRAGGABLEBLOCK-POINTERCANCEL(blocked on an A2-contract amendment first).
1. Purpose and scope
This document defines:
- Which PrimeVue component (or Tailwind / native equivalent) replaces each Vuetify component used in the SPA today
- The Aura theme tokens that carry Crewli's brand (teal primary, dark mode)
- The canonical form pattern (
@primevue/forms+ Zod resolver +<FormField>wrapper) - The canonical DataTable pattern (lazy / virtual / column-template conventions)
- When to use the
pt(pass-through) API, Tailwind utilities, or Aura tokens for customization - 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)
- Per-component props/slot reference (see https://primevue.org/)
- The full 75-component Vuetify usage inventory (see MIGRATION-AUDIT-PRIMEVUE.md §1)
2. When to read this document
| Goal | Read |
|---|---|
| Picking a PrimeVue equivalent for a Vuetify usage during F4 | §3 |
| Customizing Crewli's brand colors / dark mode | §4 |
| Writing or migrating a form | §5 |
Migrating a <VDataTable> or <VDataTableServer> |
§6 |
Deciding between pt, Tailwind utility, or Aura token override |
§7, §8 |
| Working on a not-yet-migrated surface during F4 | §9 |
| Understanding the test-runtime story during migration | ARCH-TESTING.md §7 |
Cross-document relations:
- RFC-WS-FRONTEND-PRIMEVUE.md — full migration plan, sprint breakdown, architectural decisions (AD-1 through AD-12), risk register
- MIGRATION-AUDIT-PRIMEVUE.md — Vuetify usage inventory (§1),
@core/@layoutssurface area (§2), form-layer inventory (§4), DataTable inventory (§6) - 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 §7 — explains why Vuetify lives in test infrastructure during the migration
3. Component mapping by category
The Crewli SPA uses ~73 distinct Vuetify components today (52 with ≥3 uses,
20 long-tail with <3 uses). The tables below group them into six functional
categories. Each category is followed by one paragraph on the migration
spirit — direct prop translation is rarely possible, since PrimeVue's
customization model is pt + Tailwind, not Vuetify's deep-customization
slots.
For component-level prop and slot reference, follow the linked PrimeVue docs URL — this document does not duplicate them.
3.1 Form inputs
| Vuetify | PrimeVue | Reference | Notes |
|---|---|---|---|
VTextField |
InputText |
https://primevue.org/inputtext/ | density='compact' → size='small'; wrapped via <FormField> for label + error |
VTextarea |
Textarea |
https://primevue.org/textarea/ | Auto-resize via autoResize prop |
VSelect |
Select |
https://primevue.org/select/ | Renamed from Dropdown in v4; :items → :options, item-title → optionLabel, item-value → optionValue |
VAutocomplete |
AutoComplete |
https://primevue.org/autocomplete/ | Server-side: bind @complete instead of :items |
VCombobox |
AutoComplete with :multiple |
https://primevue.org/autocomplete/ | Free-text + dropdown; PrimeVue uses one component for both |
VCheckbox |
Checkbox |
https://primevue.org/checkbox/ | binary prop for single-value checkboxes |
VRadio / VRadioGroup |
RadioButton |
https://primevue.org/radiobutton/ | No group component; bind v-model directly to each RadioButton |
VSwitch |
ToggleSwitch |
https://primevue.org/toggleswitch/ | Renamed from InputSwitch in v4 |
VOtpInput |
InputOtp |
https://primevue.org/inputotp/ | |
VFileInput (rare) |
FileUpload |
https://primevue.org/fileupload/ | mode='basic' for inline; default for full uploader |
VColorPicker (2 uses) |
ColorPicker |
https://primevue.org/colorpicker/ | |
AppDateTimePicker (Flatpickr) |
unchanged | — | Flatpickr is retained per RFC AD-4; a thin DateTimePicker.vue wrapper composes Flatpickr inside <FormField> |
Migration spirit. Form inputs are the highest-coverage area; nearly
every input flows through <FormField> (§5). Most prop renames are
trivial (density → size, :items → :options). The bigger shift is
the validation contract: Vuetify's :rules array per field is replaced
by a single Zod schema at the form level, with field-level errors
surfaced through <FormField>. See §5 for the canonical pattern.
3.2 Layout
| Vuetify | Replacement | Notes |
|---|---|---|
VContainer |
<div class="max-w-screen-xl mx-auto px-4"> |
Tailwind utility composition; no PrimeVue equivalent |
VRow |
<div class="grid grid-cols-12 gap-4"> |
Tailwind grid |
VCol cols=N md=M |
<div class="col-span-N md:col-span-M"> |
Tailwind responsive prefixes |
VCard |
Card |
https://primevue.org/card/ — slots: #title, #subtitle, #content, #footer |
VCardTitle |
Card #title slot |
Compose inside Card |
VCardSubtitle |
Card #subtitle slot |
|
VCardText |
Card #content slot or <div class="p-6"> |
Most uses are pure padding |
VCardActions |
Card #footer slot |
|
VCardItem |
manual flex layout inside Card #content |
No direct equivalent |
VDivider |
Divider |
https://primevue.org/divider/ — :vertical → layout='vertical' |
VSheet |
<div> with Tailwind |
No equivalent component |
VSpacer |
<div class="flex-1"> or surrounding justify-between |
|
VApp, VMain, VAppBar, VFooter |
layout shell rewrites | See RFC AD-3; filenames preserved, contents replaced |
VLocaleProvider |
PrimeVue <ConfigProvider> (or app-level app.use(PrimeVue, { locale })) |
See RFC AD-6 |
Migration spirit. Most layout in PrimeVue is Tailwind, not components.
VRow/VCol translates 1:1 to a 12-column Tailwind grid. VCard is the
biggest semantic preservation — its slot model maps to PrimeVue's Card
slot model with minor renames. Plain <div> with Tailwind utilities
replaces VSheet, VSpacer, and most uses of VContainer.
3.3 Data display
| Vuetify | PrimeVue / Replacement | Reference | Notes |
|---|---|---|---|
VDataTable |
DataTable + Column |
https://primevue.org/datatable/ | See §6 |
VDataTableServer |
DataTable with :lazy="true" |
https://primevue.org/datatable/#lazy_load | See §6 |
VTable |
DataTable (no lazy) |
— | For static / pre-loaded tables |
VList |
Listbox (selectable) or DataView (cards) |
https://primevue.org/listbox/ | Context-dependent; selection lists use Listbox, card grids use DataView |
VListItem / VListItemTitle / VListItemSubtitle |
MenuItem (in menu) or <li> (in list) |
— | Inside a list, just compose the row markup directly |
VListSubheader |
<li class="text-sm text-surface-500"> |
— | Plain markup |
VChip |
Tag (preferred) or Chip |
https://primevue.org/tag/ | Tag for status badges; Chip for removable filters |
VAvatar |
Avatar |
https://primevue.org/avatar/ | variant='tonal' → :style="{ background, color }" (no built-in tonal) |
VImg |
<img> (native) |
— | No wrapper needed; use loading="lazy" |
VIcon |
<Icon name="tabler-..." /> |
— | Iconify-Tabler retained per RFC AD-5; thin wrapper at apps/app/src/components/Icon.vue over @iconify/vue's <Icon>. PrimeIcons is not installed |
VLabel |
<label> (native) or <FormField label="..."> |
— | |
VTimeline / VTimelineItem |
Timeline |
https://primevue.org/timeline/ | |
VBadge (1 use) |
Badge or OverlayBadge |
https://primevue.org/badge/ |
Migration spirit. VIcon is the most widespread component (267 uses)
and it does not become a PrimeVue component. The Iconify-Tabler
naming convention survives intact: call-sites use the existing
tabler-* string identifiers (e.g. tabler-arrow-right) and the new
apps/app/src/components/Icon.vue wrapper renders them through
@iconify/vue as real <svg> markup. UnoCSS-style i-tabler-* utility
classes were considered but not adopted (UnoCSS is not in the stack);
the wrapper preserves Crewli continuity. PrimeIcons is not installed;
do not introduce pi pi-* icon classes during F4 — they will not
render. Lists are the most context-dependent area: a selection list
uses Listbox, a card grid uses DataView, a navigation list uses
MenuItem/PanelMenu, and a generic display list is just <ul><li>.
3.4 Feedback
| Vuetify | PrimeVue | Reference | Notes |
|---|---|---|---|
VAlert |
Message |
https://primevue.org/message/ | type → severity; inline alerts only |
VSnackbar |
useToast() + <Toast> |
https://primevue.org/toast/ | One <Toast> mounted in App.vue; useNotificationStore wraps useToast() so call-site API stays stable per RFC AD-11 |
VProgressLinear |
ProgressBar |
https://primevue.org/progressbar/ | :indeterminate → mode='indeterminate' |
VProgressCircular |
ProgressSpinner |
https://primevue.org/progressspinner/ | |
VSkeletonLoader |
Skeleton |
https://primevue.org/skeleton/ | type='card' has no preset; compose with explicit width, height, shape |
Migration spirit. Message is for inline alerts (rendered in the
page). Toasts are global, mounted once in App.vue, and triggered via
useToast() (or the existing useNotificationStore which now wraps it).
Skeleton is more primitive than VSkeletonLoader — Vuexy's preset
shapes (type='card', type='article') need to be reconstructed from
explicit Skeleton elements; do this once as a project-owned
<SkeletonCard> utility if a shape is reused.
3.5 Navigation
| Vuetify | PrimeVue | Reference | Notes |
|---|---|---|---|
VTabs / VTab / VWindow / VWindowItem |
Tabs + TabList + Tab + TabPanels + TabPanel |
https://primevue.org/tabs/ | Single component family; controlled via :value |
VBtnToggle |
SelectButton |
https://primevue.org/selectbutton/ | |
VPagination (1 use) |
Paginator |
https://primevue.org/paginator/ | DataTable has built-in paginator; standalone use is rare |
VBreadcrumbs (none today) |
Breadcrumb |
https://primevue.org/breadcrumb/ | Reserved for future use |
VNavigationDrawer |
layout-shell custom + PanelMenu (or Drawer for mobile) |
https://primevue.org/panelmenu/, https://primevue.org/drawer/ | Sidebar nav is part of the shell rewrite per RFC AD-3; PanelMenu provides accordion behavior equivalent to VVerticalNavGroup |
Vuexy AppStepper |
Stepper + StepList + Step + StepPanels + StepPanel |
https://primevue.org/stepper/ | Multi-step wizards (e.g., public registration) |
Vuexy VerticalNavLayout |
layout shell rewrite | — | See RFC AD-3 |
Migration spirit. PrimeVue's Tabs family expands one Vuetify component
into five elements (Tabs / TabList / Tab / TabPanels / TabPanel).
This is more verbose at call sites but more flexible (e.g., tab list can
live separately from panels). The sidebar / nav shell is a rewrite, not
a component swap — Vuexy's @layouts/ system is replaced wholesale per
RFC AD-3, but route-level nav semantics (Pinia stores, route meta) stay
the same.
3.6 Overlays
| Vuetify | PrimeVue | Reference | Notes |
|---|---|---|---|
VDialog |
Dialog |
https://primevue.org/dialog/ | :max-width='500' → :style="{ width: '500px' }"; :fullscreen → :maximizable |
VMenu |
Menu (action lists) or Popover (rich content) |
https://primevue.org/menu/, https://primevue.org/popover/ | Choose Menu for MenuItem lists, Popover for arbitrary slot content |
VTooltip |
v-tooltip directive |
https://primevue.org/tooltip/ | Directive form (v-tooltip="'Hint'") is preferred over a wrapping component |
VOverlay |
Dialog with modal prop, or custom <div> |
— | Most overlay uses are modal dialogs |
VBottomSheet (none today) |
Drawer with position='bottom' |
https://primevue.org/drawer/ | Reserved; mobile bottom-sheets if needed |
VExpansionPanel(s) |
Accordion + AccordionPanel + AccordionHeader + AccordionContent |
https://primevue.org/accordion/ | Renamed in v4 from AccordionTab |
VExpandTransition / VScaleTransition |
Vue <Transition> (native) |
— | Native transitions; Vuetify-specific transitions removed |
Native confirm() / Vuexy ConfirmDialog |
useConfirm() + <ConfirmDialog> |
https://primevue.org/confirmdialog/ | Mounted once in App.vue per RFC AD-11 |
Migration spirit. Overlays are services in PrimeVue: useToast(),
useConfirm(), useDialog(). The pattern is one mount in App.vue,
many calls from anywhere. Dialog itself is still a component (not a
service) for non-confirm modals — keep <Dialog v-model:visible="...">
for create/edit forms.
4. Aura theme and Crewli tokens
PrimeVue 4 uses CSS custom properties under the --p-* prefix, generated
from a JS preset. Crewli's preset extends Aura and overrides only the
primary color (teal) and dark-mode contrast colors. Full token plan is in
RFC Appendix B; F3 implements
apps/app/src/plugins/primevue/theme.ts.
Key conventions:
- Primary color is Crewli teal (
#0D9394), defined asprimary.500in the preset - Dark mode uses
darkModeSelector: '.dark'(matches Vuexy's existing dark-class strategy on<html>); the same toggle that flips Vuetify's dark theme today flips PrimeVue's, so no UI change for users - Surface, formField, list, navigation, overlay, content tokens use Aura defaults (no override needed)
- Brand-color customization at the component level happens via the
ptAPI (§7), not via CSS overrides
Do not introduce --p-* overrides directly in SCSS during F4. If a
brand adjustment is needed, extend the preset in theme.ts so the
override is centralized and dark-mode-aware.
5. Forms pattern (canonical)
The form layer is @primevue/forms plus a Zod resolver, called through a
Crewli-owned <FormField> wrapper at
apps/app/src/components/forms/FormField.vue.
The full API specification — usage example, prop contract, slot
contract, useFormError 422 integration, error precedence — lives in
RFC-WS-FRONTEND-PRIMEVUE Appendix A.
That is the single source of truth; it is intentionally not duplicated
here so that revisions to the API only need to land in one place.
What this document encodes (Crewli conventions on top of the API):
- One Zod schema per form, defined at the top of the component or hoisted to
apps/app/src/schemas/[module].tsif reused. Schema field names mirror the backend Form Request field names exactly (snake_case) so 422 errors map back without translation. <FormField name="..." label="..." required>wraps every PrimeVue input. The wrapper'snameprop matches the Zod schema key and the input's:nameprop. Labels are Dutch by Crewli convention.- Server validation (422): catch in the
onSubmithandler, callapplyApiErrors(e.response.data.errors)fromuseFormError(formRef). Field-level messages are injected via<FormField :apiError="...">; manualerrorsrefs are not used. - No mixed validation. A single form is either fully Zod-validated (target) or fully
:rules-validated (legacy Vuetify). Do not introduce a hybrid. - Migration phase: existing forms keep their current
ref({}) + VForm + :rules + errorspattern until the surrounding surface is migrated. Per F4 sub-package, all forms on that surface flip to PrimeVue +<FormField>+ Zod resolver in one commit. See §9.
VeeValidate is not the form library. It was previously listed in
CLAUDE.md but never adopted; the form layer is @primevue/forms + Zod
resolver per RFC AD-1.
6. DataTable pattern
<DataTable> replaces both VDataTable (client-side) and
VDataTableServer (server-side). The full DataTable inventory is in
MIGRATION-AUDIT-PRIMEVUE §6;
the strategy is in RFC AD-7.
Conventions:
| Need | PrimeVue API | Notes |
|---|---|---|
| Server-side pagination/sort/filter | :lazy="true", :totalRecords, @page / @sort / @filter event handlers |
Replaces VDataTableServer's :items-length + @update:options |
| Custom cell rendering | <Column field="..."><template #body="slotProps">...</template></Column> |
Replaces Vuetify's #item.fieldName slot |
| Custom header rendering | <Column><template #header>...</template></Column> |
Replaces #header.fieldName |
| Expandable rows | expandedRows v-model + <template #expansion> |
https://primevue.org/datatable/#row_expand |
| Row selection | selection v-model + selectionMode="single" / "multiple" |
|
| Virtual scrolling (>1000 rows) | :virtualScrollerOptions="{ itemSize: 50 }" |
Replaces v-data-table-virtual |
| Empty / loading / error states | <template #empty>, :loading, surrounding error wrapper |
The three-state pattern (loading / error / empty) per CLAUDE.md UI rules is preserved |
Slot translation cheat sheet:
VDataTable slot |
DataTable equivalent |
|---|---|
#item.email |
<Column field="email"><template #body="{ data }">{{ data.email }}</template></Column> |
#header.actions |
<Column><template #header>Acties</template></Column> |
#expanded-row |
<template #expansion="{ data }">...</template> |
#no-data |
<template #empty>...</template> |
#loading |
:loading prop + loadingIcon |
#top |
sibling <div> above <DataTable>; not a built-in slot |
Per-page conventions:
- One
<DataTable>instance per page; no nested data tables - Server-side tables wire to a TanStack Query composable that returns
{ data, isLoading, isError, refetch }; the composable's params (page,perPage,sortBy,filters) are driven by reactive refs that PrimeVue's@page/@sort/@filterhandlers update - Column widths are not set in props for the common case; use
pton<Column>only when a fixed width is structurally needed - Selection state is a Pinia store only if it crosses components (e.g., bulk-action toolbar); inline selection stays local
7. Pass-through (pt) API and slot conventions
PrimeVue's primary customization mechanism is pt (pass-through), which
targets named DOM nodes inside a component and applies attributes/classes
to them.
<Button label="Opslaan" :pt="{
root: { class: 'shadow-md' },
label: { class: 'font-semibold' }
}" />
Decision matrix — when to reach for which tool:
| Need | Use |
|---|---|
| Layout, spacing, typography around a component | Tailwind utility classes on the wrapping <div> |
Internal styling of a component's DOM (e.g., the Button's root <button>) |
pt |
| Brand color, dark-mode-aware token | Aura preset extension in theme.ts (§4) |
| One-off override that doesn't fit the above | <style scoped> as last resort, with a comment explaining why |
Slot conventions vs. Vuetify:
- Most named slots map 1:1 in name (
#default,#footer), but slot prop shapes differ — always check the PrimeVue docs page for the exact slot signature - PrimeVue's slot system is generally narrower than Vuetify's; deep customization that Vuetify did via slots is done in PrimeVue via
pt - Default slots receive no implicit data in most components — read the doc page if a slot returns nothing useful
8. Tailwind v4 + PrimeVue integration
Tailwind v4 with the tailwindcss-primeui plugin is installed alongside
PrimeVue per RFC AD-12.
Configuration lives in F3.
Source-order conventions (CSS cascade matters):
- PrimeVue base styles (auto-injected by the plugin)
- Aura theme tokens (from
theme.ts) tailwindcss-primeuiexposes Aura tokens as Tailwind utilities (bg-primary,text-surface-500, etc.)- Tailwind utility classes in templates
- Project-scoped CSS (last resort)
When to reach for what:
| Need | Tool |
|---|---|
| Margin, padding, flex, grid, gap | Tailwind utility class |
| Text color, background, border using brand tokens | Tailwind utility from tailwindcss-primeui (bg-primary-500, text-surface-700) |
| Component-internal restyle | pt on the component |
| Brand-wide override | Aura preset extension |
Avoid mixing arbitrary HEX values into templates. If a color is reused twice, it belongs in the Aura preset; reference it via the surface / primary scale token instead of literal HEX.
9. Migration phase guidance
During F4 (sub-packages F4a–F4d), the SPA contains both Vuetify and PrimeVue components in the same build. The discipline that keeps this sane:
- Surface-level consistency. Within one route or component tree, use one framework. Never mix
<VTextField>and<InputText>in the same form, never mix<VBtn>and<Button>in the same toolbar. The unit of migration is a surface (a route or a feature folder), committed as a sub-package. - No back-porting. Do not migrate "just one component" inside a not-yet-migrated surface. The sub-package is the smallest migration unit.
- Pre-migration: follow Vuetify. If you are extending an un-migrated surface during F4 (e.g., bug fix on
/portal/*while F4b is in progress on the organizer root), follow the conventions visible in the surrounding code. The pre-F2VUEXY_COMPONENTS.mdcontent is reachable at git commit1c449ff6204cae6371da08c34ea8934d6b2ffcb8for reference. - Post-migration: follow PrimeVue. Any new code on a migrated surface follows this document. New surfaces (created during or after F4) start in PrimeVue.
- Tests during migration. Vuetify stays in test infrastructure for surfaces that haven't migrated yet. See ARCH-TESTING.md §7 for the test-runtime story;
setupFileflips per RFC AD-10. - When in doubt, prefer PrimeVue conventions. If a form on a migrated surface needs a component this document doesn't yet cover, find the closest PrimeVue equivalent on https://primevue.org/, implement it, and append a note in §3 (or open a tightly scoped PR to update this document). F4 sub-packages are expected to refine §3 — that is the explicit growth path.
- F6 is the cliff. After F6 (cleanup, ~0.5 day), Vuetify is removed from
package.jsonand<V*>tags fail to compile. No more parallel mode. Per RFC §9, F6 is the point of no return for rollback.
Anti-pattern: directly applying responsive visibility utility classes (md:hidden, lg:flex, etc.) to PrimeVue components. PrimeVue's component CSS may override due to cascade order. Always wrap in a plain element that owns the visibility class.
10. Cross-references and resources
External (PrimeVue ecosystem):
- PrimeVue components: https://primevue.org/
@primevue/forms(form layer): https://primevue.org/forms/@primeuix/themes(Aura preset, the maintained successor to@primevue/themes): https://primevue.org/theming/styled/tailwindcss-primeui(token bridge): https://github.com/primefaces/tailwindcss-primeui
Internal (Crewli docs):
- RFC-WS-FRONTEND-PRIMEVUE.md — full migration plan, AD-1 to AD-12, sprint breakdown, risk register, FormField API (Appendix A), Aura theme tokens (Appendix B), version pinning policy (Appendix C)
- MIGRATION-AUDIT-PRIMEVUE.md — F1 audit: Vuetify usage inventory (§1),
@core/@layoutssurface area (§2), Vuetify config and theme (§3), form-layer inventory (§4), page and route inventory (§5), DataTable inventory (§6) - ARCH-TESTING.md — test-tier decision tree; §7 covers the Vuetify-in-test-infra temporary state during the migration
- VUEXY_COMPONENTS.md — deprecation stub; pre-F2 content recoverable via git commit
1c449ff6204cae6371da08c34ea8934d6b2ffcb8 - CLAUDE.md — project-wide conventions; UI section now points here
This document is a foundation. F4 sub-packages will extend §3 with component-level migration notes as real surfaces are converted; expect diff PRs against this file alongside each sub-package commit.