From 72643051f9fc20b09f83922802dc2d033af1af53 Mon Sep 17 00:00:00 2001 From: Jokin Date: Sun, 16 Aug 2026 01:31:54 +0200 Subject: [PATCH] Stage 0: headless screenshot harness for the Win7 skin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 , 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 --- .gitignore | 3 + scripts/shot.ts | 106 +++++++++++++++++++++++++++ ui/dev/harness.html | 172 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 281 insertions(+) create mode 100644 scripts/shot.ts create mode 100644 ui/dev/harness.html diff --git a/.gitignore b/.gitignore index a46f740..055e128 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ ui/dist/ ui/.angular/ src/shared/version.gen.ts release/ + +# headless screenshot workspace (scripts/shot.ts) +.shot/ diff --git a/scripts/shot.ts b/scripts/shot.ts new file mode 100644 index 0000000..07247d1 --- /dev/null +++ b/scripts/shot.ts @@ -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 , 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(' + + + + + 7.css harness + + + + +

1 — Window frame, glass, drop shadow, active colour

+
+
+
reference — .window.glass.active
+
+
+
+
miniSCP
+
+ + + +
+
+

Glass frame, 6px radius, drop shadow.

+
+
+
+
+
ours — bare .title-bar, no wrapper
+
+
+
+
miniSCP
+
+ + + +
+
+
+

No frame, no shadow, no glass.

+
+
+
+
+
+ +

2 — ListView (file panel)

+
+
+
reference — <table>
+ + + + + + + + + + + + + + +
NameSizeModified
..——
Documents—3/29/2007 12:00 AM
report.pdf27 KB3/23/2007 12:00 AM
notes.txt4 KB3/13/2007 12:00 AM
+
+
+
ours — div grid (7.css never applies)
+
+
+
Name
+
Size
+
Modified
+
+
..
—
—
+
Documents
—
3/29/2007
+
report.pdf
27 KB
3/23/2007
+
+
+
+ +

3 — Context menu

+
+
+
reference — ul[role=menu]
+ +
+
+
ours — div > button + hand-written CSS
+
+ + +
+ + +
+
+
+ +

4 — Controls 7.css already owns (sanity check)

+
+
+
buttons / textbox / progress
+
+
+ +
+
+
+
+
+
tabs — menu[role=tablist]
+ + + + + +
Tab body
+
+
+ +