feat: theme dropdown with system preference and comprehensive bottle-type fixes (#138)
- Replace dark/light toggle with Light/Dark/System dropdown menu - System theme follows OS prefers-color-scheme setting - Apply theme dropdown to shared schedule page - Fix 7 packageType (bottle) bugs across stock calc, share, refills, export/import - Fix planner bottle-type stock calculation and display - Fix dailyRate double-counting with per-intake takenBy - Fix About modal update check stale caching - Fix intake reminder past-intake seeding and push title - Fix phantom DB path in drizzle.config.ts - Fix mobile dose field visibility - Make medication name clickable in dashboard reminder bar - Improve planner checkbox UX with inline tooltip - Add 20+ new tests covering all fixes
This commit is contained in:
@@ -14,7 +14,7 @@ export type { Settings, UseSettingsReturn } from "./useSettings";
|
||||
export { useSettings } from "./useSettings";
|
||||
export type { UseShareReturn } from "./useShare";
|
||||
export { useShare } from "./useShare";
|
||||
export type { Theme, UseThemeReturn } from "./useTheme";
|
||||
export type { Theme, ThemePreference, UseThemeReturn } from "./useTheme";
|
||||
export { useTheme } from "./useTheme";
|
||||
export type { UseUnsavedChangesWarningReturn } from "./useUnsavedChangesWarning";
|
||||
export { useUnsavedChangesWarning } from "./useUnsavedChangesWarning";
|
||||
|
||||
@@ -1,32 +1,80 @@
|
||||
// =============================================================================
|
||||
// useTheme Hook - Theme (dark/light mode) state management
|
||||
// useTheme Hook - Theme (dark/light/system mode) state management
|
||||
// =============================================================================
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export type Theme = "light" | "dark";
|
||||
export type ThemePreference = "light" | "dark" | "system";
|
||||
|
||||
export interface UseThemeReturn {
|
||||
/** The resolved theme applied to the DOM ("light" | "dark") */
|
||||
theme: Theme;
|
||||
/** The user's preference ("light" | "dark" | "system") */
|
||||
themePreference: ThemePreference;
|
||||
/** Set the theme preference */
|
||||
setThemePreference: (pref: ThemePreference) => void;
|
||||
/** Legacy toggle: cycles light → dark → system → light */
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
function getSystemTheme(): Theme {
|
||||
if (typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: light)").matches) {
|
||||
return "light";
|
||||
}
|
||||
return "dark";
|
||||
}
|
||||
|
||||
function resolveTheme(pref: ThemePreference): Theme {
|
||||
return pref === "system" ? getSystemTheme() : pref;
|
||||
}
|
||||
|
||||
export function useTheme(): UseThemeReturn {
|
||||
const [theme, setTheme] = useState<Theme>(() => {
|
||||
const [themePreference, setThemePreferenceState] = useState<ThemePreference>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return (localStorage.getItem("theme") as Theme) || "dark";
|
||||
const stored = localStorage.getItem("theme") as ThemePreference | null;
|
||||
if (stored === "light" || stored === "dark" || stored === "system") return stored;
|
||||
return "dark";
|
||||
}
|
||||
return "dark";
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
const [theme, setTheme] = useState<Theme>(() => resolveTheme(themePreference));
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
|
||||
// Apply resolved theme to DOM whenever preference or system theme changes
|
||||
useEffect(() => {
|
||||
const resolved = resolveTheme(themePreference);
|
||||
setTheme(resolved);
|
||||
document.documentElement.setAttribute("data-theme", resolved);
|
||||
localStorage.setItem("theme", themePreference);
|
||||
}, [themePreference]);
|
||||
|
||||
// Listen for system theme changes when preference is "system"
|
||||
useEffect(() => {
|
||||
if (themePreference !== "system") return;
|
||||
const mq = window.matchMedia?.("(prefers-color-scheme: light)");
|
||||
if (!mq) return;
|
||||
|
||||
const handler = () => {
|
||||
const resolved = resolveTheme("system");
|
||||
setTheme(resolved);
|
||||
document.documentElement.setAttribute("data-theme", resolved);
|
||||
};
|
||||
mq.addEventListener("change", handler);
|
||||
return () => mq.removeEventListener("change", handler);
|
||||
}, [themePreference]);
|
||||
|
||||
const setThemePreference = useCallback((pref: ThemePreference) => {
|
||||
setThemePreferenceState(pref);
|
||||
}, []);
|
||||
|
||||
return { theme, toggleTheme };
|
||||
const toggleTheme = useCallback(() => {
|
||||
setThemePreferenceState((prev) => {
|
||||
if (prev === "light") return "dark";
|
||||
if (prev === "dark") return "system";
|
||||
return "light";
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { theme, themePreference, setThemePreference, toggleTheme };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user