Skip to content

feat: custom JSON themes - #257

Open
n00ki wants to merge 2 commits into
bholmesdev:mainfrom
n00ki:feat/themes
Open

feat: custom JSON themes#257
n00ki wants to merge 2 commits into
bholmesdev:mainfrom
n00ki:feat/themes

Conversation

@n00ki

@n00ki n00ki commented Aug 11, 2026

Copy link
Copy Markdown

Summary

  • Add a shared @hubble.md/theme package containing the theme contract, built-in themes, resolution logic, and JSON Schema.
  • Load custom .json themes from the global <Electron userData>/themes directory, watch for file changes, and report invalid themes without disrupting valid ones.
  • Add an Appearance section with visual Light, Dark, and System controls, a mode-aware theme picker, live previews, and quick access to the themes folder.
  • Apply themes throughout the desktop app, editor syntax and review colors, terminal, native window chrome, and HTML Apps.
  • Preserve separate light and dark selections, migrate the existing appearance preference, and fall back safely when a selected theme is missing or invalid.
  • Publish the canonical schema at https://hubble.md/schemas/theme.json and document the theme format.

Theme format

Themes are plain JSON files with an optional $schema declaration. The schema remains single-source in the theme package and is exposed through the website without duplicating or prematurely versioning the format.

Verification

  • pnpm check
  • pnpm build:desktop
  • pnpm test — 467 tests passed
  • pnpm check:react-compiler — zero failures
  • pnpm --filter web build
  • Real desktop smoke test covering live theme loading, custom-theme selection, and mode-aware filtering

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

@n00ki is attempting to deploy a commit to the bholmesdev's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overview

Adds @hubble.md/theme, Electron ThemeService (custom JSON themes + watch), Appearance settings UI, and wires tokens through desktop UI, terminal, editor, native chrome, and HTML apps. Shared CSS color defaults are removed in favor of runtime applyTheme / injected CSS.

Concerns

  • Shared theme.css no longer defines any color tokens. apps/www only paints via JS applyTheme after the module loads, so first paint (and any non-applyTheme consumer) loses --background / --foreground / etc.
  • Renderer theme tests were gutted to two happy paths; prior coverage for preference/OS edge behavior and no new coverage for rollback, revision gating, or preview restore.
  • HTML App CSS likewise drops static color fallbacks and relies entirely on injection + postMessage (works when the desktop pipeline is up; brittle if either path misses).

Verdict

Found: 0 critical, 3 important, 2 suggestions

Request changes


Reviewed by a Warp Factory agent.

Comment thread packages/ui/src/theme.css
@@ -1,30 +1,4 @@
:root {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] All color tokens were removed from shared :root. apps/www (and any other @hubble.md/ui/theme.css consumer) now has no CSS fallback for --background, --foreground, --primary, etc. until JS runs applyTheme.

apps/www/src/index.css still does color: var(--foreground); background-color: var(--background); at parse time, so first paint is unstyled/wrong. Keep light built-in tokens in CSS (or a tiny inline boot script) and let applyTheme override.

Comment thread apps/www/src/main.tsx
import App from "./App.tsx";
import "./index.css";

applyTheme(document.documentElement, HUBBLE_LIGHT_THEME);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] applyTheme(...) only runs after the module graph loads. With color tokens stripped from theme.css, www has no pre-JS theme. Pair this with CSS defaults (or an inline head script) so first paint is not blank/unstyled.

Comment on lines +35 to +47
it("applies the initial theme state from Electron", async () => {
const { initTheme, themeStateStore } = await import("./theme");
await initTheme();

emit(true);
expect(themeStateStore.get()).toEqual(state);
expect(document.documentElement.dataset.theme).toBe("builtin:hubble-dark");
expect(
document.documentElement.style.getPropertyValue("--background"),
).toBe("#171614");
expect(document.documentElement.classList.contains("dark")).toBe(true);

emit(false);
expect(document.documentElement.classList.contains("dark")).toBe(false);
});

