mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-05 16:01:27 +00:00
feat(#1446): CB preset becomes end-user opt-in; user + server-config overrides win
Reframe the CB-preset feature: it is now an end-user opt-in layered above
operator config, not the canonical color source for the app.
Cascade (top wins):
user per-role override > active CB preset > server config.nodeColors
> built-in :root defaults
Changes
-------
cb-presets.js
- currentPreset() returns null when no preset is stored (was 'default')
- initFromStorage() no longer auto-applies Wong on cold boot
- New clearPreset(): removes data-cb-preset attr + strips preset-written
CSS vars + fires cb-preset-changed event
- Exported MeshCorePresets.clearPreset
style.css
- Drop the body[data-cb-preset=default] block. Wong remains the :root
baseline; that block was redundantly clamping --mc-role-* in the
'no preset' state and masking server config.
roles.js
- setRoleColorOverride writes to body.style with !important so the user
pick wins on equal-specificity cascade against body[data-cb-preset=X]
rules (root cause of #1444). documentElement writes stay unmarked.
customize-v2.js
- applyCSS: when no CB preset is active, server-config nodeColors are
allowed to write --mc-role-{role} (in addition to legacy --node-{role}).
When a preset IS active, server config stays out of --mc-role-* to
preserve #1412 / #1438 invariants.
- UI reordered: Node Role Colors block now comes FIRST in the Colors tab.
- Preset selector relabelled 'Optional: Colorblind-Safe Preset' with a
'No preset (use operator / custom colors)' radio entry that calls
clearPreset() + re-runs the customizer pipeline.
- cb-preset-changed listener re-runs the pipeline so server-config
--mc-role-{role} writes take effect immediately on clearPreset.
Backward compatibility
----------------------
- Visitors who already have a CB preset in localStorage continue to see
it applied on load (cb-presets.js still reads it).
- Visitors with no stored preset who used to see Wong now see whatever
server config or built-in defaults dictate — Wong remains the :root
fallback so a default config with no nodeColors is visually identical.
Fixes #1444 cascade as a side effect.
Closes #1446
This commit is contained in:
+36
-2
@@ -279,11 +279,44 @@
|
||||
if (v && _byId(v)) return v;
|
||||
}
|
||||
} catch (e) {}
|
||||
return 'default';
|
||||
// #1446 — return null when no preset is stored. Previously this returned
|
||||
// 'default' unconditionally, which forced body[data-cb-preset="default"]
|
||||
// on every cold boot and trapped --mc-role-* in the Wong palette via the
|
||||
// matching style.css rule. The CB preset is now an end-user opt-in:
|
||||
// absent attribute = "no preset", role colors flow from server config.
|
||||
return null;
|
||||
}
|
||||
|
||||
function clearPreset() {
|
||||
try { if (typeof localStorage !== 'undefined') localStorage.removeItem(STORAGE_KEY); } catch (e) {}
|
||||
if (typeof document !== 'undefined' && document.body && document.body.removeAttribute) {
|
||||
document.body.removeAttribute(DATA_ATTR);
|
||||
}
|
||||
// Strip preset-written CSS vars from documentElement so the cascade
|
||||
// re-falls through :root defaults (or server config, which the
|
||||
// customizer pipeline re-applies via the cb-preset-changed listener).
|
||||
if (typeof document !== 'undefined' && document.documentElement && document.documentElement.style) {
|
||||
var style = document.documentElement.style;
|
||||
['repeater', 'companion', 'room', 'sensor', 'observer'].forEach(function (role) {
|
||||
style.removeProperty('--mc-role-' + role);
|
||||
style.removeProperty('--mc-role-' + role + '-text');
|
||||
});
|
||||
['confirmed', 'suspected', 'unknown'].forEach(function (k) {
|
||||
style.removeProperty('--mc-mb-' + k);
|
||||
});
|
||||
for (var ri = 0; ri < 5; ri++) style.removeProperty('--mc-rt-ramp-' + ri);
|
||||
}
|
||||
if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof window.CustomEvent === 'function') {
|
||||
try { window.dispatchEvent(new window.CustomEvent('cb-preset-changed', { detail: { id: null } })); } catch (e) {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function initFromStorage() {
|
||||
applyPreset(currentPreset(), { skipPersist: true });
|
||||
var id = currentPreset();
|
||||
// #1446 — only apply when a preset is actually stored. No stored preset
|
||||
// means "no preset active" (the new default), not "fall back to Wong".
|
||||
if (id) applyPreset(id, { skipPersist: true });
|
||||
}
|
||||
|
||||
// Cross-tab sync via storage event.
|
||||
@@ -305,6 +338,7 @@
|
||||
var api = {
|
||||
list: PRESETS,
|
||||
applyPreset: applyPreset,
|
||||
clearPreset: clearPreset,
|
||||
currentPreset: currentPreset,
|
||||
initFromStorage: initFromStorage,
|
||||
validatePreset: validatePreset,
|
||||
|
||||
+63
-15
@@ -554,12 +554,18 @@
|
||||
// overrides still flow through setRoleColorOverride() in customize.js.
|
||||
var nc = effectiveConfig.nodeColors;
|
||||
if (nc) {
|
||||
// #1438 final: scope --mc-role-{role} writes to USER overrides only.
|
||||
// Server-config nodeColors must stay out of --mc-role-* because
|
||||
// ROLE_COLORS live getter (roles.js) reads from that var; writing
|
||||
// server defaults there would re-introduce the #1412 bug (server
|
||||
// legacy palette trapping cb-preset propagation).
|
||||
// #1438 final: scope --mc-role-{role} writes to USER overrides only,
|
||||
// UNLESS no CB preset is active (#1446). When a preset is active the
|
||||
// server-config palette must stay out of --mc-role-* so the preset
|
||||
// wins (preserves #1412). When NO preset is active, the cascade is:
|
||||
// user override > server config > built-in :root default.
|
||||
// → server config gets to write --mc-role-{role} in that case.
|
||||
var userNc = (userOverrides && userOverrides.nodeColors) || {};
|
||||
var presetActive = false;
|
||||
try {
|
||||
var presetAttr = document.body && document.body.getAttribute && document.body.getAttribute('data-cb-preset');
|
||||
presetActive = !!(presetAttr && presetAttr !== 'none');
|
||||
} catch (e) {}
|
||||
for (var role in nc) {
|
||||
root.setProperty('--node-' + role, nc[role]);
|
||||
if (Object.prototype.hasOwnProperty.call(userNc, role)) {
|
||||
@@ -568,7 +574,13 @@
|
||||
// pick it up on every page load. Without this the user pick
|
||||
// sits in localStorage but --mc-role-{role} falls back to the
|
||||
// active preset on reload, reverting marker fills.
|
||||
root.setProperty('--mc-role-' + role, userNc[role]);
|
||||
root.setProperty('--mc-role-' + role, nc[role]);
|
||||
} else if (!presetActive) {
|
||||
// #1446 — no preset is active; server config is the legitimate
|
||||
// source of role colors. Write --mc-role-{role} so marker SVGs
|
||||
// honor operator's config.json without forcing visitors to pick
|
||||
// a CB preset to "unlock" their server palette.
|
||||
root.setProperty('--mc-role-' + role, nc[role]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1147,7 +1159,9 @@
|
||||
function _renderColorblindPresetSelector() {
|
||||
var MCP = (typeof window !== 'undefined') && window.MeshCorePresets;
|
||||
if (!MCP || !Array.isArray(MCP.list)) return '';
|
||||
var current = MCP.currentPreset ? MCP.currentPreset() : 'default';
|
||||
// #1446 — currentPreset() now returns null when no preset is stored.
|
||||
var current = MCP.currentPreset ? MCP.currentPreset() : null;
|
||||
var clearOpt = _renderCbPresetClearOption(current);
|
||||
var options = MCP.list.map(function (p) {
|
||||
var checked = p.id === current ? ' checked' : '';
|
||||
return '<label class="cust-cb-preset-row" style="display:flex;gap:8px;align-items:flex-start;margin:6px 0;cursor:pointer">' +
|
||||
@@ -1159,12 +1173,25 @@
|
||||
'</div>' +
|
||||
'</label>';
|
||||
}).join('');
|
||||
return '<p class="cust-section-title">Colorblind Preset</p>' +
|
||||
'<p class="cust-hint" style="margin-bottom:8px">Switch the role/status palette for color-vision variants. Achromatopsia uses a luminance-only ramp and relies on the shape/letter/glyph carriers from #1356/#1357.</p>' +
|
||||
'<div class="cust-cb-presets" data-cv2-cb-preset-group>' + options + '</div>' +
|
||||
return '<p class="cust-section-title">Optional: Colorblind-Safe Preset</p>' +
|
||||
'<p class="cust-hint" style="margin-bottom:8px">A CB preset is an end-user opt-in that swaps the role/status palette for color-vision variants. ' +
|
||||
'Leave unset to use the operator\'s configured colors (or pick from above). ' +
|
||||
'Achromatopsia uses a luminance-only ramp and relies on the shape/letter/glyph carriers from #1356/#1357.</p>' +
|
||||
'<div class="cust-cb-presets" data-cv2-cb-preset-group>' + clearOpt + options + '</div>' +
|
||||
'<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">';
|
||||
}
|
||||
|
||||
function _renderCbPresetClearOption(current) {
|
||||
var checked = !current ? ' checked' : '';
|
||||
return '<label class="cust-cb-preset-row" style="display:flex;gap:8px;align-items:flex-start;margin:6px 0;cursor:pointer">' +
|
||||
'<input type="radio" name="cv2-cb-preset" data-cv2-cb-preset value="" data-cv2-cb-preset-none' + checked + ' style="margin-top:3px">' +
|
||||
'<div style="flex:1">' +
|
||||
'<div style="font-weight:600">No preset (use operator / custom colors)</div>' +
|
||||
'<div class="cust-hint" style="font-size:12px;color:var(--text-muted)">Default — server-configured colors apply, then any per-role overrides above.</div>' +
|
||||
'</div>' +
|
||||
'</label>';
|
||||
}
|
||||
|
||||
function _renderCbPresetWarning(id) {
|
||||
var MCP = window.MeshCorePresets;
|
||||
if (!MCP || typeof MCP.validatePreset !== 'function') return '';
|
||||
@@ -1213,9 +1240,11 @@
|
||||
var liveHeatPct = Math.round(liveHeatOpacity * 100);
|
||||
|
||||
return '<div class="cust-panel' + (_activeTab === 'nodes' ? ' active' : '') + '" data-panel="nodes">' +
|
||||
_renderColorblindPresetSelector() +
|
||||
'<p class="cust-section-title">Node Role Colors</p>' + rows +
|
||||
'<p class="cust-section-title">Node Role Colors</p>' +
|
||||
'<p class="cust-hint" style="margin-bottom:8px">These are the canonical role colors used across the app. They inherit from your server config (or built-in defaults), and can be optionally remapped by a colorblind-safe preset below.</p>' +
|
||||
rows +
|
||||
'<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">' +
|
||||
_renderColorblindPresetSelector() +
|
||||
'<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>' +
|
||||
@@ -1833,14 +1862,21 @@
|
||||
if (_activeTab === 'geofilter') _initGeoFilterTab(container);
|
||||
|
||||
// #1361 Colorblind preset radio — switches preset via MeshCorePresets.applyPreset
|
||||
// #1446 — empty-value radio = "no preset" → clearPreset(), then re-run
|
||||
// the customizer pipeline so server-config colors take over.
|
||||
container.querySelectorAll('[data-cv2-cb-preset]').forEach(function (radio) {
|
||||
radio.addEventListener('change', function () {
|
||||
if (!radio.checked) return;
|
||||
var id = radio.value;
|
||||
if (window.MeshCorePresets && typeof window.MeshCorePresets.applyPreset === 'function') {
|
||||
window.MeshCorePresets.applyPreset(id);
|
||||
_refreshPanel();
|
||||
var MCP = window.MeshCorePresets;
|
||||
if (!MCP) return;
|
||||
if (!id) {
|
||||
if (typeof MCP.clearPreset === 'function') MCP.clearPreset();
|
||||
_runPipeline();
|
||||
} else if (typeof MCP.applyPreset === 'function') {
|
||||
MCP.applyPreset(id);
|
||||
}
|
||||
_refreshPanel();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2166,6 +2202,18 @@
|
||||
// 1. Migration check
|
||||
migrateOldKeys();
|
||||
|
||||
// #1446 — when a CB preset is cleared (or applied), re-run the customizer
|
||||
// pipeline so server-config nodeColors take over the --mc-role-{role}
|
||||
// CSS vars (the gating logic in applyCSS checks the body[data-cb-preset]
|
||||
// attribute to decide whether to write them).
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
||||
window.addEventListener('cb-preset-changed', function () {
|
||||
if (_initDone) _runPipeline();
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Read overrides and apply CSS immediately (before DOMContentLoaded)
|
||||
// Server defaults will be set later when /api/config/theme completes.
|
||||
// For now, apply whatever overrides exist on top of current SITE_CONFIG.
|
||||
|
||||
+17
-1
@@ -138,7 +138,23 @@
|
||||
// pick up the operator's hex without a page reload. Writing to
|
||||
// body inline style is necessary because body[data-cb-preset="..."]
|
||||
// selectors beat :root inheritance.
|
||||
targets.forEach(function (s) { s.setProperty(varName, hex); });
|
||||
//
|
||||
// #1446: write with !important so the inline body declaration also
|
||||
// beats the body[data-cb-preset="X"] CSS rule on equal specificity.
|
||||
// Without !important, the cascade order picks the later-defined
|
||||
// stylesheet rule in some browser versions even though specificity
|
||||
// (1,0,1) matches the inline body style — operator pick visibly
|
||||
// loses to active preset (root cause of #1444).
|
||||
targets.forEach(function (s) {
|
||||
// documentElement gets the value without !important (used as the
|
||||
// canonical readout for the JS getter); body gets !important so it
|
||||
// wins the CSS cascade against body[data-cb-preset="X"].
|
||||
if (s === (document.body && document.body.style)) {
|
||||
s.setProperty(varName, hex, 'important');
|
||||
} else {
|
||||
s.setProperty(varName, hex);
|
||||
}
|
||||
});
|
||||
};
|
||||
// Back-compat: also export the writable override map so customize.js's
|
||||
// `window.ROLE_COLORS[key] = inp.value` style mutation works.
|
||||
|
||||
+6
-15
@@ -3479,21 +3479,12 @@ th.sort-active { color: var(--accent, #60a5fa); }
|
||||
* truth so a regression that drops one is still caught by the other
|
||||
* (mirrors the #1356 "pill color: defense-in-depth via CSS + inline" pattern).
|
||||
* ─────────────────────────────────────────────────────────────────────── */
|
||||
body[data-cb-preset="default"] {
|
||||
--mc-role-repeater: #D55E00;
|
||||
--mc-role-companion: #56B4E9;
|
||||
--mc-role-room: #009E73;
|
||||
--mc-role-sensor: #F0E442;
|
||||
--mc-role-observer: #CC79A7;
|
||||
--mc-role-repeater-text: #1a1a1a;
|
||||
--mc-role-companion-text: #1a1a1a;
|
||||
--mc-role-room-text: #1a1a1a;
|
||||
--mc-role-sensor-text: #1a1a1a;
|
||||
--mc-role-observer-text: #1a1a1a;
|
||||
--mc-mb-confirmed: #56F0A0;
|
||||
--mc-mb-suspected: #FFD966;
|
||||
--mc-mb-unknown: #FF8888;
|
||||
}
|
||||
/* #1446 — the body data-cb-preset rule for the default (Wong) palette is
|
||||
* intentionally OMITTED. Wong is the :root baseline; selecting "default"
|
||||
* from the customizer is identical to "no preset". Writing the Wong
|
||||
* palette here would mask the server config's nodeColors (see #1446
|
||||
* cascade). Wong values remain available via :root above.
|
||||
*/
|
||||
body[data-cb-preset="deut"] {
|
||||
/* IBM 5-class deut variant — anchors shifted out of red/green collision. */
|
||||
--mc-role-repeater: #FE6100;
|
||||
|
||||
@@ -57,6 +57,7 @@ function makeSandbox(localStorageMap) {
|
||||
};
|
||||
const sandbox = {
|
||||
window: null,
|
||||
_listeners: {},
|
||||
localStorage: {
|
||||
_m: Object.assign({}, localStorageMap),
|
||||
getItem(k) { return Object.prototype.hasOwnProperty.call(this._m, k) ? this._m[k] : null; },
|
||||
@@ -76,7 +77,15 @@ function makeSandbox(localStorageMap) {
|
||||
},
|
||||
console: console,
|
||||
setTimeout: setTimeout, clearTimeout: clearTimeout,
|
||||
addEventListener() {}, dispatchEvent() { return true; },
|
||||
addEventListener(type, fn) {
|
||||
if (!this._listeners[type]) this._listeners[type] = [];
|
||||
this._listeners[type].push(fn);
|
||||
},
|
||||
dispatchEvent(ev) {
|
||||
var arr = this._listeners[ev && ev.type] || [];
|
||||
arr.forEach(function (fn) { try { fn(ev); } catch (e) {} });
|
||||
return true;
|
||||
},
|
||||
fetch: function () { return Promise.resolve({ json: function () { return Promise.resolve({}); } }); },
|
||||
matchMedia: function () { return { matches: false, addEventListener() {} }; },
|
||||
CustomEvent: function (t, o) { this.type = t; this.detail = o && o.detail; },
|
||||
|
||||
Reference in New Issue
Block a user