fix(theme): switch to .dark class (Astro/Vite strips [data-theme] selectors)

ROOT CAUSE found via build artifact analysis:
- Built CSS files (dist/_astro/Base.*.css) had ZERO [data-theme='dark']
  selectors even though source had 17. Astro/Vite CSS optimizer
  strips attribute selectors that don't match any static HTML attribute
  (we set data-theme dynamically via JS, not in JSX/HTML).
- Also stripped: all html { } and body { } rules from source.
- Result: dark mode visually did nothing. Body stayed white.

FIX:
- Replace [data-theme='dark'] with html.dark (17 occurrences in
  fx-system.css). Vite keeps .dark class selectors because the
  anti-flash script (in Base.astro <head>) sets a class, not an
  attribute, which Vite sees as 'used'.
- Update anti-flash script in Base.astro: classList.add/remove
  instead of setAttribute('data-theme', ...).
- Update UtilityBar.astro applyTheme() to use classList too.
- Restore body { background: var(--body-bg) } override (was stripped
  by Vite as 'unused' — but now html.dark applies to it correctly).

ALSO FIXED theme toggle button visibility (from previous turn):
- Removed v7-5 base .fx-theme-toggle rule (rgba(0.1) opacity made
  button invisible — only visible on hover).
- Consolidated into single rule with proper contrast for both modes.

Verified by:
- Build complete: 22 pages, 1.97s
- Built CSS: 17 html.dark selectors present (was 0)
- Body background override present in built CSS
- HTTP server on :4322 serving correct artifacts
This commit is contained in:
Kunthawat Greethong
2026-06-14 09:41:32 +07:00
parent 96caca4af6
commit caab40d9a4
3 changed files with 102 additions and 23 deletions

View File

