Customization: all CSS variables exposed, light/dark mode separate

- Nav bar now uses CSS variables (was hardcoded gradient)
- 19 customizable colors: accent, text, backgrounds, borders,
  surfaces, inputs, stripes, hover, selected, status indicators
- Light and dark mode have separate color sets
- Theme tab shows which mode you are editing
- Toggle ☀️/🌙 in nav bar to switch modes and edit the other set
- Export includes both theme and themeDark sections
- localStorage save/restore handles both modes
This commit is contained in:
you
2026-03-23 01:23:04 +00:00
parent b461a05b6d
commit da19ddef51
3 changed files with 119 additions and 23 deletions
+116 -20
View File
@@ -19,9 +19,42 @@
accentHover: '#6db3ff',
navBg: '#0f0f23',
navBg2: '#1a1a2e',
statusGreen: '#45644c',
statusYellow: '#b08b2d',
statusRed: '#b54a4a'
text: '#1a1a2e',
textMuted: '#5b6370',
border: '#e2e5ea',
surface0: '#f4f5f7',
surface1: '#ffffff',
surface2: '#ffffff',
cardBg: '#ffffff',
contentBg: '#f4f5f7',
inputBg: '#ffffff',
rowStripe: '#f9fafb',
rowHover: '#eef2ff',
selectedBg: '#dbeafe',
statusGreen: '#22c55e',
statusYellow: '#eab308',
statusRed: '#ef4444'
},
themeDark: {
accent: '#4a9eff',
accentHover: '#6db3ff',
navBg: '#0f0f23',
navBg2: '#1a1a2e',
text: '#e2e8f0',
textMuted: '#a8b8cc',
border: '#334155',
surface0: '#0f0f23',
surface1: '#1a1a2e',
surface2: '#232340',
cardBg: '#1a1a2e',
contentBg: '#0f0f23',
inputBg: '#1e1e34',
rowStripe: '#1e1e34',
rowHover: '#2d2d50',
selectedBg: '#1e3a5f',
statusGreen: '#22c55e',
statusYellow: '#eab308',
statusRed: '#ef4444'
},
nodeColors: {
repeater: '#dc2626',
@@ -58,29 +91,65 @@
accentHover: '--accent-hover',
navBg: '--nav-bg',
navBg2: '--nav-bg2',
text: '--text',
textMuted: '--text-muted',
border: '--border',
surface0: '--surface-0',
surface1: '--surface-1',
surface2: '--surface-2',
cardBg: '--card-bg',
contentBg: '--content-bg',
inputBg: '--input-bg',
rowStripe: '--row-stripe',
rowHover: '--row-hover',
selectedBg: '--selected-bg',
statusGreen: '--status-green',
statusYellow: '--status-yellow',
statusRed: '--status-red'
};
const THEME_LABELS = {
accent: 'Accent Color',
accent: 'Accent',
accentHover: 'Accent Hover',
navBg: 'Nav Background',
navBg2: 'Nav Background 2',
text: 'Text',
textMuted: 'Muted Text',
border: 'Borders',
surface0: 'Page Background',
surface1: 'Card Background',
surface2: 'Surface 2',
cardBg: 'Card Background',
contentBg: 'Content Background',
inputBg: 'Input Background',
rowStripe: 'Table Stripe',
rowHover: 'Row Hover',
selectedBg: 'Selected Row',
statusGreen: 'Status Green',
statusYellow: 'Status Yellow',
statusRed: 'Status Red'
};
const THEME_HINTS = {
accent: 'Active nav tab, buttons, links, selected rows, badges, chart bars',
accentHover: 'Button hover states, link hovers',
navBg: 'Top navigation bar background',
navBg2: 'Nav dropdown menus, darker nav areas',
statusGreen: 'Healthy nodes, online indicators, good SNR',
statusYellow: 'Degraded nodes, warning indicators, moderate SNR',
statusRed: 'Silent/offline nodes, error states, bad SNR'
accent: 'Active nav tab, buttons, links, selected rows, badges',
accentHover: 'Button and link hover states',
navBg: 'Top navigation bar gradient start',
navBg2: 'Top navigation bar gradient end',
text: 'Primary text color',
textMuted: 'Secondary/muted text, labels, timestamps',
border: 'Table borders, card borders, dividers',
surface0: 'Main page background',
surface1: 'Cards, panels, modals',
surface2: 'Nested surfaces, inputs, secondary panels',
cardBg: 'Card and detail panel backgrounds',
contentBg: 'Content area background',
inputBg: 'Text inputs, dropdowns, search boxes',
rowStripe: 'Alternating table row background',
rowHover: 'Table row hover highlight',
selectedBg: 'Selected/active row background',
statusGreen: 'Healthy nodes, online indicators',
statusYellow: 'Degraded nodes, warnings',
statusRed: 'Offline/silent nodes, errors'
};
const NODE_LABELS = {
@@ -111,6 +180,7 @@
state = {
branding: Object.assign({}, DEFAULTS.branding, cfg.branding || {}),
theme: Object.assign({}, DEFAULTS.theme, cfg.theme || {}),
themeDark: Object.assign({}, DEFAULTS.themeDark, cfg.themeDark || {}),
nodeColors: Object.assign({}, DEFAULTS.nodeColors, cfg.nodeColors || {}),
home: {
heroTitle: (cfg.home && cfg.home.heroTitle) || DEFAULTS.home.heroTitle,
@@ -122,6 +192,14 @@
};
}
function isDarkMode() {
return document.documentElement.getAttribute('data-theme') === 'dark' ||
(document.documentElement.getAttribute('data-theme') !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
}
function activeTheme() { return isDarkMode() ? state.themeDark : state.theme; }
function activeDefaults() { return isDarkMode() ? DEFAULTS.themeDark : DEFAULTS.theme; }
function saveOriginalCSS() {
var cs = getComputedStyle(document.documentElement);
originalValues = {};
@@ -131,8 +209,9 @@
}
function applyThemePreview() {
var t = activeTheme();
for (var key in THEME_CSS_MAP) {
document.documentElement.style.setProperty(THEME_CSS_MAP[key], state.theme[key]);
if (t[key]) document.documentElement.style.setProperty(THEME_CSS_MAP[key], t[key]);
}
}
@@ -258,10 +337,15 @@
}
function renderTheme() {
var dark = isDarkMode();
var modeLabel = dark ? '🌙 Dark Mode' : '☀️ Light Mode';
var themeKey = dark ? 'themeDark' : 'theme';
var defs = activeDefaults();
var current = activeTheme();
var rows = '';
for (var key in THEME_LABELS) {
var val = state.theme[key];
var def = DEFAULTS.theme[key];
var val = current[key] || defs[key] || '#000000';
var def = defs[key] || '#000000';
rows += '<div class="cust-color-row">' +
'<div><label>' + THEME_LABELS[key] + '</label>' +
'<div class="cust-hint">' + (THEME_HINTS[key] || '') + '</div></div>' +
@@ -271,8 +355,8 @@
'</div>';
}
return '<div class="cust-panel' + (activeTab === 'theme' ? ' active' : '') + '" data-panel="theme">' +
'<p class="cust-section-title">Theme Colors</p>' +
'<p style="font-size:13px;color:var(--text-muted);margin:0 0 16px">Changes apply as a live preview. Use Export tab to save.</p>' +
'<p class="cust-section-title">' + modeLabel + ' Colors</p>' +
'<p style="font-size:11px;color:var(--text-muted);margin:0 0 12px">Editing ' + (dark ? 'dark' : 'light') + ' mode. Toggle ☀️/🌙 in nav to switch.</p>' +
rows +
'<button class="cust-reset-preview" id="custResetPreview">↩ Reset Preview</button>' +
'</div>';
@@ -353,6 +437,13 @@
}
if (Object.keys(th).length) out.theme = th;
// Dark theme
var thd = {};
for (var tdk in DEFAULTS.themeDark) {
if (state.themeDark[tdk] !== DEFAULTS.themeDark[tdk]) thd[tdk] = state.themeDark[tdk];
}
if (Object.keys(thd).length) out.themeDark = thd;
// Node colors
var nc = {};
for (var nk in DEFAULTS.nodeColors) {
@@ -436,7 +527,8 @@
container.querySelectorAll('input[data-theme]').forEach(function (inp) {
inp.addEventListener('input', function () {
var key = inp.dataset.theme;
state.theme[key] = inp.value;
var themeKey = isDarkMode() ? 'themeDark' : 'theme';
state[themeKey][key] = inp.value;
var hex = container.querySelector('[data-hex="' + key + '"]');
if (hex) hex.textContent = inp.value;
applyThemePreview();
@@ -447,7 +539,8 @@
container.querySelectorAll('[data-reset-theme]').forEach(function (btn) {
btn.addEventListener('click', function () {
var key = btn.dataset.resetTheme;
state.theme[key] = DEFAULTS.theme[key];
var themeKey = isDarkMode() ? 'themeDark' : 'theme';
state[themeKey][key] = activeDefaults()[key];
applyThemePreview();
render(container);
});
@@ -655,8 +748,11 @@
const saved = localStorage.getItem('meshcore-user-theme');
if (saved) {
const userTheme = JSON.parse(saved);
if (userTheme.theme) {
for (const [key, val] of Object.entries(userTheme.theme)) {
const dark = document.documentElement.getAttribute('data-theme') === 'dark' ||
(document.documentElement.getAttribute('data-theme') !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
const themeData = dark ? (userTheme.themeDark || userTheme.theme) : userTheme.theme;
if (themeData) {
for (const [key, val] of Object.entries(themeData)) {
if (THEME_CSS_MAP[key]) document.documentElement.style.setProperty(THEME_CSS_MAP[key], val);
}
}
+2 -2
View File
@@ -22,7 +22,7 @@
<meta name="twitter:title" content="MeshCore Analyzer">
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/public/og-image.png">
<link rel="stylesheet" href="style.css?v=1774221932">
<link rel="stylesheet" href="style.css?v=1774228984">
<link rel="stylesheet" href="home.css">
<link rel="stylesheet" href="live.css?v=1774058575">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
@@ -101,6 +101,6 @@
<script src="observer-detail.js?v=1774219440" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1773985649" onerror="console.error('Failed to load:', this.src)"></script>
<script src="customize.js?v=1774228659" onerror="console.error('Failed to load:', this.src)"></script>
<script src="customize.js?v=1774228984" onerror="console.error('Failed to load:', this.src)"></script>
</body>
</html>
+1 -1
View File
@@ -87,7 +87,7 @@ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible
/* === Nav === */
.top-nav {
display: flex; align-items: center; justify-content: space-between;
background: linear-gradient(135deg, #0f0f23 0%, #151532 50%, #1a1035 100%); color: #fff; padding: 0 20px; height: 52px;
background: linear-gradient(135deg, var(--nav-bg) 0%, var(--nav-bg2) 100%); color: #fff; padding: 0 20px; height: 52px;
position: sticky; top: 0; z-index: 1100;
box-shadow: 0 2px 8px rgba(0,0,0,.3);
}