feat: dark/light theming system + design tokens (OMC design system port)
- 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>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user