diff --git a/theme/gallery.html b/theme/gallery.html new file mode 100644 index 0000000..f5404d3 --- /dev/null +++ b/theme/gallery.html @@ -0,0 +1,290 @@ + + + + + + + ARAHARAH — DSGN-02 Approval Gallery + + + + + +
+ + + + +
+

+ MOCKUP VS IMPLEMENTATION + ACTIVE ROUTE GUIDANCE +

+
+
+
MOCKUP — REFERENCE RENDER
+ Active route guidance mockup render +
+
+
IMPLEMENTATION — COMPONENT SHEET
+ Rams component sheet rendered from the token package +
+
+

Compare: maneuver banner + arrow (SEC-05), telemetry card (SEC-04), event chips (SEC-03), headline / telemetry-num typography, buttons (SEC-01).

+
+ + +
+

+ MOCKUP VS IMPLEMENTATION + NAVIGATE MAP OVERVIEW +

+
+
+
MOCKUP — REFERENCE RENDER
+ Navigate map overview mockup render +
+
+
IMPLEMENTATION — COMPONENT SHEET
+ Rams component sheet rendered from the token package +
+
+

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.

+
+ + +
+

+ MOCKUP VS IMPLEMENTATION + COMMUNITY INCIDENT REPORTER +

+
+
+
MOCKUP — REFERENCE RENDER
+ Community incident reporter mockup render +
+
+
IMPLEMENTATION — COMPONENT SHEET
+ Rams component sheet rendered from the token package +
+
+

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).

+
+ + +
+

+ MOCKUP VS IMPLEMENTATION + TELEGRAM LIVE TRAFFIC FEED +

+
+
+
MOCKUP — REFERENCE RENDER
+ Telegram live traffic feed mockup render +
+
+
IMPLEMENTATION — COMPONENT SHEET
+ Rams component sheet rendered from the token package +
+
+

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).

+
+ + +
+

+ BASEMAP DIRECTION + RAMS LIBERTY // KL METRO +

+
+
+ Rams Liberty basemap rendering over KL with the reserved signal-orange route vector +
+
+

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.

+
+ + +
+

+ TOKEN LEDGER + NAMES ONLY // D-07 +

+

Typography scale — every name rendered in its own token (the package owns every numeric value):

+
+
DISPLAY-LG
DISPLAY-LG
+
DISPLAY-LG-MOBILE
DISPLAY-LG-MOBILE
+
HEADLINE-LG
HEADLINE-LG
+
HEADLINE-LG-MOBILE
HEADLINE-LG-MOBILE
+
HEADLINE-MD
HEADLINE-MD
+
HEADLINE-SM
HEADLINE-SM
+
BODY-LG
BODY-LG
+
BODY-MD
BODY-MD
+
BODY-SM
BODY-SM
+
LABEL-LG
LABEL-LG
+
LABEL-MD
LABEL-MD
+
LABEL-SM
LABEL-SM
+
TELEMETRY-NUM
18:42
+
+

Hero color tokens — swatches render the tokens themselves:

+
+
SIGNAL-ORANGE
SIGNAL-ORANGE
+
FUNCTIONAL-RED
FUNCTIONAL-RED
+
OLIVE-GREEN
OLIVE-GREEN
+
SOFT-BEIGE
SOFT-BEIGE
+
MATTE-BLACK
MATTE-BLACK
+
CHASSIS-SUBTLE
CHASSIS-SUBTLE
+
+
+ + +
+

APPROVAL SCOPE // DSGN-02

+

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).

+

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.

+
+
+ + diff --git a/theme/package.json b/theme/package.json index 741e8a2..1c2b623 100644 --- a/theme/package.json +++ b/theme/package.json @@ -15,6 +15,7 @@ "basemap": "node basemap/build.mjs", "basemap:preview": "python3 -m http.server 8123", "audit": "node ./scripts/audit.mjs ./dist/rams.css", + "capture": "node ./scripts/screenshot.mjs", "test": "node ./scripts/audit.test.mjs" } } diff --git a/theme/scripts/audit.mjs b/theme/scripts/audit.mjs index c90cb47..7b2999e 100644 --- a/theme/scripts/audit.mjs +++ b/theme/scripts/audit.mjs @@ -99,6 +99,7 @@ const WRAPPER_AT = new Set([ const IMPLEMENTATION_PAGES = [ 'src/sheet.html', // plan 01-02 — component sheet (D-03) '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 ————— */ diff --git a/theme/scripts/screenshot.mjs b/theme/scripts/screenshot.mjs new file mode 100644 index 0000000..5041f4c --- /dev/null +++ b/theme/scripts/screenshot.mjs @@ -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-.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.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);