Light and system themes - #41
Merged
Merged
Conversation
Both strips are direct children of the space route's `flex min-h-0 flex-1 flex-col overflow-y-auto` scroller with no `shrink-0`, so they were flex-shrunk vertically: 16px and 17px containers around 32px chips, whose natural need is 48px. The rows then overlapped by 15px — the space pills rendered sliced in half under the tab row, and a short label like `code` was reduced to its two rounded end-caps. Predates the theme work (reproduced against the shipped 0.17.0), and it shows in both themes. Split out from that branch so it can be read on its own.
Collie has been hardcoded to dark since 0.1.0 (`class="dark"` on <html>). Unpin it and let the cascade decide. Cascade. Every themed value is one `light-dark()` declaration keyed on `color-scheme`, so System needs no JavaScript at all and there is no second dark block to drift. Verified first that `color-mix()` resolves a `light-dark()` argument, which the app-wide `border-border/60` and `outline-ring/50` base rules depend on. First paint. public/theme-init.js applies an explicit pin before paint. A same-origin file, so `script-src 'self'` already allows it — no CSP change. It stores a BARE string; useTheme deliberately diverges from use-display-prefs' JSON encoding to match, because a strict compare against a JSON-quoted value would fail silently and take the anti-flash with it. useTheme owns what CSS cannot: the pin class (bidirectionally — a stale class is why Dark to System would otherwise do nothing until a reload), the theme-color metas, and the OS listener. Module-scoped so the three controls agree and the listener outlives the idle lock unmounting the router. The mirror renders in dark space and light inverts it (ADR 0002 follows). Agents emit truecolor almost exclusively — 446 sequences in a live pane, zero basic ANSI — and truecolor names an absolute colour no palette can re-theme. ansi.ts still emits var(--ansi-N) for indexed colour, via both spellings: color256() resolves 0-15 through the same table, so theming one and not the other renders the same logical colour two ways. Light surfaces are a ladder (page rgb 235 / card 255) rather than stock shadcn's all-white, which rests the whole dashboard hierarchy on one 1.26:1 hairline. It also lands exactly on the mirror's inverted background, so the seam at the pre's edge disappears. Contrast work, all measured in a browser rather than modelled: light --status-* retuned against the worst ground they land on (a translucent chip over the pane header, not white — that took three attempts); --muted- foreground darkened and every /70, /80 and opacity-60 modifier on small text dropped, since no token value rescues a /70; focus rings to full strength (they were 1.77:1); the off-switch given an outline (it was a white thumb on a 1.09:1 track); header controls to 44px boxes without negative margins, which overlap their neighbours and overflow the page. Version bump rides with the release commit, so every commit on this branch passes scripts/check-version.sh.
The decision closes off an option someone will absolutely re-propose ("why
not just give the mirror a light ANSI palette?"), and the answer is a
measurement rather than a preference: four live panes emit 446/150/461/109
truecolor sequences and zero basic ANSI codes.
Records the three rules that follow — dark-space authoring, literals not
tokens, filter scoped to the pre — and the sharp edge that cost a blocker
during review: cancelling the filter is only safe on an element that fully
specifies its own foreground, or inherited text double-inverts to invisible.
Carries the short normative rule in CLAUDE.md linking here. The ADR states
its own measurements and method rather than citing a design trail, so it
stands alone.
konpyl
force-pushed
the
feat/light-and-system-theme
branch
from
July 29, 2026 03:30
26736f1 to
0c589bc
Compare
AltanS
added a commit
that referenced
this pull request
Jul 29, 2026
Light and system themes (supersedes #41)
Owner
|
Superseded by #46, which is now merged. Your four commits are carried verbatim and are in Three things changed on top:
Closing in favour of #46. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Collie has been hardcoded to dark since 0.1.0 —
class="dark"on<html>, with a full light tokenset sitting unused in
index.cssthe whole time. This unpins it, adds a System mode that follows theOS, and puts a control in two places.
Four commits, each readable on its own:
7a7a13e513d368bb1f08726736f1The interesting decision
The design originally gave the mirror a light ANSI palette — define
--ansi-0…15twice, emitvar(--ansi-N)fromlib/ansi.ts, let CSS swap them. Built, tested, and then measured against realpanes:
Zero basic and zero bright ANSI codes. Claude Code emits truecolor almost exclusively, and
truecolor names an absolute sRGB value — no palette can redirect it. On a white ground, eight of
thirteen distinct colours in a real pane fell below 3:1, including a
●at 1.0:1 and Monokai'sforeground at 1.07:1.
So the mirror now renders in dark space under every theme and light inverts it with
invert(1) hue-rotate(180deg), which preserves hue well enough to keep syntax highlighting legible.Sampling rendered pixels, light tracks dark almost exactly — median 6.73 vs 7.46. The reasoning, the
three rules it imposes, and what would justify revisiting it are in
ADR 0002.
lib/ansi.tsstill emitsvar(--ansi-N)for indexed colour, and does so via both spellings —color256()resolves 0–15 through the same table, so theming31mbut not38;5;1would render thesame logical colour two different ways in one pane.
Notes for review
script-src 'self'alreadypermits. System users get a correct first paint from CSS alone, with no JS at all.
color-mix()resolves alight-dark()argument,since the app-wide
border-border/60andoutline-ring/50base rules depend on it.oklch()/light-dark(), composited against the real ancestor stack. Three rounds ofbrowser-driven UX review across 390/768/1280 × both themes found 13 further defects (an unreadable
off-switch, half-strength focus rings, every alpha-modified muted text under 3:1); two of those
were regressions introduced by the previous round's fixes.
Legibility survives; only visual weight inverts. Documented in the ADR — greying it is worse.
apple-mobile-web-app-status-bar-style: black-translucentrenderswhite glyphs, which will vanish on a light page in an installed PWA. There's no
mediaform and noruntime API, and switching to
defaultchanges the layout contract withviewport-fit=cover. Needsa real device.
26736f1is droppable. If you'd rather cut releases yourself, drop the release commit and theother three still apply cleanly.
bun run buildclean; 1064 web tests and 318 root tests pass;scripts/check-version.sh✓ at everycommit on the branch, not just the tip.
🤖 Generated with Claude Code