theme-registration: preserve pending ext skin across reload + expand accent token allowlist
Found via live browser testing of the first consumer (e-ink skin): - An extension-registered skin selected by the user was lost on reload: both the pre-paint inline script in index.html AND the boot-time appearance sync normalized the persisted skin against the (extension-less) known-skins list and clobbered localStorage back to 'default' before the extension's registerHermesSkin() could run. Both sites now PRESERVE an unknown non-default persisted skin (a likely pending extension skin), so it survives the reload and the extension re-applies it on load. The server push is skipped for a still-pending ext skin. - Expanded the token allowlist to the full accent family (--accent-hover/-text/-bg/-bg-strong/-rgb + --surface-subtle) so a skin can fully recolor accent-driven chrome (active rail, chips, profile labels), not just --accent. Value regex now also accepts a bare RGB triple for --accent-rgb. Regression tests added for both preservation sites. Full suite: 10876 passed.
This commit is contained in:
1 parent
6d8135df3a
commit
dfe64313dc
4 files changed
+55
-16
No files matched your search
+18
-7
@@ -2013,15 +2013,17 @@ const _RESERVED_SKIN_KEYS=new Set((_SKINS||[]).map(s=>(s.value||s.name).toLowerC
|
||||
// CSS custom-property names a skin is allowed to set. Mirrors the documented
|
||||
// design-token contract; anything outside this set is dropped.
|
||||
const _ALLOWED_SKIN_TOKENS=new Set([
|
||||
'--bg','--surface','--surface2','--text','--text2','--muted',
|
||||
'--accent','--accent2','--accent3','--accent-contrast',
|
||||
'--bg','--surface','--surface2','--surface-subtle','--text','--text2','--muted',
|
||||
'--accent','--accent2','--accent3','--accent-contrast','--accent-hover',
|
||||
'--accent-text','--accent-bg','--accent-bg-strong','--accent-rgb',
|
||||
'--border','--border2','--hover-bg','--code-bg','--code-text',
|
||||
'--sidebar','--sidebar-text','--user-bubble','--assistant-bubble',
|
||||
'--success','--warning','--danger','--info','--link'
|
||||
]);
|
||||
// Accept only safe color / simple numeric-with-unit values. Rejects anything
|
||||
// Accept only safe color / simple numeric-with-unit values, OR a bare RGB triple
|
||||
// (e.g. "0, 0, 0" for --accent-rgb, consumed inside rgba(...)). Rejects anything
|
||||
// with url(), expression(), semicolons, braces, or other CSS-injection vectors.
|
||||
const _SAFE_SKIN_VALUE_RE=/^(#(?:[0-9a-fA-F]{3,8})|rg(?:b|ba)\(\s*[0-9.,%\s/]+\)|hsl(?:a)?\(\s*[0-9.,%\s/deg]+\)|[a-zA-Z]{3,20}|[0-9.]+(?:px|em|rem|%)?)$/;
|
||||
const _SAFE_SKIN_VALUE_RE=/^(#(?:[0-9a-fA-F]{3,8})|rg(?:b|ba)\(\s*[0-9.,%\s/]+\)|hsl(?:a)?\(\s*[0-9.,%\s/deg]+\)|[0-9]{1,3}\s*,\s*[0-9]{1,3}\s*,\s*[0-9]{1,3}|[a-zA-Z]{3,20}|[0-9.]+(?:px|em|rem|%)?)$/;
|
||||
|
||||
function _sanitizeSkinTokens(tokens){
|
||||
const out={};
|
||||
@@ -2299,17 +2301,26 @@ window._applyTitlebarProfileVisibility=_applyTitlebarProfileVisibility;
|
||||
const lsTheme=(localStorage.getItem('hermes-theme')||'').trim().toLowerCase();
|
||||
const lsSkin=(localStorage.getItem('hermes-skin')||'').trim().toLowerCase();
|
||||
const lsAppearance=_normalizeAppearance(lsTheme||null,lsSkin||null);
|
||||
// An unknown non-default persisted skin is most likely an extension-provided
|
||||
// skin (registerHermesSkin) whose extension script hasn't registered it yet
|
||||
// at this point in boot. Preserve it verbatim instead of normalizing it away
|
||||
// to 'default' — the extension's registerHermesSkin() will inject the CSS and
|
||||
// re-apply it once it loads. Without this, the boot sync would clobber the
|
||||
// saved choice before the extension runs.
|
||||
const lsSkinIsPendingExt=!!lsSkin&&lsSkin!=='default'&&!_VALID_SKINS.has(lsSkin)&&!_LEGACY_THEME_MAP[lsSkin];
|
||||
const lsHasExplicitSkin=lsSkin&&lsSkin!=='default';
|
||||
const lsHasExplicitTheme=lsTheme&&['system','light','dark'].includes(lsTheme);
|
||||
const theme=lsHasExplicitTheme?lsAppearance.theme:srvAppearance.theme;
|
||||
const skin=lsHasExplicitSkin?lsAppearance.skin:srvAppearance.skin;
|
||||
const skin=lsHasExplicitSkin?(lsSkinIsPendingExt?lsSkin:lsAppearance.skin):srvAppearance.skin;
|
||||
localStorage.setItem('hermes-theme',theme);
|
||||
_applyTheme(theme);
|
||||
localStorage.setItem('hermes-skin',skin);
|
||||
_applySkin(skin);
|
||||
// Reconcile: if localStorage and server disagree, push localStorage
|
||||
// values to the server so the next refresh won't revert.
|
||||
if((lsHasExplicitTheme||lsHasExplicitSkin)&&(theme!==srvAppearance.theme||skin!==srvAppearance.skin)){
|
||||
// values to the server so the next refresh won't revert. Skip the push for a
|
||||
// still-pending extension skin (don't persist it server-side until it's a
|
||||
// confirmed-registered skin — avoids writing a skin the server can't validate).
|
||||
if((lsHasExplicitTheme||lsHasExplicitSkin)&&!lsSkinIsPendingExt&&(theme!==srvAppearance.theme||skin!==srvAppearance.skin)){
|
||||
try{
|
||||
api('/api/settings',{method:'POST',body:JSON.stringify({theme,skin})});
|
||||
}catch(_){}
|
||||
|
||||
Reference in new issue
Block a user