/* === MeshCore Analyzer β€” customize.js === */ /* Tools β†’ Customization: visual config builder with live preview & JSON export */ 'use strict'; (function () { let styleEl = null; let originalValues = {}; let activeTab = 'branding'; const DEFAULTS = { branding: { siteName: 'MeshCore Analyzer', tagline: 'Real-time MeshCore LoRa mesh network analyzer', logoUrl: '', faviconUrl: '' }, theme: { 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', font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', mono: '"SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace', }, themeDark: { 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', font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', mono: '"SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace', }, nodeColors: { repeater: '#dc2626', companion: '#2563eb', room: '#16a34a', sensor: '#d97706', observer: '#8b5cf6' }, typeColors: { ADVERT: '#22c55e', GRP_TXT: '#3b82f6', TXT_MSG: '#f59e0b', ACK: '#6b7280', REQUEST: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6', ANON_REQ: '#f43f5e' }, home: { heroTitle: 'MeshCore Analyzer', heroSubtitle: 'Find your nodes to start monitoring them.', steps: [ { emoji: 'πŸ’¬', title: 'Join the Bay Area MeshCore Discord', description: 'The community Discord is the best place to get help and find local mesh enthusiasts.' }, { emoji: 'πŸ”΅', title: 'Connect via Bluetooth', description: 'Flash BLE companion firmware and pair with your device.' }, { emoji: 'πŸ“»', title: 'Set the right frequency preset', description: 'Match the frequency preset used by your local mesh community.' }, { emoji: 'πŸ“‘', title: 'Advertise yourself', description: 'Send an ADVERT so repeaters and observers can see you.' }, { emoji: 'πŸ”', title: 'Check "Heard N repeats"', description: 'Verify your node is being relayed through the mesh.' }, { emoji: 'πŸ“', title: 'Repeaters near you?', description: 'Check the map for nearby repeaters and coverage.' } ], checklist: [], footerLinks: [ { label: 'πŸ“¦ Packets', url: '#/packets' }, { label: 'πŸ—ΊοΈ Network Map', url: '#/map' }, { label: 'πŸ”΄ Live', url: '#/live' }, { label: 'πŸ“‘ All Nodes', url: '#/nodes' }, { label: 'πŸ’¬ Channels', url: '#/channels' } ] } }; // CSS variable name β†’ theme key mapping const THEME_CSS_MAP = { // Basic accent: '--accent', navBg: '--nav-bg', navText: '--nav-text', background: '--surface-0', text: '--text', statusGreen: '--status-green', statusYellow: '--status-yellow', statusRed: '--status-red', // Advanced (derived from basic by default) accentHover: '--accent-hover', navBg2: '--nav-bg2', navTextMuted: '--nav-text-muted', textMuted: '--text-muted', border: '--border', surface1: '--surface-1', surface2: '--surface-2', cardBg: '--card-bg', contentBg: '--content-bg', detailBg: '--detail-bg', inputBg: '--input-bg', rowStripe: '--row-stripe', rowHover: '--row-hover', selectedBg: '--selected-bg', font: '--font', 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']; const THEME_LABELS = { accent: 'Brand Color', navBg: 'Navigation', navText: 'Nav Text', background: 'Background', text: 'Text', statusGreen: 'Healthy', statusYellow: 'Warning', statusRed: 'Error', accentHover: 'Accent Hover', navBg2: 'Nav Gradient End', navTextMuted: 'Nav Muted Text', textMuted: 'Muted Text', border: 'Borders', surface1: 'Cards', surface2: 'Panels', cardBg: 'Card Fill', contentBg: 'Content Area', detailBg: 'Detail Panels', inputBg: 'Inputs', rowStripe: 'Table Stripe', rowHover: 'Row Hover', selectedBg: 'Selected', font: 'Body Font', mono: 'Mono Font', }; const THEME_HINTS = { accent: 'Buttons, links, active tabs, badges, charts β€” your primary brand color', navBg: 'Top navigation bar', navText: 'Nav bar text, links, brand name, buttons', background: 'Main page background', text: 'Primary text β€” muted text auto-derives', statusGreen: 'Healthy/online indicators', statusYellow: 'Warning/degraded + hop conflicts', statusRed: 'Error/offline indicators', accentHover: 'Hover state for accent elements', navBg2: 'Darker end of nav gradient', navTextMuted: 'Inactive nav links, nav buttons', textMuted: 'Labels, timestamps, secondary text', border: 'Dividers, table borders, card borders', surface1: 'Card and panel backgrounds', surface2: 'Nested surfaces, secondary panels', cardBg: 'Detail panels, modals', contentBg: 'Content area behind cards', detailBg: 'Modal, packet detail, side panels', inputBg: 'Text inputs, dropdowns', rowStripe: 'Alternating table rows', rowHover: 'Table row hover', selectedBg: 'Selected/active rows', font: 'System font stack for body text', mono: 'Monospace font for hex, code, hashes', }; const NODE_LABELS = { repeater: 'Repeater', companion: 'Companion', room: 'Room Server', sensor: 'Sensor', observer: 'Observer' }; const NODE_HINTS = { repeater: 'Infrastructure nodes that relay packets β€” map markers, packet path badges, node list', companion: 'End-user devices β€” map markers, packet detail, node list', room: 'Room/chat server nodes β€” map markers, node list', sensor: 'Sensor/telemetry nodes β€” map markers, node list', observer: 'MQTT observer stations β€” map markers (purple stars), observer list, packet headers' }; const NODE_EMOJI = { repeater: 'β—†', companion: '●', room: 'β– ', sensor: 'β–²', observer: 'β˜…' }; const TYPE_LABELS = { ADVERT: 'Advertisement', GRP_TXT: 'Channel Message', TXT_MSG: 'Direct Message', ACK: 'Acknowledgment', REQUEST: 'Request', RESPONSE: 'Response', TRACE: 'Traceroute', PATH: 'Path', ANON_REQ: 'Anonymous Request' }; const TYPE_HINTS = { ADVERT: 'Node advertisements β€” map, feed, packet list', GRP_TXT: 'Group/channel messages β€” map, feed, channels', TXT_MSG: 'Direct messages β€” map, feed', ACK: 'Acknowledgments β€” packet list', REQUEST: 'Requests β€” packet list, feed', RESPONSE: 'Responses β€” packet list', TRACE: 'Traceroute β€” map, traces page', PATH: 'Path packets β€” packet list', ANON_REQ: 'Encrypted anonymous requests β€” sender identity hidden via ephemeral key' }; const TYPE_EMOJI = { ADVERT: 'πŸ“‘', GRP_TXT: 'πŸ’¬', TXT_MSG: 'βœ‰οΈ', ACK: 'βœ“', REQUEST: '❓', RESPONSE: 'πŸ“¨', TRACE: 'πŸ”', PATH: 'πŸ›€οΈ', ANON_REQ: 'πŸ•΅οΈ' }; // Current state let state = {}; function deepClone(o) { return JSON.parse(JSON.stringify(o)); } function initState() { const cfg = window.SITE_CONFIG || {}; // Merge: DEFAULTS β†’ server config β†’ localStorage saved values var local = {}; try { var s = localStorage.getItem('meshcore-user-theme'); if (s) local = JSON.parse(s); } catch {} state = { branding: Object.assign({}, DEFAULTS.branding, cfg.branding || {}, local.branding || {}), theme: Object.assign({}, DEFAULTS.theme, cfg.theme || {}, local.theme || {}), themeDark: Object.assign({}, DEFAULTS.themeDark, cfg.themeDark || {}, local.themeDark || {}), nodeColors: Object.assign({}, DEFAULTS.nodeColors, cfg.nodeColors || {}, local.nodeColors || {}), typeColors: Object.assign({}, DEFAULTS.typeColors, cfg.typeColors || {}, local.typeColors || {}), home: { heroTitle: (local.home && local.home.heroTitle) || (cfg.home && cfg.home.heroTitle) || DEFAULTS.home.heroTitle, heroSubtitle: (local.home && local.home.heroSubtitle) || (cfg.home && cfg.home.heroSubtitle) || DEFAULTS.home.heroSubtitle, steps: deepClone((local.home && local.home.steps) || (cfg.home && cfg.home.steps) || DEFAULTS.home.steps), checklist: deepClone((local.home && local.home.checklist) || (cfg.home && cfg.home.checklist) || DEFAULTS.home.checklist), footerLinks: deepClone((local.home && local.home.footerLinks) || (cfg.home && cfg.home.footerLinks) || DEFAULTS.home.footerLinks) } }; } 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 = {}; for (var key in THEME_CSS_MAP) { originalValues[key] = cs.getPropertyValue(THEME_CSS_MAP[key]).trim(); } } function applyThemePreview() { var t = activeTheme(); for (var key in THEME_CSS_MAP) { if (t[key]) document.documentElement.style.setProperty(THEME_CSS_MAP[key], t[key]); } // Derived vars that reference other vars β€” need explicit override if (t.background) { document.documentElement.style.setProperty('--content-bg', t.background); } if (t.surface1) { document.documentElement.style.setProperty('--card-bg', t.surface1); } // Force nav bar to re-render gradient var nav = document.querySelector('.top-nav'); if (nav) { nav.style.background = 'none'; void nav.offsetHeight; nav.style.background = ''; } // Sync badge CSS from TYPE_COLORS if (window.syncBadgeColors) window.syncBadgeColors(); } function applyTypeColorCSS() { if (window.syncBadgeColors) window.syncBadgeColors(); } // Auto-save to localStorage on every change let _autoSaveTimer = null; function autoSave() { if (_autoSaveTimer) clearTimeout(_autoSaveTimer); _autoSaveTimer = setTimeout(function() { _autoSaveTimer = null; try { var data = buildExport(); localStorage.setItem('meshcore-user-theme', JSON.stringify(data)); // Sync to SITE_CONFIG so live pages (home, etc.) pick up changes if (window.SITE_CONFIG) { if (state.branding) window.SITE_CONFIG.branding = Object.assign(window.SITE_CONFIG.branding || {}, state.branding); if (state.home) window.SITE_CONFIG.home = deepClone(state.home); } // Re-render current page to reflect home/branding changes window.dispatchEvent(new HashChangeEvent('hashchange')); } catch (e) { console.error('[customize] autoSave error:', e); } }, 500); } function resetPreview() { for (var key in THEME_CSS_MAP) { document.documentElement.style.removeProperty(THEME_CSS_MAP[key]); } } function esc(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } function escAttr(s) { return (s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/ div:first-child { min-width: 160px; flex: 1; } .cust-color-row label { font-size: 12px; font-weight: 600; margin: 0; display: block; } .cust-hint { font-size: 10px; color: var(--text-muted); margin-top: 1px; line-height: 1.2; } .cust-color-row input[type="color"] { width: 40px; height: 32px; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; padding: 2px; background: var(--input-bg); } .cust-color-row .cust-hex { font-family: var(--mono); font-size: 12px; color: var(--text-muted); min-width: 70px; } .cust-color-row .cust-reset-btn { font-size: 11px; padding: 2px 8px; border: 1px solid var(--border); border-radius: 4px; background: var(--surface-2); color: var(--text-muted); cursor: pointer; } .cust-color-row .cust-reset-btn:hover { background: var(--surface-3); } .cust-node-dot { display: inline-block; width: 16px; height: 16px; border-radius: 50%; vertical-align: middle; } .cust-preview-img { max-width: 200px; max-height: 60px; margin-top: 6px; border-radius: 6px; border: 1px solid var(--border); } .cust-list-item { display: flex; flex-direction: column; gap: 4px; margin-bottom: 8px; padding: 8px; background: var(--surface-1); border: 1px solid var(--border); border-radius: 6px; } .cust-list-row { display: flex; gap: 6px; align-items: center; } .cust-list-item input { flex: 1; padding: 5px 8px; border: 1px solid var(--border); border-radius: 4px; font-size: 12px; background: var(--input-bg); color: var(--text); min-width: 0; } .cust-list-item textarea { width: 100%; padding: 5px 8px; border: 1px solid var(--border); border-radius: 4px; font-size: 11px; font-family: var(--mono); background: var(--input-bg); color: var(--text); resize: vertical; box-sizing: border-box; } .cust-list-item textarea:focus, .cust-list-item input:focus { outline: none; border-color: var(--accent); } .cust-md-hint { font-size: 9px; color: var(--text-muted); margin-top: 2px; } .cust-md-hint code { background: var(--surface-2); padding: 0 3px; border-radius: 2px; font-size: 9px; } .cust-list-item .cust-emoji-input { max-width: 40px; text-align: center; flex: 0 0 40px; } .cust-list-btn { padding: 4px 10px; border: 1px solid var(--border); border-radius: 4px; background: var(--surface-2); color: var(--text-muted); cursor: pointer; font-size: 12px; } .cust-list-btn:hover { background: var(--surface-3); } .cust-list-btn.danger { color: #ef4444; } .cust-list-btn.danger:hover { background: #fef2f2; } .cust-add-btn { display: inline-flex; align-items: center; gap: 4px; padding: 6px 14px; border: 1px dashed var(--border); border-radius: 6px; background: none; color: var(--accent); cursor: pointer; font-size: 13px; margin-top: 4px; } .cust-add-btn:hover { background: var(--hover-bg); } .cust-export-area { width: 100%; min-height: 300px; font-family: var(--mono); font-size: 12px; background: var(--surface-1); border: 1px solid var(--border); border-radius: 6px; padding: 12px; color: var(--text); resize: vertical; box-sizing: border-box; } .cust-export-btns { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; } .cust-export-btns button { padding: 6px 14px; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500; } .cust-copy-btn { background: var(--accent); color: #fff; } .cust-copy-btn:hover { opacity: 0.9; } .cust-dl-btn { background: var(--surface-2); color: var(--text); border: 1px solid var(--border) !important; } .cust-save-user { background: #22c55e; color: #fff; } .cust-save-user:hover { background: #16a34a; } .cust-reset-user { background: var(--surface-2); color: #ef4444; border: 1px solid #ef4444 !important; } .cust-reset-user:hover { background: #ef4444; color: #fff; } .cust-dl-btn:hover { background: var(--surface-3); } .cust-reset-preview { margin-top: 12px; padding: 8px 16px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-2); color: var(--text); cursor: pointer; font-size: 13px; } .cust-reset-preview:hover { background: var(--surface-3); } .cust-instructions { background: var(--surface-1); border: 1px solid var(--border); border-radius: 6px; padding: 12px 16px; margin-top: 16px; font-size: 13px; color: var(--text-muted); line-height: 1.6; } .cust-instructions code { background: var(--surface-2); padding: 2px 6px; border-radius: 3px; font-family: var(--mono); font-size: 12px; } .cust-section-title { font-size: 16px; font-weight: 600; margin: 0 0 12px; } @media (max-width: 600px) { .cust-overlay { left: 8px; right: 8px; width: auto; top: 56px; } .cust-tabs { gap: 0; } .cust-tab { padding: 6px 8px; font-size: 11px; } .cust-color-row > div:first-child { min-width: 120px; } .cust-list-item { flex-wrap: wrap; } } `; document.head.appendChild(styleEl); } function removeStyles() { if (styleEl) { styleEl.remove(); styleEl = null; } } function renderTabs() { var tabs = [ { id: 'branding', label: '🏷️', title: 'Branding' }, { id: 'theme', label: '🎨', title: 'Theme Colors' }, { id: 'nodes', label: '🎯', title: 'Colors' }, { id: 'home', label: '🏠', title: 'Home Page' }, { id: 'export', label: 'πŸ“€', title: 'Export / Save' } ]; return '
' + tabs.map(function (t) { return ''; }).join('') + '
'; } function renderBranding() { var b = state.branding; var logoPreview = b.logoUrl ? 'Logo preview' : ''; return '
' + '
' + '
' + '
' + logoPreview + '
' + '
' + '
'; } function renderColorRow(key, val, def, dataAttr) { var isFont = key === 'font' || key === 'mono'; var inputHtml = isFont ? '' : '' + '' + val + ''; return '
' + '
' + '
' + (THEME_HINTS[key] || '') + '
' + inputHtml + (val !== def ? '' : '') + '
'; } function renderTheme() { var dark = isDarkMode(); var modeLabel = dark ? 'πŸŒ™ Dark Mode' : 'β˜€οΈ Light Mode'; var defs = activeDefaults(); var current = activeTheme(); var basicRows = ''; for (var i = 0; i < BASIC_KEYS.length; i++) { var key = BASIC_KEYS[i]; basicRows += renderColorRow(key, current[key] || defs[key] || '#000000', defs[key] || '#000000', 'theme'); } var advancedRows = ''; for (var j = 0; j < ADVANCED_KEYS.length; j++) { var akey = ADVANCED_KEYS[j]; advancedRows += renderColorRow(akey, current[akey] || defs[akey] || '#000000', defs[akey] || '#000000', 'theme'); } var fontRows = ''; for (var f = 0; f < FONT_KEYS.length; f++) { var fkey = FONT_KEYS[f]; fontRows += renderColorRow(fkey, current[fkey] || defs[fkey] || '', defs[fkey] || '', 'theme'); } return '
' + renderPresets() + '

' + modeLabel + '

' + '

Toggle β˜€οΈ/πŸŒ™ in nav to edit the other mode.

' + basicRows + '
Advanced (' + ADVANCED_KEYS.length + ' options)' + advancedRows + '
' + '
Fonts' + fontRows + '
' + '' + '
'; } function renderNodes() { var rows = ''; for (var key in NODE_LABELS) { var val = state.nodeColors[key]; var def = DEFAULTS.nodeColors[key]; rows += '
' + '
' + '
' + (NODE_HINTS[key] || '') + '
' + '' + '' + '' + val + '' + (val !== def ? '' : '') + '
'; } var typeRows = ''; for (var tkey in TYPE_LABELS) { var tval = state.typeColors[tkey]; var tdef = DEFAULTS.typeColors[tkey]; typeRows += '
' + '
' + '
' + (TYPE_HINTS[tkey] || '') + '
' + '' + '' + '' + tval + '' + (tval !== tdef ? '' : '') + '
'; } var heatOpacity = parseFloat(localStorage.getItem('meshcore-heatmap-opacity')); if (isNaN(heatOpacity)) heatOpacity = 0.25; var heatPct = Math.round(heatOpacity * 100); var liveHeatOpacity = parseFloat(localStorage.getItem('meshcore-live-heatmap-opacity')); if (isNaN(liveHeatOpacity)) liveHeatOpacity = 0.3; var liveHeatPct = Math.round(liveHeatOpacity * 100); return '
' + '

Node Role Colors

' + rows + '
' + '

Packet Type Colors

' + typeRows + '
' + '

Heatmap Opacity

' + '
' + '
' + '
Heatmap overlay on the Nodes β†’ Map page (0–100%)
' + '' + '' + heatPct + '%' + '
' + '
' + '
' + '
Heatmap overlay on the Live page (0–100%)
' + '' + '' + liveHeatPct + '%' + '
' + '
'; } function renderHome() { var h = state.home; var stepsHtml = h.steps.map(function (s, i) { return '
' + '
' + '' + '' + '' + '' + '' + '
' + '' + '
Markdown: **bold** *italic* `code` [text](url) - list
' + '
'; }).join(''); var checkHtml = h.checklist.map(function (c, i) { return '
' + '
' + '' + '' + '
' + '' + '
Markdown: **bold** *italic* `code` [text](url) - list
' + '
'; }).join(''); var linksHtml = h.footerLinks.map(function (l, i) { return '
' + '
' + '' + '' + '
' + '' + '
'; }).join(''); return '
' + '
' + '
' + '

Steps

' + stepsHtml + '' + '

FAQ / Checklist

' + checkHtml + '' + '

Footer Links

' + linksHtml + '' + '
'; } function buildExport() { var out = {}; // Branding β€” only changed values var bd = {}; for (var bk in DEFAULTS.branding) { if (state.branding[bk] && state.branding[bk] !== DEFAULTS.branding[bk]) bd[bk] = state.branding[bk]; } if (Object.keys(bd).length) out.branding = bd; // Theme var th = {}; for (var tk in DEFAULTS.theme) { if (state.theme[tk] !== DEFAULTS.theme[tk]) th[tk] = state.theme[tk]; } 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) { if (state.nodeColors[nk] !== DEFAULTS.nodeColors[nk]) nc[nk] = state.nodeColors[nk]; } if (Object.keys(nc).length) out.nodeColors = nc; // Packet type colors var tc = {}; for (var tck in DEFAULTS.typeColors) { if (state.typeColors[tck] !== DEFAULTS.typeColors[tck]) tc[tck] = state.typeColors[tck]; } if (Object.keys(tc).length) out.typeColors = tc; // Home var hm = {}; if (state.home.heroTitle !== DEFAULTS.home.heroTitle) hm.heroTitle = state.home.heroTitle; if (state.home.heroSubtitle !== DEFAULTS.home.heroSubtitle) hm.heroSubtitle = state.home.heroSubtitle; if (JSON.stringify(state.home.steps) !== JSON.stringify(DEFAULTS.home.steps)) hm.steps = state.home.steps; if (JSON.stringify(state.home.checklist) !== JSON.stringify(DEFAULTS.home.checklist)) hm.checklist = state.home.checklist; if (JSON.stringify(state.home.footerLinks) !== JSON.stringify(DEFAULTS.home.footerLinks)) hm.footerLinks = state.home.footerLinks; if (Object.keys(hm).length) out.home = hm; return out; } function renderExport() { var json = JSON.stringify(buildExport(), null, 2); var hasUserTheme = !!localStorage.getItem('meshcore-user-theme'); return '
' + '

My Preferences

' + '

Save these colors just for you β€” stored in your browser, works on any instance.

' + '
' + '' + (hasUserTheme ? '' : '') + '
' + '
' + '

Admin

' + '

Download or import a theme file. Admins place it as theme.json next to the server.

' + '
' + '' + '' + '' + '' + '
' + '
Raw JSON' + '' + '
' + '
'; } let panelEl = null; function render(container) { container.innerHTML = renderTabs() + '
' + renderBranding() + renderTheme() + renderNodes() + renderHome() + renderExport() + '
'; bindEvents(container); } function bindEvents(container) { // Tab switching container.querySelectorAll('.cust-tab').forEach(function (btn) { btn.addEventListener('click', function () { activeTab = btn.dataset.tab; render(container); }); }); // 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 () { var parts = inp.dataset.key.split('.'); if (parts.length === 2) { state[parts[0]][parts[1]] = inp.value; autoSave(); } // Live DOM updates for branding if (inp.dataset.key === 'branding.siteName') { var brandEl = document.querySelector('.brand-text'); if (brandEl) brandEl.textContent = inp.value; document.title = inp.value; } if (inp.dataset.key === 'branding.logoUrl') { var iconEl = document.querySelector('.brand-icon'); if (iconEl) { if (inp.value) { iconEl.innerHTML = ''; } else { iconEl.textContent = 'πŸ“‘'; } } } if (inp.dataset.key === 'branding.faviconUrl') { var link = document.querySelector('link[rel="icon"]'); if (link && inp.value) link.href = inp.value; } }); }); // Theme color pickers container.querySelectorAll('input[data-theme]').forEach(function (inp) { inp.addEventListener('input', function () { var key = inp.dataset.theme; var themeKey = isDarkMode() ? 'themeDark' : 'theme'; state[themeKey][key] = inp.value; var hex = container.querySelector('[data-hex="' + key + '"]'); if (hex) hex.textContent = inp.value; applyThemePreview(); autoSave(); }); }); // Theme reset buttons container.querySelectorAll('[data-reset-theme]').forEach(function (btn) { btn.addEventListener('click', function () { var key = btn.dataset.resetTheme; var themeKey = isDarkMode() ? 'themeDark' : 'theme'; state[themeKey][key] = activeDefaults()[key]; applyThemePreview(); autoSave(); render(container); }); }); // Reset preview button var resetBtn = document.getElementById('custResetPreview'); if (resetBtn) { resetBtn.addEventListener('click', function () { state.theme = Object.assign({}, DEFAULTS.theme); resetPreview(); render(container); }); } // Node color pickers container.querySelectorAll('input[data-node]').forEach(function (inp) { inp.addEventListener('input', function () { var key = inp.dataset.node; state.nodeColors[key] = inp.value; // Sync to global role colors used by map/packets/etc if (window.ROLE_COLORS) window.ROLE_COLORS[key] = inp.value; if (window.ROLE_STYLE && window.ROLE_STYLE[key]) window.ROLE_STYLE[key].color = inp.value; // Trigger re-render of current page window.dispatchEvent(new CustomEvent('theme-changed')); autoSave(); var dot = container.querySelector('[data-dot="' + key + '"]'); if (dot) dot.style.background = inp.value; var hex = container.querySelector('[data-nhex="' + key + '"]'); if (hex) hex.textContent = inp.value; }); }); // Node reset buttons container.querySelectorAll('[data-reset-node]').forEach(function (btn) { btn.addEventListener('click', function () { var key = btn.dataset.resetNode; state.nodeColors[key] = DEFAULTS.nodeColors[key]; if (window.ROLE_COLORS) window.ROLE_COLORS[key] = DEFAULTS.nodeColors[key]; if (window.ROLE_STYLE && window.ROLE_STYLE[key]) window.ROLE_STYLE[key].color = DEFAULTS.nodeColors[key]; render(container); }); }); // Packet type color pickers container.querySelectorAll('input[data-type-color]').forEach(function (inp) { inp.addEventListener('input', function () { var key = inp.dataset.typeColor; state.typeColors[key] = inp.value; if (window.TYPE_COLORS) window.TYPE_COLORS[key] = inp.value; if (window.syncBadgeColors) window.syncBadgeColors(); window.dispatchEvent(new CustomEvent('theme-changed')); autoSave(); var dot = container.querySelector('[data-tdot="' + key + '"]'); if (dot) dot.style.background = inp.value; var hex = container.querySelector('[data-thex="' + key + '"]'); if (hex) hex.textContent = inp.value; }); }); container.querySelectorAll('[data-reset-type]').forEach(function (btn) { btn.addEventListener('click', function () { var key = btn.dataset.resetType; state.typeColors[key] = DEFAULTS.typeColors[key]; if (window.TYPE_COLORS) window.TYPE_COLORS[key] = DEFAULTS.typeColors[key]; render(container); }); }); // Heatmap opacity slider var heatSlider = container.querySelector('#custHeatOpacity'); if (heatSlider) { heatSlider.addEventListener('input', function () { var pct = parseInt(heatSlider.value); var label = container.querySelector('#custHeatOpacityVal'); if (label) label.textContent = pct + '%'; var opacity = pct / 100; localStorage.setItem('meshcore-heatmap-opacity', opacity); // Live-update the heatmap if visible β€” set canvas opacity for whole layer if (window._meshcoreHeatLayer) { var canvas = window._meshcoreHeatLayer._canvas || (window._meshcoreHeatLayer.getContainer && window._meshcoreHeatLayer.getContainer()); if (canvas) canvas.style.opacity = opacity; } }); } // Live heatmap opacity slider var liveHeatSlider = container.querySelector('#custLiveHeatOpacity'); if (liveHeatSlider) { liveHeatSlider.addEventListener('input', function () { var pct = parseInt(liveHeatSlider.value); var label = container.querySelector('#custLiveHeatOpacityVal'); if (label) label.textContent = pct + '%'; var opacity = pct / 100; localStorage.setItem('meshcore-live-heatmap-opacity', opacity); // Live-update the live page heatmap if visible if (window._meshcoreLiveHeatLayer) { var canvas = window._meshcoreLiveHeatLayer._canvas || (window._meshcoreLiveHeatLayer.getContainer && window._meshcoreLiveHeatLayer.getContainer()); if (canvas) canvas.style.opacity = opacity; } }); } // Steps container.querySelectorAll('[data-step-field]').forEach(function (inp) { inp.addEventListener('input', function () { var i = parseInt(inp.dataset.idx); state.home.steps[i][inp.dataset.stepField] = inp.value; autoSave(); }); }); container.querySelectorAll('[data-move-step]').forEach(function (btn) { btn.addEventListener('click', function () { var i = parseInt(btn.dataset.moveStep); var dir = btn.dataset.dir === 'up' ? -1 : 1; var j = i + dir; if (j < 0 || j >= state.home.steps.length) return; var tmp = state.home.steps[i]; state.home.steps[i] = state.home.steps[j]; state.home.steps[j] = tmp; render(container); }); }); container.querySelectorAll('[data-rm-step]').forEach(function (btn) { btn.addEventListener('click', function () { state.home.steps.splice(parseInt(btn.dataset.rmStep), 1); render(container); }); }); var addStepBtn = document.getElementById('addStep'); if (addStepBtn) addStepBtn.addEventListener('click', function () { state.home.steps.push({ emoji: 'πŸ“Œ', title: '', description: '' }); render(container); }); // Checklist container.querySelectorAll('[data-check-field]').forEach(function (inp) { inp.addEventListener('input', function () { var i = parseInt(inp.dataset.idx); state.home.checklist[i][inp.dataset.checkField] = inp.value; autoSave(); }); }); container.querySelectorAll('[data-rm-check]').forEach(function (btn) { btn.addEventListener('click', function () { state.home.checklist.splice(parseInt(btn.dataset.rmCheck), 1); render(container); }); }); var addCheckBtn = document.getElementById('addCheck'); if (addCheckBtn) addCheckBtn.addEventListener('click', function () { state.home.checklist.push({ question: '', answer: '' }); render(container); }); // Footer links container.querySelectorAll('[data-link-field]').forEach(function (inp) { inp.addEventListener('input', function () { var i = parseInt(inp.dataset.idx); state.home.footerLinks[i][inp.dataset.linkField] = inp.value; autoSave(); }); }); container.querySelectorAll('[data-rm-link]').forEach(function (btn) { btn.addEventListener('click', function () { state.home.footerLinks.splice(parseInt(btn.dataset.rmLink), 1); render(container); }); }); var addLinkBtn = document.getElementById('addLink'); if (addLinkBtn) addLinkBtn.addEventListener('click', function () { state.home.footerLinks.push({ label: '', url: '' }); render(container); }); // Export copy var copyBtn = document.getElementById('custCopy'); if (copyBtn) copyBtn.addEventListener('click', function () { var ta = document.getElementById('custExportJson'); if (ta) { navigator.clipboard.writeText(ta.value).then(function () { copyBtn.textContent = 'βœ“ Copied!'; setTimeout(function () { copyBtn.textContent = 'πŸ“‹ Copy to Clipboard'; }, 2000); }).catch(function () { ta.select(); document.execCommand('copy'); copyBtn.textContent = 'βœ“ Copied!'; setTimeout(function () { copyBtn.textContent = 'πŸ“‹ Copy to Clipboard'; }, 2000); }); } }); // Export download var dlBtn = document.getElementById('custDownload'); if (dlBtn) dlBtn.addEventListener('click', function () { var json = JSON.stringify(buildExport(), null, 2); var blob = new Blob([json], { type: 'application/json' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'config-theme.json'; a.click(); URL.revokeObjectURL(a.href); }); // Save user theme to localStorage var saveUserBtn = document.getElementById('custSaveUser'); if (saveUserBtn) saveUserBtn.addEventListener('click', function () { var exportData = buildExport(); localStorage.setItem('meshcore-user-theme', JSON.stringify(exportData)); saveUserBtn.textContent = 'βœ“ Saved!'; setTimeout(function () { saveUserBtn.textContent = 'πŸ’Ύ Save as my theme'; }, 2000); }); // Reset user theme var resetUserBtn = document.getElementById('custResetUser'); if (resetUserBtn) resetUserBtn.addEventListener('click', function () { localStorage.removeItem('meshcore-user-theme'); resetPreview(); initState(); render(container); applyThemePreview(); autoSave(); }); // Import from file var importBtn = document.getElementById('custImportFile'); var importInput = document.getElementById('custImportInput'); if (importBtn && importInput) { importBtn.addEventListener('click', function () { importInput.click(); }); importInput.addEventListener('change', function () { var file = importInput.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function () { try { var data = JSON.parse(reader.result); // Merge imported data into state if (data.branding) Object.assign(state.branding, data.branding); if (data.theme) Object.assign(state.theme, data.theme); if (data.themeDark) Object.assign(state.themeDark, data.themeDark); if (data.nodeColors) { Object.assign(state.nodeColors, data.nodeColors); if (window.ROLE_COLORS) Object.assign(window.ROLE_COLORS, data.nodeColors); if (window.ROLE_STYLE) { for (var role in data.nodeColors) { if (window.ROLE_STYLE[role]) window.ROLE_STYLE[role].color = data.nodeColors[role]; } } } if (data.typeColors) { Object.assign(state.typeColors, data.typeColors); if (window.TYPE_COLORS) Object.assign(window.TYPE_COLORS, data.typeColors); } if (data.home) { if (data.home.heroTitle) state.home.heroTitle = data.home.heroTitle; if (data.home.heroSubtitle) state.home.heroSubtitle = data.home.heroSubtitle; if (data.home.steps) state.home.steps = deepClone(data.home.steps); if (data.home.checklist) state.home.checklist = deepClone(data.home.checklist); if (data.home.footerLinks) state.home.footerLinks = deepClone(data.home.footerLinks); } applyThemePreview(); autoSave(); window.dispatchEvent(new CustomEvent('theme-changed')); render(container); importBtn.textContent = 'βœ“ Imported!'; setTimeout(function () { importBtn.textContent = 'πŸ“‚ Import File'; }, 2000); } catch (e) { importBtn.textContent = 'βœ• Invalid JSON'; setTimeout(function () { importBtn.textContent = 'πŸ“‚ Import File'; }, 3000); } }; reader.readAsText(file); importInput.value = ''; }); } } function toggle() { if (panelEl) { panelEl.classList.toggle('hidden'); return; } // First open β€” create the panel injectStyles(); saveOriginalCSS(); initState(); panelEl = document.createElement('div'); panelEl.className = 'cust-overlay'; panelEl.innerHTML = '
' + '

🎨 Customize

' + '' + '
' + '
'; document.body.appendChild(panelEl); panelEl.querySelector('.cust-close').addEventListener('click', () => panelEl.classList.add('hidden')); // Drag support const header = panelEl.querySelector('.cust-header'); let dragX = 0, dragY = 0, startX = 0, startY = 0; header.addEventListener('mousedown', (e) => { if (e.target.closest('.cust-close')) return; dragX = panelEl.offsetLeft; dragY = panelEl.offsetTop; startX = e.clientX; startY = e.clientY; const onMove = (ev) => { panelEl.style.left = Math.max(0, dragX + ev.clientX - startX) + 'px'; panelEl.style.top = Math.max(56, dragY + ev.clientY - startY) + 'px'; panelEl.style.right = 'auto'; }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }); render(panelEl.querySelector('.cust-inner')); applyThemePreview(); autoSave(); } // Restore saved user theme IMMEDIATELY (before DOMContentLoaded, before map/app init) // roles.js has already loaded ROLE_COLORS, ROLE_STYLE, TYPE_COLORS at this point try { const saved = localStorage.getItem('meshcore-user-theme'); if (saved) { const userTheme = JSON.parse(saved); 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); } // Derived vars if (themeData.background) document.documentElement.style.setProperty('--content-bg', themeData.background); if (themeData.surface1) document.documentElement.style.setProperty('--card-bg', themeData.surface1); } if (userTheme.nodeColors) { if (window.ROLE_COLORS) Object.assign(window.ROLE_COLORS, userTheme.nodeColors); if (window.ROLE_STYLE) { for (const [role, color] of Object.entries(userTheme.nodeColors)) { if (window.ROLE_STYLE[role]) window.ROLE_STYLE[role].color = color; } } } if (userTheme.typeColors && window.TYPE_COLORS) { Object.assign(window.TYPE_COLORS, userTheme.typeColors); if (window.syncBadgeColors) window.syncBadgeColors(); } } } catch {} // Wire up toggle button (needs DOM) document.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('customizeToggle'); if (btn) btn.addEventListener('click', toggle); // Restore branding from localStorage (needs DOM elements to exist) try { const saved = localStorage.getItem('meshcore-user-theme'); if (saved) { const userTheme = JSON.parse(saved); if (userTheme.branding) { if (userTheme.branding.siteName) { const brandEl = document.querySelector('.brand-text'); if (brandEl) brandEl.textContent = userTheme.branding.siteName; document.title = userTheme.branding.siteName; } if (userTheme.branding.logoUrl) { const iconEl = document.querySelector('.brand-icon'); if (iconEl) iconEl.innerHTML = ''; } if (userTheme.branding.faviconUrl) { const link = document.querySelector('link[rel="icon"]'); if (link) link.href = userTheme.branding.faviconUrl; } } } } catch {} // Watch for dark/light mode toggle and re-apply theme preview new MutationObserver(function() { if (state.theme) applyThemePreview(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); }); })();