Files
crewli/dev-docs/superpowers/specs/2026-05-15-crewli-starter-gui-redesign-design.md
bert.hausmans 890bcc88cb 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>
2026-05-15 22:54:32 +02:00

17 KiB
Raw Blame History

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.vuepages/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.

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.