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
This commit is contained in:
1 parent
c82b112624
commit
1feea40684
148 files changed
+11230
-1706
No files matched your search
@@ -0,0 +1,144 @@
|
||||
#!/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.`);
|
||||
Reference in new issue
Block a user