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
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 };
+}
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;
+}