Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,39 @@
/>

<title>drawDB | Online database diagram editor and SQL generator</title>

<script>
(function () {
const storageKey = "drawdb:theme";
const darkQuery = "(prefers-color-scheme: dark)";

function applyTheme(theme) {
const root = document.documentElement;
if (!root) return;
root.setAttribute("data-theme", theme);
root.style.setProperty("color-scheme", theme === "dark" ? "dark" : "light");
}

function readStoredTheme() {
try {
const stored = localStorage.getItem(storageKey);
return stored === "light" || stored === "dark" ? stored : null;
} catch {
return null;
}
}

function readSystemPreference() {
if (typeof window.matchMedia !== "function") return null;
return window.matchMedia(darkQuery).matches ? "dark" : "light";
}

const theme = readStoredTheme() ?? readSystemPreference() ?? "dark";
applyTheme(theme);
})();
</script>
</head>
<body theme-mode="light">
<body data-theme="dark">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
Expand Down
4 changes: 2 additions & 2 deletions src/components/EditorHeader/ControlPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1820,8 +1820,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();
Expand Down
17 changes: 11 additions & 6 deletions src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 ThemeToggle from "./ThemeToggle";

export default function Navbar() {
const [openMenu, setOpenMenu] = useState(false);
Expand Down Expand Up @@ -75,12 +76,16 @@ export default function Navbar() {
</a>
</div>
</div>
<button
onClick={() => setOpenMenu((prev) => !prev)}
className="hidden md:inline-block h-[24px]"
>
<IconMenu size="extra-large" />
</button>
<div className="flex items-center gap-4">
<ThemeToggle />
<button
onClick={() => setOpenMenu((prev) => !prev)}
className="hidden md:inline-block h-[24px]"
aria-label="Open menu"
>
<IconMenu size="extra-large" />
</button>
</div>
</div>
<hr />
<SideSheet
Expand Down
76 changes: 76 additions & 0 deletions src/components/ThemeToggle.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<button
type="button"
role="switch"
aria-checked={isDark}
aria-label="Toggle color theme"
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
className={`theme-toggle inline-flex h-11 w-11 items-center justify-center rounded-full border border-transparent transition-all duration-200 hover:shadow focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-sky-500 motion-reduce:transition-none ${className}`}
onClick={handleToggle}
onKeyDown={handleKeyDown}
>
<span className="relative flex items-center justify-center">
{/* Sun icon (visible in light) */}
<svg
className={`h-5 w-5 transition-opacity duration-200 ${isDark ? "opacity-0" : "opacity-100"} motion-reduce:transition-none`}
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M12 4V2M12 22v-2M4 12H2M22 12h-2M5 5l-1.5-1.5M20.5 20.5 19 19M19 5l1.5-1.5M4.5 19.5 6 18"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="1.5" />
</svg>

{/* Moon icon (visible in dark) */}
<svg
className={`absolute h-5 w-5 transition-opacity duration-200 ${isDark ? "opacity-100" : "opacity-0"} motion-reduce:transition-none`}
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</button>
);
}

36 changes: 30 additions & 6 deletions src/context/SettingsContext.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, useEffect, useState } from "react";
import { createContext, useEffect, useState, useContext } from "react";
import { tableWidth } from "../data/constants";
import { getTheme, setTheme } from "../themeManager";

const defaultSettings = {
strictMode: false,
Expand All @@ -15,20 +16,39 @@ const defaultSettings = {
showDebugCoordinates: false,
};

function getInitialMode() {
try {
const storedSettings = localStorage.getItem("settings");
if (storedSettings) {
const parsed = JSON.parse(storedSettings);
if (parsed.mode) {
return parsed.mode;
}
}
} catch (e) {
/* ignore and fall through */
}

return getTheme();
}

export const SettingsContext = createContext(defaultSettings);

export default function SettingsContextProvider({ children }) {
const [settings, setSettings] = useState(defaultSettings);
const [settings, setSettings] = useState(() => {
const initialMode = getInitialMode();
return { ...defaultSettings, mode: initialMode };
});

useEffect(() => {
const settings = localStorage.getItem("settings");
if (settings) {
setSettings(JSON.parse(settings));
const storedSettings = localStorage.getItem("settings");
if (storedSettings) {
setSettings(JSON.parse(storedSettings));
}
}, []);

useEffect(() => {
document.body.setAttribute("theme-mode", settings.mode);
setTheme(settings.mode);
}, [settings.mode]);

useEffect(() => {
Expand All @@ -41,3 +61,7 @@ export default function SettingsContextProvider({ children }) {
</SettingsContext.Provider>
);
}

export function useSettingsContext() {
return useContext(SettingsContext);
}
1 change: 1 addition & 0 deletions src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export { default as useTypes } from "./useTypes";
export { default as useUndoRedo } from "./useUndoRedo";
export { default as useEnums } from "./useEnums";
export { default as useThemedPage } from "./useThemedPage";
export { default as useThemePreference } from "./useThemePreference";
17 changes: 17 additions & 0 deletions src/hooks/useThemePreference.js
Original file line number Diff line number Diff line change
@@ -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;
}

7 changes: 4 additions & 3 deletions src/hooks/useThemedPage.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useLayoutEffect } from "react";
import useSettings from "./useSettings";
import { setTheme } from "../themeManager";

/**
* Adds the `theme-mode` attribute to the body element for semi-ui dark theme
* Keeps the rendered page in sync with the current editor theme.
*/
export default function useThemedPage() {
const { settings } = useSettings();

useLayoutEffect(() => {
document.body.setAttribute("theme-mode", settings.mode);
}, [settings]);
setTheme(settings.mode);
}, [settings.mode]);
}
Loading