feat(theme): promote industrial blueprint to the whole shell
Some checks failed
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

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)
This commit is contained in:
Shad 2026-06-14 02:12:11 +04:00
parent 2edba020ef
commit dba1eb3328
9 changed files with 1300 additions and 9156 deletions

View File

@ -1,9 +1,8 @@
// Doctrine palette audit (S6). // Doctrine palette audit (shell-wide).
// 1. Every hex color inside the INDUSTRIAL BLUEPRINT CANVAS CSS section // The whole app is now blueprint-native: NO source file may contain a
// must be one of the doctrinal tokens. // hardcoded hex/rgb color outside the 11 doctrinal tokens.
// 2. Blueprint TSX files must not introduce hardcoded hex/rgb literals — import { readFileSync, readdirSync, statSync } from "node:fs";
// they should reference --bp-* tokens via var(...). import { join } from "node:path";
import { readFileSync } from "node:fs";
const DOCTRINE = new Set([ const DOCTRINE = new Set([
"#f5f7fb", "#e8edf5", "#d5dde9", "#f5f7fb", "#e8edf5", "#d5dde9",
@ -15,42 +14,55 @@ const DOCTRINE = new Set([
function fail(msg) { console.log("✗", msg); process.exitCode = 1; } function fail(msg) { console.log("✗", msg); process.exitCode = 1; }
function pass(msg) { console.log("✓", msg); } function pass(msg) { console.log("✓", msg); }
const css = readFileSync("src/index.css", "utf8"); function walk(dir, exts) {
const idx = css.indexOf("INDUSTRIAL BLUEPRINT CANVAS"); const out = [];
if (idx < 0) { fail("blueprint CSS block not found"); process.exit(1); } for (const e of readdirSync(dir)) {
const cssBlock = css.slice(idx); const p = join(dir, e);
const cssHexes = cssBlock.match(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g) || []; const s = statSync(p);
const cssOffending = cssHexes.filter((h) => !DOCTRINE.has(h.toLowerCase())); if (s.isDirectory()) { out.push(...walk(p, exts)); continue; }
if (cssOffending.length === 0) { if (exts.some((x) => p.endsWith(x))) out.push(p);
pass(`CSS palette: ${cssHexes.length} hex tokens, all doctrinal`); }
} else { return out;
fail(`CSS non-doctrinal hex tokens: ${cssOffending.join(", ")}`);
} }
const cssRgba = cssBlock.match(/rgba?\([^)]*\)/g) || []; const FILES = [
if (cssRgba.length === 0) { "src/index.css",
pass("CSS has no raw rgba() literals (alpha goes through color-mix tokens)"); ...walk("src", [".ts", ".tsx", ".json"]),
} else {
fail(`CSS raw rgba() literals: ${cssRgba.join(", ")}`);
}
const tsxFiles = [
"src/components/ProcessGraph.tsx",
"src/components/BlueprintFrame.tsx",
]; ];
let tsxOffending = 0;
for (const f of tsxFiles) {
const src = readFileSync(f, "utf8");
const hex = (src.match(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g) || []).filter(
(h) => !DOCTRINE.has(h.toLowerCase()),
);
const rgba = src.match(/rgba?\(\s*\d+/g) || [];
if (hex.length || rgba.length) {
fail(`${f}: hardcoded color literals: hex=${hex.join(",") || "none"} rgba=${rgba.join(",") || "none"}`);
tsxOffending += hex.length + rgba.length;
}
}
if (tsxOffending === 0) pass(`TSX palette: ${tsxFiles.length} blueprint TSX files clean (no hardcoded color literals)`);
if (process.exitCode === 1) console.log("\nPalette audit FAILED"); // CSS color keywords that are not allowed (besides the ~doctrinal greys we
else console.log("\nPalette audit PASSED"); // already use via tokens). The doctrine forbids "hardcoded color drift", so
// we reject named colors anywhere outside test fixtures.
const FORBIDDEN_NAMED = new Set([
"white", "black", "red", "green", "blue", "yellow", "orange", "purple",
"pink", "brown", "gray", "grey", "silver", "gold", "violet", "magenta",
"cyan", "lime", "teal", "navy", "maroon", "olive", "aqua", "fuchsia",
]);
let violations = 0;
for (const f of FILES) {
if (f.endsWith(".test.ts") || f.endsWith(".test.tsx")) continue;
const src = readFileSync(f, "utf8");
// Match a hex color only when it is NOT followed by another hex digit
// (so a literal like `#d3f1a` inside a deploy id is not mis-matched as `#d3f`).
const hex = (src.match(/#[0-9a-fA-F]{6}(?![0-9a-fA-F])|#[0-9a-fA-F]{3}(?![0-9a-fA-F])/g) || [])
.filter((h) => !DOCTRINE.has(h.toLowerCase()));
const rgb = src.match(/\b(?:rgba?|hsla?)\(\s*\d+/g) || [];
const named = [];
for (const w of FORBIDDEN_NAMED) {
const re = new RegExp(`\\b(?:background|color|fill|stroke|border)[^;:]*:\\s*${w}\\b`, "gi");
const m = src.match(re);
if (m) named.push(...m);
}
if (hex.length || rgb.length || named.length) {
fail(`${f}: hex=${hex.join(",") || "none"} rgb=${rgb.join(",") || "none"} named=${named.length ? named.join("; ") : "none"}`);
violations += hex.length + rgb.length + named.length;
}
}
if (violations === 0) {
pass(`palette clean: ${FILES.length} files scanned, 0 non-doctrinal color literals`);
console.log("\nPalette audit PASSED");
} else {
console.log(`\nPalette audit FAILED · ${violations} non-doctrinal literals`);
}

View File

@ -50,11 +50,11 @@ const reactFlowNodes = await page.locator(".bp-node").count();
logOk("graph rendered blueprint nodes > 0", reactFlowNodes > 0, `nodes=${reactFlowNodes}`); logOk("graph rendered blueprint nodes > 0", reactFlowNodes > 0, `nodes=${reactFlowNodes}`);
// Switch to AR blueprint // Switch to AR blueprint
await page.locator(".mc-tab", { hasText: "Accounts Receivable" }).click(); await page.locator(".mc-tab", { hasText: /accounts receivable/i }).click();
await page.waitForTimeout(600); await page.waitForTimeout(600);
await page.screenshot({ path: `${OUT}/03-mission-ar.png` }); await page.screenshot({ path: `${OUT}/03-mission-ar.png` });
logOk("AR scenario active", (await page.locator(".mc-hero-title").innerText()).toLowerCase().includes("refund")); logOk("AR scenario active", (await page.locator(".mc-hero-title").innerText()).toLowerCase().includes("refund"));
logOk("AR shows BLUEPRINT readout (not SYNTHETIC)", await page.locator(".bp-readout-blueprint .bp-readout-val", { hasText: "BLUEPRINT" }).isVisible()); logOk("AR shows BLUEPRINT readout (not SYNTHETIC)", await page.locator(".bp-readout-blueprint .bp-readout-val", { hasText: /blueprint/i }).isVisible());
// Queue card → inspector update // Queue card → inspector update
const qCards = await page.locator(".qcard").count(); const qCards = await page.locator(".qcard").count();
@ -65,18 +65,18 @@ if (qCards > 0) {
} }
// Inspector tab switching // Inspector tab switching
await page.locator(".itab", { hasText: "Evidence" }).click(); await page.locator(".itab", { hasText: /evidence/i }).click();
await page.waitForTimeout(150); await page.waitForTimeout(150);
logOk("evidence tab opens", await page.locator(".evt, .empty").first().isVisible()); logOk("evidence tab opens", await page.locator(".evt, .empty").first().isVisible());
await page.locator(".itab", { hasText: "Raw" }).click(); await page.locator(".itab", { hasText: /^raw/i }).click();
await page.waitForTimeout(150); await page.waitForTimeout(150);
logOk("raw tab opens", await page.locator(".raw-json").isVisible()); logOk("raw tab opens", await page.locator(".raw-json").isVisible());
await page.screenshot({ path: `${OUT}/04-inspector-raw.png` }); await page.screenshot({ path: `${OUT}/04-inspector-raw.png` });
// Preview-only action toast // Preview-only action toast
await page.locator(".itab", { hasText: "Overview" }).click(); await page.locator(".itab", { hasText: /overview/i }).click();
await page.waitForTimeout(150); await page.waitForTimeout(150);
const approveBtn = page.locator(".i-actions .btn", { hasText: "Approve" }).first(); const approveBtn = page.locator(".i-actions .btn", { hasText: /approve/i }).first();
const approveCount = await approveBtn.count(); const approveCount = await approveBtn.count();
if (approveCount > 0) { if (approveCount > 0) {
await approveBtn.click(); await approveBtn.click();
@ -108,13 +108,13 @@ await page.keyboard.press("Escape");
await page.waitForTimeout(100); await page.waitForTimeout(100);
// Studio scene renders + shows JSON preview + node editor // Studio scene renders + shows JSON preview + node editor
await page.locator(".tab", { hasText: "Studio" }).click(); await page.locator(".tab", { hasText: /studio/i }).click();
await page.waitForSelector(".studio"); await page.waitForSelector(".studio");
await page.waitForTimeout(250); await page.waitForTimeout(250);
await page.screenshot({ path: `${OUT}/06-studio.png` }); await page.screenshot({ path: `${OUT}/06-studio.png` });
logOk("studio scene renders", await page.locator(".studio-panel").count() >= 4); logOk("studio scene renders", await page.locator(".studio-panel").count() >= 4);
logOk("studio JSON preview shows config payload", /\"nodes\"/.test(await page.locator(".raw-json").first().innerText())); logOk("studio JSON preview shows config payload", /\"nodes\"/.test(await page.locator(".raw-json").first().innerText()));
await page.locator(".tab", { hasText: "Mission" }).click(); await page.locator(".tab", { hasText: /mission/i }).click();
await page.waitForSelector(".bp-frame"); await page.waitForSelector(".bp-frame");
// Live-mode toggle (real network call to demo.flow-master.ai via vite proxy) // Live-mode toggle (real network call to demo.flow-master.ai via vite proxy)
@ -132,28 +132,28 @@ logOk("toggle to LIVE mode resolves and updates pill", liveOk);
if (liveOk) { if (liveOk) {
await page.waitForTimeout(400); await page.waitForTimeout(400);
await page.screenshot({ path: `${OUT}/09-live-mode.png` }); await page.screenshot({ path: `${OUT}/09-live-mode.png` });
logOk("Refresh button appears in live mode", await page.locator(".link-btn", { hasText: "Refresh" }).isVisible()); logOk("Refresh button appears in live mode", await page.locator(".link-btn", { hasText: /refresh/i }).isVisible());
// Refresh must trigger ANOTHER /api/ea2/work-items request (Oracle round 2 fix). // Refresh must trigger ANOTHER /api/ea2/work-items request (Oracle round 2 fix).
// Wait for the Refresh button to become enabled (initial fetch may still be settling). // Wait for the Refresh button to become enabled (initial fetch may still be settling).
await page.locator(".link-btn", { hasText: "Refresh" }).waitFor({ state: "visible" }); await page.locator(".link-btn", { hasText: /refresh/i }).waitFor({ state: "visible" });
await page.waitForFunction( await page.waitForFunction(
() => { () => {
const btns = Array.from(document.querySelectorAll(".link-btn")); const btns = Array.from(document.querySelectorAll(".link-btn"));
const btn = btns.find((b) => (b.textContent || "").includes("Refresh")); const btn = btns.find((b) => /refresh/i.test(b.textContent || ""));
return btn && !btn.disabled; return btn && !btn.disabled;
}, },
{ timeout: 10000 }, { timeout: 10000 },
); );
const before = networkCalls.length; const before = networkCalls.length;
await page.locator(".link-btn", { hasText: "Refresh" }).click(); await page.locator(".link-btn", { hasText: /refresh/i }).click();
await page.waitForTimeout(2500); await page.waitForTimeout(2500);
logOk("Refresh re-fetches /api/ea2/work-items", networkCalls.length > before, logOk("Refresh re-fetches /api/ea2/work-items", networkCalls.length > before,
`before=${before} after=${networkCalls.length}`); `before=${before} after=${networkCalls.length}`);
} }
// LeftRail Confirm button: toast must name /api/runtime/transactions/{id}/actions // LeftRail Confirm button: toast must name /api/runtime/transactions/{id}/actions
await page.locator(".mc-tab", { hasText: "Accounts Receivable" }).click(); await page.locator(".mc-tab", { hasText: /accounts receivable/i }).click();
await page.waitForTimeout(400); await page.waitForTimeout(400);
const confirmBtn = page.locator(".agent-acts .btn-primary").first(); const confirmBtn = page.locator(".agent-acts .btn-primary").first();
if (await confirmBtn.count() > 0) { if (await confirmBtn.count() > 0) {
@ -171,13 +171,13 @@ logOk("telemetry has no hardcoded 97.4%", !/97\.4%/.test(tel), `tel="${tel.repla
logOk("telemetry labels SLA as derived", /derived/i.test(tel)); logOk("telemetry labels SLA as derived", /derived/i.test(tel));
// Run history scene // Run history scene
await page.locator(".tab", { hasText: "Runs" }).click(); await page.locator(".tab", { hasText: /runs/i }).click();
await page.waitForSelector(".rh"); await page.waitForSelector(".rh");
await page.waitForTimeout(250); await page.waitForTimeout(250);
await page.screenshot({ path: `${OUT}/07-run-history.png` }); await page.screenshot({ path: `${OUT}/07-run-history.png` });
const rhRows = await page.locator(".rh-row").count(); const rhRows = await page.locator(".rh-row").count();
logOk("run-history rows present", rhRows > 0, `rows=${rhRows}`); logOk("run-history rows present", rhRows > 0, `rows=${rhRows}`);
await page.locator(".rh-chip", { hasText: "running" }).click(); await page.locator(".rh-chip", { hasText: /^running/i }).click();
await page.waitForTimeout(200); await page.waitForTimeout(200);
const rhRunning = await page.locator(".rh-row").count(); const rhRunning = await page.locator(".rh-row").count();
logOk("run-history filter shrinks list", rhRunning > 0 && rhRunning <= rhRows, `running=${rhRunning} of ${rhRows}`); logOk("run-history filter shrinks list", rhRunning > 0 && rhRunning <= rhRows, `running=${rhRunning} of ${rhRows}`);

View File

@ -59,20 +59,20 @@ 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); ok("S3.f legend swatches present", await p.locator(".bp-swatch").count() >= 5);
// S5: Studio + Settings + Console still render // S5: Studio + Settings + Console still render
await p.locator(".tab", { hasText: "Studio" }).click(); await p.locator(".tab", { hasText: /studio/i }).click();
await p.waitForSelector(".studio"); await p.waitForSelector(".studio");
ok("S5.a studio renders", await p.locator(".studio-panel").count() >= 4); ok("S5.a studio renders", await p.locator(".studio-panel").count() >= 4);
await p.locator(".tab", { hasText: "Settings" }).click(); await p.locator(".tab", { hasText: /settings/i }).click();
await p.waitForSelector(".settings"); await p.waitForSelector(".settings");
ok("S5.b settings renders", await p.locator(".quick-users .link-btn").count() >= 1); ok("S5.b settings renders", await p.locator(".quick-users .link-btn").count() >= 1);
await p.locator(".tab", { hasText: "Mission" }).click(); await p.locator(".tab", { hasText: /mission/i }).click();
await p.waitForSelector(".bp-frame"); await p.waitForSelector(".bp-frame");
await p.locator(".link-btn", { hasText: "Console" }).first().click(); await p.locator(".link-btn", { hasText: /console/i }).first().click();
await p.waitForSelector(".console"); await p.waitForSelector(".console");
ok("S5.c console renders", await p.locator(".console").isVisible()); ok("S5.c console renders", await p.locator(".console").isVisible());
// S6 cleanup: take a final picture of the canvas // S6 cleanup: take a final picture of the canvas
await p.locator(".link-btn", { hasText: "Console" }).first().click(); await p.locator(".link-btn", { hasText: /console/i }).first().click();
await p.waitForTimeout(300); await p.waitForTimeout(300);
await p.screenshot({ path: `${OUT}/blueprint-canvas-clean.png`, fullPage: false }); await p.screenshot({ path: `${OUT}/blueprint-canvas-clean.png`, fullPage: false });

41
qa/snap_all_scenes.mjs Normal file
View File

@ -0,0 +1,41 @@
// Take a fresh screenshot of every scene with the blueprint shell.
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
mkdirSync("qa/screenshots/v4", { recursive: true });
const URL = process.env.URL || "http://127.0.0.1:5173";
const b = await chromium.launch({ headless: true });
const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
await p.goto(URL, { waitUntil: "networkidle" });
await p.waitForTimeout(400);
await p.screenshot({ path: "qa/screenshots/v4/01-landing.png" });
await p.locator(".sc-card").first().click();
await p.waitForSelector(".mc"); await p.waitForTimeout(800);
await p.screenshot({ path: "qa/screenshots/v4/02-mission-procurement.png" });
await p.locator(".mc-tab", { hasText: /accounts receivable/i }).click();
await p.waitForTimeout(700);
await p.screenshot({ path: "qa/screenshots/v4/03-mission-ar-blueprint.png" });
await p.locator(".itab", { hasText: /^raw/i }).click(); await p.waitForTimeout(200);
await p.screenshot({ path: "qa/screenshots/v4/04-inspector-raw.png" });
await p.keyboard.press("Meta+k"); await p.waitForSelector(".cmd"); await p.waitForTimeout(200);
await p.screenshot({ path: "qa/screenshots/v4/05-command-palette.png" });
await p.keyboard.press("Escape"); await p.waitForTimeout(100);
await p.locator(".tab", { hasText: /studio/i }).click();
await p.waitForSelector(".studio"); await p.waitForTimeout(300);
await p.screenshot({ path: "qa/screenshots/v4/06-studio.png" });
await p.locator(".tab", { hasText: /settings/i }).click();
await p.waitForSelector(".settings"); await p.waitForTimeout(300);
await p.screenshot({ path: "qa/screenshots/v4/07-settings.png" });
await p.locator(".tab", { hasText: /runs/i }).click();
await p.waitForSelector(".rh"); await p.waitForTimeout(300);
await p.screenshot({ path: "qa/screenshots/v4/08-run-history.png" });
await p.locator(".tab", { hasText: /mission/i }).click(); await p.waitForTimeout(400);
await p.locator(".link-btn", { hasText: /console/i }).first().click();
await p.waitForSelector(".console");
await p.locator(".mode-toggle").first().click();
await p.waitForFunction(() => Array.from(document.querySelectorAll(".mode-pill")).some(el => el.textContent?.trim() === "LIVE"), { timeout: 15000 });
await p.waitForTimeout(2000);
await p.screenshot({ path: "qa/screenshots/v4/09-mission-live-with-console.png" });
const fs = await import("node:fs/promises");
const list = await fs.readdir("qa/screenshots/v4");
console.log(`captured ${list.length} screenshots: ${list.sort().join(", ")}`);
await b.close();

View File

@ -6,11 +6,11 @@ import { useApp } from "../state/store";
import { Close, Refresh, Layers, Pulse } from "./icons"; import { Close, Refresh, Layers, Pulse } from "./icons";
const METHOD_COLOR: Record<string, string> = { const METHOD_COLOR: Record<string, string> = {
GET: "var(--text-2)", GET: "var(--bp-muted)",
POST: "#7eb0ff", POST: "var(--bp-info)",
PUT: "#f5b755", PUT: "var(--bp-amber)",
PATCH: "#f5b755", PATCH: "var(--bp-amber)",
DELETE: "#ff8a8a", DELETE: "var(--bp-err)",
}; };
function statusClass(s: number): string { function statusClass(s: number): string {

View File

@ -90,7 +90,7 @@ function baseTour(familyId: string): TourStep[] {
// ---------- AR · Customer Refund Approval ---------- // ---------- AR · Customer Refund Approval ----------
export const arRefund = build( export const arRefund = build(
"ar", "ar",
{ id: "ar", label: "Accounts Receivable", subtitle: "Refunds, credits & collections", accent: "#10b981" }, { id: "ar", label: "Accounts Receivable", subtitle: "Refunds, credits & collections", accent: "#3d6a2c" },
"syn-ar-refund-v1", "syn-ar-refund-v1",
"AR · Customer Refund Approval", "AR · Customer Refund Approval",
"Customer refund · risk-scored · synthetic preview", "Customer refund · risk-scored · synthetic preview",
@ -161,7 +161,7 @@ export const arRefund = build(
// ---------- HCM · New Hire Onboarding ---------- // ---------- HCM · New Hire Onboarding ----------
export const hcmOnboarding = build( export const hcmOnboarding = build(
"hcm", "hcm",
{ id: "hcm", label: "People Operations", subtitle: "Onboard · Offboard · Leave", accent: "#a855f7" }, { id: "hcm", label: "People Operations", subtitle: "Onboard · Offboard · Leave", accent: "#1d6f82" },
"syn-hcm-onboarding-v1", "syn-hcm-onboarding-v1",
"HCM · New Hire Onboarding", "HCM · New Hire Onboarding",
"Day-0 → Day-30 onboarding · synthetic preview", "Day-0 → Day-30 onboarding · synthetic preview",
@ -223,7 +223,7 @@ export const hcmOnboarding = build(
// ---------- GL · Period-End Close ---------- // ---------- GL · Period-End Close ----------
export const glClose = build( export const glClose = build(
"gl", "gl",
{ id: "gl", label: "GL Close", subtitle: "Accruals, reconciliations, journals", accent: "#f59e0b" }, { id: "gl", label: "GL Close", subtitle: "Accruals, reconciliations, journals", accent: "#c46a14" },
"syn-gl-close-v1", "syn-gl-close-v1",
"GL · Period-End Close", "GL · Period-End Close",
"Period-end close · automated accrual collection · synthetic preview", "Period-end close · automated accrual collection · synthetic preview",
@ -283,7 +283,7 @@ export const glClose = build(
// ---------- Service Ops · Customer Incident ---------- // ---------- Service Ops · Customer Incident ----------
export const svcIncident = build( export const svcIncident = build(
"service", "service",
{ id: "service", label: "Service Operations", subtitle: "Tickets, incidents, support", accent: "#ef4444" }, { id: "service", label: "Service Operations", subtitle: "Tickets, incidents, support", accent: "#a6342a" },
"syn-svc-incident-v1", "syn-svc-incident-v1",
"Service Ops · Customer Incident", "Service Ops · Customer Incident",
"Triage → assign → resolve · synthetic preview", "Triage → assign → resolve · synthetic preview",

File diff suppressed because it is too large Load Diff

View File

@ -217,11 +217,11 @@ interface CandidateBucket {
} }
const FAMILIES = [ const FAMILIES = [
{ id: "procurement", label: "Procurement to Pay", subtitle: "Requisition → PO → 3-way match", accent: "#3b82f6", re: /procure|purchas|pr_to_po|atlas|requisition|po\b/i }, { id: "procurement", label: "Procurement to Pay", subtitle: "Requisition → PO → 3-way match", accent: "#1a2740", re: /procure|purchas|pr_to_po|atlas|requisition|po\b/i },
{ id: "ar", label: "Accounts Receivable", subtitle: "Refunds, credits & collections", accent: "#10b981", re: /refund|credit|collect|receivable|invoice/i }, { id: "ar", label: "Accounts Receivable", subtitle: "Refunds, credits & collections", accent: "#3d6a2c", re: /refund|credit|collect|receivable|invoice/i },
{ id: "hcm", label: "People Operations", subtitle: "Onboard · Offboard · Leave", accent: "#a855f7", re: /onboard|offboard|hire|hcm|employee|leave|payroll|hr\b/i }, { id: "hcm", label: "People Operations", subtitle: "Onboard · Offboard · Leave", accent: "#1d6f82", re: /onboard|offboard|hire|hcm|employee|leave|payroll|hr\b/i },
{ id: "gl", label: "GL Close", subtitle: "Accruals, reconciliations, journals", accent: "#f59e0b", re: /close|ledger|journal|accrual|reconcil|gl\b/i }, { id: "gl", label: "GL Close", subtitle: "Accruals, reconciliations, journals", accent: "#c46a14", re: /close|ledger|journal|accrual|reconcil|gl\b/i },
{ id: "service", label: "Service Operations", subtitle: "Tickets, incidents, support", accent: "#ef4444", re: /ticket|incident|support|service|case\b/i }, { id: "service", label: "Service Operations", subtitle: "Tickets, incidents, support", accent: "#a6342a", re: /ticket|incident|support|service|case\b/i },
]; ];
export async function buildLiveScenariosFromApi(signal?: AbortSignal): Promise<{ scenarios: ProcessScenario[]; workItems: WorkItem[]; distinctDefs: number }> { export async function buildLiveScenariosFromApi(signal?: AbortSignal): Promise<{ scenarios: ProcessScenario[]; workItems: WorkItem[]; distinctDefs: number }> {
@ -298,7 +298,7 @@ export async function buildLiveScenariosFromApi(signal?: AbortSignal): Promise<{
id: `extra-${scenarios.length}`, id: `extra-${scenarios.length}`,
label: e.graph.process_definition.display_name || e.graph.process_definition.name, label: e.graph.process_definition.display_name || e.graph.process_definition.name,
subtitle: `${e.bucket.cases.length} live cases`, subtitle: `${e.bucket.cases.length} live cases`,
accent: "#64748b", accent: "#4a5b80",
}, },
e.bucket.key, e.bucket.key,
e.graph, e.graph,

File diff suppressed because one or more lines are too long