Brainstorming outcome: pivot the PrimeVue redesign to use crewli-starter as the design source of truth, parallel /v2/ routes, PrimeVue-first fidelity, page-by-page cutover. Supersedes F4a-F4d of RFC-WS-FRONTEND-PRIMEVUE. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
17 KiB
Design — Crewli SPA GUI Redesign on the crewli-starter Design System
| Field | Value |
|---|---|
| Status | Design approved (brainstorming) — pending spec review |
| Date | 2026-05-15 |
| Author | Brainstorming session (Bert + Claude Code) |
| Supersedes | The F4a–F4d component-migration sub-packages of dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md |
| Next artifact | Implementation plan (writing-plans), then a new project RFC in dev-docs/ |
| Source design system | crewli-starter/ (sibling working directory) |
1. Context & problem
The Crewli SPA (apps/app/) is mid-migration from Vuetify+Vuexy to
PrimeVue 4.5 (Aura) + Tailwind v4. F3 (PrimeVue foundation, parallel-mode)
and F3.5 (a mockup-parity AppShell) have landed. Storybook 10 is wired
(PrimeVue + Tailwind) but contains only two smoke stories. The original
F4 plan was "translate existing Vuetify pages 1:1 to PrimeVue, preserve
UX."
A complete working prototype design system now exists in crewli-starter/:
~40 components, ~20 pages, a full app shell (sidebar / topbar / right
drawer / workspace switcher), and several rich modules (Timetable, Cue
editor, Section Builder). It already uses the same stack: PrimeVue 4.5,
Aura preset via @primeuix/themes, the Crewli teal token set, Tailwind v4,
Iconify-Tabler. It is pure JavaScript (no TypeScript).
Decision: pivot the redesign to use crewli-starter as the design
source of truth, building a new GUI in parallel with the existing one
and migrating page-by-page, rather than translating legacy Vuetify pages.
2. Decisions taken (this session)
- Cohabitation: parallel routes under
/v2/*. Old pages stay on existing routes. Cutover is route-by-route. - Fidelity rule: PrimeVue-first. Default to a stock PrimeVue
component styled with Tailwind +
ptAPI + Aura tokens. Port custom CSS only when no PrimeVue primitive fits or the visual is genuinely bespoke. Generic elements (KPI cards, error/empty states, status badges, etc.) are rebuilt on PrimeVue and accept the PrimeVue look — restyle freely. Pixel-perfect 1:1 is reserved for the genuinely bespoke set only (see §7). - Plan relationship: a new project RFC supersedes F4a–F4d. The F4 architectural decisions (PrimeVue + Tailwind + Aura + FormField + DataTable conventions) all still stand; only the page-migration strategy and design source change.
- Shell strategy: a new
AppShellV2.vue(andOrganizerLayoutV2.vue) ports the crewli-starter shell 1:1. The existingAppShell.vue/OrganizerLayout.vuestay untouched until/v2/supersedes them. - Folder convention:
pages-v2/+components-v2/mirror the existing structure. No per-fileV2suffix inside those folders (the folder name carries the distinction); the suffix is only on the two shell/layout files that sit beside their v1 siblings. - Sequencing approach: Approach A — foundation-first vertical slice. Build the minimum end-to-end first, then iterate page-by-page.
3. Folder structure & routing
apps/app/src/
├── pages-v2/ # NEW parallel page tree → /v2/*
├── components-v2/
│ ├── layout/ # AppSidebar, AppTopbar, SidebarNav, WorkspaceSwitcher, RightDrawer
│ ├── shared/ # PageHead, StatCard, StatusTag, StateBlock, EnergyDots, EnergyPicker, TagsInput, DraggableBlock
│ ├── templates/ # ListTemplate, FormTemplate, DetailTemplate, DashboardTemplate
│ ├── filters/ # SmartFilterBar + chip + popover + 5 editors + SmartListsRow
│ ├── timetable/ # (deferred — migrate with feature)
│ └── music/ # (deferred — migrate with feature)
│ # NOTE: no components-v2/forms/ — v2 reuses the existing
│ # apps/app/src/components/forms/FormField.vue. This folder is
│ # created ONLY if FormField provably forks (see §5).
├── layouts/
│ ├── OrganizerLayout.vue # EXISTING — untouched
│ ├── OrganizerLayoutV2.vue # NEW — wraps AppShellV2
│ └── components/
│ ├── AppShell.vue # EXISTING — untouched
│ └── AppShellV2.vue # NEW — ports crewli-starter shell 1:1
├── stories/ # EXPANDED — see §6
├── stores/ # + useWorkspaceStore
├── composables/ # + useRightDrawer
└── types/v2/ # shared v2 types
Routing: unplugin-vue-router generates routes from pages-v2/;
they are pinned under /v2/* (route prefix/meta). vite-plugin-vue-meta-layouts
selects OrganizerLayoutV2 for the pages-v2/* tree. No conditional
logic inside layout files — each page tree picks its own layout. The old
layout stays inert (zero regression risk on un-migrated pages).
Cutover convention (per page): when a v2 page is approved to replace
its v1 counterpart, move pages-v2/X.vue → pages/X.vue (overwrite),
rewrite internal links /v2/X → /X, delete now-unused v1 components,
commit. Router auto-updates.
4. AppShellV2 composition
layouts/components/AppShellV2.vue # Tailwind grid: sidebar | (topbar + content) | rightDrawer
# (V2 suffix: sits beside v1 AppShell.vue)
└── composes components-v2/layout/:
├── AppSidebar.vue # permanent rail (≥lg); PrimeVue Drawer overlay (<lg)
│ ├── SidebarHeader.vue # logo + collapse toggle
│ ├── SidebarNav.vue # grouped nav + active highlight
│ └── WorkspaceSwitcher.vue # bottom switcher (custom visual + PrimeVue Popover)
├── AppTopbar.vue # Breadcrumb (PrimeVue) + actions (Button/Avatar/Menu/OverlayBadge)
└── RightDrawer.vue # PrimeVue Drawer position=right + slot scaffold
(No V2 suffix on the components-v2/layout/ files — the folder carries
the distinction per decision 5. Only AppShellV2.vue and
OrganizerLayoutV2.vue carry the suffix, since they sit beside their v1
siblings in layouts/.)
| Piece | Implementation | Rationale |
|---|---|---|
| Outer container | Tailwind grid (custom) | Matches crewli-starter .app; no PrimeVue equivalent |
| Sidebar desktop | <aside> + Tailwind (custom) |
Permanent rail; no PrimeVue primitive |
| Sidebar mobile | PrimeVue Drawer |
Correct off-canvas primitive |
| Sidebar nav rows | <button> + Tailwind + Icon |
crewli-starter spec too bespoke for PanelMenu |
| Breadcrumb | PrimeVue Breadcrumb |
Standard fits |
| Topbar actions | PrimeVue Button (text) + OverlayBadge |
Standard |
| User menu | PrimeVue Avatar + Menu |
Standard |
| RightDrawer | PrimeVue Drawer + custom slot scaffold |
Plumbing free, content bespoke |
| WorkspaceSwitcher | custom visual + PrimeVue Popover |
See §7.4 |
State contract (replaces crewli-starter provide/inject):
useWorkspaceStore(Pinia): workspace, sidebar collapsed/expanded, theme, density.useRightDrawer()composable:open(component, props),close().vue-router: replacesprovide('navigate', …).<html data-theme>/<html data-density>mechanism retained (composes with AuradarkModeSelector); v2 bypasses VuexyuseSkins.ts.
F3.5's AppShell features (notification bell, help, breadcrumb,
org-switcher card) are not lost — the underlying state wires up
identically; only the visual treatment moves to the crewli-starter look.
5. Component conventions
Decision tree: (1) PrimeVue component exists → use it raw + Tailwind +
pt. (2) Same composition 3+ times → wrap it. (3) Genuinely outside
PrimeVue → custom in components-v2/.
| Category | Examples | Rule |
|---|---|---|
| Raw PrimeVue | Button, InputText, Tag, Card, Divider, Drawer, Dialog, Avatar, Breadcrumb, DataTable, Toast, Skeleton, Message | Import directly; Tailwind + pt; Aura tokens carry brand |
| Wrappers | FormField (reuse existing), Icon (reuse existing), AppDialog, DataTableLazy | Wrap when composition repeats 3+ times |
| Custom | WorkspaceSwitcher, DraggableBlock, EnergyDots, EnergyPicker, RightDrawer scaffold, the timetable/music modules | Hand-built; scoped CSS only when Tailwind can't express it |
- No
V2suffix on files insidepages-v2//components-v2/(folder carries it). Suffix only onAppShellV2.vue/OrganizerLayoutV2.vue. - Forms: reuse existing
apps/app/src/components/forms/FormField.vue(already PrimeVue + Zod). No fork unless it provably diverges. - Icons: reuse existing
apps/app/src/components/Icon.vue. crewli-starter<Icon icon="tabler:x" />→ project<Icon name="tabler-x" />(colon→hyphen is the only delta). - TypeScript conversion:
<script setup>→<script setup lang="ts">;defineProps({...})→defineProps<{...}>()+withDefaults;defineEmits([...])→ typeddefineEmits<{...}>();inject(...)→ composable/store; component-local types inline, shared types intypes/v2/. - Boundaries: existing
eslint-plugin-boundaries10-zone matrix applies to v2 folders identically; violations stay errors.
6. Storybook organization
Custom components are first-class in Storybook exactly like PrimeVue
ones. crewli-starter ComponentsPage.vue (~80 PrimeVue components) is
the source of truth for the standard-component catalog.
Story placement: custom/wrapper stories co-located next to the
.vue (moves with the file at cutover); PrimeVue standard catalog and
Foundations centralized under stories/.
Story tree: Foundations/ (Color, Typography, Spacing, Icons, Dark
mode, Density) · PrimeVue/ (~80, grouped as in ComponentsPage) ·
Layout/ (AppSidebar, AppTopbar, WorkspaceSwitcher, RightDrawer,
AppShell) · Shared/ (StatusTag, EnergyDots, StatCard, PageHead,
StateBlock, DraggableBlock) · Forms/ (FormField) · Templates/
(List, Form, Detail, Dashboard, StateBlock) · Modules/ (later:
Timetable, Cue, SectionBuilder).
Conventions (binding): CSF3; tags: ['autodocs']; every prop wired
as an argTypes control; each meaningful state is its own named export
(not a kitchen-sink story); a11y addon runs per story.
Global toolbar (.storybook/preview.ts): theme toggle (light/dark,
sets data-theme) + density toggle (comfortable/compact, sets
data-density) — every story verifiable in all 4 theme×density combos.
7. Bespoke component specs (1:1 fidelity)
7.1 DraggableBlock — fully custom
Extracted from inline TimetableGrid markup so timetable + cue editor
render identically.
defineProps<{
line1Left: { tag?: { label: string; severity?: TagSeverity }; text?: string }
line1Right?: { tag?: { label: string; severity?: TagSeverity }; pill?: string }
line2Left?: string
line2Right?: { progress: number } | null
selected?: boolean
dragging?: boolean
density?: 'compact' | 'regular' | 'comfy' // 56 / 64 / 76 px row
}>()
defineEmits<{
click: []
dragstart: [e: PointerEvent]
dragend: [delta: { x: number; y: number }]
}>()
Internals: PrimeVue Tag + PrimeVue ProgressBar; 2-line flex grid in
scoped CSS (no Tailwind utility expresses crewli-starter spacing — the
justified "port custom CSS" case). Drag = crewli-starter native pointer
drag (coupled to timeline px/min math); component emits
dragstart/dragend, parent owns positioning. vuedraggable not used
(wrong abstraction for free-position blocks). Stories: ArtistBlock,
CueBlock, WithProgress, Selected, Dragging, DarkMode, density variants.
7.2 AppDialog — PrimeVue Dialog + slot scaffold
PrimeVue Dialog provides scrim / focus-trap / escape / teleport / a11y
(replaces crewli-starter AppModal hand-rolled plumbing). Slot contract
preserved: title, sub, #tabs (optional), default = scrollable body,
#footer (rendered only if provided). :pt matches crewli-starter
header/body/footer padding + body-scroll behaviour.
7.3 RightDrawer — PrimeVue Drawer + scaffold
PrimeVue Drawer position="right". Slot scaffold: header (close +
title + #actions), scrollable default body, flush prop (removes body
padding for edge-to-edge content). Driven by useRightDrawer().
7.4 WorkspaceSwitcher — custom visual + PrimeVue Popover
Bespoke visual (gradient-square logo with initials, name/sub stack,
chevron) stays 1:1 custom CSS. Dropdown plumbing (open/close,
click-outside, positioning) replaced by PrimeVue Popover (drops manual
mousedown listener). Panel content (Workspaces header + Manage link,
list with gradient logos + current checkmark, footer New workspace /
Invite) stays custom markup. Data shape
{ initials, name, sub, gradient: [from, to] } backed by
useWorkspaceStore.
Pattern established: overlay/focus/escape/click-outside behaviour
comes free from PrimeVue Dialog/Drawer/Popover; only bespoke
visual content keeps custom CSS.
8. Additional secured elements
Tier-1 primitives (PrimeVue-based, secure now):
| Element | v2 implementation | Custom CSS |
|---|---|---|
| StatCard | PrimeVue Card + Icon + trend Tag |
none (replaces AppKpiCard) |
| StatusTag | PrimeVue Tag + severity map (+ dot via pt) |
none |
| StateBlock | Skeleton (loading) · Message+retry (error) · Card+Button (empty) |
none (replaces ErrorHeader / AppLoadingIndicator) |
| TagsInput | PrimeVue AutoComplete multiple + typeahead |
none (drop custom) |
| PageHead | thin Tailwind flex (title/sub/#actions) |
none (pure layout) |
| EnergyDots / EnergyPicker | 5-dot meter — no PrimeVue primitive (Rating is stars, wrong visual) |
minimal, justified |
Tier-2 — Smart Filter subsystem (secure generic version now):
SmartFilterBar + FilterChip + FilterPopover + AddFilterMenu +
5 editors (Text / NumberRange / MultiSelect / EnumGrid / Tag) +
SmartListsRow + useSmartFilters. Music-specific QueryChipsBar is a
variant — deferred with the music module.
Tier-3 — Template layer (highest consistency lever):
ListTemplate (PageHead + SmartFilterBar + DataTable + states),
FormTemplate (PageHead + Form/FormField + footer actions),
DetailTemplate (PageHead + Tabs + RightDrawer hook),
DashboardTemplate (StatCard grid + widget slots),
StateBlock (the CLAUDE.md mandatory loading/error/empty three-state in
one place). Pages-v2 compose a template instead of hand-rolling layout.
Tier-4 — Deferred (migrate with owning page): Timetable suite
(TimetableGrid 947, TimetableTab 687, TimetableModals,
ParkingColumn, TimetablePopover), Music/Cue suite
(CueTimelineEditor 889, SectionBuilderPro 2084, SongDrawerBody,
SongFilesPanel). DraggableBlock is the one primitive extracted early.
Reconciliation: v2 uses crewli-starter-derived versions; existing
AppKpiCard / AppSearchHeader / AppLoadingIndicator / ErrorHeader
stay frozen on v1 surfaces and die at cutover. No back-porting either
direction.
9. Foundation sprint deliverables
- Routing + layout scaffold:
OrganizerLayoutV2,AppShellV2,/v2/*mount,useWorkspaceStore,useRightDrawer(), theme/density wiring, one emptypages-v2/dashboard.vueproving boot. - Shell pieces ported 1:1 (TS): AppSidebar, AppTopbar, SidebarNav, WorkspaceSwitcher, RightDrawer, AppDialog.
- Tier-1 primitives (PrimeVue-based per §8).
- Template layer: List / Form / Detail / Dashboard / StateBlock.
- Storybook: global theme/density toolbar; story tree; stories for
every Tier-1 primitive + 4 shell components + each template +
foundations. PrimeVue standard catalog (~80) split across foundation
- Page-1 sprints.
- New project RFC in
dev-docs/superseding F4a–F4d; pointer added fromPRIMEVUE_COMPONENTS.md.
10. Sequencing after foundation
- Page-1 sprint: events list end-to-end via
ListTemplate(exercises SmartFilterBar + lazy DataTable + RightDrawer — highest pattern-validation value). Smart Filter subsystem secured here. Finish PrimeVue standard catalog stories in parallel. - Subsequent sprints: one page tree at a time (dashboard → settings
→ events detail → members → platform → portal). Each: build under
pages-v2/, add only needed components/stories, sign-off, cutover commit. - Domain modules migrate with their owning page.
- Final cutover: rename
pages-v2/→pages/,components-v2/→components/, drop*V2suffixes, delete dead v1 shell + Vuetify, one closure commit.
11. Quality gates (per sprint)
pnpm test, pnpm typecheck, pnpm lint (boundaries matrix on v2
folders), Storybook a11y panel clean on new stories, visual sign-off
against crewli-starter. No any. Existing test count must not regress.
12. Non-goals
- No backend changes.
- No deletion of v1 code until per-page cutover (or final cutover).
- No back-porting v2 components into v1 surfaces (and vice versa).
- Domain modules (Timetable, Cue, SectionBuilder) are not foundation work — they migrate with their owning page.
- Flatpickr / vue-i18n / DatePicker decisions inherit from RFC-WS-FRONTEND-PRIMEVUE unchanged.
- No PrimeVue Pro / template purchase.