docs: add GUI redesign design spec (crewli-starter as design source)

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>
This commit is contained in:
2026-05-15 22:54:32 +02:00
parent 524d0ee586
commit 890bcc88cb

View File

@@ -0,0 +1,338 @@
# 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 F4aF4d 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 F4aF4d. 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 F4aF4d; 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.