Add dark mode support and theme provider
This commit is contained in:
+3
-4
@@ -12,7 +12,7 @@ import { NuevoIngreso } from '@/sections/NuevoIngreso';
|
|||||||
import { EditIngreso } from '@/sections/EditIngreso';
|
import { EditIngreso } from '@/sections/EditIngreso';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ThemeProvider } from '@/contexts/ThemeContext';
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
@@ -185,7 +185,7 @@ function AppContent() {
|
|||||||
getAreaName={(areaId) => store.areas.find(a => a.id === areaId)?.nombre || ''}
|
getAreaName={(areaId) => store.areas.find(a => a.id === areaId)?.nombre || ''}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
case 'editaringreso': {
|
case 'editaringreso': {
|
||||||
const internacion = store.getInternacionById(store.currentInternacionId || '');
|
const internacion = store.getInternacionById(store.currentInternacionId || '');
|
||||||
const paciente = internacion ? store.getPacienteById(internacion.pacienteId) : undefined;
|
const paciente = internacion ? store.getPacienteById(internacion.pacienteId) : undefined;
|
||||||
@@ -225,8 +225,7 @@ function AppContent() {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
// @ts-expect-error - next-themes types mismatch
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
|
||||||
<AppContent />
|
<AppContent />
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-right" />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { createContext, useContext, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
type Theme = "dark" | "light" | "system"
|
||||||
|
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
defaultTheme?: Theme
|
||||||
|
storageKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeProviderState = {
|
||||||
|
theme: Theme
|
||||||
|
setTheme: (theme: Theme) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ThemeProviderState = {
|
||||||
|
theme: "system",
|
||||||
|
setTheme: () => null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "light",
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setTheme] = useState<Theme>(
|
||||||
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
|
||||||
|
root.classList.remove("light", "dark")
|
||||||
|
|
||||||
|
if (theme === "system") {
|
||||||
|
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.matches
|
||||||
|
? "dark"
|
||||||
|
: "light"
|
||||||
|
|
||||||
|
root.classList.add(systemTheme)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
root.classList.add(theme)
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
theme,
|
||||||
|
setTheme: (theme: Theme) => {
|
||||||
|
localStorage.setItem(storageKey, theme)
|
||||||
|
setTheme(theme)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeProviderContext)
|
||||||
|
|
||||||
|
if (context === undefined)
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider")
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { ThemeProvider as NextThemesProvider, useTheme as useNextTheme } from 'next-themes';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
||||||
return (
|
|
||||||
<NextThemesProvider attribute="class" defaultTheme="system" enableSystem>
|
|
||||||
{children}
|
|
||||||
</NextThemesProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTheme() {
|
|
||||||
const { theme, setTheme } = useNextTheme();
|
|
||||||
const isDarkMode = theme === 'dark';
|
|
||||||
const toggleDarkMode = () => setTheme(theme === 'dark' ? 'light' : 'dark');
|
|
||||||
return { isDarkMode, toggleDarkMode, theme, setTheme };
|
|
||||||
}
|
|
||||||
|
|
||||||
export { NextThemesProvider };
|
|
||||||
@@ -266,8 +266,8 @@ export function HistoriaClinica({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div><h2 className="font-bold text-lg text-gray-900 flex items-center gap-2">
|
<div><h2 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
<User className="h-4 w-4" />
|
<User className="h-6 w-6 text-blue-600" />
|
||||||
{paciente.apellido}, {paciente.nombre}
|
{paciente.apellido}, {paciente.nombre}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,7 +284,7 @@ export function HistoriaClinica({
|
|||||||
{paciente.fechaNacimiento ? calcularEdad(paciente.fechaNacimiento) + ' años' : ''}
|
{paciente.fechaNacimiento ? calcularEdad(paciente.fechaNacimiento) + ' años' : ''}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<Badge variant="outline" className="text-sm px-3 py-1 bg-blue-50 text-blue-700 border-blue-200">
|
<Badge variant="outline" className="text-sm px-3 py-1 bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-300">
|
||||||
DNI: {paciente.dni}
|
DNI: {paciente.dni}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
@@ -1364,7 +1364,7 @@ function SeccionEvoluciones({ evos, internacionId, add, update, del }: {
|
|||||||
<div><Label>Fecha *</Label><Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} /></div>
|
<div><Label>Fecha *</Label><Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} /></div>
|
||||||
<div><Label>Hora *</Label><Input type="time" value={hora} onChange={e => setHora(e.target.value)} /></div>
|
<div><Label>Hora *</Label><Input type="time" value={hora} onChange={e => setHora(e.target.value)} /></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-50 p-3 rounded-lg border border-blue-200">
|
<div className="bg-blue-50 dark:bg-blue-950/50 p-3 rounded-lg border border-blue-200 dark:border-blue-700">
|
||||||
<p className="text-sm font-bold text-blue-800 mb-3 flex items-center gap-2"><Activity className="h-4 w-4" />Signos Vitales</p>
|
<p className="text-sm font-bold text-blue-800 mb-3 flex items-center gap-2"><Activity className="h-4 w-4" />Signos Vitales</p>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||||
<div><Label className="flex items-center gap-1"><Heart className="h-4 w-4" />TAS</Label><Input type="number" value={presionSistolica} onChange={e => setPresionSistolica(e.target.value)} placeholder="120" /></div>
|
<div><Label className="flex items-center gap-1"><Heart className="h-4 w-4" />TAS</Label><Input type="number" value={presionSistolica} onChange={e => setPresionSistolica(e.target.value)} placeholder="120" /></div>
|
||||||
@@ -1387,9 +1387,9 @@ function SeccionEvoluciones({ evos, internacionId, add, update, del }: {
|
|||||||
<div><Label>SOMA</Label><Input value={soma} onChange={e => setSoma(e.target.value)} placeholder="Estado SOMA" /></div>
|
<div><Label>SOMA</Label><Input value={soma} onChange={e => setSoma(e.target.value)} placeholder="Estado SOMA" /></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div><Label>Novedades</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[80px]" value={novedades} onChange={e => setNovedades(e.target.value)} placeholder="Novedades..." /></div>
|
<div><Label>Novedades</Label><textarea value={novedades} onChange={e => setNovedades(e.target.value)} placeholder="Novedades..." /></div>
|
||||||
<div><Label>Comentario</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[80px]" value={comentario} onChange={e => setComentario(e.target.value)} placeholder="Comentarios adicionales..." /></div>
|
<div><Label>Comentario</Label><textarea value={comentario} onChange={e => setComentario(e.target.value)} placeholder="Comentarios adicionales..." /></div>
|
||||||
<div><Label>Pendientes</Label><textarea className="w-full p-2 border rounded-md text-sm min-h-[80px]" value={pendientes} onChange={e => setPendientes(e.target.value)} placeholder="Pendientes..." /></div>
|
<div><Label>Pendientes</Label><textarea value={pendientes} onChange={e => setPendientes(e.target.value)} placeholder="Pendientes..." /></div>
|
||||||
<div><Label>Médico *</Label><Input value={medico} onChange={e => setMedico(e.target.value)} placeholder="Nombre del médico" /></div>
|
<div><Label>Médico *</Label><Input value={medico} onChange={e => setMedico(e.target.value)} placeholder="Nombre del médico" /></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 mt-4"><Button variant="outline" onClick={() => { reset(); setDialog(false); }}><X className="h-4 w-4 mr-2" />Cancelar</Button><Button onClick={handle} disabled={!medico}><Save className="h-4 w-4 mr-2" />Guardar Evolución</Button></div>
|
<div className="flex justify-end gap-2 mt-4"><Button variant="outline" onClick={() => { reset(); setDialog(false); }}><X className="h-4 w-4 mr-2" />Cancelar</Button><Button onClick={handle} disabled={!medico}><Save className="h-4 w-4 mr-2" />Guardar Evolución</Button></div>
|
||||||
|
|||||||
+42
-21
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Bed,
|
Bed,
|
||||||
Users,
|
Users,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
FileText,
|
FileText,
|
||||||
FlaskConical,
|
|
||||||
Activity,
|
Activity,
|
||||||
Microscope,
|
Microscope,
|
||||||
Menu,
|
Menu,
|
||||||
Sun,
|
Sun,
|
||||||
@@ -13,9 +13,17 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
import { useTheme } from '@/contexts/ThemeContext';
|
|
||||||
import type { Vista } from '@/types';
|
import type { Vista } from '@/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { useTheme } from "@/components/theme-provider"
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
vistaActual: Vista;
|
vistaActual: Vista;
|
||||||
@@ -34,7 +42,9 @@ const menuItems: { vista: Vista; label: string; icon: React.ElementType }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function Layout({ children, vistaActual, onCambiarVista }: LayoutProps) {
|
export function Layout({ children, vistaActual, onCambiarVista }: LayoutProps) {
|
||||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
const { theme, setTheme } = useTheme()
|
||||||
|
|
||||||
|
const toggleDarkMode = () => setTheme(theme === "dark" ? "light" : "dark")
|
||||||
|
|
||||||
const NavContent = () => (
|
const NavContent = () => (
|
||||||
<nav className="flex flex-col gap-2">
|
<nav className="flex flex-col gap-2">
|
||||||
@@ -76,15 +86,26 @@ export function Layout({ children, vistaActual, onCambiarVista }: LayoutProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
|
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Button
|
<DropdownMenu>
|
||||||
variant="ghost"
|
<DropdownMenuTrigger asChild>
|
||||||
size="sm"
|
<Button variant="outline" size="icon">
|
||||||
onClick={toggleDarkMode}
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
className="flex items-center gap-2"
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
>
|
<span className="sr-only">Toggle theme</span>
|
||||||
{isDarkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
</Button>
|
||||||
<span className="text-xs">{isDarkMode ? 'Modo Claro' : 'Modo Oscuro'}</span>
|
</DropdownMenuTrigger>
|
||||||
</Button>
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||||
|
Light
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||||
|
Dark
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
|
System
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 dark:text-gray-500 text-center mt-2">
|
<div className="text-xs text-gray-400 dark:text-gray-500 text-center mt-2">
|
||||||
v2.0
|
v2.0
|
||||||
@@ -122,8 +143,8 @@ export function Layout({ children, vistaActual, onCambiarVista }: LayoutProps) {
|
|||||||
onClick={toggleDarkMode}
|
onClick={toggleDarkMode}
|
||||||
className="w-full flex items-center justify-center gap-2 dark:text-white"
|
className="w-full flex items-center justify-center gap-2 dark:text-white"
|
||||||
>
|
>
|
||||||
{isDarkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||||
<span className="text-sm">{isDarkMode ? 'Modo Claro' : 'Modo Oscuro'}</span>
|
<span className="text-sm">{theme === "dark" ? 'Modo Claro' : 'Modo Oscuro'}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user