Desktop icons move from lucide to Phosphor (@phosphor-icons/react, per-icon imports, regular weight; fill for the active nav item, notices and rating stars). The site's icons and the fixture art use Phosphor's own files from @phosphor-icons/core. App Store and Google Play are shown with their marks from simple-icons (CC0 data), vendored unchanged by tools/site-icons.ts and rendered as a mask in the neutral text colour next to the store's name; the trademark note is in docs/desktop.md and the site footer. lucide-react and lucide-static removed. The no-hand-written-SVG guard allows only the vendored files (byte-equal to their packages) and per-icon Phosphor imports. Site stylesheet ?v= bumped.
70 lines
4.9 KiB
TypeScript
70 lines
4.9 KiB
TypeScript
#!/usr/bin/env bun
|
|
// Renders the desktop dev fixtures' art (apps/desktop/src/dev/art/*.png): app icons and store screenshot stand-ins,
|
|
// each a Phosphor icon (@phosphor-icons/core, MIT, "fill" weight) on a CSS gradient, photographed by Playwright's
|
|
// Chromium. No drawn SVG: the icons are Phosphor's own files. Dev only (the fixtures never ship in the release bundle).
|
|
// bun tools/fixture-art.ts (Playwright's Chromium, or an installed Google Chrome; same launcher as storebound shots)
|
|
import { mkdirSync, readFileSync, renameSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { launch } from '../cli/shots/browser';
|
|
|
|
const root = new URL('..', import.meta.url).pathname;
|
|
const out = join(root, 'apps/desktop/src/dev/art');
|
|
const phosphor = (name: string) => readFileSync(join(root, 'node_modules/@phosphor-icons/core/assets/fill', `${name}-fill.svg`), 'utf8');
|
|
|
|
/** [file, Phosphor icon, gradient from, gradient to] */
|
|
const ICONS: [string, string, string, string][] = [
|
|
['icon-prism', 'drop', '#7c5cff', '#ff5fa2'], ['icon-tidy', 'list-checks', '#2fd18b', '#0f9d74'],
|
|
['icon-orbit', 'planet', '#0ea5e9', '#6366f1'], ['icon-block', 'cube', '#22c55e', '#15803d'],
|
|
['icon-lantern', 'lamp', '#f59e0b', '#b45309'], ['icon-tycoon', 'coins', '#a855f7', '#6d28d9'],
|
|
['icon-racer', 'car', '#ef4444', '#991b1b'], ['icon-habit', 'egg', '#14b8a6', '#0f766e'],
|
|
['icon-focus', 'timer', '#f43f5e', '#be123c'], ['icon-sketch', 'star', '#fb923c', '#ea580c'],
|
|
['icon-night', 'moon', '#1e293b', '#4338ca'], ['icon-mood', 'grid-four', '#ec4899', '#8b5cf6'],
|
|
];
|
|
/** Four scenes of the demo game: [Phosphor icon, from, to, accent] */
|
|
const SCENES: [string, string, string, string][] = [
|
|
['drop', '#7c5cff', '#ff5fa2', '#ffd36e'], ['lightning', '#ff7a59', '#ff3d7f', '#ffe08a'],
|
|
['calendar-dots', '#2fb5ff', '#5b6cff', '#b8f3ff'], ['trophy', '#ffb03b', '#ff7a1a', '#fff1b8'],
|
|
];
|
|
/** Screenshot shapes at a small preview size, in the stores' aspect ratios. */
|
|
const SHAPES: [string, number, number][] = [['phone', 240, 521], ['play', 240, 427], ['ipad', 300, 400], ['feature', 512, 250]];
|
|
|
|
const page = (w: number, h: number, body: string, css: string) =>
|
|
`<!doctype html><html><head><style>*{margin:0;box-sizing:border-box}html,body{width:${w}px;height:${h}px;overflow:hidden}${css}</style></head><body>${body}</body></html>`;
|
|
|
|
mkdirSync(out, { recursive: true });
|
|
const browser = await launch(root, {});
|
|
const tab = await browser.newPage({ deviceScaleFactor: 1 });
|
|
async function snap(file: string, w: number, h: number, html: string) {
|
|
await tab.setViewportSize({ width: w, height: h });
|
|
await tab.setContent(html);
|
|
const png = join(out, `${file}.png`);
|
|
await tab.screenshot({ path: png, clip: { x: 0, y: 0, width: w, height: h } });
|
|
// 8-bit palette PNG (ffmpeg, already needed for storebound icons): about a quarter of the bytes; kept as is without it
|
|
if (Bun.which('ffmpeg')) {
|
|
const tmp = `${png}.q.png`;
|
|
const r = Bun.spawnSync(['ffmpeg', '-v', 'error', '-y', '-i', png, '-vf', 'split[a][b];[a]palettegen=max_colors=96:stats_mode=full[p];[b][p]paletteuse=dither=bayer:bayer_scale=3', tmp]);
|
|
if (r.exitCode === 0) renameSync(tmp, png);
|
|
}
|
|
console.log(`fixture-art: ${file}.png ${w}x${h}`);
|
|
}
|
|
for (const [file, icon, a, b] of ICONS) {
|
|
await snap(file, 128, 128, page(128, 128, `<div class="t">${phosphor(icon)}</div>`,
|
|
`.t{width:128px;height:128px;display:grid;place-items:center;background:linear-gradient(135deg,${a},${b});color:#fff}.t svg{width:72px;height:72px}`));
|
|
}
|
|
for (const [i, [icon, a, b, c]] of SCENES.entries()) {
|
|
for (const [shape, w, h] of SHAPES) {
|
|
const land = w > h;
|
|
const phone = land ? { w: h * 0.42, h: h * 0.86 } : { w: w * 0.62, h: Math.min(h * 0.72, w * 0.62 * 2.05) };
|
|
await snap(`scene${i + 1}-${shape}`, w, h, page(w, h,
|
|
`<div class="bg"><div class="glow"></div><div class="big">${phosphor(icon)}</div><div class="ph"><div class="scr">${phosphor(icon)}</div></div></div>`,
|
|
`.bg{position:relative;width:${w}px;height:${h}px;background:linear-gradient(135deg,${a},${b});color:#fff;overflow:hidden}
|
|
.glow{position:absolute;inset:0;background:radial-gradient(circle at 50% 30%,rgba(255,255,255,.35),transparent 70%)}
|
|
.big{position:absolute;${land ? `left:${w * 0.1}px;top:${h * 0.22}px` : `left:50%;top:${h * 0.05}px;transform:translateX(-50%)`};color:${c}}
|
|
.big svg{width:${land ? h * 0.5 : w * 0.26}px;height:${land ? h * 0.5 : w * 0.26}px}
|
|
.ph{position:absolute;width:${phone.w}px;height:${phone.h}px;${land ? `right:${w * 0.1}px;top:${h * 0.07}px` : `left:50%;bottom:-${phone.w * 0.12}px;transform:translateX(-50%)`};
|
|
border-radius:${phone.w * 0.14}px;background:#10142a;border:${Math.max(2, phone.w * 0.03)}px solid rgba(255,255,255,.85);padding:${phone.w * 0.06}px}
|
|
.scr{width:100%;height:100%;border-radius:${phone.w * 0.08}px;background:#1d2350;display:grid;place-items:center;color:${c}}
|
|
.scr svg{width:45%;height:45%}`));
|
|
}
|
|
}
|
|
await browser.close();
|