# Crewli GUI Redesign — Foundation Plan 1 (RFC + bootable /v2/ slice) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Land the new project RFC (superseding F4a–F4d) and the structural foundation so that `/v2/dashboard` boots through a new `OrganizerLayoutV2` + `AppShellV2` skeleton, with route-name collision prevention, boundary zones, layout-meta enforcement, and the v2 UI state layer — all green under lint/typecheck/test/build. **Architecture:** Parallel `/v2/*` route tree (own `routesFolder` with a `v2-` route-name prefix), a new `OrganizerLayoutV2` selected via `definePage({ meta: { layout: 'OrganizerLayoutV2' } })` (enforced by a custom ESLint rule), a Tailwind-grid `AppShellV2` *skeleton* with named slot regions (PrimeVue shell pieces arrive in Plan 2), a single `useShellUiStore` for sidebar/theme/density/right-drawer state, and a thin `useRightDrawer()` facade over it. No v1 code is touched except additive config. **Tech Stack:** Vue 3 ` ``` - [ ] **Step 4: Typecheck the config change** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | head -20` Expected: no new errors referencing `vite.config.ts` or `v2RouteName`. (`routeNode.fullPath` is a documented `TreeNode` field in unplugin-vue-router 0.8.x.) - [ ] **Step 5: Verify the route is generated with the prefixed name** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vite build 2>&1 | tail -5` Expected: build succeeds. Then run: `grep -rn "v2-dashboard\|/v2/dashboard" "$(find /Users/berthausmans/Documents/Development/crewli/apps/app -name 'typed-router.d.ts' | head -1)"` Expected: an entry mapping path `/v2/dashboard` to route name `v2-dashboard`. **If the name is `dashboard` (no `v2-` prefix), the defensive path read returned empty** — the installed unplugin-vue-router exposes the node path differently. Remediate before continuing: temporarily add `console.log(JSON.stringify({ k: Object.keys(routeNode), fp: routeNode.fullPath, vp: routeNode.value?.path }))` inside `getRouteName`, run `pnpm exec vite build 2>&1 | grep v2`, read the actual field carrying `/v2/...`, update the `nodePath` expression to read that field, remove the log, rebuild, re-grep. Do not proceed to Task 4 until `typed-router.d.ts` shows `v2-dashboard`. - [ ] **Step 6: Commit** ```bash cd /Users/berthausmans/Documents/Development/crewli git add apps/app/vite.config.ts apps/app/src/pages-v2/dashboard.vue git commit -m "feat(router): mount pages-v2 at /v2/* with v2- name prefix" ``` --- ## Task 4: eslint-plugin-boundaries — v2 zones + matrix **Files:** - Modify: `apps/app/.eslintrc.cjs` (`boundaries/elements` and `boundaries/element-types`) - Test: `apps/app/tests/unit/boundaries-v2.spec.ts` - [ ] **Step 1: Write the failing test (ESLint Node API on fixtures)** Create `apps/app/tests/unit/boundaries-v2.spec.ts`: ```ts import { describe, expect, it } from 'vitest' import { ESLint } from 'eslint' const eslint = new ESLint({ cwd: `${process.cwd()}` }) async function boundaryErrors(filePath: string, code: string) { const [result] = await eslint.lintText(code, { filePath }) return result.messages.filter(m => m.ruleId === 'boundaries/element-types') } describe('boundaries — v2 zones', () => { it('allows pages-v2 → components-v2', async () => { const errs = await boundaryErrors( 'src/pages-v2/dashboard.vue', ``, ) expect(errs).toHaveLength(0) }) it('allows components-v2 → components-foundation (FormField bridge)', async () => { const errs = await boundaryErrors( 'src/components-v2/forms/Demo.vue', ``, ) expect(errs).toHaveLength(0) }) it('forbids v1 components → components-v2 (no back-porting)', async () => { const errs = await boundaryErrors( 'src/components/organizer/Legacy.vue', ``, ) expect(errs.length).toBeGreaterThan(0) }) }) ``` - [ ] **Step 2: Run it to verify it fails** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run tests/unit/boundaries-v2.spec.ts` Expected: FAIL — the "forbids v1 → components-v2" case currently produces 0 errors (no zone exists, so the import is unclassified and allowed), so `expect(errs.length).toBeGreaterThan(0)` fails. - [ ] **Step 3: Add the element zones** In `apps/app/.eslintrc.cjs`, find this exact line inside `'boundaries/elements'`: ```js { type: 'components', pattern: 'src/components/**' }, ``` Insert **directly above** it (so the narrow zones win — first-match-wins, order matters): ```js // GUI-redesign v2 zones (RFC-WS-GUI-REDESIGN AD-G5). Declared // before the generic `components` catch-all. `components-foundation` // is the ONLY sanctioned v1→v2 bridge (FormField + Icon — audited // to live in the generic `components` zone, not components-shared). // eslint-plugin-boundaries 6.0.2 micromatch supports the brace // form below; if a future bump breaks it, split into two entries // with the same `type` (see RFC §14 fallback). { type: 'components-foundation', pattern: 'src/components/{forms/**,Icon.vue}' }, { type: 'components-v2', pattern: 'src/components-v2/**' }, ``` Then find this exact line: ```js { type: 'pages', pattern: 'src/pages/**' }, ``` Insert **directly above** it: ```js { type: 'pages-v2', pattern: 'src/pages-v2/**' }, ``` - [ ] **Step 4: Add the matrix rows** In `apps/app/.eslintrc.cjs`, find this exact line inside `'boundaries/element-types'` `rules`: ```js { from: 'components', allow: ['types', 'utils', 'lib', 'composables', 'composables-forms', 'stores', 'components', 'components-shared', 'components-organizer'] }, ``` Insert **directly above** it: ```js // v2 zones. components-v2 may use the FormField/Icon bridge // (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). { 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'] }, ``` - [ ] **Step 5: Run the test to verify it passes** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run tests/unit/boundaries-v2.spec.ts` Expected: PASS — 3 tests. (If the brace-glob misbehaves on 6.0.2, replace the `components-foundation` element line with two lines — `pattern: 'src/components/forms/**'` and `pattern: 'src/components/Icon.vue'` — same `type`; re-run.) - [ ] **Step 6: Confirm no regression on existing zones** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec eslint 'src/components/organizer/**/*.vue' --rule '{}' 2>&1 | tail -3` Expected: no new `boundaries/element-types` errors introduced by the additions (exit status unchanged from baseline). - [ ] **Step 7: Commit** ```bash cd /Users/berthausmans/Documents/Development/crewli git add apps/app/.eslintrc.cjs apps/app/tests/unit/boundaries-v2.spec.ts git commit -m "feat(lint): add components-v2/pages-v2 boundary zones (no back-port)" ``` --- ## Task 5: Custom ESLint rule — `require-v2-layout-meta` **Files:** - Create: `apps/app/eslint-rules/require-v2-layout-meta.cjs` - Create: `apps/app/eslint-local-rules.cjs` - Create: `apps/app/eslint-rules/__tests__/require-v2-layout-meta.spec.ts` - Modify: `apps/app/package.json` (add `eslint-plugin-local-rules` dev dep) - Modify: `apps/app/.eslintrc.cjs` (`plugins` + new `overrides` entry) - [ ] **Step 1: Add the local-rules plugin dependency** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm add -D eslint-plugin-local-rules@3.0.2` Expected: `package.json` devDependencies gains `"eslint-plugin-local-rules": "3.0.2"`. - [ ] **Step 2: Write the failing rule test (RuleTester)** Create `apps/app/eslint-rules/__tests__/require-v2-layout-meta.spec.ts`: ```ts import { createRequire } from 'node:module' import { describe, it } from 'vitest' import { RuleTester } from 'eslint' // Project is ESLint 8.57 — RuleTester uses the legacy `parser` + // `parserOptions` shape (NOT ESLint-9 `languageOptions`). createRequire // gives us a CJS `require` inside this ESM test for the .cjs rule + the // parser path. const require = createRequire(import.meta.url) const rule = require('../require-v2-layout-meta.cjs') const ruleTester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, }) describe('require-v2-layout-meta', () => { it('passes valid and rejects invalid', () => { ruleTester.run('require-v2-layout-meta', rule, { valid: [ { filename: 'src/pages-v2/dashboard.vue', code: ``, }, { // non-v2 file is ignored entirely filename: 'src/pages/dashboard.vue', code: ``, }, ], invalid: [ { filename: 'src/pages-v2/dashboard.vue', code: ``, errors: [{ messageId: 'missing' }], }, { filename: 'src/pages-v2/events/index.vue', code: ``, errors: [{ messageId: 'wrongLayout' }], }, ], }) }) }) ``` - [ ] **Step 3: Run it to verify it fails** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run eslint-rules/__tests__/require-v2-layout-meta.spec.ts` Expected: FAIL — `Cannot find module '../require-v2-layout-meta.cjs'`. - [ ] **Step 4: Write the rule** Create `apps/app/eslint-rules/require-v2-layout-meta.cjs`: ```js /** * Enforces that every src/pages-v2/**.vue page declares * definePage({ meta: { layout: 'OrganizerLayoutV2' } }) * (or 'PortalLayoutV2' for src/pages-v2/portal/**). Without this a v2 * page silently falls back to the `default` layout — a no-error * wrong-shell bug. RFC-WS-GUI-REDESIGN AD-G2. */ 'use strict' module.exports = { meta: { type: 'problem', docs: { description: 'require definePage layout meta on pages-v2' }, messages: { missing: 'pages-v2 page must call definePage({ meta: { layout: ... } }).', wrongLayout: 'pages-v2 layout must be {{expected}} (got {{actual}}).', }, schema: [], }, create(context) { const filename = (context.filename || context.getFilename() || '').replace(/\\/g, '/') if (!filename.includes('src/pages-v2/')) return {} const expected = filename.includes('src/pages-v2/portal/') ? 'PortalLayoutV2' : 'OrganizerLayoutV2' let sawDefinePage = false return { CallExpression(node) { if (node.callee.type !== 'Identifier' || node.callee.name !== 'definePage') return sawDefinePage = true const arg = node.arguments[0] const metaProp = arg && arg.type === 'ObjectExpression' ? arg.properties.find(p => p.key && p.key.name === 'meta') : null const layoutProp = metaProp && metaProp.value.type === 'ObjectExpression' ? metaProp.value.properties.find(p => p.key && p.key.name === 'layout') : null if (!layoutProp || layoutProp.value.value !== expected) { context.report({ node, messageId: 'wrongLayout', data: { expected, actual: layoutProp ? String(layoutProp.value.value) : 'none' }, }) } }, 'Program:exit': function (node) { if (!sawDefinePage) context.report({ node, messageId: 'missing' }) }, } }, } ``` - [ ] **Step 5: Create the local-rules entry** Create `apps/app/eslint-local-rules.cjs`: ```js 'use strict' module.exports = { 'require-v2-layout-meta': require('./eslint-rules/require-v2-layout-meta.cjs'), } ``` - [ ] **Step 6: Run the rule test to verify it passes** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run eslint-rules/__tests__/require-v2-layout-meta.spec.ts` Expected: PASS. - [ ] **Step 7: Wire the rule into .eslintrc.cjs** In `apps/app/.eslintrc.cjs`, find the top-level `plugins: [` array (≈ line 30) and add `'local-rules'` as an element. Then add this entry to the top-level `overrides:` array: ```js { files: ['src/pages-v2/**/*.vue'], rules: { 'local-rules/require-v2-layout-meta': 'error', }, }, ``` - [ ] **Step 8: Verify the rule is active end-to-end** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && printf '%s' "" > /tmp/bad.vue && cp /tmp/bad.vue src/pages-v2/__rule_probe.vue && pnpm exec eslint src/pages-v2/__rule_probe.vue ; rm src/pages-v2/__rule_probe.vue` Expected: ESLint reports `local-rules/require-v2-layout-meta` `missing`. (The probe file is removed by the same command.) - [ ] **Step 9: Commit** ```bash cd /Users/berthausmans/Documents/Development/crewli git add apps/app/eslint-rules apps/app/eslint-local-rules.cjs apps/app/.eslintrc.cjs apps/app/package.json apps/app/pnpm-lock.yaml git commit -m "feat(lint): enforce definePage layout meta on pages-v2" ``` --- ## Task 6: `useShellUiStore` (sidebar / theme / density / right-drawer) **Files:** - Create: `apps/app/src/stores/useShellUiStore.ts` - Test: `apps/app/src/stores/__tests__/useShellUiStore.spec.ts` - [ ] **Step 1: Write the failing test** Create `apps/app/src/stores/__tests__/useShellUiStore.spec.ts`: ```ts import { beforeEach, describe, expect, it } from 'vitest' import { createPinia, setActivePinia } from 'pinia' import { useShellUiStore } from '@/stores/useShellUiStore' describe('useShellUiStore', () => { beforeEach(() => { setActivePinia(createPinia()) document.documentElement.removeAttribute('data-theme') document.documentElement.removeAttribute('data-density') document.documentElement.classList.remove('dark') }) it('defaults: expanded sidebar, comfortable density, light theme, closed drawer', () => { const s = useShellUiStore() expect(s.sidebarCollapsed).toBe(false) expect(s.density).toBe('comfortable') expect(s.theme).toBe('light') expect(s.drawer.isOpen).toBe(false) }) it('toggleSidebar flips collapsed', () => { const s = useShellUiStore() s.toggleSidebar() expect(s.sidebarCollapsed).toBe(true) }) it('applyDomAttributes writes data-theme/data-density and .dark', () => { const s = useShellUiStore() s.setTheme('dark') s.setDensity('compact') s.applyDomAttributes() expect(document.documentElement.getAttribute('data-theme')).toBe('dark') expect(document.documentElement.getAttribute('data-density')).toBe('compact') expect(document.documentElement.classList.contains('dark')).toBe(true) }) it('openDrawer/closeDrawer mutate drawer state', () => { const s = useShellUiStore() s.openDrawer('PersonCard', { id: '01H' }) expect(s.drawer).toEqual({ isOpen: true, component: 'PersonCard', props: { id: '01H' } }) s.closeDrawer() expect(s.drawer.isOpen).toBe(false) }) }) ``` - [ ] **Step 2: Run it to verify it fails** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run src/stores/__tests__/useShellUiStore.spec.ts` Expected: FAIL — cannot resolve `@/stores/useShellUiStore`. - [ ] **Step 3: Write the store** Create `apps/app/src/stores/useShellUiStore.ts`: ```ts import { defineStore } from 'pinia' import { ref } from 'vue' // v2 shell UI state ONLY (RFC-WS-GUI-REDESIGN AD-G4). No tenant/org // state — that stays in useAuthStore/useOrganisationStore. Owns the // writes to //.dark (composes with // Aura darkModeSelector '.dark'); v2 bypasses Vuexy useSkins.ts. export type ShellTheme = 'light' | 'dark' export type ShellDensity = 'comfortable' | 'compact' export interface ShellDrawerState { isOpen: boolean component: string | null props: Record } export const useShellUiStore = defineStore('shellUi', () => { const sidebarCollapsed = ref(false) const density = ref('comfortable') const theme = ref('light') const drawer = ref({ isOpen: false, component: null, props: {} }) function toggleSidebar(): void { sidebarCollapsed.value = !sidebarCollapsed.value } function setTheme(next: ShellTheme): void { theme.value = next } function setDensity(next: ShellDensity): void { density.value = next } function applyDomAttributes(): void { const el = document.documentElement el.setAttribute('data-theme', theme.value) el.setAttribute('data-density', density.value) el.classList.toggle('dark', theme.value === 'dark') } function openDrawer(component: string, props: Record = {}): void { drawer.value = { isOpen: true, component, props } } function closeDrawer(): void { drawer.value = { isOpen: false, component: null, props: {} } } return { sidebarCollapsed, density, theme, drawer, toggleSidebar, setTheme, setDensity, applyDomAttributes, openDrawer, closeDrawer, } }) ``` - [ ] **Step 4: Run the test to verify it passes** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run src/stores/__tests__/useShellUiStore.spec.ts` Expected: PASS — 5 tests. - [ ] **Step 5: Commit** ```bash cd /Users/berthausmans/Documents/Development/crewli git add apps/app/src/stores/useShellUiStore.ts apps/app/src/stores/__tests__/useShellUiStore.spec.ts git commit -m "feat(stores): add useShellUiStore for v2 shell UI state" ``` --- ## Task 7: `useRightDrawer()` facade composable **Files:** - Create: `apps/app/src/composables/useRightDrawer.ts` - Test: `apps/app/src/composables/__tests__/useRightDrawer.spec.ts` - [ ] **Step 1: Write the failing test** Create `apps/app/src/composables/__tests__/useRightDrawer.spec.ts`: ```ts import { beforeEach, describe, expect, it } from 'vitest' import { createPinia, setActivePinia } from 'pinia' import { useRightDrawer } from '@/composables/useRightDrawer' import { useShellUiStore } from '@/stores/useShellUiStore' describe('useRightDrawer', () => { beforeEach(() => setActivePinia(createPinia())) it('open() writes through to the store', () => { const { open } = useRightDrawer() open('ArtistCard', { id: '01H' }) const s = useShellUiStore() expect(s.drawer).toEqual({ isOpen: true, component: 'ArtistCard', props: { id: '01H' } }) }) it('close() clears the store drawer', () => { const { open, close, isOpen } = useRightDrawer() open('ArtistCard') close() expect(isOpen.value).toBe(false) expect(useShellUiStore().drawer.isOpen).toBe(false) }) }) ``` - [ ] **Step 2: Run it to verify it fails** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run src/composables/__tests__/useRightDrawer.spec.ts` Expected: FAIL — cannot resolve `@/composables/useRightDrawer`. - [ ] **Step 3: Write the composable** Create `apps/app/src/composables/useRightDrawer.ts`: ```ts import { storeToRefs } from 'pinia' import type { ComputedRef } from 'vue' import { computed } from 'vue' import { useShellUiStore } from '@/stores/useShellUiStore' // Thin facade over useShellUiStore.drawer (RFC-WS-GUI-REDESIGN AD-G4, // issue 5). NOT a module-level ref singleton: state lives in the Pinia // store so Playwright CT can drive the drawer via @pinia/testing // without rendering the shell, and tests don't leak state across cases. export interface UseRightDrawer { isOpen: ComputedRef component: ComputedRef props: ComputedRef> open: (component: string, props?: Record) => void close: () => void } export function useRightDrawer(): UseRightDrawer { const store = useShellUiStore() const { drawer } = storeToRefs(store) return { isOpen: computed(() => drawer.value.isOpen), component: computed(() => drawer.value.component), props: computed(() => drawer.value.props), open: (component, props = {}) => store.openDrawer(component, props), close: () => store.closeDrawer(), } } ``` - [ ] **Step 4: Run the test to verify it passes** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run src/composables/__tests__/useRightDrawer.spec.ts` Expected: PASS — 2 tests. - [ ] **Step 5: Commit** ```bash cd /Users/berthausmans/Documents/Development/crewli git add apps/app/src/composables/useRightDrawer.ts apps/app/src/composables/__tests__/useRightDrawer.spec.ts git commit -m "feat(composables): add useRightDrawer facade over useShellUiStore" ``` --- ## Task 8: `OrganizerLayoutV2` + `AppShellV2` skeleton **Files:** - Create: `apps/app/src/layouts/components/AppShellV2.vue` - Create: `apps/app/src/layouts/OrganizerLayoutV2.vue` - Test: `apps/app/tests/component/layouts/AppShellV2.spec.ts` > Skeleton only: a Tailwind 12-col-ish grid with **named slot regions** > (`sidebar`, `topbar`, default=content, `drawer`) + `RouterView`. No > PrimeVue parts yet (spec: outer container is custom Tailwind grid; > PrimeVue shell pieces arrive in Plan 2). Per spec §13, the skeleton > gets a Vitest mount test now; Playwright-CT visual baselines are > captured in Plan 2 when the real shell pieces land. - [ ] **Step 1: Write the failing mount test** Create `apps/app/tests/component/layouts/AppShellV2.spec.ts`: ```ts import { describe, expect, it } from 'vitest' import { mount } from '@vue/test-utils' import { createPinia } from 'pinia' import AppShellV2 from '@/layouts/components/AppShellV2.vue' describe('AppShellV2 (skeleton)', () => { it('renders the grid regions and default slot content', () => { const wrapper = mount(AppShellV2, { global: { plugins: [createPinia()] }, slots: { sidebar: '', topbar: '
TB
', default: '
CONTENT
', drawer: '', }, }) expect(wrapper.find('[data-testid="appshell-v2"]').exists()).toBe(true) expect(wrapper.find('[data-testid="sb"]').exists()).toBe(true) expect(wrapper.find('[data-testid="tb"]').exists()).toBe(true) expect(wrapper.find('[data-testid="content"]').text()).toBe('CONTENT') expect(wrapper.find('[data-testid="dr"]').exists()).toBe(true) }) it('applies the collapsed modifier from useShellUiStore', async () => { const pinia = createPinia() const wrapper = mount(AppShellV2, { global: { plugins: [pinia] } }) const { useShellUiStore } = await import('@/stores/useShellUiStore') useShellUiStore().toggleSidebar() await wrapper.vm.$nextTick() expect(wrapper.find('[data-testid="appshell-v2"]').classes()).toContain('is-collapsed') }) }) ``` - [ ] **Step 2: Run it to verify it fails** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run --project component tests/component/layouts/AppShellV2.spec.ts` Expected: FAIL — cannot resolve `@/layouts/components/AppShellV2.vue`. - [ ] **Step 3: Write `AppShellV2.vue`** Create `apps/app/src/layouts/components/AppShellV2.vue`: ```vue ``` - [ ] **Step 4: Run the test to verify it passes** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vitest run --project component tests/component/layouts/AppShellV2.spec.ts` Expected: PASS — 2 tests. - [ ] **Step 5: Write `OrganizerLayoutV2.vue`** Create `apps/app/src/layouts/OrganizerLayoutV2.vue` (must live in `src/layouts/` — MetaLayouts `target`): ```vue ``` - [ ] **Step 6: Typecheck** Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | grep -E 'AppShellV2|OrganizerLayoutV2' | head` Expected: no output (no type errors in the new files). - [ ] **Step 7: Commit** ```bash cd /Users/berthausmans/Documents/Development/crewli git add apps/app/src/layouts/OrganizerLayoutV2.vue apps/app/src/layouts/components/AppShellV2.vue apps/app/tests/component/layouts/AppShellV2.spec.ts git commit -m "feat(layouts): add OrganizerLayoutV2 + AppShellV2 skeleton" ``` --- ## Task 9: Boot proof — `/v2/dashboard` + full gate **Files:** - Modify: `apps/app/src/pages-v2/dashboard.vue` (flesh out from the Task 3 stub) - Test: `apps/app/tests/playwright-ct/v2/appshell-boot.spec.ts` - [ ] **Step 1: Flesh out the page** Replace the contents of `apps/app/src/pages-v2/dashboard.vue`: ```vue ``` - [ ] **Step 2: Write the failing CT smoke (mounts the skeleton + page body)** Create `apps/app/tests/playwright-ct/v2/appshell-boot.spec.ts`: ```ts import { expect, test } from '@playwright/experimental-ct-vue' import { createTestingPinia } from '@pinia/testing' import AppShellV2 from '@/layouts/components/AppShellV2.vue' test('AppShellV2 mounts and renders content in the CT runner', async ({ mount }) => { const component = await mount(AppShellV2, { global: { plugins: [createTestingPinia({ stubActions: false })] }, slots: { default: '
v2 foundation OK
' }, }) await expect(component.getByTestId('appshell-v2')).toBeVisible() await expect(component.getByTestId('v2-dashboard')).toContainText('v2 foundation OK') }) ``` - [ ] **Step 3: Run the smoke (integration test — depends on Task 8)** This is an integration smoke, not unit TDD: it proves the CT runner can mount the Plan-1 skeleton built in Task 8. There is no artificial red phase — the test file is the new artifact; if `AppShellV2` were absent the import would fail. Run: `cd /Users/berthausmans/Documents/Development/crewli/apps/app && grep '@pinia/testing' package.json && pnpm exec playwright test --config=playwright-ct.config.ts tests/playwright-ct/v2/appshell-boot.spec.ts 2>&1 | tail -6` Expected: `@pinia/testing` is present in package.json (it is, `^1.0.3`); result `1 passed`. If the first CT run reports the Chromium browser is missing, run `pnpm exec playwright install chromium` once and re-run. - [ ] **Step 4: Run the full foundation gate** Run each; all must pass: ```bash cd /Users/berthausmans/Documents/Development/crewli/apps/app pnpm exec vue-tsc --noEmit -p tsconfig.json # typecheck: 0 new errors pnpm exec eslint src/pages-v2 src/components-v2 src/stores/useShellUiStore.ts src/composables/useRightDrawer.ts # boundaries + v2 layout rule clean pnpm exec vitest run src/stores/__tests__/useShellUiStore.spec.ts src/composables/__tests__/useRightDrawer.spec.ts src/plugins/1.router/__tests__/v2RouteName.spec.ts tests/unit/boundaries-v2.spec.ts pnpm exec vitest run --project component tests/component/layouts/AppShellV2.spec.ts pnpm exec vite build # production build succeeds ``` Expected: typecheck 0 new errors; eslint 0 errors; all vitest specs pass; build succeeds with a `/v2/dashboard` → `v2-dashboard` entry in `typed-router.d.ts`. - [ ] **Step 5: Commit** ```bash cd /Users/berthausmans/Documents/Development/crewli git add apps/app/src/pages-v2/dashboard.vue apps/app/tests/playwright-ct/v2/appshell-boot.spec.ts git commit -m "feat(v2): boot /v2/dashboard through OrganizerLayoutV2 + AppShellV2" ``` --- ## Definition of Done (Plan 1) - New RFC committed; F4a–F4d banner + PRIMEVUE_COMPONENTS pointer added. - `/v2/dashboard` renders via `OrganizerLayoutV2` → `AppShellV2`; its route name is `v2-dashboard` (no collision with v1 `dashboard`). - `useShellUiStore` + `useRightDrawer()` unit-tested; drawer state is store-backed (CT-drivable). - New boundary zones active; back-port v1→`components-v2` is an error. - A `pages-v2/**` page missing the `OrganizerLayoutV2` layout meta is an ESLint error. - `pnpm exec vue-tsc --noEmit`, `pnpm lint`, `pnpm test`, the CT smoke, and `pnpm exec vite build` all pass. Existing test count not reduced. - No `src/components-v2/forms/` directory created (FormField reused via the `components-foundation` bridge). --- ## Subsequent plans (authored after Plan 1 lands) - **Plan 2 — Shell pieces:** `AppSidebar`, `SidebarHeader`, `SidebarNav`, `WorkspaceSwitcher` (PrimeVue `Popover` + computed over `useAuthStore`/`useOrganisationStore`), `AppTopbar` (PrimeVue `Breadcrumb`/`Button`/`Avatar`/`Menu`/`OverlayBadge`), `RightDrawer` (PrimeVue `Drawer` + scaffold, driven by `useRightDrawer()`), `AppDialog` (PrimeVue `Dialog` + scaffold). Each: Vitest mount + a Playwright-CT `@visual` baseline captured after parity-check vs crewli-starter. Fills the `OrganizerLayoutV2` slots. - **Plan 3 — Tier-1 primitives + DraggableBlock:** `StatusTag` (+ `statusSeverity.ts` map seeded from `src/types/` enums), `StatCard`, `StateBlock`, `PageHead`, `TagsInput`, `EnergyDots`, `EnergyPicker`, and `DraggableBlock` (foundation despite Tier-4 deferring the Timetable/Cue pages — spec §8/§9). Co-located `.stories.ts` each. - **Plan 4 — Template layer:** `ListTemplate`, `FormTemplate`, `DetailTemplate`, `DashboardTemplate`, `StateBlock` integration. - **Plan 5 — Storybook catalog + toolbar:** global theme/density toolbar decorators in `.storybook/preview.ts`; the ~80-component PrimeVue standard catalog (grouped per crewli-starter `ComponentsPage.vue`); Foundations stories. CT specs stay standalone (no `@storybook/test-runner`), per spec §13. Then the Smart-Filter sub-sprint, then Page-1 (events list), then the remaining page trees, per spec §10. ```