From cd69fab17b15e41c1bdc9ae0fddc677a5ca48fc0 Mon Sep 17 00:00:00 2001 From: innochic Date: Tue, 24 Feb 2026 21:19:30 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(style):=20=E6=96=B0=E5=A2=9E=20light/d?= =?UTF-8?q?ark=20=E9=9B=99=E4=B8=BB=E9=A1=8C=20CSS=20token=20=E8=88=87?= =?UTF-8?q?=E5=B9=B3=E6=BB=91=E5=88=87=E6=8F=9B=E9=81=8E=E6=B8=A1=E5=8B=95?= =?UTF-8?q?=E7=95=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.css | 159 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 152 insertions(+), 7 deletions(-) diff --git a/src/index.css b/src/index.css index 507bc0c..0edaaa4 100644 --- a/src/index.css +++ b/src/index.css @@ -3,7 +3,9 @@ ======================================== */ /* --- CSS Custom Properties --- */ -:root { + +/* Dark Theme (Default) */ +[data-theme="dark"] { /* Colors */ --color-bg: #0a0e1a; --color-bg-elevated: #111827; @@ -31,6 +33,57 @@ --color-border: rgba(148, 163, 184, 0.12); --color-border-hover: rgba(148, 163, 184, 0.25); + /* Navbar specific */ + --color-navbar-bg: rgba(10, 14, 26, 0.85); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4); + --shadow-glow: 0 0 40px var(--color-primary-glow); +} + +/* Light Theme */ +[data-theme="light"] { + /* Colors */ + --color-bg: #f8faff; + --color-bg-elevated: #ffffff; + --color-bg-card: #ffffff; + --color-bg-card-hover: #f0f4ff; + --color-surface: #e8edfa; + + --color-primary: #4f46e5; + --color-primary-light: #6366f1; + --color-primary-dark: #3730a3; + --color-primary-glow: rgba(79, 70, 229, 0.15); + + --color-accent: #0891b2; + --color-accent-light: #06b6d4; + + --color-success: #059669; + --color-warning: #d97706; + --color-error: #dc2626; + + --color-text: #0f172a; + --color-text-secondary: #475569; + --color-text-muted: #94a3b8; + --color-text-inverse: #f1f5f9; + + --color-border: rgba(15, 23, 42, 0.08); + --color-border-hover: rgba(15, 23, 42, 0.18); + + /* Navbar specific */ + --color-navbar-bg: rgba(248, 250, 255, 0.9); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.12); + --shadow-glow: 0 0 40px var(--color-primary-glow); +} + +:root { + /* Typography */ --font-sans: 'Inter', 'Noto Sans TC', system-ui, -apple-system, sans-serif; --font-mono: 'JetBrains Mono', ui-monospace, monospace; @@ -68,11 +121,7 @@ --radius-2xl: 1.5rem; --radius-full: 9999px; - /* Shadows */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3); - --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4); - --shadow-glow: 0 0 40px var(--color-primary-glow); + /* Note: Shadows are defined per theme above */ /* Transitions */ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); @@ -106,6 +155,8 @@ body { background: var(--color-bg); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + /* Theme transition — spec: 300ms smooth */ + transition: background-color 0.3s ease, color 0.3s ease; } img { @@ -256,9 +307,10 @@ button { right: 0; height: var(--navbar-height); z-index: 1000; - background: rgba(10, 14, 26, 0.8); + background: var(--color-navbar-bg); backdrop-filter: blur(16px); border-bottom: 1px solid var(--color-border); + transition: background-color 0.3s ease, border-color 0.3s ease; } .navbar__inner { @@ -1243,3 +1295,96 @@ button { grid-template-columns: 1fr; } } + +/* ======================================== + THEME TOGGLE BUTTON + ======================================== */ +.theme-toggle { + position: relative; + display: inline-flex; + align-items: center; + width: 52px; + height: 28px; + border-radius: var(--radius-full); + background: var(--color-surface); + border: 1.5px solid var(--color-border-hover); + cursor: pointer; + transition: background-color 0.3s ease, border-color 0.3s ease; + flex-shrink: 0; + padding: 0; +} + +.theme-toggle:hover { + border-color: var(--color-primary-light); + background: var(--color-bg-card-hover); +} + +.theme-toggle:focus-visible { + outline: 2px solid var(--color-primary-light); + outline-offset: 2px; +} + +/* The sliding thumb */ +.theme-toggle__thumb { + position: absolute; + left: 3px; + width: 20px; + height: 20px; + border-radius: 50%; + background: linear-gradient(135deg, var(--color-primary-light), var(--color-primary)); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), + background 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + line-height: 1; +} + +/* Slide to right when light theme active */ +[data-theme="light"] .theme-toggle__thumb { + transform: translateX(24px); + background: linear-gradient(135deg, #f59e0b, #fbbf24); +} + +/* Icon inside the thumb */ +.theme-toggle__icon { + display: block; + transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.25s ease; + transform-origin: center; +} + +.theme-toggle__icon--moon { + opacity: 1; + transform: rotate(0deg) scale(1); +} + +.theme-toggle__icon--sun { + display: none; +} + +[data-theme="light"] .theme-toggle__icon--moon { + display: none; +} + +[data-theme="light"] .theme-toggle__icon--sun { + display: block; + opacity: 1; + transform: rotate(180deg) scale(1); +} + +/* Track background glow */ +.theme-toggle::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + background: linear-gradient(90deg, var(--color-primary-glow), transparent); + opacity: 0; + transition: opacity 0.3s ease; +} + +.theme-toggle:hover::before { + opacity: 1; +} From cde3a4948bae70c4ebb69d4e84f19cb093a58068 Mon Sep 17 00:00:00 2001 From: innochic Date: Tue, 24 Feb 2026 21:20:23 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(theme-toggle):=20=E6=96=B0=E5=A2=9E=20?= =?UTF-8?q?useTheme=20Hook=20=E8=88=87=20ThemeToggle=20=E5=85=83=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ThemeToggle.jsx | 31 +++++++++++++++++++++++++ src/hooks/useTheme.js | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/components/ThemeToggle.jsx create mode 100644 src/hooks/useTheme.js diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..12f523f --- /dev/null +++ b/src/components/ThemeToggle.jsx @@ -0,0 +1,31 @@ +/** + * ThemeToggle — 日/月圖示動畫切換按鈕 + * 接收 theme ('dark'|'light') 與 toggleTheme callback + */ +function ThemeToggle({ theme, toggleTheme }) { + const isDark = theme === 'dark'; + + return ( + + ); +} + +export default ThemeToggle; diff --git a/src/hooks/useTheme.js b/src/hooks/useTheme.js new file mode 100644 index 0000000..ccc8eda --- /dev/null +++ b/src/hooks/useTheme.js @@ -0,0 +1,41 @@ +import { useState, useEffect } from 'react'; + +const STORAGE_KEY = 'sp-theme'; + +/** + * 讀取初始主題,優先順序: + * 1. localStorage 中的使用者偏好 + * 2. OS 系統層級 prefers-color-scheme + * 3. 預設 'dark' + */ +function getInitialTheme() { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === 'light' || stored === 'dark') { + return stored; + } + if (window.matchMedia('(prefers-color-scheme: light)').matches) { + return 'light'; + } + return 'dark'; +} + +/** + * useTheme Hook + * 管理主題狀態,同步至 與 localStorage + */ +export function useTheme() { + const [theme, setTheme] = useState(() => getInitialTheme()); + + // 當 theme 改變時,同步至 html attribute 與 localStorage + useEffect(() => { + const root = document.documentElement; + root.setAttribute('data-theme', theme); + localStorage.setItem(STORAGE_KEY, theme); + }, [theme]); + + function toggleTheme() { + setTheme((prev) => (prev === 'dark' ? 'light' : 'dark')); + } + + return { theme, toggleTheme }; +} From 4c24808f3084ebd37d1d8acb1c02e9d4b5916249 Mon Sep 17 00:00:00 2001 From: innochic Date: Tue, 24 Feb 2026 21:20:42 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(navbar):=20=E6=95=B4=E5=90=88=E4=B8=BB?= =?UTF-8?q?=E9=A1=8C=E5=88=87=E6=8F=9B=E6=8C=89=E9=88=95=E8=87=B3=20Navbar?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=20App=20=E4=B8=BB=E9=A1=8C=E7=8B=80?= =?UTF-8?q?=E6=85=8B=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 5 ++++- src/components/Navbar.jsx | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 3a7c65b..2436993 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,3 +1,4 @@ +import { useTheme } from './hooks/useTheme'; import Navbar from './components/Navbar'; import Hero from './components/Hero'; import SocialProof from './components/SocialProof'; @@ -8,9 +9,11 @@ import CallToAction from './components/CallToAction'; import Footer from './components/Footer'; function App() { + const { theme, toggleTheme } = useTheme(); + return (
- +
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 5e205a8..88d19a3 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,7 +1,8 @@ import { useState } from 'react'; import { NAV_LINKS, BRAND } from '../data/navigation'; +import ThemeToggle from './ThemeToggle'; -function Navbar() { +function Navbar({ theme, toggleTheme }) { const [menuOpen, setMenuOpen] = useState(false); return ( @@ -39,6 +40,10 @@ function Navbar() { ))} + + {/* Theme Toggle Button */} + + 預約 Demo