diff --git a/dev-docs/superpowers/plans/2026-05-16-gui-redesign-foundation.md b/dev-docs/superpowers/plans/2026-05-16-gui-redesign-foundation.md new file mode 100644 index 00000000..8d611522 --- /dev/null +++ b/dev-docs/superpowers/plans/2026-05-16-gui-redesign-foundation.md @@ -0,0 +1,1186 @@ +# 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. +```