Every scene now reads as the same FM doctrine: paper canvas + navy
frame + amber accent, 1px hairlines, square edges, monospace
uppercase labels, no glass, no shadows, no gradients.
WHAT CHANGED
- src/index.css rewritten end-to-end. Doctrinal hex tokens declared
at :root, legacy --bg/--surface/--text/--primary/--border aliases
repointed at doctrine values so existing component classes inherit
the blueprint palette without per-component churn. Global
* { border-radius: 0 } + box-shadow strip-out. All low-opacity
tints via color-mix(in srgb, var(--bp-navy) X%, transparent) so no
rgba() literals survive.
- Topbar redone as a 44px instrument strip: navy brand-lock with
amber mark, uppercase mono tabs, amber-on-paper selected tab,
square link buttons, mode pill with currentColor border.
- Mission Control hero + scenario tab strip + KPI cards retoken
with amber underline on selected.
- Left rail: 1px-bordered KPI grid, queue cards stack as a single
bordered list, agent supervision actions span the full width.
- Inspector: tabbed nav with amber selected, hairline-separated
fields, square rule + run + evidence cards.
- Command palette: paper bg, amber-bordered selected item, mono
caps headings.
- Live API console: paper drawer, mono call rows, amber filter chip,
color-tokenised METHOD_COLOR map.
- Toaster: left-border accent on paper surface.
- Telemetry: navy gauges, mono row, square tick.
- Studio + Settings: shared studio-grid layout — paper panels in a
1px navy grid with no margins between, mono inputs.
- Run History: mono table rows with amber selected filter chip.
- Landing: mono hero, square brand mark, stats strip as one
bordered row, scenario cards in a 1px grid (no glow, no shadow,
no gradient).
- Family accents in synthetic.ts and buildScenarios.ts retoken to
doctrine hex. src/scenarios.json snapshot patched in place.
- Console.tsx METHOD_COLOR map → var(--bp-muted/info/amber/err).
AUDIT
- qa/palette_audit.mjs upgraded: scans 31 source files (.ts/.tsx/
.json + index.css), catches rgb()/rgba()/hsl()/hsla() literals,
and refuses named CSS colors (white/red/blue/...) as background/
color/fill/stroke/border values. Hex regex uses (?![0-9a-fA-F])
lookahead so deploy-id text like '#d3f1a' is not a false positive.
Result: 0 non-doctrinal literals anywhere in src/.
- qa/smoke.mjs + qa/smoke_blueprint.mjs hasText matches converted
to case-insensitive regex because the doctrine uppercases every
user-visible label via text-transform: uppercase.
- qa/snap_all_scenes.mjs captures 9 fresh 1440x900 screenshots
in qa/screenshots/v4/ (landing, mission procurement, mission AR
blueprint, inspector raw, command palette, studio, settings,
run history, mission live with console).
VERIFY
- tsc -b clean
- vite build green (CSS 41 KB / 8 KB gz, JS 851 KB / 230 KB gz)
- vitest 5 files / 24 tests green
- main smoke 27/27, 0 console errors
- blueprint smoke 15/15, 0 console errors
- palette audit clean (31 files)
ORACLE-REVIEWED
Round 1 PASS with <promise>VERIFIED</promise>. Two non-blocking
audit hardening notes landed in this same commit: scan JSON,
catch rgb/hsl/named CSS colors.
Confidence: high
Scope-risk: moderate (theme sweep, all scenes touched)
Not-tested: pixel-level visual diff vs prior theme (Oracle could
not inspect images this round; relied on programmatic checks)
84 lines
4.0 KiB
JavaScript
84 lines
4.0 KiB
JavaScript
// Blueprint canvas smoke covering S1, S2, S3 from the canvas-reimagine
|
|
// scenario contract. Run after `vite dev` is up at 127.0.0.1:5173.
|
|
import { chromium } from "playwright";
|
|
import { mkdirSync } from "node:fs";
|
|
|
|
const URL = process.env.URL || "http://127.0.0.1:5173";
|
|
const OUT = "qa/screenshots";
|
|
mkdirSync(OUT, { recursive: true });
|
|
|
|
function ok(label, cond, detail = "") {
|
|
console.log(`${cond ? "✓" : "✗"} ${label}${detail ? " · " + detail : ""}`);
|
|
if (!cond) process.exitCode = 1;
|
|
}
|
|
|
|
const b = await chromium.launch({ headless: true });
|
|
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
|
|
const p = await ctx.newPage();
|
|
const errors = [];
|
|
p.on("pageerror", (e) => errors.push(`pageerror ${e.message}`));
|
|
p.on("console", (m) => { if (m.type() === "error") errors.push(`console.error ${m.text().slice(0, 200)}`); });
|
|
|
|
await p.goto(URL, { waitUntil: "networkidle" });
|
|
await p.locator(".sc-card").first().click();
|
|
await p.waitForSelector(".mc");
|
|
await p.waitForTimeout(800);
|
|
await p.screenshot({ path: `${OUT}/blueprint-canvas.png`, fullPage: false });
|
|
|
|
// S1: paper background + navy hairline grid + square nodes
|
|
const frame = p.locator(".bp-frame").first();
|
|
ok("S1.a blueprint frame mounted", await frame.isVisible());
|
|
const nodeStyle = await p.locator(".bp-node").first().evaluate((el) => {
|
|
const cs = getComputedStyle(el);
|
|
return {
|
|
radius: cs.borderTopLeftRadius,
|
|
borderColor: cs.borderTopColor,
|
|
fontFamily: cs.fontFamily,
|
|
};
|
|
});
|
|
ok("S1.b node has zero border radius", nodeStyle.radius === "0px", `was ${nodeStyle.radius}`);
|
|
ok("S1.c node uses monospace font", /mono|Fira Code/i.test(nodeStyle.fontFamily), `font=${nodeStyle.fontFamily}`);
|
|
const canvasBg = await p.locator(".bp-canvas-wrap").first().evaluate((el) => getComputedStyle(el).backgroundColor);
|
|
ok("S1.d canvas background is paper #f5f7fb", canvasBg === "rgb(245, 247, 251)", `was ${canvasBg}`);
|
|
const gridLines = await p.locator(".react-flow__background").count();
|
|
ok("S1.e two grid backgrounds rendered (8px minor + 64px major)", gridLines >= 2, `count=${gridLines}`);
|
|
|
|
// S2: edges use step (orthogonal) — path commands are only M/L/H/V
|
|
const pathD = await p.locator(".react-flow__edge-path").first().evaluate((el) => el.getAttribute("d") || "");
|
|
const hasCurve = /[CQcq]/.test(pathD);
|
|
ok("S2 edges are orthogonal (no curve commands)", !hasCurve, `d=${pathD.slice(0, 60)}…`);
|
|
|
|
// S3: ruler chrome + corner readout
|
|
ok("S3.a top ruler present", await p.locator(".bp-ruler-top").isVisible());
|
|
ok("S3.b left ruler present", await p.locator(".bp-ruler-left").isVisible());
|
|
const tickCount = await p.locator(".bp-tick.is-major").count();
|
|
ok("S3.c major ticks rendered (>= 4)", tickCount >= 4, `major ticks=${tickCount}`);
|
|
ok("S3.d corner glyph present", await p.locator(".bp-corner-tl").isVisible());
|
|
const readouts = await p.locator(".bp-readout-top .bp-readout-cell").count();
|
|
ok("S3.e top readout has >= 6 cells", readouts >= 6, `cells=${readouts}`);
|
|
ok("S3.f legend swatches present", await p.locator(".bp-swatch").count() >= 5);
|
|
|
|
// S5: Studio + Settings + Console still render
|
|
await p.locator(".tab", { hasText: /studio/i }).click();
|
|
await p.waitForSelector(".studio");
|
|
ok("S5.a studio renders", await p.locator(".studio-panel").count() >= 4);
|
|
await p.locator(".tab", { hasText: /settings/i }).click();
|
|
await p.waitForSelector(".settings");
|
|
ok("S5.b settings renders", await p.locator(".quick-users .link-btn").count() >= 1);
|
|
await p.locator(".tab", { hasText: /mission/i }).click();
|
|
await p.waitForSelector(".bp-frame");
|
|
await p.locator(".link-btn", { hasText: /console/i }).first().click();
|
|
await p.waitForSelector(".console");
|
|
ok("S5.c console renders", await p.locator(".console").isVisible());
|
|
|
|
// S6 cleanup: take a final picture of the canvas
|
|
await p.locator(".link-btn", { hasText: /console/i }).first().click();
|
|
await p.waitForTimeout(300);
|
|
await p.screenshot({ path: `${OUT}/blueprint-canvas-clean.png`, fullPage: false });
|
|
|
|
console.log(`\nconsole errors: ${errors.length}`);
|
|
errors.slice(0, 5).forEach((e) => console.log(" -", e));
|
|
if (errors.length > 0) process.exitCode = 1;
|
|
|
|
await b.close();
|