mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 18:15:47 +00:00
Each color picker now has a description underneath: - Accent: 'Active nav tab, buttons, links, selected rows, badges' - Status Green: 'Healthy nodes, online indicators, good SNR' - Repeater: 'Infrastructure nodes — map markers, packet path badges' etc.
579 lines
26 KiB
JavaScript
579 lines
26 KiB
JavaScript
/* === 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',
|
|
accentHover: '#6db3ff',
|
|
navBg: '#0f0f23',
|
|
navBg2: '#1a1a2e',
|
|
statusGreen: '#45644c',
|
|
statusYellow: '#b08b2d',
|
|
statusRed: '#b54a4a'
|
|
},
|
|
nodeColors: {
|
|
repeater: '#dc2626',
|
|
companion: '#2563eb',
|
|
room: '#16a34a',
|
|
sensor: '#d97706',
|
|
observer: '#8b5cf6'
|
|
},
|
|
home: {
|
|
heroTitle: 'MeshCore Analyzer',
|
|
heroSubtitle: 'Find your nodes to start monitoring them.',
|
|
steps: [
|
|
{ emoji: '📡', title: 'Connect', description: 'Link your node to the mesh' },
|
|
{ emoji: '🔍', title: 'Monitor', description: 'Watch packets flow in real-time' },
|
|
{ emoji: '📊', title: 'Analyze', description: "Understand your network's health" }
|
|
],
|
|
checklist: [
|
|
{ question: 'How do I add my node?', answer: 'Search for your node name or paste your public key.' },
|
|
{ question: 'What regions are covered?', answer: 'Check the map page to see active observers and nodes.' }
|
|
],
|
|
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 = {
|
|
accent: '--accent',
|
|
accentHover: '--accent-hover',
|
|
navBg: '--nav-bg',
|
|
navBg2: '--nav-bg2',
|
|
statusGreen: '--status-green',
|
|
statusYellow: '--status-yellow',
|
|
statusRed: '--status-red'
|
|
};
|
|
|
|
const THEME_LABELS = {
|
|
accent: 'Accent Color',
|
|
accentHover: 'Accent Hover',
|
|
navBg: 'Nav Background',
|
|
navBg2: 'Nav Background 2',
|
|
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'
|
|
};
|
|
|
|
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: '★' };
|
|
|
|
// Current state
|
|
let state = {};
|
|
|
|
function deepClone(o) { return JSON.parse(JSON.stringify(o)); }
|
|
|
|
function initState() {
|
|
const cfg = window.SITE_CONFIG || {};
|
|
state = {
|
|
branding: Object.assign({}, DEFAULTS.branding, cfg.branding || {}),
|
|
theme: Object.assign({}, DEFAULTS.theme, cfg.theme || {}),
|
|
nodeColors: Object.assign({}, DEFAULTS.nodeColors, cfg.nodeColors || {}),
|
|
home: {
|
|
heroTitle: (cfg.home && cfg.home.heroTitle) || DEFAULTS.home.heroTitle,
|
|
heroSubtitle: (cfg.home && cfg.home.heroSubtitle) || DEFAULTS.home.heroSubtitle,
|
|
steps: deepClone((cfg.home && cfg.home.steps) || DEFAULTS.home.steps),
|
|
checklist: deepClone((cfg.home && cfg.home.checklist) || DEFAULTS.home.checklist),
|
|
footerLinks: deepClone((cfg.home && cfg.home.footerLinks) || DEFAULTS.home.footerLinks)
|
|
}
|
|
};
|
|
}
|
|
|
|
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() {
|
|
for (var key in THEME_CSS_MAP) {
|
|
document.documentElement.style.setProperty(THEME_CSS_MAP[key], state.theme[key]);
|
|
}
|
|
}
|
|
|
|
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(/</g, '<'); }
|
|
|
|
function injectStyles() {
|
|
if (styleEl) return;
|
|
styleEl = document.createElement('style');
|
|
styleEl.textContent = `
|
|
.cust { max-width: 900px; margin: 0 auto; padding: 20px 16px; }
|
|
.cust h1 { margin: 0 0 4px; font-size: 24px; }
|
|
.cust-sub { color: var(--text-muted); margin: 0 0 20px; font-size: 14px; }
|
|
.cust-tabs { display: flex; gap: 4px; border-bottom: 2px solid var(--border); margin-bottom: 20px; overflow-x: auto; }
|
|
.cust-tab { padding: 8px 16px; cursor: pointer; border: none; background: none; color: var(--text-muted);
|
|
font-size: 14px; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -2px; white-space: nowrap; }
|
|
.cust-tab:hover { color: var(--text); }
|
|
.cust-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
.cust-panel { display: none; }
|
|
.cust-panel.active { display: block; }
|
|
.cust-field { margin-bottom: 16px; }
|
|
.cust-field label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: var(--text); }
|
|
.cust-field input[type="text"], .cust-field textarea { width: 100%; padding: 8px 10px; border: 1px solid var(--border);
|
|
border-radius: 6px; font-size: 14px; background: var(--input-bg); color: var(--text); box-sizing: border-box; }
|
|
.cust-field input[type="text"]:focus, .cust-field textarea:focus { outline: none; border-color: var(--accent); }
|
|
.cust-color-row { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
|
|
.cust-color-row > div:first-child { min-width: 220px; }
|
|
.cust-color-row label { font-size: 13px; font-weight: 600; margin: 0; display: block; }
|
|
.cust-hint { font-size: 11px; color: var(--text-muted); margin-top: 2px; line-height: 1.3; }
|
|
.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; gap: 8px; align-items: flex-start; margin-bottom: 8px; padding: 8px;
|
|
background: var(--surface-1); border: 1px solid var(--border); border-radius: 6px; }
|
|
.cust-list-item input { flex: 1; padding: 6px 8px; border: 1px solid var(--border); border-radius: 4px;
|
|
font-size: 13px; background: var(--input-bg); color: var(--text); }
|
|
.cust-list-item .cust-emoji-input { max-width: 50px; text-align: center; }
|
|
.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: 12px; }
|
|
.cust-export-btns button { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; 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-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 { padding: 12px 8px; }
|
|
.cust-tabs { gap: 0; }
|
|
.cust-tab { padding: 8px 10px; font-size: 12px; }
|
|
.cust-color-row label { min-width: 100px; }
|
|
.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: '🏷️ Branding' },
|
|
{ id: 'theme', label: '🎨 Theme' },
|
|
{ id: 'nodes', label: '📡 Nodes' },
|
|
{ id: 'home', label: '🏠 Home Page' },
|
|
{ id: 'export', label: '📤 Export' }
|
|
];
|
|
return '<div class="cust-tabs">' +
|
|
tabs.map(function (t) {
|
|
return '<button class="cust-tab' + (t.id === activeTab ? ' active' : '') + '" data-tab="' + t.id + '">' + t.label + '</button>';
|
|
}).join('') + '</div>';
|
|
}
|
|
|
|
function renderBranding() {
|
|
var b = state.branding;
|
|
var logoPreview = b.logoUrl ? '<img class="cust-preview-img" src="' + escAttr(b.logoUrl) + '" alt="Logo preview" onerror="this.style.display=\'none\'">' : '';
|
|
return '<div class="cust-panel' + (activeTab === 'branding' ? ' active' : '') + '" data-panel="branding">' +
|
|
'<div class="cust-field"><label>Site Name</label><input type="text" data-key="branding.siteName" value="' + escAttr(b.siteName) + '"></div>' +
|
|
'<div class="cust-field"><label>Tagline</label><input type="text" data-key="branding.tagline" value="' + escAttr(b.tagline) + '"></div>' +
|
|
'<div class="cust-field"><label>Logo URL</label><input type="text" data-key="branding.logoUrl" value="' + escAttr(b.logoUrl) + '" placeholder="https://...">' + logoPreview + '</div>' +
|
|
'<div class="cust-field"><label>Favicon URL</label><input type="text" data-key="branding.faviconUrl" value="' + escAttr(b.faviconUrl) + '" placeholder="https://..."></div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderTheme() {
|
|
var rows = '';
|
|
for (var key in THEME_LABELS) {
|
|
var val = state.theme[key];
|
|
var def = DEFAULTS.theme[key];
|
|
rows += '<div class="cust-color-row">' +
|
|
'<div><label>' + THEME_LABELS[key] + '</label>' +
|
|
'<div class="cust-hint">' + (THEME_HINTS[key] || '') + '</div></div>' +
|
|
'<input type="color" data-theme="' + key + '" value="' + val + '">' +
|
|
'<span class="cust-hex" data-hex="' + key + '">' + val + '</span>' +
|
|
(val !== def ? '<button class="cust-reset-btn" data-reset-theme="' + key + '">Reset</button>' : '') +
|
|
'</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>' +
|
|
rows +
|
|
'<button class="cust-reset-preview" id="custResetPreview">↩ Reset Preview</button>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderNodes() {
|
|
var rows = '';
|
|
for (var key in NODE_LABELS) {
|
|
var val = state.nodeColors[key];
|
|
var def = DEFAULTS.nodeColors[key];
|
|
rows += '<div class="cust-color-row">' +
|
|
'<div><label>' + NODE_EMOJI[key] + ' ' + NODE_LABELS[key] + '</label>' +
|
|
'<div class="cust-hint">' + (NODE_HINTS[key] || '') + '</div></div>' +
|
|
'<input type="color" data-node="' + key + '" value="' + val + '">' +
|
|
'<span class="cust-node-dot" style="background:' + val + '" data-dot="' + key + '"></span>' +
|
|
'<span class="cust-hex" data-nhex="' + key + '">' + val + '</span>' +
|
|
(val !== def ? '<button class="cust-reset-btn" data-reset-node="' + key + '">Reset</button>' : '') +
|
|
'</div>';
|
|
}
|
|
return '<div class="cust-panel' + (activeTab === 'nodes' ? ' active' : '') + '" data-panel="nodes">' +
|
|
'<p class="cust-section-title">Node Role Colors</p>' + rows + '</div>';
|
|
}
|
|
|
|
function renderHome() {
|
|
var h = state.home;
|
|
var stepsHtml = h.steps.map(function (s, i) {
|
|
return '<div class="cust-list-item" data-step="' + i + '">' +
|
|
'<input class="cust-emoji-input" data-step-field="emoji" data-idx="' + i + '" value="' + escAttr(s.emoji) + '" placeholder="📡">' +
|
|
'<input data-step-field="title" data-idx="' + i + '" value="' + escAttr(s.title) + '" placeholder="Title">' +
|
|
'<input data-step-field="description" data-idx="' + i + '" value="' + escAttr(s.description) + '" placeholder="Description" style="flex:2">' +
|
|
'<button class="cust-list-btn" data-move-step="' + i + '" data-dir="up" title="Move up">↑</button>' +
|
|
'<button class="cust-list-btn" data-move-step="' + i + '" data-dir="down" title="Move down">↓</button>' +
|
|
'<button class="cust-list-btn danger" data-rm-step="' + i + '" title="Remove">✕</button>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
var checkHtml = h.checklist.map(function (c, i) {
|
|
return '<div class="cust-list-item" data-check="' + i + '">' +
|
|
'<input data-check-field="question" data-idx="' + i + '" value="' + escAttr(c.question) + '" placeholder="Question">' +
|
|
'<input data-check-field="answer" data-idx="' + i + '" value="' + escAttr(c.answer) + '" placeholder="Answer" style="flex:2">' +
|
|
'<button class="cust-list-btn danger" data-rm-check="' + i + '" title="Remove">✕</button>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
var linksHtml = h.footerLinks.map(function (l, i) {
|
|
return '<div class="cust-list-item" data-link="' + i + '">' +
|
|
'<input data-link-field="label" data-idx="' + i + '" value="' + escAttr(l.label) + '" placeholder="Label">' +
|
|
'<input data-link-field="url" data-idx="' + i + '" value="' + escAttr(l.url) + '" placeholder="URL" style="flex:2">' +
|
|
'<button class="cust-list-btn danger" data-rm-link="' + i + '" title="Remove">✕</button>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
return '<div class="cust-panel' + (activeTab === 'home' ? ' active' : '') + '" data-panel="home">' +
|
|
'<div class="cust-field"><label>Hero Title</label><input type="text" data-key="home.heroTitle" value="' + escAttr(h.heroTitle) + '"></div>' +
|
|
'<div class="cust-field"><label>Hero Subtitle</label><input type="text" data-key="home.heroSubtitle" value="' + escAttr(h.heroSubtitle) + '"></div>' +
|
|
'<p class="cust-section-title" style="margin-top:20px">Steps</p>' + stepsHtml +
|
|
'<button class="cust-add-btn" id="addStep">+ Add Step</button>' +
|
|
'<p class="cust-section-title" style="margin-top:24px">FAQ / Checklist</p>' + checkHtml +
|
|
'<button class="cust-add-btn" id="addCheck">+ Add Question</button>' +
|
|
'<p class="cust-section-title" style="margin-top:24px">Footer Links</p>' + linksHtml +
|
|
'<button class="cust-add-btn" id="addLink">+ Add Link</button>' +
|
|
'</div>';
|
|
}
|
|
|
|
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;
|
|
|
|
// 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;
|
|
|
|
// 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);
|
|
return '<div class="cust-panel' + (activeTab === 'export' ? ' active' : '') + '" data-panel="export">' +
|
|
'<p class="cust-section-title">Export Configuration</p>' +
|
|
'<textarea class="cust-export-area" readonly id="custExportJson">' + esc(json) + '</textarea>' +
|
|
'<div class="cust-export-btns">' +
|
|
'<button class="cust-copy-btn" id="custCopy">📋 Copy to Clipboard</button>' +
|
|
'<button class="cust-dl-btn" id="custDownload">💾 Download config-theme.json</button>' +
|
|
'</div>' +
|
|
'<div class="cust-instructions">' +
|
|
'<strong>How to apply:</strong><br>' +
|
|
'Merge this JSON into your <code>config.json</code> file and restart the server.<br>' +
|
|
'Only values that differ from defaults are included.' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function render(container) {
|
|
container.innerHTML = '<div class="cust">' +
|
|
'<h1>🎨 Customize</h1>' +
|
|
'<p class="cust-sub">Configure branding, colors, and home page content. Changes preview live — export when ready.</p>' +
|
|
renderTabs() +
|
|
renderBranding() +
|
|
renderTheme() +
|
|
renderNodes() +
|
|
renderHome() +
|
|
renderExport() +
|
|
'</div>';
|
|
bindEvents(container);
|
|
}
|
|
|
|
function bindEvents(container) {
|
|
// Tab switching
|
|
container.querySelectorAll('.cust-tab').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
activeTab = btn.dataset.tab;
|
|
render(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;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Theme color pickers
|
|
container.querySelectorAll('input[data-theme]').forEach(function (inp) {
|
|
inp.addEventListener('input', function () {
|
|
var key = inp.dataset.theme;
|
|
state.theme[key] = inp.value;
|
|
var hex = container.querySelector('[data-hex="' + key + '"]');
|
|
if (hex) hex.textContent = inp.value;
|
|
applyThemePreview();
|
|
});
|
|
});
|
|
|
|
// Theme reset buttons
|
|
container.querySelectorAll('[data-reset-theme]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var key = btn.dataset.resetTheme;
|
|
state.theme[key] = DEFAULTS.theme[key];
|
|
applyThemePreview();
|
|
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;
|
|
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];
|
|
render(container);
|
|
});
|
|
});
|
|
|
|
// 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;
|
|
});
|
|
});
|
|
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;
|
|
});
|
|
});
|
|
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;
|
|
});
|
|
});
|
|
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);
|
|
});
|
|
}
|
|
|
|
function init(container) {
|
|
injectStyles();
|
|
saveOriginalCSS();
|
|
initState();
|
|
render(container);
|
|
applyThemePreview();
|
|
}
|
|
|
|
function destroy() {
|
|
// Keep preview active across pages — don't reset on navigate
|
|
removeStyles();
|
|
}
|
|
|
|
registerPage('customize', { init: init, destroy: destroy });
|
|
})();
|