Stage 0: headless screenshot harness for the Win7 skin
Everything about this branch's look has so far been verified by reading build
logs, which is how a fully black window got reported as a clean boot. This adds
a way to actually LOOK at the skin.
- scripts/shot.ts — renders a page with Firefox headless (already installed; no
Chromium/puppeteer download) and writes a PNG. Firefox needs a dedicated
profile, since a running Firefox locks the default one.
- ui/dev/harness.html — static gallery pairing 7.css's own documented markup
("reference") against what the app currently renders ("ours"), for the window
frame, ListView, context menu, buttons, textbox, progress bars and tabs. Lives
outside ui/src so the Angular build never sees it.
Two traps found while building it, both documented in the files:
- The composed page MUST be written into ui/dist beside the stylesheet.
Firefox's security.fileuri.strict_origin_policy blocks a file:// document
from loading CSS in another directory, and the result renders completely
unstyled — indistinguishable from "the skin is broken".
- replaceAll, not replace, for the {{STYLES}} token: the harness documents the
token in a header comment that precedes the <link>, so a first-match replace
substitutes the comment and leaves the stylesheet unlinked. This produced one
false "everything is unstyled" result before it was spotted.
What the first screenshots already confirm, visually:
- The window has no Aero frame, radius, drop shadow or glass, and its title-bar
controls sit in their inactive colours — we render a bare .title-bar with no
.window.glass.active wrapper.
- The file list gets none of the ListView treatment (header gradient, sort
indicator, cell separators, selection band) because it is a div grid.
- The context menu gets none of the menu treatment (gutter, dividers, disabled
state, real checkmarks).
- NEW, not previously predicted: `.win7 button { min-width:0; padding:
revert-layer }` — added to stop Tailwind icon buttons ballooning — also
crushes legitimate dialog push buttons well below Win7's 75x23 metrics.
- Dark mode: ListView header labels and tab labels are invisible (dark on
dark), since listview.css hardcodes light literals the dark block never
restates.
Caveat recorded in the script: Firefox is Gecko, not the WebKitGTK engine the
app ships on. Good for skin fidelity, useless for native window transparency.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
||||
// Headless screenshot of the skin harness, so the Win7 look can be reviewed
|
||||
// visually during development rather than inferred from a clean build log.
|
||||
//
|
||||
// bun scripts/shot.ts [--dark] [--width 1200] [--height 1400] [--out path.png]
|
||||
// bun scripts/shot.ts --url http://localhost:4200 (any page, not just the harness)
|
||||
//
|
||||
// Firefox is used because it ships on this machine and has a built-in
|
||||
// --screenshot mode; no Chromium/puppeteer download is needed. Two constraints
|
||||
// it imposes:
|
||||
// - a DEDICATED PROFILE is mandatory. A running Firefox holds a lock on the
|
||||
// default profile and the headless run aborts with "Firefox is already
|
||||
// running, but is not responding".
|
||||
// - it renders with Gecko, NOT the WebKitGTK engine the app actually uses.
|
||||
// Good enough for skin fidelity (layout, colour, gradients, borders);
|
||||
// useless for engine-specific behaviour or native window transparency.
|
||||
// Real confirmation is still `bun run start`.
|
||||
//
|
||||
// The harness links the REAL hashed stylesheet from ui/dist (produced by the
|
||||
// normal `bun run build:ui`), so there is no second build pipeline to keep in
|
||||
// sync.
|
||||
//
|
||||
// The composed page is written INTO that same dist folder, and this is not
|
||||
// incidental: Firefox's security.fileuri.strict_origin_policy stops a file://
|
||||
// document from loading a stylesheet that lives in a different directory. Host
|
||||
// the page elsewhere and it renders completely unstyled — which looks exactly
|
||||
// like "the skin is broken" rather than "the CSS never loaded". Keeping them
|
||||
// co-located also lets media/ fonts resolve.
|
||||
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = path.join(import.meta.dir, "..");
|
||||
const DIST = path.join(ROOT, "ui", "dist", "ui", "browser");
|
||||
const HARNESS = path.join(ROOT, "ui", "dev", "harness.html");
|
||||
const WORK = path.join(ROOT, ".shot");
|
||||
|
||||
function arg(name: string): string | undefined {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
return i >= 0 ? process.argv[i + 1] : undefined;
|
||||
}
|
||||
const flag = (name: string) => process.argv.includes(`--${name}`);
|
||||
|
||||
function fail(message: string): never {
|
||||
console.error(`[shot] ERROR: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---- resolve the built stylesheet ----
|
||||
|
||||
if (!existsSync(DIST)) fail(`no build output at ${DIST} — run: bun run build:ui`);
|
||||
const styles = readdirSync(DIST).find((f) => f.startsWith("styles-") && f.endsWith(".css"));
|
||||
if (!styles) fail(`no styles-*.css in ${DIST} — run: bun run build:ui`);
|
||||
|
||||
// ---- compose the page ----
|
||||
|
||||
mkdirSync(WORK, { recursive: true });
|
||||
const profile = path.join(WORK, "profile");
|
||||
mkdirSync(profile, { recursive: true });
|
||||
|
||||
const url = arg("url");
|
||||
let target: string;
|
||||
|
||||
if (url) {
|
||||
target = url;
|
||||
} else {
|
||||
const dark = flag("dark");
|
||||
let html = await Bun.file(HARNESS).text();
|
||||
// replaceAll, not replace: the harness documents its own {{STYLES}} token in
|
||||
// a header comment that precedes the <link>, so a first-match replace
|
||||
// substitutes the comment and silently leaves the stylesheet unlinked.
|
||||
html = html.replaceAll("{{STYLES}}", styles); // relative: page sits next to it
|
||||
// The preload bakes these attributes in the real app; do it by hand here.
|
||||
if (dark) html = html.replace('<html class="win7"', '<html class="win7" data-theme="dark"');
|
||||
const page = path.join(DIST, dark ? "_harness.dark.html" : "_harness.light.html");
|
||||
await Bun.write(page, html);
|
||||
target = `file://${page}`;
|
||||
}
|
||||
|
||||
// ---- shoot ----
|
||||
|
||||
const out = path.resolve(arg("out") ?? path.join(WORK, flag("dark") ? "dark.png" : "light.png"));
|
||||
const width = arg("width") ?? "1200";
|
||||
const height = arg("height") ?? "1500";
|
||||
|
||||
// A stale lock from a killed run makes every later shot fail confusingly.
|
||||
rmSync(path.join(profile, "parent.lock"), { force: true });
|
||||
|
||||
const proc = Bun.spawnSync(
|
||||
[
|
||||
"firefox",
|
||||
"--headless",
|
||||
"--profile",
|
||||
profile,
|
||||
"--screenshot",
|
||||
out,
|
||||
"--window-size",
|
||||
`${width},${height}`,
|
||||
target,
|
||||
],
|
||||
{ cwd: ROOT, stdout: "pipe", stderr: "pipe" },
|
||||
);
|
||||
|
||||
if (!existsSync(out)) {
|
||||
console.error(proc.stderr.toString().trim());
|
||||
fail("firefox produced no image (see stderr above)");
|
||||
}
|
||||
console.log(`[shot] ${out}`);
|
||||
Reference in New Issue
Block a user