feat: add light mode to dev portal (#2072)

## Summary

Adds a light / dark mode toggle to the dev portal.

<img width="1549" height="922" alt="Screenshot 2026-04-08 at 18 57 29" src="https://github.com/user-attachments/assets/e118d21b-6840-4db3-8309-a8af43ea698b" />
<img width="1549" height="922" alt="Screenshot 2026-04-08 at 18 57 24" src="https://github.com/user-attachments/assets/ff08c270-2ba8-4514-b46d-4b671957e8bf" />
This commit is contained in:
Karl Power 2026-04-08 22:17:33 +02:00 committed by GitHub
parent 73b746cbcf
commit 337ebff054
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 130 additions and 12 deletions

View file

@ -66,10 +66,26 @@
</svg>
HyperDX Dev Portal
</h1>
<div class="header-right">
<div class="status">
<div class="dot"></div>
<span id="refresh-status">Auto-refreshing every 3s</span>
</div>
<button
class="theme-toggle"
id="theme-toggle"
onclick="toggleTheme()"
title="Toggle light/dark mode"
>
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m8.66-13.66l-.71.71M4.05 19.95l-.71.71M21 12h-1M4 12H3m16.66 7.66l-.71-.71M4.05 4.05l-.71-.71M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="display:none">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12.79A9 9 0 1111.21 3a7 7 0 009.79 9.79z"/>
</svg>
</button>
</div>
</div>
<div class="tab-bar">
@ -120,6 +136,46 @@
</div>
<script>
// --- Theme ---
function getPreferredTheme() {
const stored = localStorage.getItem('hdx-dev-portal-theme');
if (stored === 'light' || stored === 'dark') return stored;
return window.matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark';
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
const sunIcon = document.getElementById('theme-icon-sun');
const moonIcon = document.getElementById('theme-icon-moon');
if (sunIcon && moonIcon) {
// Sun icon shown in dark mode (click to go light), moon in light mode
sunIcon.style.display = theme === 'dark' ? '' : 'none';
moonIcon.style.display = theme === 'light' ? '' : 'none';
}
}
function toggleTheme() {
const current =
document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
localStorage.setItem('hdx-dev-portal-theme', next);
applyTheme(next);
}
// Apply immediately to avoid flash
applyTheme(getPreferredTheme());
// Listen for OS theme changes (only when no explicit preference saved)
window
.matchMedia('(prefers-color-scheme: light)')
.addEventListener('change', () => {
if (!localStorage.getItem('hdx-dev-portal-theme')) {
applyTheme(getPreferredTheme());
}
});
const contentEl = document.getElementById('content');
const historyEl = document.getElementById('history-content');
const errorEl = document.getElementById('error-banner');

View file

@ -15,6 +15,36 @@
--orange: #db6d28;
--log-bg: #141517; /* dark-8 */
--hover: #25262b; /* dark-6: color-bg-hover */
--log-text: #c9d1d9;
--row-hover: rgba(88, 166, 255, 0.05);
--row-active: rgba(88, 166, 255, 0.1);
--table-border: rgba(48, 54, 61, 0.5);
--modal-backdrop: rgba(0, 0, 0, 0.6);
--hover-overlay: rgba(255, 255, 255, 0.05);
}
:root[data-theme='light'] {
--bg: #ffffff;
--card-bg: #f8f9fa;
--card-surface: #f1f3f5;
--border: #dee2e6;
--border-emphasis: #ced4da;
--text: #212529;
--text-muted: #868e96;
--accent: #12b886;
--accent-hover: #0ca678;
--green: #12b886;
--red: #e03131;
--yellow: #f08c00;
--orange: #e8590c;
--log-bg: #f8f9fa;
--hover: #e9ecef;
--log-text: #212529;
--row-hover: rgba(0, 0, 0, 0.03);
--row-active: rgba(0, 0, 0, 0.06);
--table-border: rgba(0, 0, 0, 0.08);
--modal-backdrop: rgba(0, 0, 0, 0.3);
--hover-overlay: rgba(0, 0, 0, 0.05);
}
* {
@ -120,7 +150,7 @@ body {
white-space: pre-wrap;
word-break: break-all;
background: var(--bg);
color: #c9d1d9;
color: var(--log-text);
}
.log-content .log-line {
@ -166,6 +196,12 @@ body {
flex-shrink: 0;
}
.header .header-right {
display: flex;
align-items: center;
gap: 12px;
}
.header .status {
font-size: 13px;
color: var(--text-muted);
@ -329,7 +365,7 @@ body {
.services-table td {
padding: 10px 20px;
font-size: 14px;
border-bottom: 1px solid rgba(48, 54, 61, 0.5);
border-bottom: 1px solid var(--table-border);
}
.services-table tr:last-child td {
@ -342,11 +378,11 @@ body {
}
.services-table tr.clickable:hover {
background: rgba(88, 166, 255, 0.05);
background: var(--row-hover);
}
.services-table tr.active {
background: rgba(88, 166, 255, 0.1);
background: var(--row-active);
}
.service-name {
@ -517,7 +553,7 @@ body {
.history-toggle-btn:hover {
color: var(--text);
background: rgba(255, 255, 255, 0.05);
background: var(--hover-overlay);
}
.history-card-body {
@ -543,7 +579,7 @@ body {
justify-content: space-between;
padding: 12px 20px;
background: var(--card-surface);
border-bottom: 1px solid rgba(48, 54, 61, 0.5);
border-bottom: 1px solid var(--table-border);
}
.history-entry-header .history-meta {
@ -595,11 +631,11 @@ body {
}
.file-item:hover {
background: rgba(88, 166, 255, 0.05);
background: var(--row-hover);
}
.file-item.active {
background: rgba(88, 166, 255, 0.1);
background: var(--row-active);
}
.file-name {
@ -660,7 +696,7 @@ body {
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
background: var(--modal-backdrop);
display: flex;
align-items: center;
justify-content: center;
@ -735,3 +771,29 @@ body {
.modal-danger:hover {
opacity: 0.85;
}
/* --- Theme toggle --- */
.theme-toggle {
background: none;
border: 1px solid var(--border);
color: var(--text-muted);
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
flex-shrink: 0;
}
.theme-toggle:hover {
color: var(--text);
border-color: var(--text-muted);
}
.theme-toggle svg {
width: 16px;
height: 16px;
}