From 9a115b88926a0c9021a7274831fb7c6ef8318cfa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 9 Jul 2026 03:15:14 +0000 Subject: [PATCH 1/2] Header: collapse theme switcher into single icon + tray menu Instead of always showing all 3 theme icon buttons (light/dark/xanga) next to the hamburger, the header now shows only the active theme's icon. Clicking it opens a small dropdown tray listing the 3 theme options; selecting one applies the theme, closes the tray, and updates the trigger icon to match. Closes on outside click or Escape. Co-authored-by: Thomas Bohn --- components/Header.tsx | 174 ++++++++++++++++++++++++++---------------- 1 file changed, 110 insertions(+), 64 deletions(-) diff --git a/components/Header.tsx b/components/Header.tsx index 1fc87e1..a746a55 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,12 +1,12 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import Link from 'next/link' import { usePathname } from 'next/navigation' -import { useTheme } from './ThemeProvider' +import { useTheme, type Theme } from './ThemeProvider' import { useXangaLayoutOptional } from '@/components/xanga/XangaLayoutContext' -function ThemeIcon({ theme }: { theme: 'light' | 'dark' | 'xanga' }) { +function ThemeIcon({ theme }: { theme: Theme }) { if (theme === 'light') { return ( void -}) { - const label = value === 'xanga' ? 'Xanga' : value === 'dark' ? 'Dark' : 'Light' +const THEME_OPTIONS: { value: Theme; label: string }[] = [ + { value: 'light', label: 'Light' }, + { value: 'dark', label: 'Dark' }, + { value: 'xanga', label: 'Xanga' }, +] + +function themeLabel(theme: Theme) { + return theme === 'xanga' ? 'Xanga' : theme === 'dark' ? 'Dark' : 'Light' +} + +// Shows only the active theme's icon. Clicking it opens a small tray/menu +// with all 3 theme options; picking one applies the theme, closes the tray, +// and swaps the trigger icon to match. +function ThemeMenu({ theme, setTheme }: { theme: Theme; setTheme: (t: Theme) => void }) { + const [open, setOpen] = useState(false) + const containerRef = useRef(null) + const triggerRef = useRef(null) + + useEffect(() => { + if (!open) return + + function handlePointerDown(event: MouseEvent) { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setOpen(false) + } + } + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') { + setOpen(false) + triggerRef.current?.focus() + } + } + + document.addEventListener('mousedown', handlePointerDown) + document.addEventListener('keydown', handleKeyDown) + return () => { + document.removeEventListener('mousedown', handlePointerDown) + document.removeEventListener('keydown', handleKeyDown) + } + }, [open]) + return ( - {label} - - {label} - - +
+ + + {open && ( +
+ {THEME_OPTIONS.map((option) => { + const isActive = theme === option.value + return ( + + ) + })} +
+ )} +
) } @@ -236,39 +297,24 @@ export function Header() { })} -
-
- {(['light', 'dark'] as const).map((t) => ( - setTheme(t)} - /> - ))} - {showCollapsedIcon && layout ? ( - - ) : ( - setTheme('xanga')} - /> - )} -
+
+ {showCollapsedIcon && layout && ( + + )} + {/* Hamburger toggle for mobile navigation */} )} diff --git a/components/XangaLayoutWrapper.tsx b/components/XangaLayoutWrapper.tsx index df502d7..001d994 100644 --- a/components/XangaLayoutWrapper.tsx +++ b/components/XangaLayoutWrapper.tsx @@ -2,11 +2,14 @@ import { useEffect, useState } from 'react' import { useTheme } from '@/components/ThemeProvider' -import { XangaLayoutProvider } from '@/components/xanga/XangaLayoutContext' +import { XangaLayoutProvider, type SidebarSide, type VisibleSidebarSide } from '@/components/xanga/XangaLayoutContext' export function XangaLayoutWrapper({ children }: { children: React.ReactNode }) { const { theme } = useTheme() - const [sidebarSide, setSidebarSide] = useState<'left' | 'right' | 'hide'>('right') + const [sidebarSide, setSidebarSide] = useState('right') + // Remembers which side the sidebar was last visible on, so "restore" can + // put it back where the user left it instead of always defaulting to right. + const [lastVisibleSide, setLastVisibleSide] = useState('right') const isXanga = theme === 'xanga' @@ -14,20 +17,30 @@ export function XangaLayoutWrapper({ children }: { children: React.ReactNode }) if (!isXanga) return const saved = localStorage.getItem('xangaSidebarSide') if (saved === 'left' || saved === 'right' || saved === 'hide') setSidebarSide(saved) + const savedLastVisible = localStorage.getItem('xangaSidebarLastVisibleSide') + if (savedLastVisible === 'left' || savedLastVisible === 'right') setLastVisibleSide(savedLastVisible) }, [isXanga]) - const persistSetSidebarSide = (side: 'left' | 'right' | 'hide') => { + const persistSetSidebarSide = (side: SidebarSide) => { setSidebarSide(side) localStorage.setItem('xangaSidebarSide', side) + if (side !== 'hide') { + setLastVisibleSide(side) + localStorage.setItem('xangaSidebarLastVisibleSide', side) + } } + const restoreSidebar = () => persistSetSidebarSide(lastVisibleSide) + // Only provide context in Xanga mode, otherwise just render children if (!isXanga) { return <>{children} } return ( - + {children} ) diff --git a/components/xanga/XangaLayoutContext.tsx b/components/xanga/XangaLayoutContext.tsx index 1aa73d0..164a691 100644 --- a/components/xanga/XangaLayoutContext.tsx +++ b/components/xanga/XangaLayoutContext.tsx @@ -3,10 +3,15 @@ import { createContext, useContext } from 'react' export type SidebarSide = 'left' | 'right' | 'hide' +export type VisibleSidebarSide = 'left' | 'right' type XangaLayoutContextValue = { sidebarSide: SidebarSide setSidebarSide: (side: SidebarSide) => void + /** The side the sidebar was on before it was last hidden (defaults to 'right'). */ + lastVisibleSide: VisibleSidebarSide + /** Brings the sidebar back on whichever side it was on before it was hidden. */ + restoreSidebar: () => void } export const XangaLayoutContext = createContext(null)