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.
* 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).
* Reconcile the statically declared #theme-data stylesheet href for the current base URI.
*
* This is critical for dark mode: if #theme-data is injected early by
* pre-hydration scripts but styles.css loads later and redefines base
* tokens, those redefinitions win the cascade unless theme-data is
* repositioned to come after them.
* The link is authored in app/index.html so browsers can discover and fetch theme-data.css
* during HTML parsing. Runtime only updates href when needed (notably file:// Electron paths).
*/
function loadThemeDataStylesheet(): void {
if (!isBrowser) return;
@@ -176,22 +170,13 @@ function loadThemeDataStylesheet(): void {
const existingLink = document.getElementById(THEME_DATA_ID) as HTMLLinkElement | null;
if (existingLink) {
// Link exists - update href if it differs from expected (handles baseURI changes)
if (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;
}
// No existing link - create one
// Defensive fallback: index.html should always provide this link.
const link = document.createElement("link");
link.rel = "stylesheet";
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 {
if (!isBrowser) return;
const existing = document.getElementById(THEME_DATA_ID);
if (existing) {
existing.remove();
}
}
function unloadThemeDataStylesheet(): void {}
/**
* Custom hook for theme management.
@@ -444,19 +424,8 @@ export function getThemeInitScript(): string {
}
var existingLink = document.getElementById('theme-data');
if (existingLink && existingLink.tagName === 'LINK') {
if (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);
if (existingLink && existingLink.tagName === 'LINK' && existingLink.href !== themeDataUrl) {
existingLink.href = themeDataUrl;
}
}
} catch (e) {

View File

@@ -38,8 +38,6 @@
document.documentElement.setAttribute('data-theme', effectiveMode);
document.documentElement.setAttribute('data-color-theme', colorTheme);
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') {
var base = document.baseURI || (document.location && document.location.href) || '';
var themeDataUrl;
@@ -48,33 +46,20 @@
} else if (base.indexOf('http://') === 0 || base.indexOf('https://') === 0) {
themeDataUrl = new URL('/theme-data.css', base).toString();
} else if (base.indexOf('file://') === 0) {
themeDataUrl = base.endsWith('/')
? base.slice(0, -1) + '/theme-data.css'
: base.replace(/\/[^\/]+$/, '/theme-data.css');
if (base.endsWith('/')) {
themeDataUrl = base.slice(0, -1) + '/theme-data.css';
} else {
var lastSlashIndex = base.lastIndexOf('/');
themeDataUrl = lastSlashIndex >= 0
? base.slice(0, lastSlashIndex) + '/theme-data.css'
: '/theme-data.css';
}
} else {
themeDataUrl = '/theme-data.css';
}
// Check for existing link and update href if stale
var existingLink = document.getElementById('theme-data');
if (existingLink && existingLink.tagName === 'LINK') {
// Update href if it differs (handles baseURI changes between loads)
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);
if (existingLink && existingLink.tagName === 'LINK' && existingLink.href !== themeDataUrl) {
existingLink.href = themeDataUrl;
}
}
} catch (e) {
@@ -85,6 +70,7 @@
})();
</script>
<!-- fusion:view-preload -->
<link rel="stylesheet" href="/theme-data.css" id="theme-data" />
</head>
<body>
<div id="root"></div>