diff --git a/.github/trigger-deploy b/.github/trigger-deploy index 4fff828e..566643f7 100644 --- a/.github/trigger-deploy +++ b/.github/trigger-deploy @@ -1 +1 @@ -1785992689 +1786027990 diff --git a/.github/trigger-test b/.github/trigger-test index a23595e0..0302ffd0 100644 --- a/.github/trigger-test +++ b/.github/trigger-test @@ -1 +1 @@ -1786026794 +1786029809 diff --git a/scripts/test-theme.js b/scripts/test-theme.js index 77e8d1b4..1fabb962 100644 --- a/scripts/test-theme.js +++ b/scripts/test-theme.js @@ -1,11 +1,23 @@ -// Dark-theme toggle — offline unit tests. No network. Verifies the shared -// shell ships the pre-paint theme script (no flash of the wrong theme), the -// moon/sun toggle button, and the dark-mode CSS overrides, and that the -// palette flip is internally consistent (--ink and --cream flip together so -// the ~100 solid-ink chips invert cleanly). +// Dark-only theme — offline unit tests. No network. +// +// The site used to ship a light default plus a moon/sun toggle, a pre-paint +// script reading localStorage, and a :root[data-theme="dark"] override block. +// That is all gone: dark is now the ONLY theme, set directly on :root. +// +// Why that shape rather than just defaulting the toggle to dark: a palette +// applied through an attribute needs JavaScript to set it, which means a frame +// of the wrong colours before the script runs, plus a stored preference that +// can disagree with the markup. Dark values on :root make the first paint +// already correct with no script at all. +// +// These assertions exist because a HALF-removed theme is worse than either +// state. Reintroducing a light token, an override block, or a toggle should +// fail here rather than ship a page that is dark in some places and white in +// others. // // node scripts/test-theme.js import { ledgerShell, LEDGER_CSS } from "../src/ledger-chrome.js"; +import { readFileSync } from "node:fs"; let passed = 0, failed = 0; const ok = (cond, msg) => { @@ -18,36 +30,55 @@ const html = ledgerShell({ baseUrl: "https://agent402.tools", body: "
hi
", }); -// --- no-flash: theme is applied from storage/prefers BEFORE first paint ------- -const headStart = html.slice(0, html.indexOf("")); -ok(headStart.includes("localStorage.getItem('a402-theme')"), "pre-paint script reads the saved theme"); -ok(headStart.includes("prefers-color-scheme:dark"), "falls back to the OS preference"); -ok(headStart.includes("setAttribute('data-theme','dark')"), "sets data-theme before paint"); -// the setter script must be in too, so the button's onclick resolves -ok(headStart.includes("function a402ToggleTheme"), "toggle function defined in head"); -ok(html.indexOf("function a402ToggleTheme") < html.indexOf(""), "toggle fn is above the body"); - -// --- the moon/sun control lives in the nav ----------------------------------- -ok(html.includes('class="ml-theme-toggle"'), "nav has the theme toggle button"); -ok(html.includes('onclick="a402ToggleTheme()"'), "button is wired to the toggle"); -ok(html.includes('class="ml-moon"') && html.includes('class="ml-sun"'), "both moon + sun glyphs present"); -ok(/aria-label="Toggle dark mode"/.test(html), "toggle is labelled for a11y"); - -// --- dark palette exists and flips the paired tokens together ---------------- -ok(LEDGER_CSS.includes(':root[data-theme="dark"]'), "dark-theme CSS block present"); -// --ink (foreground) goes light AND --cream (text on ink chips) goes dark, so -// every `background:var(--ink);color:var(--cream)` chip becomes light-on-dark. -const dark = LEDGER_CSS.slice(LEDGER_CSS.indexOf(':root[data-theme="dark"]')); -const darkBlock = dark.slice(0, dark.indexOf("}")); -ok(/--ink:\s*#[EeFf]/.test(darkBlock), "dark --ink is light"); -ok(/--cream:\s*#0/.test(darkBlock), "dark --cream is dark (chips invert cleanly)"); -ok(/--paper:\s*#0/.test(darkBlock), "dark --paper is dark"); -// the moon hides in dark, the sun hides in light (CSS-only, no JS needed) -ok(LEDGER_CSS.includes('.ml-theme-toggle .ml-sun'), "sun hidden by default (light)"); -ok(LEDGER_CSS.includes(':root[data-theme="dark"] .ml-theme-toggle .ml-moon'), "moon hidden in dark"); - -// light mode stays the default: no data-theme attribute is hardcoded on -ok(!/]*data-theme=/.test(html), "html has no hardcoded theme (light is the default)"); +// --- the :root palette IS the dark palette ---------------------------------- +const rootBlock = LEDGER_CSS.slice(LEDGER_CSS.indexOf(":root {"), LEDGER_CSS.indexOf("}", LEDGER_CSS.indexOf("--font-mono"))); +const tok = (name) => (rootBlock.match(new RegExp(`${name}:\\s*(\\S+);`)) || [])[1] || ""; +const isDark = (h) => /^#[0-3]/.test(h); // #0E0E10, #171719, #1E1E21… +const isLight = (h) => /^#[C-Fc-f]/.test(h); // #ECECEA, #F4F4F2… + +ok(isDark(tok("--paper")), `page background is dark (--paper ${tok("--paper")})`); +ok(isLight(tok("--ink")), `foreground is light (--ink ${tok("--ink")})`); +ok(isDark(tok("--card")), `cards are dark (--card ${tok("--card")})`); +// --ink and --cream must stay paired: ~100 chips are background:var(--ink) +// with color:var(--cream), so if only one of them flips they go invisible. +ok(isDark(tok("--cream")), `--cream is dark so ink chips read light-on-dark (${tok("--cream")})`); +ok(isLight(tok("--on-dark")), `text on dark surfaces stays light (--on-dark ${tok("--on-dark")})`); +ok(/color-scheme:\s*dark/.test(LEDGER_CSS), "color-scheme is dark so form controls and scrollbars match"); +ok(!/color-scheme:\s*light/.test(LEDGER_CSS), "no light color-scheme survives"); + +// --- nothing of the toggle mechanism is left -------------------------------- +ok(!LEDGER_CSS.includes('[data-theme="dark"]'), "no [data-theme] override block in the CSS"); +// Matches the ATTRIBUTE form (`data-theme=`) and the selector form +// (`[data-theme`), not the bare word: the CSS comment above legitimately +// explains why the attribute is gone, and a test that fails on its own +// documentation is testing prose rather than behaviour. +ok(!/data-theme\s*=/.test(html), "no data-theme attribute is set on any element"); +ok(!/\[data-theme/.test(html.replace(/\/\*[\s\S]*?\*\//g, "")), "no [data-theme] selector outside comments"); +ok(!html.includes("a402ToggleTheme"), "no toggle function ships"); +ok(!html.includes("a402-theme"), "no stored theme preference is read or written"); +ok(!html.includes("prefers-color-scheme"), "the OS preference no longer decides the palette"); +ok(!html.includes("ml-theme-toggle"), "the toggle button is gone from the nav"); +ok(!html.includes("ml-moon") && !html.includes("ml-sun"), "moon and sun glyphs are gone"); + +// --- no orphaned selector where the toggle rules used to be ----------------- +// The first removal pass matched from `.ml-theme-toggle` to end of line, which +// stranded `:root[data-theme="dark"] ` in front of the following comment and +// produced a malformed selector - the kind that silently invalidates the rule +// after it. Whole-line removal fixed it; this keeps it fixed. +ok((LEDGER_CSS.match(/\{/g) || []).length === (LEDGER_CSS.match(/\}/g) || []).length, + "LEDGER_CSS braces balance"); +const stranded = [...LEDGER_CSS.matchAll(/\}\s*([^\n{}]*)\/\*/g)].map((m) => m[1].trim()).filter(Boolean); +ok(stranded.length === 0, + `no selector text stranded before a comment${stranded.length ? ` (found "${stranded[0].slice(0, 60)}")` : ""}`); + +// --- the revenue chart palette had to follow the theme ---------------------- +// Its dark series colours were keyed on [data-theme="dark"]. With the attribute +// gone that rule could never match, so the chart would have kept LIGHT series +// colours on a permanently dark page: still legible, easy to miss in review, +// and wrong. +const revenueSrc = readFileSync(new URL("../src/revenue-live.js", import.meta.url), "utf8"); +ok(!revenueSrc.includes('[data-theme="dark"]'), "revenue chart has no dead [data-theme] rule"); +ok(/\.rvz\{--s1:#3987e5/.test(revenueSrc), "the chart's dark series palette is the one that ships"); console.log(`\n${failed ? "FAILED" : "OK"}: ${passed} passed, ${failed} failed`); process.exit(failed ? 1 : 0); diff --git a/src/ledger-chrome.js b/src/ledger-chrome.js index c9857755..a7852ee4 100644 --- a/src/ledger-chrome.js +++ b/src/ledger-chrome.js @@ -58,40 +58,6 @@ html { overflow-x: clip; } override on each dark container. Accent-as-background keeps white text legible on #BF360C (5.8:1); brightening it would BREAK that, so dark containers only override text, never accent-bg buttons. */ - --accent: #BF360C; - --accent-lit: #F0522E; - --paper: #FFFFFF; - --card: #F7F7F5; - --card-zebra: #F1F1EF; - --footer-bg: #F2F2F0; - --ink: #0B0B0B; - --ink-panel: #151515; - --muted: #4A4A4A; - --faint: #6A6A6A; - --hairline: #E0E0DE; - --dash: #C9C9C7; - --dark-border: #262626; - --dark-border2: #343434; - --cream: #FFFFFF; - --cream2: #F5F5F5; - --surface: #0B0B0B; - --on-dark: #FFFFFF; - --on-dark2: #F5F5F5; - --dk-muted: #9C9C9C; - --dk-muted2: #B8B8B8; - --dk-muted3: #888888; - --green: #3E9B6E; - --font-body: 'Archivo', 'Archivo Fallback', system-ui, sans-serif; - --font-mono: 'Space Mono', 'Space Mono Fallback', monospace; -} -/* Dark theme. Only the true foreground/page tokens flip: --ink (text + borders) - goes light, --paper/--card/--muted/--faint/--hairline/--dash flip to their - dark values. Dark SURFACES do NOT invert - every card, terminal, and CTA that - was background:var(--surface) with color:var(--on-dark) stays dark with light - text in both themes (that split is exactly why --surface / --on-dark exist, - separate from the dual-use --ink). Applied from localStorage (or - prefers-color-scheme) before first paint - see ledgerShell. */ -:root[data-theme="dark"] { --accent: #F0522E; --accent-lit: #F0522E; --paper: #0E0E10; @@ -99,28 +65,34 @@ html { overflow-x: clip; } --card-zebra: #1E1E21; --footer-bg: #131315; --ink: #ECECEA; + --ink-panel: #171719; --muted: #9E9E98; --faint: #6C6C68; --hairline: #2A2A30; --dash: #35353B; + --dark-border: #262626; + --dark-border2: #343434; --cream: #0E0E10; --cream2: #171719; - /* Dark surfaces (was background:var(--surface)) STAY dark - a card/terminal must - not invert to light. Their text (--on-dark*) STAYS light. Only foreground - --ink/--muted and page --paper actually flip. */ --surface: #17171A; --on-dark: #F4F4F2; --on-dark2: #CFCFCB; - --ink-panel: #171719; + --dk-muted: #9C9C9C; + --dk-muted2: #B8B8B8; + --dk-muted3: #888888; + --green: #3E9B6E; + --font-body: 'Archivo', 'Archivo Fallback', system-ui, sans-serif; + --font-mono: 'Space Mono', 'Space Mono Fallback', monospace; } -:root { color-scheme: light; } -:root[data-theme="dark"] { color-scheme: dark; } +/* Dark is the ONLY theme. The palette above IS the dark palette, set directly + on :root rather than behind a [data-theme] attribute, so the first paint is + already dark: no flash, no pre-paint script, no stored preference, and + nothing to get out of sync. There is deliberately no light mode and no + toggle. Dark SURFACES still do not invert - a card or terminal that was + background:var(--surface) with color:var(--on-dark) keeps light text, which + is why --surface / --on-dark stay separate from the dual-use --ink. */ +:root { color-scheme: dark; } body { transition: background-color .18s ease, color .18s ease; } -.ml-theme-toggle { display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; padding:0; border:1.5px solid var(--ink); background:transparent; color:var(--ink); cursor:pointer; } -.ml-theme-toggle:hover { background: var(--card-zebra); } -.ml-theme-toggle .ml-sun { display:none; } -:root[data-theme="dark"] .ml-theme-toggle .ml-moon { display:none; } -:root[data-theme="dark"] .ml-theme-toggle .ml-sun { display:inline-flex; } /* --- mobile hamburger menu (the hover nav dropdowns don't work on touch, and the inline links get squeezed to zero on a phone - so ≤880px collapses the whole nav into a tap menu) --- */ @@ -484,10 +456,7 @@ function nav(activePath) {
github - + ${activePath === "" ? "" : `ADD TO CLAUDE →`}