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

@ -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,