fix(#1438): write customizer override to body.style too (follow-up to #1439) (#1441)

## 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 <bot@openclaw>
This commit is contained in:
Kpa-clawbotandOpenClaw Bot authored and GitHub committed 2026-05-27 10:14:34 -07:00
1 parent a7e750ad71
commit 074e3d6bed
2 files changed
+42 -18

No files matched your search

+34 -18
View File
@@ -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.
+8
View File
@@ -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)');
}
}