Skip to content

Add light mode following the OS color scheme, with --theme to force#56

Merged
david-crespo merged 8 commits into
oxidecomputer:mainfrom
guygrigsby:light-mode
Jul 8, 2026
Merged

Add light mode following the OS color scheme, with --theme to force#56
david-crespo merged 8 commits into
oxidecomputer:mainfrom
guygrigsby:light-mode

Conversation

@guygrigsby

@guygrigsby guygrigsby commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Made this for me (light diffs on a dark desktop) and it might help others. Closes #55.

  • Every color token in styles.css becomes a light-dark() OKLCH pair resolved by color-scheme on :root. The dark side is byte-identical to the current palette.
  • --theme light|dark|system rides on the URL the CLI opens (?theme=), read before first paint, so there's no wrong-scheme flash and a forced theme still applies when the diff fetch fails. A hand-typed bare URL falls back to the OS scheme.
  • Forced themes pass a single theme to CodeView so Shiki tokenizes once; system mode uses the dual github light/dark pair.
  • cssTarget raised to the first light-dark() versions. Otherwise esbuild lowers the tokens to a prefers-color-scheme polyfill that ignores the forced scheme; dev serves raw CSS so it only shows in the built package. Not a new floor in practice, @pierre/diffs already emits light-dark() at runtime.
  • Reordered the error guard above the loading guard in DiffView. A failed fetch settles with data undefined, so the error screen was unreachable and it showed Loading... forever. Pre-existing; found it testing the theme on the error path.

Verified against the built package on both OS schemes with all three --theme values. npm run ci green.

The theme is known at server start but was riding on /api/diff, so a
forced scheme applied only after the slowest request resolved (OS-scheme
flash for the whole diff run) and never applied at all when the fetch
failed. Carry it as ?theme= on the URL the CLI opens and read it at
module load, before first paint. Deletes the server and DiffResponse
plumbing; THEME_MODES in shared/types.ts now drives the CLI choices and
the client's param validation from one list.

A forced theme now also passes a single CodeView theme instead of the
dual object: Shiki tokenizes once instead of once per theme (the dual
path runs a full grammar pass per theme and ships both themes' token
styles even though a pinned color-scheme can only ever render one).

Also reorder the error guard above the loading guard: a failed fetch
settles with data undefined, so the loading-first check showed
'Loading...' forever and the error branch was unreachable.
Both were left fixed at their dark values in the light-dark()
conversion. In light mode the primary button hover lightened under
white text (contrast dropping to about 3.6:1, opposite hover direction
from --blue-bright) and the stale viewed label rendered about 3.6:1 on
the control surface. Darken both light arms to 50% L, in line with the
other accent text variants.
Vite's default cssTarget predates light-dark(), so esbuild lowered every
token pair to a prefers-color-scheme polyfill that follows the OS and
ignores the color-scheme --theme pins. Dev serves the raw CSS, which is
why the bug only showed in the built package: forced light rendered the
diff bodies light (the library generates its CSS at runtime) but left
the page chrome dark. Raise cssTarget to the first versions with
light-dark() support; not a new floor in practice, since the diff
library already emits light-dark() at runtime.
@david-crespo

Copy link
Copy Markdown
Collaborator

Looks good, thanks. Will take a closer look tomorrow.

@david-crespo

david-crespo commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Ok, I changed nearly everything 😁 but I think it's good. I still want to tweak the colors slightly.

Basically by storing the theme server-side like the diff viewed state, we can eliminate the need for the CLI flag and the query param. This means the theme is set system-wide, but that is probably a good thing rather than a downside. I also added a theme button in the UI and a keyboard shortcut t to cycle themes:

image

If you have two skepsis windows open and change theme in one, the other one should refetch theme on focus thanks to React Query and update its theme as well.

🤖 summary of changes

Replaces the --theme flag and URL param from the earlier commits with a toggle in the header bar (also bound to t) cycling system → light → dark. The preference persists in ~/.local/share/skepsis/settings.json — server-side because the ephemeral port means a fresh origin, and therefore fresh localStorage, on every run — and other running instances pick up changes on window focus via react-query's default refetch.

A render-blocking /api/theme.js script in index.html sets data-theme on <html> before first paint, so a forced theme never flashes; the CSS pins color-scheme off that attribute. In dev the script reaches the API server through the existing /api proxy, so there's no separate dev path. CodeView now always gets the dual Shiki theme, making the toggle a pure CSS flip instead of a retokenize.

Also dropped the cssTarget override: it was only needed when forcing was an inline style, which the CSS minifier's light-dark() fallback couldn't see. The fallback rewrites the [data-theme] rules correctly (verified against the built output).

@david-crespo

Copy link
Copy Markdown
Collaborator

Ok, did the color pass too, on dark mode as well as light. Fixed some contrast issues that had been bothering me as well as the lack of side borders on the file boxes.

Screenshot 2026-07-08 at 2 30 49 PM Screenshot 2026-07-08 at 2 30 55 PM

@david-crespo
david-crespo merged commit 9c1783a into oxidecomputer:main Jul 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Light mode

2 participants