fix(ui): put the theme switcher where it's actually visible — the sidebar

The previous commit added the theme button to shell/Header.tsx, which is
imported NOWHERE (dead component — the app shell renders its own top bar,
desktop has none at all), so the tester couldn't find any button.

New ThemeSwitcher: segmented Light / Mid / Dark control at the bottom of
the sidebar (desktop sidebar + mobile drawer, above the user box) —
always visible on every page. Dead Header.tsx deleted so the next change
doesn't land there again.
This commit is contained in:
2026-07-24 11:17:55 +02:00
parent 5233a65103
commit 3af68fcddc
3 changed files with 38 additions and 95 deletions
@@ -4,6 +4,7 @@ import { Fragment } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { XMarkIcon, ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline';
import Navigation from './Navigation';
import ThemeSwitcher from './ThemeSwitcher';
import { ShieldCheckIcon } from '@heroicons/react/24/solid';
import { UserInfo } from '../../../types';
import api from '../../../lib/api';
@@ -59,6 +60,7 @@ export default function Drawer({ sidebarOpen, setSidebarOpen, user }: DrawerProp
</div>
<Navigation />
<div className="mt-auto pb-4">
<ThemeSwitcher />
{user && (
<div className="mb-4 px-2 py-3 bg-gray-800/50 rounded-lg border border-gray-700/50">
<div className="flex items-center gap-3">
@@ -99,6 +101,7 @@ export default function Drawer({ sidebarOpen, setSidebarOpen, user }: DrawerProp
</div>
<Navigation />
<div className="mt-auto pb-4">
<ThemeSwitcher />
{user && (
<div className="mb-4 px-2 py-3 bg-gray-800/50 rounded-lg border border-gray-700/50">
<div className="flex items-center gap-3">
@@ -1,95 +0,0 @@
"use client";
import Link from 'next/link';
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import {
ArrowRightOnRectangleIcon,
Bars3Icon,
MoonIcon,
SunIcon,
UserCircleIcon,
} from '@heroicons/react/24/outline';
import { MoonIcon as MoonIconSolid } from '@heroicons/react/24/solid';
import { ChevronDownIcon } from '@heroicons/react/20/solid';
import useTheme from '@/hooks/useTheme';
import api from '@/lib/api';
interface HeaderProps {
setSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const Header = ({ setSidebarOpen }: HeaderProps) => {
const { theme, toggleTheme } = useTheme();
const router = useRouter();
const [user, setUser] = useState<{ name?: string, username?: string }>({ name: 'User' });
useEffect(() => {
api.get('/auth/me')
.then((res) => setUser(res.data))
.catch(() => setUser({ name: 'User' }));
}, []);
const handleSignOut = () => {
api.post('/auth/logout')
.catch(() => null)
.finally(() => router.push('/login'));
};
return (
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center justify-between border-b border-base-300 bg-base-100 px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8">
<div className="flex items-center gap-x-4">
<button
type="button"
className="-m-2.5 p-2.5 text-base-content lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</button>
<div className="h-6 w-px bg-base-300 lg:hidden" aria-hidden="true" />
</div>
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
<div className="relative flex flex-1"></div>
<div className="flex items-center gap-x-4 lg:gap-x-6">
{/* Theme cycle: light → mid (soft dark) → dark (full dark) */}
<button
type="button"
className="-m-2.5 p-2.5 flex items-center gap-1.5 text-base-content hover:text-primary"
onClick={toggleTheme}
title={`Theme: ${theme} — click to switch (light → mid → dark)`}
>
<span className="sr-only">Switch theme</span>
{theme === 'light' && <SunIcon className="h-6 w-6" aria-hidden="true" />}
{theme === 'mid' && <MoonIcon className="h-6 w-6" aria-hidden="true" />}
{theme === 'dark' && <MoonIconSolid className="h-6 w-6" aria-hidden="true" />}
<span className="hidden sm:inline text-xs font-mono uppercase text-base-content/60">{theme}</span>
</button>
<div className="dropdown dropdown-end">
<div role="button" tabIndex={0} className="flex items-center gap-x-1 cursor-pointer">
<span className="text-sm font-semibold leading-6 text-base-content font-mono">{user.username || user.name}</span>
<ChevronDownIcon className="h-5 w-5 text-base-content/50" aria-hidden="true" />
</div>
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li>
<Link href="/settings" className="flex items-center gap-x-2">
<UserCircleIcon className="h-4 w-4" /> Profile
</Link>
</li>
<li>
<button onClick={handleSignOut} className="flex items-center gap-x-2 text-error">
<ArrowRightOnRectangleIcon className="h-4 w-4" /> Logout
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
);
};
export default Header;
@@ -0,0 +1,35 @@
"use client";
import { SunIcon, MoonIcon } from '@heroicons/react/24/outline';
import { MoonIcon as MoonIconSolid } from '@heroicons/react/24/solid';
import useTheme, { Theme } from '@/hooks/useTheme';
// Segmented Light / Mid / Dark control for the (always dark) sidebar.
const OPTIONS: { value: Theme; label: string; Icon: typeof SunIcon }[] = [
{ value: 'light', label: 'Light', Icon: SunIcon },
{ value: 'mid', label: 'Mid', Icon: MoonIcon },
{ value: 'dark', label: 'Dark', Icon: MoonIconSolid },
];
export default function ThemeSwitcher() {
const { theme, setTheme } = useTheme();
return (
<div className="mb-3 flex rounded-lg bg-gray-800/60 border border-gray-700/50 p-1">
{OPTIONS.map(({ value, label, Icon }) => (
<button
key={value}
onClick={() => setTheme(value)}
title={`${label} theme`}
className={`flex-1 flex items-center justify-center gap-1 rounded-md px-2 py-1.5 text-[11px] font-mono uppercase tracking-wide transition-colors ${
theme === value
? 'bg-gray-700 text-white'
: 'text-gray-400 hover:text-white'
}`}
>
<Icon className="h-4 w-4" aria-hidden="true" />
{label}
</button>
))}
</div>
);
}