feat(FN-4916): complete Step 1 — add static theme-data link

Fusion-Task-Id: FN-4916
Fusion-Task-Lineage: 80b56c97-04c6-4cd4-bb83-e3ef8faaa4ee
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 10:16:20 -07:00
committed by gsxdsm
parent 03a10c7419
commit e25f8b20aa
2 changed files with 20 additions and 65 deletions

View File

@@ -158,16 +158,10 @@ function applyThemeAttributes(
} }
/** /**
* Load theme-data.css for non-default themes. * Reconcile the statically declared #theme-data stylesheet href for the current base URI.
* Safely handles existing links by checking href and updating if stale.
* After href reconciliation, existing links are moved to the end of <head>
* to ensure color-theme CSS rules take precedence over base token
* redefinitions in subsequent stylesheets (CSS cascade correctness).
* *
* This is critical for dark mode: if #theme-data is injected early by * The link is authored in app/index.html so browsers can discover and fetch theme-data.css
* pre-hydration scripts but styles.css loads later and redefines base * during HTML parsing. Runtime only updates href when needed (notably file:// Electron paths).
* tokens, those redefinitions win the cascade unless theme-data is
* repositioned to come after them.
*/ */
function loadThemeDataStylesheet(): void { function loadThemeDataStylesheet(): void {
if (!isBrowser) return; if (!isBrowser) return;
@@ -176,22 +170,13 @@ function loadThemeDataStylesheet(): void {
const existingLink = document.getElementById(THEME_DATA_ID) as HTMLLinkElement | null; const existingLink = document.getElementById(THEME_DATA_ID) as HTMLLinkElement | null;
if (existingLink) { if (existingLink) {
// Link exists - update href if it differs from expected (handles baseURI changes)
if (existingLink.href !== expectedHref) { if (existingLink.href !== expectedHref) {
existingLink.href = expectedHref; existingLink.href = expectedHref;
} }
// Move existing link to end of <head> for CSS cascade correctness.
// This ensures color-theme rules (which use [data-color-theme="..."] selectors)
// are evaluated AFTER any subsequent stylesheets that might redefine base tokens.
// Without this, dark color themes can appear broken because base token
// redefinitions win the cascade over color-theme rules.
if (existingLink.parentNode === document.head && document.head.lastChild !== existingLink) {
document.head.appendChild(existingLink);
}
return; return;
} }
// No existing link - create one // Defensive fallback: index.html should always provide this link.
const link = document.createElement("link"); const link = document.createElement("link");
link.rel = "stylesheet"; link.rel = "stylesheet";
link.href = expectedHref; link.href = expectedHref;
@@ -200,15 +185,10 @@ function loadThemeDataStylesheet(): void {
} }
/** /**
* Unload theme-data.css when returning to default theme. * No-op: #theme-data stays mounted permanently via static index.html markup.
* Theme rules only apply when [data-color-theme="..."] selectors match.
*/ */
function unloadThemeDataStylesheet(): void { function unloadThemeDataStylesheet(): void {}
if (!isBrowser) return;
const existing = document.getElementById(THEME_DATA_ID);
if (existing) {
existing.remove();
}
}
/** /**
* Custom hook for theme management. * Custom hook for theme management.
@@ -444,19 +424,8 @@ export function getThemeInitScript(): string {
} }
var existingLink = document.getElementById('theme-data'); var existingLink = document.getElementById('theme-data');
if (existingLink && existingLink.tagName === 'LINK') { if (existingLink && existingLink.tagName === 'LINK' && existingLink.href !== themeDataUrl) {
if (existingLink.href !== themeDataUrl) { existingLink.href = themeDataUrl;
existingLink.href = themeDataUrl;
}
if (existingLink.parentNode === document.head && document.head.lastChild !== existingLink) {
document.head.appendChild(existingLink);
}
} else {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = themeDataUrl;
link.id = 'theme-data';
document.head.appendChild(link);
} }
} }
} catch (e) { } catch (e) {

View File

@@ -38,8 +38,6 @@
document.documentElement.setAttribute('data-theme', effectiveMode); document.documentElement.setAttribute('data-theme', effectiveMode);
document.documentElement.setAttribute('data-color-theme', colorTheme); document.documentElement.setAttribute('data-color-theme', colorTheme);
document.documentElement.style.fontSize = fontScale + '%'; document.documentElement.style.fontSize = fontScale + '%';
// Load theme-data.css for non-default themes to prevent flash
// This logic mirrors app/hooks/useTheme.ts#getThemeDataUrl()
if (colorTheme !== 'default') { if (colorTheme !== 'default') {
var base = document.baseURI || (document.location && document.location.href) || ''; var base = document.baseURI || (document.location && document.location.href) || '';
var themeDataUrl; var themeDataUrl;
@@ -48,33 +46,20 @@
} else if (base.indexOf('http://') === 0 || base.indexOf('https://') === 0) { } else if (base.indexOf('http://') === 0 || base.indexOf('https://') === 0) {
themeDataUrl = new URL('/theme-data.css', base).toString(); themeDataUrl = new URL('/theme-data.css', base).toString();
} else if (base.indexOf('file://') === 0) { } else if (base.indexOf('file://') === 0) {
themeDataUrl = base.endsWith('/') if (base.endsWith('/')) {
? base.slice(0, -1) + '/theme-data.css' themeDataUrl = base.slice(0, -1) + '/theme-data.css';
: base.replace(/\/[^\/]+$/, '/theme-data.css'); } else {
var lastSlashIndex = base.lastIndexOf('/');
themeDataUrl = lastSlashIndex >= 0
? base.slice(0, lastSlashIndex) + '/theme-data.css'
: '/theme-data.css';
}
} else { } else {
themeDataUrl = '/theme-data.css'; themeDataUrl = '/theme-data.css';
} }
// Check for existing link and update href if stale
var existingLink = document.getElementById('theme-data'); var existingLink = document.getElementById('theme-data');
if (existingLink && existingLink.tagName === 'LINK') { if (existingLink && existingLink.tagName === 'LINK' && existingLink.href !== themeDataUrl) {
// Update href if it differs (handles baseURI changes between loads) existingLink.href = themeDataUrl;
if (existingLink.href !== themeDataUrl) {
existingLink.href = themeDataUrl;
}
// Move existing link to end of <head> for CSS cascade correctness.
// This ensures color-theme rules take precedence over base token
// redefinitions in subsequent stylesheets.
if (existingLink.parentNode === document.head && document.head.lastChild !== existingLink) {
document.head.appendChild(existingLink);
}
} else {
// No existing link - create one
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = themeDataUrl;
link.id = 'theme-data';
document.head.appendChild(link);
} }
} }
} catch (e) { } catch (e) {
@@ -85,6 +70,7 @@
})(); })();
</script> </script>
<!-- fusion:view-preload --> <!-- fusion:view-preload -->
<link rel="stylesheet" href="/theme-data.css" id="theme-data" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>