/** * Design history gallery — generates an HTML timeline of all design explorations * for a project. Shows every approved/rejected variant, feedback notes, organized * by date. Self-contained HTML with base64-embedded images. */ import fs from "fs"; import path from "path"; export interface GalleryOptions { designsDir: string; // ~/.gstack/projects/$SLUG/designs/ output: string; } interface SessionData { dir: string; name: string; date: string; approved: any | null; variants: string[]; // paths to variant PNGs } export function generateGalleryHtml(designsDir: string): string { const sessions: SessionData[] = []; if (!fs.existsSync(designsDir)) { return generateEmptyGallery(); } const entries = fs.readdirSync(designsDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const sessionDir = path.join(designsDir, entry.name); let approved: any = null; // Read approved.json if it exists const approvedPath = path.join(sessionDir, "approved.json"); if (fs.existsSync(approvedPath)) { try { approved = JSON.parse(fs.readFileSync(approvedPath, "utf-8")); } catch { // Corrupted JSON, skip but still show the session } } // Find variant PNGs const variants: string[] = []; try { const files = fs.readdirSync(sessionDir); for (const f of files) { if (f.match(/variant-[A-Z]\.png$/i) || f.match(/variant-\d+\.png$/i)) { variants.push(path.join(sessionDir, f)); } } variants.sort(); } catch { // Can't read directory, skip } // Extract date from directory name (e.g., homepage-20260327) const dateMatch = entry.name.match(/(\d{8})$/); const date = dateMatch ? `${dateMatch[1].slice(0, 4)}-${dateMatch[1].slice(4, 6)}-${dateMatch[1].slice(6, 8)}` : approved?.date?.slice(0, 10) || "Unknown"; sessions.push({ dir: sessionDir, name: entry.name.replace(/-\d{8}$/, "").replace(/-/g, " "), date, approved, variants, }); } if (sessions.length === 0) { return generateEmptyGallery(); } // Sort by date, newest first sessions.sort((a, b) => b.date.localeCompare(a.date)); const sessionCards = sessions.map(session => { const variantImgs = session.variants.map((vPath, i) => { try { const imgData = fs.readFileSync(vPath).toString("base64"); const ext = path.extname(vPath).slice(1) || "png"; const label = path.basename(vPath, `.${ext}`).replace("variant-", ""); const isApproved = session.approved?.approved_variant === label; return ` `; } catch { return ""; // Skip unreadable images } }).filter(Boolean).join("\n"); const feedbackNote = session.approved?.feedback ? `` : ""; return ` `; }).join("\n"); return ` Design History

Design History

${sessions.length} exploration${sessions.length === 1 ? "" : "s"}
`; } function generateEmptyGallery(): string { return ` Design History

No design history yet

Run /design-shotgun to start exploring design directions.

`; } function escapeHtml(str: string): string { return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } /** * Gallery command: generate HTML timeline from design explorations. */ export function gallery(options: GalleryOptions): void { const html = generateGalleryHtml(options.designsDir); const outputDir = path.dirname(options.output); fs.mkdirSync(outputDir, { recursive: true }); fs.writeFileSync(options.output, html); console.log(JSON.stringify({ outputPath: options.output })); }