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:
@@ -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 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.
|
||||
Reference in New Issue
Block a user