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:
@@ -3,10 +3,11 @@
|
|||||||
* MOREMINIMORE - UtilityBar (from v6-utility)
|
* MOREMINIMORE - UtilityBar (from v6-utility)
|
||||||
* Extracted from Desktop/moreminomore-mockup-v7-5.html lines 571-589
|
* 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)
|
* 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")
|
* 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 { getEntry } from 'astro:content';
|
||||||
import type { CollectionEntry } 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>
|
<span class="fx-utility-item" id="fx-date">📅 — — —</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="fx-utility-bar-right">
|
<div class="fx-utility-bar-right">
|
||||||
<span class="fx-mode-indicator">system</span>
|
<span class="fx-mode-indicator" id="fx-mode-indicator">light</span>
|
||||||
<span class="fx-theme-toggle" id="fx-theme-toggle">◐ auto</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>
|
<a href={`mailto:${email}`} class="fx-utility-item" style="text-decoration:none">✉ {email}</a>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|||||||
@@ -37,6 +37,29 @@ const {
|
|||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
|
||||||
|
<!-- Anti-flash theme: apply data-theme 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. -->
|
||||||
|
<script is:inline>
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var stored = localStorage.getItem('moreminimore-theme');
|
||||||
|
var theme;
|
||||||
|
if (stored === 'light' || stored === 'dark') {
|
||||||
|
theme = stored;
|
||||||
|
} else {
|
||||||
|
// 'auto' or unset: follow system
|
||||||
|
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage blocked or no matchMedia — default light
|
||||||
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<UtilityBar />
|
<UtilityBar />
|
||||||
|
|||||||
@@ -480,9 +480,108 @@ img{max-width:100%;display:block}
|
|||||||
.fx-contact-form:hover{box-shadow:6px 6px 0 var(--coral),0 12px 32px rgba(10,10,10,0.12);transform:translate(-2px,-2px)}
|
.fx-contact-form:hover{box-shadow:6px 6px 0 var(--coral),0 12px 32px rgba(10,10,10,0.12);transform:translate(-2px,-2px)}
|
||||||
.fx-btn.coral{background:linear-gradient(135deg,var(--coral) 0%,#E64A2C 100%)}
|
.fx-btn.coral{background:linear-gradient(135deg,var(--coral) 0%,#E64A2C 100%)}
|
||||||
.fx-btn.coral:hover{background:linear-gradient(135deg,var(--ink) 0%,var(--coral) 100%)}
|
.fx-btn.coral:hover{background:linear-gradient(135deg,var(--ink) 0%,var(--coral) 100%)}
|
||||||
.fx-marquee-track{animation:none}
|
.fx-marquee-track{animation:marquee 40s linear infinite}
|
||||||
.fx-faq-a::after{display:none}
|
.fx-faq-a::after{content:"█";display:inline-block;margin-left:4px;color:var(--coral);font-family:'JetBrains Mono',monospace;animation:blink-cursor 1s steps(1) infinite}
|
||||||
.fx-hero::after{display:none}
|
.fx-hero::after{content:"";position:absolute;inset:0;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.4'/%3E%3C/svg%3E");opacity:0.05;pointer-events:none;z-index:1;mix-blend-mode:multiply}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
DARK MODE OVERRIDE (added 2026-06-13 evening)
|
||||||
|
Per user spec: light + dark mode with toggle.
|
||||||
|
Strategy: override CSS tokens, then fix
|
||||||
|
.fx-utility-bar which had hardcoded dark colors
|
||||||
|
(background:#0A0A0A) — must use --ink/--paper vars.
|
||||||
|
============================================ */
|
||||||
|
:root {
|
||||||
|
--brand-yellow: #FFD60A;
|
||||||
|
--coral: #FF5A3C;
|
||||||
|
--ink: #0A0A0A;
|
||||||
|
--ink-2: #1A1A1A;
|
||||||
|
--paper: #FAFAFA;
|
||||||
|
--paper-2: #F5F5F5;
|
||||||
|
--paper-3: #F0F0F0;
|
||||||
|
--line: rgba(10,10,10,0.1);
|
||||||
|
--line-2: rgba(10,10,10,0.2);
|
||||||
|
--text-dim: rgba(10,10,10,0.6);
|
||||||
|
--text-dimmer: rgba(10,10,10,0.4);
|
||||||
|
/* Surface aliases used by utility bar / nav (were hardcoded) */
|
||||||
|
--utility-bg: #0A0A0A;
|
||||||
|
--utility-fg: rgba(250,250,250,0.85);
|
||||||
|
--utility-fg-dim: rgba(250,250,250,0.7);
|
||||||
|
--utility-fg-dimmer: rgba(250,250,250,0.5);
|
||||||
|
--nav-bg: rgba(250,250,250,0.78);
|
||||||
|
--hero-content-bg: rgba(255,255,255,0.6);
|
||||||
|
--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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--ink: #FAFAFA;
|
||||||
|
--ink-2: #F0F0F0;
|
||||||
|
--paper: #0A0A0A;
|
||||||
|
--paper-2: #1A1A1A;
|
||||||
|
--paper-3: #2A2A2A;
|
||||||
|
--line: rgba(250,250,250,0.12);
|
||||||
|
--line-2: rgba(250,250,250,0.22);
|
||||||
|
--text-dim: rgba(250,250,250,0.65);
|
||||||
|
--text-dimmer: rgba(250,250,250,0.4);
|
||||||
|
/* In dark mode, utility bar inverts: light bar on dark bg */
|
||||||
|
--utility-bg: #FAFAFA;
|
||||||
|
--utility-fg: rgba(10,10,10,0.85);
|
||||||
|
--utility-fg-dim: rgba(10,10,10,0.7);
|
||||||
|
--utility-fg-dimmer: rgba(10,10,10,0.5);
|
||||||
|
/* Nav: dark glass */
|
||||||
|
--nav-bg: rgba(10,10,10,0.85);
|
||||||
|
/* Hero content: dark glass */
|
||||||
|
--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);
|
||||||
|
/* Coral stays vibrant in dark mode (it's already high-contrast) */
|
||||||
|
/* Yellow stays vibrant (high luminance) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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)}
|
||||||
|
.fx-mode-indicator{color:var(--utility-fg-dimmer)}
|
||||||
|
[data-theme="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)}
|
||||||
|
|
||||||
|
/* Nav: use --nav-bg for theme-aware glass */
|
||||||
|
.fx-nav{background:var(--nav-bg)}
|
||||||
|
|
||||||
|
/* Hero content: use --hero-content-bg for theme-aware glass */
|
||||||
|
.fx-hero-content{background:var(--hero-content-bg)}
|
||||||
|
|
||||||
|
/* Hero side glow + FAQ item bg: theme-aware */
|
||||||
|
.fx-hero-side{box-shadow:var(--soft-yellow-glow)}
|
||||||
|
.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)}
|
||||||
|
|
||||||
|
/* Smooth theme transition (not too jarring) */
|
||||||
|
html { transition: background-color 0.3s ease, color 0.3s ease; }
|
||||||
|
body, .fx-utility-bar, .fx-nav, .fx-hero-content, .fx-case, .fx-services, .fx-callout,
|
||||||
|
.fx-portfolio, .fx-process, .fx-pricing, .fx-faq, .fx-contact, .fx-footer, .fx-section-header,
|
||||||
|
.fx-service-card, .fx-portfolio-card, .fx-pricing-card, .fx-faq-item, .fx-prose {
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
NAV DROPDOWN ENHANCEMENT (added 2026-06-13, plan round 2)
|
NAV DROPDOWN ENHANCEMENT (added 2026-06-13, plan round 2)
|
||||||
|
|||||||
Reference in New Issue
Block a user