From 074e3d6bed444e42064f1b29890c78a2bcd6ac3a Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Wed, 27 May 2026 10:14:34 -0700 Subject: [PATCH] fix(#1438): write customizer override to body.style too (follow-up to #1439) (#1441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Follow-up to #1439. Empirical CDP verification on staging caught a residual bug: the customizer per-role override updated `documentElement.style` (where the override helper writes) but mounted SVG markers and other CSS-var consumers kept showing the active preset colour. ## Root cause `cb-presets.js` ships stylesheet rules of the form: ```css body[data-cb-preset="deut"] { --mc-role-companion: #648FFF; ... } ``` This selector beats inheritance from `:root.style` (which is where #1439's `setRoleColorOverride` wrote). Body inline style beats both. ## Fix `setRoleColorOverride` now writes the override to BOTH `documentElement.style` and `document.body.style`. The first-override snapshot is captured per target so clear-override still restores the active preset value (#1412 contract preserved). ## Verification - `test-issue-1438-marker-css-vars.js` extended with assertion E2 (helper touches `document.body` / `body.style`) - `test-issue-1412-customizer-no-override.js` — 13/13 still pass (clear-override-restores-preset) - `test-issue-1407-cb-preset-propagation.js` — 61/61 still pass - Staging CDP verified: `applyPreset('deut')` + `setRoleColorOverride('companion', '#ff00ff')` repaints all 55 mounted companion markers to magenta without reload. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — clean. Fixes the residual case left after #1439. Co-authored-by: OpenClaw Bot --- public/roles.js | 52 +++++++++++++++++++----------- test-issue-1438-marker-css-vars.js | 8 +++++ 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/public/roles.js b/public/roles.js index 302c1b74..a607217d 100644 --- a/public/roles.js +++ b/public/roles.js @@ -90,39 +90,55 @@ // that want to override at runtime (customizer "node colors" path). // #1438: snapshot of the cb-preset (or initial) CSS-var value per role // so that clearing an override restores the preset, not nothing. + // We write the override to BOTH documentElement and body inline styles + // because cb-presets ships stylesheet rules of the form + // body[data-cb-preset="deut"] { --mc-role-X: #...; } + // which beats inheritance from :root. Body inline beats both. var _presetCssSnapshot = {}; + function _styleTargets() { + var t = []; + try { if (document.documentElement && document.documentElement.style) t.push(document.documentElement.style); } catch (e) {} + try { if (document.body && document.body.style) t.push(document.body.style); } catch (e) {} + return t.filter(function (s) { return s && typeof s.setProperty === 'function'; }); + } window.setRoleColorOverride = function (role, hex) { if (!role) return; - var rootStyle = null; - try { rootStyle = document.documentElement && document.documentElement.style; } - catch (e) { /* SSR */ } + var targets = _styleTargets(); + var varName = '--mc-role-' + role; if (hex == null || hex === '') { - // Clear override → restore prior CSS var value (the preset's - // value when the override was first set), so CSS-var consumers - // see the preset color again — matching the JS getter behavior. + // Clear override → restore prior CSS var values captured at + // first-override time, so CSS-var consumers see the preset color + // again (matches JS getter behavior, preserves #1412 contract). delete _roleOverrides[role]; - if (rootStyle && Object.prototype.hasOwnProperty.call(_presetCssSnapshot, role)) { - var prior = _presetCssSnapshot[role]; - if (prior) rootStyle.setProperty('--mc-role-' + role, prior); - else rootStyle.removeProperty('--mc-role-' + role); + if (Object.prototype.hasOwnProperty.call(_presetCssSnapshot, role)) { + var snap = _presetCssSnapshot[role] || {}; + targets.forEach(function (s, i) { + var prior = snap[i]; + if (prior && prior.length) s.setProperty(varName, prior); + else s.removeProperty(varName); + }); delete _presetCssSnapshot[role]; + } else { + targets.forEach(function (s) { s.removeProperty(varName); }); } return; } - // Capture the current CSS var (preset / default) before overwriting, + // Capture the current per-target CSS var values before overwriting, // but only on the first override for this role so repeated picks // don't lose the original preset value. - if (rootStyle && !Object.prototype.hasOwnProperty.call(_presetCssSnapshot, role)) { - _presetCssSnapshot[role] = rootStyle.getPropertyValue - ? (rootStyle.getPropertyValue('--mc-role-' + role) || '').trim() - : ''; + if (!Object.prototype.hasOwnProperty.call(_presetCssSnapshot, role)) { + _presetCssSnapshot[role] = targets.map(function (s) { + return s.getPropertyValue ? (s.getPropertyValue(varName) || '').trim() : ''; + }); } _roleOverrides[role] = hex; // #1438: drive the CSS var so CSS-var consumers (cluster pills, - // route lines, all marker SVGs that now use fill="var(--mc-role-X)") - // pick up the operator's hex without a page reload. - if (rootStyle) rootStyle.setProperty('--mc-role-' + role, hex); + // route lines, all marker SVGs that use fill="var(--mc-role-X)") + // 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); }); }; // Back-compat: also export the writable override map so customize.js's // `window.ROLE_COLORS[key] = inp.value` style mutation works. diff --git a/test-issue-1438-marker-css-vars.js b/test-issue-1438-marker-css-vars.js index 6343933a..c08905f0 100644 --- a/test-issue-1438-marker-css-vars.js +++ b/test-issue-1438-marker-css-vars.js @@ -114,6 +114,14 @@ console.log('\n=== #1438 E: roles.js setRoleColorOverride writes --mc-role-* === if (fnMatch) { assert(/--mc-role-/.test(fnMatch[0]), 'setRoleColorOverride body writes --mc-role-{role} CSS var'); + // Follow-up: cb-presets ships stylesheet rules of the form + // body[data-cb-preset="X"] { --mc-role-Y: ...; } + // which beat inheritance from :root. The override helper MUST + // write to body.style.setProperty too, otherwise the customizer + // picks remain invisible after a preset switch. + assert(/document\.body/.test(fnMatch[0]) || + /body\.style/.test(rolesSrc.slice(rolesSrc.indexOf('_styleTargets'), rolesSrc.indexOf('_styleTargets') + 600)), + 'setRoleColorOverride writes to document.body.style (beats body[data-cb-preset] cascade)'); } }