fix(theme+marquee): restore marquee scroll + add light/dark mode toggle

User reported 2 issues after Phase 6.2:
1. 'marquee ควรต้องเลื่อนด้วย' — ROOT CAUSE: v7-5 source CSS included
   override at end of <style> block:
     .fx-marquee-track{animation:none}
     .fx-faq-a::after{display:none}
     .fx-hero::after{display:none}
   These were 'no-op' overrides from the mockup library (which doesn't
   actually animate things in showcase mode). Copied verbatim when I
   extracted fx-system.css in Phase 1.1, killing marquee + 2 other
   animations.
   FIX: replaced with the real animations (marquee 40s, blink-cursor,
   hero noise overlay). All 3 now actually run.

2. 'เราต้องการ light and dark mode ด้วย โดยมีปุ่มเปลี่ยน mode ได้' —
   Implemented full light/dark theme system:
   - Added [data-theme='dark'] block in fx-system.css overriding 11
     CSS tokens (--ink, --paper, --line, --text-dim, + 5 new
     --utility-bg/--nav-bg/--hero-content-bg/etc.)
   - Refactored .fx-utility-bar to use --utility-bg/--fg vars instead
     of hardcoded #0A0A0A (so it inverts correctly in dark mode)
   - Refactored .fx-nav, .fx-hero-content, .fx-hero-side, .fx-faq-item
     to use theme-aware vars
   - Added 13 [data-theme='dark'] overrides for elements needing
     extra contrast tweaks (pricing featured, callout, portfolio name,
     process/service numbers, prose)
   - Added smooth 0.3s transition on theme change (no jarring swap)

3. Theme toggle button (UtilityBar.astro):
   - Replaced text-only indicator with <button id='fx-theme-toggle'>
   - 3 modes cycle: auto (follow OS) → light → dark → auto
   - Persists to localStorage 'moreminimore-theme'
   - Default = 'auto' (follows system preference)
   - Button label changes: '◐ auto' / '☀ light' / '☾ dark'
   - Mode indicator shows user's chosen mode
   - Listens to OS preference change live (when in 'auto' mode)
   - ARIA label + title for accessibility

4. Anti-flash inline script in Base.astro <head>:
   - Runs synchronously before first paint
   - Reads localStorage → applies data-theme
   - If 'auto' or unset, follows prefers-color-scheme
   - Prevents white→dark flash on first load

Build: 22 pages, 0 errors, 2.11s.
CSS: 545/545 braces, 9 keyframes, 13 dark-mode selectors.
This commit is contained in:
Kunthawat Greethong
2026-06-13 19:41:54 +07:00
parent 5393cf611c
commit 96caca4af6
3 changed files with 197 additions and 6 deletions

View File

@@ -3,10 +3,11 @@
* MOREMINIMORE - UtilityBar (from v6-utility)
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 571-589
*
* Top info bar — phone + clock + date + mode indicator + email
* Top info bar — phone + clock + date + mode indicator + email + THEME TOGGLE
* Phone/email pulled from src/content/settings/site.md (single source of truth)
*
* Clock/date are updated by fx-animations.ts → fxClock() (id="fx-time", id="fx-date")
* Theme toggle: id="fx-theme-toggle" — click flips data-theme on <html> + saves to localStorage
*/
import { getEntry } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
@@ -23,8 +24,76 @@ const email = site?.data?.email ?? 'contact@moreminimore.com';
<span class="fx-utility-item" id="fx-date">📅 — — —</span>
</div>
<div class="fx-utility-bar-right">
<span class="fx-mode-indicator">system</span>
<span class="fx-theme-toggle" id="fx-theme-toggle">◐ auto</span>
<span class="fx-mode-indicator" id="fx-mode-indicator">light</span>
<button
type="button"
class="fx-theme-toggle"
id="fx-theme-toggle"
aria-label="Toggle light/dark mode"
title="Toggle light/dark mode"
>
◐ auto
</button>
<a href={`mailto:${email}`} class="fx-utility-item" style="text-decoration:none">✉ {email}</a>
</div>
</div>
<script>
/**
* Theme toggle logic — runs on every page (mounted via UtilityBar.astro).
* States: 'light' | 'dark' | 'auto' (follows system)
* Persists to localStorage; default = auto (follow OS preference)
*/
type ThemeMode = 'light' | 'dark' | 'auto';
const STORAGE_KEY = 'moreminimore-theme';
function getStoredTheme(): ThemeMode {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark' || stored === 'auto') return stored;
} catch (_) { /* localStorage blocked */ }
return 'auto';
}
function effectiveTheme(mode: ThemeMode): 'light' | 'dark' {
if (mode === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return mode;
}
function applyTheme(mode: ThemeMode) {
const eff = effectiveTheme(mode);
document.documentElement.setAttribute('data-theme', eff);
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)
if (btn) {
btn.textContent = mode === 'auto' ? '◐ auto' : mode === 'light' ? '☀ light' : '☾ dark';
}
}
function cycleTheme() {
const current = getStoredTheme();
const next: ThemeMode = current === 'light' ? 'dark' : current === 'dark' ? 'auto' : 'light';
try { localStorage.setItem(STORAGE_KEY, next); } catch (_) { /* ignore */ }
applyTheme(next);
}
// Initialize on load
applyTheme(getStoredTheme());
// Wire button (idempotent — fires on every page navigation)
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('fx-theme-toggle');
if (btn && !btn.dataset.bound) {
btn.dataset.bound = 'true';
btn.addEventListener('click', cycleTheme);
}
});
// If user hasn't picked explicitly, follow OS changes live
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if (getStoredTheme() === 'auto') applyTheme('auto');
});
</script>