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>
339 lines
17 KiB
Markdown
339 lines
17 KiB
Markdown
# 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)
|
||
|
||
1. **Cohabitation:** parallel routes under `/v2/*`. Old pages stay on
|
||
existing routes. Cutover is route-by-route.
|
||
2. **Fidelity rule:** PrimeVue-first. Default to a stock PrimeVue
|
||
component styled with Tailwind + `pt` API + 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).
|
||
3. **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.
|
||
4. **Shell strategy:** a new `AppShellV2.vue` (and `OrganizerLayoutV2.vue`)
|
||
ports the crewli-starter shell 1:1. The existing `AppShell.vue` /
|
||
`OrganizerLayout.vue` stay untouched until `/v2/` supersedes them.
|
||
5. **Folder convention:** `pages-v2/` + `components-v2/` mirror the
|
||
existing structure. No per-file `V2` suffix 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.
|
||
6. **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`: replaces `provide('navigate', …)`.
|
||
- `<html data-theme>` / `<html data-density>` mechanism retained
|
||
(composes with Aura `darkModeSelector`); v2 bypasses Vuexy `useSkins.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 `V2` suffix** on files inside `pages-v2/` / `components-v2/` (folder
|
||
carries it). Suffix only on `AppShellV2.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([...])` → typed `defineEmits<{...}>()`; `inject(...)` →
|
||
composable/store; component-local types inline, shared types in
|
||
`types/v2/`.
|
||
- **Boundaries:** existing `eslint-plugin-boundaries` 10-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.
|
||
|
||
```ts
|
||
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
|
||
|
||
1. Routing + layout scaffold: `OrganizerLayoutV2`, `AppShellV2`, `/v2/*`
|
||
mount, `useWorkspaceStore`, `useRightDrawer()`, theme/density wiring,
|
||
one empty `pages-v2/dashboard.vue` proving boot.
|
||
2. Shell pieces ported 1:1 (TS): AppSidebar, AppTopbar, SidebarNav,
|
||
WorkspaceSwitcher, RightDrawer, AppDialog.
|
||
3. Tier-1 primitives (PrimeVue-based per §8).
|
||
4. Template layer: List / Form / Detail / Dashboard / StateBlock.
|
||
5. 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.
|
||
6. New project RFC in `dev-docs/` superseding F4a–F4d; pointer added
|
||
from `PRIMEVUE_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 `*V2` suffixes, 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.
|