/** * Theme Screenshot Capture Script * WorkRoot IT Solutions — Dark/Light Mode Visual Reference * * Generates full-page screenshots of all major pages in both themes * across desktop, tablet, and mobile viewports. * * Usage: * 1. Start dev server: npm run dev * 2. Run this script: node scripts/capture-theme-screenshots.js * * Output: .agents/frontend-specialist/screenshots/{light,dark}/{page}-{viewport}.png * * Prerequisites: * npm install playwright * npx playwright install chromium */ const { chromium } = require('playwright'); const path = require('path'); const fs = require('fs'); // ─── Configuration ──────────────────────────────────────────────────────────── const BASE_URL = process.env.BASE_URL || 'http://localhost:4321'; const PAGES = [ { name: 'home', path: '/' }, { name: 'about', path: '/about' }, { name: 'services', path: '/services' }, { name: 'portfolio', path: '/portfolio' }, { name: 'contact', path: '/contact' }, ]; const VIEWPORTS = [ { name: 'desktop', width: 1440, height: 900 }, { name: 'tablet', width: 768, height: 1024 }, { name: 'mobile', width: 390, height: 844 }, ]; const THEMES = ['light', 'dark']; /** Milliseconds to wait after theme switch for transitions to settle */ const THEME_SETTLE_MS = 300; /** Milliseconds to wait after page load for animations to complete */ const PAGE_SETTLE_MS = 500; /** Output directory (relative to project root) */ const OUTPUT_DIR = path.join( __dirname, '..', '.agents', 'frontend-specialist', 'screenshots' ); // ─── Helpers ────────────────────────────────────────────────────────────────── function ensureDir(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } function screenshotPath(theme, pageName, viewportName) { return path.join(OUTPUT_DIR, theme, `${pageName}-${viewportName}.png`); } async function waitForPageReady(page) { // Wait for network idle + animations to complete await page.waitForLoadState('networkidle'); await page.waitForTimeout(PAGE_SETTLE_MS); } async function setTheme(page, theme) { await page.evaluate((t) => { if (t === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } try { localStorage.setItem('theme', t); } catch (_) { // localStorage may be unavailable in some contexts } }, theme); // Wait for CSS transitions to settle await page.waitForTimeout(THEME_SETTLE_MS); } async function captureScreenshot(page, filePath, fullPage = true) { await page.screenshot({ path: filePath, fullPage, animations: 'disabled', // Freeze CSS/JS animations for clean captures }); const sizeKB = Math.round(fs.statSync(filePath).size / 1024); console.log(` ✓ Saved: ${path.relative(process.cwd(), filePath)} (${sizeKB}KB)`); } // ─── Main ───────────────────────────────────────────────────────────────────── async function main() { console.log(''); console.log('═══════════════════════════════════════════════════════════'); console.log(' WorkRoot Theme Screenshot Capture'); console.log(` Base URL: ${BASE_URL}`); console.log(` Output: ${OUTPUT_DIR}`); console.log('═══════════════════════════════════════════════════════════'); console.log(''); // Create output directories for (const theme of THEMES) { ensureDir(path.join(OUTPUT_DIR, theme)); } const browser = await chromium.launch({ headless: true, }); let totalScreenshots = 0; let errors = []; try { for (const viewport of VIEWPORTS) { console.log(`\n── Viewport: ${viewport.name} (${viewport.width}×${viewport.height}) ──`); const context = await browser.newContext({ viewport: { width: viewport.width, height: viewport.height }, deviceScaleFactor: 2, // Retina quality for crisp screenshots colorScheme: 'light', // Start light; we override via JS }); const page = await context.newPage(); // Disable animations globally for cleaner screenshots await page.addInitScript(() => { const style = document.createElement('style'); style.textContent = ` *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } `; document.head.appendChild(style); }); for (const pageConfig of PAGES) { const url = `${BASE_URL}${pageConfig.path}`; console.log(`\n Page: ${pageConfig.name} (${url})`); for (const theme of THEMES) { try { // Navigate to page await page.goto(url, { waitUntil: 'domcontentloaded' }); await waitForPageReady(page); // Set theme await setTheme(page, theme); // Capture screenshot const filePath = screenshotPath(theme, pageConfig.name, viewport.name); await captureScreenshot(page, filePath); totalScreenshots++; } catch (err) { const label = `${theme}/${pageConfig.name}-${viewport.name}`; errors.push({ label, error: err.message }); console.error(` ✗ Failed ${label}: ${err.message}`); } } } await context.close(); } } finally { await browser.close(); } // Summary console.log(''); console.log('═══════════════════════════════════════════════════════════'); console.log(` Done! Captured ${totalScreenshots} screenshots`); if (errors.length > 0) { console.log(` ⚠ ${errors.length} error(s):`); for (const { label, error } of errors) { console.log(` • ${label}: ${error}`); } } console.log('═══════════════════════════════════════════════════════════'); console.log(''); // Generate index file listing all screenshots generateIndex(totalScreenshots, errors); } // ─── Index Generator ────────────────────────────────────────────────────────── function generateIndex(total, errors) { const lines = [ '# Theme Screenshots Index', '', `Generated: ${new Date().toISOString()}`, `Total: ${total} screenshots`, errors.length > 0 ? `Errors: ${errors.length}` : 'All captures successful ✓', '', '## File Listing', '', ]; for (const theme of THEMES) { lines.push(`### ${theme.charAt(0).toUpperCase() + theme.slice(1)} Theme`); lines.push(''); lines.push('| Page | Desktop | Tablet | Mobile |'); lines.push('|------|---------|--------|--------|'); for (const pageConfig of PAGES) { const cells = VIEWPORTS.map((vp) => { const file = `${pageConfig.name}-${vp.name}.png`; const fullPath = path.join(OUTPUT_DIR, theme, file); const exists = fs.existsSync(fullPath); if (!exists) return '❌ missing'; const sizeKB = Math.round(fs.statSync(fullPath).size / 1024); return `[${file}](./${theme}/${file}) (${sizeKB}KB)`; }); lines.push(`| ${pageConfig.name} | ${cells.join(' | ')} |`); } lines.push(''); } if (errors.length > 0) { lines.push('## Errors'); lines.push(''); for (const { label, error } of errors) { lines.push(`- **${label}**: ${error}`); } lines.push(''); } lines.push('---'); lines.push('*Generated by `scripts/capture-theme-screenshots.js`*'); const indexPath = path.join(OUTPUT_DIR, 'INDEX.md'); fs.writeFileSync(indexPath, lines.join('\n')); console.log(` Index written: ${path.relative(process.cwd(), indexPath)}`); } // ─── Entry Point ────────────────────────────────────────────────────────────── main().catch((err) => { console.error('Fatal error:', err); process.exit(1); });