Put settings in the sidebar and let the sidebar frost composite
The footer was a user card with the theme picker and log out buried in its dropdown. It is now two plain entries, Settings and Log Out, and the theme moved into the settings page as its own tab. The dark sidebar never matched the canvas because _canvas.tsx painted bg-card across the whole viewport underneath it, so the floating panel's bg-card/80 resolved to card over card and its gutter was card too. Nothing there should paint a surface; the panel now reads the same as the other frosted chrome. The superuser guard sliced the first three tabs, which silently changed meaning when a fourth arrived. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkmeRiyeYmVZqJVwuyHq9o
This commit is contained in:
co-authored by
Claude Opus 5
parent
ca7a16c8bc
commit
6b73ca3645
@@ -1,6 +1,6 @@
|
||||
import { Monitor, Moon, Sun } from "lucide-react"
|
||||
|
||||
import { type Theme, useTheme } from "@/components/theme-provider"
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -8,63 +8,6 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
type LucideIcon = React.FC<React.SVGProps<SVGSVGElement>>
|
||||
|
||||
const ICON_MAP: Record<Theme, LucideIcon> = {
|
||||
system: Monitor,
|
||||
light: Sun,
|
||||
dark: Moon,
|
||||
}
|
||||
|
||||
export const SidebarAppearance = () => {
|
||||
const { isMobile } = useSidebar()
|
||||
const { setTheme, theme } = useTheme()
|
||||
const Icon = ICON_MAP[theme]
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton tooltip="Appearance" data-testid="theme-button">
|
||||
<Icon className="size-4 text-muted-foreground" />
|
||||
<span>Appearance</span>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side={isMobile ? "top" : "right"}
|
||||
align="end"
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
data-testid="light-mode"
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
<Sun className="mr-2 h-4 w-4" />
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
data-testid="dark-mode"
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
<Moon className="mr-2 h-4 w-4" />
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
<Monitor className="mr-2 h-4 w-4" />
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export const Appearance = () => {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Home, Users, Workflow } from "lucide-react"
|
||||
import { Home, LogOut, Settings, Users, Workflow } from "lucide-react"
|
||||
|
||||
import { SidebarAppearance } from "@/components/Common/Appearance"
|
||||
import { Logo } from "@/components/Common/Logo"
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
} from "@/components/ui/sidebar"
|
||||
import useAuth from "@/hooks/useAuth"
|
||||
import { type Item, Main } from "./Main"
|
||||
import { User } from "./User"
|
||||
|
||||
const baseItems: Item[] = [
|
||||
{ icon: Home, title: "Dashboard", path: "/" },
|
||||
@@ -19,15 +17,20 @@ const baseItems: Item[] = [
|
||||
]
|
||||
|
||||
export function AppSidebar() {
|
||||
const { user: currentUser } = useAuth()
|
||||
const { user: currentUser, logout } = useAuth()
|
||||
|
||||
const items = currentUser?.is_superuser
|
||||
? [...baseItems, { icon: Users, title: "Admin", path: "/admin" }]
|
||||
: baseItems
|
||||
|
||||
const footerItems: Item[] = [
|
||||
{ icon: Settings, title: "Settings", path: "/settings" },
|
||||
{ icon: LogOut, title: "Log Out", onClick: logout },
|
||||
]
|
||||
|
||||
return (
|
||||
// Floating frosted chrome over the bg-card content region; see the root
|
||||
// DESIGN-GUIDELINES.md → Shells and → Overlay surfaces & content chips.
|
||||
// Floating frosted chrome over whatever surface the shell paints; see the
|
||||
// root DESIGN-GUIDELINES.md → Shells and → Overlay surfaces & content chips.
|
||||
<Sidebar
|
||||
collapsible="icon"
|
||||
variant="floating"
|
||||
@@ -47,9 +50,9 @@ export function AppSidebar() {
|
||||
<SidebarContent>
|
||||
<Main items={items} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarAppearance />
|
||||
<User user={currentUser} />
|
||||
{/* Main already pads horizontally; the footer only adds the bottom gap. */}
|
||||
<SidebarFooter className="px-0">
|
||||
<Main items={footerItems} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
|
||||
/** A nav entry is either a route (`path`) or an action (`onClick`), never both. */
|
||||
export type Item = {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
path: string
|
||||
path?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
interface MainProps {
|
||||
@@ -36,22 +38,31 @@ export function Main({ items }: MainProps) {
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{items.map((item) => {
|
||||
const isActive =
|
||||
item.path === "/"
|
||||
const path = item.path
|
||||
const isActive = path
|
||||
? path === "/"
|
||||
? currentPath === "/"
|
||||
: currentPath.startsWith(item.path)
|
||||
: currentPath.startsWith(path)
|
||||
: false
|
||||
const label = (
|
||||
<>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
tooltip={item.title}
|
||||
isActive={isActive}
|
||||
asChild
|
||||
asChild={!!path}
|
||||
onClick={() => {
|
||||
handleMenuClick()
|
||||
item.onClick?.()
|
||||
}}
|
||||
>
|
||||
<RouterLink to={item.path} onClick={handleMenuClick}>
|
||||
<item.icon />
|
||||
<span>{item.title}</span>
|
||||
</RouterLink>
|
||||
{path ? <RouterLink to={path}>{label}</RouterLink> : label}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { type LucideIcon, Monitor, Moon, Sun } from "lucide-react"
|
||||
|
||||
import { type Theme, useTheme } from "@/components/theme-provider"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const options: { value: Theme; title: string; icon: LucideIcon }[] = [
|
||||
{ value: "light", title: "Light", icon: Sun },
|
||||
{ value: "dark", title: "Dark", icon: Moon },
|
||||
{ value: "system", title: "System", icon: Monitor },
|
||||
]
|
||||
|
||||
const Appearance = () => {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<div className="max-w-md">
|
||||
<h3 className="text-lg font-semibold py-4">Appearance</h3>
|
||||
<p className="text-sm text-muted-foreground pb-4">
|
||||
Choose how Fluksio looks to you. System follows your device setting.
|
||||
</p>
|
||||
{/* The one segmented shape: a single border pill, transparent segments,
|
||||
bg-accent on the selected one (root DESIGN-GUIDELINES.md). */}
|
||||
<div
|
||||
data-testid="theme-button"
|
||||
className="flex w-fit items-center gap-1 rounded-full border border-border p-1"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
aria-pressed={theme === option.value}
|
||||
data-testid={`${option.value}-mode`}
|
||||
onClick={() => setTheme(option.value)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-full px-3 py-1.5 text-sm transition-colors",
|
||||
theme === option.value
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-muted-foreground hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
<option.icon className="size-4" />
|
||||
{option.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Appearance
|
||||
Reference in New Issue
Block a user