From ae85d212dba72434468607500b6b5f7ad4b4f06f Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Wed, 27 May 2026 22:23:54 +0200 Subject: [PATCH] feat(board): canonical board definition and BoardView (T2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encode the board as blocks of stacked-section columns (C1) and render it as the ASCII-styled DOM grid (C2) with hover highlight and a select emit. Layout mirrors the physical board (Client Satisfaction above Product Architecture; Macroplan above Defect Visualisation, both left of Feature Kanban) and boxes use true ISO 216 proportions (1:√2, each step up scaled by √2). Both views render from the one definition; tests pin section presence, click, hover, layout order, and a snapshot. --- app/board/definition.ts | 65 ++++++++++++++ app/components/BoardView.vue | 112 +++++++++++++++++++++++++ app/pages/dashboard.vue | 1 + app/pages/index.vue | 9 +- tasks/todo.md | 2 +- tests/__snapshots__/board.spec.ts.snap | 32 +++++++ tests/board.spec.ts | 52 ++++++++++++ 7 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 app/board/definition.ts create mode 100644 app/components/BoardView.vue create mode 100644 tests/__snapshots__/board.spec.ts.snap create mode 100644 tests/board.spec.ts diff --git a/app/board/definition.ts b/app/board/definition.ts new file mode 100644 index 0000000..9686b54 --- /dev/null +++ b/app/board/definition.ts @@ -0,0 +1,65 @@ +// The canonical board (C1) — the single source both apps render from (F1). +// Section `id`s are STABLE: defects reference them, so renaming a label is safe +// but changing an id orphans existing defects. Add/rename here, then redeploy. +// +// Each block lays its sections out as COLUMNS of vertically-stacked sections, +// mirroring the physical board (e.g. Macroplan above Defect Visualisation, both +// to the left of Feature Kanban). + +export type PaperSize = 'A2' | 'A3' | 'A4' + +export interface Section { + id: string + label: string + size: PaperSize +} + +export interface Block { + id: string + title: string + /** Left-to-right columns; each column is top-to-bottom sections. */ + columns: Section[][] +} + +export const board: Block[] = [ + { + id: 'value-for-customer', + title: 'Value for Customer', + columns: [ + [ + { id: 'client-satisfaction', label: 'Client Satisfaction', size: 'A4' }, + { id: 'product-architecture', label: 'Product Architecture', size: 'A4' }, + ], + ], + }, + { + id: 'right-first-time-jit', + title: 'Right First Time & Just in Time', + columns: [ + [ + { id: 'macroplan', label: 'Macroplan', size: 'A4' }, + { id: 'defect-visualisation', label: 'Defect Visualisation', size: 'A4' }, + ], + [{ id: 'feature-kanban', label: 'Feature Kanban', size: 'A2' }], + ], + }, + { + id: 'tech-enabled-network', + title: 'Tech-Enabled Network of Teams', + columns: [[{ id: 'tech-working-condition', label: 'Tech Working Condition', size: 'A3' }]], + }, + { + id: 'learning-organisation', + title: 'Building a Learning Organisation', + columns: [ + [{ id: 'problem-solving', label: 'Problem Solving', size: 'A3' }], + [{ id: 'weak-point-management', label: 'Weak Point Management', size: 'A3' }], + ], + }, +] + +/** Flat list of every section, for lookups and validation. */ +export const sections: Section[] = board.flatMap((block) => block.columns.flat()) + +/** Set of valid section ids — used server-side to validate filed defects. */ +export const sectionIds: ReadonlySet = new Set(sections.map((s) => s.id)) diff --git a/app/components/BoardView.vue b/app/components/BoardView.vue new file mode 100644 index 0000000..3ca7130 --- /dev/null +++ b/app/components/BoardView.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/app/pages/dashboard.vue b/app/pages/dashboard.vue index 213fd62..d50b8b5 100644 --- a/app/pages/dashboard.vue +++ b/app/pages/dashboard.vue @@ -1,6 +1,7 @@ diff --git a/app/pages/index.vue b/app/pages/index.vue index 28d98b4..4065dd3 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,6 +1,13 @@ + + diff --git a/tasks/todo.md b/tasks/todo.md index 7c300e5..b7f752a 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -6,7 +6,7 @@ review. ## Phase 1 — Foundation & board spine - [x] **T1** Scaffold Nuxt 4 + tooling + Dockerised dev (M) — _deps: none_ -- [ ] **T2** Board-definition module (C1) + BoardView (C2) — F1 (M) — _deps: T1_ +- [x] **T2** Board-definition module (C1) + BoardView (C2) — F1 (M) — _deps: T1_ - [ ] ⛳ **Checkpoint:** build + tests pass, board interactive, human review ## Phase 2 — File a defect (G1) diff --git a/tests/__snapshots__/board.spec.ts.snap b/tests/__snapshots__/board.spec.ts.snap new file mode 100644 index 0000000..7e2c8f6 --- /dev/null +++ b/tests/__snapshots__/board.spec.ts.snap @@ -0,0 +1,32 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`BoardView > matches the rendered board snapshot 1`] = ` +"
+
+

Value for Customer

+
+
+
+
+
+

Right First Time & Just in Time

+
+
+
+
+
+
+

Tech-Enabled Network of Teams

+
+
+
+
+
+

Building a Learning Organisation

+
+
+
+
+
+
" +`; diff --git a/tests/board.spec.ts b/tests/board.spec.ts new file mode 100644 index 0000000..3b88934 --- /dev/null +++ b/tests/board.spec.ts @@ -0,0 +1,52 @@ +// @vitest-environment nuxt +import { describe, it, expect } from 'vitest' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import BoardView from '../app/components/BoardView.vue' +import { board, sections } from '../app/board/definition' + +describe('BoardView', () => { + it('renders every block and section from the one definition', async () => { + const wrapper = await mountSuspended(BoardView) + for (const block of board) { + expect(wrapper.text()).toContain(block.title) + } + for (const section of sections) { + expect(wrapper.find(`[data-section-id="${section.id}"]`).exists()).toBe(true) + expect(wrapper.text()).toContain(section.label) + } + }) + + it('emits the stable section id on click', async () => { + const wrapper = await mountSuspended(BoardView) + await wrapper.find('[data-section-id="macroplan"]').trigger('click') + expect(wrapper.emitted('select')?.[0]).toEqual(['macroplan']) + }) + + it('highlights a section on hover', async () => { + const wrapper = await mountSuspended(BoardView) + const macroplan = wrapper.find('[data-section-id="macroplan"]') + expect(macroplan.classes()).not.toContain('is-hovered') + await macroplan.trigger('mouseenter') + expect(macroplan.classes()).toContain('is-hovered') + await macroplan.trigger('mouseleave') + expect(macroplan.classes()).not.toContain('is-hovered') + }) + + it('stacks and orders sections as on the physical board', async () => { + const wrapper = await mountSuspended(BoardView) + const order = wrapper + .findAll('[data-section-id]') + .map((el) => el.attributes('data-section-id')) + const at = (id: string) => order.indexOf(id) + // Product Architecture below Client Satisfaction (same column). + expect(at('client-satisfaction')).toBeLessThan(at('product-architecture')) + // Macroplan above Defect Visualisation, both left of Feature Kanban. + expect(at('macroplan')).toBeLessThan(at('defect-visualisation')) + expect(at('defect-visualisation')).toBeLessThan(at('feature-kanban')) + }) + + it('matches the rendered board snapshot', async () => { + const wrapper = await mountSuspended(BoardView) + expect(wrapper.html()).toMatchSnapshot() + }) +})