Skip to content

[diffshub] Add a Shiki theme switcher#745

Open
necolas wants to merge 7 commits into
mainfrom
nicolas/diffshub-themes
Open

[diffshub] Add a Shiki theme switcher#745
necolas wants to merge 7 commits into
mainfrom
nicolas/diffshub-themes

Conversation

@necolas
Copy link
Copy Markdown
Contributor

@necolas necolas commented May 26, 2026

Clean history version of #731

Summary

Adds a theme switcher to the diffshub code view so reviewers can render diffs
and the surrounding chrome in any Shiki theme, independent of the app's
global light/dark palette.

  • Theme picker in the header (next to the gear): a light-theme list, a
    dark-theme list, and an Auto/Light/Dark color-mode toggle, all in one
    in-place dropdown that fits narrow viewports. Picks persist across reloads.
  • Themed chrome: the sidebar, file tree, header, comment cards, and inline
    annotations all source their colors from the resolved Shiki theme. Foreground
    and muted-foreground are chosen by contrast against the resolved surface
    (not the theme's declared values) so text stays legible on mixed-palette
    themes (e.g. slack-ochin, ayu-dark).
  • System Monitor theme-cycle button sweeps through every theme for quick
    previewing.
  • Diff and chrome repaints are kept in the same frame on a theme swap, gated on
    theme hydration so client-side navigation doesn't tokenize against the
    default palette.
  • [trees] themeToTreeStyles now rejects a theme's list.hoverBackground
    when its luminance would erase the hovered row's text, and derives the rgba
    hover fallback from the sidebar's actual luminance rather than the declared
    theme type.

Commits

Organized into 7 focused commits (foundations first, then UI):

  1. [trees] Reject list.hoverBackground when it would erase row text
  2. [diffshub] Add a localStorage-backed persisted state hook
  3. [diffshub] Add the Shiki theme catalog
  4. [diffshub] Resolve Shiki themes into themed chrome tokens
  5. [diffshub] Add a theme switcher to the code view header
  6. [diffshub] Apply the resolved theme across the chrome
  7. [diffshub] Add a theme-cycle button to the System Monitor

Test plan

  • Pick light/dark themes from the header; confirm diff + chrome update together
  • Reload and confirm picks persist; navigate away and back, confirm no
    wrong-palette flash
  • Try mixed-palette themes (slack-ochin, ayu-dark) and confirm sidebar
    labels / comment labels stay legible
  • Run the System Monitor theme-cycle button, scroll quickly up and down to ensure theme is correctly applied to collapsed and open files

necolas added 7 commits May 26, 2026 22:13
Some themes ship a list.hoverBackground designed for their editor
surface, whose palette can be the inverse of the sidebar's. Slack-ochin
is the worst case I've seen: editor.background is white, but
sideBar.background is dark navy with light gray text, and
list.hoverBackground is #d5e1ea — a near-white wash. Applied to the
sidebar that overlay lands on the same luminance band as
sideBar.foreground and the hovered row's text disappears.

Add a luminance-based heuristic to themeToTreeStyles: parse the hover
bg, sidebar bg, and sidebar fg as hex (the dominant format in
@shikijs/themes), compute relative luminance, and drop the hover bg
when it sits closer in luminance to the fg than to the bg. Non-hex
formats fall through unchanged so themes that use modern color syntax
keep their intended hover.

Also pick the rgba fallback's tint from the sidebar's actual
luminance rather than `theme.type`. Otherwise slack-ochin (declared
`light` for the editor) would land on `rgba(0,0,0,0.06)` over a dark
navy surface — invisible. Reading the bg directly picks
`rgba(255,255,255,0.08)` for the dark sidebar so users still get
hover feedback.

Cover the new behavior with focused unit tests for the slack-ochin
shape, a typical light theme, the equal-color short-circuit, and a
non-hex hover bg.
Add usePersistedState, which mirrors a string-valued useState to
localStorage and rehydrates from it on mount (in an effect, not the
useState initializer, so the SSR markup stays identical to the first
client render and hydration doesn't mismatch). Rehydration only accepts
values present in a `validValues` allowlist, so a stale or hand-edited
key can't push a consumer into an unknown value. A hydrated-ref guard
skips the first write so the initial commit's default can't clobber the
stored value before the read effect runs. The hook also returns a
`hydrated` flag so callers can wait for the real value before pushing it
to long-lived singletons.
List the light/dark Shiki theme names available to the diffshub theme
pickers, along with the color-mode type and default light/dark picks.
Mirrors the catalog shown in the diffs docs "Adapts to any Shiki theme"
example so users can browse the same set from the diffshub UI.
Add useResolvedTreeThemeStyles / useThemeChromeStyle, which resolve the
selected light/dark Shiki theme (cached after first use) and derive a set
of CSS custom properties for the diffshub chrome: sidebar/header surface,
popover and control colors, comment-card and inline-annotation tokens.

Foreground and muted-foreground are picked by contrast against the
resolved surface rather than trusting the theme's declared values, so
chrome text stays legible on mixed-palette themes. The muted threshold
(4.5:1) is split from the primary one because muted sidebar labels render
at body-text size; when a theme's descriptionForeground fails the bar,
deriveMutedFg blends primaryFg into the background until it clears 4.5:1.
Add a theme picker next to the gear icon in the diffshub header: a
light-theme list, a dark-theme list, and an Auto/Light/Dark color-mode
toggle, all in a single dropdown that switches between views in place so
it fits narrow viewports. The list auto-scrolls to the active row and
uses the shared mini scrollbar.

ReviewUI owns the light/dark/color-mode state (light and dark picks are
persisted) and drives the CodeView's `themeType` from the shared
ThemeProvider color mode so Auto/Light/Dark flips both the diff and the
app's light/dark class.
Source the sidebar, file tree, header dropdowns, comment cards, and
inline annotations from the resolved Shiki chrome tokens so the whole UI
tracks the selected theme instead of the global light/dark palette.
Addition/deletion comment labels and the tab count badge are tinted from
the active surface so they stay legible on mixed-palette themes; the file
tree now follows the picked theme rather than the fixed pierre-soft pair.

Keep diff and chrome repaints in the same frame on a theme swap: push
theme changes to the WorkerPool from a useLayoutEffect (gated on theme
hydration so client-side navigation doesn't tokenize the first batch
against the default palette), and drop the color transitions on comment
cards and inline links that otherwise interpolated through the old
palette while the rest of the UI snapped instantly.
Add a button to the System Monitor panel that sweeps through every Shiki
theme so reviewers can preview the full set without picking each one by
hand. useThemeCycle captures the user's current pick when cycling starts
so the visible theme anchors the rotation, and drives the same light/dark
and color-mode setters the header picker uses.
@necolas necolas requested review from SlexAxton, amadeus and mdo May 26, 2026 20:42
@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pierre-docs-diffshub Ready Ready Preview May 26, 2026 8:42pm
pierre-docs-trees Ready Ready Preview May 26, 2026 8:42pm
pierrejs-diff-demo Ready Ready Preview May 26, 2026 8:42pm
pierrejs-docs Ready Ready Preview May 26, 2026 8:42pm

Request Review

@mdo
Copy link
Copy Markdown
Contributor

mdo commented May 26, 2026

CleanShot 2026-05-26 at 14 49 28@2x

Looks like maybe our menus/overlays are a little janky with borders/shadows now?

CleanShot 2026-05-26 at 14 50 15@2x

Some border color issue on collapsed files?

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.

2 participants