diff --git a/package-lock.json b/package-lock.json index 94a15bc..8a78af0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "react-dom": "19.2.4", "react-hook-form": "^7.75.0", "tailwind-merge": "^3.6.0", + "tailwindcss-animate": "^1.0.7", "zod": "^4.4.3" }, "devDependencies": { @@ -9308,9 +9309,17 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", - "dev": true, "license": "MIT" }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", diff --git a/package.json b/package.json index 7e0a715..1da08b7 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-dom": "19.2.4", "react-hook-form": "^7.75.0", "tailwind-merge": "^3.6.0", + "tailwindcss-animate": "^1.0.7", "zod": "^4.4.3" }, "devDependencies": { diff --git a/src/app/globals.css b/src/app/globals.css index 706ff48..2a777b9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,64 +1,154 @@ @import "tailwindcss"; +@plugin "tailwindcss-animate"; @source not "../../.01_projects/**"; @source not "../../.claude/**"; @source not "../../scripts/**"; @source not "../../.planning/**"; +/* Class-based dark mode (Tailwind v4). The FOUC guard in layout.tsx toggles + `.dark` on before paint; the useTheme hook keeps it in sync. */ +@custom-variant dark (&:where(.dark, .dark *)); + /* ========================================================= - Design tokens — iamcavalli brand palette (Tailwind v4 @theme) + Design tokens — iamcavalli brand palette + Raw values live in :root (light) / .dark (dark) so the whole + app themes off a single class toggle; @theme inline below maps + them to Tailwind color utilities (bg-background, text-primary…). ========================================================= */ +:root { + /* Base */ + --background: #ffffff; + --foreground: #1a1a1a; + + /* Brand primary — verde scuro iamcavalli */ + --primary: #1A463C; + --primary-foreground: #ffffff; + + /* Brand accent — giallo lime */ + --accent: #DEF168; + --accent-foreground: #1A463C; + + /* Secondary — grigio neutro */ + --secondary: #f4f4f5; + --secondary-foreground: #1a1a1a; + + /* Muted — per testi e sfondi secondari */ + --muted: #f9f9f9; + --muted-foreground: #71717a; + + /* Destructive */ + --destructive: #ef4444; + --destructive-foreground: #ffffff; + + /* Card */ + --card: #ffffff; + --card-foreground: #1a1a1a; + + /* Popover */ + --popover: #ffffff; + --popover-foreground: #1a1a1a; + + /* Border / Input / Ring */ + --border: #e5e7eb; + --input: #e5e7eb; + --ring: #1A463C; + + /* Semantic — stato task/pagamenti */ + --success: #16a34a; + --warning: #ca8a04; + --info: #2563eb; + + /* Legacy — usati inline nei componenti esistenti */ + --tertiary: #999999; + --bg-subtle: #f9f9f9; + --border-light: #e5e5e5; + + --radius: 0.5rem; +} + +.dark { + /* Base — verde-nero, coerente col brand */ + --background: #0e1512; + --foreground: #f2f4f3; + + /* Brand primary — verde schiarito per contrasto su fondo scuro */ + --primary: #3FA88C; + --primary-foreground: #06110d; + + /* Brand accent — lime invariato, testo verde scuro */ + --accent: #DEF168; + --accent-foreground: #14231d; + + --secondary: #1b2420; + --secondary-foreground: #f2f4f3; + + --muted: #171f1b; + --muted-foreground: #9aa39e; + + --destructive: #f87171; + --destructive-foreground: #1a0a0a; + + --card: #131a16; + --card-foreground: #f2f4f3; + + --popover: #131a16; + --popover-foreground: #f2f4f3; + + --border: #26302b; + --input: #26302b; + --ring: #3FA88C; + + --success: #22c55e; + --warning: #eab308; + --info: #3b82f6; + + --tertiary: #8a938d; + --bg-subtle: #171f1b; + --border-light: #26302b; +} + @theme inline { /* Font */ --font-sans: var(--font-geist-sans), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; --font-mono: var(--font-geist-mono); - /* Base */ - --color-background: #ffffff; - --color-foreground: #1a1a1a; + /* Colors — mapped from the raw tokens above */ + --color-background: var(--background); + --color-foreground: var(--foreground); - /* Brand primary — verde scuro iamcavalli */ - --color-primary: #1A463C; - --color-primary-foreground: #ffffff; + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); - /* Brand accent — giallo lime */ - --color-accent: #DEF168; - --color-accent-foreground: #1A463C; + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); - /* Secondary — grigio neutro */ - --color-secondary: #f4f4f5; - --color-secondary-foreground: #1a1a1a; + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); - /* Muted — per testi e sfondi secondari */ - --color-muted: #f9f9f9; - --color-muted-foreground: #71717a; + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); - /* Destructive */ - --color-destructive: #ef4444; - --color-destructive-foreground: #ffffff; + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); - /* Card */ - --color-card: #ffffff; - --color-card-foreground: #1a1a1a; + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); - /* Popover */ - --color-popover: #ffffff; - --color-popover-foreground: #1a1a1a; + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); - /* Border / Input / Ring */ - --color-border: #e5e7eb; - --color-input: #e5e7eb; - --color-ring: #1A463C; + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); - /* Semantic — stato task/pagamenti */ - --color-success: #16a34a; - --color-warning: #ca8a04; - --color-info: #2563eb; + --color-success: var(--success); + --color-warning: var(--warning); + --color-info: var(--info); - /* Legacy — usati inline nei componenti esistenti */ - --color-tertiary: #999999; - --color-bg-subtle: #f9f9f9; - --color-border-light: #e5e5e5; + --color-tertiary: var(--tertiary); + --color-bg-subtle: var(--bg-subtle); + --color-border-light: var(--border-light); } /* ========================================================= @@ -75,10 +165,44 @@ html { } body { - background-color: #ffffff; - color: #171717; + background-color: var(--background); + color: var(--foreground); font-family: var(--font-sans); line-height: 1.6; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -} \ No newline at end of file +} + +/* ========================================================= + Scrollbar utilities (ported from OMC design system) + ========================================================= */ +@layer utilities { + /* Hide the native scrollbar without breaking scroll. */ + .no-scrollbar { + scrollbar-width: none; + -ms-overflow-style: none; + } + .no-scrollbar::-webkit-scrollbar { + display: none; + } + + /* Very thin, muted scrollbar — discoverable but low-noise. */ + .thin-scrollbar { + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; + } + .thin-scrollbar::-webkit-scrollbar { + height: 3px; + width: 3px; + } + .thin-scrollbar::-webkit-scrollbar-track { + background: transparent; + } + .thin-scrollbar::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 9999px; + } + .thin-scrollbar::-webkit-scrollbar-thumb:hover { + background: var(--muted-foreground); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e43c606..e15a154 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -32,7 +32,12 @@ export default function RootLayout({ lang="it" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} > -
+ + {children} diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..56bba57 --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -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