it("notifies subscribers when the resolved appearance changes", () => {
const { emit } = mockMatchMedia(false);
initTheme("system");
const listener = vi.fn();
const unsubscribe = subscribeTheme(listener);
it("updates immediately and persists the full selection", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] Theme tests shrank to two happy paths (initial apply + setSettings success). Deleted coverage for system/explicit preference behavior, and nothing exercises: setThemeSettings IPC failure rollback, stale revision drops in acceptThemeState, or previewTheme / restoreThemePreview.

Add tests for those distinct paths before merge.

Comment on lines 1 to 2
:root {
color-scheme: light dark;
--brand: oklch(0.426 0.08 159.6);
--brand-accent: oklch(0.847 0.168 84.7);
--brand-accent-foreground: oklch(0.327 0.051 164.9);
--background: oklch(0.995 0.001 95);
--foreground: oklch(0.27 0.008 150);
--card: oklch(0.998 0.001 95);
--card-foreground: var(--foreground);
--primary: var(--brand);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.968 0.003 95);
--secondary-foreground: var(--foreground);
--muted: oklch(0.96 0.003 95);
--muted-foreground: oklch(0.55 0.01 95);
--accent: oklch(0.965 0.05 90);
--accent-foreground: var(--foreground);
--selected: oklch(0.9 0.12 90);
--selected-foreground: var(--foreground);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.9 0.004 95);
--input: oklch(0.91 0.004 95);
--ring: var(--brand-accent);
--radius: 0.625rem;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 [SUGGESTION] Static color tokens and the dark prefers-color-scheme block are gone; HTML apps now depend only on main-process themeCss(...) injection + hubble:theme postMessage. Fine when both fire, but a failed/late update leaves apps with no colors.

Keep a minimal built-in light (and optional dark) fallback in this sheet so apps still render if injection/messaging misses.

Comment thread apps/desktop/src/theme.ts
Comment on lines +67 to +76
const rollback = {
...current,
settings: previous.settings,
active: activeTheme(
previous.settings,
current.themes,
current.systemAppearance,
),
};
themeStateStore.set(rollback);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 [SUGGESTION] Rollback restores settings/active but does not bump revision. A concurrent main push with the failed settings and a higher revision can re-apply the rejected selection after rollback.

On rollback, either bump a local revision past the failed request or ignore main updates that still match the rejected settings until a newer authoritative state arrives.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overview

Follow-up on custom JSON themes (@hubble.md/theme, Electron ThemeService, Appearance UI, desktop/www/runtime wiring).

Prior review

  • Addressed: shared theme.css light/dark CSS fallbacks + alignment test; renderer tests for IPC rollback, stale revision, and preview restore; electron system-mode coverage.
  • Still open (non-blocking): HTML App sheet still has no static color tokens (injection + hubble:theme only); optimistic setThemeSettings rollback still does not bump/gate revision.

Concerns

  • No new correctness/security blockers in the delta or remaining PR surface.
  • Optional hardening only (HTML App CSS fallbacks; rollback vs concurrent main revision).

Verdict

Found: 0 critical, 0 important, 2 suggestions

Approve with nits


Reviewed by a Warp Factory agent.

--border: oklch(0.9 0.004 95);
--input: oklch(0.91 0.004 95);
--ring: var(--brand-accent);
--radius: 0.625rem;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 [SUGGESTION] Still open from prior review: static color tokens / dark prefers-color-scheme were removed. Desktop injects themeCss(active) and posts hubble:theme, so the happy path is fine, but a missed injection or late postMessage leaves apps with only --radius.

Keep a minimal built-in light (and optional dark) fallback here so HTML apps still render if those paths miss.

Comment thread apps/desktop/src/theme.ts
Comment on lines +67 to +76
const rollback = {
...current,
settings: previous.settings,
active: activeTheme(
previous.settings,
current.themes,
current.systemAppearance,
),
};
themeStateStore.set(rollback);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 [SUGGESTION] Still open: rollback restores settings/active but does not bump revision or ignore main updates that still match the rejected settings. A rare path (write succeeded, IPC error; or another source republishes the failed selection at a higher revision) can re-apply the rejected theme after rollback.

On rollback, bump a local revision past the failed request, or ignore main updates that still equal the rejected settings until newer authoritative state arrives.

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.

1 participant