Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/trigger-deploy
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1785992689
1786027990
2 changes: 1 addition & 1 deletion .github/trigger-test
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1786026794
1786029809
101 changes: 66 additions & 35 deletions scripts/test-theme.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand All @@ -18,36 +30,55 @@ const html = ledgerShell({
baseUrl: "https://agent402.tools", body: "<main>hi</main>",
});

// --- no-flash: theme is applied from storage/prefers BEFORE first paint -------
const headStart = html.slice(0, html.indexOf("</head>"));
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 <head> too, so the button's onclick resolves
ok(headStart.includes("function a402ToggleTheme"), "toggle function defined in head");
ok(html.indexOf("function a402ToggleTheme") < html.indexOf("</head>"), "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 <html>
ok(!/<html[^>]*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);
69 changes: 18 additions & 51 deletions src/ledger-chrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,69 +58,41 @@ 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;
--card: #171719;
--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) --- */
Expand Down Expand Up @@ -484,10 +456,7 @@ function nav(activePath) {
</div>
<div style="margin-left:auto;display:flex;align-items:center;gap:14px;">
<a class="ml-nav-gh" href="https://github.com/MikeyPetrillo/Agent402" rel="noopener" style="font-family:var(--font-mono);font-size:13px;color:var(--muted);text-decoration:none;">github</a>
<button type="button" onclick="a402ToggleTheme()" class="ml-theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">
<svg class="ml-moon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<svg class="ml-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>

${activePath === "" ? "" : `<a class="ml-nav-cta" href="/docs#add" style="background:var(--surface);color:var(--on-dark);font-family:var(--font-mono);font-weight:700;font-size:13px;text-decoration:none;padding:9px 15px;">ADD TO CLAUDE →</a>`}
<button type="button" onclick="a402ToggleMenu()" class="ml-burger" aria-label="Open menu" aria-expanded="false">
<svg class="ml-burger-open" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
Expand Down Expand Up @@ -657,8 +626,6 @@ export function ledgerShell({ title, description, canonical, baseUrl, activePath
<html lang="en">
<head>
<meta charset="utf-8">
<script>(function(){try{var t=localStorage.getItem('a402-theme')||(window.matchMedia&&window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');if(t==='dark')document.documentElement.setAttribute('data-theme','dark');}catch(e){}})();
function a402ToggleTheme(){try{var r=document.documentElement,d=r.getAttribute('data-theme')==='dark';if(d){r.removeAttribute('data-theme');localStorage.setItem('a402-theme','light');}else{r.setAttribute('data-theme','dark');localStorage.setItem('a402-theme','dark');}}catch(e){}}
function a402ToggleMenu(){try{var o=document.documentElement.classList.toggle('ml-menu-open');var b=document.querySelector('.ml-burger');if(b)b.setAttribute('aria-expanded',o?'true':'false');}catch(e){}}</script>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(title)}</title>
Expand Down
3 changes: 1 addition & 2 deletions src/revenue-live.js
Original file line number Diff line number Diff line change
Expand Up @@ -1289,8 +1289,7 @@ function mppSection(mpp) {
export function revenueChartSection() {
return `
<style>
.rvz{--s1:#2a78d6;--s2:#eb6834;--s3:#1baf7a;--s4:#eda100;--s5:#e87ba4;--s6:#008300;--s7:#4a3aa7;--s8:#e34948;--sfree:#8a8f98;--snewbuyers:#1baf7a;--sretbuyers:#4a3aa7;--scumbuyers:#2a78d6;--vsurf:var(--card)}
:root[data-theme="dark"] .rvz{--s1:#3987e5;--s2:#d95926;--s3:#199e70;--s4:#c98500;--s5:#d55181;--s6:#008300;--s7:#9085e9;--s8:#e66767;--sfree:#9aa0aa;--snewbuyers:#199e70;--sretbuyers:#9085e9;--scumbuyers:#3987e5;--vsurf:var(--card)}
.rvz{--s1:#3987e5;--s2:#d95926;--s3:#199e70;--s4:#c98500;--s5:#d55181;--s6:#008300;--s7:#9085e9;--s8:#e66767;--sfree:#9aa0aa;--snewbuyers:#199e70;--sretbuyers:#9085e9;--scumbuyers:#3987e5;--vsurf:var(--card)}
.rvz{border:1.5px solid var(--ink);background:var(--card);padding:18px 20px;margin:0 0 26px}
.rvz-controls{display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-bottom:14px}
.rvz-seg{display:inline-flex;border:1.5px solid var(--ink)}
Expand Down
Loading