fix(voyage): inline screenshot gallery loads docs/screenshots/ PNGs (31d28f65)

This commit is contained in:
Kjell Tore Guttormsen 2026-05-10 21:25:01 +02:00
commit 412b4561f5
5 changed files with 137 additions and 8 deletions

View file

@ -14,7 +14,7 @@
| 3 | Theme bootstrap IIFE | PASS | Group A test SC1.3; `data-theme` + `prefers-color-scheme` IIFE i HTML head |
| 4 | Onboarding-grid (redefinert) | PASS-redef | Group A test SC1.4; voyage bruker `fleet-grid` + `fleet-tile` istedenfor onboarding-grid (scope-guardian SC-GAP-1, Assumption #21) |
| 5 | A11Y-panel | PASS | Group A test SC1.5; `guide-panel--info` + `key-stats` + `findings__items` (Wave 5 Step 22) |
| 6 | Screenshots-spor (redefinert) | PASS-redef | Group A test SC1.6; `window.__voyage` hooks + `docs/screenshots/README.md` istedenfor inline gallery (scope-guardian SC-GAP-2, Assumption #22) |
| 6 | Screenshots-spor | PASS | Group D test SC1.6; `loadProjectDirectory` leser `docs/screenshots/**/*.png` via `readAsDataURL` (2 MB cap) og `renderScreenshotGallery` mounter en `<figure>`-grid i dashboardet. Erstatter scope-guardian SC-GAP-2 PASS-redef med faktisk inline gallery (v4.3 Step 8, finding 31d28f65). |
| 7 | Body typografi | PASS | Group A test SC1.7; `var(--font-size-*)` + `var(--font-family-mono)` brukt gjennomgående |
| 8 | Spacing rhythm | PASS | Group A test SC1.8; ≥5 distinkte `var(--space-N)` referanser |
| 9 | Color-token fidelity | PASS | Group A test SC1.9; `badge--scope-voyage` + `--color-scope-voyage` brukt |
@ -57,13 +57,12 @@
- **Test:** `SC1.5 A11Y panel — guide-panel--info + key-stats + findings`
- **Status:** PASS
### Element 6 — Screenshots-spor (REDEFINERT)
### Element 6 — Screenshots-spor
- **Bokstavelig krav:** llm-security har inline gallery med thumbnail-grid for case-studies
- **Voyage-tolkning:** Inline gallery deferret. Wave 5 Step 23 leverer `window.__voyage` hooks-spor (`navigate`, `scheduleRender`, `getProjectArtifacts`) + `docs/screenshots/README.md` mappe-konvensjon
- **Implementering:** Hooks brukes av Playwright-spec (Step 30); manuell screenshot-prosedyre dokumentert i `docs/screenshots/README.md`
- **Test:** `SC1.6 screenshots-spor — window.__voyage hooks + docs convention`
- **Status:** PASS-redef (scope-guardian SC-GAP-2, Assumption #22 — operatør-sign-off ved sesjon-start)
- **Risk:** Samme som Element 4 — `/trekreview` kan kreve faktisk gallery-komponent.
- **Voyage-implementering (v4.3 Step 8, finding 31d28f65):** `loadProjectDirectory` detekterer `docs/screenshots/**/*.png` via path-prefix-match, leser dem via `FileReader.readAsDataURL()` med 2 MB per-bilde cap (overskridelse annonseres via aria-live). `renderScreenshotGallery(screenshots)` bygger en `<figure>`-grid med `<img src="<dataUrl>" alt="<filename>">` som mountes under `fleet-grid` i `renderDashboard`. Erstatter den tidligere PASS-redef-tolkningen — voyage har nå et faktisk inline gallery.
- **Beholder:** `window.__voyage` hooks fra Wave 5 Step 23 (`navigate`, `scheduleRender`, `getProjectArtifacts`) + `docs/screenshots/README.md` mappe-konvensjon — fortsatt brukt av Playwright-spec og av operatør for manuell screenshot-prosedyre.
- **Test:** Group D Playwright-test `SC1.6 inline gallery — data:image PNGs rendered (31d28f65)` injiserer en fixture-artifact via `scheduleRender` og asserter `#voyage-dashboard img[src^="data:image/png"]` count > 0.
- **Status:** PASS (inline gallery implementert; scope-guardian SC-GAP-2 lukket).
### Element 7 — Body typografi
- **Bokstavelig krav:** typografi-skala bruker DS-tokens, ikke literal pixel-verdier

View file

@ -844,6 +844,42 @@
}
.voyage-back-btn:hover { background: var(--color-bg-soft); }
/* v4.3 Step 8 — inline screenshot gallery (finding 31d28f65).
Renders below the fleet-grid in the dashboard. <figure> grid with
responsive auto-fit columns at 240px min; <figcaption> shows the
relative path so operators can correlate to docs/screenshots/. */
.voyage-screenshot-gallery { margin-top: var(--space-4); }
.voyage-screenshot-gallery h3 {
margin: 0 0 var(--space-3) 0;
font-size: var(--font-size-lg);
color: var(--color-text-primary);
}
.voyage-screenshot-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--space-3);
}
.voyage-screenshot {
margin: 0;
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
background: var(--color-surface);
overflow: hidden;
}
.voyage-screenshot img {
display: block;
width: 100%;
height: auto;
}
.voyage-screenshot figcaption {
padding: var(--space-2) var(--space-3);
font-family: var(--font-family-mono);
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
border-top: 1px solid var(--color-border-subtle);
word-break: break-all;
}
/* v4.3 remediation: color-contrast fix for finding 09132940.
The vendored DS sets `.key-stat__label` to var(--color-text-tertiary)
which is #6E7781 in light theme — borderline 4.5:1 WCAG-AA contrast
@ -1455,6 +1491,10 @@ playground first-run shows a complete round-trip-able artifact.
progress: null,
research: [],
architecture: { overview: null, gaps: null, looseFiles: [] },
// v4.3 Step 8 — inline screenshot gallery (finding 31d28f65).
// `screenshots` is populated below from docs/screenshots/**/*.png
// entries. Each item: { path, dataUrl }.
screenshots: [],
looseFiles: []
};
@ -1480,13 +1520,38 @@ playground first-run shows a complete round-trip-able artifact.
else if (parts.length === 2 && parts[0] === 'architecture' && name === 'overview.md') bucket = 'arch_overview';
else if (parts.length === 2 && parts[0] === 'architecture' && name === 'gaps.md') bucket = 'arch_gaps';
else if (parts.length === 2 && parts[0] === 'architecture' && /\.md$/.test(name)) bucket = 'arch_loose';
// v4.3 Step 8 — docs/screenshots/**/*.png → inline gallery
else if (parts[0] === 'docs' && parts[1] === 'screenshots' && /\.png$/i.test(name)) bucket = 'screenshot';
else bucket = 'loose';
classified.push({ file: f, rel: inside, bucket: bucket });
}
// Phase 2 — read content + parse frontmatter (async).
// v4.3 Step 8 — `screenshot` bucket uses readAsDataURL with a 2 MB
// per-image cap (finding 31d28f65); oversized PNGs are skipped
// with an aria-live announce so AT users know what was suppressed.
var SCREENSHOT_MAX_BYTES = 2 * 1024 * 1024;
for (var j = 0; j < classified.length; j++) {
var c = classified[j];
if (c.bucket === 'screenshot') {
if (c.file && typeof c.file.size === 'number' && c.file.size > SCREENSHOT_MAX_BYTES) {
announce('Hopper over for stort screenshot: ' + c.rel);
continue;
}
var dataUrl = '';
try {
dataUrl = await new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function () { resolve(String(reader.result || '')); };
reader.onerror = function () { reject(reader.error); };
reader.readAsDataURL(c.file);
});
} catch (_) { dataUrl = ''; }
if (dataUrl) {
artifacts.screenshots.push({ path: c.rel, dataUrl: dataUrl });
}
continue;
}
var text = '';
try { text = await c.file.text(); } catch (_) { text = ''; }
var fm = quickParseFrontmatter(text);
@ -1504,6 +1569,7 @@ playground first-run shows a complete round-trip-able artifact.
}
}
artifacts.research.sort(function (a, b) { return a.path.localeCompare(b.path); });
artifacts.screenshots.sort(function (a, b) { return a.path.localeCompare(b.path); });
// Phase 3 — deterministic storage-key from basePath (NOT URL params)
// so draft-annotation state is isolated per project.
@ -1807,6 +1873,26 @@ playground first-run shows a complete round-trip-able artifact.
return parts[parts.length - 1] || p;
}
// v4.3 Step 8 — inline screenshot gallery (finding 31d28f65).
// Builds a <figure> grid from the screenshots[] array populated by
// loadProjectDirectory. Each item renders as a data:image/png <img>
// wrapped in a <figure>/<figcaption>. Returns an empty string when
// there are no screenshots so the dashboard layout stays clean.
function renderScreenshotGallery(screenshots) {
if (!screenshots || !screenshots.length) return '';
var items = screenshots.map(function (s) {
var name = (s.path || '').split('/').pop() || s.path || '';
return '<figure class="voyage-screenshot">' +
'<img src="' + s.dataUrl + '" alt="' + escapeHtml(name) + '" loading="lazy">' +
'<figcaption>' + escapeHtml(s.path) + '</figcaption>' +
'</figure>';
}).join('');
return '<section class="voyage-screenshot-gallery" aria-label="Screenshots">' +
'<h3>Screenshots</h3>' +
'<div class="voyage-screenshot-grid">' + items + '</div>' +
'</section>';
}
function renderDashboard(projectArtifacts, slot) {
var host = slot || $('voyage-dashboard');
if (!host) return;
@ -1828,7 +1914,8 @@ playground first-run shows a complete round-trip-able artifact.
}).join('');
var projectName = shortenBasePath(projectArtifacts.basePath);
var bodyHtml = '<div class="fleet-grid">' + tilesHtml + '</div>';
var galleryHtml = renderScreenshotGallery(projectArtifacts.screenshots || []);
var bodyHtml = '<div class="fleet-grid">' + tilesHtml + '</div>' + galleryHtml;
host.innerHTML = renderPageShell({
eyebrow: 'Project dashboard',
title: projectName,

View file

@ -145,6 +145,38 @@ test.describe('voyage-playground a11y (axe-core)', () => {
expect(nodes, `color-contrast violations remain: ${JSON.stringify(nodes, null, 2)}`).toEqual([]);
});
// v4.3 Step 8 — inline screenshot gallery (finding 31d28f65).
// Injects a pre-built artifacts object with screenshots[] via the
// window.__voyage.scheduleRender hook (avoids webkitdirectory which
// is not programmatically triggerable). Asserts the dashboard renders
// at least one data:image PNG <img> tag.
test('SC1.6 inline gallery — data:image PNGs rendered (31d28f65)', async ({ page }) => {
await page.goto('voyage-playground.html');
await page.waitForLoadState('domcontentloaded');
// 1×1 transparent PNG (same base64 as the fixture file)
const SAMPLE_DATA_URL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==';
await page.evaluate((dataUrl) => {
window.__voyage.scheduleRender({
artifacts: {
basePath: 'fixture-project',
storageKey: 'voyage_proj_fixture',
brief: { path: 'brief.md', content: '# Fixture', frontmatter: {} },
plan: null,
review: null,
progress: null,
research: [],
architecture: { overview: null, gaps: null, looseFiles: [] },
screenshots: [{ path: 'docs/screenshots/dashboard/sample.png', dataUrl: dataUrl }],
looseFiles: [],
},
});
}, SAMPLE_DATA_URL);
// The gallery is rendered inside #voyage-dashboard
const imgCount = await page.locator('#voyage-dashboard img[src^="data:image/png"]').count();
expect(imgCount, 'expected at least one data:image/png <img> in the gallery').toBeGreaterThan(0);
});
test('pixel-diff smoke 1280×900 — light + dark within 2% threshold (SC1 backup)', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 900 });
// Light theme baseline

View file

@ -0,0 +1,11 @@
---
task: Screenshot gallery fixture for Group D test
slug: screenshot-project
project_dir: tests/fixtures/screenshot-project
---
# Screenshot fixture brief
Minimal brief.md so `loadProjectDirectory` reaches its render phase
without emitting the "brief.md mangler" warning. Real verification
is the data:image PNG count assertion in the Group D test.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B