docs: apply spec review round 1 corrections (GUI redesign design)
All corrections audited against the codebase: - TEST-INFRA-001 verified Resolved; add §13 testing strategy - §3 specify exact routesFolder + definePage layout meta convention - §5 boundaries claim corrected; add §14 zone/matrix extension - §4 drop useWorkspaceStore (dup) → reuse auth/org stores + useShellUiStore - §12 explicit portal scope (/portal/*); §10 SmartFilter own sub-sprint Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,21 @@
|
|||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Status** | Design approved (brainstorming) — pending spec review |
|
| **Status** | Design approved; spec review round 1 corrections applied — pending re-approval |
|
||||||
| **Date** | 2026-05-15 |
|
| **Date** | 2026-05-15 |
|
||||||
| **Author** | Brainstorming session (Bert + Claude Code) |
|
| **Author** | Brainstorming session (Bert + Claude Code) |
|
||||||
| **Supersedes** | The F4a–F4d component-migration sub-packages of `dev-docs/RFC-WS-FRONTEND-PRIMEVUE.md` |
|
| **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/` |
|
| **Next artifact** | Implementation plan (writing-plans), then a new project RFC in `dev-docs/` |
|
||||||
| **Source design system** | `crewli-starter/` (sibling working directory) |
|
| **Source design system** | `crewli-starter/` (sibling working directory) |
|
||||||
|
|
||||||
|
> **Spec review round 1 (2026-05-15) — corrections applied, all audited against the codebase:**
|
||||||
|
> 1. **TEST-INFRA-001** (blocking): audited → ✅ Resolved; new §13 Testing strategy added (Playwright CT gate kept, re-baselined against crewli-starter, Storybook a11y complementary).
|
||||||
|
> 2. **Layout-selection mechanism** (blocking): §3 now specifies the exact `definePage({ meta: { layout: 'OrganizerLayoutV2' } })` convention + `routesFolder` vite config (both audited).
|
||||||
|
> 3. **eslint-plugin-boundaries** (blocking): §5 corrected (was factually wrong); new §14 specifies the `components-v2`/`pages-v2`/`components-foundation` zones + no-back-port asymmetry.
|
||||||
|
> 4. `useWorkspaceStore` → reuse `useAuthStore`/`useOrganisationStore` for org data + new `useShellUiStore` for sidebar/theme/density only (§4); `provide`/`inject` substitution is now a mandatory per-port rule.
|
||||||
|
> 5. Portal scope made explicit (§12, in scope, own later sprint, `/portal/*` not `/p/*`).
|
||||||
|
> 6. Smart Filter promoted to its own sub-sprint (§10).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Context & problem
|
## 1. Context & problem
|
||||||
@@ -78,16 +86,41 @@ apps/app/src/
|
|||||||
│ ├── AppShell.vue # EXISTING — untouched
|
│ ├── AppShell.vue # EXISTING — untouched
|
||||||
│ └── AppShellV2.vue # NEW — ports crewli-starter shell 1:1
|
│ └── AppShellV2.vue # NEW — ports crewli-starter shell 1:1
|
||||||
├── stories/ # EXPANDED — see §6
|
├── stories/ # EXPANDED — see §6
|
||||||
├── stores/ # + useWorkspaceStore
|
├── stores/ # + useShellUiStore (sidebar/theme/density ONLY —
|
||||||
|
│ # org/context data reuses existing stores, §4)
|
||||||
├── composables/ # + useRightDrawer
|
├── composables/ # + useRightDrawer
|
||||||
└── types/v2/ # shared v2 types
|
└── types/v2/ # shared v2 types
|
||||||
```
|
```
|
||||||
|
|
||||||
**Routing:** `unplugin-vue-router` generates routes from `pages-v2/`;
|
**Routing (exact mechanism — audited 2026-05-15).** `vite.config.ts`
|
||||||
they are pinned under `/v2/*` (route prefix/meta). `vite-plugin-vue-meta-layouts`
|
currently calls `VueRouter({ getRouteName: … })` with **no `routesFolder`**,
|
||||||
selects `OrganizerLayoutV2` for the `pages-v2/*` tree. No conditional
|
so it defaults to `src/pages` only — `pages-v2/` would generate zero
|
||||||
logic inside layout files — each page tree picks its own layout. The old
|
routes without a config change. Required change (foundation deliverable
|
||||||
layout stays inert (zero regression risk on un-migrated pages).
|
1, §9):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
VueRouter({
|
||||||
|
routesFolder: [
|
||||||
|
{ src: 'src/pages' }, // unchanged, no prefix
|
||||||
|
{ src: 'src/pages-v2', path: 'v2/' }, // new → all routes prefixed /v2/
|
||||||
|
],
|
||||||
|
getRouteName: …, // unchanged
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Layout selection (exact convention — audited).** The project uses
|
||||||
|
`MetaLayouts({ target: './src/layouts', defaultLayout: 'default' })` and
|
||||||
|
`setupLayouts` in `src/plugins/1.router/index.ts`. Pages opt into a
|
||||||
|
layout via `definePage({ meta: { layout: '<LayoutFileName>' } })`
|
||||||
|
(verified: `login.vue` → `'blank'`, `forbidden.vue` → `'PublicLayout'`).
|
||||||
|
**Binding convention for v2:** every `pages-v2/**` page declares
|
||||||
|
`definePage({ meta: { layout: 'OrganizerLayoutV2' } })`, and
|
||||||
|
`src/layouts/OrganizerLayoutV2.vue` must exist (MetaLayouts `target` is
|
||||||
|
`./src/layouts`). No conditional logic inside layout files — each page
|
||||||
|
tree pins its own layout. The old layout stays inert (zero regression
|
||||||
|
risk on un-migrated pages). A lint/CI check (or a thin
|
||||||
|
`pages-v2/`-scoped wrapper) enforces the meta-key so a page can't
|
||||||
|
silently fall back to `default`.
|
||||||
|
|
||||||
**Cutover convention (per page):** when a v2 page is approved to replace
|
**Cutover convention (per page):** when a v2 page is approved to replace
|
||||||
its v1 counterpart, move `pages-v2/X.vue` → `pages/X.vue` (overwrite),
|
its v1 counterpart, move `pages-v2/X.vue` → `pages/X.vue` (overwrite),
|
||||||
@@ -125,13 +158,34 @@ siblings in `layouts/`.)
|
|||||||
| RightDrawer | PrimeVue `Drawer` + custom slot scaffold | Plumbing free, content bespoke |
|
| RightDrawer | PrimeVue `Drawer` + custom slot scaffold | Plumbing free, content bespoke |
|
||||||
| WorkspaceSwitcher | custom visual + PrimeVue `Popover` | See §7.4 |
|
| WorkspaceSwitcher | custom visual + PrimeVue `Popover` | See §7.4 |
|
||||||
|
|
||||||
**State contract** (replaces crewli-starter `provide`/`inject`):
|
**State contract** (replaces crewli-starter `provide`/`inject`). Audited
|
||||||
- `useWorkspaceStore` (Pinia): workspace, sidebar collapsed/expanded,
|
2026-05-15: existing stores are `useAuthStore`, `useOrganisationStore`,
|
||||||
theme, density.
|
`useImpersonationStore`, `useNotificationStore` — there is **no**
|
||||||
|
workspace/theme/density store today (theme/density currently via Vuexy
|
||||||
|
`useSkins.ts`). crewli-starter's "workspace" concept maps to Crewli's
|
||||||
|
**organisation/context**, which already lives in `useAuthStore` +
|
||||||
|
`useOrganisationStore`. To avoid duplicating tenant state:
|
||||||
|
|
||||||
|
- **Org / context / workspace data:** read from existing
|
||||||
|
`useAuthStore` (`availableContexts`, current org) +
|
||||||
|
`useOrganisationStore` (org list, branding). `WorkspaceSwitcher`
|
||||||
|
consumes these — it does **not** own org data. Switching context
|
||||||
|
goes through the existing auth/org flow.
|
||||||
|
- **`useShellUiStore` (NEW — the only new store):** holds *only* genuinely
|
||||||
|
new v2-shell UI state with no existing home — `sidebarCollapsed`,
|
||||||
|
`density`, `theme` (v2 light/dark). Nothing tenant-related.
|
||||||
- `useRightDrawer()` composable: `open(component, props)`, `close()`.
|
- `useRightDrawer()` composable: `open(component, props)`, `close()`.
|
||||||
- `vue-router`: replaces `provide('navigate', …)`.
|
- `vue-router`: replaces `provide('navigate', …)`.
|
||||||
- `<html data-theme>` / `<html data-density>` mechanism retained
|
- `<html data-theme>` / `<html data-density>` mechanism retained
|
||||||
(composes with Aura `darkModeSelector`); v2 bypasses Vuexy `useSkins.ts`.
|
(composes with Aura `darkModeSelector`); v2 bypasses Vuexy
|
||||||
|
`useSkins.ts`. `useShellUiStore` owns the writes to these attributes.
|
||||||
|
|
||||||
|
**`provide`/`inject` substitution is a mandatory per-port rule:**
|
||||||
|
crewli-starter uses `provide`/`inject` pervasively (`workspace`,
|
||||||
|
`navigate`, `openDrawer`, `theme`, `density`, …). Every ported component
|
||||||
|
must replace these with the store/composable/router equivalents above —
|
||||||
|
no `inject()` survives the port. This is an architectural decision in
|
||||||
|
the RFC, applied to every component without exception.
|
||||||
|
|
||||||
F3.5's `AppShell` features (notification bell, help, breadcrumb,
|
F3.5's `AppShell` features (notification bell, help, breadcrumb,
|
||||||
org-switcher card) are not lost — the underlying state wires up
|
org-switcher card) are not lost — the underlying state wires up
|
||||||
@@ -161,8 +215,13 @@ PrimeVue → custom in `components-v2/`.
|
|||||||
`defineEmits([...])` → typed `defineEmits<{...}>()`; `inject(...)` →
|
`defineEmits([...])` → typed `defineEmits<{...}>()`; `inject(...)` →
|
||||||
composable/store; component-local types inline, shared types in
|
composable/store; component-local types inline, shared types in
|
||||||
`types/v2/`.
|
`types/v2/`.
|
||||||
- **Boundaries:** existing `eslint-plugin-boundaries` 10-zone matrix
|
- **Boundaries (CORRECTED — audited 2026-05-15):** the matrix does **not**
|
||||||
applies to v2 folders identically; violations stay errors.
|
apply to v2 folders automatically. `boundaries/elements` uses explicit
|
||||||
|
path patterns (`src/components/**`, `src/components/{shared,auth,settings}/**`,
|
||||||
|
`src/pages/{events,members,…}/**`, etc.); **nothing matches
|
||||||
|
`src/components-v2/**` or `src/pages-v2/**`** — they fall through to no
|
||||||
|
element type. New zones + matrix rows are a required foundation
|
||||||
|
deliverable. Specification in §14.
|
||||||
|
|
||||||
## 6. Storybook organization
|
## 6. Storybook organization
|
||||||
|
|
||||||
@@ -291,9 +350,12 @@ direction.
|
|||||||
|
|
||||||
## 9. Foundation sprint deliverables
|
## 9. Foundation sprint deliverables
|
||||||
|
|
||||||
1. Routing + layout scaffold: `OrganizerLayoutV2`, `AppShellV2`, `/v2/*`
|
1. Routing + layout scaffold: **`vite.config.ts` `routesFolder` change**
|
||||||
mount, `useWorkspaceStore`, `useRightDrawer()`, theme/density wiring,
|
(§3), `OrganizerLayoutV2`, `AppShellV2`, `/v2/*` mount via the
|
||||||
one empty `pages-v2/dashboard.vue` proving boot.
|
`definePage` meta-key convention (§3), `useShellUiStore`,
|
||||||
|
`useRightDrawer()`, theme/density wiring, **`eslint-plugin-boundaries`
|
||||||
|
zone + matrix extension (§14)**, one empty `pages-v2/dashboard.vue`
|
||||||
|
proving boot + a passing lint run.
|
||||||
2. Shell pieces ported 1:1 (TS): AppSidebar, AppTopbar, SidebarNav,
|
2. Shell pieces ported 1:1 (TS): AppSidebar, AppTopbar, SidebarNav,
|
||||||
WorkspaceSwitcher, RightDrawer, AppDialog.
|
WorkspaceSwitcher, RightDrawer, AppDialog.
|
||||||
3. Tier-1 primitives (PrimeVue-based per §8).
|
3. Tier-1 primitives (PrimeVue-based per §8).
|
||||||
@@ -307,14 +369,20 @@ direction.
|
|||||||
|
|
||||||
## 10. Sequencing after foundation
|
## 10. Sequencing after foundation
|
||||||
|
|
||||||
|
- **Smart Filter sub-sprint:** the Smart Filter subsystem (5 editors +
|
||||||
|
`useSmartFilters` composable + bar/chip/popover, ~600 lines) is its
|
||||||
|
**own sub-sprint**, not "in parallel" work bundled into Page-1. It
|
||||||
|
lands immediately before Page-1 because Page-1 is its first consumer.
|
||||||
- **Page-1 sprint:** events list end-to-end via `ListTemplate`
|
- **Page-1 sprint:** events list end-to-end via `ListTemplate`
|
||||||
(exercises SmartFilterBar + lazy DataTable + RightDrawer — highest
|
(exercises the now-secured SmartFilterBar + lazy DataTable +
|
||||||
pattern-validation value). Smart Filter subsystem secured here.
|
RightDrawer — highest pattern-validation value). Finish PrimeVue
|
||||||
Finish PrimeVue standard catalog stories in parallel.
|
standard catalog stories here.
|
||||||
- **Subsequent sprints:** one page tree at a time (dashboard → settings
|
- **Subsequent sprints:** one page tree at a time (dashboard → settings
|
||||||
→ events detail → members → platform → portal). Each: build under
|
→ events detail → members → platform → **portal**). Each: build under
|
||||||
`pages-v2/`, add only needed components/stories, sign-off, cutover
|
`pages-v2/`, add only needed components/stories, sign-off, cutover
|
||||||
commit.
|
commit. Portal is in scope (§12) but is its own later sprint with its
|
||||||
|
own `PortalLayoutV2` (token-auth, different chrome than the organizer
|
||||||
|
shell).
|
||||||
- **Domain modules** migrate with their owning page.
|
- **Domain modules** migrate with their owning page.
|
||||||
- **Final cutover:** rename `pages-v2/`→`pages/`,
|
- **Final cutover:** rename `pages-v2/`→`pages/`,
|
||||||
`components-v2/`→`components/`, drop `*V2` suffixes, delete dead v1
|
`components-v2/`→`components/`, drop `*V2` suffixes, delete dead v1
|
||||||
@@ -322,9 +390,11 @@ direction.
|
|||||||
|
|
||||||
## 11. Quality gates (per sprint)
|
## 11. Quality gates (per sprint)
|
||||||
|
|
||||||
`pnpm test`, `pnpm typecheck`, `pnpm lint` (boundaries matrix on v2
|
`pnpm test`, `pnpm typecheck`, `pnpm lint` (with the §14 boundaries
|
||||||
folders), Storybook a11y panel clean on new stories, visual sign-off
|
extension active — v2 folders are zoned, not unmatched), Playwright CT +
|
||||||
against crewli-starter. No `any`. Existing test count must not regress.
|
visual baselines per §13, Storybook a11y panel clean on new stories,
|
||||||
|
visual sign-off against crewli-starter. No `any`. Existing test count
|
||||||
|
must not regress.
|
||||||
|
|
||||||
## 12. Non-goals
|
## 12. Non-goals
|
||||||
|
|
||||||
@@ -336,3 +406,67 @@ against crewli-starter. No `any`. Existing test count must not regress.
|
|||||||
- Flatpickr / vue-i18n / DatePicker decisions inherit from
|
- Flatpickr / vue-i18n / DatePicker decisions inherit from
|
||||||
RFC-WS-FRONTEND-PRIMEVUE unchanged.
|
RFC-WS-FRONTEND-PRIMEVUE unchanged.
|
||||||
- No PrimeVue Pro / template purchase.
|
- No PrimeVue Pro / template purchase.
|
||||||
|
|
||||||
|
**Portal scope (explicit decision):** the portal surface (route prefix
|
||||||
|
is **`/portal/*`**, not `/p/*` — RFC-WS-FRONTEND-PRIMEVUE corrected this;
|
||||||
|
token-auth, `portal.token` middleware) **is in scope** for the v2
|
||||||
|
redesign — crewli-starter ships portal designs (`PortalPage`,
|
||||||
|
`PortalVolunteerPage`, `PortalCustomerPage`, `PortalVolunteerFormPage`).
|
||||||
|
It is **not** foundation work: it is a dedicated later sprint with its
|
||||||
|
own `PortalLayoutV2` (the token-auth portal chrome differs from the
|
||||||
|
organizer shell). During parallel mode it lives at `/v2/portal/*`;
|
||||||
|
cutover folds it to `/portal/*` like every other tree.
|
||||||
|
|
||||||
|
## 13. Testing strategy (audited 2026-05-15)
|
||||||
|
|
||||||
|
The review correctly flagged that RFC Amendment A-1 made TEST-INFRA-001
|
||||||
|
a prerequisite to F2. **Audit result: TEST-INFRA-001, TEST-CONTRACT-001,
|
||||||
|
and TEST-VISUAL-001 are all ✅ Resolved** (`BACKLOG.md` §§ at lines
|
||||||
|
2307 / 2371 / 2424; closed in branch `chore/test-infra-001`, commits
|
||||||
|
`b8d18e6` / `2dfb1e8` / `f6509d9`). The Playwright Component Testing +
|
||||||
|
visual-regression foundation **exists in the repo today**
|
||||||
|
(`playwright-ct.config.ts`, `pnpm test:component`, `pnpm test:visual`).
|
||||||
|
The precondition is therefore satisfied — but RFC R-11's rationale
|
||||||
|
("jsdom does not detect visual regression in a multi-component
|
||||||
|
migration") is **more** relevant here, not less, because v2 builds
|
||||||
|
components from scratch rather than translating them. Explicit decision:
|
||||||
|
|
||||||
|
| Tier | Tool | v2 application |
|
||||||
|
|---|---|---|
|
||||||
|
| Logic / props / emits | Vitest + jsdom | Unchanged. Component-local logic, store/composable units. |
|
||||||
|
| Component render + a11y | Playwright CT (`test:component`) | **Required** for every bespoke component (§7) and every template (§8 Tier-3). Reuses the TEST-INFRA-001 foundation. |
|
||||||
|
| Visual regression | Playwright CT `@visual` (`test:visual`) | **Required** baselines for bespoke components + templates + the shell. Workflow: the v2 component is visually signed off against crewli-starter (human parity check), *then* its rendered output is captured as the committed baseline screenshot; thereafter CI flags any drift from that approved baseline. (Not an automated cross-repo diff against crewli-starter, and *not* the legacy Vuexy prototype HTML that TEST-VISUAL-001 baselined.) Sub-package closure requires green baselines. |
|
||||||
|
| Accessibility (authoring aid) | Storybook `addon-a11y` | **Complementary, not a replacement** for Playwright a11y. Storybook is the authoring/QA surface; CI correctness gate stays Playwright CT + `vitest-axe`. |
|
||||||
|
|
||||||
|
Storybook + manual sign-off does **not** replace Playwright CT — it
|
||||||
|
augments it. This is the deliberate, bidirectional-valid choice the
|
||||||
|
review asked for, made explicitly: keep the Playwright gate, add
|
||||||
|
Storybook as the authoring/review surface, re-baseline visuals against
|
||||||
|
crewli-starter.
|
||||||
|
|
||||||
|
## 14. eslint-plugin-boundaries extension (audited 2026-05-15)
|
||||||
|
|
||||||
|
My original §5 claim ("matrix applies identically") was **factually
|
||||||
|
wrong** and is corrected here. `.eslintrc.cjs` `boundaries/elements`
|
||||||
|
uses explicit, order-sensitive path patterns; none match
|
||||||
|
`src/components-v2/**` or `src/pages-v2/**`. Required foundation change
|
||||||
|
(deliverable 1):
|
||||||
|
|
||||||
|
1. **New element zones** (added *before* the generic `components`
|
||||||
|
catch-all, since first-match wins):
|
||||||
|
- `{ type: 'components-v2', pattern: 'src/components-v2/**' }`
|
||||||
|
- `{ type: 'pages-v2', pattern: 'src/pages-v2/**' }`
|
||||||
|
- A narrow whitelist zone for the two intentional cross-imports:
|
||||||
|
`{ type: 'components-foundation', pattern: 'src/components/{forms/**,Icon.vue}' }`
|
||||||
|
(FormField + Icon — audited to live in the generic `components`
|
||||||
|
zone today, not `components-shared`).
|
||||||
|
2. **New matrix rows** (`boundaries/element-types`):
|
||||||
|
- `{ from: 'components-v2', allow: ['types','utils','lib','composables','composables-forms','stores','components-v2','components-foundation'] }`
|
||||||
|
- `{ from: 'pages-v2', allow: ['types','utils','lib','composables','composables-forms','stores','navigation','components-v2','components-foundation','layouts','plugins'] }`
|
||||||
|
3. **Asymmetry rule:** `components-v2` / `pages-v2` may import
|
||||||
|
`components-foundation` (FormField, Icon) — the *only* permitted
|
||||||
|
v1→v2 bridge. v1 zones get **no** `allow` entry for `components-v2`
|
||||||
|
(no back-porting, enforced structurally, matching §12).
|
||||||
|
|
||||||
|
Exact ESLint object syntax is an implementation-plan task; this section
|
||||||
|
fixes the *contract* (zones, allowed edges, the no-back-port asymmetry).
|
||||||
|
|||||||
Reference in New Issue
Block a user