diff --git a/apps/app/.eslintrc.cjs b/apps/app/.eslintrc.cjs index ee51fb29..cc1069d7 100644 --- a/apps/app/.eslintrc.cjs +++ b/apps/app/.eslintrc.cjs @@ -252,9 +252,16 @@ module.exports = { // (components-foundation) but NOT any other v1 component zone. // No v1 `from` rule lists components-v2/pages-v2 → back-porting // is structurally impossible (RFC-WS-GUI-REDESIGN AD-G5). + // layouts-v2 (src/layouts/*V2*.vue, e.g. OrganizerLayoutV2) is + // the v2 shell-composition zone: SAME v2 capability as pages-v2 + // (may import components-v2 + navigation) so AD-G2's + // "OrganizerLayoutV2 wraps AppShellV2" holds, WITHOUT widening + // the v1 `layouts` zone (still cannot reach components-v2 → + // AD-G5 isolation intact). Locked by tests/unit/boundaries-v2.spec.ts. { from: 'components-foundation', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components-foundation'] }, { 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'] }, + { from: 'layouts-v2', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'navigation', 'components-v2', 'components-foundation', 'layouts', 'plugins'] }, { from: 'components', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components', 'components-shared', 'components-organizer'] }, { from: 'layouts', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'stores-portal', 'navigation', 'components', 'components-shared', 'components-portal', 'components-organizer', 'layouts'] }, @@ -315,6 +322,14 @@ module.exports = { { type: 'components-foundation', pattern: 'src/components/Icon.vue', mode: 'file' }, { type: 'components-v2', pattern: 'src/components-v2/**' }, { type: 'components', pattern: 'src/components/**' }, + // layouts-v2 MUST precede the generic `layouts` element: first + // match wins. The single `*` does not cross `/`, so this matches + // only top-level v2 layout files (src/layouts/OrganizerLayoutV2.vue) + // and NOT src/layouts/components/AppShellV2.vue (subdir → stays + // `layouts`, which is correct: AppShellV2 imports only stores). + // mode:'file' is REQUIRED for a file-glob element (same reason as + // the Icon.vue bridge above) — RFC AD-G5 / boundaries-v2.spec.ts. + { type: 'layouts-v2', pattern: 'src/layouts/*V2*.vue', mode: 'file' }, { type: 'layouts', pattern: 'src/layouts/**' }, { type: 'pages-register', pattern: 'src/pages/register/**' }, { type: 'pages-portal', pattern: 'src/pages/portal/**' }, diff --git a/apps/app/tests/unit/boundaries-v2.spec.ts b/apps/app/tests/unit/boundaries-v2.spec.ts index 8b1b7be7..d0baead9 100644 --- a/apps/app/tests/unit/boundaries-v2.spec.ts +++ b/apps/app/tests/unit/boundaries-v2.spec.ts @@ -67,4 +67,38 @@ describe('boundaries — v2 zones', () => { expect(errs.length).toBeGreaterThan(0) }) + + // ------------------------------------------------------------------------- + // layouts-v2 zone (RFC AD-G5): the v2 shell layout (src/layouts/*V2*.vue) + // gets pages-v2-equivalent v2 capability so AD-G2 ("OrganizerLayoutV2 + // wraps AppShellV2") holds — WITHOUT widening the v1 `layouts` zone. + // These lock both halves: the new edge AND the preserved isolation. + // ------------------------------------------------------------------------- + + it('allows layouts-v2 → components-v2 (AD-G5: v2 shell composition)', async () => { + const errs = await boundaryErrors( + 'src/layouts/OrganizerLayoutV2.vue', + '', + ) + + expect(errs).toHaveLength(0) + }) + + it('allows layouts-v2 → navigation (v2 layout sources nav data)', async () => { + const errs = await boundaryErrors( + 'src/layouts/OrganizerLayoutV2.vue', + '', + ) + + expect(errs).toHaveLength(0) + }) + + it('forbids v1 layouts → components-v2 (AD-G5 isolation preserved)', async () => { + const errs = await boundaryErrors( + 'src/layouts/OrganizerLayout.vue', + '', + ) + + expect(errs.length).toBeGreaterThan(0) + }) }) diff --git a/dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md b/dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md index ed7efe60..e63a7b80 100644 --- a/dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md +++ b/dev-docs/RFC-WS-GUI-REDESIGN-CREWLI-STARTER.md @@ -30,9 +30,19 @@ Tailwind + FormField + DataTable conventions) remain binding. `useAuthStore`/`useOrganisationStore`. One new `useShellUiStore` holds only sidebar/theme/density + right-drawer state. `provide`/`inject` from crewli-starter is replaced per-port (no `inject()` survives). -- **AD-G5 — Boundaries.** New `components-v2`/`pages-v2` zones; the only - v1→v2 bridge is a narrow `components-foundation` zone (FormField, - Icon). No back-porting (structurally enforced). +- **AD-G5 — Boundaries.** New `components-v2`/`pages-v2`/`layouts-v2` + zones; the only v1→v2 bridge is a narrow `components-foundation` zone + (FormField, Icon). No back-porting (structurally enforced). The v2 + shell layout (`src/layouts/*V2*.vue`, e.g. `OrganizerLayoutV2`) is the + `layouts-v2` zone — same v2 capability as `pages-v2` (may import + `components-v2` + `navigation`) so AD-G2's "`OrganizerLayoutV2` wraps + `AppShellV2`" holds. The v1 `layouts` zone is unchanged and still + cannot import `components-v2`, so v2 isolation is preserved (only + top-level `*V2*.vue` layout files gain v2 capability; + `src/layouts/components/AppShellV2.vue` stays in `layouts` since it + imports only `stores`). Locked by `tests/unit/boundaries-v2.spec.ts` + (`layouts-v2 → components-v2` allowed; v1 `layouts → components-v2` + forbidden). - **AD-G6 — Testing.** TEST-INFRA-001 (✅ Resolved) Playwright-CT + visual foundation is kept as the CI gate; Storybook a11y is complementary. v2 visual baselines are captured from the v2 component