mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-02 00:41:38 +00:00
Red commit: 07a69e48eb (CI run: pending —
PR triggers first run)
Fixes #1509
## Problem
`--nav-active-bg` is defined in `public/style.css` (line 105) and used
by every
active-state nav link (`.nav-link.active`, `.nav-more-menu
.nav-link.active`,
plus the responsive blocks), but the customizer has never mapped it into
`THEME_CSS_MAP`. Result: presets, per-operator overrides, and
server-side
`theme.*` config can recolor every other nav token (`navBg`, `navBg2`,
`navText`,
`navTextMuted`) — but the active-pill background stays stuck on the
hardcoded
`rgba(74, 158, 255, 0.15)` (light) / dark-mode equivalent. Themes look
broken on
the one element users stare at.
## Fix
Triage-specified path, no scope creep:
- Add `navActiveBg: '--nav-active-bg'` to `THEME_CSS_MAP` in
`public/customize-v2.js`.
- Surface in the Theme tab's advanced color list (`THEME_COLOR_KEYS`
derives from
the map; adding to `ADVANCED_KEYS` makes it render in the panel).
- Add label + hint so the input is self-explanatory.
- Seed defaults on the default preset's `theme` + `themeDark` so the
rendered
value matches today's hardcoded rgba and dark mode doesn't bleed the
light value.
- Document the new field in `config.example.json` per AGENTS.md config
rule.
## TDD
Red commit `07a69e48` adds `test-issue-1509-nav-active-bg.js` and wires
it
into the CI unit-test step. Assertions fail on master
(`THEME_CSS_MAP.navActiveBg`
is `undefined`; `applyCSS` does not write the variable). Green commit
`29d22ff5`
makes the assertions pass without touching any other test.
## Verification
- `node test-issue-1509-nav-active-bg.js` → 3/3 pass on this branch, 0/3
on master
- `node test-customizer-v2.js` → 59/60 (the 1 failure is pre-existing on
master,
not caused by this PR — same failure with the diff stashed)
- pr-preflight: clean (all gates pass)
---------
Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: Kpa-clawbot <kpa-clawbot@users.noreply.github.com>
Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer>
This commit is contained in:
@@ -127,6 +127,8 @@ jobs:
|
||||
node test-issue-1420-tile-providers.js
|
||||
node test-issue-1438-marker-css-vars.js
|
||||
node test-issue-1562-observers-summary.js
|
||||
node test-issue-1509-nav-active-bg.js
|
||||
node test-issue-1509-detect-preset.js
|
||||
node test-live.js
|
||||
node test-issue-1532-live-fullscreen.js
|
||||
node test-xss-escape-sinks.js
|
||||
|
||||
+2
-1
@@ -36,10 +36,11 @@
|
||||
"accentHover": "#6db3ff",
|
||||
"navBg": "#0f0f23",
|
||||
"navBg2": "#1a1a2e",
|
||||
"navActiveBg": "rgba(74,158,255,0.15)",
|
||||
"statusGreen": "#45644c",
|
||||
"statusYellow": "#b08b2d",
|
||||
"statusRed": "#b54a4a",
|
||||
"_comment": "CSS color overrides. Use the in-app Theme Customizer for live preview, then export values here."
|
||||
"_comment": "CSS color overrides. Use the in-app Theme Customizer for live preview, then export values here. navActiveBg (#1509) controls the background of the currently-active top-nav link (the 'pill'); accepts any CSS color, typically a translucent rgba() so the nav gradient shows through."
|
||||
},
|
||||
"nodeColors": {
|
||||
"repeater": "#dc2626",
|
||||
|
||||
+24
-6
@@ -42,6 +42,7 @@
|
||||
var THEME_CSS_MAP = {
|
||||
accent: '--accent', accentHover: '--accent-hover',
|
||||
navBg: '--nav-bg', navBg2: '--nav-bg2', navText: '--nav-text', navTextMuted: '--nav-text-muted',
|
||||
navActiveBg: '--nav-active-bg',
|
||||
background: '--surface-0', text: '--text', textMuted: '--text-muted', border: '--border',
|
||||
statusGreen: '--status-green', statusYellow: '--status-yellow', statusRed: '--status-red',
|
||||
surface1: '--surface-1', surface2: '--surface-2', surface3: '--surface-3',
|
||||
@@ -109,7 +110,7 @@
|
||||
theme: {
|
||||
accent: '#4a9eff', navBg: '#0f0f23', navText: '#ffffff', background: '#f4f5f7', text: '#1a1a2e',
|
||||
statusGreen: '#22c55e', statusYellow: '#eab308', statusRed: '#ef4444',
|
||||
accentHover: '#6db3ff', navBg2: '#1a1a2e', navTextMuted: '#cbd5e1', textMuted: '#5b6370', border: '#e2e5ea',
|
||||
accentHover: '#6db3ff', navBg2: '#1a1a2e', navTextMuted: '#cbd5e1', navActiveBg: 'rgba(74,158,255,0.15)', textMuted: '#5b6370', border: '#e2e5ea',
|
||||
surface1: '#ffffff', surface2: '#ffffff', cardBg: '#ffffff', contentBg: '#f4f5f7',
|
||||
detailBg: '#ffffff', inputBg: '#ffffff', rowStripe: '#f9fafb', rowHover: '#eef2ff', selectedBg: '#dbeafe',
|
||||
surface3: '#ffffff', sectionBg: '#eef2ff'
|
||||
@@ -117,7 +118,7 @@
|
||||
themeDark: {
|
||||
accent: '#4a9eff', navBg: '#0f0f23', navText: '#ffffff', background: '#0f0f23', text: '#e2e8f0',
|
||||
statusGreen: '#22c55e', statusYellow: '#eab308', statusRed: '#ef4444',
|
||||
accentHover: '#6db3ff', navBg2: '#1a1a2e', navTextMuted: '#cbd5e1', textMuted: '#a8b8cc', border: '#334155',
|
||||
accentHover: '#6db3ff', navBg2: '#1a1a2e', navTextMuted: '#cbd5e1', navActiveBg: 'rgba(74,158,255,0.18)', textMuted: '#a8b8cc', border: '#334155',
|
||||
surface1: '#1a1a2e', surface2: '#232340', cardBg: '#1a1a2e', contentBg: '#0f0f23',
|
||||
detailBg: '#232340', inputBg: '#1e1e34', rowStripe: '#1e1e34', rowHover: '#2d2d50', selectedBg: '#1e3a5f',
|
||||
surface3: '#2d2d50', sectionBg: '#1e1e34'
|
||||
@@ -275,6 +276,7 @@
|
||||
var THEME_LABELS = {
|
||||
accent: 'Brand Color', accentHover: 'Accent Hover',
|
||||
navBg: 'Navigation', navBg2: 'Nav Gradient End', navText: 'Nav Text', navTextMuted: 'Nav Muted Text',
|
||||
navActiveBg: 'Nav Active Pill',
|
||||
background: 'Background', text: 'Text', textMuted: 'Muted Text', border: 'Borders',
|
||||
statusGreen: 'Healthy', statusYellow: 'Warning', statusRed: 'Error',
|
||||
surface1: 'Cards', surface2: 'Panels', surface3: 'Tertiary Surface', sectionBg: 'Section Header', cardBg: 'Card Fill', contentBg: 'Content Area',
|
||||
@@ -290,6 +292,7 @@
|
||||
statusGreen: 'Healthy/online indicators', statusYellow: 'Warning/degraded + hop conflicts',
|
||||
statusRed: 'Error/offline indicators', accentHover: 'Hover state for accent elements',
|
||||
navBg2: 'Darker end of nav gradient', navTextMuted: 'Inactive nav links, nav buttons',
|
||||
navActiveBg: 'Background of the active nav link (current page pill)',
|
||||
textMuted: 'Labels, timestamps, secondary text', border: 'Dividers, table borders, card borders',
|
||||
surface1: 'Card and panel backgrounds', surface2: 'Nested surfaces, secondary panels',
|
||||
surface3: 'Tertiary surfaces, hover accents', sectionBg: 'Section header backgrounds',
|
||||
@@ -321,7 +324,7 @@
|
||||
};
|
||||
|
||||
var BASIC_KEYS = ['accent', 'navBg', 'navText', 'background', 'text', 'statusGreen', 'statusYellow', 'statusRed'];
|
||||
var ADVANCED_KEYS = ['accentHover', 'navBg2', 'navTextMuted', 'textMuted', 'border', 'surface1', 'surface2', 'cardBg', 'contentBg', 'detailBg', 'inputBg', 'rowStripe', 'rowHover', 'selectedBg'];
|
||||
var ADVANCED_KEYS = ['accentHover', 'navBg2', 'navTextMuted', 'navActiveBg', 'textMuted', 'border', 'surface1', 'surface2', 'cardBg', 'contentBg', 'detailBg', 'inputBg', 'rowStripe', 'rowHover', 'selectedBg'];
|
||||
var FONT_KEYS = ['font', 'mono'];
|
||||
|
||||
// ── Validation helpers ──
|
||||
@@ -1144,16 +1147,29 @@
|
||||
var effDark = eff.themeDark || {};
|
||||
for (var id in PRESETS) {
|
||||
var p = PRESETS[id];
|
||||
var pTheme = p.theme || {};
|
||||
var pDark = p.themeDark || {};
|
||||
var match = true;
|
||||
for (var i = 0; i < THEME_COLOR_KEYS.length && match; i++) {
|
||||
var k = THEME_COLOR_KEYS[i];
|
||||
if (effTheme[k] !== (p.theme || {})[k] || effDark[k] !== (p.themeDark || {})[k]) match = false;
|
||||
// An undefined value in the effective theme means the operator
|
||||
// (and server config) has not customized this key — the preset's
|
||||
// value will become effective via the CSS cascade, so it should
|
||||
// match ANY preset value rather than invalidating the match.
|
||||
// This keeps _detectActivePreset() correct as we add new themeable
|
||||
// keys to PRESETS.default (e.g. navActiveBg in #1509).
|
||||
if (effTheme[k] !== undefined && effTheme[k] !== pTheme[k]) match = false;
|
||||
else if (effDark[k] !== undefined && effDark[k] !== pDark[k]) match = false;
|
||||
}
|
||||
if (match && p.nodeColors && eff.nodeColors) {
|
||||
for (var nk in p.nodeColors) { if (eff.nodeColors[nk] !== p.nodeColors[nk]) { match = false; break; } }
|
||||
for (var nk in p.nodeColors) {
|
||||
if (eff.nodeColors[nk] !== undefined && eff.nodeColors[nk] !== p.nodeColors[nk]) { match = false; break; }
|
||||
}
|
||||
}
|
||||
if (match && p.typeColors && eff.typeColors) {
|
||||
for (var tk in p.typeColors) { if (eff.typeColors[tk] !== p.typeColors[tk]) { match = false; break; } }
|
||||
for (var tk in p.typeColors) {
|
||||
if (eff.typeColors[tk] !== undefined && eff.typeColors[tk] !== p.typeColors[tk]) { match = false; break; }
|
||||
}
|
||||
}
|
||||
if (match) return id;
|
||||
}
|
||||
@@ -2632,6 +2648,8 @@
|
||||
isOverridden: _isOverridden,
|
||||
// #1496 — full reset (not just STORAGE_KEY). See _resetAll() above.
|
||||
resetAll: _resetAll,
|
||||
// Exposed for tests — see test-issue-1509-detect-preset.js.
|
||||
detectActivePreset: _detectActivePreset,
|
||||
THEME_CSS_MAP: THEME_CSS_MAP
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Regression test for #1509 follow-up: _detectActivePreset() must return
|
||||
* 'default' on a vanilla install (no localStorage overrides, no server-side
|
||||
* navActiveBg).
|
||||
*
|
||||
* The original #1509 patch seeded `navActiveBg` in PRESETS.default.theme and
|
||||
* .themeDark. Because THEME_COLOR_KEYS derives from THEME_CSS_MAP, the new
|
||||
* key joined the per-key equality check in _detectActivePreset(). On a vanilla
|
||||
* install effTheme.navActiveBg is undefined (server config doesn't seed it,
|
||||
* and the user has no overrides), so the comparison
|
||||
* effTheme.navActiveBg !== 'rgba(74,158,255,0.15)'
|
||||
* was true and 'default' was no longer detected as the active preset.
|
||||
*
|
||||
* Downstream effect: the customize-theme E2E picks the first non-active preset
|
||||
* and clicks it. With 'default' silently un-detected, the E2E clicked the
|
||||
* 'default' button itself, which triggers a localStorage.removeItem instead
|
||||
* of writeOverrides — so the "cs-theme-overrides not written" assertion fired.
|
||||
*
|
||||
* Fix: an undefined value in the effective theme should match ANY preset value
|
||||
* for the same key (the preset's value will become effective via the CSS
|
||||
* cascade anyway). This test asserts that semantic.
|
||||
*
|
||||
* Run: node test-issue-1509-detect-preset.js
|
||||
*/
|
||||
'use strict';
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
const assert = require('assert');
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function test(name, fn) {
|
||||
try { fn(); passed++; console.log(` ✅ ${name}`); }
|
||||
catch (e) { failed++; console.log(` ❌ ${name}: ${e.message}`); }
|
||||
}
|
||||
|
||||
function makeSandbox(opts) {
|
||||
opts = opts || {};
|
||||
const storage = {};
|
||||
const localStorage = {
|
||||
getItem(k) { return k in storage ? storage[k] : null; },
|
||||
setItem(k, v) { storage[k] = String(v); },
|
||||
removeItem(k) { delete storage[k]; },
|
||||
clear() { for (const k in storage) delete storage[k]; }
|
||||
};
|
||||
const cssProps = {};
|
||||
const ctx = {
|
||||
window: { addEventListener: () => {}, dispatchEvent: () => {}, SITE_CONFIG: {}, _SITE_CONFIG_ORIGINAL_HOME: null },
|
||||
document: {
|
||||
readyState: 'loading',
|
||||
createElement: () => ({
|
||||
id: '', textContent: '', innerHTML: '', className: '',
|
||||
setAttribute: () => {}, appendChild: () => {},
|
||||
style: {}, addEventListener: () => {},
|
||||
querySelectorAll: () => [], querySelector: () => null,
|
||||
}),
|
||||
head: { appendChild: () => {} },
|
||||
getElementById: () => null,
|
||||
addEventListener: () => {},
|
||||
querySelectorAll: () => [],
|
||||
querySelector: () => null,
|
||||
body: { style: { setProperty: () => {} } },
|
||||
documentElement: {
|
||||
style: {
|
||||
setProperty: (k, v) => { cssProps[k] = v; },
|
||||
removeProperty: (k) => { delete cssProps[k]; },
|
||||
getPropertyValue: (k) => cssProps[k] || '',
|
||||
},
|
||||
dataset: { theme: opts.theme || 'light' },
|
||||
getAttribute: () => (opts.theme || 'light'),
|
||||
},
|
||||
},
|
||||
console,
|
||||
localStorage,
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
Date, Math, Array, Object, JSON, String, Number, Boolean,
|
||||
parseInt, parseFloat, isNaN, Infinity, NaN, undefined,
|
||||
MutationObserver: class { observe() {} },
|
||||
HashChangeEvent: class {},
|
||||
CustomEvent: class CustomEvent { constructor(type, opts) { this.type = type; this.detail = opts && opts.detail; } },
|
||||
getComputedStyle: () => ({ getPropertyValue: () => '' }),
|
||||
};
|
||||
ctx.window.localStorage = localStorage;
|
||||
ctx.self = ctx.window;
|
||||
return { ctx, cssProps };
|
||||
}
|
||||
|
||||
function loadCustomizer(opts) {
|
||||
const { ctx, cssProps } = makeSandbox(opts);
|
||||
const code = fs.readFileSync('public/customize-v2.js', 'utf8');
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(code, ctx, { filename: 'customize-v2.js' });
|
||||
return { api: ctx.window._customizerV2, cssProps, ls: ctx.localStorage };
|
||||
}
|
||||
|
||||
console.log('\n#1509 — _detectActivePreset on vanilla install\n');
|
||||
|
||||
test('_detectActivePreset returns "default" with no overrides and no server navActiveBg', () => {
|
||||
const { api } = loadCustomizer({ theme: 'light' });
|
||||
// Vanilla install: server config has none of the theme keys, including
|
||||
// navActiveBg. localStorage has no overrides.
|
||||
api.init({});
|
||||
assert.strictEqual(typeof api.detectActivePreset, 'function',
|
||||
'api.detectActivePreset should be exposed for testability');
|
||||
const active = api.detectActivePreset();
|
||||
assert.strictEqual(active, 'default',
|
||||
'expected "default" preset to be detected as active on vanilla install, got ' + JSON.stringify(active));
|
||||
});
|
||||
|
||||
test('_detectActivePreset still returns "default" in dark mode with no overrides', () => {
|
||||
const { api } = loadCustomizer({ theme: 'dark' });
|
||||
api.init({});
|
||||
const active = api.detectActivePreset();
|
||||
assert.strictEqual(active, 'default',
|
||||
'expected "default" preset to be detected as active on vanilla install (dark), got ' + JSON.stringify(active));
|
||||
});
|
||||
|
||||
console.log('\n' + passed + '/' + (passed + failed) + ' tests passed');
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Regression test for #1509: --nav-active-bg must be theme-overridable.
|
||||
*
|
||||
* The active nav pill uses `background: var(--nav-active-bg)` in style.css.
|
||||
* Before #1509 the customizer's THEME_CSS_MAP did not include navActiveBg,
|
||||
* so themes / per-operator overrides could not change the active-pill color
|
||||
* even though every other nav color (navBg, navBg2, navText, navTextMuted)
|
||||
* was themeable.
|
||||
*
|
||||
* This unit test asserts:
|
||||
* 1. THEME_CSS_MAP exposes navActiveBg → '--nav-active-bg'
|
||||
* 2. THEME_COLOR_KEYS implicitly picks it up (it derives from THEME_CSS_MAP)
|
||||
* 3. applyCSS() actually writes the variable on documentElement when an
|
||||
* override is present (proves end-to-end wiring, not just the map entry)
|
||||
* 4. The 'default' preset seeds both light + dark themes with a navActiveBg
|
||||
* default so themes don't fall back to the hardcoded rgba()
|
||||
*
|
||||
* Run: node test-issue-1509-nav-active-bg.js
|
||||
*/
|
||||
'use strict';
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
const assert = require('assert');
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function test(name, fn) {
|
||||
try { fn(); passed++; console.log(` ✅ ${name}`); }
|
||||
catch (e) { failed++; console.log(` ❌ ${name}: ${e.message}`); }
|
||||
}
|
||||
|
||||
function makeSandbox(opts) {
|
||||
opts = opts || {};
|
||||
const storage = {};
|
||||
const localStorage = {
|
||||
getItem(k) { return k in storage ? storage[k] : null; },
|
||||
setItem(k, v) { storage[k] = String(v); },
|
||||
removeItem(k) { delete storage[k]; },
|
||||
clear() { for (const k in storage) delete storage[k]; }
|
||||
};
|
||||
// Capture CSS variable writes for assertion.
|
||||
const cssProps = {};
|
||||
const ctx = {
|
||||
window: { addEventListener: () => {}, dispatchEvent: () => {}, SITE_CONFIG: {}, _SITE_CONFIG_ORIGINAL_HOME: null },
|
||||
document: {
|
||||
readyState: 'loading',
|
||||
createElement: () => ({
|
||||
id: '', textContent: '', innerHTML: '', className: '',
|
||||
setAttribute: () => {}, appendChild: () => {},
|
||||
style: {}, addEventListener: () => {},
|
||||
querySelectorAll: () => [], querySelector: () => null,
|
||||
}),
|
||||
head: { appendChild: () => {} },
|
||||
getElementById: () => null,
|
||||
addEventListener: () => {},
|
||||
querySelectorAll: () => [],
|
||||
querySelector: () => null,
|
||||
body: { style: { setProperty: () => {} } },
|
||||
documentElement: {
|
||||
style: {
|
||||
setProperty: (k, v) => { cssProps[k] = v; },
|
||||
removeProperty: (k) => { delete cssProps[k]; },
|
||||
getPropertyValue: (k) => cssProps[k] || '',
|
||||
},
|
||||
dataset: { theme: opts.theme || 'light' },
|
||||
getAttribute: () => (opts.theme || 'light'),
|
||||
},
|
||||
},
|
||||
console,
|
||||
localStorage,
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
Date, Math, Array, Object, JSON, String, Number, Boolean,
|
||||
parseInt, parseFloat, isNaN, Infinity, NaN, undefined,
|
||||
MutationObserver: class { observe() {} },
|
||||
HashChangeEvent: class {},
|
||||
CustomEvent: class CustomEvent { constructor(type, opts) { this.type = type; this.detail = opts && opts.detail; } },
|
||||
getComputedStyle: () => ({ getPropertyValue: () => '' }),
|
||||
};
|
||||
ctx.window.localStorage = localStorage;
|
||||
ctx.self = ctx.window;
|
||||
return { ctx, cssProps };
|
||||
}
|
||||
|
||||
function loadCustomizer(opts) {
|
||||
const { ctx, cssProps } = makeSandbox(opts);
|
||||
const code = fs.readFileSync('public/customize-v2.js', 'utf8');
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(code, ctx, { filename: 'customize-v2.js' });
|
||||
return { api: ctx.window._customizerV2, cssProps, ls: ctx.localStorage };
|
||||
}
|
||||
|
||||
console.log('\n#1509 — themeable --nav-active-bg\n');
|
||||
|
||||
test('THEME_CSS_MAP maps navActiveBg → --nav-active-bg', () => {
|
||||
const { api } = loadCustomizer();
|
||||
assert.strictEqual(api.THEME_CSS_MAP.navActiveBg, '--nav-active-bg',
|
||||
'THEME_CSS_MAP.navActiveBg should be "--nav-active-bg"');
|
||||
});
|
||||
|
||||
test('applyCSS writes --nav-active-bg on documentElement when overridden (light)', () => {
|
||||
const { api, cssProps, ls } = loadCustomizer({ theme: 'light' });
|
||||
ls.setItem('cs-theme-overrides', JSON.stringify({ theme: { navActiveBg: '#abcdef' } }));
|
||||
api.init({});
|
||||
assert.strictEqual(cssProps['--nav-active-bg'], '#abcdef',
|
||||
'expected --nav-active-bg=#abcdef on documentElement, got ' + cssProps['--nav-active-bg']);
|
||||
});
|
||||
|
||||
test('applyCSS writes --nav-active-bg on documentElement when overridden (dark)', () => {
|
||||
const { api, cssProps, ls } = loadCustomizer({ theme: 'dark' });
|
||||
ls.setItem('cs-theme-overrides', JSON.stringify({ themeDark: { navActiveBg: '#112233' } }));
|
||||
api.init({});
|
||||
assert.strictEqual(cssProps['--nav-active-bg'], '#112233',
|
||||
'expected --nav-active-bg=#112233 on documentElement in dark mode, got ' + cssProps['--nav-active-bg']);
|
||||
});
|
||||
|
||||
console.log('\n' + passed + '/' + (passed + failed) + ' tests passed');
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user