feat(chrome): light and system appearance for the editor chrome (light-dark() variant) - #204
feat(chrome): light and system appearance for the editor chrome (light-dark() variant)#204pvinis wants to merge 12 commits into
Conversation
readState rebuilt state from `sessions` alone, so anything else at the top level of state.json was silently lost on the next session write. spread the parsed state instead, and add get/setThemePreference on top of it for a device-wide theme preference under config.theme. without the readState fix that preference would reset itself the moment any session was touched.
device-wide setting stored as config.theme in state.json, so every browser on the machine agrees instead of each one keeping its own localStorage toggle. nothing reads the value yet, that comes next. this is just the command surface, validation, and docs.
the chrome already had a semantic token layer over the raw ramp, but a few surfaces still had literal colors baked into the rule: the three hovers, the three overlay scrims, the layout banner, the danger tint, and the shadow color. no visual change, every new token resolves to exactly the value it replaced. this just puts them in one place so a light theme has something to reassign. .frame stays #fff on purpose. it is the artifact viewport, not chrome, so it must not follow the chrome theme.
createChromeHtml now emits <html data-theme-pref="system|light|dark">, read from the device config on each session request. chrome.css is a render-blocking <link> in <head>, so keying the theme off an attribute means it resolves before first paint. no inline boot script, no flash of the wrong theme, no js. nothing styles off it yet, that's the next commit.
adds a warm paper counterpart to the semantic token layer. each themed token carries both palettes inline through light-dark(), so the preference only has to pick a color-scheme and the browser resolves per token. `system` leaves color-scheme as `light dark` so it follows the OS live, `light` and `dark` pin it. no js, no duplicated palette block, and the stylesheet is render-blocking so nothing flashes. light foregrounds are tuned so every pair clears WCAG AA on its own surface, and --accent/--accent-hover go dark brass since --accent-hover is used as link text on paper as well as a button fill. needs light-dark(), baseline may 2024. verified in chrome across the matrix, including the nested --shadow-color -> --shadow-tooltip -> box-shadow chain. renders pixel-identical to the attribute-selector variant in both themes.
…, theme whiteboard
whiteboardTheme now reads document.documentElement.dataset.themePref, but the fake document in the chrome harness never modelled documentElement, so every whiteboard message path threw and 15 tests failed. a real document always has one, so the stub was the gap.
|
Speaking as Kun's firstmate: Holding this. Sibling of held #203: same chrome-only #202 is labeled ready-for-pr; that is not a merge vote. Landing waits on rebase of the existing PRs (#203 / #204). hold-ci: required |
|
Speaking as Kun's firstmate: Holding this. It is the light-dark() sibling of held #203: same The branch is CONFLICTING with main (12 ahead / 34 behind). I am not rebasing a PR that needs that call, and I am not flagging it while DIRTY. ready-for-pr on #202 is not a merge vote. |
closes #202
this is one of two PRs for the same feature. only one should land. #203 is the alternative. say which you want and i'll close the other same day.
pick one: the difference is one file
both PRs carry the same four base commits and the same review gate fixes, on top of one differing implementation commit. what actually differs between the two branches is
src/chrome.css, its test, and a one line invariant note. they render pixel identical in both themes, so this is purely an implementation choice.:root[data-theme-pref="light"]plus aprefers-color-schememedia querylight-dark()chrome.cssvs mainlight-dark(), baseline may 2024short version: this one is smaller and has nothing to keep in sync, at the cost of needing 2024 css. #203 is the conservative pick if that floor bothers you.
for what it's worth on the floor: there's no
browserslistor@supportsanywhere in the repo,enginesonly pins node, and lavish opens a tab in the developer's own current browser. #201 is an eventsource failure on firefox 151, not a css feature problem.what it does
lavish-axi config theme <system|light|dark>, device wide, drives the editor chrome. defaults tosystem.following your notes on #131
configcommand instate.json, not per browser localStorage. done.also dropped from #131:
src/appearance.js. it shelled out to osascript, the windows registry and gsettings to resolvesystemat cli time, and that only existed to serve the agent guidance half. the chrome runs in a browser, so the browser can do it.the chrome theme still resolves in pure css. no injected stylesheet, no boot script, no
matchMedialistener.what i can't claim any more is "zero new javascript" overall, which is what i said when i first opened this. the review gate found two real bugs in the shared commits and fixing them added js. details below. none of it changes how the theme resolves, and all of it is in the shared commits, so #203 carries exactly the same code.
the default
systemmeans a light mode machine gets different looking chrome after upgrading. deliberate, but it is a visible change, so shout if you'd rather it default todarkand stay opt in. one line.how
every themed token carries both palettes, so the preference only has to pick a
color-schemeand the browser resolves per token:systemleaving both in play means it follows the os live, nomatchMedialistener.chrome.cssis a render blocking<link>in<head>so nothing flashes either.no palette block is written twice, so there's nothing to keep in sync.
what the review gate changed
pushed through
no-mistakesper CONTRIBUTING.md. it found two genuine bugs in the shared commits that i'd missed, both worth having:state.jsonhad a second writer.configCommanddid a read modify write straight from the cli process, which breaks the invariantAGENTS.mdleans on ("writes are serialized through the single server process"). concurrent with a server sidequeuePrompts, last writer wins and the other change is silently gone. fixed at the shared boundary:writeStateis now a temp file plus rename, andconfig themeposts to a newPOST /api/configwhen a server is live. it writes directly only when nothing answers/health, and refuses outright rather than writing beside a stale server.whiteboardTheme()read onlyprefers-color-scheme, soconfig theme lighton a dark os gave you a light chrome around a dark excalidraw canvas. it now reads the resolved preference first.that's the new javascript. i also fixed a test harness gap the gate's own fix exposed: the fake document in
test/chrome-client-queue.test.jsnever modelleddocumentElement, so the whiteboard change failed 15 tests there until it did.pnpm run checkis green: 625 tests, 623 pass, 2 skipped (they need chrome).chrome vs artifact
worth being explicit since it surprised me too. the artifact iframe is sandboxed without
allow-same-origin, so it can't see the chrome preference, only the os. so:prefers-color-schemedark mode: follows the oswith the default
systemthat means chrome and artifact now agree, which they never did before: today the chrome is pinned dark, so every light mode user already gets a light artifact inside dark chrome. the only way to get a mismatch after this is to pin the chrome against your own os, which is an explicit choice.not trying to solve artifact theming here, that's #180.
known limitations
flagged by the review gate, left deliberately unfixed. shout if you disagree with any of these and i'll do them:
light-dark()fallback. on a browser without support (chrome/edge <123, safari <17.5, firefox <120) the tokens are invalid and the chrome renders unstyled rather than degrading to the old dark palette. that is the tradeoff this variant exists to offer, and adding fallbacks would mean writing the palette twice, which is just feat(chrome): light and system appearance for the editor chrome #203. calling it out so it's a choice and not a surprise.src/artifact-sdk.js), so a light chrome pops a dark card. that's the artifact side, so feat(design): make the default artifact theme configurable via LAVISH_AXI_DEFAULT_THEME #180.color-schememoved to:root, and the artifact iframe inherits the computed value. artifact markup and css are genuinely untouched, but ua styled surfaces in there (scrollbars, unstyled form controls) do follow the chrome preference. one path i didn't expect, documented rather than papered over.matchMedialistener, which is more js than this is worth.lavish-designskill under.agents/still describes the chrome as dark only, so prototyping from it produces dark only markup. it's machine generated, so regenerating it against the new palette is a separate job.one prerequisite bug
readStaterebuilt state fromsessionsalone, so every other top level key was dropped on the next session write.config.themewould have reset itself the moment you opened anything. fixed in the first commit with a regression test. stands on its own regardless of what you do with the rest.also in here
.framestays#fffon purpose, it's the artifact viewport, not chrome.--accentgoes dark brass because--accent-hoveris used as link text on paper, not just as a button fill.verified
matrix in a real browser via devtools color scheme emulation:
systemlightdarksystemfollows the os with no reload. dark resolves to exactly the values it had before, so nothing changes for anyone who stays dark.also checked the nested case, since
light-dark()inside a custom property consumed throughvar()is the bit worth doubting:--shadow-color->--shadow-tooltip->box-shadowresolves correctly per theme.systemon a light ossystemon a dark oslight chrome, menu open
the mismatch case: chrome pinned
darkon a light osthe artifact follows the os, the chrome follows your pin. documented above.
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
src/chrome.css:41- chrome.css moves every themed token to light-dark() with no fallback, so on a browser without light-dark() support (Chrome/Edge <123, Safari <17.5, Firefox <120) the chrome loses its colors entirely rather than degrading to the previous dark palette. Custom properties store the token stream fine, but substituting it (body { background: var(--bg); color: var(--fg) }at src/chrome.css:155-156) is invalid at computed-value time, so background falls back to transparent and color to the inherited canvas default. Withcolor-scheme: light darkstill honored, a dark-OS user on such a browser gets near-black canvas plus black text — unreadable, where the base commit rendered correctly. A@supports not (color: light-dark(#000, #fff))block restoring the ink/steel ramp on the semantic tokens would keep the old dark chrome as the floor. Flagging because PR feat(chrome): light and system appearance for the editor chrome (light-dark() variant) #204 is explicitly the light-dark() variant of feat(chrome): light and system appearance for the editor chrome #203, so the compat floor is part of the choice being made.src/cli.js:257-config theme <value>is the only CLI command that hard-fails against a version-mismatched server instead of self-healing:openCommand/pollCommandgo throughensureServer, which asks the stale server to shut down (and SIGTERMs pre-handshake ones) before continuing, while this path throws and tells the user to runlavish-axi stopmanually. The refusal itself is correct — writing beside a stale server would be dropped on its next session write — but reusing the existing shutdown path (shutdownServerOnPort, then the direct write, still without spawning a server) would keep the same invariant with no manual step. Deliberate per commit ff0d757, so raising rather than changing.✅ **Test** - passed
✅ No issues found.
pnpm install --frozen-lockfile(deps and dist bundle were absent in the worktree)node --test test/session-store.test.js test/chrome-client-queue.test.js— 70 pass, covers theme round-trip, config surviving session writes, unrecognized top-level key preservation, atomic writeStatenode --test test/server.test.js— 164 pass, coversPOST /api/config,data-theme-prefon the chrome html, color-scheme mapping, both-palette token audit, whiteboardTheme precedencenode --test test/cli-output.test.js— 96 pass, covers config read/write/validation, server-routed write, stale and pre-handshake server refusalsManual CLI:lavish-axi config,config theme light|dark|system,config theme,config theme sepia,config colouragainst an isolatedLAVISH_AXI_STATE_DIRwith no server runningManual CLI:config theme lightagainst a live current-version server on port 4611, thencurl -s .../session/<key>to confirm the served chrome html carriesdata-theme-prefManual CLI:config theme lightagainst a fake/healthserver reporting version0.1.11— confirmedSERVER_ERROR, non-zero exit, and unchanged stored preferenceHeadless Chrome Canary via CDP (Emulation.setEmulatedMediaonprefers-color-scheme,Page.captureScreenshot): pinned dark under light OS, pinned light under dark OS,systemunder light OS, andsystemflipped to dark OS with no reloadHeadless Chrome CDP interaction: opened the overflow menu, publish dialog, and ended overlay in light mode and read computed--hover,--scrim,--scrim-strongvaluesHeadless Chrome CDP on a Mermaid artifact: inline Excalidraw whiteboard captured with theme pinned light under a dark OS and pinned dark under a light OSInspectedstate.jsonafter two live sessions plus a theme write to confirmconfigandsessionsboth persist✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.