feat(01-04): Playwright capture toolchain + DSGN-02 approval gallery (D-14, D-12)
- screenshot.mjs: committed real-browser capture tool — tiny node static server (theme/ docroot), sheet fullPage capture (390px), 4 mockup captures via file:// (network CDN renders; committed screen.png fallback on failure — mockups never edited, D-06), basemap capture gated on the proof page's RAMS-BASEMAP-READY console signal + 2s tile settle (1280x800); blank gate >=10KB per capture; wired as root+theme `capture` - gallery.html: the DSGN-02 mechanism template — 4 mockup-vs-sheet comparison rows + basemap direction row + token ledger (13 typography specimens + 6 hero color swatches, names only per D-07) + approval scope strip; CDN-free, built from dist/rams.css + var()-only page shell; every later UI phase extends this same page - audit.mjs: gallery.html registered as an implementation page — audit covers its <style> block + style attributes + class harvest (354 declarations, still 0 off-token) - verified: CAPTURE OK 6/6 (45KB-1.2MB), gallery render check 0 failed requests / all 6 images decode, AUDIT CLEAN
This commit is contained in:
@@ -0,0 +1,290 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
araharah — DSGN-02 approval gallery (the fidelity gate mechanism)
|
||||||
|
==================================================================
|
||||||
|
THIS PAGE IS THE DSGN-02 MECHANISM TEMPLATE. Every later UI phase
|
||||||
|
(Mini App shell, search, route pick, guidance, backgrounded
|
||||||
|
turn-cards, ...) extends THIS SAME PAGE with one comparison row
|
||||||
|
per new product screen:
|
||||||
|
|
||||||
|
<section> — h2.card-header plate: SCREEN NAME + card-ref
|
||||||
|
left : the mockup reference render (read-only, D-06)
|
||||||
|
right : the implementation capture (Playwright, D-14)
|
||||||
|
note : which sheet/product families the row compares
|
||||||
|
|
||||||
|
The token ledger and approval strip below stay unchanged. The
|
||||||
|
automated evidence stays `npm run audit` (zero off-token values,
|
||||||
|
D-13) — run it from the repo root. Phase approval is recorded in
|
||||||
|
the phase SUMMARY (DSGN-02's durable artifact trail).
|
||||||
|
|
||||||
|
Captures are regenerated by: npm run capture (D-14 toolchain).
|
||||||
|
theme/screenshots/ is gitignored (regenerated artifacts) — after
|
||||||
|
a fresh checkout run `npm run capture`, then serve theme/ and
|
||||||
|
open this page, e.g.:
|
||||||
|
npm run basemap:preview --workspace theme
|
||||||
|
→ http://localhost:8123/gallery.html
|
||||||
|
|
||||||
|
CDN-free by construction (D-16): only the token package's built
|
||||||
|
stylesheet + local captures; no network dependency at all.
|
||||||
|
-->
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>ARAHARAH — DSGN-02 Approval Gallery</title>
|
||||||
|
<!-- token package: Rams tokens + self-hosted Geist / Space Grotesk -->
|
||||||
|
<link rel="stylesheet" href="dist/rams.css">
|
||||||
|
<style>
|
||||||
|
/*
|
||||||
|
* Page shell — gallery layout only, not component API.
|
||||||
|
* Every value is a token reference (the audit scans this
|
||||||
|
* block: it must stay var()-only, zero roundedness, no
|
||||||
|
* shadows or blur).
|
||||||
|
*/
|
||||||
|
body {
|
||||||
|
background: var(--shell-bg);
|
||||||
|
color: var(--shell-on-surface);
|
||||||
|
font: var(--type-body-md);
|
||||||
|
letter-spacing: var(--type-body-md-tracking);
|
||||||
|
padding: var(--spacing-margin);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-space-lg);
|
||||||
|
max-width: var(--spacing-gutter-desktop);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
main > section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-space-sm);
|
||||||
|
}
|
||||||
|
/* sheet-furniture pattern (plan 01-02): section plates ride the
|
||||||
|
* shell slots so the register stays legible under both schemes */
|
||||||
|
main > section > h2.card-header {
|
||||||
|
color: var(--shell-on-surface);
|
||||||
|
border-bottom-color: var(--shell-on-surface);
|
||||||
|
}
|
||||||
|
.gallery-masthead {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-space-xs);
|
||||||
|
border-bottom: 2px solid var(--shell-on-surface);
|
||||||
|
padding-bottom: var(--spacing-space-md);
|
||||||
|
}
|
||||||
|
.evidence {
|
||||||
|
border: 1px solid var(--shell-on-surface);
|
||||||
|
padding: var(--spacing-space-sm) var(--spacing-space-md);
|
||||||
|
}
|
||||||
|
.compare-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--spacing-space-sm);
|
||||||
|
}
|
||||||
|
.compare-row figure {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-space-xs);
|
||||||
|
}
|
||||||
|
.compare-row img,
|
||||||
|
.full-row img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
border: 1px solid var(--color-matte-black);
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
.compare-note {
|
||||||
|
color: var(--shell-on-surface);
|
||||||
|
}
|
||||||
|
.ledger {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(var(--spacing-space-2xl), 1fr));
|
||||||
|
gap: var(--spacing-space-md) var(--spacing-space-lg);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.ledger > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-space-xs);
|
||||||
|
border-left: 1px solid var(--shell-on-surface);
|
||||||
|
padding-left: var(--spacing-space-sm);
|
||||||
|
}
|
||||||
|
.ledger dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.swatch {
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--spacing-space-lg);
|
||||||
|
height: var(--spacing-space-lg);
|
||||||
|
border: 1px solid var(--color-matte-black);
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: var(--spacing-space-xs);
|
||||||
|
}
|
||||||
|
.approval {
|
||||||
|
border: 2px solid var(--color-matte-black);
|
||||||
|
padding: var(--spacing-space-md);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-space-xs);
|
||||||
|
background: var(--color-soft-beige);
|
||||||
|
color: var(--color-matte-black);
|
||||||
|
}
|
||||||
|
.swatch-signal-orange { background: var(--color-signal-orange); }
|
||||||
|
.swatch-functional-red { background: var(--color-functional-red); }
|
||||||
|
.swatch-olive-green { background: var(--color-olive-green); }
|
||||||
|
.swatch-soft-beige { background: var(--color-soft-beige); }
|
||||||
|
.swatch-matte-black { background: var(--color-matte-black); }
|
||||||
|
.swatch-chassis-subtle { background: var(--color-chassis-subtle); }
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.compare-row { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<!-- Masthead -->
|
||||||
|
<header class="gallery-masthead">
|
||||||
|
<p class="text-label-sm">ARAHARAH // TELEMETRY RAMS</p>
|
||||||
|
<h1 class="text-display-lg-mobile">DSGN-02 APPROVAL GALLERY</h1>
|
||||||
|
<p class="text-body-md">Rams implementation vs mockups — side-by-side, token by token. Left columns are the mockup reference renders; right columns are the implementation component sheet rendered from the token package.</p>
|
||||||
|
<div class="evidence">
|
||||||
|
<p class="text-label-md">AUTOMATED EVIDENCE (D-13)</p>
|
||||||
|
<p class="text-body-sm">`npm run audit` exits 0 — zero off-token values across every rendered declaration (colors, typography, radius, shadows, blur all derive from theme/src/tokens.css). Re-run it from the repo root; the gallery is the human side of the same gate.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Row 1 — active route guidance -->
|
||||||
|
<section>
|
||||||
|
<h2 class="card-header">
|
||||||
|
<span class="text-label-sm">MOCKUP VS IMPLEMENTATION</span>
|
||||||
|
<span class="card-ref">ACTIVE ROUTE GUIDANCE</span>
|
||||||
|
</h2>
|
||||||
|
<div class="compare-row">
|
||||||
|
<figure>
|
||||||
|
<figcaption class="text-label-sm">MOCKUP — REFERENCE RENDER</figcaption>
|
||||||
|
<img src="screenshots/mockup-active_route_guidance.png" alt="Active route guidance mockup render">
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="text-label-sm">IMPLEMENTATION — COMPONENT SHEET</figcaption>
|
||||||
|
<img src="screenshots/sheet-full.png" alt="Rams component sheet rendered from the token package">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<p class="compare-note text-body-sm">Compare: maneuver banner + arrow (SEC-05), telemetry card (SEC-04), event chips (SEC-03), headline / telemetry-num typography, buttons (SEC-01).</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 2 — navigate map overview -->
|
||||||
|
<section>
|
||||||
|
<h2 class="card-header">
|
||||||
|
<span class="text-label-sm">MOCKUP VS IMPLEMENTATION</span>
|
||||||
|
<span class="card-ref">NAVIGATE MAP OVERVIEW</span>
|
||||||
|
</h2>
|
||||||
|
<div class="compare-row">
|
||||||
|
<figure>
|
||||||
|
<figcaption class="text-label-sm">MOCKUP — REFERENCE RENDER</figcaption>
|
||||||
|
<img src="screenshots/mockup-navigate_map_overview.png" alt="Navigate map overview mockup render">
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="text-label-sm">IMPLEMENTATION — COMPONENT SHEET</figcaption>
|
||||||
|
<img src="screenshots/sheet-full.png" alt="Rams component sheet rendered from the token package">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<p class="compare-note text-body-sm">Compare: layer-control toggle (SEC-06), chips / status badges (SEC-03), label typography, inputs (SEC-02). The map canvas itself is the basemap row below.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 3 — community incident reporter -->
|
||||||
|
<section>
|
||||||
|
<h2 class="card-header">
|
||||||
|
<span class="text-label-sm">MOCKUP VS IMPLEMENTATION</span>
|
||||||
|
<span class="card-ref">COMMUNITY INCIDENT REPORTER</span>
|
||||||
|
</h2>
|
||||||
|
<div class="compare-row">
|
||||||
|
<figure>
|
||||||
|
<figcaption class="text-label-sm">MOCKUP — REFERENCE RENDER</figcaption>
|
||||||
|
<img src="screenshots/mockup-community_incident_reporter.png" alt="Community incident reporter mockup render">
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="text-label-sm">IMPLEMENTATION — COMPONENT SHEET</figcaption>
|
||||||
|
<img src="screenshots/sheet-full.png" alt="Rams component sheet rendered from the token package">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<p class="compare-note text-body-sm">Compare: input fields (SEC-02), buttons (SEC-01), checkbox (SEC-06), hazard chips (SEC-03), body typography. The reporter's interactive modal flow is intentionally static here (out of scope, deferred).</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Row 4 — telegram live traffic feed -->
|
||||||
|
<section>
|
||||||
|
<h2 class="card-header">
|
||||||
|
<span class="text-label-sm">MOCKUP VS IMPLEMENTATION</span>
|
||||||
|
<span class="card-ref">TELEGRAM LIVE TRAFFIC FEED</span>
|
||||||
|
</h2>
|
||||||
|
<div class="compare-row">
|
||||||
|
<figure>
|
||||||
|
<figcaption class="text-label-sm">MOCKUP — REFERENCE RENDER</figcaption>
|
||||||
|
<img src="screenshots/mockup-telegram_live_traffic_feed.png" alt="Telegram live traffic feed mockup render">
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<figcaption class="text-label-sm">IMPLEMENTATION — COMPONENT SHEET</figcaption>
|
||||||
|
<img src="screenshots/sheet-full.png" alt="Rams component sheet rendered from the token package">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<p class="compare-note text-body-sm">Compare: full typography specimen (SEC-00) — display, headline, body, labels, telemetry-num — plus chips (SEC-03), buttons (SEC-01), inputs (SEC-02), telemetry card (SEC-04).</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Final row — basemap direction -->
|
||||||
|
<section>
|
||||||
|
<h2 class="card-header">
|
||||||
|
<span class="text-label-sm">BASEMAP DIRECTION</span>
|
||||||
|
<span class="card-ref">RAMS LIBERTY // KL METRO</span>
|
||||||
|
</h2>
|
||||||
|
<div class="full-row">
|
||||||
|
<figure>
|
||||||
|
<img src="screenshots/basemap-kl.png" alt="Rams Liberty basemap rendering over KL with the reserved signal-orange route vector">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<p class="compare-note text-body-sm">Grayscale / muted land + water grounding, desaturated roads, muted labels — every cartographic color derived from the token package (D-09), labels in Rams-adjacent hosted fonts (D-10). Signal-orange appears nowhere except the route vector placeholder, reserved for Phase 5 (D-08). Confirm: no orange in the cartography, no saturated non-red hues, hierarchy by road weight, not tone.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Token ledger — names + rendered specimens only (D-07: no numeric restatement) -->
|
||||||
|
<section>
|
||||||
|
<h2 class="card-header">
|
||||||
|
<span class="text-label-sm">TOKEN LEDGER</span>
|
||||||
|
<span class="card-ref">NAMES ONLY // D-07</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-body-sm">Typography scale — every name rendered in its own token (the package owns every numeric value):</p>
|
||||||
|
<dl class="ledger">
|
||||||
|
<div><dt class="text-label-sm">DISPLAY-LG</dt><dd class="text-display-lg">DISPLAY-LG</dd></div>
|
||||||
|
<div><dt class="text-label-sm">DISPLAY-LG-MOBILE</dt><dd class="text-display-lg-mobile">DISPLAY-LG-MOBILE</dd></div>
|
||||||
|
<div><dt class="text-label-sm">HEADLINE-LG</dt><dd class="text-headline-lg">HEADLINE-LG</dd></div>
|
||||||
|
<div><dt class="text-label-sm">HEADLINE-LG-MOBILE</dt><dd class="text-headline-lg-mobile">HEADLINE-LG-MOBILE</dd></div>
|
||||||
|
<div><dt class="text-label-sm">HEADLINE-MD</dt><dd class="text-headline-md">HEADLINE-MD</dd></div>
|
||||||
|
<div><dt class="text-label-sm">HEADLINE-SM</dt><dd class="text-headline-sm">HEADLINE-SM</dd></div>
|
||||||
|
<div><dt class="text-label-sm">BODY-LG</dt><dd class="text-body-lg">BODY-LG</dd></div>
|
||||||
|
<div><dt class="text-label-sm">BODY-MD</dt><dd class="text-body-md">BODY-MD</dd></div>
|
||||||
|
<div><dt class="text-label-sm">BODY-SM</dt><dd class="text-body-sm">BODY-SM</dd></div>
|
||||||
|
<div><dt class="text-label-sm">LABEL-LG</dt><dd class="text-label-lg">LABEL-LG</dd></div>
|
||||||
|
<div><dt class="text-label-sm">LABEL-MD</dt><dd class="text-label-md">LABEL-MD</dd></div>
|
||||||
|
<div><dt class="text-label-sm">LABEL-SM</dt><dd class="text-label-sm">LABEL-SM</dd></div>
|
||||||
|
<div><dt class="text-label-sm">TELEMETRY-NUM</dt><dd class="text-telemetry-num">18:42</dd></div>
|
||||||
|
</dl>
|
||||||
|
<p class="text-body-sm">Hero color tokens — swatches render the tokens themselves:</p>
|
||||||
|
<dl class="ledger">
|
||||||
|
<div><dt class="text-label-sm">SIGNAL-ORANGE</dt><dd class="text-label-md"><span class="swatch swatch-signal-orange"></span>SIGNAL-ORANGE</dd></div>
|
||||||
|
<div><dt class="text-label-sm">FUNCTIONAL-RED</dt><dd class="text-label-md"><span class="swatch swatch-functional-red"></span>FUNCTIONAL-RED</dd></div>
|
||||||
|
<div><dt class="text-label-sm">OLIVE-GREEN</dt><dd class="text-label-md"><span class="swatch swatch-olive-green"></span>OLIVE-GREEN</dd></div>
|
||||||
|
<div><dt class="text-label-sm">SOFT-BEIGE</dt><dd class="text-label-md"><span class="swatch swatch-soft-beige"></span>SOFT-BEIGE</dd></div>
|
||||||
|
<div><dt class="text-label-sm">MATTE-BLACK</dt><dd class="text-label-md"><span class="swatch swatch-matte-black"></span>MATTE-BLACK</dd></div>
|
||||||
|
<div><dt class="text-label-sm">CHASSIS-SUBTLE</dt><dd class="text-label-md"><span class="swatch swatch-chassis-subtle"></span>CHASSIS-SUBTLE</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Approval strip -->
|
||||||
|
<section class="approval">
|
||||||
|
<p class="text-label-md">APPROVAL SCOPE // DSGN-02</p>
|
||||||
|
<p class="text-body-md">This gate covers the fidelity bar — every component family on the sheet renders from the token package, verified token-by-token against the mockup renders — and the basemap direction (Rams-constant cartography, accent reserved). It does NOT cover interactive flows: incident-report modal flows, map interaction, and animation polish are intentionally out of scope for this phase (deferred per the phase context).</p>
|
||||||
|
<p class="text-body-sm">Resume signal for the phase checkpoint: "approved" (fidelity bar met, basemap direction good) — or a list of specific screens / components / basemap aspects to fix and iterate before DSGN-02 is recorded.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"basemap": "node basemap/build.mjs",
|
"basemap": "node basemap/build.mjs",
|
||||||
"basemap:preview": "python3 -m http.server 8123",
|
"basemap:preview": "python3 -m http.server 8123",
|
||||||
"audit": "node ./scripts/audit.mjs ./dist/rams.css",
|
"audit": "node ./scripts/audit.mjs ./dist/rams.css",
|
||||||
|
"capture": "node ./scripts/screenshot.mjs",
|
||||||
"test": "node ./scripts/audit.test.mjs"
|
"test": "node ./scripts/audit.test.mjs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ const WRAPPER_AT = new Set([
|
|||||||
const IMPLEMENTATION_PAGES = [
|
const IMPLEMENTATION_PAGES = [
|
||||||
'src/sheet.html', // plan 01-02 — component sheet (D-03)
|
'src/sheet.html', // plan 01-02 — component sheet (D-03)
|
||||||
'basemap/index.html', // plan 01-03 — basemap proof page (D-08/D-09)
|
'basemap/index.html', // plan 01-03 — basemap proof page (D-08/D-09)
|
||||||
|
'gallery.html', // plan 01-04 — DSGN-02 approval gallery (D-12)
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ————— small helpers ————— */
|
/* ————— small helpers ————— */
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/*
|
||||||
|
* screenshot.mjs — Playwright capture toolchain (D-14)
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
* Produces the DSGN-02 gallery captures as REAL-BROWSER screenshots
|
||||||
|
* (chromium via playwright) — the same toolchain later UI phases
|
||||||
|
* reuse for their gallery rows (this script is the committed
|
||||||
|
* evidence generator; nothing here is scratch tooling).
|
||||||
|
*
|
||||||
|
* npm run capture (root delegate → theme workspace)
|
||||||
|
*
|
||||||
|
* Outputs (theme/screenshots/ — regenerated artifacts, gitignored):
|
||||||
|
* sheet-full.png component sheet, 390px, full page
|
||||||
|
* mockup-<screen>.png ×4 the four mockup screens rendered
|
||||||
|
* via file:// (CDN Tailwind + fonts,
|
||||||
|
* network required); on capture
|
||||||
|
* failure or a blank/undersized shot
|
||||||
|
* the screen's committed screen.png
|
||||||
|
* is copied in as fallback — mockups
|
||||||
|
* are read-only reference (D-06) and
|
||||||
|
* are NEVER edited
|
||||||
|
* basemap-kl.png Rams Liberty basemap over KL
|
||||||
|
* (1280×800, waits for the proof
|
||||||
|
* page's RAMS-BASEMAP-READY console
|
||||||
|
* signal + tile settle)
|
||||||
|
*
|
||||||
|
* The static server below serves theme/ as docroot so that
|
||||||
|
* ./dist/rams.css, the self-hosted woff2 fonts, and
|
||||||
|
* basemap/liberty-rams.json resolve exactly as they do for a user
|
||||||
|
* serving the theme workspace (e.g. npm run basemap:preview).
|
||||||
|
*/
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { copyFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { dirname, extname, join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
|
const SCRIPTS = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const THEME = resolve(SCRIPTS, '..');
|
||||||
|
const SHOTS = join(THEME, 'screenshots');
|
||||||
|
const MOCKUPS = resolve(THEME, '..', 'stitch_minimalist_route_navigator');
|
||||||
|
|
||||||
|
/* Minimum plausible non-blank PNG (the plan's own blank bar). */
|
||||||
|
const MIN_BYTES = 10240;
|
||||||
|
|
||||||
|
/* The four mockup screens (stitch directory names verbatim). */
|
||||||
|
const SCREENS = [
|
||||||
|
'active_route_guidance',
|
||||||
|
'community_incident_reporter',
|
||||||
|
'navigate_map_overview',
|
||||||
|
'telegram_live_traffic_feed',
|
||||||
|
];
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.json': 'application/json; charset=utf-8',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Tiny static server: theme/ as docroot (no deps beyond node itself). */
|
||||||
|
function serveTheme() {
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(req.url, 'http://127.0.0.1');
|
||||||
|
const target = join(THEME, decodeURIComponent(url.pathname));
|
||||||
|
if (!target.startsWith(THEME)) {
|
||||||
|
res.writeHead(403);
|
||||||
|
res.end('forbidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = await readFile(target);
|
||||||
|
res.writeHead(200, { 'content-type': MIME[extname(target)] || 'application/octet-stream' });
|
||||||
|
res.end(body);
|
||||||
|
} catch {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end('not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new Promise((resolveReady) => {
|
||||||
|
server.listen(0, '127.0.0.1', () => resolveReady({ server, port: server.address().port }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizeOf(file) {
|
||||||
|
return statSync(file).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = [];
|
||||||
|
const failures = [];
|
||||||
|
|
||||||
|
mkdirSync(SHOTS, { recursive: true });
|
||||||
|
const { server, port } = await serveTheme();
|
||||||
|
const base = `http://127.0.0.1:${port}`;
|
||||||
|
log.push(`serving theme/ → ${base}/`);
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
|
||||||
|
try {
|
||||||
|
/* 1 — component sheet (the implementation render): 390 viewport, full page. */
|
||||||
|
{
|
||||||
|
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
||||||
|
await page.goto(`${base}/src/sheet.html`, { waitUntil: 'networkidle', timeout: 45000 });
|
||||||
|
await page.evaluate(() => document.fonts.ready);
|
||||||
|
await page.waitForTimeout(400); // font raster settle
|
||||||
|
await page.screenshot({ path: join(SHOTS, 'sheet-full.png'), fullPage: true });
|
||||||
|
await page.close();
|
||||||
|
log.push(`captured sheet-full.png (${sizeOf(join(SHOTS, 'sheet-full.png'))}B, fullPage)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2 — mockup screens via file:// (CDN Tailwind + Google fonts need network).
|
||||||
|
* Fallback: committed <screen>/screen.png copied in — never edit mockups (D-06). */
|
||||||
|
for (const screen of SCREENS) {
|
||||||
|
const target = join(SHOTS, `mockup-${screen}.png`);
|
||||||
|
const url = pathToFileURL(join(MOCKUPS, screen, 'code.html')).href;
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
||||||
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 45000 });
|
||||||
|
await page.evaluate(() => document.fonts.ready);
|
||||||
|
await page.waitForTimeout(800); // plan's settle window
|
||||||
|
await page.screenshot({ path: target });
|
||||||
|
await page.close();
|
||||||
|
if (sizeOf(target) < MIN_BYTES) {
|
||||||
|
throw new Error(`capture undersized (${sizeOf(target)}B < ${MIN_BYTES}B) — likely blank`);
|
||||||
|
}
|
||||||
|
log.push(`captured mockup-${screen}.png (${sizeOf(target)}B, network render)`);
|
||||||
|
} catch (err) {
|
||||||
|
const committed = join(MOCKUPS, screen, 'screen.png');
|
||||||
|
if (!existsSync(committed)) {
|
||||||
|
failures.push(`mockup-${screen}.png: capture failed AND committed screen.png missing (${err.message})`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
copyFileSync(committed, target);
|
||||||
|
log.push(`FALLBACK mockup-${screen}.png ← committed screen.png (${err.message})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3 — basemap proof (1280×800): wait for the page's own READY signal, then settle. */
|
||||||
|
{
|
||||||
|
const target = join(SHOTS, 'basemap-kl.png');
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
||||||
|
const ready = new Promise((resolveReady) => {
|
||||||
|
page.on('console', (m) => {
|
||||||
|
if (m.text().startsWith('RAMS-BASEMAP-READY')) resolveReady(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.goto(`${base}/basemap/index.html`, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
||||||
|
const signal = await Promise.race([
|
||||||
|
ready,
|
||||||
|
new Promise((r) => setTimeout(() => r('timeout'), 30000)),
|
||||||
|
]);
|
||||||
|
if (signal === 'timeout') {
|
||||||
|
throw new Error('RAMS-BASEMAP-READY console signal never arrived (tiles/network?)');
|
||||||
|
}
|
||||||
|
await page.waitForTimeout(2000); // tile render settle (plan: first render + 2s)
|
||||||
|
await page.screenshot({ path: target });
|
||||||
|
await page.close();
|
||||||
|
if (sizeOf(target) < MIN_BYTES) {
|
||||||
|
throw new Error(`capture undersized (${sizeOf(target)}B < ${MIN_BYTES}B)`);
|
||||||
|
}
|
||||||
|
log.push(`captured basemap-kl.png (${sizeOf(target)}B, READY-signaled)`);
|
||||||
|
} catch (err) {
|
||||||
|
failures.push(`basemap-kl.png: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ————— report + blank gate ————— */
|
||||||
|
for (const line of log) console.log(`[capture] ${line}`);
|
||||||
|
|
||||||
|
const expected = ['sheet-full.png', 'basemap-kl.png', ...SCREENS.map((s) => `mockup-${s}.png`)];
|
||||||
|
const missing = expected.filter((f) => !existsSync(join(SHOTS, f)));
|
||||||
|
const undersized = expected.filter((f) => existsSync(join(SHOTS, f)) && sizeOf(join(SHOTS, f)) < MIN_BYTES);
|
||||||
|
|
||||||
|
if (missing.length || undersized.length || failures.length) {
|
||||||
|
for (const f of failures) console.error(`[capture] FAILED ${f}`);
|
||||||
|
for (const f of missing) console.error(`[capture] MISSING ${f}`);
|
||||||
|
for (const f of undersized) console.error(`[capture] UNDERSIZED ${f} (<${MIN_BYTES}B)`);
|
||||||
|
console.error(`CAPTURE FAILED — ${expected.length - missing.length}/${expected.length} usable captures`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`CAPTURE OK — ${expected.length}/${expected.length} non-blank captures under theme/screenshots/`);
|
||||||
|
process.exit(0);
|
||||||
Reference in New Issue
Block a user