From a2db5f767cb464f0056920b6e7bb1e84faf5dbb9 Mon Sep 17 00:00:00 2001 From: you Date: Mon, 23 Mar 2026 05:48:51 +0000 Subject: [PATCH] Add 8 preset themes with theme picker UI Adds a Theme Presets section at the top of the Theme Colors tab with 8 WCAG AA-verified preset themes: - Default: Original MeshCore blue (#4a9eff) - Ocean: Deep blues and teals, professional - Forest: Greens and earth tones, natural - Sunset: Warm oranges, ambers, and reds - Monochrome: Pure grays, no color accent, minimal - High Contrast: WCAG AAA (7:1), bold colors, accessibility-first - Midnight: Deep purples and indigos, elegant - Ember: Dark warm red/orange accents, cyberpunk feel Each theme has both light and dark variants with all 20 color keys. High Contrast theme includes custom nodeColors and typeColors for maximum distinguishability. Active preset is auto-detected and highlighted. Users can select a preset then tweak individual colors (becomes Custom). --- public/customize.js | 247 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) diff --git a/public/customize.js b/public/customize.js index bebfd15b..73c26112 100644 --- a/public/customize.js +++ b/public/customize.js @@ -96,6 +96,245 @@ mono: '--mono', }; + /* ── Theme Presets ── */ + const THEME_COLOR_KEYS = ['accent', 'navBg', 'navText', 'background', 'text', 'statusGreen', 'statusYellow', 'statusRed', + 'accentHover', 'navBg2', 'navTextMuted', 'textMuted', 'border', 'surface1', 'surface2', 'cardBg', 'contentBg', + 'detailBg', 'inputBg', 'rowStripe', 'rowHover', 'selectedBg']; + + const PRESETS = { + default: { + name: 'Default', desc: 'MeshCore blue', + preview: ['#4a9eff', '#0f0f23', '#f4f5f7', '#1a1a2e', '#22c55e'], + light: { + accent: '#4a9eff', navBg: '#0f0f23', navText: '#ffffff', background: '#f4f5f7', text: '#1a1a2e', + statusGreen: '#22c55e', statusYellow: '#eab308', statusRed: '#ef4444', + accentHover: '#6db3ff', navBg2: '#1a1a2e', navTextMuted: '#cbd5e1', textMuted: '#5b6370', border: '#e2e5ea', + surface1: '#ffffff', surface2: '#ffffff', cardBg: '#ffffff', contentBg: '#f4f5f7', + detailBg: '#ffffff', inputBg: '#ffffff', rowStripe: '#f9fafb', rowHover: '#eef2ff', selectedBg: '#dbeafe', + }, + dark: { + accent: '#4a9eff', navBg: '#0f0f23', navText: '#ffffff', background: '#0f0f23', text: '#e2e8f0', + statusGreen: '#22c55e', statusYellow: '#eab308', statusRed: '#ef4444', + accentHover: '#6db3ff', navBg2: '#1a1a2e', navTextMuted: '#cbd5e1', textMuted: '#a8b8cc', border: '#334155', + surface1: '#1a1a2e', surface2: '#232340', cardBg: '#1a1a2e', contentBg: '#0f0f23', + detailBg: '#232340', inputBg: '#1e1e34', rowStripe: '#1e1e34', rowHover: '#2d2d50', selectedBg: '#1e3a5f', + } + }, + ocean: { + name: 'Ocean', desc: 'Deep blues & teals', + preview: ['#0077b6', '#03045e', '#f0f7fa', '#48cae4', '#15803d'], + light: { + accent: '#0077b6', navBg: '#03045e', navText: '#ffffff', background: '#f0f7fa', text: '#0a1628', + statusGreen: '#15803d', statusYellow: '#a16207', statusRed: '#dc2626', + accentHover: '#0096d6', navBg2: '#023e8a', navTextMuted: '#90caf9', textMuted: '#4a6580', border: '#c8dce8', + surface1: '#ffffff', surface2: '#e8f4f8', cardBg: '#ffffff', contentBg: '#f0f7fa', + detailBg: '#ffffff', inputBg: '#ffffff', rowStripe: '#f5fafd', rowHover: '#e0f0f8', selectedBg: '#bde0fe', + }, + dark: { + accent: '#48cae4', navBg: '#03045e', navText: '#ffffff', background: '#0a1929', text: '#e0e7ef', + statusGreen: '#4ade80', statusYellow: '#facc15', statusRed: '#f87171', + accentHover: '#76d7ea', navBg2: '#012a4a', navTextMuted: '#90caf9', textMuted: '#8eafc4', border: '#1e3a5f', + surface1: '#0d2137', surface2: '#122d4a', cardBg: '#0d2137', contentBg: '#0a1929', + detailBg: '#122d4a', inputBg: '#0d2137', rowStripe: '#0d2137', rowHover: '#153450', selectedBg: '#1a4570', + } + }, + forest: { + name: 'Forest', desc: 'Greens & earth tones', + preview: ['#2d6a4f', '#1b3a2d', '#f2f7f4', '#52b788', '#15803d'], + light: { + accent: '#2d6a4f', navBg: '#1b3a2d', navText: '#ffffff', background: '#f2f7f4', text: '#1a2e24', + statusGreen: '#15803d', statusYellow: '#a16207', statusRed: '#dc2626', + accentHover: '#40916c', navBg2: '#2d6a4f', navTextMuted: '#a3c4b5', textMuted: '#557063', border: '#c8dcd2', + surface1: '#ffffff', surface2: '#e8f0eb', cardBg: '#ffffff', contentBg: '#f2f7f4', + detailBg: '#ffffff', inputBg: '#ffffff', rowStripe: '#f5faf7', rowHover: '#e4f0e8', selectedBg: '#c2e0cc', + }, + dark: { + accent: '#52b788', navBg: '#1b3a2d', navText: '#ffffff', background: '#0d1f17', text: '#d8e8df', + statusGreen: '#4ade80', statusYellow: '#facc15', statusRed: '#f87171', + accentHover: '#74c69d', navBg2: '#14532d', navTextMuted: '#86b89a', textMuted: '#8aac9a', border: '#2d4a3a', + surface1: '#162e23', surface2: '#1d3a2d', cardBg: '#162e23', contentBg: '#0d1f17', + detailBg: '#1d3a2d', inputBg: '#162e23', rowStripe: '#162e23', rowHover: '#1f4030', selectedBg: '#265940', + } + }, + sunset: { + name: 'Sunset', desc: 'Warm oranges & ambers', + preview: ['#c2410c', '#431407', '#fef7f2', '#fb923c', '#dc2626'], + light: { + accent: '#c2410c', navBg: '#431407', navText: '#ffffff', background: '#fef7f2', text: '#1c0f06', + statusGreen: '#15803d', statusYellow: '#a16207', statusRed: '#dc2626', + accentHover: '#ea580c', navBg2: '#7c2d12', navTextMuted: '#fdba74', textMuted: '#6b5344', border: '#e8d5c8', + surface1: '#ffffff', surface2: '#fef0e6', cardBg: '#ffffff', contentBg: '#fef7f2', + detailBg: '#ffffff', inputBg: '#ffffff', rowStripe: '#fefaf7', rowHover: '#fef0e0', selectedBg: '#fed7aa', + }, + dark: { + accent: '#fb923c', navBg: '#431407', navText: '#ffffff', background: '#1a0f08', text: '#f0ddd0', + statusGreen: '#4ade80', statusYellow: '#facc15', statusRed: '#f87171', + accentHover: '#fdba74', navBg2: '#7c2d12', navTextMuted: '#c2855a', textMuted: '#b09080', border: '#4a2a18', + surface1: '#261a10', surface2: '#332214', cardBg: '#261a10', contentBg: '#1a0f08', + detailBg: '#332214', inputBg: '#261a10', rowStripe: '#261a10', rowHover: '#3a2818', selectedBg: '#5c3518', + } + }, + mono: { + name: 'Monochrome', desc: 'Pure grays, no color', + preview: ['#525252', '#171717', '#f5f5f5', '#a3a3a3', '#737373'], + light: { + accent: '#525252', navBg: '#171717', navText: '#ffffff', background: '#f5f5f5', text: '#171717', + statusGreen: '#15803d', statusYellow: '#a16207', statusRed: '#dc2626', + accentHover: '#737373', navBg2: '#262626', navTextMuted: '#a3a3a3', textMuted: '#525252', border: '#d4d4d4', + surface1: '#ffffff', surface2: '#fafafa', cardBg: '#ffffff', contentBg: '#f5f5f5', + detailBg: '#ffffff', inputBg: '#ffffff', rowStripe: '#fafafa', rowHover: '#efefef', selectedBg: '#e5e5e5', + }, + dark: { + accent: '#a3a3a3', navBg: '#171717', navText: '#ffffff', background: '#0a0a0a', text: '#e5e5e5', + statusGreen: '#4ade80', statusYellow: '#facc15', statusRed: '#f87171', + accentHover: '#d4d4d4', navBg2: '#1a1a1a', navTextMuted: '#737373', textMuted: '#a3a3a3', border: '#333333', + surface1: '#171717', surface2: '#1f1f1f', cardBg: '#171717', contentBg: '#0a0a0a', + detailBg: '#1f1f1f', inputBg: '#171717', rowStripe: '#141414', rowHover: '#222222', selectedBg: '#2a2a2a', + } + }, + highContrast: { + name: 'High Contrast', desc: 'WCAG AAA, max readability', + preview: ['#0050a0', '#000000', '#ffffff', '#66b3ff', '#006400'], + light: { + accent: '#0050a0', navBg: '#000000', navText: '#ffffff', background: '#ffffff', text: '#000000', + statusGreen: '#006400', statusYellow: '#7a5900', statusRed: '#b30000', + accentHover: '#0068cc', navBg2: '#1a1a1a', navTextMuted: '#e0e0e0', textMuted: '#333333', border: '#000000', + surface1: '#ffffff', surface2: '#f0f0f0', cardBg: '#ffffff', contentBg: '#ffffff', + detailBg: '#ffffff', inputBg: '#ffffff', rowStripe: '#f0f0f0', rowHover: '#e0e8f5', selectedBg: '#cce0ff', + }, + dark: { + accent: '#66b3ff', navBg: '#000000', navText: '#ffffff', background: '#000000', text: '#ffffff', + statusGreen: '#66ff66', statusYellow: '#ffff00', statusRed: '#ff6666', + accentHover: '#99ccff', navBg2: '#0a0a0a', navTextMuted: '#cccccc', textMuted: '#cccccc', border: '#ffffff', + surface1: '#111111', surface2: '#1a1a1a', cardBg: '#111111', contentBg: '#000000', + detailBg: '#1a1a1a', inputBg: '#111111', rowStripe: '#0d0d0d', rowHover: '#1a2a3a', selectedBg: '#003366', + }, + nodeColors: { repeater: '#ff0000', companion: '#0066ff', room: '#009900', sensor: '#cc8800', observer: '#9933ff' }, + typeColors: { + ADVERT: '#009900', GRP_TXT: '#0066ff', TXT_MSG: '#cc8800', ACK: '#666666', + REQUEST: '#9933ff', RESPONSE: '#0099cc', TRACE: '#cc0066', PATH: '#009999', ANON_REQ: '#cc3355' + } + }, + midnight: { + name: 'Midnight', desc: 'Deep purples & indigos', + preview: ['#7c3aed', '#1e1045', '#f5f3ff', '#a78bfa', '#15803d'], + light: { + accent: '#7c3aed', navBg: '#1e1045', navText: '#ffffff', background: '#f5f3ff', text: '#1a1040', + statusGreen: '#15803d', statusYellow: '#a16207', statusRed: '#dc2626', + accentHover: '#8b5cf6', navBg2: '#2e1065', navTextMuted: '#c4b5fd', textMuted: '#5b5075', border: '#d8d0e8', + surface1: '#ffffff', surface2: '#ede9fe', cardBg: '#ffffff', contentBg: '#f5f3ff', + detailBg: '#ffffff', inputBg: '#ffffff', rowStripe: '#faf8ff', rowHover: '#ede9fe', selectedBg: '#ddd6fe', + }, + dark: { + accent: '#a78bfa', navBg: '#1e1045', navText: '#ffffff', background: '#0f0a24', text: '#e2ddf0', + statusGreen: '#4ade80', statusYellow: '#facc15', statusRed: '#f87171', + accentHover: '#c4b5fd', navBg2: '#2e1065', navTextMuted: '#9d8abf', textMuted: '#9a90b0', border: '#352a55', + surface1: '#1a1338', surface2: '#221a48', cardBg: '#1a1338', contentBg: '#0f0a24', + detailBg: '#221a48', inputBg: '#1a1338', rowStripe: '#1a1338', rowHover: '#2a2050', selectedBg: '#352a6a', + } + }, + ember: { + name: 'Ember', desc: 'Warm red/orange, cyberpunk', + preview: ['#dc2626', '#1a0a0a', '#faf5f5', '#ef4444', '#15803d'], + light: { + accent: '#dc2626', navBg: '#1a0a0a', navText: '#ffffff', background: '#faf5f5', text: '#1a0a0a', + statusGreen: '#15803d', statusYellow: '#a16207', statusRed: '#dc2626', + accentHover: '#ef4444', navBg2: '#2a1010', navTextMuted: '#f0a0a0', textMuted: '#6b4444', border: '#e0c8c8', + surface1: '#ffffff', surface2: '#faf0f0', cardBg: '#ffffff', contentBg: '#faf5f5', + detailBg: '#ffffff', inputBg: '#ffffff', rowStripe: '#fdf8f8', rowHover: '#fce8e8', selectedBg: '#fecaca', + }, + dark: { + accent: '#ef4444', navBg: '#1a0505', navText: '#ffffff', background: '#0d0505', text: '#f0dada', + statusGreen: '#4ade80', statusYellow: '#facc15', statusRed: '#f87171', + accentHover: '#f87171', navBg2: '#2a0a0a', navTextMuted: '#c07070', textMuted: '#b09090', border: '#4a2020', + surface1: '#1a0d0d', surface2: '#261414', cardBg: '#1a0d0d', contentBg: '#0d0505', + detailBg: '#261414', inputBg: '#1a0d0d', rowStripe: '#1a0d0d', rowHover: '#301818', selectedBg: '#4a1a1a', + } + } + }; + + function detectActivePreset() { + for (var id in PRESETS) { + var p = PRESETS[id]; + var match = true; + for (var i = 0; i < THEME_COLOR_KEYS.length; i++) { + var k = THEME_COLOR_KEYS[i]; + if (state.theme[k] !== p.light[k] || state.themeDark[k] !== p.dark[k]) { match = false; break; } + } + if (match && p.nodeColors) { + for (var nk in p.nodeColors) { if (state.nodeColors[nk] !== p.nodeColors[nk]) { match = false; break; } } + } + if (match && p.typeColors) { + for (var tk in p.typeColors) { if (state.typeColors[tk] !== p.typeColors[tk]) { match = false; break; } } + } + if (match) return id; + } + return null; + } + + function renderPresets(container) { + var active = detectActivePreset(); + var html = '
' + + '

Theme Presets

' + + '
'; + for (var id in PRESETS) { + var p = PRESETS[id]; + var isActive = id === active; + var dots = ''; + for (var di = 0; di < p.preview.length; di++) { + dots += ''; + } + html += ''; + } + html += '
'; + return html; + } + + function applyPreset(id, container) { + var p = PRESETS[id]; + if (!p) return; + // Apply light theme colors + for (var i = 0; i < THEME_COLOR_KEYS.length; i++) { + var k = THEME_COLOR_KEYS[i]; + state.theme[k] = p.light[k]; + state.themeDark[k] = p.dark[k]; + } + // Apply node/type colors + if (p.nodeColors) { + Object.assign(state.nodeColors, p.nodeColors); + if (window.ROLE_COLORS) Object.assign(window.ROLE_COLORS, p.nodeColors); + if (window.ROLE_STYLE) { + for (var role in p.nodeColors) { + if (window.ROLE_STYLE[role]) window.ROLE_STYLE[role].color = p.nodeColors[role]; + } + } + } else { + // Reset to defaults + Object.assign(state.nodeColors, DEFAULTS.nodeColors); + if (window.ROLE_COLORS) Object.assign(window.ROLE_COLORS, DEFAULTS.nodeColors); + } + if (p.typeColors) { + Object.assign(state.typeColors, p.typeColors); + if (window.TYPE_COLORS) Object.assign(window.TYPE_COLORS, p.typeColors); + } else { + Object.assign(state.typeColors, DEFAULTS.typeColors); + if (window.TYPE_COLORS) Object.assign(window.TYPE_COLORS, DEFAULTS.typeColors); + } + applyThemePreview(); + if (window.syncBadgeColors) window.syncBadgeColors(); + window.dispatchEvent(new CustomEvent('theme-changed')); + autoSave(); + render(container); + } + const BASIC_KEYS = ['accent', 'navBg', 'navText', 'background', 'text', 'statusGreen', 'statusYellow', 'statusRed']; const ADVANCED_KEYS = ['accentHover', 'navBg2', 'navTextMuted', 'textMuted', 'border', 'surface1', 'surface2', 'cardBg', 'contentBg', 'detailBg', 'inputBg', 'rowStripe', 'rowHover', 'selectedBg']; const FONT_KEYS = ['font', 'mono']; @@ -441,6 +680,7 @@ } return '
' + + renderPresets() + '

' + modeLabel + '

' + '

Toggle ☀️/🌙 in nav to edit the other mode.

' + basicRows + @@ -635,6 +875,13 @@ }); }); + // Preset buttons + container.querySelectorAll('.cust-preset-btn').forEach(function (btn) { + btn.addEventListener('click', function () { + applyPreset(btn.dataset.preset, container); + }); + }); + // Text inputs (branding + home hero) container.querySelectorAll('input[data-key]').forEach(function (inp) { inp.addEventListener('input', function () {