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:
Daniel Volz
2026-02-08 20:32:40 +01:00
committed by GitHub
parent b19bcf02c2
commit 8c5deed4c2
29 changed files with 1053 additions and 166 deletions
+1 -1
View File
@@ -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";
+58 -10
View File
@@ -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 };
}