Skip to content

feat: dark mode — CSS tokens, theme toggle, no-flash init - #2

Merged
bharvey88 merged 2 commits into
ApolloAutomation:mainfrom
jesserockz:copilot/implement-dark-mode-support
Jul 9, 2026
Merged

feat: dark mode — CSS tokens, theme toggle, no-flash init#2
bharvey88 merged 2 commits into
ApolloAutomation:mainfrom
jesserockz:copilot/implement-dark-mode-support

Conversation

@jesserockz

@jesserockz jesserockz commented Jul 9, 2026

Copy link
Copy Markdown

Adds full dark mode support: system-preference auto-detection, an explicit 3-way toggle (System / Dark / Light), and localStorage persistence. Light mode remains the default.

CSS (css/style.css)

  • All colour values were already on CSS variables; added [data-theme="dark"] block overriding the full token set with dark-appropriate values
  • Extracted 5 hard-coded hex colours (.device-card img, .device-head img, .fallback, .release-notes, .error-box) into new tokens (--img-bg, --bg-blue-tint, --bg-green-tint, --bg-error, --border-error) so they switch automatically

No-flash init (index.html <head>)

Inline script runs before the stylesheet is applied — reads localStorage and sets data-theme on <html> before first paint:

<script>
  (function () {
    try {
      var t = localStorage.getItem('theme');
      if (t === 'dark' || (!t && matchMedia('(prefers-color-scheme: dark)').matches))
        document.documentElement.setAttribute('data-theme', 'dark');
    } catch (e) {}
  }());
</script>

Theme toggle (js/theme.js, index.html, js/app.js)

  • New #theme-toggle button in the header nav; hidden via CSS until JS initialises it (avoids a ghost-button flash)
  • Cycles System → Dark 🌙 → Light on each click; aria-label / title always describe the current mode
  • System mode removes the localStorage key and re-evaluates prefers-color-scheme; a MediaQueryList change listener keeps the page in sync if the OS preference changes while loaded
2026-07-09_14-42-49

Summary by CodeRabbit

  • New Features

    • Added a theme switcher with system, light, and dark modes.
    • Theme choice is remembered and updates automatically to match system color settings when set to system mode.
    • Updated navigation with a dedicated theme toggle button and improved accessibility labels.
  • Style

    • Refreshed theme-aware colors and borders across key UI areas, including headers, cards, release notes, fallback content, and error messages.
    • Improved spacing and alignment in the top navigation.

Copilot AI added 2 commits July 9, 2026 00:44
- Introduce [data-theme="dark"] CSS token overrides for all colour variables
- Add new tokens (--img-bg, --bg-blue-tint, --bg-green-tint, --bg-error,
  --border-error) and replace hard-coded hex colours in .device-card img,
  .device-head img, .fallback, .release-notes, and .error-box
- Add .theme-btn styles for the header toggle button
- Add no-flash inline script in <head> that sets data-theme before first paint
- Add #theme-toggle button to header nav (System ⬤ / Dark 🌙 / Light ☀)
- New js/theme.js: 3-way cycle (system → dark → light), localStorage
  persistence, and live media-query listener for OS preference changes
- Wire initThemeToggle() into js/app.js
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a light/dark/system theme toggle: a new js/theme.js module manages preference persistence and application, js/app.js wires initialization at startup, index.html adds a boot-time theme script and toggle button, and css/style.css introduces themeable variables applied across UI components.

Changes

Theme Toggle Feature

Layer / File(s) Summary
Theme module: constants, persistence, and apply logic
js/theme.js
Defines theme cycle (system/dark/light), icon/label/help-text mappings, localStorage persistence with fallback, and logic to apply dark mode via data-theme attribute.
Theme toggle button wiring
js/theme.js
initThemeToggle() binds click handler to cycle themes, syncs with OS prefers-color-scheme changes, and initializes button icon/aria-label/title.
App startup integration
js/app.js, index.html
Imports and calls initThemeToggle() on startup; adds inline boot-time script to set data-theme early and a #theme-toggle button in header nav.
Themeable CSS variables and styles
css/style.css
Adds --img-bg, --bg-blue-tint, --bg-green-tint, --bg-error, --border-error variables for light/dark themes, styles .theme-btn, and replaces hardcoded colors on device images, fallback, release-notes, and error-box.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant IndexHTML as index.html (boot script)
    participant AppJS as app.js
    participant ThemeJS as theme.js
    participant DOM as document.documentElement

    Browser->>IndexHTML: Load page
    IndexHTML->>DOM: Read localStorage.theme / prefers-color-scheme
    IndexHTML->>DOM: Set data-theme="dark" (if applicable)
    Browser->>AppJS: Execute app startup
    AppJS->>ThemeJS: initThemeToggle()
    ThemeJS->>DOM: Find `#theme-toggle` button
    ThemeJS->>ThemeJS: Load saved preference
    ThemeJS->>DOM: Apply theme (set/remove data-theme)
    ThemeJS->>ThemeJS: Register matchMedia change listener
    Browser->>ThemeJS: Click `#theme-toggle`
    ThemeJS->>ThemeJS: Cycle to next theme
    ThemeJS->>DOM: Persist and re-apply theme
    ThemeJS->>DOM: Update button icon/aria-label/title
