Skip to content

Switch to radio buttons for theme selection#44518

Open
lukeheath wants to merge 2 commits intomainfrom
fix-dark-mode-bugs
Open

Switch to radio buttons for theme selection#44518
lukeheath wants to merge 2 commits intomainfrom
fix-dark-mode-bugs

Conversation

@lukeheath
Copy link
Copy Markdown
Member

@lukeheath lukeheath commented Apr 30, 2026

Summary by CodeRabbit

  • New Features

    • Expanded theme selection to three options: system, light, and dark (replacing the binary toggle).
    • Updated selector UI to radio-style controls for clearer selection and immediate persistence of user choice.
    • Theme preference now persists and respects system preference when set to "system".
  • Style

    • Updated theme selector layout and label styles for improved accessibility and visual clarity.

Copilot AI review requested due to automatic review settings April 30, 2026 17:50
@lukeheath lukeheath requested a review from a team as a code owner April 30, 2026 17:50
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@lukeheath
Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a77afb5e-cc7d-4c34-9e23-996fe0500e01

📥 Commits

Reviewing files that changed from the base of the PR and between 56a8bc0 and 79dfa69.

📒 Files selected for processing (3)
  • frontend/pages/AccountPage/AccountSidePanel/AccountSidePanel.tsx
  • frontend/pages/AccountPage/AccountSidePanel/_styles.scss
  • frontend/utilities/theme.ts

Walkthrough

Replaces the binary dark-mode toggle with a tri-state theme picker supporting system, light, and dark modes. The AccountSidePanel UI switches from a switch control to a radiogroup of radio inputs and stores the selected mode via setThemeMode(). Theme utilities were refactored: introduced ThemeMode type, getThemeMode(), setThemeMode(), readStoredMode(), and resolveDark(); isDarkMode() now derives from the tri-state mode and toggleDarkMode() was removed. Initialization and OS-preference listeners now apply only when the stored mode is system.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author; the template requires sections like related issue, checklist items, testing, and database migrations. Add a detailed PR description covering the related issue number, applicable checklist items (especially testing and changes files), and any relevant implementation notes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main UI change: replacing the binary theme toggle with a radio button interface for three-state theme selection.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-dark-mode-bugs

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
Review rate limit: 6/8 reviews remaining, refill in 10 minutes and 23 seconds.

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 55.55556% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.75%. Comparing base (8cd2277) to head (79dfa69).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
frontend/utilities/theme.ts 55.55% 8 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #44518   +/-   ##
=======================================
  Coverage   66.75%   66.75%           
=======================================
  Files        2636     2636           
  Lines      211725   211731    +6     
  Branches     9556     9559    +3     
=======================================
+ Hits       141332   141337    +5     
- Misses      57549    57550    +1     
  Partials    12844    12844           
Flag Coverage Δ
frontend 54.78% <55.55%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the frontend theme selection UX from a binary dark-mode toggle to a tri-state theme mode (system/light/dark) backed by a new localStorage key, enabling “follow system” behavior while keeping explicit user selections sticky.

Changes:

  • Replace fleet-dark-mode boolean storage with fleet-theme mode storage (system/light/dark) and add legacy-key migration.
  • Update Account side panel UI to use radio buttons for theme selection.
  • Adjust Account side panel styling to support the new radio-based theme picker layout.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
