From 482096250aa07ac9443de64222b2a88410bf98c2 Mon Sep 17 00:00:00 2001 From: ovais koite Date: Thu, 20 Nov 2025 16:00:27 +0530 Subject: [PATCH 1/3] feat(landing): add light/dark theme toggle; persist choice in localStorage --- src/components/Navbar.jsx | 69 ++++++++++++++++++--- src/index.css | 70 +++++++++++++++++++++ src/pages/THEME_TOGGLE.md | 9 +++ src/theme.js | 124 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 src/pages/THEME_TOGGLE.md create mode 100644 src/theme.js diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 8ef775d07..cd9505e58 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,12 +1,31 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Link } from "react-router-dom"; import logo from "../assets/logo_light_160.png"; import { SideSheet } from "@douyinfe/semi-ui"; import { IconMenu } from "@douyinfe/semi-icons"; import { socials } from "../data/socials"; +import { getTheme, toggleTheme, onSystemPrefChange } from "../theme"; export default function Navbar() { const [openMenu, setOpenMenu] = useState(false); + const [isDark, setIsDark] = useState(() => getTheme() === "dark"); + + useEffect(() => { + // Keep local state synced when system preference changes (only when user hasn't chosen a theme) + const unsubscribe = onSystemPrefChange((prefersDark) => { + // if there's an explicit stored preference, ignore system changes + try { + const stored = window.localStorage && window.localStorage.getItem("drawdb:theme"); + if (!stored) { + setIsDark(prefersDark); + } + } catch (e) { + // ignore storage errors + } + }); + + return () => unsubscribe && unsubscribe(); + }, []); return ( <> @@ -75,12 +94,48 @@ export default function Navbar() { - +
+ + + +

system > default dark +export function getTheme() { + const stored = getStoredTheme(); + if (stored === "dark" || stored === "light") return stored; + const sys = isSystemDark() ? "dark" : "light"; + return sys || "dark"; +} + +export function applyTheme(theme) { + applyThemeClass(theme); +} + +// toggle and persist +export function toggleTheme() { + const current = getTheme(); + const next = current === "dark" ? "light" : "dark"; + try { + setStoredTheme(next); + } catch (e) { + // ignore + } + applyThemeClass(next); + return next; +} + +// Initialize on page load: apply stored or system preference, defaulting to dark +export function initTheme() { + const stored = getStoredTheme(); + const theme = stored === "dark" || stored === "light" ? stored : (isSystemDark() ? "dark" : "light"); + applyThemeClass(theme); +} + +// Listen to system preference changes and call callback(prefersDark) +// Returns an unsubscribe function +export function onSystemPrefChange(cb) { + try { + if (typeof window === "undefined" || !window.matchMedia) return () => {}; + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const handler = (ev) => cb(!!ev.matches); + // modern API + if (mq.addEventListener) mq.addEventListener("change", handler); + else mq.addListener && mq.addListener(handler); + return () => { + if (mq.removeEventListener) mq.removeEventListener("change", handler); + else mq.removeListener && mq.removeListener(handler); + }; + } catch (e) { + return () => {}; + } +} + +// Auto-init if running in browser +if (typeof window !== "undefined") { + try { + initTheme(); + } catch (e) { + // ignore + } +} + +export default { + getTheme, + applyTheme, + toggleTheme, + initTheme, + onSystemPrefChange, +}; From 93fa815f5c6c5a33066d6b35d25aaac8dfca2e7e Mon Sep 17 00:00:00 2001 From: ovais koite Date: Wed, 26 Nov 2025 14:58:05 +0530 Subject: [PATCH 2/3] feat(landing): initial theme bootstrap + themeManager + toggle Ensure the landing page resolves its color scheme before React renders to prevent FOUC, add a reusable theme manager that persists under 'drawdb:theme', and wire up an accessible sun/moon toggle in the header. --- index.html | 33 ++++- src/components/ThemeToggle.jsx | 76 +++++++++++ src/hooks/useThemePreference.js | 17 +++ src/index.css | 226 ++++++++++++++++++++++++++------ src/main.jsx | 3 + src/themeManager.js | 138 +++++++++++++++++++ 6 files changed, 451 insertions(+), 42 deletions(-) create mode 100644 src/components/ThemeToggle.jsx create mode 100644 src/hooks/useThemePreference.js create mode 100644 src/themeManager.js diff --git a/index.html b/index.html index 793799c3c..abce1644a 100644 --- a/index.html +++ b/index.html @@ -51,8 +51,39 @@ /> drawDB | Online database diagram editor and SQL generator + + - +
diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx new file mode 100644 index 000000000..fb0887fa2 --- /dev/null +++ b/src/components/ThemeToggle.jsx @@ -0,0 +1,76 @@ +import { useCallback, useEffect, useState } from "react"; +import { getTheme, subscribe, toggleTheme } from "../themeManager"; + +export default function ThemeToggle({ className = "" }) { + const [theme, setThemeState] = useState(() => getTheme()); + + useEffect(() => { + const unsubscribe = subscribe(setThemeState); + return unsubscribe; + }, []); + + const handleToggle = useCallback(() => { + toggleTheme(); + }, []); + + const handleKeyDown = useCallback( + (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + handleToggle(); + } + }, + [handleToggle], + ); + + const isDark = theme === "dark"; + + return ( + + ); +} + diff --git a/src/hooks/useThemePreference.js b/src/hooks/useThemePreference.js new file mode 100644 index 000000000..1e5c22199 --- /dev/null +++ b/src/hooks/useThemePreference.js @@ -0,0 +1,17 @@ +import { useEffect, useState } from "react"; +import { getTheme, subscribe } from "../themeManager"; + +/** + * React hook that mirrors the global landing-page theme. + */ +export default function useThemePreference() { + const [theme, setTheme] = useState(() => getTheme()); + + useEffect(() => { + const unsubscribe = subscribe(setTheme); + return unsubscribe; + }, []); + + return theme; +} + diff --git a/src/index.css b/src/index.css index 52f068b60..db59509bc 100644 --- a/src/index.css +++ b/src/index.css @@ -6,9 +6,6 @@ The default border color has changed to `currentColor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. - - If we ever want to remove these styles, we need to add an explicit border - color utility to any element that depends on these defaults. */ @layer base { *, @@ -36,13 +33,34 @@ } } -/* Theme variables --------------------------------------------------------- */ :root { - /* default (light) values — may be overridden by .theme-dark */ - --bg: 255 255 255; - --text: 17 24 39; /* slate-900 */ - --muted: 107 114 128; /* slate-500 */ - --accent: 14 165 233; /* sky-400 */ + color-scheme: light; + --bg: #ffffff; + --text: #111827; + --muted: #6b7280; + --accent: #0ea5e9; + --surface: #f8fafc; + --border: #e2e8f0; + + --accent-rgb: 14 165 233; + --section-bg: 244 244 245; + --surface-bg: 248 250 252; + --card-bg: 255 255 255; + --card-muted: 248 250 252; + --border-soft: 226 232 240; + --border-strong: 203 213 225; + --chip-bg: 203 213 225; + --chip-text: 30 41 59; + --button-primary-bg: 12 74 110; + --button-primary-hover: 8 47 73; + --button-primary-text: 255 255 255; + --button-secondary-bg: 255 255 255; + --button-secondary-text: 17 24 39; + --button-secondary-border: 226 232 240; + --shadow-rgb: 15,23,42; + --landing-strip-from: #12495e; + --landing-strip-via: #4b5563; + --landing-strip-to: #12495e; --semi-grey-0: 255,255,255; --semi-grey-1: 249,250,251; --semi-grey-2: 241,245,249; @@ -51,43 +69,159 @@ --semi-grey-9: 30,41,59; --semi-blue-5: 14,165,233; --semi-blue-6: 2,132,199; - --semi-color-text-1: rgb(var(--text)); - --semi-color-bg-0: rgb(var(--bg)); - --semi-color-bg-2: rgb(var(--bg)); - --semi-color-bg-3: rgb(var(--bg)); -} - -/* Dark theme overrides */ -.theme-dark, -html.theme-dark { - --bg: 17 24 39; /* slate-900 */ - --text: 243 244 246; /* slate-50 */ - --muted: 148 163 184; /* slate-400 */ - --accent: 56 189 248; /* sky-400 bright */ - --semi-grey-0: 17,24,39; - --semi-grey-1: 20,23,29; - --semi-grey-2: 30,41,59; - --semi-grey-3: 39,47,61; - --semi-grey-4: 55,65,81; + --semi-color-text-1: var(--text); + --semi-color-bg-0: var(--bg); + --semi-color-bg-2: var(--bg); + --semi-color-bg-3: var(--bg); +} + +[data-theme="dark"] { + color-scheme: dark; + --bg: #0c1018; + --text: #e4eaf5; + --muted: #94a3b8; + --accent: #7dd3fc; + --surface: #121a26; + --border: #3b4252; + + --accent-rgb: 125 211 252; + --section-bg: 8 12 20; + --surface-bg: 14 19 27; + --card-bg: 18 26 38; + --card-muted: 22 30 44; + --border-soft: 51 65 85; + --border-strong: 71 85 105; + --chip-bg: 56 189 248; + --chip-text: 8 47 73; + --button-primary-bg: 56 189 248; + --button-primary-hover: 14 165 233; + --button-primary-text: 8 25 37; + --button-secondary-bg: 18 26 38; + --button-secondary-text: 229 233 240; + --button-secondary-border: 71 85 105; + --shadow-rgb: 0,0,0; + --landing-strip-from: #0f172a; + --landing-strip-via: #082f49; + --landing-strip-to: #0ea5e9; + --semi-grey-0: 30,30,30; + --semi-grey-1: 35,35,35; + --semi-grey-2: 45,45,45; + --semi-grey-3: 50,50,50; + --semi-grey-4: 70,70,70; --semi-grey-9: 243,244,246; --semi-blue-5: 56,189,248; --semi-blue-6: 14,165,233; - --semi-color-text-1: rgb(var(--text)); - --semi-color-bg-0: rgb(var(--bg)); - --semi-color-bg-2: rgb(var(--bg)); - --semi-color-bg-3: rgb(var(--bg)); -} - -/* Light theme explicit class (optional; root defaults to light) */ -.theme-light, -html.theme-light { - /* no-op; variables already set in :root for light */ + --semi-color-text-1: var(--text); + --semi-color-bg-0: var(--bg); + --semi-color-bg-2: var(--bg); + --semi-color-bg-3: var(--bg); } /* apply variables to body for layout */ body { - background-color: rgb(var(--bg)); - color: rgb(var(--text)); + /* This ensures the entire body uses the variable set above */ + background-color: var(--bg) !important; + color: var(--text); +} + +.landing-page { + background-color: var(--section-bg); + color: var(--text); +} + +.landing-surface { + background-color: rgb(var(--surface-bg)); +} + +.landing-section { + background-color: var(--section-bg); + color: inherit; +} + +.landing-card { + background-color: rgb(var(--card-bg)); + border: 1px solid rgb(var(--border-soft) / 0.45); + box-shadow: 0 25px 60px rgba(var(--shadow-rgb), 0.08); + transition: background-color 200ms ease, border-color 200ms ease, color 200ms ease, box-shadow 200ms ease; +} + +.landing-feature-card { + background-color: rgb(var(--card-bg)); + border: 1px solid rgb(var(--border-soft) / 0.4); + box-shadow: 0 18px 50px rgba(var(--shadow-rgb), 0.08); +} + +.landing-top-strip { + background-image: linear-gradient(90deg, var(--landing-strip-from), var(--landing-strip-via), var(--landing-strip-to)); +} + +.landing-chip { + display: inline-flex; + align-items: center; + padding: 0.1rem 0.75rem; + border-radius: 9999px; + font-size: 0.85rem; + font-weight: 600; + background-color: rgb(var(--chip-bg) / 0.35); + color: rgb(var(--chip-text)); +} + +.landing-button { + font-weight: 600; + transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease, box-shadow 180ms ease; +} + +.landing-button-primary { + background-color: rgb(var(--button-primary-bg)); + color: rgb(var(--button-primary-text)); + border: 1px solid transparent; +} + +.landing-button-primary:hover { + background-color: rgb(var(--button-primary-hover)); +} + +.landing-button-secondary { + background-color: rgb(var(--button-secondary-bg)); + color: rgb(var(--button-secondary-text)); + border: 1px solid rgb(var(--button-secondary-border)); + box-shadow: 0 12px 28px rgba(var(--shadow-rgb), 0.09); +} + +.landing-button-secondary:hover { + background-color: rgb(var(--button-secondary-bg) / 0.95); +} + +.landing-accent-text { + color: var(--accent); +} + +.landing-tweets { + border-radius: 1.25rem; + border: 1px solid rgb(var(--border-soft) / 0.5); + background-color: rgb(var(--card-muted)); + padding: 2rem; +} + +.hero-panel { + overflow: hidden; + background: radial-gradient(circle at top right, rgb(var(--accent-rgb) / 0.12), transparent 55%); +} + +.landing-divider { + border-color: rgb(var(--border-soft) / 0.35); +} + +.theme-toggle { + background-color: rgb(var(--card-bg) / 0.9); + color: var(--text); + border-color: rgb(var(--border-soft) / 0.5); +} + +[data-theme="dark"] .theme-toggle { + background-color: rgb(var(--card-bg)); + color: var(--text); + border-color: rgb(var(--border-soft) / 0.8); } /* Reduced motion: disable transitions */ @@ -225,13 +359,23 @@ body { border-color: rgba(var(--semi-grey-2), 1); } +/* FIX: Ensure bg-dots changes to dark background */ .bg-dots { - background-color: white; + /* Set default (light mode) */ + background-color: var(--bg); opacity: 0.8; - background-image: radial-gradient(rgb(118, 118, 209) 1px, white 1px); + background-image: radial-gradient(rgb(118, 118, 209) 1px, var(--bg) 1px); background-size: 20px 20px; } +[data-theme="dark"] .bg-dots { + background-color: var(--bg); /* Inherits dark background */ + opacity: 0.5; /* Slightly less opaque in dark mode */ + /* Change the white dots to a dark blue/cyan dot color */ + background-image: radial-gradient(rgb(14, 165, 233) 1px, var(--bg) 1px); +} + + .sliding-vertical span { animation: top-to-bottom 9s linear infinite 0s; -ms-animation: top-to-bottom 9s linear infinite 0s; diff --git a/src/main.jsx b/src/main.jsx index f09f4682b..d76accf48 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -6,6 +6,9 @@ import App from "./App.jsx"; import en_US from "@douyinfe/semi-ui/lib/es/locale/source/en_US"; import "./index.css"; import "./i18n/i18n.js"; +import { initTheme } from "./themeManager"; + +initTheme(); const root = ReactDOM.createRoot(document.getElementById("root")); root.render( diff --git a/src/themeManager.js b/src/themeManager.js new file mode 100644 index 000000000..9fa86aebf --- /dev/null +++ b/src/themeManager.js @@ -0,0 +1,138 @@ +const THEME_STORAGE_KEY = "drawdb:theme"; +const DARK_QUERY = "(prefers-color-scheme: dark)"; +const VALID_THEMES = new Set(["light", "dark"]); + +const subscribers = new Set(); +let currentTheme = null; +let mediaQueryList = null; +let hasExplicitPreference = false; +const isBrowser = typeof window !== "undefined"; + +function readStoredTheme() { + if (!isBrowser) return null; + try { + const value = window.localStorage.getItem(THEME_STORAGE_KEY); + return VALID_THEMES.has(value) ? value : null; + } catch { + return null; + } +} + +function writeStoredTheme(theme) { + if (!isBrowser) return; + try { + window.localStorage.setItem(THEME_STORAGE_KEY, theme); + } catch { + /* ignore storage errors */ + } +} + +function getSystemPreference() { + if (!isBrowser || typeof window.matchMedia !== "function") return null; + return window.matchMedia(DARK_QUERY).matches ? "dark" : "light"; +} + +function applyTheme(theme) { + if (typeof document === "undefined") return; + const root = document.documentElement; + root?.setAttribute("data-theme", theme); + root?.style?.setProperty("color-scheme", theme === "dark" ? "dark" : "light"); + document.body?.setAttribute("data-theme", theme); +} + +function notify(theme) { + subscribers.forEach((listener) => { + try { + listener(theme); + } catch (error) { + console.warn("[themeManager] subscriber error", error); + } + }); +} + +function handleSystemPreferenceChange(event) { + if (hasExplicitPreference) return; + setTheme(event.matches ? "dark" : "light", { persist: false }); +} + +function ensureMediaListener() { + if (!isBrowser || typeof window.matchMedia !== "function") return; + if (mediaQueryList) return; + mediaQueryList = window.matchMedia(DARK_QUERY); + mediaQueryList.addEventListener("change", handleSystemPreferenceChange); +} + +function resolveTheme() { + const stored = readStoredTheme(); + if (stored) { + hasExplicitPreference = true; + return stored; + } + const system = getSystemPreference(); + if (system) return system; + return "dark"; +} + +export function initTheme() { + if (currentTheme) return currentTheme; + currentTheme = resolveTheme(); + applyTheme(currentTheme); + ensureMediaListener(); + return currentTheme; +} + +export function getTheme() { + if (currentTheme) return currentTheme; + if (typeof document !== "undefined") { + const attr = document.documentElement.getAttribute("data-theme"); + if (VALID_THEMES.has(attr)) { + currentTheme = attr; + return currentTheme; + } + } + return initTheme(); +} + +export function setTheme(theme, options = {}) { + const next = VALID_THEMES.has(theme) ? theme : "dark"; + const shouldPersist = options.persist !== false; + + if (currentTheme === next) { + if (shouldPersist) { + writeStoredTheme(next); + hasExplicitPreference = true; + } + return next; + } + + currentTheme = next; + applyTheme(next); + + if (shouldPersist) { + writeStoredTheme(next); + hasExplicitPreference = true; + } else { + hasExplicitPreference = false; + } + + notify(next); + return next; +} + +export function toggleTheme() { + return setTheme(getTheme() === "dark" ? "light" : "dark"); +} + +export function subscribe(listener) { + if (typeof listener !== "function") return () => {}; + subscribers.add(listener); + return () => subscribers.delete(listener); +} + +// Backwards compatibility exports +export const initializeTheme = initTheme; +export const getCurrentTheme = getTheme; +export const subscribeToThemeChanges = subscribe; + +export { THEME_STORAGE_KEY }; + From d7164b96ff6ff4dcafbe7c2f8656138e6f92975b Mon Sep 17 00:00:00 2001 From: ovais koite Date: Wed, 26 Nov 2025 15:20:27 +0530 Subject: [PATCH 3/3] feat(landing): theme toggle implementation final --- src/components/EditorHeader/ControlPanel.jsx | 4 +- src/components/Navbar.jsx | 56 +-------- src/context/SettingsContext.jsx | 36 +++++- src/hooks/index.js | 1 + src/hooks/useThemedPage.js | 7 +- src/pages/LandingPage.jsx | 51 ++++---- src/pages/THEME_TOGGLE.md | 10 +- src/theme.js | 124 ------------------- 8 files changed, 72 insertions(+), 217 deletions(-) delete mode 100644 src/theme.js diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index cc1968d77..82fef9b01 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -1808,8 +1808,8 @@ export default function ControlPanel({ className="py-1 px-2 hover-2 rounded-sm text-xl -mt-0.5" onClick={() => { const body = document.body; - if (body.hasAttribute("theme-mode")) { - if (body.getAttribute("theme-mode") === "light") { + if (body.hasAttribute("data-theme")) { + if (body.getAttribute("data-theme") === "light") { menu["view"]["theme"].children[1].function(); } else { menu["view"]["theme"].children[0].function(); diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index cd9505e58..75eef949c 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,31 +1,13 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Link } from "react-router-dom"; import logo from "../assets/logo_light_160.png"; import { SideSheet } from "@douyinfe/semi-ui"; import { IconMenu } from "@douyinfe/semi-icons"; import { socials } from "../data/socials"; -import { getTheme, toggleTheme, onSystemPrefChange } from "../theme"; +import ThemeToggle from "./ThemeToggle"; export default function Navbar() { const [openMenu, setOpenMenu] = useState(false); - const [isDark, setIsDark] = useState(() => getTheme() === "dark"); - - useEffect(() => { - // Keep local state synced when system preference changes (only when user hasn't chosen a theme) - const unsubscribe = onSystemPrefChange((prefersDark) => { - // if there's an explicit stored preference, ignore system changes - try { - const stored = window.localStorage && window.localStorage.getItem("drawdb:theme"); - if (!stored) { - setIsDark(prefersDark); - } - } catch (e) { - // ignore storage errors - } - }); - - return () => unsubscribe && unsubscribe(); - }, []); return ( <> @@ -95,39 +77,7 @@ export default function Navbar() {
- - + Try it for yourself @@ -103,10 +104,10 @@ export default function LandingPage() { {/* Learn more */}
-
+
{/* Supported by */}
-
+
Supported by
@@ -127,7 +128,7 @@ export default function LandingPage() {
-
+
Build diagrams with a few clicks, see the full picture, export SQL scripts, customize your editor, and more. @@ -136,7 +137,7 @@ export default function LandingPage() {
-
+
{shortenNumber(stats.stars)}
@@ -144,7 +145,7 @@ export default function LandingPage() {
-
+
{shortenNumber(stats.forks)}
@@ -152,7 +153,7 @@ export default function LandingPage() {
-
+
{shortenNumber(languages.length)}
@@ -189,9 +190,9 @@ export default function LandingPage() {
{/* Features */} -
+
-
+
More than just an editor
@@ -201,7 +202,7 @@ export default function LandingPage() { {features.map((f, i) => (
@@ -216,13 +217,13 @@ export default function LandingPage() {
{/* Tweets */} -
+
What the internet says about us
@@ -244,7 +245,7 @@ export default function LandingPage() { fill="#f4f4f5" /> -
+
Reach out to us
@@ -299,7 +300,7 @@ export default function LandingPage() { Attention! The diagrams are saved in your browser. Before clearing the browser make sure to back up your data.
-
+
© {new Date().getFullYear()} drawDB - All rights reserved.
diff --git a/src/pages/THEME_TOGGLE.md b/src/pages/THEME_TOGGLE.md index d03721ac4..bd1f7e3a6 100644 --- a/src/pages/THEME_TOGGLE.md +++ b/src/pages/THEME_TOGGLE.md @@ -1,9 +1,11 @@ Theme toggle (light / dark) -------------------------------- -The landing page includes a theme toggle in the top-right header. It persists the user's choice in localStorage using the key `drawdb:theme` and falls back to the system preference; if neither is available the default is dark. +The landing page header exposes a sun/moon toggle that persists the user choice in `localStorage` (`drawdb:theme`). We default to dark unless a stored preference or system preference (via `prefers-color-scheme`) is available. To avoid flashes of incorrect colors, an inline script in `index.html` applies the resolved theme before React renders. Files: -- `src/theme.js` — theme helper (init, toggle, storage, system listener) -- `src/components/Navbar.jsx` — toggle button UI and ARIA attributes -- `src/index.css` — CSS variables and `.theme-dark` / `.theme-light` classes +- `index.html` — inline guard script that bootstraps the `data-theme` attribute prior to hydration. +- `src/themeManager.js` — theme helper (init, toggle, storage, matchMedia listener, subscribers). +- `src/components/ThemeToggle.jsx` — accessible switch UI (role="switch", keyboard support). +- `src/components/Navbar.jsx` — renders the toggle in the landing header. +- `src/index.css` — CSS variables (`:root` / `[data-theme='dark']`), landing-page specific surfaces, and animated transitions that respect `prefers-reduced-motion`. diff --git a/src/theme.js b/src/theme.js deleted file mode 100644 index 09cfacf82..000000000 --- a/src/theme.js +++ /dev/null @@ -1,124 +0,0 @@ -// Theme helper for landing page -// Responsibilities: -// - read/write `drawdb:theme` from localStorage (safely) -// - apply `.theme-dark` or `.theme-light` class to document element -// - provide toggle function -// - listen to system preference changes and notify - -const STORAGE_KEY = "drawdb:theme"; - -function safeGetStorage() { - try { - if (typeof window === "undefined" || !window.localStorage) return null; - return window.localStorage; - } catch (e) { - return null; - } -} - -function getStoredTheme() { - const s = safeGetStorage(); - try { - return s ? s.getItem(STORAGE_KEY) : null; - } catch (e) { - return null; - } -} - -function setStoredTheme(value) { - const s = safeGetStorage(); - try { - if (s) s.setItem(STORAGE_KEY, value); - } catch (e) { - // ignore storage errors (e.g., private mode) - } -} - -function isSystemDark() { - try { - if (typeof window === "undefined" || !window.matchMedia) return true; // fallback to dark - return window.matchMedia("(prefers-color-scheme: dark)").matches; - } catch (e) { - return true; - } -} - -function applyThemeClass(theme) { - try { - const root = document.documentElement || document.body; - if (!root) return; - root.classList.remove("theme-dark", "theme-light"); - if (theme === "dark") root.classList.add("theme-dark"); - else root.classList.add("theme-light"); - } catch (e) { - // no-op - } -} - -// Determine active theme: stored > system > default dark -export function getTheme() { - const stored = getStoredTheme(); - if (stored === "dark" || stored === "light") return stored; - const sys = isSystemDark() ? "dark" : "light"; - return sys || "dark"; -} - -export function applyTheme(theme) { - applyThemeClass(theme); -} - -// toggle and persist -export function toggleTheme() { - const current = getTheme(); - const next = current === "dark" ? "light" : "dark"; - try { - setStoredTheme(next); - } catch (e) { - // ignore - } - applyThemeClass(next); - return next; -} - -// Initialize on page load: apply stored or system preference, defaulting to dark -export function initTheme() { - const stored = getStoredTheme(); - const theme = stored === "dark" || stored === "light" ? stored : (isSystemDark() ? "dark" : "light"); - applyThemeClass(theme); -} - -// Listen to system preference changes and call callback(prefersDark) -// Returns an unsubscribe function -export function onSystemPrefChange(cb) { - try { - if (typeof window === "undefined" || !window.matchMedia) return () => {}; - const mq = window.matchMedia("(prefers-color-scheme: dark)"); - const handler = (ev) => cb(!!ev.matches); - // modern API - if (mq.addEventListener) mq.addEventListener("change", handler); - else mq.addListener && mq.addListener(handler); - return () => { - if (mq.removeEventListener) mq.removeEventListener("change", handler); - else mq.removeListener && mq.removeListener(handler); - }; - } catch (e) { - return () => {}; - } -} - -// Auto-init if running in browser -if (typeof window !== "undefined") { - try { - initTheme(); - } catch (e) { - // ignore - } -} - -export default { - getTheme, - applyTheme, - toggleTheme, - initTheme, - onSystemPrefChange, -};