@@ -64,7 +64,12 @@ const email = site?.data?.email ?? 'contact@moreminimore.com';
function applyTheme(mode: ThemeMode) {
const eff = effectiveTheme(mode);
document.documentElement.setAttribute('data-theme', eff);
// Use class instead of data-theme (Astro/Vite keeps .dark selectors)
if (eff === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
const indicator = document.getElementById('fx-mode-indicator');
const btn = document.getElementById('fx-theme-toggle');
if (indicator) indicator.textContent = mode; // shows user's chosen mode (not effective)

View File

@@ -38,10 +38,13 @@ const {
<title>{title}</title>
<!-- Anti-flash theme: apply data-theme BEFORE first paint.
<!-- Anti-flash theme: apply .dark class on <html> BEFORE first paint.
Reads localStorage 'moreminimore-theme' or falls back to OS preference.
If neither set, defaults to 'light' (matches v7-5 demo).
Inline (no defer) is intentional — must run synchronously. -->
Inline (no defer) is intentional — must run synchronously.
NOTE: We use .dark class instead of data-theme="dark" because
Astro/Vite CSS optimizer strips [data-theme="..."] selectors
that don't match any static HTML (we set it dynamically via JS). -->
<script is:inline>
(function() {
try {
@@ -53,10 +56,15 @@ const {
// 'auto' or unset: follow system
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.setAttribute('data-theme', theme);
// Use class instead of data-theme attribute (Vite/Astro keep .dark selectors)
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} catch (e) {
// localStorage blocked or no matchMedia — default light
document.documentElement.setAttribute('data-theme', 'light');
document.documentElement.classList.remove('dark');
}
})();
</script>

View File

@@ -248,9 +248,10 @@ img{max-width:100%;display:block}
.fx-utility-bar-left,.fx-utility-bar-right{display:flex;align-items:center;gap:16px}
.fx-utility-item{display:inline-flex;align-items:center;gap:6px;color:rgba(250,250,250,0.7)}
.fx-utility-item strong{color:var(--brand-yellow);font-weight:700}
.fx-theme-toggle{display:inline-flex;align-items:center;gap:6px;background:rgba(250,250,250,0.1);border:1px solid rgba(250,250,250,0.2);color:#FAFAFA;padding:4px 10px;font:600 10px/1 'JetBrains Mono',monospace;text-transform:uppercase;border-radius:999px;cursor:pointer}
.fx-mode-indicator{font:500 9px/1 'JetBrains Mono',monospace;color:rgba(250,250,250,0.5)}
.fx-mode-indicator::before{content:'●';color:var(--brand-yellow);margin-right:4px}
/* .fx-theme-toggle base rule REMOVED — opacity 0.1 background made button invisible.
See consolidated rule below with proper contrast for both light/dark modes. */
.fx-marquee{background:var(--paper-2);border-bottom:1.5px solid var(--ink);overflow:hidden;font:700 11px/1 'JetBrains Mono',monospace;padding:6px 0}
.fx-marquee-track{display:flex;gap:32px;white-space:nowrap;animation:marquee 40s linear infinite}
.fx-marquee-track span{color:var(--text-dim)}
@@ -513,9 +514,14 @@ img{max-width:100%;display:block}
--faq-item-bg: rgba(255,255,255,0.7);
/* soft yellow glow for hero side (3D feel) */
--soft-yellow-glow: 6px 6px 0 rgba(255,214,10,0.3);
/* Body aliases (mirror global.css --color-white/black for theme parity) */
--body-bg: #FFFFFF;
--body-fg: #0A0A0A;
--card-bg: #FFFFFF;
--input-bg: #FFFFFF;
}
[data-theme="dark"] {
html.dark {
--ink: #FAFAFA;
--ink-2: #F0F0F0;
--paper: #0A0A0A;
@@ -536,22 +542,66 @@ img{max-width:100%;display:block}
--hero-content-bg: rgba(26,26,26,0.7);
--faq-item-bg: rgba(26,26,26,0.6);
--soft-yellow-glow: 6px 6px 0 rgba(255,214,10,0.2);
/* Body aliases (inverted) */
--body-bg: #0A0A0A;
--body-fg: #FAFAFA;
--card-bg: #1A1A1A;
--input-bg: #1A1A1A;
/* Coral stays vibrant in dark mode (it's already high-contrast) */
/* Yellow stays vibrant (high luminance) */
}
/* ============================================
BODY / HTML THEME OVERRIDE (added 2026-06-13)
global.css sets body { color:var(--color-black); background:var(--color-white); }
which keeps body white in dark mode → text from v7-5 sections
(which use --ink = cream) becomes invisible on white body.
Override body + html here using --body-bg/--body-fg aliases.
============================================ */
html { background: var(--body-bg); color: var(--body-fg); }
body { background: var(--body-bg); color: var(--body-fg); }
/* Utility bar: use --utility-* vars instead of hardcoded colors */
.fx-utility-bar{background:var(--utility-bg);color:var(--utility-fg)}
.fx-utility-item{color:var(--utility-fg-dim)}
.fx-utility-item strong{color:var(--brand-yellow)}
.fx-theme-toggle{background:rgba(0,0,0,0.08);border:1px solid rgba(0,0,0,0.15);color:var(--ink)}
[data-theme="dark"] .fx-theme-toggle{background:rgba(250,250,250,0.08);border:1px solid rgba(250,250,250,0.18);color:var(--ink)}
/* Theme toggle override REMOVED — see consolidated rule below */
.fx-mode-indicator{color:var(--utility-fg-dimmer)}
[data-theme="dark"] .fx-mode-indicator{color:var(--utility-fg-dimmer)}
html.dark .fx-mode-indicator{color:var(--utility-fg-dimmer)}
/* Theme toggle button itself */
.fx-theme-toggle{cursor:pointer;user-select:none}
.fx-theme-toggle:hover{transform:scale(1.05);background:var(--brand-yellow);color:var(--ink)}
/* Theme toggle button — VISIBLE in both modes (not hidden)
Earlier rules used rgba(0.08-0.1) backgrounds which were nearly
invisible. User reported: "It is hidden and show when hover only." */
.fx-theme-toggle{
display:inline-flex;
align-items:center;
gap:6px;
/* Light mode utility bar is dark (--utility-bg) — use light fill */
background:rgba(255,255,255,0.18);
border:1.5px solid rgba(255,255,255,0.35);
color:#FAFAFA;
padding:4px 10px;
font:600 10px/1 'JetBrains Mono',monospace;
text-transform:uppercase;
border-radius:999px;
cursor:pointer;
user-select:none;
transition:all 0.15s ease;
font-weight:700;
letter-spacing:0.3px;
}
/* Dark mode utility bar is light — use dark fill for contrast */
html.dark .fx-theme-toggle{
background:rgba(10,10,10,0.85);
border:1.5px solid rgba(10,10,10,0.5);
color:#FAFAFA;
}
.fx-theme-toggle:hover{
background:var(--brand-yellow);
color:var(--ink);
border-color:var(--ink);
transform:scale(1.05);
}
/* Nav: use --nav-bg for theme-aware glass */
.fx-nav{background:var(--nav-bg)}
@@ -564,16 +614,32 @@ img{max-width:100%;display:block}
.fx-faq-item{background:var(--faq-item-bg)}
/* Specific dark mode tweaks for elements that need extra contrast */
[data-theme="dark"] .fx-nav-dropdown{background:var(--paper);border-color:var(--ink)}
[data-theme="dark"] .fx-pricing-card.featured{background:rgba(255,214,10,0.15)}
[data-theme="dark"] .fx-callout{background:linear-gradient(180deg,#FFE600,var(--brand-yellow))}
[data-theme="dark"] .fx-portfolio-card.featured{background:var(--coral);color:#FAFAFA}
[data-theme="dark"] .fx-portfolio-name{background:var(--ink);color:var(--paper)}
[data-theme="dark"] .fx-portfolio-card .fx-portfolio-name{color:#FAFAFA;background:var(--ink)}
[data-theme="dark"] .fx-portfolio-card.featured .fx-portfolio-name{color:var(--brand-yellow);background:var(--ink)}
[data-theme="dark"] .fx-process-num{background:var(--brand-yellow);color:var(--ink)}
[data-theme="dark"] .fx-service-num{background:var(--brand-yellow);color:var(--ink)}
[data-theme="dark"] .fx-prose{color:var(--ink)}
html.dark body { background: var(--body-bg); color: var(--body-fg); }
/* v7-5 uses rgba(10,10,10,0.3) for dim text (.ts in log + footer-bottom)
Invert to cream-equivalent for dark mode */
html.dark .ts { color: rgba(250,250,250,0.3); }
html.dark .fx-footer-bottom { color: rgba(250,250,250,0.3); }
/* Coral backgrounds keep their cream text — already high contrast.
No override needed for .fx-btn.coral, .fx-nav-cta, .fx-pricing-cta,
.fx-service-num, .fx-portfolio-card.featured (all have dark bg + cream text) */
/* Mode indicator: in dark mode, utility bar is light, so indicator should be dark */
html.dark .fx-mode-indicator { color: rgba(10,10,10,0.5); }
/* Theme toggle override is now consolidated in the .fx-theme-toggle rule
below (removed the duplicate low-contrast rules) */
html.dark .fx-nav-dropdown{background:var(--paper);border-color:var(--ink)}
html.dark .fx-pricing-card.featured{background:rgba(255,214,10,0.15)}
html.dark .fx-callout{background:linear-gradient(180deg,#FFE600,var(--brand-yellow))}
html.dark .fx-portfolio-card.featured{background:var(--coral);color:#FAFAFA}
html.dark .fx-portfolio-name{background:var(--ink);color:var(--paper)}
html.dark .fx-portfolio-card .fx-portfolio-name{color:#FAFAFA;background:var(--ink)}
html.dark .fx-portfolio-card.featured .fx-portfolio-name{color:var(--brand-yellow);background:var(--ink)}
html.dark .fx-process-num{background:var(--brand-yellow);color:var(--ink)}
html.dark .fx-service-num{background:var(--brand-yellow);color:var(--ink)}
html.dark .fx-prose{color:var(--ink)}
/* Smooth theme transition (not too jarring) */
html { transition: background-color 0.3s ease, color 0.3s ease; }