diff --git a/app/components/Layout.tsx b/app/components/Layout.tsx index 3a1b374..83969b3 100644 --- a/app/components/Layout.tsx +++ b/app/components/Layout.tsx @@ -67,7 +67,10 @@ export function Layout({ children, currentPage, onNavigate }: LayoutProps) {
- +
+ Theme + +
) })} @@ -47,32 +100,31 @@ export function ThemeToggle({ variant = 'pill', className = '' }: ThemeTogglePro ) } - // Pill variant + // Default: animated icon button (Sun/Moon) with View Transition return ( -
- {options.map(({ mode, Icon, label }) => { - const active = themeMode === mode - return ( - - ) - })} -
+ {/* Sun icon — shown in light mode */} + + {/* Moon icon — shown in dark mode */} + + ) } diff --git a/app/globals.css b/app/globals.css index ae5823a..cd06e60 100644 --- a/app/globals.css +++ b/app/globals.css @@ -18,8 +18,32 @@ * { @apply border-slate-200 dark:border-slate-800; } + + /* ── View Transition: animated theme toggle ────────────────────────────── */ + /* Disable the default cross-fade so our clip-path runs cleanly */ + ::view-transition-old(root), + ::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; + } + + /* When the JS sets data-theme-vt="active", animate the new state */ + [data-theme-vt='active']::view-transition-new(root) { + clip-path: var(--vt-clip-to, circle(100% at 50% 50%)); + animation: vt-clip-reveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; + } + + @keyframes vt-clip-reveal { + from { + clip-path: var(--vt-clip-from, circle(0px at 50% 50%)); + } + to { + clip-path: var(--vt-clip-to, circle(100% at 50% 50%)); + } + } } + @layer components { .app-surface { @apply border border-slate-200/80 bg-white/90 shadow-sm shadow-slate-200/60 backdrop-blur dark:border-slate-800 dark:bg-slate-900/80 dark:shadow-black/20;