Loading

Poem

A rabbit hops at dusk and dawn,
flicking switches, light to gone.
🌙 dark, ☀️ light, 🖥️ system too,
one little click brings each in view.
Themes now dance with CSS flair—
this bunny's proud, without a care! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: dark mode support with theme tokens, a toggle, and no-flash initialization.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
js/theme.js (2)

35-35: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

matchMedia is unguarded in theme.js while the boot script wraps it in try/catch.

If matchMedia throws (e.g., in a restricted environment), initThemeToggle will abort at line 35 before the button is made visible, leaving the toggle permanently hidden. The boot script already handles this gracefully. Consider wrapping the matchMedia call or providing a fallback.

Also applies to: 26-26

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

In `@js/theme.js` at line 35, `initThemeToggle` in `theme.js` calls
`matchMedia('(prefers-color-scheme: dark)')` without protection, so mirror the
boot script’s defensive handling here. Wrap the `matchMedia` lookup in try/catch
or add a safe fallback so `initThemeToggle` can continue and still reveal the
toggle button even when `matchMedia` is unavailable or throws. Apply the same
fix anywhere else this unguarded `matchMedia` usage appears in the theme setup
flow.

22-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Boot script and applyPref duplicate dark-detection logic.

The inline script in index.html (lines 9–18) and applyPref here independently implement the same dark-mode decision tree. While the inline script can't import from this module (it runs before module evaluation), the two copies can silently drift if one is updated without the other. Consider adding a brief comment in both locations noting the contract they must keep in sync.

Also applies to: 9-18

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

In `@js/theme.js` around lines 22 - 29, The dark-mode detection logic is
duplicated between the inline boot script and applyPref, so add a short
sync-contract comment in both places to keep their behavior aligned. Update the
applyPref function and the corresponding inline script block to clearly note
that they must use the same pref === 'dark' / system matchMedia decision tree,
so future changes are made in both locations together.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@js/theme.js`:
- Line 35: `initThemeToggle` in `theme.js` calls
`matchMedia('(prefers-color-scheme: dark)')` without protection, so mirror the
boot script’s defensive handling here. Wrap the `matchMedia` lookup in try/catch
or add a safe fallback so `initThemeToggle` can continue and still reveal the
toggle button even when `matchMedia` is unavailable or throws. Apply the same
fix anywhere else this unguarded `matchMedia` usage appears in the theme setup
flow.
- Around line 22-29: The dark-mode detection logic is duplicated between the
inline boot script and applyPref, so add a short sync-contract comment in both
places to keep their behavior aligned. Update the applyPref function and the
corresponding inline script block to clearly note that they must use the same
pref === 'dark' / system matchMedia decision tree, so future changes are made in
both locations together.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c854ff3f-1648-4ed5-ae3c-af3f90735196

📥 Commits

Reviewing files that changed from the base of the PR and between 6327e4f and 186966e.

📒 Files selected for processing (4)
  • css/style.css
  • index.html
  • js/app.js
  • js/theme.js

@bharvey88 bharvey88 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Really nice work on this, thanks @jesserockz. I pulled it down and went through it end to end, and from my side it's good to merge.

What I checked:

  • Playwright suite passes 10/10 against your branch. theme.js loads fine and the token refactor didn't disturb any selector the tests key off.
  • Deploy path: our Pages workflow stages files with cp -r ... js ..., so the whole js/ dir (theme.js included) ships to production automatically. No workflow change needed.
  • Brand palette: light :root keeps --blue #417AAB and --green #9ABD32, and the dark overrides read as intentional lightened variants. No regression in light mode.
  • No-flash init: the inline script is dependency-free and matches applyPref()'s logic, so no flash and no extra request or build step. Nice touch keeping the toggle hidden until the module wires it up.

Two tiny, non-blocking notes (take or leave):

  1. The toggle is a 3-way cycle (system -> dark -> light). We have an a11y pass queued in #6 that adds aria-pressed to the filter pills and toggles. Worth remembering this one is a cycle rather than a boolean, so the aria-label you already update is the right call here, not aria-pressed.
  2. The placeholder moon glyph in the button markup never actually renders (the button stays hidden until updateBtn sets the real icon), so it's harmless. Just flagging in case a future reader wonders why it's there.

Thanks again for kicking off the dark theme.

@bharvey88
bharvey88 merged commit 84d4d29 into ApolloAutomation:main Jul 9, 2026
3 checks passed
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