# 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 `
v2 dashboard
```
- [ ] **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
v2 shell (skeleton)
```
- [ ] **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
v2 foundation OK
AppShellV2 skeleton renders this route at /v2/dashboard.
```
- [ ] **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.
```