frontend/utilities/theme.ts Introduces tri-state theme mode storage + legacy migration and resolves effective dark mode from mode/system preference.
frontend/pages/AccountPage/AccountSidePanel/AccountSidePanel.tsx Replaces switch toggle with a radiogroup using shared Radio component, persisting the selected mode.
frontend/pages/AccountPage/AccountSidePanel/_styles.scss Removes toggle styling and adds styles for the new theme picker and label.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to +47
const onThemeSelect = (value: string) => {
const mode = value as ThemeMode;
setThemeModeState(mode);
setThemeMode(mode);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

onThemeSelect uses a type assertion (value as ThemeMode) and then persists that value. Because value is just string, this can silently allow invalid values into component state/localStorage if the handler is ever reused or called with an unexpected value. Consider validating against the allowed set (e.g., value === "system" || value === "light" || value === "dark") before calling setThemeModeState/setThemeMode, or narrowing the handler so it only accepts ThemeMode values.

Suggested change
const onThemeSelect = (value: string) => {
const mode = value as ThemeMode;
setThemeModeState(mode);
setThemeMode(mode);
const isThemeMode = (value: string): value is ThemeMode => {
return value === "system" || value === "light" || value === "dark";
};
const onThemeSelect = (value: string) => {
if (!isThemeMode(value)) {
return;
}
setThemeModeState(value);
setThemeMode(value);

Copilot uses AI. Check for mistakes.
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.

We already have isThemeMode in frontend/utilities/theme.ts, we can just export it and use it here. But honestly if this is passing TypeScript checking already I don't think this would add a lot of value unless we plan on throwing an error if we receive an invalid value.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/utilities/theme.ts`:
- Around line 17-19: readStoredMode currently calls localStorage.getItem
directly which can throw in non-browser or restricted environments; wrap the
localStorage access in a try/catch and/or guard with a runtime check (e.g.,
typeof window !== "undefined" && window.localStorage) so that readStoredMode
returns the safe default "system" on any error or when storage is unavailable;
apply the same pattern to the other functions touching storage (the
setter/remover around THEME_KEY, e.g., setStoredMode and clearStoredMode) so all
localStorage.getItem/setItem/removeItem calls are protected and fall back
gracefully.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d901c316-f452-45ce-b262-e7d6227b1ffb

📥 Commits

Reviewing files that changed from the base of the PR and between 56a8bc0 and 79dfa69.

📒 Files selected for processing (3)
  • frontend/pages/AccountPage/AccountSidePanel/AccountSidePanel.tsx
  • frontend/pages/AccountPage/AccountSidePanel/_styles.scss
  • frontend/utilities/theme.ts

Comment on lines +17 to +19
const readStoredMode = (): ThemeMode => {
const stored = localStorage.getItem(THEME_KEY);
if (stored !== null) {
return stored === "true";
}
return systemPrefersDark();
return isThemeMode(stored) ? stored : "system";
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard localStorage access to prevent runtime crashes.

localStorage.getItem/setItem/removeItem can throw in restricted environments (or non-browser execution), which would break both initialization and theme selection.

Proposed fix
+const getStorage = (): Storage | null => {
+  if (typeof window === "undefined") return null;
+  try {
+    return window.localStorage;
+  } catch {
+    return null;
+  }
+};
+
 const readStoredMode = (): ThemeMode => {
-  const stored = localStorage.getItem(THEME_KEY);
+  const stored = getStorage()?.getItem(THEME_KEY);
   return isThemeMode(stored) ? stored : "system";
 };
 
 export const setThemeMode = (mode: ThemeMode): void => {
-  if (mode === "system") {
-    localStorage.removeItem(THEME_KEY);
-  } else {
-    localStorage.setItem(THEME_KEY, mode);
+  const storage = getStorage();
+  if (storage) {
+    if (mode === "system") {
+      storage.removeItem(THEME_KEY);
+    } else {
+      storage.setItem(THEME_KEY, mode);
+    }
   }
   applyDarkMode(resolveDark(mode), true);
 };

Also applies to: 45-49

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/utilities/theme.ts` around lines 17 - 19, readStoredMode currently
calls localStorage.getItem directly which can throw in non-browser or restricted
environments; wrap the localStorage access in a try/catch and/or guard with a
runtime check (e.g., typeof window !== "undefined" && window.localStorage) so
that readStoredMode returns the safe default "system" on any error or when
storage is unavailable; apply the same pattern to the other functions touching
storage (the setter/remover around THEME_KEY, e.g., setStoredMode and
clearStoredMode) so all localStorage.getItem/setItem/removeItem calls are
protected and fall back gracefully.

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.

I don't think this is very high risk, especially "non-browser execution", but it's also cheap so sure

return isThemeMode(stored) ? stored : "system";
};

export const getThemeMode = (): ThemeMode => readStoredMode();
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.

I don't follow why we need both the internal readStoredMode() and the exported getThemeMode() that wraps it

Comment on lines +44 to +47
const onThemeSelect = (value: string) => {
const mode = value as ThemeMode;
setThemeModeState(mode);
setThemeMode(mode);
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.

We already have isThemeMode in frontend/utilities/theme.ts, we can just export it and use it here. But honestly if this is passing TypeScript checking already I don't think this would add a lot of value unless we plan on throwing an error if we receive an invalid value.

Comment on lines +17 to +19
const readStoredMode = (): ThemeMode => {
const stored = localStorage.getItem(THEME_KEY);
if (stored !== null) {
return stored === "true";
}
return systemPrefersDark();
return isThemeMode(stored) ? stored : "system";
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.

I don't think this is very high risk, especially "non-browser execution", but it's also cheap so sure

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants