diff --git a/packages/devtools-extension/src/panel/panel.ts b/packages/devtools-extension/src/panel/panel.ts index 7cc325e..4ef8bee 100644 --- a/packages/devtools-extension/src/panel/panel.ts +++ b/packages/devtools-extension/src/panel/panel.ts @@ -82,11 +82,61 @@ function initResizeHandles() { }); } +// ── Theme toggle ───────────────────────────────────────────────────────────── + +const THEME_KEY = 'wolfcola:theme'; + +function getPreferredTheme(): 'dark' | 'light' { + const stored = localStorage.getItem(THEME_KEY); + if (stored === 'light' || stored === 'dark') return stored; + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; +} + +function applyTheme(theme: 'dark' | 'light') { + if (theme === 'light') { + root.setAttribute('data-theme', 'light'); + } else { + root.removeAttribute('data-theme'); + } +} + +function initThemeToggle() { + let theme = getPreferredTheme(); + applyTheme(theme); + + const btn = document.createElement('button'); + btn.className = 'theme-toggle'; + btn.title = 'Toggle light/dark mode'; + btn.ariaLabel = 'Toggle light/dark mode'; + btn.textContent = theme === 'light' ? '☀' : '☾'; + + btn.addEventListener('click', () => { + theme = theme === 'dark' ? 'light' : 'dark'; + applyTheme(theme); + localStorage.setItem(THEME_KEY, theme); + btn.textContent = theme === 'light' ? '☀' : '☾'; + }); + + // Keep the observer alive — Elm's virtual DOM re-renders the toolbar + // on any model change and removes nodes it doesn't know about. + // IMPORTANT: always appendChild (never insertBefore) so the toggle + // lives *after* all Elm-managed children. Elm patches by index, so + // inserting in the middle shifts indices and corrupts button state. + const observer = new MutationObserver(() => { + const toolbar = document.querySelector('.toolbar'); + if (toolbar && btn.parentElement !== toolbar) { + toolbar.appendChild(btn); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); +} + // ── App init ────────────────────────────────────────────────────────────────── const app = Elm.Main.init({ node: document.getElementById('app'), flags: null }); initResizeHandles(); +initThemeToggle(); function copyToClipboard(text: string): void { if (navigator.clipboard?.writeText) { diff --git a/packages/devtools-ui/src/panel.css b/packages/devtools-ui/src/panel.css index 23d1c9a..f152343 100644 --- a/packages/devtools-ui/src/panel.css +++ b/packages/devtools-ui/src/panel.css @@ -19,11 +19,18 @@ --muted: #8b949e; --dim: #484f58; --blue: #58a6ff; + --blue-rgb: 88, 166, 255; --green: #3fb950; + --green-rgb: 63, 185, 80; --red: #f85149; + --red-rgb: 248, 81, 73; --orange: #ffa657; + --orange-rgb: 255, 166, 87; --yellow: #d29922; + --yellow-rgb: 210, 153, 34; --purple: #bc8cff; + --purple-rgb: 188, 140, 255; + --shadow-alpha: 0.3; --font-ui: 'Segoe UI', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; --font-mono: 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; @@ -33,6 +40,32 @@ --toolbar-h: 32px; } +[data-theme='light'] { + --base: #ffffff; + --surface: #f6f8fa; + --raised: #eaeef2; + --hover: #eef1f5; + --sel: #ddf4ff; + --border: #d0d7de; + --bdim: #e4e8ec; + --text: #1f2328; + --muted: #636c76; + --dim: #8c959f; + --blue: #0969da; + --blue-rgb: 9, 105, 218; + --green: #1a7f37; + --green-rgb: 26, 127, 55; + --red: #cf222e; + --red-rgb: 207, 34, 46; + --orange: #bc4c00; + --orange-rgb: 188, 76, 0; + --yellow: #9a6700; + --yellow-rgb: 154, 103, 0; + --purple: #8250df; + --purple-rgb: 130, 80, 223; + --shadow-alpha: 0.12; +} + html, body { height: 100%; @@ -146,7 +179,7 @@ body { border-color: var(--red); } .tb-btn.recording:hover { - background: rgba(248, 81, 73, 0.1); + background: rgba(var(--red-rgb), 0.1); } .rec-dot { @@ -205,14 +238,41 @@ body { color: var(--dim); } +/* ── Theme Toggle ───────────────────────────────────────── */ +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + background: transparent; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--muted); + cursor: pointer; + font-size: 13px; + line-height: 1; + transition: + color 0.12s, + border-color 0.12s, + background 0.12s; + flex-shrink: 0; +} +.theme-toggle:hover { + color: var(--text); + border-color: var(--muted); + background: var(--raised); +} + /* ── Error Banner ────────────────────────────────────────── */ .err-banner { padding: 5px 12px; - background: rgba(248, 81, 73, 0.08); + background: rgba(var(--red-rgb), 0.08); color: var(--red); font-family: var(--font-mono); font-size: 11px; - border-bottom: 1px solid rgba(248, 81, 73, 0.25); + border-bottom: 1px solid rgba(var(--red-rgb), 0.25); flex-shrink: 0; } @@ -283,19 +343,19 @@ body { flex-shrink: 0; } .b-net { - background: rgba(88, 166, 255, 0.1); + background: rgba(var(--blue-rgb), 0.1); color: var(--blue); } .b-sdk { - background: rgba(63, 185, 80, 0.1); + background: rgba(var(--green-rgb), 0.1); color: var(--green); } .b-ses { - background: rgba(255, 166, 87, 0.1); + background: rgba(var(--orange-rgb), 0.1); color: var(--orange); } .b-cfg { - background: rgba(188, 140, 255, 0.1); + background: rgba(var(--purple-rgb), 0.1); color: var(--purple); } @@ -379,15 +439,15 @@ body { overflow: visible; } .tag-cors { - background: rgba(248, 81, 73, 0.12); + background: rgba(var(--red-rgb), 0.12); color: var(--red); } .tag-oidc { - background: rgba(88, 166, 255, 0.12); + background: rgba(var(--blue-rgb), 0.12); color: var(--blue); } .tag-coll { - background: rgba(88, 166, 255, 0.12); + background: rgba(var(--blue-rgb), 0.12); color: var(--blue); } @@ -522,7 +582,7 @@ body { transition: background 0.12s; } .cause-btn:hover { - background: rgba(88, 166, 255, 0.1); + background: rgba(var(--blue-rgb), 0.1); } /* collector card */ @@ -531,7 +591,7 @@ body { border-radius: 4px; padding: 8px 10px; margin-bottom: 8px; - background: rgba(0, 0, 0, 0.2); + background: rgba(0, 0, 0, calc(var(--shadow-alpha) * 0.7)); } .coll-card-header { display: flex; @@ -558,14 +618,25 @@ body { line-height: 1; } .fv-copy-btn:hover { - color: var(--fg); - border-color: var(--fg); + color: var(--text); + border-color: var(--text); } .coll-copy-all { font-size: 10px; padding: 2px 8px; } +/* ── Payload Tab ─────────────────────────────────────────── */ +.payload-section { + margin-bottom: 8px; +} +.payload-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} + /* ── JsonTree ────────────────────────────────────────────── */ .jt-sec { margin-bottom: 14px; @@ -633,8 +704,8 @@ body { font-weight: 700; letter-spacing: 0.04em; color: var(--purple); - background: rgba(188, 140, 255, 0.1); - border: 1px solid rgba(188, 140, 255, 0.3); + background: rgba(var(--purple-rgb), 0.1); + border: 1px solid rgba(var(--purple-rgb), 0.3); border-radius: 3px; padding: 1px 6px; list-style: none; @@ -653,8 +724,8 @@ details[open] > .jwt-summary::before { .jwt-body { margin-top: 6px; padding: 8px 10px; - background: rgba(188, 140, 255, 0.04); - border: 1px solid rgba(188, 140, 255, 0.15); + background: rgba(var(--purple-rgb), 0.04); + border: 1px solid rgba(var(--purple-rgb), 0.15); border-radius: 4px; font-family: var(--font-mono); font-size: 11px; @@ -706,7 +777,7 @@ details[open] > .jwt-summary::before { .jwt-expired { display: inline-flex; align-items: center; - background: rgba(248, 81, 73, 0.15); + background: rgba(var(--red-rgb), 0.15); color: var(--red); font-size: 9px; font-weight: 700; @@ -852,11 +923,11 @@ details[open] > .jwt-summary::before { font-size: 11px; } .fh-error { - background: rgba(248, 81, 73, 0.08); + background: rgba(var(--red-rgb), 0.08); border-left: 3px solid var(--red); } .fh-warning { - background: rgba(210, 153, 34, 0.08); + background: rgba(var(--yellow-rgb), 0.08); border-left: 3px solid var(--yellow); } .fh-header { @@ -952,11 +1023,11 @@ details[open] > .jwt-summary::before { } .diag-issue-error { border-left-color: var(--red); - background: rgba(248, 81, 73, 0.06); + background: rgba(var(--red-rgb), 0.06); } .diag-issue-warning { border-left-color: var(--yellow); - background: rgba(210, 153, 34, 0.06); + background: rgba(var(--yellow-rgb), 0.06); } .diag-issue-info { border-left-color: var(--blue); @@ -1116,10 +1187,10 @@ body.resizing { top: 100%; left: 0; z-index: 100; - background: var(--bg-2, #252526); - border: 1px solid var(--border, #3c3c3c); + background: var(--raised); + border: 1px solid var(--border); border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + box-shadow: 0 2px 8px rgba(0, 0, 0, var(--shadow-alpha)); min-width: 140px; padding: 2px 0; } @@ -1130,12 +1201,12 @@ body.resizing { text-align: left; background: none; border: none; - color: var(--fg, #ccc); + color: var(--text); cursor: pointer; font-size: 11px; } .tb-dropdown-item:hover { - background: var(--bg-hover, #2a2d2e); + background: var(--hover); } /* ── Snapshot dropdown ────────────────────────────────── */ @@ -1192,7 +1263,7 @@ body.resizing { } .snapshot-delete:hover { color: var(--red); - background: rgba(248, 81, 73, 0.1); + background: rgba(var(--red-rgb), 0.1); } /* ── Import banner ────────────────────────────────── */ @@ -1201,22 +1272,22 @@ body.resizing { align-items: center; justify-content: space-between; padding: 4px 12px; - background: var(--bg-info, #063b49); - border-bottom: 1px solid var(--border, #3c3c3c); + background: var(--sel); + border-bottom: 1px solid var(--border); font-size: 11px; - color: var(--fg, #ccc); + color: var(--text); } .import-banner-clear { background: none; - border: 1px solid var(--border, #3c3c3c); - color: var(--fg, #ccc); + border: 1px solid var(--border); + color: var(--text); padding: 1px 8px; border-radius: 3px; cursor: pointer; font-size: 10px; } .import-banner-clear:hover { - background: var(--bg-hover, #2a2d2e); + background: var(--hover); } /* ── Import paste panel ─────────────────────────────── */ diff --git a/packages/devtools-ui/src/src/Inspector.elm b/packages/devtools-ui/src/src/Inspector.elm index 1925469..e4ce76a 100644 --- a/packages/devtools-ui/src/src/Inspector.elm +++ b/packages/devtools-ui/src/src/Inspector.elm @@ -87,8 +87,24 @@ viewTabs maybeEvent activeTab maybeDiagnosis = else [] ) - ++ [ tabButton "Headers" HeadersTab activeTab - , tabButton "Cookies" CookiesTab activeTab + ++ [ tabButton "Headers" HeadersTab activeTab ] + ++ (case maybeEvent of + Just event -> + case event.data of + Network net -> + if net.requestBody /= Nothing || net.responseBody /= Nothing then + [ tabButton "Payload" PayloadTab activeTab ] + + else + [] + + _ -> + [] + + Nothing -> + [] + ) + ++ [ tabButton "Cookies" CookiesTab activeTab , tabButton "CORS" CorsTab activeTab , tabButton "SDK State" SdkStateTab activeTab ] @@ -166,19 +182,35 @@ viewContent maybeEvent activeTab maybeDiagnosis = Just h -> JsonTree.view "Response Headers" h Nothing -> viewEmptySection "Response Headers" ] - ++ (case net.requestBody of - Just b -> [ JsonTree.view "Request Body" b ] - Nothing -> [] - ) + ) + + _ -> + div [ class "insp-empty" ] + [ text "Select a network request to see headers." ] + + ( Just event, PayloadTab ) -> + case event.data of + Network net -> + div [] + ((case net.requestBody of + Just b -> + [ viewPayloadSection "Request Body" b ] + + Nothing -> + [] + ) ++ (case net.responseBody of - Just b -> [ JsonTree.view "Response Body" b ] - Nothing -> [] + Just b -> + [ viewPayloadSection "Response Body" b ] + + Nothing -> + [] ) ) _ -> div [ class "insp-empty" ] - [ text "Select a network request to see headers." ] + [ text "No payload data for this event." ] ( Just event, CookiesTab ) -> case event.data of @@ -407,6 +439,21 @@ viewEmptySection label = ] +viewPayloadSection : String -> Decode.Value -> Html Msg +viewPayloadSection label body = + div [ class "payload-section" ] + [ div [ class "payload-header" ] + [ div [ class "sect-hdr", style "margin" "0", style "border" "none", style "padding" "0" ] [ text label ] + , button + [ class "fv-copy-btn" + , onClick (CopyToClipboard (Encode.encode 4 body)) + ] + [ text "\u{2398}" ] + ] + , JsonTree.view label body + ] + + viewCookies : NetworkData -> List (Html Msg) viewCookies net = let diff --git a/packages/devtools-ui/src/src/Types.elm b/packages/devtools-ui/src/src/Types.elm index 1265623..83e007f 100644 --- a/packages/devtools-ui/src/src/Types.elm +++ b/packages/devtools-ui/src/src/Types.elm @@ -179,6 +179,7 @@ type alias OidcSemanticData = type InspectorTab = DiagnosisTab | HeadersTab + | PayloadTab | CookiesTab | CorsTab | SdkStateTab diff --git a/packages/devtools-ui/src/src/Update.elm b/packages/devtools-ui/src/src/Update.elm index 86f77c4..4136cb6 100644 --- a/packages/devtools-ui/src/src/Update.elm +++ b/packages/devtools-ui/src/src/Update.elm @@ -105,6 +105,17 @@ update msg model = else OidcTab + ( PayloadTab, Just e ) -> + case e.data of + Network net -> + if net.requestBody == Nothing && net.responseBody == Nothing then + HeadersTab + else + PayloadTab + + _ -> + HeadersTab + _ -> model.activeTab in