mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-28 09:25:16 +00:00
**Red commit:** `851cc8c3a024b1675558092d772444bf4f1ec625` — failing test on a stub branch (will link CI run after PR opens). Partial fix for #1648 (M5 of 6). **Do NOT close the tracking issue** — M6 (server-side residual emoji sweep + lint gate) still pending. ## Per-file swap counts | File | Phosphor `<use>` refs | Notes | |---|---|---| | `public/customize.js` | 20 | DEFAULTS → `ph:<name>` tokens; render path keeps legacy emoji branch (back-compat) | | `public/customize-v2.js` | 26 | same as v1; cv2 overrides path unchanged | | `public/home.js` | (helpers added) | `_renderHomeGlyph` / `_renderHomeLabel` accept both `ph:<name>` and legacy emoji | | `public/geofilter-builder.html` | 5 | clear / undo / save / load buttons (+inline `.ph-icon` CSS) | | `public/audio.js` | 1 | audio unlock prompt | | `public/filter-ux.js` | 5 (3 new) | help popover star + close, saved-filter delete | | `public/style.css` | 0 | `#chList .ch-share-btn::before { content: '📤' }` removed; JS now renders an inline sprite | | `cmd/server/routes.go` | (6 `ph:` tokens) | onboarding home defaults updated in lockstep with customize-v2.js | ## Operator config back-compat — PROMINENT Per design call #1 (user-locked): existing operator-stored emoji values in `config.json` / `localStorage` are **NOT** touched. The render path supports both: ```js function renderConfigGlyph(value) { var m = String(value || '').match(/^ph:([a-z][a-z0-9-]+)$/); if (m) return '<svg class="ph-icon"><use href="/icons/phosphor-sprite.svg#ph-' + m[1] + '"/></svg>'; return esc(value); // EMOJI-OK-LEGACY-RENDER — operator-stored emoji/text path } ``` Defaults flipped to `ph:<name>` tokens, so new operators (and operators who hit "Reset to Defaults") see Phosphor sprites. Operators with stored emoji values continue to see their emoji exactly as before. Verified end-to-end (see E2E (b) below). ## cmd/server/routes.go — changed in lockstep Per design call #2: the home-defaults `steps` / `footerLinks` mirror the JS DEFAULTS, so they MUST update together. routes.go now emits `ph:<name>` tokens; the frontend home-render path resolves them. Existing tests (`TestConfigThemeHomeDefaults`) still pass — they assert structure, not glyph values. ## E2E assertions added - `test-issue-1648-m5-emoji-scan.js` — per-file zero-emoji + ph-token DEFAULTS + sprite presence - `test-issue-1648-m5-icons-e2e.js`: - (a) customize chrome — tabs/header rendered as sprites; chrome text icon-free - **(b) back-compat — injects fake `🐙` operator step into localStorage, reloads, opens customize, asserts the emoji renders verbatim in both the input value AND the live preview span; asserts the ph-token step renders as a sprite** (design call #1 in action) - (c) `/channels` modal sprite count - (d) `/audio-lab` sprite presence - (e) `geofilter-builder.html` control buttons sprite-driven - (f) every `<use>` resolves to a defined symbol id ## Out of scope (M6 cleanup) - cmd/server/routes.go residual server-rendered emoji **not** tied to customize defaults (none found by my grep — file already audited) - `make lint-no-emoji` CI grep gate (M6 owns it) - `public/icons/README.md` workflow doc cross-stack: justified — design call #2 requires Go + JS update together. --------- Co-authored-by: openclaw-bot <bot@openclaw.local>
This commit is contained in:
co-authored by
openclaw-bot
parent
2b6809cd28
commit
1116801b2f
+11
-6
@@ -564,14 +564,19 @@ func (s *Server) handleConfigTheme(w http.ResponseWriter, r *http.Request) {
|
||||
"heroTitle": "CoreScope",
|
||||
"heroSubtitle": "Real-time MeshCore LoRa mesh network analyzer",
|
||||
"steps": []interface{}{
|
||||
map[string]interface{}{"emoji": "🔵", "title": "Connect via Bluetooth", "description": "Flash **BLE companion** firmware from [MeshCore Flasher](https://flasher.meshcore.io/).\n- Screenless devices: default PIN `123456`\n- Screen devices: random PIN shown on display\n- If pairing fails: forget device, reboot, re-pair"},
|
||||
map[string]interface{}{"emoji": "📻", "title": "Set the right frequency preset", "description": "**US Recommended:**\n`910.525 MHz · BW 62.5 kHz · SF 7 · CR 5`\nSelect **\"US Recommended\"** in the app or flasher."},
|
||||
map[string]interface{}{"emoji": "📡", "title": "Advertise yourself", "description": "Tap the signal icon → **Flood** to broadcast your node to the mesh. Companions only advert when you trigger it manually."},
|
||||
map[string]interface{}{"emoji": "🔁", "title": "Check \"Heard N repeats\"", "description": "- **\"Sent\"** = transmitted, no confirmation\n- **\"Heard 0 repeats\"** = no repeater picked it up\n- **\"Heard 1+ repeats\"** = you're on the mesh!"},
|
||||
// #1648 M5: defaults use 'ph:<name>' Phosphor sprite tokens.
|
||||
// The frontend render path (home.js _renderHomeGlyph, customize-v2.js
|
||||
// renderConfigGlyph) ALSO accepts legacy emoji strings, so any
|
||||
// operator config.json that still stores raw emoji values continues
|
||||
// to render as-is — this changes ONLY the built-in default set.
|
||||
map[string]interface{}{"emoji": "ph:bluetooth", "title": "Connect via Bluetooth", "description": "Flash **BLE companion** firmware from [MeshCore Flasher](https://flasher.meshcore.io/).\n- Screenless devices: default PIN `123456`\n- Screen devices: random PIN shown on display\n- If pairing fails: forget device, reboot, re-pair"},
|
||||
map[string]interface{}{"emoji": "ph:radio", "title": "Set the right frequency preset", "description": "**US Recommended:**\n`910.525 MHz · BW 62.5 kHz · SF 7 · CR 5`\nSelect **\"US Recommended\"** in the app or flasher."},
|
||||
map[string]interface{}{"emoji": "ph:broadcast", "title": "Advertise yourself", "description": "Tap the signal icon → **Flood** to broadcast your node to the mesh. Companions only advert when you trigger it manually."},
|
||||
map[string]interface{}{"emoji": "ph:repeat", "title": "Check \"Heard N repeats\"", "description": "- **\"Sent\"** = transmitted, no confirmation\n- **\"Heard 0 repeats\"** = no repeater picked it up\n- **\"Heard 1+ repeats\"** = you're on the mesh!"},
|
||||
},
|
||||
"footerLinks": []interface{}{
|
||||
map[string]interface{}{"label": "📦 Packets", "url": "#/packets"},
|
||||
map[string]interface{}{"label": "🗺️ Network Map", "url": "#/map"},
|
||||
map[string]interface{}{"label": "ph:package Packets", "url": "#/packets"},
|
||||
map[string]interface{}{"label": "ph:map-trifold Network Map", "url": "#/map"},
|
||||
},
|
||||
}
|
||||
home := mergeMap(defaultHome, s.cfg.Home, theme.Home)
|
||||
|
||||
+1
-1
@@ -191,7 +191,7 @@
|
||||
_overlayShown = true;
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'audio-unlock-overlay';
|
||||
overlay.innerHTML = '<div class="audio-unlock-prompt">🔊 Tap to enable audio</div>';
|
||||
overlay.innerHTML = '<div class="audio-unlock-prompt"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-speaker-high"/></svg> Tap to enable audio</div>';
|
||||
overlay.addEventListener('click', () => {
|
||||
if (audioCtx) audioCtx.resume();
|
||||
overlay.remove();
|
||||
|
||||
+79
-53
@@ -9,15 +9,18 @@
|
||||
var DEFAULT_HOME = {
|
||||
heroTitle: 'CoreScope',
|
||||
heroSubtitle: 'Real-time MeshCore LoRa mesh network analyzer',
|
||||
// #1648 M5: defaults use 'ph:<name>' sprite tokens. Back-compat: the
|
||||
// render path (renderConfigGlyph) ALSO accepts legacy emoji strings
|
||||
// for operators with stored config values.
|
||||
steps: [
|
||||
{ emoji: '🔵', title: 'Connect via Bluetooth', description: 'Flash **BLE companion** firmware from [MeshCore Flasher](https://flasher.meshcore.io/).\n- Screenless devices: default PIN `123456`\n- Screen devices: random PIN shown on display\n- If pairing fails: forget device, reboot, re-pair' },
|
||||
{ emoji: '📻', title: 'Set the right frequency preset', description: '**US Recommended:**\n`910.525 MHz · BW 62.5 kHz · SF 7 · CR 5`\nSelect **"US Recommended"** in the app or flasher.' },
|
||||
{ emoji: '📡', title: 'Advertise yourself', description: 'Tap the signal icon → **Flood** to broadcast your node to the mesh. Companions only advert when you trigger it manually.' },
|
||||
{ emoji: '🔁', title: 'Check "Heard N repeats"', description: '- **"Sent"** = transmitted, no confirmation\n- **"Heard 0 repeats"** = no repeater picked it up\n- **"Heard 1+ repeats"** = you\'re on the mesh!' }
|
||||
{ emoji: 'ph:bluetooth', title: 'Connect via Bluetooth', description: 'Flash **BLE companion** firmware from [MeshCore Flasher](https://flasher.meshcore.io/).\n- Screenless devices: default PIN `123456`\n- Screen devices: random PIN shown on display\n- If pairing fails: forget device, reboot, re-pair' },
|
||||
{ emoji: 'ph:radio', title: 'Set the right frequency preset', description: '**US Recommended:**\n`910.525 MHz · BW 62.5 kHz · SF 7 · CR 5`\nSelect **"US Recommended"** in the app or flasher.' },
|
||||
{ emoji: 'ph:broadcast', title: 'Advertise yourself', description: 'Tap the signal icon → **Flood** to broadcast your node to the mesh. Companions only advert when you trigger it manually.' },
|
||||
{ emoji: 'ph:repeat', title: 'Check "Heard N repeats"', description: '- **"Sent"** = transmitted, no confirmation\n- **"Heard 0 repeats"** = no repeater picked it up\n- **"Heard 1+ repeats"** = you\'re on the mesh!' }
|
||||
],
|
||||
footerLinks: [
|
||||
{ label: '📦 Packets', url: '#/packets' },
|
||||
{ label: '🗺️ Network Map', url: '#/map' }
|
||||
{ label: 'ph:package Packets', url: '#/packets' },
|
||||
{ label: 'ph:map-trifold Network Map', url: '#/map' }
|
||||
]
|
||||
};
|
||||
|
||||
@@ -106,7 +109,7 @@
|
||||
// Apply a validated branding.homeUrl to <a class="nav-brand">[href]. When
|
||||
// url is empty / invalid, restore the in-app default '#/' so operators can
|
||||
// clear the override and immediately see the SPA route restored.
|
||||
// Bottom-nav 🏠 stays in-app — do NOT touch public/bottom-nav.js here, that
|
||||
// Bottom-nav house glyph stays in-app — do NOT touch public/bottom-nav.js here, that
|
||||
// preserves the SPA back-stack on mobile (per #1518 triage).
|
||||
function _setBrandHomeUrl(url) {
|
||||
var brandLink = document.querySelector('a.nav-brand');
|
||||
@@ -323,7 +326,7 @@
|
||||
repeater: 'Infrastructure nodes that relay packets', companion: 'End-user devices',
|
||||
room: 'Room/chat server nodes', sensor: 'Sensor/telemetry nodes', observer: 'MQTT observer stations'
|
||||
};
|
||||
var NODE_EMOJI = { repeater: '◆', companion: '●', room: '■', sensor: '▲', observer: '★' };
|
||||
var NODE_EMOJI = { repeater: 'ph:diamond', companion: 'ph:circle-fill', room: 'ph:square-fill', sensor: 'ph:triangle', observer: 'ph:star-fill' };
|
||||
|
||||
var TYPE_LABELS = {
|
||||
ADVERT: 'Advertisement', GRP_TXT: 'Channel Message', TXT_MSG: 'Direct Message', ACK: 'Acknowledgment',
|
||||
@@ -334,11 +337,31 @@
|
||||
ACK: 'Acknowledgments', REQUEST: 'Requests', RESPONSE: 'Responses',
|
||||
TRACE: 'Traceroute', PATH: 'Path packets', ANON_REQ: 'Encrypted anonymous requests'
|
||||
};
|
||||
// #1648 M5: defaults use 'ph:<name>' tokens. renderConfigGlyph() below
|
||||
// accepts both new ph tokens AND legacy emoji strings (back-compat).
|
||||
var TYPE_EMOJI = {
|
||||
ADVERT: '📡', GRP_TXT: '💬', TXT_MSG: '✉️', ACK: '✓', REQUEST: '❓',
|
||||
RESPONSE: '📨', TRACE: '🔍', PATH: '🛤️', ANON_REQ: '🕵️'
|
||||
ADVERT: 'ph:broadcast', GRP_TXT: 'ph:chat-circle', TXT_MSG: 'ph:envelope', ACK: 'ph:check', REQUEST: 'ph:question',
|
||||
RESPONSE: 'ph:envelope-simple', TRACE: 'ph:magnifying-glass', PATH: 'ph:path', ANON_REQ: 'ph:lock'
|
||||
};
|
||||
|
||||
// renderConfigGlyph(value): renders a Phosphor sprite for 'ph:<name>' OR
|
||||
// returns the operator's stored string (escaped) for legacy emoji/text.
|
||||
// This is #1648 M5 design call #1: never override operator-stored values.
|
||||
function renderConfigGlyph(value) {
|
||||
if (value == null) return '';
|
||||
var s = String(value);
|
||||
var m = s.match(/^ph:([a-z][a-z0-9-]+)$/);
|
||||
if (m) return '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-' + m[1] + '"/></svg>';
|
||||
return esc(s); // EMOJI-OK-LEGACY-RENDER
|
||||
}
|
||||
function renderConfigLabel(label) {
|
||||
if (label == null) return '';
|
||||
var s = String(label);
|
||||
var m = s.match(/^(ph:[a-z][a-z0-9-]+)\s+(.*)$/);
|
||||
if (m) return renderConfigGlyph(m[1]) + ' ' + esc(m[2]);
|
||||
return esc(s); // EMOJI-OK-LEGACY-RENDER
|
||||
}
|
||||
|
||||
var BASIC_KEYS = ['accent', 'navBg', 'navText', 'background', 'text', 'statusGreen', 'statusYellow', 'statusRed'];
|
||||
var ADVANCED_KEYS = ['accentHover', 'navBg2', 'navTextMuted', 'navActiveBg', 'textMuted', 'border', 'surface1', 'surface2', 'cardBg', 'contentBg', 'detailBg', 'inputBg', 'rowStripe', 'rowHover', 'selectedBg'];
|
||||
var FONT_KEYS = ['font', 'mono'];
|
||||
@@ -1011,7 +1034,7 @@
|
||||
if (!el) return;
|
||||
if (status === 'saved') { el.textContent = 'All changes saved'; el.style.color = 'var(--text-muted)'; }
|
||||
else if (status === 'saving') { el.textContent = 'Saving...'; el.style.color = 'var(--text-muted)'; }
|
||||
else if (status === 'error') { el.textContent = '⚠️ Storage full — changes may not be saved'; el.style.color = '#ef4444'; }
|
||||
else if (status === 'error') { el.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-warning"/></svg> Storage full — changes may not be saved'; el.style.color = '#ef4444'; }
|
||||
}
|
||||
|
||||
function _showQuotaWarning() {
|
||||
@@ -1080,7 +1103,7 @@
|
||||
|
||||
function _overrideDot(section, key) {
|
||||
if (!_isOverridden(section, key)) return '';
|
||||
return '<span class="cv2-override-dot" data-reset-s="' + (section || '') + '" data-reset-k="' + key + '" title="Reset to server default">●</span>';
|
||||
return '<span class="cv2-override-dot" data-reset-s="' + (section || '') + '" data-reset-k="' + key + '" title="Reset to server default"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-circle-fill"/></svg></span>';
|
||||
}
|
||||
|
||||
function _injectStyles() {
|
||||
@@ -1156,13 +1179,13 @@
|
||||
|
||||
function _renderTabs() {
|
||||
var tabs = [
|
||||
{ id: 'branding', label: '🏷️', title: 'Branding', badge: _tabBadge('branding') },
|
||||
{ id: 'theme', label: '🎨', title: 'Theme', badge: _tabBadge(isDarkMode() ? 'themeDark' : 'theme') },
|
||||
{ id: 'nodes', label: '🎯', title: 'Colors', badge: (function () { var n = _countOverrides('nodeColors') + _countOverrides('typeColors'); return n ? ' <span class="cv2-tab-badge">' + n + '</span>' : ''; })() },
|
||||
{ id: 'home', label: '🏠', title: 'Home', badge: _tabBadge('home') },
|
||||
{ id: 'display', label: '🖥️', title: 'Display', badge: (function () { var n = _countOverrides('timestamps') + (_isOverridden(null, 'distanceUnit') ? 1 : 0); return n ? ' <span class="cv2-tab-badge">' + n + '</span>' : ''; })() },
|
||||
{ id: 'geofilter', label: '🗺️', title: 'GeoFilter' },
|
||||
{ id: 'export', label: '📤', title: 'Export' }
|
||||
{ id: 'branding', glyph: 'ph:tag', title: 'Branding', badge: _tabBadge('branding') },
|
||||
{ id: 'theme', glyph: 'ph:palette', title: 'Theme', badge: _tabBadge(isDarkMode() ? 'themeDark' : 'theme') },
|
||||
{ id: 'nodes', glyph: 'ph:target', title: 'Colors', badge: (function () { var n = _countOverrides('nodeColors') + _countOverrides('typeColors'); return n ? ' <span class="cv2-tab-badge">' + n + '</span>' : ''; })() },
|
||||
{ id: 'home', glyph: 'ph:house', title: 'Home', badge: _tabBadge('home') },
|
||||
{ id: 'display', glyph: 'ph:monitor', title: 'Display', badge: (function () { var n = _countOverrides('timestamps') + (_isOverridden(null, 'distanceUnit') ? 1 : 0); return n ? ' <span class="cv2-tab-badge">' + n + '</span>' : ''; })() },
|
||||
{ id: 'geofilter', glyph: 'ph:map-trifold', title: 'GeoFilter' },
|
||||
{ id: 'export', glyph: 'ph:download-simple', title: 'Export' }
|
||||
];
|
||||
// #1508 — operators can hide individual tabs server-side via
|
||||
// config.customizer.disabledTabs. The list arrives on
|
||||
@@ -1174,7 +1197,7 @@
|
||||
}
|
||||
return '<div class="cust-tabs">' + tabs.map(function (t) {
|
||||
return '<button class="cust-tab' + (t.id === _activeTab ? ' active' : '') + '" data-tab="' + t.id + '" title="' + t.title + '">' +
|
||||
t.label + ' <span class="cust-tab-text">' + t.title + '</span>' + (t.badge || '') + '</button>';
|
||||
renderConfigGlyph(t.glyph) + ' <span class="cust-tab-text">' + t.title + '</span>' + (t.badge || '') + '</button>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
@@ -1273,7 +1296,7 @@
|
||||
var server = _getServer();
|
||||
var current = dark ? Object.assign({}, eff.theme || {}, eff.themeDark || {}) : (eff.theme || {});
|
||||
var serverCurrent = dark ? Object.assign({}, server.theme || {}, server.themeDark || {}) : (server.theme || {});
|
||||
var modeLabel = dark ? '🌙 Dark Mode' : '☀️ Light Mode';
|
||||
var modeLabel = dark ? (renderConfigGlyph('ph:moon') + ' Dark Mode') : (renderConfigGlyph('ph:sun') + ' Light Mode');
|
||||
|
||||
var basicRows = '';
|
||||
for (var i = 0; i < BASIC_KEYS.length; i++) basicRows += _renderColorRow(BASIC_KEYS[i], section, current[BASIC_KEYS[i]], serverCurrent[BASIC_KEYS[i]]);
|
||||
@@ -1285,7 +1308,7 @@
|
||||
return '<div class="cust-panel' + (_activeTab === 'theme' ? ' active' : '') + '" data-panel="theme">' +
|
||||
_renderPresets() +
|
||||
'<p class="cust-section-title">' + modeLabel + '</p>' +
|
||||
'<p style="font-size:11px;color:var(--text-muted);margin:0 0 10px">Toggle ☀️/🌙 in nav to edit the other mode.</p>' +
|
||||
'<p style="font-size:11px;color:var(--text-muted);margin:0 0 10px">Toggle ' + renderConfigGlyph('ph:sun') + '/' + renderConfigGlyph('ph:moon') + ' in nav to edit the other mode.</p>' +
|
||||
basicRows +
|
||||
'<details class="cust-advanced"><summary style="font-size:12px;font-weight:600;cursor:pointer;color:var(--text-muted);margin:12px 0 8px">Advanced (' + ADVANCED_KEYS.length + ' options)</summary>' + advancedRows + '</details>' +
|
||||
'<details class="cust-fonts" style="margin-top:12px"><summary style="font-size:12px;font-weight:600;cursor:pointer;color:var(--text-muted);margin:12px 0 8px">Fonts</summary>' + fontRows + '</details>' +
|
||||
@@ -1419,7 +1442,7 @@
|
||||
var failing = rep.filter(function (r) { return dark ? !r.passDark : !r.passLight; });
|
||||
if (!failing.length) return '';
|
||||
var names = failing.map(function (r) { return r.role; }).join(', ');
|
||||
return '<div class="cust-cb-warn" style="margin-top:4px;font-size:11px;color:var(--status-yellow);background:rgba(255,200,0,0.08);padding:4px 6px;border-radius:4px">⚠ WCAG 1.4.11: ' + esc(names) + ' below 3:1 vs ' + (dark ? 'dark' : 'light') + ' tiles</div>';
|
||||
return '<div class="cust-cb-warn" style="margin-top:4px;font-size:11px;color:var(--status-yellow);background:rgba(255,200,0,0.08);padding:4px 6px;border-radius:4px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-warning"/></svg> WCAG 1.4.11: ' + esc(names) + ' below 3:1 vs ' + (dark ? 'dark' : 'light') + ' tiles</div>';
|
||||
}
|
||||
|
||||
function _renderNodes() {
|
||||
@@ -1431,7 +1454,7 @@
|
||||
for (var key in NODE_LABELS) {
|
||||
var val = nc[key] || '#000000';
|
||||
rows += '<div class="cust-color-row">' +
|
||||
'<div><label>' + NODE_EMOJI[key] + ' ' + NODE_LABELS[key] + _overrideDot('nodeColors', key) + '</label>' +
|
||||
'<div><label>' + renderConfigGlyph(NODE_EMOJI[key]) + ' ' + NODE_LABELS[key] + _overrideDot('nodeColors', key) + '</label>' +
|
||||
'<div class="cust-hint">' + (NODE_HINTS[key] || '') + '</div></div>' +
|
||||
'<input type="color" data-cv2-field="nodeColors.' + key + '" value="' + val + '">' +
|
||||
'<span class="cust-node-dot" style="background:' + val + '"></span>' +
|
||||
@@ -1445,7 +1468,7 @@
|
||||
for (var tkey in TYPE_LABELS) {
|
||||
var tval = tc[tkey] || fallbackTC[tkey] || '#000000';
|
||||
typeRows += '<div class="cust-color-row">' +
|
||||
'<div><label>' + (TYPE_EMOJI[tkey] || '') + ' ' + TYPE_LABELS[tkey] + _overrideDot('typeColors', tkey) + '</label>' +
|
||||
'<div><label>' + renderConfigGlyph(TYPE_EMOJI[tkey] || '') + ' ' + TYPE_LABELS[tkey] + _overrideDot('typeColors', tkey) + '</label>' +
|
||||
'<div class="cust-hint">' + (TYPE_HINTS[tkey] || '') + '</div></div>' +
|
||||
'<input type="color" data-cv2-field="typeColors.' + tkey + '" value="' + tval + '">' +
|
||||
'<span class="cust-node-dot" style="background:' + tval + '"></span>' +
|
||||
@@ -1476,11 +1499,11 @@
|
||||
'<p class="cust-section-title">Packet Type Colors</p>' + typeRows +
|
||||
'<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">' +
|
||||
'<p class="cust-section-title">Heatmap Opacity</p>' +
|
||||
'<div class="cust-color-row"><div><label>🗺️ Nodes Map' + _overrideDot(null, 'heatmapOpacity') + '</label>' +
|
||||
'<div class="cust-color-row"><div><label><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-map-trifold"/></svg> Nodes Map' + _overrideDot(null, 'heatmapOpacity') + '</label>' +
|
||||
'<div class="cust-hint">Heatmap overlay on the Nodes → Map page (0–100%)</div></div>' +
|
||||
'<input type="range" data-cv2-slider="heatmapOpacity" min="0" max="100" value="' + heatPct + '" style="width:120px;cursor:pointer">' +
|
||||
'<span class="cust-hex" id="cv2HeatPct">' + heatPct + '%</span></div>' +
|
||||
'<div class="cust-color-row"><div><label>📡 Live Map' + _overrideDot(null, 'liveHeatmapOpacity') + '</label>' +
|
||||
'<div class="cust-color-row"><div><label><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg> Live Map' + _overrideDot(null, 'liveHeatmapOpacity') + '</label>' +
|
||||
'<div class="cust-hint">Heatmap overlay on the Live page (0–100%)</div></div>' +
|
||||
'<input type="range" data-cv2-slider="liveHeatmapOpacity" min="0" max="100" value="' + liveHeatPct + '" style="width:120px;cursor:pointer">' +
|
||||
'<span class="cust-hex" id="cv2LiveHeatPct">' + liveHeatPct + '%</span></div>' +
|
||||
@@ -1623,11 +1646,14 @@
|
||||
var stepsHtml = steps.map(function (s, i) {
|
||||
return '<div class="cust-list-item">' +
|
||||
'<div class="cust-list-row">' +
|
||||
'<input class="cust-emoji-input" data-cv2-home="steps.' + i + '.emoji" value="' + escAttr(s.emoji) + '" placeholder="📡">' +
|
||||
// Glyph preview + freeform input. EMOJI-OK-LEGACY-RENDER: legacy
|
||||
// operator emoji values still display via renderConfigGlyph().
|
||||
'<span class="cust-emoji-preview" aria-hidden="true" style="min-width:1em;display:inline-block">' + renderConfigGlyph(s.emoji) + '</span>' +
|
||||
'<input class="cust-emoji-input" data-cv2-home="steps.' + i + '.emoji" value="' + escAttr(s.emoji) + '" placeholder="ph:broadcast">' +
|
||||
'<input data-cv2-home="steps.' + i + '.title" value="' + escAttr(s.title) + '" placeholder="Title">' +
|
||||
'<button class="cust-list-btn" data-cv2-move="steps.' + i + '.up">↑</button>' +
|
||||
'<button class="cust-list-btn" data-cv2-move="steps.' + i + '.down">↓</button>' +
|
||||
'<button class="cust-list-btn danger" data-cv2-rm="steps.' + i + '">✕</button>' +
|
||||
'<button class="cust-list-btn" data-cv2-move="steps.' + i + '.up"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-up"/></svg></button>' +
|
||||
'<button class="cust-list-btn" data-cv2-move="steps.' + i + '.down"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-down"/></svg></button>' +
|
||||
'<button class="cust-list-btn danger" data-cv2-rm="steps.' + i + '"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button>' +
|
||||
'</div>' +
|
||||
'<textarea data-cv2-home="steps.' + i + '.description" placeholder="Description" rows="2">' + esc(s.description) + '</textarea>' +
|
||||
'<div class="cust-md-hint">Markdown: <code>**bold**</code> <code>*italic*</code> <code>`code`</code> <code>[text](url)</code></div></div>';
|
||||
@@ -1636,14 +1662,14 @@
|
||||
var checkHtml = checklist.map(function (c, i) {
|
||||
return '<div class="cust-list-item">' +
|
||||
'<div class="cust-list-row"><input data-cv2-home="checklist.' + i + '.question" value="' + escAttr(c.question) + '" placeholder="Question">' +
|
||||
'<button class="cust-list-btn danger" data-cv2-rm="checklist.' + i + '">✕</button></div>' +
|
||||
'<button class="cust-list-btn danger" data-cv2-rm="checklist.' + i + '"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button></div>' +
|
||||
'<textarea data-cv2-home="checklist.' + i + '.answer" placeholder="Answer" rows="2">' + esc(c.answer) + '</textarea></div>';
|
||||
}).join('');
|
||||
|
||||
var linksHtml = footerLinks.map(function (l, i) {
|
||||
return '<div class="cust-list-item">' +
|
||||
'<div class="cust-list-row"><input data-cv2-home="footerLinks.' + i + '.label" value="' + escAttr(l.label) + '" placeholder="Label">' +
|
||||
'<button class="cust-list-btn danger" data-cv2-rm="footerLinks.' + i + '">✕</button></div>' +
|
||||
'<div class="cust-list-row"><input data-cv2-home="footerLinks.' + i + '.label" value="' + escAttr(l.label) + '" placeholder="Label (e.g. ph:package Packets)">' +
|
||||
'<button class="cust-list-btn danger" data-cv2-rm="footerLinks.' + i + '"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button></div>' +
|
||||
'<input data-cv2-home="footerLinks.' + i + '.url" value="' + escAttr(l.url) + '" placeholder="URL"></div>';
|
||||
}).join('');
|
||||
|
||||
@@ -1667,7 +1693,7 @@
|
||||
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:12px">Shows the active geographic filter. Nodes outside this area are excluded at ingest time and in API responses.</p>' +
|
||||
'<div style="position:relative;margin-bottom:8px">' +
|
||||
'<div id="cv2-gf-map" style="height:200px;border-radius:6px;border:1px solid var(--border);background:var(--surface-1);cursor:pointer"></div>' +
|
||||
'<div style="position:absolute;top:7px;right:7px;background:rgba(255,255,255,0.88);border-radius:4px;padding:3px 8px;font-size:11px;color:#444;pointer-events:none;box-shadow:0 1px 3px rgba(0,0,0,0.15)">🔍 click to expand</div>' +
|
||||
'<div style="position:absolute;top:7px;right:7px;background:rgba(255,255,255,0.88);border-radius:4px;padding:3px 8px;font-size:11px;color:#444;pointer-events:none;box-shadow:0 1px 3px rgba(0,0,0,0.15)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-magnifying-glass"/></svg> click to expand</div>' +
|
||||
'</div>' +
|
||||
'<div id="cv2-gf-status" style="font-size:12px;color:var(--text-muted);margin-bottom:10px">Loading current filter…</div>' +
|
||||
// Edit controls — hidden until server confirms write access (writeEnabled=true)
|
||||
@@ -1721,11 +1747,11 @@
|
||||
if (_gfWriteEnabled) {
|
||||
var undoBtn = document.createElement('button');
|
||||
undoBtn.id = 'cv2-gfm-undo';
|
||||
undoBtn.textContent = '↩ Undo';
|
||||
undoBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-arrow-u-up-left"/></svg> Undo';
|
||||
undoBtn.style.cssText = 'padding:5px 10px;background:#eee;color:#555;border:1px solid #ccc;border-radius:6px;cursor:pointer;font-size:12px;';
|
||||
var clearBtn = document.createElement('button');
|
||||
clearBtn.id = 'cv2-gfm-clear';
|
||||
clearBtn.textContent = '✕ Clear';
|
||||
clearBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg> Clear';
|
||||
clearBtn.style.cssText = 'padding:5px 10px;background:#fee;color:#c44;border:1px solid #fcc;border-radius:6px;cursor:pointer;font-size:12px;';
|
||||
var countEl = document.createElement('span');
|
||||
countEl.id = 'cv2-gfm-count';
|
||||
@@ -2047,18 +2073,18 @@
|
||||
'<p class="cust-section-title">Export / Import</p>' +
|
||||
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Your customizations are stored in your browser. Export to share or back up.</p>' +
|
||||
'<div class="cust-export-btns">' +
|
||||
'<button class="cust-dl-btn" id="cv2Download">💾 Download JSON</button>' +
|
||||
'<button class="cust-dl-btn" id="cv2ImportFile">📂 Import File</button>' +
|
||||
'<button class="cust-dl-btn" id="cv2Download"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-floppy-disk"/></svg> Download JSON</button>' +
|
||||
'<button class="cust-dl-btn" id="cv2ImportFile"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-folder-open"/></svg> Import File</button>' +
|
||||
'<input type="file" id="cv2ImportInput" accept=".json,application/json" style="display:none">' +
|
||||
'<button class="cust-copy-btn" id="cv2Copy">📋 Copy</button>' +
|
||||
'<button class="cust-copy-btn" id="cv2Copy"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clipboard-text"/></svg> Copy</button>' +
|
||||
'</div>' +
|
||||
(hasDelta ? '<div style="margin-top:12px"><button class="cust-reset-all" id="cv2ResetAll">🗑️ Reset All Customizations</button></div>' : '') +
|
||||
(hasDelta ? '<div style="margin-top:12px"><button class="cust-reset-all" id="cv2ResetAll"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-trash"/></svg> Reset All Customizations</button></div>' : '') +
|
||||
'<details style="margin-top:12px"><summary style="font-size:12px;font-weight:600;cursor:pointer;color:var(--text-muted)">Raw JSON</summary>' +
|
||||
'<textarea id="cv2ExportJson" style="width:100%;min-height:200px;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;margin-top:8px">' + esc(json) + '</textarea>' +
|
||||
'</details>' +
|
||||
'<p class="cust-section-title" style="margin-top:20px">Tools</p>' +
|
||||
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:10px">Server-side configuration helpers.</p>' +
|
||||
'<a href="/geofilter-builder.html" target="_blank" style="display:inline-block;padding:7px 14px;background:var(--surface-1);border:1px solid var(--border);border-radius:6px;color:var(--accent);font-size:13px;text-decoration:none;font-weight:500">🗺️ GeoFilter Builder →</a>' +
|
||||
'<a href="/geofilter-builder.html" target="_blank" style="display:inline-block;padding:7px 14px;background:var(--surface-1);border:1px solid var(--border);border-radius:6px;color:var(--accent);font-size:13px;text-decoration:none;font-weight:500"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-map-trifold"/></svg> GeoFilter Builder →</a>' +
|
||||
'<p style="font-size:11px;color:var(--text-muted);margin-top:6px">Draw a polygon on the map to generate a <code style="font-family:var(--mono)">geo_filter</code> block for <code style="font-family:var(--mono)">config.json</code>.</p>' +
|
||||
'</div>';
|
||||
}
|
||||
@@ -2300,7 +2326,7 @@
|
||||
var iconEl = document.querySelector('.brand-icon');
|
||||
if (iconEl) {
|
||||
if (inp.value) iconEl.innerHTML = '<img src="' + inp.value + '" style="height:24px" onerror="this.style.display=\'none\'">';
|
||||
else iconEl.textContent = '📡';
|
||||
else iconEl.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg>';
|
||||
}
|
||||
}
|
||||
if (section === 'branding' && key === 'faviconUrl') {
|
||||
@@ -2443,7 +2469,7 @@
|
||||
var eff = _getEffective();
|
||||
var home = JSON.parse(JSON.stringify(eff.home || {}));
|
||||
var arr = home[listKey] || [];
|
||||
if (listKey === 'steps') arr.push({ emoji: '📌', title: '', description: '' });
|
||||
if (listKey === 'steps') arr.push({ emoji: 'ph:push-pin', title: '', description: '' });
|
||||
else if (listKey === 'checklist') arr.push({ question: '', answer: '' });
|
||||
else if (listKey === 'footerLinks') arr.push({ label: '', url: '' });
|
||||
setOverride('home', listKey, arr);
|
||||
@@ -2467,8 +2493,8 @@
|
||||
var json = JSON.stringify(readOverrides(), null, 2);
|
||||
if (window.copyToClipboard) {
|
||||
window.copyToClipboard(json, function () {
|
||||
copyBtn.textContent = '✓ Copied!';
|
||||
setTimeout(function () { copyBtn.textContent = '📋 Copy'; }, 2000);
|
||||
copyBtn.textContent = 'Copied!';
|
||||
setTimeout(function () { copyBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clipboard-text"/></svg> Copy'; }, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -2487,18 +2513,18 @@
|
||||
var data = JSON.parse(reader.result);
|
||||
var result = validateShape(data);
|
||||
if (!result.valid) {
|
||||
importBtn.textContent = '✕ ' + result.errors[0];
|
||||
setTimeout(function () { importBtn.textContent = '📂 Import File'; }, 3000);
|
||||
importBtn.textContent = result.errors[0];
|
||||
setTimeout(function () { importBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-folder-open"/></svg> Import File'; }, 3000);
|
||||
return;
|
||||
}
|
||||
writeOverrides(data);
|
||||
_runPipeline();
|
||||
_renderPanel(container);
|
||||
importBtn.textContent = '✓ Imported!';
|
||||
setTimeout(function () { importBtn.textContent = '📂 Import File'; }, 2000);
|
||||
importBtn.textContent = 'Imported!';
|
||||
setTimeout(function () { importBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-folder-open"/></svg> Import File'; }, 2000);
|
||||
} catch (e) {
|
||||
importBtn.textContent = '✕ Invalid JSON';
|
||||
setTimeout(function () { importBtn.textContent = '📂 Import File'; }, 3000);
|
||||
importBtn.textContent = 'Invalid JSON';
|
||||
setTimeout(function () { importBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-folder-open"/></svg> Import File'; }, 3000);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
@@ -2543,7 +2569,7 @@
|
||||
_panelEl = document.createElement('div');
|
||||
_panelEl.className = 'cust-overlay';
|
||||
_panelEl.innerHTML =
|
||||
'<div class="cust-header"><h2>🎨 Customize</h2><button class="cust-close" title="Close">✕</button></div>' +
|
||||
'<div class="cust-header"><h2><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-palette"/></svg> Customize</h2><button class="cust-close" title="Close"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button></div>' +
|
||||
'<div class="cv2-local-banner">These settings are saved in your browser only and don\'t affect other users.</div>' +
|
||||
'<div class="cust-inner"></div>' +
|
||||
'<div class="cv2-footer"><span id="cv2-save-status">All changes saved</span></div>';
|
||||
|
||||
+89
-50
@@ -77,21 +77,27 @@
|
||||
home: {
|
||||
heroTitle: 'CoreScope',
|
||||
heroSubtitle: 'Find your nodes to start monitoring them.',
|
||||
// #1648 M5: default step "emoji" values are now Phosphor sprite tokens
|
||||
// ('ph:<icon-name>'). The render path (renderConfigGlyph) ALSO accepts
|
||||
// raw emoji strings for back-compat with operators who saved emoji
|
||||
// values to their existing config — see EMOJI-OK-LEGACY-RENDER below.
|
||||
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.' }
|
||||
{ emoji: 'ph:chat-circle', title: 'Join the Bay Area MeshCore Discord', description: 'The community Discord is the best place to get help and find local mesh enthusiasts.' },
|
||||
{ emoji: 'ph:bluetooth', title: 'Connect via Bluetooth', description: 'Flash BLE companion firmware and pair with your device.' },
|
||||
{ emoji: 'ph:radio', title: 'Set the right frequency preset', description: 'Match the frequency preset used by your local mesh community.' },
|
||||
{ emoji: 'ph:broadcast', title: 'Advertise yourself', description: 'Send an ADVERT so repeaters and observers can see you.' },
|
||||
{ emoji: 'ph:repeat', title: 'Check "Heard N repeats"', description: 'Verify your node is being relayed through the mesh.' },
|
||||
{ emoji: 'ph:map-pin', title: 'Repeaters near you?', description: 'Check the map for nearby repeaters and coverage.' }
|
||||
],
|
||||
checklist: [],
|
||||
// #1648 M5: defaults use 'ph:<name>' tokens. _renderFooterLinkLabel()
|
||||
// accepts either 'ph:<name> Text' or legacy 'emoji Text' for back-compat.
|
||||
footerLinks: [
|
||||
{ label: '📦 Packets', url: '#/packets' },
|
||||
{ label: '🗺️ Network Map', url: '#/map' },
|
||||
{ label: '🔴 Live', url: '#/live' },
|
||||
{ label: '📡 All Nodes', url: '#/nodes' },
|
||||
{ label: '💬 Channels', url: '#/channels' }
|
||||
{ label: 'ph:package Packets', url: '#/packets' },
|
||||
{ label: 'ph:map-trifold Network Map', url: '#/map' },
|
||||
{ label: 'ph:broadcast Live', url: '#/live' },
|
||||
{ label: 'ph:broadcast All Nodes', url: '#/nodes' },
|
||||
{ label: 'ph:chats Channels', url: '#/channels' }
|
||||
]
|
||||
},
|
||||
ui: {
|
||||
@@ -445,7 +451,10 @@
|
||||
observer: 'MQTT observer stations — map markers (purple stars), observer list, packet headers'
|
||||
};
|
||||
|
||||
const NODE_EMOJI = { repeater: '◆', companion: '●', room: '■', sensor: '▲', observer: '★' };
|
||||
// #1648 M5: glyph tokens for role/type/step DEFAULTS. Format: 'ph:<name>'
|
||||
// → renders as Phosphor sprite. Legacy emoji strings remain readable via
|
||||
// renderConfigGlyph() for operators with stored config (back-compat).
|
||||
const NODE_EMOJI = { repeater: 'ph:diamond', companion: 'ph:circle-fill', room: 'ph:square-fill', sensor: 'ph:triangle', observer: 'ph:star-fill' };
|
||||
|
||||
const TYPE_LABELS = {
|
||||
ADVERT: 'Advertisement', GRP_TXT: 'Channel Message', TXT_MSG: 'Direct Message', ACK: 'Acknowledgment',
|
||||
@@ -463,10 +472,35 @@
|
||||
PATH: 'Path packets — packet list',
|
||||
ANON_REQ: 'Encrypted anonymous requests — sender identity hidden via ephemeral key'
|
||||
};
|
||||
// #1648 M5: defaults are ph:<name>; renderConfigGlyph handles legacy emoji.
|
||||
const TYPE_EMOJI = {
|
||||
ADVERT: '📡', GRP_TXT: '💬', TXT_MSG: '✉️', ACK: '✓', REQUEST: '❓', RESPONSE: '📨', TRACE: '🔍', PATH: '🛤️', ANON_REQ: '🕵️'
|
||||
ADVERT: 'ph:broadcast', GRP_TXT: 'ph:chat-circle', TXT_MSG: 'ph:envelope', ACK: 'ph:check', REQUEST: 'ph:question', RESPONSE: 'ph:envelope-simple', TRACE: 'ph:magnifying-glass', PATH: 'ph:path', ANON_REQ: 'ph:lock'
|
||||
};
|
||||
|
||||
// renderConfigGlyph(value): given an operator-customizable config string,
|
||||
// returns HTML — a Phosphor sprite <svg> when value starts with 'ph:<name>',
|
||||
// otherwise the raw string escaped (back-compat: operator-stored emoji or
|
||||
// any plain text is rendered as-is). #1648 design call #1.
|
||||
function renderConfigGlyph(value) {
|
||||
if (value == null) return '';
|
||||
var s = String(value);
|
||||
var m = s.match(/^ph:([a-z][a-z0-9-]+)$/);
|
||||
if (m) {
|
||||
return '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-' + m[1] + '"/></svg>';
|
||||
}
|
||||
return esc(s); // EMOJI-OK-LEGACY-RENDER — operator-stored emoji/text path
|
||||
}
|
||||
// Footer-link / step labels can be either 'ph:<name> Text' OR 'emoji Text'
|
||||
// OR plain text. Split the first token; if it's a ph token render sprite +
|
||||
// remainder; otherwise render the whole label as text (back-compat).
|
||||
function renderConfigLabel(label) {
|
||||
if (label == null) return '';
|
||||
var s = String(label);
|
||||
var m = s.match(/^(ph:[a-z][a-z0-9-]+)\s+(.*)$/);
|
||||
if (m) return renderConfigGlyph(m[1]) + ' ' + esc(m[2]);
|
||||
return esc(s); // EMOJI-OK-LEGACY-RENDER
|
||||
}
|
||||
|
||||
// Current state
|
||||
let state = {};
|
||||
|
||||
@@ -695,16 +729,16 @@
|
||||
|
||||
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: 'display', label: '🖥️', title: 'Display' },
|
||||
{ id: 'export', label: '📤', title: 'Export / Save' }
|
||||
{ id: 'branding', glyph: 'ph:tag', title: 'Branding' },
|
||||
{ id: 'theme', glyph: 'ph:palette', title: 'Theme Colors' },
|
||||
{ id: 'nodes', glyph: 'ph:target', title: 'Colors' },
|
||||
{ id: 'home', glyph: 'ph:house', title: 'Home Page' },
|
||||
{ id: 'display', glyph: 'ph:monitor', title: 'Display' },
|
||||
{ id: 'export', glyph: 'ph:download-simple', title: 'Export / Save' }
|
||||
];
|
||||
return '<div class="cust-tabs">' +
|
||||
tabs.map(function (t) {
|
||||
return '<button class="cust-tab' + (t.id === activeTab ? ' active' : '') + '" data-tab="' + t.id + '" title="' + t.title + '">' + t.label + ' <span class="cust-tab-text">' + t.title + '</span></button>';
|
||||
return '<button class="cust-tab' + (t.id === activeTab ? ' active' : '') + '" data-tab="' + t.id + '" title="' + t.title + '">' + renderConfigGlyph(t.glyph) + ' <span class="cust-tab-text">' + t.title + '</span></button>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
@@ -775,7 +809,7 @@
|
||||
|
||||
function renderTheme() {
|
||||
var dark = isDarkMode();
|
||||
var modeLabel = dark ? '🌙 Dark Mode' : '☀️ Light Mode';
|
||||
var modeLabel = dark ? (renderConfigGlyph('ph:moon') + ' Dark Mode') : (renderConfigGlyph('ph:sun') + ' Light Mode');
|
||||
var defs = activeDefaults();
|
||||
var current = activeTheme();
|
||||
|
||||
@@ -800,7 +834,7 @@
|
||||
return '<div class="cust-panel' + (activeTab === 'theme' ? ' active' : '') + '" data-panel="theme">' +
|
||||
renderPresets() +
|
||||
'<p class="cust-section-title">' + modeLabel + '</p>' +
|
||||
'<p style="font-size:11px;color:var(--text-muted);margin:0 0 10px">Toggle ☀️/🌙 in nav to edit the other mode.</p>' +
|
||||
'<p style="font-size:11px;color:var(--text-muted);margin:0 0 10px">Toggle ' + renderConfigGlyph('ph:sun') + '/' + renderConfigGlyph('ph:moon') + ' in nav to edit the other mode.</p>' +
|
||||
basicRows +
|
||||
'<details class="cust-advanced"><summary style="font-size:12px;font-weight:600;cursor:pointer;color:var(--text-muted);margin:12px 0 8px">Advanced (' + ADVANCED_KEYS.length + ' options)</summary>' +
|
||||
advancedRows +
|
||||
@@ -818,7 +852,7 @@
|
||||
var val = state.nodeColors[key];
|
||||
var def = DEFAULTS.nodeColors[key];
|
||||
rows += '<div class="cust-color-row">' +
|
||||
'<div><label for="cust-node-' + key + '">' + NODE_EMOJI[key] + ' ' + NODE_LABELS[key] + '</label>' +
|
||||
'<div><label for="cust-node-' + key + '">' + renderConfigGlyph(NODE_EMOJI[key]) + ' ' + NODE_LABELS[key] + '</label>' +
|
||||
'<div class="cust-hint">' + (NODE_HINTS[key] || '') + '</div></div>' +
|
||||
'<input type="color" id="cust-node-' + key + '" data-node="' + key + '" value="' + val + '">' +
|
||||
'<span class="cust-node-dot" style="background:' + val + '" data-dot="' + key + '"></span>' +
|
||||
@@ -831,7 +865,7 @@
|
||||
var tval = state.typeColors[tkey];
|
||||
var tdef = DEFAULTS.typeColors[tkey];
|
||||
typeRows += '<div class="cust-color-row">' +
|
||||
'<div><label for="cust-type-' + tkey + '">' + (TYPE_EMOJI[tkey] || '') + ' ' + TYPE_LABELS[tkey] + '</label>' +
|
||||
'<div><label for="cust-type-' + tkey + '">' + renderConfigGlyph(TYPE_EMOJI[tkey] || '') + ' ' + TYPE_LABELS[tkey] + '</label>' +
|
||||
'<div class="cust-hint">' + (TYPE_HINTS[tkey] || '') + '</div></div>' +
|
||||
'<input type="color" id="cust-type-' + tkey + '" data-type-color="' + tkey + '" value="' + tval + '">' +
|
||||
'<span class="cust-node-dot" style="background:' + tval + '" data-tdot="' + tkey + '"></span>' +
|
||||
@@ -852,13 +886,13 @@
|
||||
'<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">' +
|
||||
'<p class="cust-section-title">Heatmap Opacity</p>' +
|
||||
'<div class="cust-color-row">' +
|
||||
'<div><label for="custHeatOpacity">🗺️ Nodes Map</label>' +
|
||||
'<div><label for="custHeatOpacity"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-map-trifold"/></svg> Nodes Map</label>' +
|
||||
'<div class="cust-hint">Heatmap overlay on the Nodes → Map page (0–100%)</div></div>' +
|
||||
'<input type="range" id="custHeatOpacity" min="0" max="100" value="' + heatPct + '" style="width:120px;cursor:pointer">' +
|
||||
'<span id="custHeatOpacityVal" style="font-family:var(--mono);font-size:12px;color:var(--text-muted);min-width:36px">' + heatPct + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="cust-color-row">' +
|
||||
'<div><label for="custLiveHeatOpacity">📡 Live Map</label>' +
|
||||
'<div><label for="custLiveHeatOpacity"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg> Live Map</label>' +
|
||||
'<div class="cust-hint">Heatmap overlay on the Live page (0–100%)</div></div>' +
|
||||
'<input type="range" id="custLiveHeatOpacity" min="0" max="100" value="' + liveHeatPct + '" style="width:120px;cursor:pointer">' +
|
||||
'<span id="custLiveHeatOpacityVal" style="font-family:var(--mono);font-size:12px;color:var(--text-muted);min-width:36px">' + liveHeatPct + '%</span>' +
|
||||
@@ -871,11 +905,16 @@
|
||||
var stepsHtml = h.steps.map(function (s, i) {
|
||||
return '<div class="cust-list-item" data-step="' + i + '">' +
|
||||
'<div class="cust-list-row">' +
|
||||
'<input class="cust-emoji-input" data-step-field="emoji" data-idx="' + i + '" value="' + escAttr(s.emoji) + '" placeholder="📡" aria-label="Step ' + (i + 1) + ' emoji">' +
|
||||
// Glyph input: accepts a 'ph:<name>' token OR a legacy emoji/text
|
||||
// string. Preview rendered alongside via renderConfigGlyph(); the
|
||||
// input field itself stays a plain text input so operators can
|
||||
// type either form. EMOJI-OK-LEGACY-RENDER.
|
||||
'<span class="cust-emoji-preview" aria-hidden="true" style="min-width:1em;display:inline-block">' + renderConfigGlyph(s.emoji) + '</span>' +
|
||||
'<input class="cust-emoji-input" data-step-field="emoji" data-idx="' + i + '" value="' + escAttr(s.emoji) + '" placeholder="ph:broadcast" aria-label="Step ' + (i + 1) + ' glyph (ph:name or emoji)">' +
|
||||
'<input data-step-field="title" data-idx="' + i + '" value="' + escAttr(s.title) + '" placeholder="Title" aria-label="Step ' + (i + 1) + ' title">' +
|
||||
'<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>' +
|
||||
'<button class="cust-list-btn" data-move-step="' + i + '" data-dir="up" title="Move up"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-up"/></svg></button>' +
|
||||
'<button class="cust-list-btn" data-move-step="' + i + '" data-dir="down" title="Move down"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-down"/></svg></button>' +
|
||||
'<button class="cust-list-btn danger" data-rm-step="' + i + '" title="Remove"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button>' +
|
||||
'</div>' +
|
||||
'<textarea data-step-field="description" data-idx="' + i + '" placeholder="Description" rows="2" aria-label="Step ' + (i + 1) + ' description">' + esc(s.description) + '</textarea>' +
|
||||
'<div class="cust-md-hint">Markdown: <code>**bold**</code> <code>*italic*</code> <code>`code`</code> <code>[text](url)</code> <code>- list</code></div>' +
|
||||
@@ -886,7 +925,7 @@
|
||||
return '<div class="cust-list-item" data-check="' + i + '">' +
|
||||
'<div class="cust-list-row">' +
|
||||
'<input data-check-field="question" data-idx="' + i + '" value="' + escAttr(c.question) + '" placeholder="Question" aria-label="Checklist item ' + (i + 1) + ' question">' +
|
||||
'<button class="cust-list-btn danger" data-rm-check="' + i + '" title="Remove">✕</button>' +
|
||||
'<button class="cust-list-btn danger" data-rm-check="' + i + '" title="Remove"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button>' +
|
||||
'</div>' +
|
||||
'<textarea data-check-field="answer" data-idx="' + i + '" placeholder="Answer" rows="2" aria-label="Checklist item ' + (i + 1) + ' answer">' + esc(c.answer) + '</textarea>' +
|
||||
'<div class="cust-md-hint">Markdown: <code>**bold**</code> <code>*italic*</code> <code>`code`</code> <code>[text](url)</code> <code>- list</code></div>' +
|
||||
@@ -896,8 +935,8 @@
|
||||
var linksHtml = h.footerLinks.map(function (l, i) {
|
||||
return '<div class="cust-list-item" data-link="' + i + '">' +
|
||||
'<div class="cust-list-row">' +
|
||||
'<input data-link-field="label" data-idx="' + i + '" value="' + escAttr(l.label) + '" placeholder="Label" aria-label="Footer link ' + (i + 1) + ' label">' +
|
||||
'<button class="cust-list-btn danger" data-rm-link="' + i + '" title="Remove">✕</button>' +
|
||||
'<input data-link-field="label" data-idx="' + i + '" value="' + escAttr(l.label) + '" placeholder="Label (e.g. ph:package Packets)" aria-label="Footer link ' + (i + 1) + ' label">' +
|
||||
'<button class="cust-list-btn danger" data-rm-link="' + i + '" title="Remove"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button>' +
|
||||
'</div>' +
|
||||
'<input data-link-field="url" data-idx="' + i + '" value="' + escAttr(l.url) + '" placeholder="URL" aria-label="Footer link ' + (i + 1) + ' URL">' +
|
||||
'</div>';
|
||||
@@ -979,17 +1018,17 @@
|
||||
'<p class="cust-section-title">My Preferences</p>' +
|
||||
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Save these colors just for you — stored in your browser, works on any instance.</p>' +
|
||||
'<div class="cust-export-btns" style="margin-bottom:16px">' +
|
||||
'<button class="cust-save-user" id="custSaveUser">💾 Save as my theme</button>' +
|
||||
(hasUserTheme ? '<button class="cust-reset-user" id="custResetUser">🗑️ Reset my theme</button>' : '') +
|
||||
'<button class="cust-save-user" id="custSaveUser"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-floppy-disk"/></svg> Save as my theme</button>' +
|
||||
(hasUserTheme ? '<button class="cust-reset-user" id="custResetUser"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-trash"/></svg> Reset my theme</button>' : '') +
|
||||
'</div>' +
|
||||
'<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">' +
|
||||
'<p class="cust-section-title">Admin</p>' +
|
||||
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Download or import a theme file. Admins place it as <code>theme.json</code> next to the server.</p>' +
|
||||
'<div class="cust-export-btns" style="margin-bottom:12px">' +
|
||||
'<button class="cust-dl-btn" id="custDownload">💾 Download theme.json</button>' +
|
||||
'<button class="cust-dl-btn" id="custImportFile">📂 Import File</button>' +
|
||||
'<button class="cust-dl-btn" id="custDownload"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-floppy-disk"/></svg> Download theme.json</button>' +
|
||||
'<button class="cust-dl-btn" id="custImportFile"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-folder-open"/></svg> Import File</button>' +
|
||||
'<input type="file" id="custImportInput" accept=".json,application/json" style="display:none" aria-label="Import theme file">' +
|
||||
'<button class="cust-copy-btn" id="custCopy">📋 Copy</button>' +
|
||||
'<button class="cust-copy-btn" id="custCopy"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clipboard-text"/></svg> Copy</button>' +
|
||||
'</div>' +
|
||||
'<details style="margin-top:8px"><summary style="font-size:12px;font-weight:600;cursor:pointer;color:var(--text-muted)">Raw JSON</summary>' +
|
||||
'<textarea class="cust-export-area" id="custExportJson" style="margin-top:8px" aria-label="Theme JSON data">' + esc(json) + '</textarea>' +
|
||||
@@ -1054,7 +1093,7 @@
|
||||
var iconEl = document.querySelector('.brand-icon');
|
||||
if (iconEl) {
|
||||
if (inp.value) { iconEl.innerHTML = '<img src="' + inp.value + '" style="height:24px" onerror="this.style.display=\'none\'">'; }
|
||||
else { iconEl.textContent = '📡'; }
|
||||
else { iconEl.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg>'; }
|
||||
}
|
||||
}
|
||||
if (inp.dataset.key === 'branding.faviconUrl') {
|
||||
@@ -1262,7 +1301,7 @@
|
||||
});
|
||||
var addStepBtn = document.getElementById('addStep');
|
||||
if (addStepBtn) addStepBtn.addEventListener('click', function () {
|
||||
state.home.steps.push({ emoji: '📌', title: '', description: '' });
|
||||
state.home.steps.push({ emoji: 'ph:push-pin', title: '', description: '' });
|
||||
render(container); autoSave();
|
||||
});
|
||||
|
||||
@@ -1310,8 +1349,8 @@
|
||||
var ta = document.getElementById('custExportJson');
|
||||
if (ta) {
|
||||
window.copyToClipboard(ta.value, function () {
|
||||
copyBtn.textContent = '✓ Copied!';
|
||||
setTimeout(function () { copyBtn.textContent = '📋 Copy to Clipboard'; }, 2000);
|
||||
copyBtn.textContent = 'Copied!';
|
||||
setTimeout(function () { copyBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clipboard-text"/></svg> Copy to Clipboard'; }, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1333,8 +1372,8 @@
|
||||
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);
|
||||
saveUserBtn.textContent = 'Saved!';
|
||||
setTimeout(function () { saveUserBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-floppy-disk"/></svg> Save as my theme'; }, 2000);
|
||||
});
|
||||
|
||||
// Reset user theme
|
||||
@@ -1387,11 +1426,11 @@
|
||||
autoSave();
|
||||
window.dispatchEvent(new CustomEvent('theme-changed'));
|
||||
render(container);
|
||||
importBtn.textContent = '✓ Imported!';
|
||||
setTimeout(function () { importBtn.textContent = '📂 Import File'; }, 2000);
|
||||
importBtn.textContent = 'Imported!';
|
||||
setTimeout(function () { importBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-folder-open"/></svg> Import File'; }, 2000);
|
||||
} catch (e) {
|
||||
importBtn.textContent = '✕ Invalid JSON';
|
||||
setTimeout(function () { importBtn.textContent = '📂 Import File'; }, 3000);
|
||||
importBtn.textContent = 'Invalid JSON';
|
||||
setTimeout(function () { importBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-folder-open"/></svg> Import File'; }, 3000);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
@@ -1415,8 +1454,8 @@
|
||||
panelEl.className = 'cust-overlay';
|
||||
panelEl.innerHTML =
|
||||
'<div class="cust-header">' +
|
||||
'<h2>🎨 Customize</h2>' +
|
||||
'<button class="cust-close" title="Close">✕</button>' +
|
||||
'<h2><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-palette"/></svg> Customize</h2>' +
|
||||
'<button class="cust-close" title="Close"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button>' +
|
||||
'</div>' +
|
||||
'<div class="cust-inner"></div>';
|
||||
document.body.appendChild(panelEl);
|
||||
|
||||
+3
-3
@@ -150,7 +150,7 @@
|
||||
'<ul>',
|
||||
'<li>Right-click any cell in the packet table to add a clause for that value.</li>',
|
||||
'<li>Type a partial field name to autocomplete; Tab/Enter accepts, Esc dismisses.</li>',
|
||||
'<li>Save commonly-used expressions via the ★ Save button — they appear in the Saved dropdown.</li>',
|
||||
'<li>Save commonly-used expressions via the <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-star"/></svg> Save button — they appear in the Saved dropdown.</li>',
|
||||
'</ul>',
|
||||
].join('');
|
||||
}
|
||||
@@ -169,7 +169,7 @@
|
||||
var pop = _h('div', { id: 'filterHelpPopover', class: 'modal fux-popover', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Filter syntax help' });
|
||||
pop.innerHTML =
|
||||
'<div class="fux-popover-header"><strong>Filter syntax</strong>' +
|
||||
'<button type="button" class="fux-popover-close" aria-label="Close">✕</button></div>' +
|
||||
'<button type="button" class="fux-popover-close" aria-label="Close"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button></div>' +
|
||||
'<div class="fux-popover-body">' + _buildHelpHtml() + '</div>';
|
||||
overlay.appendChild(pop);
|
||||
document.body.appendChild(overlay);
|
||||
@@ -362,7 +362,7 @@
|
||||
var list = SavedFilters.list();
|
||||
var rows = list.map(function(f, i) {
|
||||
var del = f.builtin ? '' :
|
||||
'<button type="button" class="fux-saved-del" data-name="' + _esc(f.name) + '" title="Delete">✕</button>';
|
||||
'<button type="button" class="fux-saved-del" data-name="' + _esc(f.name) + '" title="Delete"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg></button>';
|
||||
return '<div class="fux-saved-item" data-idx="' + i + '">' +
|
||||
'<span class="fux-saved-name">' + _esc(f.name) + '</span>' +
|
||||
'<span class="fux-saved-expr fux-mono">' + _esc(f.expr) + '</span>' +
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
/* #1648 M5: Phosphor sprite glyph — inherits currentColor, sized to font. */
|
||||
.ph-icon { width: 1em; height: 1em; fill: currentColor; vertical-align: -0.125em; }
|
||||
body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; }
|
||||
header { padding: 12px 16px; background: #0f0f23; border-bottom: 1px solid #333; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
||||
header h1 { font-size: 1rem; font-weight: 600; color: #4a9eff; white-space: nowrap; }
|
||||
@@ -49,10 +51,10 @@
|
||||
<a href="/" id="back-link">← CoreScope</a>
|
||||
<h1>GeoFilter Builder</h1>
|
||||
<div class="controls">
|
||||
<button id="btnUndo">↩ Undo</button>
|
||||
<button id="btnClear">✕ Clear</button>
|
||||
<button id="btnSaveDraft">💾 Save Draft</button>
|
||||
<button id="btnLoadDraft">📂 Load Draft</button>
|
||||
<button id="btnUndo"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-arrow-u-up-left"/></svg> Undo</button>
|
||||
<button id="btnClear"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg> Clear</button>
|
||||
<button id="btnSaveDraft"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-floppy-disk"/></svg> Save Draft</button>
|
||||
<button id="btnLoadDraft"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-folder-open"/></svg> Load Draft</button>
|
||||
</div>
|
||||
<div class="bufferRow">
|
||||
<label for="bufferKm">Buffer km:</label>
|
||||
@@ -182,8 +184,8 @@ document.getElementById('btnSaveDraft').addEventListener('click', function() {
|
||||
const bufferKm = parseFloat(document.getElementById('bufferKm').value) || 0;
|
||||
GeofilterDraft.saveDraft(points, bufferKm);
|
||||
const btn = document.getElementById('btnSaveDraft');
|
||||
btn.textContent = '✓ Saved';
|
||||
setTimeout(() => { btn.textContent = '💾 Save Draft'; }, 2000);
|
||||
btn.textContent = 'Saved';
|
||||
setTimeout(() => { btn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-floppy-disk"/></svg> Save Draft'; }, 2000);
|
||||
});
|
||||
|
||||
document.getElementById('btnLoadDraft').addEventListener('click', function() {
|
||||
|
||||
+22
-2
@@ -7,6 +7,26 @@
|
||||
let searchAbort = null; // AbortController for document-level listeners
|
||||
let _themeRefreshHandler = null;
|
||||
|
||||
// #1648 M5: home steps + footerLinks may store either a 'ph:<name>' sprite
|
||||
// token (new defaults) or a legacy emoji/text string (operator config).
|
||||
// Return safe HTML — sprite for ph tokens, escaped text for everything
|
||||
// else. EMOJI-OK-LEGACY-RENDER below preserves operator-stored emoji.
|
||||
function _renderHomeGlyph(value) {
|
||||
if (value == null) return '';
|
||||
const s = String(value);
|
||||
const m = s.match(/^ph:([a-z][a-z0-9-]+)$/);
|
||||
if (m) return `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-${m[1]}"/></svg>`;
|
||||
return _escHtml(s); // EMOJI-OK-LEGACY-RENDER
|
||||
}
|
||||
function _renderHomeLabel(label) {
|
||||
if (label == null) return '';
|
||||
const s = String(label);
|
||||
const m = s.match(/^(ph:[a-z][a-z0-9-]+)\s+(.*)$/);
|
||||
if (m) return _renderHomeGlyph(m[1]) + ' ' + _escHtml(m[2]);
|
||||
return _escHtml(s); // EMOJI-OK-LEGACY-RENDER
|
||||
}
|
||||
function _escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
const PREF_KEY = 'meshcore-user-level';
|
||||
const MY_NODES_KEY = 'meshcore-my-nodes'; // [{pubkey, name, addedAt}]
|
||||
|
||||
@@ -119,7 +139,7 @@
|
||||
|
||||
<section class="home-footer">
|
||||
<div class="home-footer-links">
|
||||
${homeCfg?.footerLinks ? homeCfg.footerLinks.map(l => `<a href="${escapeAttr(l.url)}" class="home-footer-link" target="_blank" rel="noopener">${escapeHtml(l.label)}</a>`).join('') : `
|
||||
${homeCfg?.footerLinks ? homeCfg.footerLinks.map(l => `<a href="${escapeAttr(l.url)}" class="home-footer-link" target="_blank" rel="noopener">${_renderHomeLabel(l.label)}</a>`).join('') : `
|
||||
<a href="#/packets" class="home-footer-link"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-package"/></svg> Packets</a>
|
||||
<a href="#/map" class="home-footer-link"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-map-trifold"/></svg> Network Map</a>
|
||||
<a href="#/live" class="home-footer-link"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg> Live</a>
|
||||
@@ -540,7 +560,7 @@
|
||||
var html = '';
|
||||
// Render steps (getting started guide)
|
||||
if (homeCfg?.steps?.length) {
|
||||
html += homeCfg.steps.map(s => `<div class="checklist-item"><div class="checklist-q" role="button" tabindex="0" aria-expanded="false">${escapeHtml(s.emoji || '')} ${escapeHtml(s.title)}</div><div class="checklist-a">${window.miniMarkdown ? miniMarkdown(s.description) : escapeHtml(s.description)}</div></div>`).join('');
|
||||
html += homeCfg.steps.map(s => `<div class="checklist-item"><div class="checklist-q" role="button" tabindex="0" aria-expanded="false">${_renderHomeGlyph(s.emoji || '')} ${escapeHtml(s.title)}</div><div class="checklist-a">${window.miniMarkdown ? miniMarkdown(s.description) : escapeHtml(s.description)}</div></div>`).join('');
|
||||
}
|
||||
// Render FAQ/checklist (additional Q&A)
|
||||
if (homeCfg?.checklist?.length) {
|
||||
|
||||
@@ -29,11 +29,14 @@
|
||||
<symbol id="ph-crosshair" viewBox="0 0 256 256"><path d="M232,120h-8.34A96.14,96.14,0,0,0,136,32.34V24a8,8,0,0,0-16,0v8.34A96.14,96.14,0,0,0,32.34,120H24a8,8,0,0,0,0,16h8.34A96.14,96.14,0,0,0,120,223.66V232a8,8,0,0,0,16,0v-8.34A96.14,96.14,0,0,0,223.66,136H232a8,8,0,0,0,0-16Zm-96,87.6V200a8,8,0,0,0-16,0v7.6A80.15,80.15,0,0,1,48.4,136H56a8,8,0,0,0,0-16H48.4A80.15,80.15,0,0,1,120,48.4V56a8,8,0,0,0,16,0V48.4A80.15,80.15,0,0,1,207.6,120H200a8,8,0,0,0,0,16h7.6A80.15,80.15,0,0,1,136,207.6ZM128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Z"/></symbol>
|
||||
<symbol id="ph-diamond" viewBox="0 0 256 256"><path d="M235.33,116.72,139.28,20.66a16,16,0,0,0-22.56,0l-96,96.06a16,16,0,0,0,0,22.56l96.05,96.06h0a16,16,0,0,0,22.56,0l96.05-96.06a16,16,0,0,0,0-22.56ZM128,224h0L32,128,128,32,224,128Z"/></symbol>
|
||||
<symbol id="ph-dice-five" viewBox="0 0 256 256"><path d="M192,32H64A32,32,0,0,0,32,64V192a32,32,0,0,0,32,32H192a32,32,0,0,0,32-32V64A32,32,0,0,0,192,32Zm16,160a16,16,0,0,1-16,16H64a16,16,0,0,1-16-16V64A16,16,0,0,1,64,48H192a16,16,0,0,1,16,16ZM104,92A12,12,0,1,1,92,80,12,12,0,0,1,104,92Zm72,0a12,12,0,1,1-12-12A12,12,0,0,1,176,92Zm-72,72a12,12,0,1,1-12-12A12,12,0,0,1,104,164Zm36-36a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm36,36a12,12,0,1,1-12-12A12,12,0,0,1,176,164Z"/></symbol>
|
||||
<symbol id="ph-download-simple" viewBox="0 0 256 256"><path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"/></symbol>
|
||||
<symbol id="ph-envelope" viewBox="0 0 256 256"><path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48Zm-96,85.15L52.57,64H203.43ZM98.71,128,40,181.81V74.19Zm11.84,10.85,12,11.05a8,8,0,0,0,10.82,0l12-11.05,58,53.15H52.57ZM157.29,128,216,74.18V181.82Z"/></symbol>
|
||||
<symbol id="ph-envelope-simple" viewBox="0 0 256 256"><path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48ZM203.43,64,128,133.15,52.57,64ZM216,192H40V74.19l82.59,75.71a8,8,0,0,0,10.82,0L216,74.19V192Z"/></symbol>
|
||||
<symbol id="ph-eye" viewBox="0 0 256 256"><path d="M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"/></symbol>
|
||||
<symbol id="ph-flame" viewBox="0 0 256 256"><path d="M173.79,51.48a221.25,221.25,0,0,0-41.67-34.34,8,8,0,0,0-8.24,0A221.25,221.25,0,0,0,82.21,51.48C54.59,80.48,40,112.47,40,144a88,88,0,0,0,176,0C216,112.47,201.41,80.48,173.79,51.48ZM96,184c0-27.67,22.53-47.28,32-54.3,9.48,7,32,26.63,32,54.3a32,32,0,0,1-64,0Zm77.27,15.93A47.8,47.8,0,0,0,176,184c0-44-42.09-69.79-43.88-70.86a8,8,0,0,0-8.24,0C122.09,114.21,80,140,80,184a47.8,47.8,0,0,0,2.73,15.93A71.88,71.88,0,0,1,56,144c0-34.41,20.4-63.15,37.52-81.19A216.21,216.21,0,0,1,128,33.54a215.77,215.77,0,0,1,34.48,29.27C193.49,95.5,200,125,200,144A71.88,71.88,0,0,1,173.27,199.93Z"/></symbol>
|
||||
<symbol id="ph-flag" viewBox="0 0 256 256"><path d="M42.76,50A8,8,0,0,0,40,56V224a8,8,0,0,0,16,0V179.77c26.79-21.16,49.87-9.75,76.45,3.41,16.4,8.11,34.06,16.85,53,16.85,13.93,0,28.54-4.75,43.82-18a8,8,0,0,0,2.76-6V56A8,8,0,0,0,218.76,50c-28,24.23-51.72,12.49-79.21-1.12C111.07,34.76,78.78,18.79,42.76,50ZM216,172.25c-26.79,21.16-49.87,9.74-76.45-3.41-25-12.35-52.81-26.13-83.55-8.4V59.79c26.79-21.16,49.87-9.75,76.45,3.4,25,12.35,52.82,26.13,83.55,8.4Z"/></symbol>
|
||||
<symbol id="ph-floppy-disk" viewBox="0 0 256 256"><path d="M219.31,72,184,36.69A15.86,15.86,0,0,0,172.69,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V83.31A15.86,15.86,0,0,0,219.31,72ZM168,208H88V152h80Zm40,0H184V152a16,16,0,0,0-16-16H88a16,16,0,0,0-16,16v56H48V48H172.69L208,83.31ZM160,72a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h56A8,8,0,0,1,160,72Z"/></symbol>
|
||||
<symbol id="ph-folder-open" viewBox="0 0 256 256"><path d="M245,110.64A16,16,0,0,0,232,104H216V88a16,16,0,0,0-16-16H130.67L102.94,51.2a16.14,16.14,0,0,0-9.6-3.2H40A16,16,0,0,0,24,64V208h0a8,8,0,0,0,8,8H211.1a8,8,0,0,0,7.59-5.47l28.49-85.47A16.05,16.05,0,0,0,245,110.64ZM93.34,64,123.2,86.4A8,8,0,0,0,128,88h72v16H69.77a16,16,0,0,0-15.18,10.94L40,158.7V64Zm112,136H43.1l26.67-80H232Z"/></symbol>
|
||||
<symbol id="ph-gear" viewBox="0 0 256 256"><path d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm88-29.84q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.21,107.21,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.71,107.71,0,0,0-26.25-10.87,8,8,0,0,0-7.06,1.49L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.21,107.21,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06Zm-16.1-6.5a73.93,73.93,0,0,1,0,8.68,8,8,0,0,0,1.74,5.48l14.19,17.73a91.57,91.57,0,0,1-6.23,15L187,173.11a8,8,0,0,0-5.1,2.64,74.11,74.11,0,0,1-6.14,6.14,8,8,0,0,0-2.64,5.1l-2.51,22.58a91.32,91.32,0,0,1-15,6.23l-17.74-14.19a8,8,0,0,0-5-1.75h-.48a73.93,73.93,0,0,1-8.68,0,8,8,0,0,0-5.48,1.74L100.45,215.8a91.57,91.57,0,0,1-15-6.23L82.89,187a8,8,0,0,0-2.64-5.1,74.11,74.11,0,0,1-6.14-6.14,8,8,0,0,0-5.1-2.64L46.43,170.6a91.32,91.32,0,0,1-6.23-15l14.19-17.74a8,8,0,0,0,1.74-5.48,73.93,73.93,0,0,1,0-8.68,8,8,0,0,0-1.74-5.48L40.2,100.45a91.57,91.57,0,0,1,6.23-15L69,82.89a8,8,0,0,0,5.1-2.64,74.11,74.11,0,0,1,6.14-6.14A8,8,0,0,0,82.89,69L85.4,46.43a91.32,91.32,0,0,1,15-6.23l17.74,14.19a8,8,0,0,0,5.48,1.74,73.93,73.93,0,0,1,8.68,0,8,8,0,0,0,5.48-1.74L155.55,40.2a91.57,91.57,0,0,1,15,6.23L173.11,69a8,8,0,0,0,2.64,5.1,74.11,74.11,0,0,1,6.14,6.14,8,8,0,0,0,5.1,2.64l22.58,2.51a91.32,91.32,0,0,1,6.23,15l-14.19,17.74A8,8,0,0,0,199.87,123.66Z"/></symbol>
|
||||
<symbol id="ph-globe" viewBox="0 0 256 256"><path d="M128,24h0A104,104,0,1,0,232,128,104.12,104.12,0,0,0,128,24Zm88,104a87.61,87.61,0,0,1-3.33,24H174.16a157.44,157.44,0,0,0,0-48h38.51A87.61,87.61,0,0,1,216,128ZM102,168H154a115.11,115.11,0,0,1-26,45A115.27,115.27,0,0,1,102,168Zm-3.9-16a140.84,140.84,0,0,1,0-48h59.88a140.84,140.84,0,0,1,0,48ZM40,128a87.61,87.61,0,0,1,3.33-24H81.84a157.44,157.44,0,0,0,0,48H43.33A87.61,87.61,0,0,1,40,128ZM154,88H102a115.11,115.11,0,0,1,26-45A115.27,115.27,0,0,1,154,88Zm52.33,0H170.71a135.28,135.28,0,0,0-22.3-45.6A88.29,88.29,0,0,1,206.37,88ZM107.59,42.4A135.28,135.28,0,0,0,85.29,88H49.63A88.29,88.29,0,0,1,107.59,42.4ZM49.63,168H85.29a135.28,135.28,0,0,0,22.3,45.6A88.29,88.29,0,0,1,49.63,168Zm98.78,45.6a135.28,135.28,0,0,0,22.3-45.6h35.66A88.29,88.29,0,0,1,148.41,213.6Z"/></symbol>
|
||||
<symbol id="ph-graph" viewBox="0 0 256 256"><path d="M200,152a31.84,31.84,0,0,0-19.53,6.68l-23.11-18A31.65,31.65,0,0,0,160,128c0-.74,0-1.48-.08-2.21l13.23-4.41A32,32,0,1,0,168,104c0,.74,0,1.48.08,2.21l-13.23,4.41A32,32,0,0,0,128,96a32.59,32.59,0,0,0-5.27.44L115.89,81A32,32,0,1,0,96,88a32.59,32.59,0,0,0,5.27-.44l6.84,15.4a31.92,31.92,0,0,0-8.57,39.64L73.83,165.44a32.06,32.06,0,1,0,10.63,12l25.71-22.84a31.91,31.91,0,0,0,37.36-1.24l23.11,18A31.65,31.65,0,0,0,168,184a32,32,0,1,0,32-32Zm0-64a16,16,0,1,1-16,16A16,16,0,0,1,200,88ZM80,56A16,16,0,1,1,96,72,16,16,0,0,1,80,56ZM56,208a16,16,0,1,1,16-16A16,16,0,0,1,56,208Zm56-80a16,16,0,1,1,16,16A16,16,0,0,1,112,128Zm88,72a16,16,0,1,1,16-16A16,16,0,0,1,200,200Z"/></symbol>
|
||||
@@ -84,8 +87,10 @@
|
||||
<symbol id="ph-star" viewBox="0 0 256 256"><path d="M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z"/></symbol>
|
||||
<symbol id="ph-star-fill" viewBox="0 0 256 256"><path d="M234.29,114.85l-45,38.83L203,211.75a16.4,16.4,0,0,1-24.5,17.82L128,198.49,77.47,229.57A16.4,16.4,0,0,1,53,211.75l13.76-58.07-45-38.83A16.46,16.46,0,0,1,31.08,86l59-4.76,22.76-55.08a16.36,16.36,0,0,1,30.27,0l22.75,55.08,59,4.76a16.46,16.46,0,0,1,9.37,28.86Z"/></symbol>
|
||||
<symbol id="ph-sun" viewBox="0 0 256 256"><path d="M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"/></symbol>
|
||||
<symbol id="ph-tag" viewBox="0 0 256 256"><path d="M243.31,136,144,36.69A15.86,15.86,0,0,0,132.69,32H40a8,8,0,0,0-8,8v92.69A15.86,15.86,0,0,0,36.69,144L136,243.31a16,16,0,0,0,22.63,0l84.68-84.68a16,16,0,0,0,0-22.63Zm-96,96L48,132.69V48h84.69L232,147.31ZM96,84A12,12,0,1,1,84,72,12,12,0,0,1,96,84Z"/></symbol>
|
||||
<symbol id="ph-target" viewBox="0 0 256 256"><path d="M221.87,83.16A104.1,104.1,0,1,1,195.67,49l22.67-22.68a8,8,0,0,1,11.32,11.32l-96,96a8,8,0,0,1-11.32-11.32l27.72-27.72a40,40,0,1,0,17.87,31.09,8,8,0,1,1,16-.9,56,56,0,1,1-22.38-41.65L184.3,60.39a87.88,87.88,0,1,0,23.13,29.67,8,8,0,0,1,14.44-6.9Z"/></symbol>
|
||||
<symbol id="ph-thermometer" viewBox="0 0 256 256"><path d="M212,56a28,28,0,1,0,28,28A28,28,0,0,0,212,56Zm0,40a12,12,0,1,1,12-12A12,12,0,0,1,212,96Zm-84,57V88a8,8,0,0,0-16,0v65a32,32,0,1,0,16,0Zm-8,47a16,16,0,1,1,16-16A16,16,0,0,1,120,200Zm40-66V48a40,40,0,0,0-80,0v86a64,64,0,1,0,80,0Zm-40,98a48,48,0,0,1-27.42-87.4A8,8,0,0,0,96,138V48a24,24,0,0,1,48,0v90a8,8,0,0,0,3.42,6.56A48,48,0,0,1,120,232Z"/></symbol>
|
||||
<symbol id="ph-trash" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"/></symbol>
|
||||
<symbol id="ph-trend-up" viewBox="0 0 256 256"><path d="M240,56v64a8,8,0,0,1-16,0V75.31l-82.34,82.35a8,8,0,0,1-11.32,0L96,123.31,29.66,189.66a8,8,0,0,1-11.32-11.32l72-72a8,8,0,0,1,11.32,0L136,140.69,212.69,64H168a8,8,0,0,1,0-16h64A8,8,0,0,1,240,56Z"/></symbol>
|
||||
<symbol id="ph-triangle" viewBox="0 0 256 256"><path d="M236.8,188.09,149.35,36.22a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.34,24.34,0,0,0,40.55,224h174.9a24.34,24.34,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8Z"/></symbol>
|
||||
<symbol id="ph-trophy" viewBox="0 0 256 256"><path d="M232,64H208V48a8,8,0,0,0-8-8H56a8,8,0,0,0-8,8V64H24A16,16,0,0,0,8,80V96a40,40,0,0,0,40,40h3.65A80.13,80.13,0,0,0,120,191.61V216H96a8,8,0,0,0,0,16h64a8,8,0,0,0,0-16H136V191.58c31.94-3.23,58.44-25.64,68.08-55.58H208a40,40,0,0,0,40-40V80A16,16,0,0,0,232,64ZM48,120A24,24,0,0,1,24,96V80H48v32q0,4,.39,8Zm144-8.9c0,35.52-29,64.64-64,64.9a64,64,0,0,1-64-64V56H192ZM232,96a24,24,0,0,1-24,24h-.5a81.81,81.81,0,0,0,.5-8.9V80h24Z"/></symbol>
|
||||
|
||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
+15
-10
@@ -19,7 +19,7 @@
|
||||
}
|
||||
|
||||
/* Status-token color classes (#1648 M3).
|
||||
* Replace 🟢🟡🔴⚪/✅⚠️❌ emoji dots with Phosphor sprites that inherit
|
||||
* Replace 🟢🟡🔴⚪/✅⚠️❌ emoji dots with Phosphor sprites that inherit EMOJI-OK-COMMENT
|
||||
* their color via currentColor. Each class threads the existing
|
||||
* --status-* color token so dark/light theming continues to work.
|
||||
*
|
||||
@@ -242,7 +242,7 @@
|
||||
--z-tooltip: 9200;
|
||||
}
|
||||
|
||||
/* ⚠️ DARK THEME VARIABLES — KEEP BOTH BLOCKS IN SYNC
|
||||
/* ⚠️ DARK THEME VARIABLES — KEEP BOTH BLOCKS IN SYNC EMOJI-OK-COMMENT
|
||||
The media query handles OS-level dark mode (auto); [data-theme="dark"] handles manual toggle.
|
||||
When changing dark theme variables, update BOTH blocks below. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -278,7 +278,7 @@
|
||||
--section-bg: #1e1e34;
|
||||
}
|
||||
}
|
||||
/* ⚠️ DARK THEME VARIABLES — KEEP IN SYNC with @media block above */
|
||||
/* ⚠️ DARK THEME VARIABLES — KEEP IN SYNC with @media block above (EMOJI-OK-COMMENT) */
|
||||
[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
/* Brighter link-strength hues for contrast on dark map tiles + surfaces. */
|
||||
@@ -836,7 +836,7 @@ img.brand-logo {
|
||||
/* === Filter toolbar (#1122)
|
||||
* Toolbar layout is composed of fenced .filter-group clusters:
|
||||
* 1. Quick filters — text inputs (hash, node, observer, region, type, channel)
|
||||
* 2. Toggles — Group by Hash, ★ My Nodes
|
||||
* 2. Toggles — Group by Hash, ★ My Nodes EMOJI-OK-COMMENT
|
||||
* 3. Time window — last-N selector
|
||||
* 4. Sort & view options — observer sort, columns toggle, hex paths
|
||||
* Adjacent .filter-group + .filter-group siblings get a left divider + padding
|
||||
@@ -1051,7 +1051,7 @@ img.brand-logo {
|
||||
/* #4: sheet structure — kill handle, sticky header inside sheet, clear
|
||||
* bottom-nav (58px tall) so last row doesn't get cut off, taller max */
|
||||
/* Restructure: outer sheet has fixed height + overflow:hidden; the
|
||||
* .mobile-sheet-content is the ONLY scroll region. ✕ close stays sticky
|
||||
* .mobile-sheet-content is the ONLY scroll region. ✕ close stays sticky EMOJI-OK-COMMENT
|
||||
* pinned at top of the sheet. Title also stays sticky within the
|
||||
* scroll region. */
|
||||
.mobile-detail-sheet {
|
||||
@@ -1276,7 +1276,7 @@ body.scroll-locked { overflow: hidden; }
|
||||
}
|
||||
.detail-meta dt { color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: .3px; }
|
||||
.detail-meta dd { font-weight: 500; margin-bottom: 4px; }
|
||||
/* #1281: 📍map link inside detail-meta — UA-default blue is unreadable on
|
||||
/* #1281: 📍map link inside detail-meta — UA-default blue is unreadable on EMOJI-OK-COMMENT
|
||||
* dark backgrounds. Force theme-aware color via --accent. */
|
||||
.loc-map-link { color: var(--accent); font-size: 0.85em; text-decoration: none; }
|
||||
.loc-map-link:hover { text-decoration: underline; }
|
||||
@@ -1590,7 +1590,7 @@ button.ch-item.selected { background: var(--selected-bg); }
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
/* Shared icon button base for sidebar row controls (remove ✕, share ⤴).
|
||||
/* Shared icon button base for sidebar row controls (remove ✕, share ⤴). EMOJI-OK-COMMENT
|
||||
WCAG 2.5.5 / Apple HIG: 44x44 CSS px minimum touch target. */
|
||||
.ch-icon-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
@@ -2193,8 +2193,13 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; }
|
||||
#chList .ch-item-name { flex: 1 1 auto; min-width: 0; }
|
||||
#chList .ch-item-top { gap: 6px; }
|
||||
#chList .ch-icon-btn { min-width: 32px; min-height: 32px; padding: 2px 4px; }
|
||||
/* #1648 M5: share-btn now renders an inline <svg.ph-icon> from channels.js,
|
||||
* so the legacy `font-size: 0` + `::before { content: '<emoji>' }` shim is
|
||||
* obsolete. The icon-button class above already sets the touch target;
|
||||
* the SVG inherits color via currentColor. */
|
||||
#chList .ch-share-btn .ph-icon { font-size: 13px; }
|
||||
#chList .ch-share-btn { font-size: 0; }
|
||||
#chList .ch-share-btn::before { content: '📤'; font-size: 13px; }
|
||||
#chList .ch-share-btn .ph-icon, #chList .ch-share-btn svg { font-size: 13px; }
|
||||
#chList .ch-remove-btn { font-size: 12px; padding: 2px 6px; }
|
||||
/* Hide the analytics-style time on mobile rows to free width for the name. */
|
||||
#chList .ch-item-time { font-size: 10px; }
|
||||
@@ -4154,12 +4159,12 @@ th.sort-active { color: var(--accent, #60a5fa); }
|
||||
* Design source: github.com/Kpa-clawbot/CoreScope/issues/1356 (Tufte + audit comments).
|
||||
*
|
||||
* Carriers (NON-color) of meaning, per WCAG 1.4.1:
|
||||
* - V1 cluster bubbles: size (40/48/56px) + numeral + border-style ramp
|
||||
* - V1 cluster bubbles: size (40/48/56px) + numeral + border-style ramp EMOJI-OK-COMMENT
|
||||
* (1.5px solid / 2.5px solid / 2px double). Fill is a single neutral.
|
||||
* - V2 role pills: capital-letter prefix (R/C/M/S/O). Wong (2011) palette
|
||||
* hue is secondary. Dark text (#1a1a1a) on ALL five pills (audit override
|
||||
* so only ONE text-color rule is needed and every pill passes 4.5:1).
|
||||
* - V3 multi-byte hash labels: unicode glyph prefix (✓/?/✗) + neutral fill
|
||||
* - V3 multi-byte hash labels: unicode glyph prefix (✓/?/✗) + neutral fill EMOJI-OK-COMMENT
|
||||
* + 3px colored left-border using the audit's high-luminance accent set
|
||||
* (NOT Tol "vibrant" — those failed 3:1 vs the neutral fill).
|
||||
*
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env node
|
||||
/* Issue #1648 — M5: emoji → Phosphor sprite migration (static scan).
|
||||
*
|
||||
* M5 covers settings, customize, modals, misc surfaces:
|
||||
* customize.js, customize-v2.js (config DEFAULTS + UI controls — must
|
||||
* keep emoji branch in render path for back-compat with operator
|
||||
* stored values),
|
||||
* geofilter-builder.html, audio.js, filter-ux.js, style.css ::before
|
||||
* glyph, cmd/server/routes.go (lockstep with customize-v2.js defaults).
|
||||
*
|
||||
* Asserts (per file):
|
||||
* 1. Zero UI-iconography codepoints outside an allowlist
|
||||
* (// EMOJI-OK / EMOJI-OK-LEGACY-RENDER / EMOJI-OK-COMMENT lines).
|
||||
* For customize.js / customize-v2.js, the LEGACY-RENDER allowlist
|
||||
* flags the back-compat branch of renderConfigGlyph() that MUST
|
||||
* still accept operator-stored emoji values at runtime (design
|
||||
* call #1 from #1648 M5).
|
||||
* 2. The in-source DEFAULTS for customize.js / customize-v2.js use
|
||||
* `ph:<name>` tokens (asserted by grep for known keys).
|
||||
* 3. ≥N <use href="...#ph-..."> refs per file (sanity floor).
|
||||
*
|
||||
* Anti-tautology: this test FAILS pre-implementation by construction —
|
||||
* every M5 file has at least one icon emoji or misc-symbol today.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const ROOT = path.resolve(__dirname);
|
||||
const PUB = path.join(ROOT, 'public');
|
||||
|
||||
const M5_FILES = [
|
||||
'public/customize.js',
|
||||
'public/customize-v2.js',
|
||||
'public/geofilter-builder.html',
|
||||
'public/audio.js',
|
||||
'public/filter-ux.js',
|
||||
'public/style.css',
|
||||
'cmd/server/routes.go',
|
||||
];
|
||||
|
||||
const MIN_USE_REFS = {
|
||||
'public/customize.js': 4,
|
||||
'public/customize-v2.js': 4,
|
||||
'public/geofilter-builder.html': 3,
|
||||
'public/audio.js': 1,
|
||||
'public/filter-ux.js': 2, // M2 already added some
|
||||
'public/style.css': 0, // CSS uses background-image SVG data-URI; not a literal <use>
|
||||
'cmd/server/routes.go': 0,
|
||||
};
|
||||
|
||||
const EMOJI = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u;
|
||||
const MISC_ICON = /[◆●■▲★☆○✓✗⚠✉✕]/u;
|
||||
|
||||
const ALLOW_TOKENS = ['EMOJI-OK', 'EMOJI-OK-LEGACY-RENDER', 'EMOJI-OK-COMMENT'];
|
||||
|
||||
function scanFile(rel) {
|
||||
const abs = path.join(ROOT, rel);
|
||||
const txt = fs.readFileSync(abs, 'utf8');
|
||||
const lines = txt.split('\n');
|
||||
const hits = [];
|
||||
lines.forEach((line, idx) => {
|
||||
if (ALLOW_TOKENS.some(t => line.includes(t))) return;
|
||||
if (EMOJI.test(line) || MISC_ICON.test(line)) {
|
||||
hits.push({ file: rel, line: idx + 1, text: line.trim().slice(0, 200) });
|
||||
}
|
||||
});
|
||||
return hits;
|
||||
}
|
||||
|
||||
function countUseRefs(rel) {
|
||||
const abs = path.join(ROOT, rel);
|
||||
const txt = fs.readFileSync(abs, 'utf8');
|
||||
return (txt.match(/<use href="\/icons\/phosphor-sprite\.svg#ph-/g) || []).length;
|
||||
}
|
||||
|
||||
function assertSpriteHasM5Icons() {
|
||||
const sp = path.join(PUB, 'icons', 'phosphor-sprite.svg');
|
||||
const txt = fs.readFileSync(sp, 'utf8');
|
||||
const need = [
|
||||
'ph-tag', 'ph-trash', 'ph-floppy-disk', 'ph-folder-open',
|
||||
'ph-download-simple', 'ph-x', 'ph-check', 'ph-warning',
|
||||
'ph-speaker-high', 'ph-star', 'ph-bluetooth',
|
||||
];
|
||||
const missing = need.filter(id => !txt.includes(`id="${id}"`));
|
||||
if (missing.length) throw new Error(`sprite missing M5 symbols: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
function assertCustomizeDefaultsUsePhTokens() {
|
||||
// Both files must store sprite tokens (ph:<name>) in DEFAULTS, not emoji.
|
||||
// The string `ph:` should appear in the NODE_EMOJI / TYPE_EMOJI / steps /
|
||||
// footerLinks DEFAULTS region of both files. We assert by literal-count.
|
||||
for (const rel of ['public/customize.js', 'public/customize-v2.js']) {
|
||||
const txt = fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
||||
const phTokens = (txt.match(/['"]ph:[a-z-]+['"]/g) || []).length;
|
||||
assert.ok(phTokens >= 10,
|
||||
`${rel} must have ≥10 'ph:<name>' DEFAULT tokens (got ${phTokens})`);
|
||||
assert.ok(/renderConfigGlyph\s*\(/.test(txt),
|
||||
`${rel} must define a renderConfigGlyph() helper (back-compat for legacy emoji values)`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertRoutesGoUsesPhTokens() {
|
||||
const txt = fs.readFileSync(path.join(ROOT, 'cmd/server/routes.go'), 'utf8');
|
||||
// Onboarding home defaults must echo Phosphor tokens, not raw emoji.
|
||||
// 4 steps × 1 token + 2 footerLinks × 1 token = 6 occurrences minimum.
|
||||
const phTokens = (txt.match(/ph:[a-z-]+/g) || []).length;
|
||||
assert.ok(phTokens >= 6,
|
||||
`cmd/server/routes.go must echo ≥6 'ph:<name>' tokens in home defaults (got ${phTokens})`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
let failed = 0;
|
||||
console.log('— Issue #1648 M5 — emoji/misc-icon scan');
|
||||
|
||||
try { assertSpriteHasM5Icons(); console.log(' ✓ sprite has required M5 symbols'); }
|
||||
catch (e) { console.error(` ✗ ${e.message}`); failed++; }
|
||||
|
||||
try { assertCustomizeDefaultsUsePhTokens(); console.log(' ✓ customize.js + customize-v2.js DEFAULTS use ph: tokens'); }
|
||||
catch (e) { console.error(` ✗ ${e.message}`); failed++; }
|
||||
|
||||
try { assertRoutesGoUsesPhTokens(); console.log(' ✓ cmd/server/routes.go home defaults use ph: tokens'); }
|
||||
catch (e) { console.error(` ✗ ${e.message}`); failed++; }
|
||||
|
||||
for (const rel of M5_FILES) {
|
||||
const hits = scanFile(rel);
|
||||
if (hits.length === 0) {
|
||||
console.log(` ✓ ${rel} clean`);
|
||||
} else {
|
||||
console.error(` ✗ ${rel} has ${hits.length} emoji/misc-icon hit(s):`);
|
||||
for (const h of hits.slice(0, 30)) console.error(` ${h.file}:${h.line} ${h.text}`);
|
||||
if (hits.length > 30) console.error(` … (+${hits.length - 30} more)`);
|
||||
failed++;
|
||||
}
|
||||
const min = MIN_USE_REFS[rel] || 0;
|
||||
if (min > 0) {
|
||||
const useRefs = countUseRefs(rel);
|
||||
if (useRefs < min) {
|
||||
console.error(` ✗ ${rel} has only ${useRefs} <use href="…#ph-…"> refs (expected ≥${min})`);
|
||||
failed++;
|
||||
} else {
|
||||
console.log(` ✓ ${rel} has ${useRefs} Phosphor <use> refs (≥${min})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
console.error(`\nFAIL: ${failed} M5 check(s) failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Hard asserts for CI
|
||||
for (const rel of M5_FILES) {
|
||||
const hits = scanFile(rel);
|
||||
assert.strictEqual(hits.length, 0,
|
||||
`${rel} must contain zero emoji/misc-icon iconography (got ${hits.length} hit(s))`);
|
||||
const min = MIN_USE_REFS[rel] || 0;
|
||||
if (min > 0) {
|
||||
const useRefs = countUseRefs(rel);
|
||||
assert.ok(useRefs >= min,
|
||||
`${rel} must have ≥${min} <use href="…#ph-…"> refs (got ${useRefs})`);
|
||||
}
|
||||
}
|
||||
console.log('\nPASS: all M5 surfaces icon-free and Phosphor-swapped');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env node
|
||||
/* Issue #1648 — M5: emoji → Phosphor sprite (E2E behavioral).
|
||||
*
|
||||
* Asserts (in a real Chromium against a running server):
|
||||
* (a) Customize panel — toggling the customizer renders Phosphor sprites
|
||||
* in tab labels, header, and reset/save buttons.
|
||||
* (b) Customize panel BACK-COMPAT: when localStorage has operator config
|
||||
* with a legacy emoji step value, that emoji must STILL render — the
|
||||
* new code must NOT override operator-stored emoji values (design
|
||||
* call #1 from the M5 brief).
|
||||
* (c) /channels modal open — modal action buttons render sprites.
|
||||
* (d) /audio-lab — audio unlock prompt + action icons render sprites.
|
||||
* (e) geofilter-builder.html — control buttons render sprites.
|
||||
* (f) NO .notdef anywhere — every <use> resolves to a defined symbol id.
|
||||
*
|
||||
* CI gating: CHROMIUM_REQUIRE=1 makes Chromium-launch failure a HARD FAIL.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
const assert = require('assert');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
const M5_FORBIDDEN_RE = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}◆●■▲★☆○✓✗⚠✉✕]/u;
|
||||
|
||||
let passes = 0, failures = 0;
|
||||
function pass(msg) { console.log(` ✓ ${msg}`); passes++; }
|
||||
function fail(msg) { console.error(` ✗ ${msg}`); failures++; }
|
||||
|
||||
async function gotoSpa(page, route) {
|
||||
await page.goto(`${BASE}/#${route}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!document.querySelector('#app'), null, { timeout: 8000 }).catch(() => {});
|
||||
await page.waitForTimeout(700);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const requireChromium = process.env.CHROMIUM_REQUIRE === '1';
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||||
});
|
||||
} catch (err) {
|
||||
if (requireChromium) {
|
||||
console.error(`test-issue-1648-m5-icons-e2e.js: HARD FAIL — Chromium unavailable: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.warn(`SKIP — Chromium unavailable: ${err.message}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
|
||||
// ── (a) Customize panel — defaults render as Phosphor sprites ──────
|
||||
await gotoSpa(page, '/');
|
||||
// Click the customize trigger (palette icon in nav).
|
||||
await page.evaluate(() => {
|
||||
const btn = document.querySelector('[data-toggle="customize"], #customizeToggle, .cust-toggle, [aria-label*="ustomize" i]');
|
||||
if (btn) btn.click();
|
||||
else if (window.Customize && window.Customize.toggle) window.Customize.toggle();
|
||||
else if (window.CustomizeV2 && window.CustomizeV2.toggle) window.CustomizeV2.toggle();
|
||||
});
|
||||
await page.waitForTimeout(900);
|
||||
const custA = await page.evaluate(() => {
|
||||
const overlay = document.querySelector('.cust-overlay');
|
||||
if (!overlay) return { open: false };
|
||||
const tabs = overlay.querySelectorAll('.cust-tab');
|
||||
let tabSprites = 0; tabs.forEach(t => { tabSprites += t.querySelectorAll('svg.ph-icon use').length; });
|
||||
return {
|
||||
open: true,
|
||||
tabCount: tabs.length,
|
||||
tabSprites,
|
||||
headerSprites: overlay.querySelectorAll('.cust-header svg.ph-icon use').length,
|
||||
overlayText: overlay.textContent || '',
|
||||
};
|
||||
});
|
||||
if (!custA.open) {
|
||||
fail('(a) customize panel did not open — cannot assert');
|
||||
} else {
|
||||
if (custA.tabSprites < custA.tabCount) {
|
||||
fail(`(a) customize tabs: ${custA.tabSprites} sprite(s) for ${custA.tabCount} tabs (each tab needs ≥1)`);
|
||||
} else {
|
||||
pass(`(a) customize tabs: ${custA.tabSprites} sprite(s) across ${custA.tabCount} tabs`);
|
||||
}
|
||||
if (custA.headerSprites < 2) {
|
||||
fail(`(a) customize header: ${custA.headerSprites} sprite(s) (expected ≥2 — title + close)`);
|
||||
} else {
|
||||
pass(`(a) customize header: ${custA.headerSprites} sprite(s)`);
|
||||
}
|
||||
// Note: the overlay still RENDERS legacy operator emoji values via
|
||||
// renderConfigGlyph for back-compat (test (b) below). The overlay's own
|
||||
// chrome/tabs/headers must be sprite-driven, so we look at chrome only.
|
||||
const chromeText = await page.evaluate(() => {
|
||||
const ovl = document.querySelector('.cust-overlay');
|
||||
if (!ovl) return '';
|
||||
// Walk only the tab + header chrome, not the input previews (which
|
||||
// legitimately echo operator values — including legacy emoji).
|
||||
const parts = [];
|
||||
ovl.querySelectorAll('.cust-tab, .cust-header').forEach(el => parts.push(el.textContent || ''));
|
||||
return parts.join(' | ');
|
||||
});
|
||||
if (M5_FORBIDDEN_RE.test(chromeText)) {
|
||||
fail(`(a) customize chrome still contains an emoji/misc-icon: ${JSON.stringify(chromeText.match(M5_FORBIDDEN_RE))}`);
|
||||
} else {
|
||||
pass('(a) customize chrome (tabs + header) is icon-free text');
|
||||
}
|
||||
}
|
||||
|
||||
// ── (b) BACK-COMPAT: operator-stored emoji values must still render ──
|
||||
// Inject a fake user-theme into localStorage with a legacy emoji step,
|
||||
// reload, re-open customize, navigate to home tab, and confirm the
|
||||
// emoji STILL renders verbatim (not stripped, not replaced).
|
||||
await page.evaluate(() => {
|
||||
const legacy = {
|
||||
home: {
|
||||
heroTitle: 'CoreScope',
|
||||
heroSubtitle: 'legacy test',
|
||||
steps: [
|
||||
{ emoji: '🐙', title: 'Legacy octopus step', description: 'Operator-stored emoji must survive M5 migration.' },
|
||||
{ emoji: 'ph:bluetooth', title: 'New ph token step', description: 'Renders as sprite.' },
|
||||
],
|
||||
footerLinks: [{ label: '🦄 Legacy unicorn', url: '#/legacy' }],
|
||||
},
|
||||
};
|
||||
localStorage.setItem('meshcore-user-theme', JSON.stringify(legacy));
|
||||
// customize-v2 reads from cv2 overrides too
|
||||
try {
|
||||
const cv2 = { home: legacy.home };
|
||||
localStorage.setItem('meshcore-cv2-overrides', JSON.stringify(cv2));
|
||||
} catch (e) {}
|
||||
});
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(700);
|
||||
// Re-open customize and switch to Home tab.
|
||||
await page.evaluate(() => {
|
||||
const btn = document.querySelector('[data-toggle="customize"], #customizeToggle, .cust-toggle, [aria-label*="ustomize" i]');
|
||||
if (btn) btn.click();
|
||||
else if (window.Customize && window.Customize.toggle) window.Customize.toggle();
|
||||
else if (window.CustomizeV2 && window.CustomizeV2.toggle) window.CustomizeV2.toggle();
|
||||
});
|
||||
await page.waitForTimeout(700);
|
||||
await page.evaluate(() => {
|
||||
const home = document.querySelector('.cust-overlay .cust-tab[data-tab="home"]');
|
||||
if (home) home.click();
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
const custB = await page.evaluate(() => {
|
||||
const overlay = document.querySelector('.cust-overlay');
|
||||
if (!overlay) return { open: false };
|
||||
// The step emoji input value should contain the literal 🐙 char.
|
||||
const inputs = Array.from(overlay.querySelectorAll('.cust-emoji-input'));
|
||||
const previews = Array.from(overlay.querySelectorAll('.cust-emoji-preview'));
|
||||
return {
|
||||
open: true,
|
||||
inputValues: inputs.map(i => i.value),
|
||||
previewHtmls: previews.map(p => p.innerHTML),
|
||||
};
|
||||
});
|
||||
if (!custB.open) {
|
||||
fail('(b) back-compat: customize panel did not open after localStorage inject');
|
||||
} else {
|
||||
const hasOctopus = custB.inputValues.some(v => v && v.indexOf('🐙') !== -1);
|
||||
if (!hasOctopus) {
|
||||
fail(`(b) back-compat: operator-stored emoji 🐙 missing from inputs (values=${JSON.stringify(custB.inputValues)})`);
|
||||
} else {
|
||||
pass('(b) back-compat: operator-stored emoji 🐙 survives in step input value');
|
||||
}
|
||||
const hasOctopusPreview = custB.previewHtmls.some(h => h && h.indexOf('🐙') !== -1);
|
||||
if (!hasOctopusPreview) {
|
||||
fail(`(b) back-compat: emoji preview span does not render 🐙 (htmls=${JSON.stringify(custB.previewHtmls)})`);
|
||||
} else {
|
||||
pass('(b) back-compat: renderConfigGlyph rendered 🐙 as text (legacy branch)');
|
||||
}
|
||||
const hasPhSprite = custB.previewHtmls.some(h => h && /<use[^>]+ph-bluetooth/.test(h));
|
||||
if (!hasPhSprite) {
|
||||
fail('(b) back-compat: new ph:bluetooth step did not render as sprite');
|
||||
} else {
|
||||
pass('(b) back-compat: ph:bluetooth step rendered as Phosphor sprite alongside legacy emoji');
|
||||
}
|
||||
}
|
||||
// Clean up the legacy injection so it doesn't leak into later assertions.
|
||||
await page.evaluate(() => {
|
||||
localStorage.removeItem('meshcore-user-theme');
|
||||
localStorage.removeItem('meshcore-cv2-overrides');
|
||||
});
|
||||
|
||||
// ── (c) /channels modal open — modal controls render sprites ────────
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await gotoSpa(page, '/channels');
|
||||
// The share/QR modals are gated on a user-added channel; we just assert
|
||||
// any modal-close button present on the page already renders a sprite
|
||||
// (the M3 swap landed `.modal-close` chrome → sprite).
|
||||
const ch = await page.evaluate(() => {
|
||||
const modals = Array.from(document.querySelectorAll('.modal, .ch-modal, .modal-overlay'));
|
||||
let modalSprites = 0;
|
||||
modals.forEach(m => { modalSprites += m.querySelectorAll('svg.ph-icon use').length; });
|
||||
const pageSprites = document.querySelectorAll('svg.ph-icon use').length;
|
||||
return { modalCount: modals.length, modalSprites, pageSprites };
|
||||
});
|
||||
if (ch.pageSprites < 3) fail(`(c) /channels: only ${ch.pageSprites} sprites total (expected ≥3)`);
|
||||
else pass(`(c) /channels: ${ch.pageSprites} sprite refs on page (${ch.modalSprites} inside ${ch.modalCount} modal element(s))`);
|
||||
|
||||
// ── (d) /audio-lab — sprite icons in audio prompts/controls ────────
|
||||
await gotoSpa(page, '/audio-lab');
|
||||
await page.waitForTimeout(500);
|
||||
const aud = await page.evaluate(() => {
|
||||
return {
|
||||
sprites: document.querySelectorAll('svg.ph-icon use').length,
|
||||
bodyText: (document.getElementById('app') || document.body).textContent || '',
|
||||
};
|
||||
});
|
||||
if (aud.sprites < 1) fail(`(d) /audio-lab: ${aud.sprites} sprite refs (expected ≥1)`);
|
||||
else pass(`(d) /audio-lab: ${aud.sprites} sprite refs`);
|
||||
|
||||
// ── (e) geofilter-builder.html (standalone) ─────────────────────────
|
||||
await page.goto(`${BASE}/geofilter-builder.html`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
const geo = await page.evaluate(() => {
|
||||
const ctrls = document.querySelectorAll('.controls button');
|
||||
let s = 0; ctrls.forEach(b => { s += b.querySelectorAll('svg.ph-icon use').length; });
|
||||
return {
|
||||
ctrlSprites: s,
|
||||
ctrlCount: ctrls.length,
|
||||
btnTexts: Array.from(ctrls).map(b => b.textContent || ''),
|
||||
};
|
||||
});
|
||||
if (geo.ctrlSprites < geo.ctrlCount) {
|
||||
fail(`(e) geofilter-builder: ${geo.ctrlSprites} sprite(s) for ${geo.ctrlCount} control button(s)`);
|
||||
} else {
|
||||
pass(`(e) geofilter-builder: ${geo.ctrlSprites} sprite(s) across ${geo.ctrlCount} control buttons`);
|
||||
}
|
||||
const ctrlEmoji = geo.btnTexts.find(t => M5_FORBIDDEN_RE.test(t));
|
||||
if (ctrlEmoji) fail(`(e) geofilter-builder: control button still contains emoji/misc-icon: ${JSON.stringify(ctrlEmoji)}`);
|
||||
else pass('(e) geofilter-builder: control button text is icon-free');
|
||||
|
||||
// ── (f) Every <use> resolves ────────────────────────────────────────
|
||||
await gotoSpa(page, '/');
|
||||
const undef = await page.evaluate(async () => {
|
||||
const resp = await fetch('/icons/phosphor-sprite.svg').catch(() => null);
|
||||
if (!resp || !resp.ok) return { error: 'sprite fetch failed' };
|
||||
const text = await resp.text();
|
||||
const ids = new Set();
|
||||
for (const m of text.matchAll(/id="(ph-[a-z-]+)"/g)) ids.add(m[1]);
|
||||
const uses = Array.from(document.querySelectorAll('svg.ph-icon use'));
|
||||
const missing = [];
|
||||
for (const u of uses) {
|
||||
const href = u.getAttribute('href') || u.getAttribute('xlink:href') || '';
|
||||
const m = href.match(/#(ph-[a-z-]+)/);
|
||||
if (!m) { missing.push(href); continue; }
|
||||
if (!ids.has(m[1])) missing.push(m[1]);
|
||||
}
|
||||
return { count: uses.length, ids: ids.size, missing };
|
||||
});
|
||||
if (undef.error) fail(`(f) sprite fetch: ${undef.error}`);
|
||||
else if (undef.missing && undef.missing.length) fail(`(f) ${undef.missing.length} sprite ref(s) unresolved: ${undef.missing.slice(0,5).join(', ')}`);
|
||||
else pass(`(f) all ${undef.count} sprite refs resolve to one of ${undef.ids} defined symbols`);
|
||||
|
||||
await browser.close();
|
||||
console.log(`\ntest-issue-1648-m5-icons-e2e.js: ${passes} passed, ${failures} failed`);
|
||||
assert.strictEqual(failures, 0, `${failures} M5 icon-render assertions failed`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('test-issue-1648-m5-icons-e2e.js: FAIL —', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user