Files
Your Name 9e6804c1ab
Deploy to GitHub Pages / build (push) Canceled after 0s
Deploy to GitHub Pages / deploy (push) Canceled after 0s
Initial commit
2026-10-11 18:09:42 +02:00

60 lines
3.8 KiB
TypeScript

// Regenerates favicons, the small touch icons and placeholder screenshots.
// Run: bun run assets (placeholders never overwrite a file that already exists)
import sharp from 'sharp';
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { screens } from '../src/lib/screens.ts';
const root = new URL('..', import.meta.url).pathname;
const NIB =
'M330 340 C330 296 362 266 406 266 H618 C662 266 694 296 694 340 C694 448 646 528 598 600 C560 656 530 724 512 806 C494 724 464 656 426 600 C378 528 330 448 330 340 Z';
// Favicon: the app's nib on a flat tile (the full icon's filters are too heavy for 16px).
const favicon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="100 100 824 824">
<defs><linearGradient id="n" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f7d3c9"/><stop offset=".6" stop-color="#e4ada5"/><stop offset="1" stop-color="#c97a6d"/></linearGradient></defs>
<rect x="100" y="100" width="824" height="824" rx="190" fill="#1a1a1c"/>
<path d="${NIB}" fill="url(#n)"/>
<path d="M512 520 V800" stroke="#2a1a16" stroke-width="22" stroke-linecap="round"/>
<circle cx="512" cy="500" r="34" fill="#2a1a16"/>
</svg>`;
writeFileSync(`${root}public/favicon.svg`, favicon);
for (const [file, size] of [['favicon-32.png', 32], ['apple-touch-icon.png', 180], ['icon-192.png', 192], ['icon-512.png', 512]] as const) {
await sharp(Buffer.from(favicon), { density: 384 }).resize(size, size).png().toFile(`${root}public/${file}`);
}
// Placeholder screenshots: 16:10 wireframes that say what to capture.
const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;');
function wrap(text: string, max: number): string[] {
const out: string[] = [];
let line = '';
for (const w of text.split(' ')) {
if ((line + ' ' + w).trim().length > max) { out.push(line); line = w; } else line = (line + ' ' + w).trim();
}
return line ? [...out, line] : out;
}
function placeholder(name: string, capture: string, theme: 'dark' | 'light') {
const c = theme === 'dark'
? { ground: '#0e0e0f', island: '#161617', bar: 'rgba(255,255,255,.07)', ink: '#ececed', mute: '#a1a1a6', accent: '#e4ada5' }
: { ground: '#e7e7e9', island: '#fbfbfb', bar: 'rgba(0,0,0,.07)', ink: '#1c1c1e', mute: '#5c5c61', accent: '#b8695f' };
const lines = wrap(capture, 62).slice(0, 5);
const side = [0, 1, 2, 3, 4, 5, 6].map((i) => `<rect x="48" y="${100 + i * 44}" width="${150 - (i % 3) * 30}" height="14" rx="7" fill="${c.bar}"/>`).join('');
const text = [0, 1, 2, 3].map((i) => `<rect x="${340}" y="${280 + i * 40}" width="${760 - (i % 2) * 160}" height="12" rx="6" fill="${c.bar}"/>`).join('');
return `<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000" font-family="Inter, system-ui, sans-serif">
<rect width="1600" height="1000" fill="${c.ground}"/>
<rect x="20" y="20" width="248" height="960" rx="14" fill="${c.island}"/>${side}
<rect x="288" y="20" width="1292" height="960" rx="14" fill="${c.island}"/>
<rect x="340" y="110" width="420" height="30" rx="8" fill="${c.bar}"/>${text}
<text x="800" y="640" text-anchor="middle" font-size="30" font-weight="600" fill="${c.accent}">Placeholder: ${esc(name)}-${theme}.png</text>
${lines.map((l, i) => `<text x="800" y="${690 + i * 30}" text-anchor="middle" font-size="20" fill="${c.mute}">${esc(l)}</text>`).join('')}
</svg>`;
}
mkdirSync(`${root}src/assets/screens`, { recursive: true });
for (const [name, spec] of Object.entries(screens)) {
for (const theme of ['dark', 'light'] as const) {
const path = `${root}src/assets/screens/${name}-${theme}.svg`;
// Real captures (see scripts/capture) win; don't recreate a placeholder they shadow.
const real = ['png', 'webp', 'jpg'].some((e) => existsSync(path.replace(/\.svg$/, `.${e}`)));
if (!real && !existsSync(path)) writeFileSync(path, placeholder(name, spec.capture, theme));
}
}
console.log('assets done');