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>
26 KiB
Design — Crewli SPA GUI Redesign on the crewli-starter Design System
| Field | Value |
|---|---|
| Status | Design approved; spec review round 1 corrections applied — pending re-approval |
| 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) |
Spec review round 1 (2026-05-15) — corrections applied, all audited against the codebase:
- TEST-INFRA-001 (blocking): audited → ✅ Resolved; new §13 Testing strategy added (Playwright CT gate kept, re-baselined against crewli-starter, Storybook a11y complementary).
- Layout-selection mechanism (blocking): §3 now specifies the exact
definePage({ meta: { layout: 'OrganizerLayoutV2' } })convention +routesFoldervite config (both audited).- eslint-plugin-boundaries (blocking): §5 corrected (was factually wrong); new §14 specifies the
components-v2/pages-v2/components-foundationzones + no-back-port asymmetry.useWorkspaceStore→ reuseuseAuthStore/useOrganisationStorefor org data + newuseShellUiStorefor sidebar/theme/density only (§4);provide/injectsubstitution is now a mandatory per-port rule.- Portal scope made explicit (§12, in scope, own later sprint,
/portal/*not/p/*).- Smart Filter promoted to its own sub-sprint (§10).
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)
- Cohabitation: parallel routes under
/v2/*. Old pages stay on existing routes. Cutover is route-by-route. - Fidelity rule: PrimeVue-first. Default to a stock PrimeVue
component styled with Tailwind +
ptAPI + 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). - 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.
- Shell strategy: a new
AppShellV2.vue(andOrganizerLayoutV2.vue) ports the crewli-starter shell 1:1. The existingAppShell.vue/OrganizerLayout.vuestay untouched until/v2/supersedes them. - Folder convention:
pages-v2/+components-v2/mirror the existing structure. No per-fileV2suffix 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. - 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/ # + useShellUiStore (sidebar/theme/density ONLY —
│ # org/context data reuses existing stores, §4)
├── composables/ # + useRightDrawer
└── types/v2/ # shared v2 types
Routing (exact mechanism — audited 2026-05-15). vite.config.ts
currently calls VueRouter({ getRouteName: … }) with no routesFolder,
so it defaults to src/pages only — pages-v2/ would generate zero
routes without a config change. Required change (foundation deliverable
1, §9):
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
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). Audited
2026-05-15: existing stores are useAuthStore, useOrganisationStore,
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).WorkspaceSwitcherconsumes 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().vue-router: replacesprovide('navigate', …).<html data-theme>/<html data-density>mechanism retained (composes with AuradarkModeSelector); v2 bypasses VuexyuseSkins.ts.useShellUiStoreowns 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,
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
V2suffix on files insidepages-v2//components-v2/(folder carries it). Suffix only onAppShellV2.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([...])→ typeddefineEmits<{...}>();inject(...)→ composable/store; component-local types inline, shared types intypes/v2/. - Boundaries (CORRECTED — audited 2026-05-15): the matrix does not
apply to v2 folders automatically.
boundaries/elementsuses explicit path patterns (src/components/**,src/components/{shared,auth,settings}/**,src/pages/{events,members,…}/**, etc.); nothing matchessrc/components-v2/**orsrc/pages-v2/**— they fall through to no element type. New zones + matrix rows are a required foundation deliverable. Specification in §14.
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
- Routing + layout scaffold:
vite.config.tsroutesFolderchange (§3),OrganizerLayoutV2,AppShellV2,/v2/*mount via thedefinePagemeta-key convention (§3),useShellUiStore,useRightDrawer(), theme/density wiring,eslint-plugin-boundarieszone + matrix extension (§14), one emptypages-v2/dashboard.vueproving boot + a passing lint run. - Shell pieces ported 1:1 (TS): AppSidebar, AppTopbar, SidebarNav, WorkspaceSwitcher, RightDrawer, AppDialog.
- Tier-1 primitives (PrimeVue-based per §8).
- Template layer: List / Form / Detail / Dashboard / StateBlock.
- 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.
- New project RFC in
dev-docs/superseding F4a–F4d; pointer added fromPRIMEVUE_COMPONENTS.md.
10. Sequencing after foundation
- Smart Filter sub-sprint: the Smart Filter subsystem (5 editors +
useSmartFilterscomposable + 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(exercises the now-secured SmartFilterBar + lazy DataTable + RightDrawer — highest pattern-validation value). Finish PrimeVue standard catalog stories here. - 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. Portal is in scope (§12) but is its own later sprint with its ownPortalLayoutV2(token-auth, different chrome than the organizer shell). - Domain modules migrate with their owning page.
- Final cutover: rename
pages-v2/→pages/,components-v2/→components/, drop*V2suffixes, delete dead v1 shell + Vuetify, one closure commit.
11. Quality gates (per sprint)
pnpm test, pnpm typecheck, pnpm lint (with the §14 boundaries
extension active — v2 folders are zoned, not unmatched), Playwright CT +
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
- 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.
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):
- New element zones (added before the generic
componentscatch-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 genericcomponentszone today, notcomponents-shared).
- 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'] }
- Asymmetry rule:
components-v2/pages-v2may importcomponents-foundation(FormField, Icon) — the only permitted v1→v2 bridge. v1 zones get noallowentry forcomponents-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).