Files
crewli/dev-docs/PRIMEVUE_COMPONENTS.md

397 lines
26 KiB
Markdown
Raw Blame History

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