Skip to content

feat(devtools-ui): add light mode with theme toggle#48

Merged
ryanbas21 merged 5 commits into
mainfrom
feat/light-mode
May 13, 2026
Merged

feat(devtools-ui): add light mode with theme toggle#48
ryanbas21 merged 5 commits into
mainfrom
feat/light-mode

Conversation

@ryanbas21
Copy link
Copy Markdown
Owner

Summary

  • Adds a full light mode color scheme via [data-theme="light"] CSS variable overrides (GitHub-light inspired palette)
  • Injects a ☾/☀ toggle button into the toolbar from panel.ts (same pattern as resize handles — outside Elm's virtual DOM)
  • Persists theme choice to localStorage, defaults to OS prefers-color-scheme
  • Cleans up hardcoded fallback CSS variables (--bg-2, --fg, --bg-hover, --bg-info) that bypassed the theme system

Test plan

  • Load extension in Chrome DevTools — verify dark mode renders as before
  • Click the theme toggle button (☾) in the toolbar — verify switch to light mode
  • Verify all panels (timeline, inspector, flow view, graph) use light colors
  • Close and reopen DevTools — verify theme preference persists
  • Test with OS set to light mode — verify it defaults to light without prior toggle
  • Verify dropdown menus (Export, Snapshot) render correctly in both themes
  • Verify import banner and paste panel render correctly in both themes

🤖 Generated with Claude Code

ryanbas21 and others added 2 commits May 13, 2026 11:02
Extract request/response bodies from the Headers tab into a dedicated
"Payload" tab (matching Chrome DevTools naming). The tab only appears
when a network event has body data, and each section includes a copy
button for easy clipboard export. The Headers tab now shows only URL,
method, and header data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a light mode color scheme using CSS variable overrides via
[data-theme="light"] on the root element. The toggle button is injected
into the toolbar by panel.ts (same pattern as resize handles), persists
the user's choice to localStorage, and defaults to the OS preference
via prefers-color-scheme.

Also cleans up hardcoded fallback CSS variables (--bg-2, --fg, --bg-hover,
--bg-info) that bypassed the theme system, replacing them with proper
theme-aware variables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Caution

The theme toggle button is destroyed on the first Elm virtual DOM re-render, making the toggle inoperable after any user interaction. See inline comment on panel.ts:130. Do not merge until this is fixed.

TL;DR — Adds a light/dark mode theme toggle with CSS variable overrides and localStorage persistence, plus a Payload tab in the Inspector panel. The CSS variable migration from hardcoded fallbacks is complete and correct. However, the theme toggle has a critical lifecycle bug: the injected DOM button is removed by Elm's virtual DOM reconciliation on the next model change.

Key changes

  • Add theme toggle with light mode CSS palette — Injects a ☾/☀ toggle button into the toolbar via MutationObserver, persists preference to localStorage, detects OS prefers-color-scheme on first visit, and adds a full [data-theme='light'] CSS variable override block
  • Migrate CSS variables away from hardcoded fallbacks — Replaces --bg-2, --fg, --bg-hover, --bg-info with the proper theme-aware variables (--raised, --text, --hover, --sel, --border). All call sites updated — no stale references remain.
  • Add Payload tab to Inspector — Shows request/response body content in a dedicated tab when network data has a payload, with proper fallback logic in Update.elm

Summary | 5 files | 2 commits | base: mainfeat/light-mode


Critical: theme toggle destroyed by Elm re-render

Before: MutationObserver inserts the button into .toolbar once, then disconnects permanently
After: Elm re-renders .toolbar on any model change, removes the injected button (not in Elm's VDOM), and the disconnected observer cannot recover it

The initThemeToggle function (panel.ts:103) injects a <button> into .toolbar via MutationObserver, then calls observer.disconnect() at line 130. However, .toolbar is rendered by Elm's virtual DOM (View.elm:279). When any Msg triggers a model change — toggling recording, switching view mode, importing, receiving an event — Elm re-renders the toolbar and its VDOM diff removes the injected button since it isn't in Elm's VDOM tree. The observer is already disconnected, so there's no recovery.

Fix: Remove the observer.disconnect() call. Instead, in the callback, check whether btn.parentElement is still the toolbar and re-insert if missing. This gracefully handles any Elm re-render cycle.

Why doesn't Elm cause an infinite loop? Inserting a DOM node from within the MutationObserver callback won't trigger Elm to re-render — Elm only re-renders in response to a `Msg` delivered through its ports. The mutation is batched with the one that triggered the callback. Even if it fired independently, the parent-element guard (`btn.parentElement !== toolbar`) prevents redundant insertions.

panel.ts · View.elm (not in diff — context reference)


CSS variable migration complete

The diff replaces all four hardcoded fallback variables (--bg-2, --fg, --bg-hover, --bg-info) with the proper theme variables. Confirmed zero remaining references across all CSS files. The --shadow-alpha variable (added at line 27 of panel.css) correctly darkens the dropdown shadow in light mode via rgba(0, 0, 0, calc(var(--shadow-alpha) * 0.7)).

Payload tab fallback logic correct

The PayloadTab variant is guarded at three levels: the tab button only appears when network data has a request or response body (Inspector.elm:94-96), the SelectEvent handler falls back to HeadersTab when the new event lacks payload data (Update.elm:108-117), and the initial model starts on HeadersTab (Model.elm:39). All three guards are correct.


Minor observations (non-blocking)

  • No prefers-color-scheme change listener (panel.ts:92) — the OS theme change is only checked at startup. If the user changes their OS theme while the DevTools panel is open and has no stored preference, the panel won't adapt until reopening.
  • Accessibility (panel.ts:109) — the toggle button uses title but no aria-label. For a button whose only visible content is a unicode symbol, consider adding aria-label="Toggle light/dark mode" for better screen-reader support.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run𝕏

Comment thread packages/devtools-extension/src/panel/panel.ts Outdated
@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented May 13, 2026

Addressed the critical review finding: the theme toggle button was being destroyed by Elm VDOM re-renders because the MutationObserver disconnected after the first insertion. Fixed by keeping the observer alive and guarding re-insertion with btn.parentElement !== toolbar. Also added aria-label for accessibility per the minor observation. All 24 tests pass.

Task list (7/7 completed)
  • Checkout PR feat(devtools-ui): add light mode with theme toggle #48 branch
  • Fetch review comments for review #4283960700
  • Fix: keep MutationObserver alive to re-insert button after Elm re-renders
  • Add aria-label for accessibility
  • Test, commit, push changes
  • Reply to review comments and resolve threads
  • Report progress

Pullfrog  | View workflow run | via Pullfrog𝕏

pullfrog Bot and others added 2 commits May 13, 2026 18:06
…id Elm VDOM corruption

The MutationObserver was inserting the theme toggle button before
.tb-spacer (middle of the toolbar), which shifted child indices and
caused Elm's index-based VDOM diffing to patch the wrong elements.
This broke the recording toggle state and clear button in E2E tests.

Using appendChild instead keeps the toggle after all Elm-managed
children, where it won't interfere with virtual DOM patching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues. Reviewed the following changes:

  • Simplified theme toggle re-insertion to always appendChild at the end of the toolbar, avoiding the Elm VDOM index corruption that insertBefore before .tb-spacer could trigger
  • Added clarifying comment documenting why insertBefore is unsafe with Elm's index-based patching

Pullfrog  | View workflow run𝕏

@ryanbas21
Copy link
Copy Markdown
Owner Author

@pullfrog approve or request changes

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Important

This PR adds light mode support but 21 hardcoded rgba() backgrounds across panel.css still use dark-theme RGB values, causing tinted UI surfaces to render with wrong hues in light mode. Before merging, these should use CSS variables or explicit [data-theme='light'] overrides. The pattern is the same everywhere — pick a representative subset and fix the rest by analogy.

TL;DR — Adds light mode CSS variable overrides, a theme toggle button with localStorage persistence, and a Payload tab for request/response body inspection. The theme support is mostly complete for primary text/background colors, but tinted surfaces (badges, tags, error banners, diagnosis panels, JWT decoder, snapshot buttons) still hardcode dark-theme RGB values in rgba() calls.

Key changes

  • Add [data-theme='light'] CSS variable overrides — All 16 color variables and --shadow-alpha get light-theme values, and the hardcoded fallbacks (var(--fg, #ccc)var(--text)) are cleaned up
  • Inject theme toggle via panel.ts — MutationObserver-based toggle button appended after Elm-managed toolbar children, using appendChild (not insertBefore) to avoid VDOM index corruption. Same pattern as the existing resize handles
  • Add Payload tab to inspector — Conditionally rendered when request/response body data exists, with its own sectioned view and copy button
  • Clean up fallback CSS variablesvar(--bg-2, ...), var(--fg, ...), var(--bg-hover, ...), var(--bg-info, ...) all replaced with proper theme-aware variables

Summary | 5 files | 4 commits | base: mainfeat/light-mode


Hardcoded rgba() backgrounds in light mode

Before: Tinted backgrounds in .err-banner, .b-net, .tag-cors, .jwt-summary, .diag-issue-error, and 15 other rules use dark-theme RGB values like rgba(248, 81, 73, 0.08) — these don't adapt in light mode.
After: Each should use either a CSS variable for the tinted color or an explicit [data-theme='light'] override with light-theme RGB values.

For example, .b-net uses rgba(88, 166, 255, 0.1)#58a6ff is the dark-theme blue. In light mode the correct blue is #0969da, so the tint should be rgba(9, 105, 218, 0.1). This pattern repeats across all 21 occurrences. The text color (var(--blue)) adapts correctly via the CSS variable, but the tinted background behind it stays dark-themed.

A systematic fix would introduce --blue-rgb, --red-rgb, etc. variables in both themes (e.g. --red-rgb: 248,81,73 / --red-rgb: 207,34,46) and use rgba(var(--red-rgb), 0.08) throughout. Alternatively, add [data-theme='light'] rule blocks with corrected RGB values for each affected selector.

The most visible impacted surfaces: error banner (renders at the top of the panel), event type badges (every row in the timeline), JWT decoder panels (shown for every OIDC event), and diagnosis issue callouts.

panel.css


Theme toggle implementation

Before: No theme toggle; dark mode only.
After: Theme toggle button injected into the toolbar via MutationObserver, with localStorage persistence and OS prefers-color-scheme fallback.

The implementation is correct and follows the established pattern from the resize handles. Key details verified:

  • getPreferredTheme() handles invalid localStorage values gracefully (falls through to OS preference)
  • applyTheme('dark') uses removeAttribute('data-theme') — this is correct as :root defaults to dark, but is fragile. Consider adding an explicit [data-theme='dark'] block (even if identical to :root) to make the two states symmetric
  • The MutationObserver uses appendChild (not insertBefore) with .toolbar as target — this avoids Elm VDOM index corruption, as explained in the comments
  • CopyToClipboard port wiring in Main.elm:46-47 correctly forwards to the JS port

panel.ts · Main.elm


Payload tab

Before: Request/response body data was shown inline under the Headers tab.
After: Body data gets a dedicated Payload tab, conditionally rendered when body data exists.

The Elm wiring is correct — PayloadTab constructor added to InspectorTab union type, handled in viewTabs (conditional rendering), viewContent (sectioned display with copy button), and update (fallback to HeadersTab when switching to an event without body data). The silent fallback to HeadersTab is consistent with how other conditional tabs (Collectors, Session, Config, Oidc) work. Note that users switching between a POST with body data and a GET without will see the tab bar change on every click.

Inspector.elm · Update.elm · Types.elm


Note: 4 inline comment(s) dropped because they did not anchor to lines inside the PR diff:

  • packages/devtools-ui/src/panel.css:259 (RIGHT) — line 259 (RIGHT) is not inside a diff hunk
  • packages/devtools-ui/src/panel.css:333-334 (RIGHT) — line 334 (RIGHT) is not inside a diff hunk
  • packages/devtools-ui/src/panel.css:694-695 (RIGHT) — line 695 (RIGHT) is not inside a diff hunk
  • packages/devtools-ui/src/panel.css:1014 (RIGHT) — line 1014 (RIGHT) is not inside a diff hunk

Pullfrog  | Fix it ➔View workflow run𝕏

@pullfrog
Copy link
Copy Markdown
Contributor

pullfrog Bot commented May 13, 2026

Addressed the review feedback about 21 hardcoded rgba() backgrounds in panel.css:

  • Added 6 --*-rgb CSS variables (red, blue, green, orange, yellow, purple) to both :root (dark theme) and [data-theme='light'] blocks
  • Replaced all 21 hardcoded rgba(R, G, B, alpha) calls with rgba(var(--color-rgb), alpha)
  • Two existing rgba(0,0,0,...) shadow calls already used var(--shadow-alpha) and were left unchanged

Pushed to feat/light-mode as commit ffe197c.

Task list (6/6 completed)
  • Understand the feedback: 21 hardcoded rgba() backgrounds in panel.css need CSS variable replacements
  • Find current CSS variable definitions and all rgba() usages in panel.css
  • Add --*-rgb CSS variables to :root and [data-theme='light'] blocks
  • Replace all hardcoded rgba() calls with rgba(var(--*-rgb), alpha)
  • Verify changes and test
  • Commit, push, reply to comments, and report progress

Pullfrog  | View workflow run | via Pullfrog𝕏

@ryanbas21
Copy link
Copy Markdown
Owner Author

@pullfrog request changes or approve.

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

No new issues found.

TL;DR — Adds light-mode theme support via [data-theme="light"] CSS variable overrides with a MutationObserver-based toggle button (same pattern as the existing resize handles). Also adds a conditionally-shown "Payload" tab to the Inspector for network events with request/response body data.

Key changes

  • Light mode color palette — Full [data-theme='light'] block in panel.css with GitHub-light-inspired values. Every color also gets a companion --*-rgb variable for use in rgba() calls, adapting across both themes correctly.

  • Theme toggle buttoninitThemeToggle() in panel.ts injects a ☾/☀ toggle into the toolbar via MutationObserver (same pattern as initResizeHandles), persists choice to localStorage, and defaults to OS prefers-color-scheme.

  • Hardcoded fallback cleanup — Replaces all var(--bg-2, #252526), var(--fg, #ccc), var(--bg-hover, #2a2d2e), and var(--bg-info, #063b49) fallbacks with proper theme variables (--raised, --text, --hover, --sel).

  • Payload tab for network requests — Adds PayloadTab constructor to InspectorTab, conditionally shown when a network event has request/response body data, with a viewPayloadSection helper in Inspector.elm.

Summary | 5 files | 4 commits | base: mainfeat/light-mode

panel.ts · panel.css · Inspector.elm · Types.elm · Update.elm

Pullfrog  | View workflow run𝕏

@ryanbas21 ryanbas21 merged commit 7021ad6 into main May 13, 2026
1 check passed
@ryanbas21 ryanbas21 deleted the feat/light-mode branch May 13, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant