Files
textbee/web/components/shared/theme-toggle.tsx
isra el 0ea85c05e0 feat: clean up app chrome, rebuild footer and fix sidebar overlap
Top bar (app-header):
- Removed the Contribute button. It stays reachable from the footer.
- Removed the theme selector; it moves to the sidebar footer.
- Removed the mobile sheet for signed-in users: it duplicated the bottom tab
  bar for navigation and the avatar menu for identity. Signed-out visitors
  keep it, since auth pages have no tab bar.
The header is now brand on the left, account on the right.

Footer:
- Rebuilt as a slim single bar. A logged-in user is already converted, so the
  app does not need the marketing site's multi-column link farm. It borrows
  that footer's visual language (muted surface, muted-to-foreground hovers,
  green status pill) so the two still read as one product.
- Fixed the "cut by the side nav" bug. The footer was rendered in
  (app)/layout.tsx as a sibling of <main>, so it spanned the full viewport
  while the dashboard's fixed sidebar painted over its left 240px, and the
  fixed mobile tab bar covered its bottom edge. It is now rendered per
  section, inside each content column, with the dashboard copy padded clear
  of the tab bar.

Theme control:
- Moved to the sidebar footer and restyled from a dropdown into a three-way
  Light/Dark/System segmented control, which now has room to show all three
  states at once.

Sidebar icon:
- Dashboard uses LayoutDashboard instead of Home, matching the icon the
  avatar menu already uses for the same destination.

E2e harness, two real fixes found while verifying:
- The suite ran against `next dev`, which compiles routes on demand, so
  parallel workers hitting cold routes timed out at random. Switched to a
  production build; the suite went from 1.3m to ~31s.
- The survey modal (a Math.random() coin flip) and the update-app prompt
  (whenever a mocked device reports an old version) could open over any page.
  A Radix dialog marks the rest of the page aria-hidden while open, so every
  getByRole query found nothing and a different test failed each run. Both are
  now suppressed deterministically in the session helper.
Full suite: 16/16 on four consecutive runs.

New chrome.spec.ts guards the footer geometry against both the sidebar and
the mobile tab bar, asserts the top bar stays reduced, and asserts the
sidebar theme control actually toggles the dark class.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 20:21:54 +03:00

55 lines
1.7 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
import { Sun, Moon, Laptop } from 'lucide-react'
import { cn } from '@/lib/utils'
const options = [
{ value: 'light', label: 'Light', icon: Sun },
{ value: 'dark', label: 'Dark', icon: Moon },
{ value: 'system', label: 'System', icon: Laptop },
] as const
// Three-way segmented control. It lives in the sidebar footer rather than the
// top bar, where it has room to show all three states at once instead of
// hiding them behind a dropdown.
export default function ThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
// The active theme is unknown during SSR, so selection is only rendered
// after mount to avoid a hydration mismatch.
useEffect(() => setMounted(true), [])
return (
<div
className='flex items-center gap-0.5 rounded-lg bg-muted p-0.5'
role='group'
aria-label='Color theme'
>
{options.map(({ value, label, icon: Icon }) => {
const active = mounted && theme === value
return (
<button
key={value}
type='button'
onClick={() => setTheme(value)}
aria-label={label}
aria-pressed={active}
title={label}
className={cn(
'flex flex-1 items-center justify-center rounded-md py-1.5 transition-colors',
active
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
<Icon className='h-4 w-4' />
</button>
)
})}
</div>
)
}