#!/usr/bin/env -S npx -y tsx /** * open-design-landing — SVG framework placeholder generator. * * When `imagery.strategy === 'placeholder'`, this script writes one * paper-textured SVG file per slot in `assets/image-manifest.json`. * The generated files live alongside the schema-named PNGs that the * composer references (`hero.png`, `about.png`, `lab-1.png`, …) so * the layout renders fully without any image budget. * * Each placeholder shows: slot id · ratio · pixel dimensions · the * `prompt_section` hint copied from the manifest. Drop the real PNG * with the same filename to swap in production imagery; no markup * change required. * * Usage: * npx tsx scripts/placeholder.ts * * Default out-dir is `./assets/`. */ import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { resolve, dirname, isAbsolute, basename } from 'node:path'; import { fileURLToPath } from 'node:url'; const SKILL_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); interface ManifestSlot { id: string; file: string; width: number; height: number; ratio: string; prompt_section: string; required: boolean; rekey_on_brand_change: boolean; } interface Manifest { skill: string; design_system: string; slots: ManifestSlot[]; } const PAPER = '#efe7d2'; const INK_FAINT = '#8b8676'; const CORAL = '#ed6f5c'; const LINE = 'rgba(21, 20, 15, 0.16)'; /** Compose a single paper-textured SVG for one slot. */ export function placeholderSvg(slot: ManifestSlot): string { const w = slot.width; const h = slot.height; const cx = w / 2; const cy = h / 2; const isPortrait = h > w; const titleSize = Math.round(Math.min(w, h) * (isPortrait ? 0.075 : 0.07)); const metaSize = Math.round(Math.min(w, h) * 0.028); const dimsSize = Math.round(Math.min(w, h) * 0.024); // Inner frame inset. const inset = Math.round(Math.min(w, h) * 0.04); const frame = { x: inset, y: inset, w: w - inset * 2, h: h - inset * 2, }; // Diagonal strokes for the classic "image goes here" cross. const cross = ` `; const cornerLen = Math.round(Math.min(w, h) * 0.05); const corners = ` `; return ` ${cross} ${corners} PLATE · ${slot.id.toUpperCase()} ${w} × ${h} · ${slot.ratio} ${escapeXml(slot.id)} ${escapeXml(slot.prompt_section.toUpperCase())} ${slot.required ? 'REQUIRED' : 'OPTIONAL'} · ${slot.rekey_on_brand_change ? 'REKEY ON BRAND' : 'STABLE'} OPEN DESIGN · ATELIER ZERO `; } function escapeXml(s: string): string { return s .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } async function loadManifest(): Promise { const path = resolve(SKILL_ROOT, 'assets', 'image-manifest.json'); return JSON.parse(await readFile(path, 'utf8')) as Manifest; } /** * Write `/` for every slot. The composer references * slots by .png filename; we honor that by writing `.svg` * AND a `.png.svg` symlink-style fallback. Most static * hosts serve SVG to just fine, so the practical convention * is: if you want placeholders, point your `imagery.assets_path` at * a directory of `.svg` files OR rename the SVGs to `.png` (some * browsers honor extensionless content-sniffing). * * For the most reliable result, write BOTH: * - `.svg` — clean, editable * - `` — same SVG content under the .png filename so the * composer's `` works * without changing markup. */ export async function writePlaceholders(outDir: string): Promise { const manifest = await loadManifest(); await mkdir(outDir, { recursive: true }); const written: string[] = []; for (const slot of manifest.slots) { const svg = placeholderSvg(slot); const svgPath = resolve(outDir, `${slot.id}.svg`); const pngPath = resolve(outDir, slot.file); await writeFile(svgPath, svg, 'utf8'); await writeFile(pngPath, svg, 'utf8'); written.push(svgPath, pngPath); } return written; } async function main(): Promise { const [, , outArg] = process.argv; const out = isAbsolute(outArg ?? '') ? outArg! : resolve(process.cwd(), outArg ?? './assets/'); const written = await writePlaceholders(out); const pngs = written.filter((p) => p.endsWith('.png')).length; const svgs = written.filter((p) => p.endsWith('.svg')).length; console.log(`✓ wrote ${pngs} png-named placeholders + ${svgs} svg files into ${out}`); console.log(` (${written.map((p) => basename(p)).join(', ')})`); } const isMain = import.meta.url === `file://${process.argv[1]}`; if (isMain) { main().catch((err) => { console.error(err); process.exit(1); }); }