Skip to content

feat(web): add theme gallery and custom palettes - #2501

Closed
an2tha wants to merge 2 commits into
pingdotgg:mainfrom
an2tha:feat/theme-gallery
Closed

feat(web): add theme gallery and custom palettes#2501
an2tha wants to merge 2 commits into
pingdotgg:mainfrom
an2tha:feat/theme-gallery

Conversation

@an2tha

@an2tha an2tha commented May 4, 2026

Copy link
Copy Markdown

Note

Medium Risk
Adds new persisted settings (theme, customCSS) and rewrites client-side theme application to inject CSS and toggle root classes, which can impact global styling across the app and desktop bridge theme syncing.

Overview
Adds a dedicated Themes settings section with a gallery-style picker (including multiple built-in dark themes) and a Custom CSS editor, wired to persisted client settings.

Extends ClientSettings/patch schema to store theme and customCSS, updates tests to include these fields, and refactors useTheme to read/write via settings, apply theme classes deterministically, inject theme CSS into a <style> tag, and sync browser/desktop chrome colors. Also updates base CSS to use :root.dark for dark variables and adds a new /settings/themes route + sidebar nav item.

Reviewed by Cursor Bugbot for commit 1f2724f. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add theme gallery and custom CSS palette editor to settings

  • Adds a dedicated /settings/themes page with a visual grid of clickable theme previews (system, light, dark, dracula, one-dark, oled-dark, nord, tokyo-night, gruvbox-dark, custom) and removes theme selection from the General settings panel.
  • Adds ThemePreview cards that render representative color tokens for each theme; selecting a theme applies it immediately via class toggling on <html> and CSS injection.
  • Persists theme and customCSS in ClientSettingsSchema (with defaults "system" and "") instead of localStorage; the useTheme hook reads/writes via useSettings/useUpdateSettings.
  • Behavioral Change: dark-mode CSS variables are now applied via :root.dark class selector instead of a Tailwind @variant dark block, which may affect any custom CSS that relied on the old variant.
📊 Macroscope summarized 85c6016. 6 files reviewed, 2 issues evaluated, 0 issues filtered, 2 comments posted

🗂️ Filtered Issues

@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

@an2tha is attempting to deploy a commit to the Ping Labs Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 364eba3e-17cb-4580-b535-f4c0cad5e873

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

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

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels May 4, 2026
@an2tha an2tha closed this May 4, 2026
Comment on lines +365 to +373
const isDark =
theme === "dark" ||
theme === "dracula" ||
theme === "one-dark" ||
theme === "oled-dark" ||
theme === "nord" ||
theme === "tokyo-night" ||
theme === "gruvbox-dark" ||
(theme === "system" && getSystemDark());

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.

🟠 High hooks/useTheme.ts:365

When theme === "custom", the isDark check (lines 365-373) omits "custom" so isDark evaluates to false. This skips adding the "dark" class (lines 380-381) and causes syncDesktopTheme to receive "light" instead of "dark". However, useTheme reports resolvedTheme: "dark" for custom themes (line 439), so the DOM classes and desktop sync are inconsistent with the hook's return value.

-  const isDark =
-    theme === "dark" ||
-    theme === "dracula" ||
-    theme === "one-dark" ||
-    theme === "oled-dark" ||
-    theme === "nord" ||
-    theme === "tokyo-night" ||
-    theme === "gruvbox-dark" ||
-    (theme === "system" && getSystemDark());
+  const isDark =
+    theme === "dark" ||
+    theme === "dracula" ||
+    theme === "one-dark" ||
+    theme === "oled-dark" ||
+    theme === "nord" ||
+    theme === "tokyo-night" ||
+    theme === "gruvbox-dark" ||
+    theme === "custom" ||
+    (theme === "system" && getSystemDark());
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/hooks/useTheme.ts around lines 365-373:

When `theme === "custom"`, the `isDark` check (lines 365-373) omits `"custom"` so `isDark` evaluates to `false`. This skips adding the `"dark"` class (lines 380-381) and causes `syncDesktopTheme` to receive `"light"` instead of `"dark"`. However, `useTheme` reports `resolvedTheme: "dark"` for custom themes (line 439), so the DOM classes and desktop sync are inconsistent with the hook's return value.

