From 2a4e058684a50ffd619f409f824f588dc91f862d Mon Sep 17 00:00:00 2001 From: Jarvis Jr Hermes Date: Tue, 6 Oct 2026 07:25:53 +0000 Subject: [PATCH] fix: [verified] simplify Roadmap adoption timeline --- .../strategy-roadmap-mobile.workflow.json | 33 ++--- diagrams/strategy-roadmap.workflow.json | 25 ++-- package.json | 2 +- scripts/browser/roadmap-timeline.cjs | 129 ++++++++++++++++++ scripts/roadmap-viewer.mjs | 17 +-- scripts/roadmap-viewer.test.mjs | 2 +- scripts/roadmap.test.mjs | 7 +- src/components/ArchifyDiagram/index.tsx | 11 +- src/css/custom.css | 13 +- 9 files changed, 185 insertions(+), 54 deletions(-) create mode 100644 scripts/browser/roadmap-timeline.cjs diff --git a/diagrams/strategy-roadmap-mobile.workflow.json b/diagrams/strategy-roadmap-mobile.workflow.json index 29e878b..142e30f 100644 --- a/diagrams/strategy-roadmap-mobile.workflow.json +++ b/diagrams/strategy-roadmap-mobile.workflow.json @@ -30,11 +30,10 @@ "col": 0, "type": "backend", "label": "Understand today", - "sublabel": "Assess code quality and real delivery.\nOutput: baseline, process map, prioritized gaps.", - "tag": "Exit: Workflow understood", + "sublabel": "Code quality and delivery processes understood.", "width": 340, - "height": 460, - "yOffset": -1000 + "height": 240, + "yOffset": -560 }, { "id": "readiness", @@ -42,11 +41,10 @@ "col": 0, "type": "backend", "label": "Prepare foundations", - "sublabel": "Make the pilot safe.\nOutput: readiness decision and pilot scope.", - "tag": "Exit: Critical gaps closed", + "sublabel": "Critical pilot gaps closed.", "width": 340, - "height": 460, - "yOffset": -500 + "height": 240, + "yOffset": -280 }, { "id": "pilot", @@ -54,10 +52,9 @@ "col": 0, "type": "backend", "label": "Pilot", - "sublabel": "Try reviewed skills.\nOutput: results compared with the baseline.", - "tag": "Exit: Continue, adjust or stop", + "sublabel": "AI skills tested against the baseline.", "width": 340, - "height": 460, + "height": 240, "yOffset": 0 }, { @@ -66,11 +63,10 @@ "col": 0, "type": "backend", "label": "Expand", - "sublabel": "Add a workflow or team.\nOutput: evidence for the next scope.", - "tag": "Exit: Controls proven", + "sublabel": "Next workflow justified by evidence.", "width": 340, - "height": 460, - "yOffset": 500 + "height": 240, + "yOffset": 280 }, { "id": "sustain", @@ -78,11 +74,10 @@ "col": 0, "type": "backend", "label": "Sustain", - "sublabel": "Review and improve.\nOutput: maintained skills and refreshed backlog.", - "tag": "Exit: Reassess when changed", + "sublabel": "Skills reviewed and improved.", "width": 340, - "height": 460, - "yOffset": 1000 + "height": 240, + "yOffset": 560 } ], "edges": [ diff --git a/diagrams/strategy-roadmap.workflow.json b/diagrams/strategy-roadmap.workflow.json index 0499a08..1d96f63 100644 --- a/diagrams/strategy-roadmap.workflow.json +++ b/diagrams/strategy-roadmap.workflow.json @@ -30,10 +30,9 @@ "col": 0, "type": "backend", "label": "Understand today", - "sublabel": "Assess code quality and real delivery.\nOutput: baseline, process map, prioritized gaps.", - "tag": "Exit: Workflow understood", + "sublabel": "Code quality and delivery processes understood.", "width": 340, - "height": 460 + "height": 240 }, { "id": "readiness", @@ -41,10 +40,9 @@ "col": 1, "type": "backend", "label": "Prepare foundations", - "sublabel": "Make the pilot safe.\nOutput: readiness decision and pilot scope.", - "tag": "Exit: Critical gaps closed", + "sublabel": "Critical pilot gaps closed.", "width": 340, - "height": 460 + "height": 240 }, { "id": "pilot", @@ -52,10 +50,9 @@ "col": 2, "type": "backend", "label": "Pilot", - "sublabel": "Try reviewed skills.\nOutput: results compared with the baseline.", - "tag": "Exit: Continue, adjust or stop", + "sublabel": "AI skills tested against the baseline.", "width": 340, - "height": 460 + "height": 240 }, { "id": "expansion", @@ -63,10 +60,9 @@ "col": 3, "type": "backend", "label": "Expand", - "sublabel": "Add a workflow or team.\nOutput: evidence for the next scope.", - "tag": "Exit: Controls proven", + "sublabel": "Next workflow justified by evidence.", "width": 340, - "height": 460 + "height": 240 }, { "id": "sustain", @@ -74,10 +70,9 @@ "col": 4, "type": "backend", "label": "Sustain", - "sublabel": "Review and improve.\nOutput: maintained skills and refreshed backlog.", - "tag": "Exit: Reassess when changed", + "sublabel": "Skills reviewed and improved.", "width": 340, - "height": 460 + "height": 240 } ], "edges": [ diff --git a/package.json b/package.json index aa22bcc..df229ac 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "verify:build": "node scripts/verify.mjs --build", "diagrams:build": "node scripts/build-diagrams.mjs", "test": "node --test scripts/*.test.mjs", - "test:browser": "node scripts/browser/strategy-story.cjs && node scripts/browser/inline-controls.cjs && node scripts/browser/sdlc-cycle.cjs && node scripts/browser/target-midpoints.cjs && node scripts/browser/skills-measurement.cjs && node scripts/browser/roadmap.cjs", + "test:browser": "node scripts/browser/strategy-story.cjs && node scripts/browser/inline-controls.cjs && node scripts/browser/sdlc-cycle.cjs && node scripts/browser/target-midpoints.cjs && node scripts/browser/skills-measurement.cjs && node scripts/browser/roadmap.cjs && node scripts/browser/roadmap-timeline.cjs", "kpi:example": "node scripts/ai-kpi.mjs" }, "dependencies": { diff --git a/scripts/browser/roadmap-timeline.cjs b/scripts/browser/roadmap-timeline.cjs new file mode 100644 index 0000000..196d2bd --- /dev/null +++ b/scripts/browser/roadmap-timeline.cjs @@ -0,0 +1,129 @@ +const {chromium} = require('playwright'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const ids = ['assessment','readiness','pilot','expansion','sustain']; +(async () => { + const base = process.argv[2] || 'http://127.0.0.1:4173'; + const output = `.cache/roadmap-timeline-${process.argv[3] || 'local'}`; + fs.mkdirSync(output, {recursive:true}); + const browser = await chromium.launch({headless:true, executablePath:process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE, args:['--no-sandbox']}); + const results = []; + const inspect = async (frame, vertical) => { + await frame.locator('svg[role="img"]').waitFor(); + const geometry = await frame.locator('svg[role="img"]').evaluate(svg => ({ + width:innerWidth, height:innerHeight, + cards:[...svg.querySelectorAll('[data-node-id]')].map(node => ({ + id:node.dataset.nodeId, box:node.querySelector('rect').getBoundingClientRect().toJSON(), + number:node.querySelector('[data-stage-number]').textContent, + numberColor:getComputedStyle(node.querySelector('[data-stage-number]')).fill, + titleColor:getComputedStyle(node.querySelector('[data-node-label]')).fill, + text:[...node.querySelectorAll('text')].map(text => ({value:text.textContent, box:text.getBoundingClientRect().toJSON(), size:parseFloat(getComputedStyle(text).fontSize)*text.getScreenCTM().a})) + })) + })); + geometry.cards.sort((a,b) => a.box.y-b.box.y || a.box.x-b.box.x); + assert.deepEqual(geometry.cards.map(n => n.id), ids); + assert.deepEqual(geometry.cards.map(n => n.number), ['1','2','3','4','5']); + for (const [index, card] of geometry.cards.entries()) { + assert.equal(card.text.length, 3, 'number, title and one outcome only'); + assert.equal(card.numberColor,card.titleColor,'stage number must inherit readable theme color'); + assert(card.box.left >= 0 && card.box.top >= 0 && card.box.right <= geometry.width + 1 && card.box.bottom <= geometry.height + 1, `outside iframe: ${card.id}`); + for (const text of card.text) { + assert(text.size >= 13, `small text: ${card.id} ${text.size}`); + assert(!/OUTPUT|Exit:/.test(text.value)); + assert(text.box.left >= card.box.left && text.box.right <= card.box.right + 1 && text.box.top >= card.box.top && text.box.bottom <= card.box.bottom + 1, `clipped: ${card.id}`); + } + for (let t=1; t errors.push(error.message)); + assert.equal((await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'})).status(),200); + const panel = page.locator('[data-diagram="strategy-roadmap"]'); + const iframe = panel.locator(width < 1360 ? '.story-mobile' : '.story-desktop'); + const frame = await (await iframe.elementHandle()).contentFrame(); + await frame.locator('svg[role="img"]').waitFor(); + const opener = panel.getByRole('button', {name:/^Expand /}); + for (const theme of ['light','dark']) { + if (await frame.locator('html').getAttribute('data-theme') !== theme) await frame.locator('#btn-theme').click(); + await page.waitForFunction(t => document.documentElement.dataset.theme === t, theme); + const inline = await inspect(frame,width < 1360); + await panel.screenshot({path:`${output}/${width}-${theme}-inline.png`}); + await opener.focus(); await page.keyboard.press('Enter'); + const close = panel.getByRole('button', {name:'Close full-screen diagram', exact:true}); + await close.waitFor({state:'visible'}); + const fullscreen = await (await panel.locator('dialog iframe').elementHandle()).contentFrame(); + await fullscreen.waitForFunction(t => document.documentElement.dataset.theme === t, theme); + const expanded = await inspect(fullscreen,width < 1000); + await page.screenshot({path:`${output}/${width}-${theme}-fullscreen.png`}); + await page.keyboard.press('Escape'); + await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open); + assert(await opener.evaluate(n => n === document.activeElement)); + assert(!await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)); + results.push({width,theme,inline,expanded}); + fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2)); + } + assert.deepEqual(errors,[]); + } finally {await context.close();} + } + assert.equal(results.length,8); + for (const viewport of [{width:390,height:667},{width:320,height:568}]) { + const context = await browser.newContext({viewport}); + try { + const page = await context.newPage(); + await page.goto(`${base}/eldorado-v1/documentation/strategy/roadmap/`, {waitUntil:'networkidle'}); + const panel = page.locator('[data-diagram="strategy-roadmap"]'); + const opener = panel.getByRole('button', {name:/^Expand /}); + await opener.focus(); await page.keyboard.press('Enter'); + const modal = panel.locator('dialog'); + const close = modal.getByRole('button', {name:'Close full-screen diagram', exact:true}); + await close.waitFor({state:'visible'}); + const iframe = modal.locator('iframe'); + const frame = await (await iframe.elementHandle()).contentFrame(); + const geometry = await inspect(frame,true); + assert(await modal.evaluate(n => n.scrollHeight > n.clientHeight), 'short fullscreen must scroll'); + for (const card of geometry.cards) { + await modal.evaluate((n, y) => {n.scrollTop = y;}, card.box.top); + const frameBox = await iframe.boundingBox(); + const toolbarBox = await modal.locator('.archify-modal-toolbar').boundingBox(); + const closeBox = await close.boundingBox(); + assert(closeBox.y >= 0 && closeBox.y + closeBox.height <= viewport.height, 'close remains visible'); + assert(frameBox.y + card.box.top >= toolbarBox.y + toolbarBox.height - 1 && frameBox.y + card.box.bottom <= viewport.height + 1, `stage unreachable: ${card.id}`); + } + // Controls stay reachable at the beginning of the scrollable viewer. + await modal.evaluate(n => {n.scrollTop = 0;}); + for (const selector of ['#btn-theme','#btn-preset','#btn-export']) { + const control = frame.locator(selector); + const box = await control.boundingBox(); + assert(box.x >= 0 && box.x + box.width <= viewport.width && box.y >= 0 && box.y + box.height <= viewport.height, `control outside viewport: ${selector}`); + await control.click(); + if (selector !== '#btn-theme') await control.click(); + } + await close.focus(); await page.keyboard.press('Escape'); + await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open); + await iframe.waitFor({state:'detached'}); + assert(await opener.evaluate(n => n === document.activeElement), 'Escape restores opener focus'); + await page.keyboard.press('Enter'); + await close.waitFor({state:'visible'}); + await close.focus(); await page.keyboard.press('Enter'); + await page.waitForFunction(() => !document.querySelector('[data-diagram="strategy-roadmap"] dialog').open); + assert(await opener.evaluate(n => n === document.activeElement), 'close button restores opener focus'); + results.push({viewport,scrollable:geometry}); + fs.writeFileSync(`${output}/results.json`,JSON.stringify(results,null,2)); + } finally {await context.close();} + } + assert.equal(results.length,10); + console.log('PASS 16 timeline contexts: 4 widths × 2 themes × inline/fullscreen; ordered readable text, no clipping, keyboard exit.'); + console.log('PASS short fullscreen 390×667 and 320×568: readable stages reachable by scroll, sticky close, accessible controls, keyboard close and focus restoration.'); + } finally {await browser.close();} +})().catch(error => {console.error(error); process.exitCode=1;}); diff --git a/scripts/roadmap-viewer.mjs b/scripts/roadmap-viewer.mjs index 49221c6..1e374ef 100644 --- a/scripts/roadmap-viewer.mjs +++ b/scripts/roadmap-viewer.mjs @@ -20,23 +20,19 @@ export function roadmapViewer(html, spec) { if (start < 0 || next < start || end < next) throw new Error(`Roadmap node drift: ${node.id}`); const original = html.slice(start, end); const rect = / { const lines = wrap(value, size === 30 ? 18 : 21); - const maxLines = top === 166 || top === 310 ? 3 : 2; + const maxLines = top === 160 ? 3 : 2; if (lines.length > maxLines || lines.some(line => line.length > 21)) throw new Error(`Roadmap copy exceeds card capacity: ${node.id}`); return `${lines.map((line,i) => `${escape(line)}`).join('')}`; }; - const [action, output] = node.sublabel.split('\nOutput: '); - if (!action || !output || !node.tag.startsWith('Exit: ')) throw new Error(`Roadmap content contract drift: ${node.id}`); - const content = text(String(index+1).padStart(2,'0'), 40, 26, 't-muted') + if (!node.sublabel || node.tag || /Output:|Exit:|\n/.test(node.sublabel)) throw new Error(`Roadmap content contract drift: ${node.id}`); + const content = text(String(index+1), 40, 26, 't-primary', 'data-stage-number="" font-weight="700"') + text(node.label, 88, 30, 't-primary', 'data-node-label="" font-weight="700"') - + text(action, 166, 26, 't-primary') - + text('OUTPUT', 274, 20, 't-muted', 'font-weight="700"') - + text(output, 310, 26, 't-primary') - + text(node.tag, 416, 24, 't-primary'); + + text(node.sublabel, 160, 26, 't-muted'); // Preserve native node title/metadata and rectangles; replace sigil/text only. // Sigils are nested SVG groups. Anchor on the two native node rectangles. const rectangles = [...original.matchAll(/]*\/>/g)]; @@ -58,7 +54,8 @@ export function roadmapViewer(html, spec) { [data-node-id] text {font-family: Arial, sans-serif;} svg[data-preset="classic"] [data-node-id] > rect.c-backend, svg[data-preset="editorial"] [data-node-id] > rect.c-backend {fill:var(--panel); stroke:var(--border);} - svg[data-preset="classic"] .c-grid {opacity:0;} + .c-grid {opacity:0;} + `; html = html.replace(roots[0][0], root + styles); return html; diff --git a/scripts/roadmap-viewer.test.mjs b/scripts/roadmap-viewer.test.mjs index de9636c..06e31aa 100644 --- a/scripts/roadmap-viewer.test.mjs +++ b/scripts/roadmap-viewer.test.mjs @@ -28,7 +28,7 @@ for (const suffix of ['', '-mobile']) test(`native Workflow${suffix} retains gra assert.equal(checked.composition.metrics.labelCanvasOverflowIssues, 0); assert.throws(() => roadmapViewer('', spec), /drift/); const oversized = structuredClone(spec); - oversized.nodes[0].sublabel = 'Too much copy '.repeat(20) + '\nOutput: baseline'; + oversized.nodes[0].sublabel = 'Too much copy '.repeat(20); assert.throws(() => roadmapViewer(native, oversized), /capacity/); assert.throws(() => roadmapViewer(native, {...spec,diagram_type:'architecture'}), /contract drift/); } finally {rmSync(dir, {recursive:true, force:true});} diff --git a/scripts/roadmap.test.mjs b/scripts/roadmap.test.mjs index 2761521..654c220 100644 --- a/scripts/roadmap.test.mjs +++ b/scripts/roadmap.test.mjs @@ -26,10 +26,9 @@ test('both roadmap diagrams make assessment and readiness prerequisites to the p assert.deepEqual(diagram.nodes.map(n => n.id), ids); assert.deepEqual(diagram.nodes.map(n => n.label), ['Understand today', 'Prepare foundations', 'Pilot', 'Expand', 'Sustain']); assert.deepEqual(diagram.edges.map(e => [e.from,e.to]), ids.slice(1).map((id, i) => [ids[i],id])); - assert.match(diagram.nodes[0].sublabel, /code quality and real delivery/); - assert.match(diagram.nodes[0].sublabel, /baseline, process map, prioritized gaps/); - assert.match(diagram.nodes[1].tag, /Critical gaps closed/); - assert(diagram.nodes.every(n => n.sublabel.includes('Output:') && n.tag.startsWith('Exit:'))); + assert.equal(diagram.nodes[0].sublabel, 'Code quality and delivery processes understood.'); + assert.equal(diagram.nodes[1].sublabel, 'Critical pilot gaps closed.'); + assert(diagram.nodes.every(n => !n.sublabel.includes('Output:') && !n.tag && n.height === 240)); assert.deepEqual(diagram.nodes.map(n => n.col), suffix ? [0,0,0,0,0] : [0,1,2,3,4]); assert.equal(diagram.lanes.length, 1); } diff --git a/src/components/ArchifyDiagram/index.tsx b/src/components/ArchifyDiagram/index.tsx index b454af1..880ff7c 100644 --- a/src/components/ArchifyDiagram/index.tsx +++ b/src/components/ArchifyDiagram/index.tsx @@ -11,6 +11,15 @@ export default function ArchifyDiagram({name, title, compact = false, story = fa const opener = useRef(null); const headingId = useId(); const [expanded, setExpanded] = useState(false); + const [narrowRoadmap, setNarrowRoadmap] = useState(false); + useEffect(() => { + if (name !== 'strategy-roadmap') return; + const media = window.matchMedia('(max-width: 999px)'); + const update = () => setNarrowRoadmap(media.matches); + update(); + media.addEventListener('change', update); + return () => media.removeEventListener('change', update); + }, [name]); const {colorMode, setColorMode} = useColorMode(); const initialTheme = useRef(colorMode); const desktop = useRef(null); @@ -65,7 +74,7 @@ export default function ArchifyDiagram({name, title, compact = false, story = fa :