Skip to content

fix+feat: theme propagation into OpenCode embed + Solarized/Latte themes + light/dark/auto mode (#589, #585)#595

Open
brendandebeasi wants to merge 4 commits into
spacedriveapp:mainfrom
brendandebeasi:fix/589-opencode-shadow-theme
Open

fix+feat: theme propagation into OpenCode embed + Solarized/Latte themes + light/dark/auto mode (#589, #585)#595
brendandebeasi wants to merge 4 commits into
spacedriveapp:mainfrom
brendandebeasi:fix/589-opencode-shadow-theme

Conversation

@brendandebeasi
Copy link
Copy Markdown

@brendandebeasi brendandebeasi commented May 9, 2026

Closes #589 and #585. Two related theme fixes; combined here because the new themes added in #585 only render correctly inside the OpenCode embed once the propagation work from #589 is in place.


Part 1 — Theme propagation into OpenCode embed (#589)

OpenCode mounts inside a Shadow DOM in OpenCodeEmbed.tsx, so the document-level theme class and SpaceUI CSS custom properties never reached it. Workbench prompt-input + buttons stayed dark on every Spacebot theme — most visibly broken on vanilla (light), structurally wrong on every theme.

Three layers

interface/src/components/OpenCodeEmbed.tsx — forward CSS vars + colorScheme

  • Inject a <style> element into the shadow root that mirrors all SpaceUI --color-* tokens as :host { --color-app-box: …; … } so the embed's CSS resolves var() against the active theme.
  • Re-runs on every theme change without remounting (preserves OpenCode session state).
  • Pass colorScheme derived from the host theme (isLightlight else dark) at mount, and call the new handle.setColorScheme reactively when the host theme changes.

interface/opencode-embed-src/embed.tsx — reactive colorScheme

  • ThemeInjector switched from onMount to createEffect so it re-runs when props.colorScheme updates.
  • mountOpenCode creates a SolidJS signal for colorScheme; the returned handle exposes a new setColorScheme(scheme) method that updates that signal.
  • MountOpenCodeHandle extended with setColorScheme.

interface/opencode-embed-src/spacebot-theme.json — light-theme contrast

light.overrides was identical to dark.overrides — bright lime/lavender/light-blue values that fail WCAG AA on the near-white vanilla background. Tuned light overrides for ≥6:1 contrast against vanilla's --color-app-box (most ≥8:1):

Token Was Now Ratio
syntax-string #5cf0b0 #066b30 6.38
syntax-primitive #f06060 #a00808 7.94
syntax-type #f0c070 #5d3800 9.91
syntax-constant #70d0f0 #0a4d85 8.34
syntax-info #70d0f0 #0a4d85 8.34
syntax-property #c080f0 #5818b8 8.99
syntax-comment var(--color-ink-faint) #5a5a66 6.51
text-weaker var(--color-ink-faint) #5a5a66 6.51
markdown-code #5cf0b0 #a00808 7.94
text-interactive-{base,strong}, markdown-link, markdown-link-text var(--color-accent) #5818b8 8.99

Dark overrides unchanged.


Part 2 — New themes + light/dark/auto mode (#585)

New theme tokens

CSS files under interface/src/themes/, imported from styles.css:

  • solarized-light — Ethan Schoonover's classic light palette (Solarized Light, blue accent)
  • solarized-dark — its dark counterpart
  • catppuccin-latte — Catppuccin's official light variant pairing the existing mocha (Catppuccin Mocha)

Mappings translate Solarized's 16 named colors and Catppuccin's 26 named colors into SpaceUI's token surface (--color-app, --color-ink, --color-app-line, etc.). Local override files for now; can be moved upstream to @spacedrive/tokens in a follow-up.

Mode selector

useTheme.ts rewritten to expose:

  • mode: "light" | "dark" | "system" — single switch
  • lightTheme / darkTheme — independent per-mode choices
  • theme — computed effective id (consumers stay simple)
  • isLight — computed boolean (used by colorSchemeForTheme from Part 1, so any new light theme propagates into OpenCode automatically)

A prefers-color-scheme MediaQueryList listener recomputes the effective theme when the OS toggles dark mode.

Settings UI

AppearanceSection shows:

  • Mode selector at top (Light / Dark / Auto)
  • One theme picker in light or dark mode (filtered by isLight)
  • Two side-by-side pickers in auto mode (so users pick which light + which dark theme the system uses for each OS state)
  • "active" badge on whichever picker reflects what's currently rendering

Migration

Legacy localStorage["spacebot-theme"] migrates automatically on first load to the new tri-key model:

  • spacebot-theme-mode = "system"
  • spacebot-theme-light = <legacy if isLight, else default vanilla>
  • spacebot-theme-dark = <legacy if !isLight, else default default>

Migration runs at module import time (before React hydrates → no FOUC), is one-shot (legacy key removed only after writes succeed), idempotent across reloads, and SSR-safe. Logs a single console.info when it runs for debugging visibility.


Verification

End-to-end on a dev VM:

  • All ten themes render with correct surfaces + accents
  • Vanilla / Solarized Light / Latte show light backgrounds with readable syntax colors inside the OpenCode embed
  • Switching themes hot-swaps the embed's color scheme without losing OpenCode session state (no remount)
  • Static palette contrast check on vanilla: 23/24 light text tokens at WCAG AA, syntax tokens 6.4–9.9 ratio
  • prefers-color-scheme listener verified by toggling macOS Appearance: dashboard auto-swaps in auto mode
  • Migration log fires once on legacy localStorage; subsequent reloads see new keys and skip migration

Out of scope (future PRs)

  • Custom user-authored themes
  • Per-component theme overrides
  • Upstreaming Solarized + Latte tokens to @spacedrive/tokens
  • scripts/build-opencode-embed.sh auto-invocation from build.rs (currently a documented manual step before cargo build)

…iveapp#589)

OpenCode mounts inside a Shadow DOM so the document-level theme class
and SpaceUI CSS custom properties never reached it — the workbench
prompt-input + buttons stayed dark on every Spacebot theme, illegible
on vanilla. Three-layer fix:

1. interface/src/components/OpenCodeEmbed.tsx
   - Forward all SpaceUI `--color-*` tokens into a `<style>` element
     inside the shadow root as `:host { --color-app-box: …; … }` so
     the embed's CSS can resolve var() refs against the active theme.
   - Re-runs on every theme change without remounting (preserves
     OpenCode session state).
   - Pass colorScheme derived from the host theme (`vanilla` → light,
     others → dark) at mount, and call the new `handle.setColorScheme`
     reactively when the host theme changes.

2. interface/opencode-embed-src/embed.tsx
   - Convert ThemeInjector to read `props.colorScheme` reactively via
     createEffect (was onMount, fired once).
   - mountOpenCode creates a SolidJS signal for colorScheme so the
     handle's new `setColorScheme(scheme)` method can update the
     ThemeProvider without remounting the SolidJS app.
   - Extend MountOpenCodeHandle with setColorScheme.

3. interface/opencode-embed-src/spacebot-theme.json
   - Light scheme had identical syntax/text/link overrides to the dark
     scheme — bright lime/lavender/light-blue values that read fine on
     dark backgrounds but failed WCAG AA on near-white. Tuned light
     overrides for ≥6:1 contrast against vanilla's app-box (most ≥8:1):
       syntax-string    → #066b30
       syntax-primitive → #a00808
       syntax-type      → #5d3800
       syntax-constant  → #0a4d85
       syntax-info      → #0a4d85
       syntax-property  → #5818b8
       syntax-comment   → #5a5a66
       text-weaker      → #5a5a66
       markdown-code    → #a00808
       text-interactive-{base,strong},
       markdown-link, markdown-link-text → #5818b8
   - Dark scheme overrides unchanged.

Verified end-to-end on a dev VM:
- Vanilla theme renders the embed with light backgrounds + readable
  syntax colors. Switching to midnight/noir/slate hot-swaps the embed
  to dark instantly without losing the OpenCode session.
- Static contrast check: 23/24 light text tokens at WCAG AA, syntax
  tokens 6.4–9.9 ratio. Sole remaining outlier is markdown-image-text
  (alt-text fallback, decorative).

Closes spacedriveapp#589
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aa3d3d61-1217-4102-bcc5-8a18cdb411f7

📥 Commits

Reviewing files that changed from the base of the PR and between 2475f16 and 1723f83.

📒 Files selected for processing (2)
  • interface/src/components/settings/AppearanceSection.tsx
  • interface/src/hooks/useTheme.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • interface/src/components/settings/AppearanceSection.tsx
  • interface/src/hooks/useTheme.ts

Walkthrough

The PR enables runtime color scheme synchronization between the host dashboard theme and the embedded OpenCode app. The embedded Solid component exposes a reactive setter, the React host forwards Spacebot CSS variables into the embed Shadow DOM, and theme mode/persistence, Appearance UI, and new theme CSS assets are added.

Changes

Embed theme reactivity

Layer / File(s) Summary
Public API type contracts
interface/opencode-embed-src/embed.tsx, interface/src/components/OpenCodeEmbed.tsx
MountOpenCodeHandle type is extended with setColorScheme(scheme); the cached handle type in OpenCodeEmbed optionally includes setColorScheme.
Embed-side reactive signal and effect
interface/opencode-embed-src/embed.tsx
createEffect imported; mountOpenCode creates a reactive colorScheme signal from config; ThemeInjector applies changes reactively; render tree passes colorScheme(); returned handle wires setColorScheme.
Host-side theme token forwarding and mapping
interface/src/components/OpenCodeEmbed.tsx
New forwarding list and helpers read Spacebot CSS variables and inject them as :host declarations into the embed Shadow DOM (spacebot-theme-forward); useTheme() used to derive initial colorScheme mapping; a React effect re-forwards tokens and calls setColorScheme on theme changes.
Appearance UI
interface/src/components/settings/AppearanceSection.tsx
AppearanceSection now supports mode (light/dark/system), ModeSelector, and separate light/dark ThemePickerSections with preview colors from PREVIEW_COLORS.
Theme hook and persistence
interface/src/hooks/useTheme.ts
Hook rewritten to support mode, separate persisted lightTheme/darkTheme slots with migration, OS prefers-color-scheme handling, effective-theme computation, and pre-hydration initialization.
Styles / Assets
interface/src/styles.css, interface/src/themes/*.css
Import new local theme CSS files and add .catppuccin-latte-theme, .solarized-light-theme, and .solarized-dark-theme with full SpaceUI token definitions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: theme propagation into OpenCode embed plus new Solarized/Latte themes and light/dark/auto mode support, with issue references.
Description check ✅ Passed The description provides comprehensive coverage of both PR parts: theme propagation mechanics, new themes, mode system, settings UI, and migration strategy with verification details.
Linked Issues check ✅ Passed The PR fully addresses #589's requirements: forwards CSS variables into the Shadow DOM, implements reactive colorScheme updates via ThemeInjector's createEffect, and adjusts light-theme overrides for WCAG AA contrast compliance.
Out of Scope Changes check ✅ Passed All changes directly support the two linked objectives: Shadow DOM theme propagation, light/dark/system mode support, new theme tokens, and legacy storage migration. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 93.75% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…edriveapp#585)

Adds three new themes and replaces the single-theme picker with a mode
selector + per-mode theme picks that follow the OS prefers-color-scheme
setting in `auto` mode.

## New themes
- `solarized-light` — Ethan Schoonover's classic light palette
- `solarized-dark` — its dark counterpart
- `catppuccin-latte` — Catppuccin's official light variant, paired with the
  existing mocha (Catppuccin Mocha) dark theme

Token CSS lives in `interface/src/themes/` and imported from `styles.css`,
matching the per-theme class-selector convention used by `@spacedrive/tokens`.
A future PR can move these upstream once the mappings settle.

## Mode selector
`useTheme.ts` rewritten to expose:
- `mode: "light" | "dark" | "system"` — single switch
- `lightTheme` / `darkTheme` — independent per-mode choices
- `theme` — computed effective id (consumers stay simple)
- `isLight` — computed boolean for theme-aware downstream logic

A `prefers-color-scheme` MediaQueryList listener recomputes the effective
theme automatically when the OS toggles dark mode.

## Settings UI
`AppearanceSection` now shows:
- Mode selector at top (Light / Dark / Auto)
- One theme picker in `light` or `dark` mode (filtered to the matching
  `isLight`)
- Two theme pickers side-by-side in `auto` mode (so users pick which light
  + which dark theme the system uses for each OS state)
- An "active" badge on whichever picker reflects what's currently rendering

## Migration
The legacy single-key format `localStorage["spacebot-theme"]` migrates
automatically on first load to the new tri-key model:
- `spacebot-theme-mode = "system"`
- `spacebot-theme-light = <legacy if isLight>` (or default `vanilla`)
- `spacebot-theme-dark = <legacy if !isLight>` (or default `default`)

Migration runs at module import time (before React hydrates), is one-shot
(legacy key removed after writes succeed), and is idempotent. Logs a
single `console.info` for visibility. SSR-safe.

## Out of scope (separate PRs)
- Custom user-authored themes
- Per-component theme overrides
- Upstreaming Solarized + Latte to `@spacedrive/tokens`
@brendandebeasi brendandebeasi changed the title fix(workbench): propagate Spacebot theme into OpenCode embed (#589) fix+feat: theme propagation into OpenCode embed + Solarized/Latte themes + light/dark/auto mode (#589, #585) May 9, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@interface/src/components/settings/AppearanceSection.tsx`:
- Around line 86-115: The mode/theme picker uses buttons with role="radio" but
lacks full native radio semantics and keyboard behavior; update the UI in
AppearanceSection (where MODE_OPTIONS, mode, and setMode are used) to use actual
radio inputs (input type="radio") grouped in a fieldset/role="radiogroup" or
implement the full ARIA radio keyboard pattern (handle
ArrowLeft/ArrowRight/ArrowUp/ArrowDown and Space/Enter, manage tabindex for each
option, and expose aria-checked) so assistive tech reliably announces selection;
ensure each option binds to the same name, reflects mode state, calls
setMode(opt.id) on change, and keep the existing visual styling by visually
hiding the native input if needed while preserving focus and accessibility.

In `@interface/src/hooks/useTheme.ts`:
- Around line 128-150: During migration in useTheme.ts, preserve the old
single-theme behavior by assigning the legacy theme to both slots so the user
stays on their chosen surface regardless of OS mode; when legacyTheme is found
(from LEGACY_KEY via getThemeById), set lightTheme = legacyTheme.id and
darkTheme = legacyTheme.id (instead of only setting the slot matching
legacyTheme.isLight), then set mode = "system", persist MODE_KEY, LIGHT_KEY,
DARK_KEY, remove LEGACY_KEY and keep the existing console.info call (referencing
mode, lightTheme, darkTheme, DEFAULT_LIGHT, DEFAULT_DARK).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f6d9f02-a69d-44b9-9c22-3971e677687b

📥 Commits

Reviewing files that changed from the base of the PR and between 1dbcf94 and bd3807e.

📒 Files selected for processing (6)
  • interface/src/components/settings/AppearanceSection.tsx
  • interface/src/hooks/useTheme.ts
  • interface/src/styles.css
  • interface/src/themes/catppuccin-latte.css
  • interface/src/themes/solarized-dark.css
  • interface/src/themes/solarized-light.css
✅ Files skipped from review due to trivial changes (4)
  • interface/src/styles.css
  • interface/src/themes/solarized-dark.css
  • interface/src/themes/catppuccin-latte.css
  • interface/src/themes/solarized-light.css

Comment thread interface/src/components/settings/AppearanceSection.tsx Outdated
Comment thread interface/src/hooks/useTheme.ts Outdated
Brendan DeBeasi added 2 commits May 9, 2026 21:36
useTheme.ts migration regression
--------------------------------
Migrating legacy spacebot-theme to mode = "system" silently flips users to
the default theme of the opposite surface. A user who had picked nord (dark)
lands on vanilla whenever the OS is in light mode, because the untouched
light slot falls back to DEFAULT_LIGHT.

Pin mode to the surface the user explicitly chose ("light" or "dark") based
on legacyTheme.isLight. Auto/system mode is still available — they just have
to opt in.

AppearanceSection a11y — native radios
--------------------------------------
Mode + theme pickers used <button role="radio"> without the full ARIA radio
keyboard pattern (no arrow-key navigation, no aria-checked on theme cards).
Replace with native <input type="radio"> inside <fieldset>: browsers handle
arrow-key cycling, space/enter activation, and screen-reader announcements
("radio button, X of Y, selected") natively. Visual styling unchanged
(input is sr-only, label is the visible card; focus ring on focus-within).
CodeRabbit flagged docstring coverage at 43.75% on this PR's changed files
(threshold 80%). Added one-line docstrings on the public surface of the two
files I just touched: useTheme exports, internal helpers in useTheme, and
the four components in AppearanceSection.

Kept docstrings short and WHY-leaning where there's something non-obvious
(e.g. why getSystemPrefersDark defaults to dark on SSR; why ThemePreview
uses static colors instead of resolved CSS vars).
@b-client-vm
Copy link
Copy Markdown

⚠️ Docstring Coverage: 43.75% < 80% threshold

Pushed minimal one-liners on the public surface of useTheme and AppearanceSection in 1723f83 to nudge the number up. But it's worth flagging that the 80% threshold is at odds with how this codebase generally treats comments — the project convention (per CLAUDE.md) is "default to no comments; only add one when the WHY is non-obvious", which keeps ~~/// the getThemeById / loadScript / applyThemeClass style of trivial wrappers reading clearly without restating their signatures in prose.

Suggesting we either:

  1. Lower the threshold to ~50% in the CodeRabbit Org UI (closer to the natural rate of meaningful WHY-comments in this codebase), or
  2. Disable the Docstring Coverage pre-merge check entirely and rely on review judgement for when prose actually adds clarity.

Either is a one-click change in the CodeRabbit settings — there's no .coderabbit.yaml in the repo to update. Happy to chase higher coverage on this PR if you'd rather keep the gate; otherwise this comment is just to surface the tradeoff.

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.

[Bug] Dashboard theme does not penetrate the OpenCode embed (Shadow DOM); workbench renders dark on light themes

2 participants