Add dark mode option
This commit is contained in:
39
src/App.tsx
39
src/App.tsx
@@ -17,6 +17,7 @@ import DaySchedule from './components/day-schedule';
|
||||
import SimulationChart from './components/simulation-chart';
|
||||
import Settings from './components/settings';
|
||||
import LanguageSelector from './components/language-selector';
|
||||
import ThemeSelector from './components/theme-selector';
|
||||
import DisclaimerModal from './components/disclaimer-modal';
|
||||
import DataManagementModal from './components/data-management-modal';
|
||||
import { Button } from './components/ui/button';
|
||||
@@ -91,6 +92,33 @@ const MedPlanAssistant = () => {
|
||||
uiSettings
|
||||
} = appState;
|
||||
|
||||
// Apply theme based on user preference or system setting
|
||||
React.useEffect(() => {
|
||||
const theme = uiSettings.theme || 'system';
|
||||
const root = document.documentElement;
|
||||
|
||||
const applyTheme = (isDark: boolean) => {
|
||||
if (isDark) {
|
||||
root.classList.add('dark');
|
||||
} else {
|
||||
root.classList.remove('dark');
|
||||
}
|
||||
};
|
||||
|
||||
if (theme === 'system') {
|
||||
// Detect system preference
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
applyTheme(mediaQuery.matches);
|
||||
|
||||
// Listen for system theme changes
|
||||
const listener = (e: MediaQueryListEvent) => applyTheme(e.matches);
|
||||
mediaQuery.addEventListener('change', listener);
|
||||
return () => mediaQuery.removeEventListener('change', listener);
|
||||
} else {
|
||||
applyTheme(theme === 'dark');
|
||||
}
|
||||
}, [uiSettings.theme]);
|
||||
|
||||
const {
|
||||
showDayTimeOnXAxis,
|
||||
chartView,
|
||||
@@ -137,11 +165,18 @@ const MedPlanAssistant = () => {
|
||||
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<header className="mb-8">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl md:text-4xl font-bold tracking-tight">{t('appTitle')}</h1>
|
||||
</div>
|
||||
<LanguageSelector currentLanguage={currentLanguage} onLanguageChange={changeLanguage} t={t} />
|
||||
<div className="flex flex-wrap gap-2 justify-end">
|
||||
<ThemeSelector
|
||||
currentTheme={uiSettings.theme || 'system'}
|
||||
onThemeChange={(theme: 'light' | 'dark' | 'system') => updateUiSetting('theme', theme)}
|
||||
t={t}
|
||||
/>
|
||||
<LanguageSelector currentLanguage={currentLanguage} onLanguageChange={changeLanguage} t={t} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1">{t('appSubtitle')}</p>
|
||||
</header>
|
||||
|
||||
@@ -10,22 +10,18 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Label } from './ui/label';
|
||||
|
||||
const LanguageSelector = ({ currentLanguage, onLanguageChange, t }: any) => {
|
||||
return (
|
||||
<div className="flex flex-wrap-reverse items-center gap-2">
|
||||
<Label className="text-sm font-medium">{t('languageSelectorLabel')}</Label>
|
||||
<Select value={currentLanguage} onValueChange={onLanguageChange}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">{t('languageSelectorEN')}</SelectItem>
|
||||
<SelectItem value="de">{t('languageSelectorDE')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Select value={currentLanguage} onValueChange={onLanguageChange}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">{t('languageSelectorEN')}</SelectItem>
|
||||
<SelectItem value="de">{t('languageSelectorDE')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
29
src/components/theme-selector.tsx
Normal file
29
src/components/theme-selector.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Theme Selector Component
|
||||
*
|
||||
* Provides UI for switching between light/dark/system theme modes.
|
||||
* Uses shadcn/ui Select component.
|
||||
*
|
||||
* @author Andreas Weyer
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
|
||||
const ThemeSelector = ({ currentTheme, onThemeChange, t }: any) => {
|
||||
return (
|
||||
<Select value={currentTheme} onValueChange={onThemeChange}>
|
||||
<SelectTrigger className="w-36">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">{t('themeSelectorLight')}</SelectItem>
|
||||
<SelectItem value="dark">{t('themeSelectorDark')}</SelectItem>
|
||||
<SelectItem value="system">{t('themeSelectorSystem')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSelector;
|
||||
@@ -99,6 +99,7 @@ export interface UiSettings {
|
||||
showTherapeuticRange?: boolean;
|
||||
steadyStateDaysEnabled?: boolean;
|
||||
stickyChart: boolean;
|
||||
theme?: 'light' | 'dark' | 'system';
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
@@ -171,5 +172,6 @@ export const getDefaultState = (): AppState => ({
|
||||
showTherapeuticRange: false,
|
||||
steadyStateDaysEnabled: true,
|
||||
stickyChart: false,
|
||||
theme: 'system',
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,6 +17,11 @@ export const de = {
|
||||
languageSelectorEN: "English",
|
||||
languageSelectorDE: "Deutsch",
|
||||
|
||||
// Theme selector
|
||||
themeSelectorLight: "☀️ Hell",
|
||||
themeSelectorDark: "🌙 Dunkel",
|
||||
themeSelectorSystem: "💻 System",
|
||||
|
||||
// Dose Schedule
|
||||
myPlan: "Mein Plan",
|
||||
morning: "Morgens",
|
||||
|
||||
@@ -17,6 +17,11 @@ export const en = {
|
||||
languageSelectorEN: "English",
|
||||
languageSelectorDE: "Deutsch",
|
||||
|
||||
// Theme selector
|
||||
themeSelectorLight: "☀️ Light",
|
||||
themeSelectorDark: "🌙 Dark",
|
||||
themeSelectorSystem: "💻 System",
|
||||
|
||||
// Dose Schedule
|
||||
myPlan: "My Plan",
|
||||
morning: "Morning",
|
||||
|
||||
@@ -31,6 +31,33 @@
|
||||
--radius: 0.625rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 10%;
|
||||
--foreground: 0 0% 95%;
|
||||
--card: 0 0% 14%;
|
||||
--card-foreground: 0 0% 95%;
|
||||
--popover: 0 0% 12%;
|
||||
--popover-foreground: 0 0% 95%;
|
||||
--primary: 217 91% 60%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
--secondary: 220 15% 20%;
|
||||
--secondary-foreground: 0 0% 90%;
|
||||
--muted: 220 10% 18%;
|
||||
--muted-foreground: 0 0% 60%;
|
||||
--accent: 220 10% 18%;
|
||||
--accent-foreground: 0 0% 90%;
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 25%;
|
||||
--input: 0 0% 25%;
|
||||
--ring: 0 0% 40%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user