43cb7e7469
- Restructure design tokens into :root/.dark raw vars mapped via @theme inline - Add light/dark/system theming: useTheme hook + FOUC guard + ThemeToggle - Keep iamcavalli palette (primary #1A463C, accent #DEF168) and Geist fonts - Derive brand-consistent dark palette (verde-nero bg, lightened primary) - Add scrollbar utilities (.no-scrollbar/.thin-scrollbar) - Install tailwindcss-animate, register via @plugin (Tailwind v4) - Mount ThemeToggle in admin sidebar footer Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { Check, Monitor, Moon, Sun } from 'lucide-react'
|
|
|
|
import { useTheme, type ThemePref } from '@/lib/theme'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
const OPTIONS: { value: ThemePref; label: string; icon: typeof Sun }[] = [
|
|
{ value: 'system', label: 'Sistema', icon: Monitor },
|
|
{ value: 'light', label: 'Chiaro', icon: Sun },
|
|
{ value: 'dark', label: 'Scuro', icon: Moon },
|
|
]
|
|
|
|
/** Header theme switcher — Sistema / Chiaro / Scuro. Defaults to Sistema
|
|
* (follows the OS). Dropdown closes on outside-click + Esc. */
|
|
export default function ThemeToggle() {
|
|
const { pref, resolved, setTheme } = useTheme()
|
|
const [open, setOpen] = useState(false)
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
const onDocClick = (e: MouseEvent) => {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
}
|
|
const onEsc = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') setOpen(false)
|
|
}
|
|
document.addEventListener('mousedown', onDocClick)
|
|
document.addEventListener('keydown', onEsc)
|
|
return () => {
|
|
document.removeEventListener('mousedown', onDocClick)
|
|
document.removeEventListener('keydown', onEsc)
|
|
}
|
|
}, [open])
|
|
|
|
const TriggerIcon = pref === 'system' ? Monitor : resolved === 'dark' ? Moon : Sun
|
|
|
|
return (
|
|
<div className="relative" ref={ref}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((o) => !o)}
|
|
title="Tema"
|
|
aria-label="Tema"
|
|
className="flex items-center justify-center size-9 rounded-full border bg-card/70 hover:bg-card transition-colors text-muted-foreground hover:text-foreground"
|
|
>
|
|
<TriggerIcon className="size-4" />
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="absolute top-full right-0 mt-2 w-40 bg-popover text-popover-foreground border rounded-xl shadow-xl overflow-hidden z-50 p-1">
|
|
{OPTIONS.map((o) => {
|
|
const Icon = o.icon
|
|
const active = o.value === pref
|
|
return (
|
|
<button
|
|
key={o.value}
|
|
type="button"
|
|
onClick={() => {
|
|
setTheme(o.value)
|
|
setOpen(false)
|
|
}}
|
|
className={cn(
|
|
'w-full flex items-center gap-2 px-2.5 py-2 rounded-md text-sm transition-colors text-left',
|
|
active ? 'bg-primary/10 text-primary' : 'hover:bg-muted',
|
|
)}
|
|
>
|
|
<Icon className="size-4" />
|
|
{o.label}
|
|
{active && <Check className="size-3.5 ml-auto" />}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|