Files
clarity-helper/scripts/package-snapshot-gallery.mjs
jpremill 1feea40684 feat(ui): redesign voice cockpit with clinical companion drawer, Svelte 5 migration, and Gemini 3.8 voice
- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer
- Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support
- Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline
- Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots)
- Migrate core application to Svelte 5 runes (, , ) with reactive stores
- Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
2026-09-29 23:35:05 -04:00

145 lines
6.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Playwright Visual Snapshot Review Gallery Packager
*
* Scans docs/screenshots/<viewport>/*.png, generates an interactive,
* responsive HTML review gallery with viewport filters, comparison matrices,
* and high-DPI lightbox inspection.
*/
import fs from 'fs';
import path from 'path';
const screenshotsDir = path.resolve(process.cwd(), 'docs/screenshots');
const outputHtml = path.join(screenshotsDir, 'index.html');
if (!fs.existsSync(screenshotsDir)) {
console.log(`[Snapshot Gallery] No screenshots directory found at ${screenshotsDir}. Skipping gallery build.`);
process.exit(0);
}
const entries = [];
const viewports = fs.readdirSync(screenshotsDir, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => d.name);
for (const vp of viewports) {
const vpDir = path.join(screenshotsDir, vp);
const files = fs.readdirSync(vpDir).filter(f => f.endsWith('.png'));
for (const file of files) {
const filePath = path.join(vpDir, file);
const relPath = `${vp}/${file}`;
const stats = fs.statSync(filePath);
entries.push({
viewport: vp,
fileName: file,
title: file.replace(/^\d+-/, '').replace(/\.png$/, '').replace(/-/g, ' ').toUpperCase(),
relPath,
sizeKb: Math.round(stats.size / 1024)
});
}
}
entries.sort((a, b) => a.fileName.localeCompare(b.fileName));
const htmlContent = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cognitive Clarity — Playwright Visual Snapshot Gallery</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
.gallery-img { cursor: zoom-in; transition: transform 0.2s ease; }
.gallery-img:hover { transform: scale(1.01); }
</style>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen">
<header class="border-b border-slate-800 bg-slate-900/60 backdrop-blur-md sticky top-0 z-40 px-6 py-4">
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 class="text-xl font-bold tracking-tight text-white flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-teal-400 animate-pulse"></span>
Cognitive Clarity — Visual Snapshot Gallery
</h1>
<p class="text-xs text-slate-400 mt-0.5">
${entries.length} snapshots across ${viewports.length} viewports (${viewports.join(', ')})
</p>
</div>
<div class="flex flex-wrap gap-2" id="filter-buttons">
<button onclick="filterViewport('all')" class="vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-teal-600 text-white" data-vp="all">All</button>
${viewports.map(vp => `<button onclick="filterViewport('${vp}')" class="vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-slate-800 text-slate-300 hover:bg-slate-700" data-vp="${vp}">${vp}</button>`).join('')}
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-6 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="gallery-grid">
${entries.map(e => `
<div class="gallery-card bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden shadow-lg flex flex-col" data-viewport="${e.viewport}">
<div class="p-3 border-b border-slate-800 flex justify-between items-center text-xs">
<span class="font-bold text-slate-200">${e.title}</span>
<span class="px-2 py-0.5 rounded text-[10px] font-mono uppercase bg-slate-800 text-teal-400">${e.viewport}</span>
</div>
<div class="bg-black/40 flex-grow flex items-center justify-center p-2">
<img src="${e.relPath}" alt="${e.title}" class="gallery-img max-h-72 w-auto object-contain rounded" onclick="openLightbox('${e.relPath}', '${e.title}')">
</div>
<div class="p-2.5 bg-slate-900/80 border-t border-slate-800 flex justify-between items-center text-[10px] text-slate-500 font-mono">
<span>${e.fileName}</span>
<span>${e.sizeKb} KB</span>
</div>
</div>
`).join('')}
</div>
</main>
<!-- Lightbox Modal -->
<div id="lightbox" class="fixed inset-0 z-50 bg-black/90 backdrop-blur-md hidden flex flex-col items-center justify-center p-4" onclick="closeLightbox()">
<div class="max-w-6xl max-h-[90vh] flex flex-col items-center" onclick="event.stopPropagation()">
<div class="flex justify-between items-center w-full mb-3 text-white">
<h3 id="lightbox-title" class="font-bold text-base"></h3>
<button onclick="closeLightbox()" class="text-slate-400 hover:text-white px-3 py-1 bg-white/10 rounded-lg text-sm">Close ✕</button>
</div>
<img id="lightbox-img" src="" class="max-h-[80vh] max-w-full rounded-xl shadow-2xl border border-white/10 object-contain">
</div>
</div>
<script>
function filterViewport(vp) {
document.querySelectorAll('.vp-btn').forEach(btn => {
if (btn.dataset.vp === vp) {
btn.className = 'vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-teal-600 text-white';
} else {
btn.className = 'vp-btn px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider bg-slate-800 text-slate-300 hover:bg-slate-700';
}
});
document.querySelectorAll('.gallery-card').forEach(card => {
if (vp === 'all' || card.dataset.viewport === vp) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
}
function openLightbox(src, title) {
document.getElementById('lightbox-img').src = src;
document.getElementById('lightbox-title').innerText = title;
document.getElementById('lightbox').classList.remove('hidden');
}
function closeLightbox() {
document.getElementById('lightbox').classList.add('hidden');
}
</script>
</body>
</html>`;
fs.writeFileSync(outputHtml, htmlContent, 'utf-8');
console.log(`[Snapshot Gallery] Successfully generated review gallery at ${outputHtml} with ${entries.length} snapshots.`);