Evidence trail:
apps/web/src/hooks/useTheme.ts lines 365-373 (isDark check omits 'custom'), line 375 (isBaseTheme check), lines 378-383 (else branch — dark class not added when isDark is false), line 404 (syncDesktopTheme receives 'light'), lines 436-440 (resolvedTheme returns 'dark' for custom theme with comment '// All custom themes are dark for now'). Reviewed at commit REVIEWED_COMMIT.

window.removeEventListener("storage", handleStorage);
};
}
const systemDark = useMemo(() => getSystemDark(), []);

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.

🟡 Medium hooks/useTheme.ts:434

systemDark is captured once on mount via useMemo(..., []) and never updates. When the system preference changes, resolvedTheme still returns the old value (e.g., "light" after switching to dark) until the component remounts. Consumers relying on resolvedTheme will see stale data.

-  const systemDark = useMemo(() => getSystemDark(), []);
+  const [systemDark, setSystemDark] = useState(() => getSystemDark());
+
+  useEffect(() => {
+    const mq = window.matchMedia(MEDIA_QUERY);
+    const handleChange = () => setSystemDark(mq.matches);
+    mq.addEventListener("change", handleChange);
+    return () => mq.removeEventListener("change", handleChange);
+  }, []);
🤖 Copy this AI Prompt to have your agent fix this:
In file apps/web/src/hooks/useTheme.ts around line 434:

`systemDark` is captured once on mount via `useMemo(..., [])` and never updates. When the system preference changes, `resolvedTheme` still returns the old value (e.g., `"light"` after switching to dark) until the component remounts. Consumers relying on `resolvedTheme` will see stale data.

Evidence trail:
apps/web/src/hooks/useTheme.ts lines 434 (useMemo with empty deps), 436-440 (resolvedTheme depending on systemDark), 455-462 (useEffect handles visual theme change but doesn't update systemDark), 464 (stale resolvedTheme returned to consumers)

@macroscopeapp

macroscopeapp Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

New feature adding theme gallery and custom CSS capability. Multiple unresolved review comments identify bugs: custom CSS is unreachable from the UI, the 'custom' theme has incorrect dark mode handling, and resolvedTheme becomes stale on OS preference changes.

You can customize Macroscope's approvability policy. Learn more.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1f2724f. Configure here.

}
if (theme === "custom" && customCSS) {
fullCSS += `\n.custom {\n${customCSS}\n}`;
}

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.

Custom CSS never applied due to unreachable condition

High Severity

The customCSS is only injected into the page when theme === "custom", but the "custom" theme value is deliberately excluded from BUILT_IN_THEME_OPTIONS and has no selectable UI option. This means the Custom CSS textarea in ThemesPanel is entirely non-functional — users can type CSS but it will never be applied regardless of which theme they select.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1f2724f. Configure here.

if (theme === "system") return systemDark ? "dark" : "light";
if (theme === "light") return "light";
return "dark"; // All custom themes are dark for now
}, [theme, systemDark]);

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.

Stale resolvedTheme when OS theme changes

Medium Severity

systemDark is computed once on mount with useMemo(() => getSystemDark(), []) and never updates. When the OS switches between light/dark mode, the DOM updates correctly via the media query listener, but resolvedTheme remains stale. Components consuming resolvedTheme (e.g., ChatMarkdown for syntax highlighting themes, ChatView) won't re-render with the correct value.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1f2724f. Configure here.

Comment thread package.json
},
"dependencies": {
"@t3tools/monorepo": "."
},

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.

Self-referential dependency in root package.json

Low Severity

The root package.json (named @t3tools/monorepo) declares itself as a dependency with "@t3tools/monorepo": ".". This self-referential dependency serves no purpose and appears to be accidentally committed, possibly from a debugging session or tooling mishap.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1f2724f. Configure here.

}, []);
useEffect(() => {
applyTheme(theme, customCSS, true);
}, [theme, customCSS]);

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.

Theme flash on load due to removed synchronous initialization

Medium Severity

The old code had a synchronous applyTheme(getStored()) at module load that read the theme from localStorage before React rendered, preventing a flash of wrong theme. This was removed with no replacement. Now the theme is only applied inside a useEffect (which runs after the first paint), and the settings are loaded asynchronously via hydrateClientSettings. Users with a non-system theme will see a brief flash of the default "system" theme on every page load until hydration completes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1f2724f. Configure here.

@an2tha
an2tha deleted the feat/theme-gallery branch May 9, 2026 11:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant