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

@an2thaan2tha 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

vercelBot 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

coderabbitaiBot 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-actionsgithub-actionsBot 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
@an2thaan2tha 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.

🟠 Highhooks/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.

🟡 Mediumhooks/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

macroscopeappBot 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.

@cursorcursorBot 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 CursorFix 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 CursorFix in Web

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

Comment threadpackage.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 CursorFix 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@an2tha
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@an2thaan2tha 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

vercelBot 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

coderabbitaiBot 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-actionsgithub-actionsBot 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
@an2thaan2tha 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.

🟠 Highhooks/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.

🟡 Mediumhooks/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

macroscopeappBot 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.

@cursorcursorBot 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 CursorFix 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 CursorFix in Web

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

Comment threadpackage.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 CursorFix 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@an2tha
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@an2thaan2tha 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

vercelBot 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

coderabbitaiBot 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-actionsgithub-actionsBot 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
@an2thaan2tha 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.

🟠 Highhooks/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.

🟡 Mediumhooks/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

macroscopeappBot 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.

@cursorcursorBot 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 CursorFix 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 CursorFix in Web

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

Comment threadpackage.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 CursorFix 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@an2tha
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@an2thaan2tha 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

vercelBot 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

coderabbitaiBot 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-actionsgithub-actionsBot 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
@an2thaan2tha 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.

🟠 Highhooks/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.

🟡 Mediumhooks/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

macroscopeappBot 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.

@cursorcursorBot 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 CursorFix 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 CursorFix in Web

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

Comment threadpackage.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 CursorFix 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@an2tha
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@an2thaan2tha 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

vercelBot 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

coderabbitaiBot 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-actionsgithub-actionsBot 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
@an2thaan2tha 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.

🟠 Highhooks/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.

🟡 Mediumhooks/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

macroscopeappBot 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.

@cursorcursorBot 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 CursorFix 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 CursorFix in Web

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

Comment threadpackage.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 CursorFix 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@an2tha
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@an2thaan2tha 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

vercelBot 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

coderabbitaiBot 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-actionsgithub-actionsBot 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
@an2thaan2tha 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.

🟠 Highhooks/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.

🟡 Mediumhooks/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

macroscopeappBot 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.

@cursorcursorBot 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 CursorFix 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 CursorFix in Web

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

Comment threadpackage.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 CursorFix 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@an2tha
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@an2thaan2tha 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

vercelBot 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

coderabbitaiBot 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-actionsgithub-actionsBot 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
@an2thaan2tha 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.

🟠 Highhooks/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.

🟡 Mediumhooks/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

macroscopeappBot 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.

@cursorcursorBot 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 CursorFix 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 CursorFix in Web

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

Comment threadpackage.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 CursorFix 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@an2tha
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@an2thaan2tha 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

vercelBot 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

coderabbitaiBot 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-actionsgithub-actionsBot 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
@an2thaan2tha 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.

🟠 Highhooks/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.

🟡 Mediumhooks/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

macroscopeappBot 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.

@cursorcursorBot 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 CursorFix 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 CursorFix in Web

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

Comment threadpackage.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 CursorFix 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 CursorFix 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@an2tha