feat(frontend): integrate ThemeSwitcher, restore custom palette on page load

This commit is contained in:
ZhenYi 2026-04-20 19:33:04 +08:00
parent ce29eb3062
commit 7736869fc4
2 changed files with 109 additions and 78 deletions

View File

@ -10,32 +10,47 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {BookOpen, Box, ChevronDown, Compass, Home, LayoutGrid, Monitor, Moon, Plus, Sun, Users} from 'lucide-react';
import {
BookOpen,
Box,
ChevronDown,
Compass,
Home,
LayoutGrid,
Monitor,
Moon,
Plus,
Sliders,
Sun,
Users
} from 'lucide-react';
import {useNavigate} from 'react-router-dom';
import {Avatar, AvatarFallback, AvatarImage} from '@/components/ui/avatar';
import {useState} from 'react';
import {ThemeSwitcher} from '@/components/room/ThemeSwitcher';
const btnClass = 'flex w-full h-9 justify-start items-center rounded-md font-medium hover:bg-muted cursor-pointer bg-transparent border-0 text-left text-sm';
export function SidebarSystem({collapsed}: {collapsed: boolean}) {
export function SidebarSystem({collapsed}: { collapsed: boolean }) {
const {theme, setTheme} = useTheme();
const navigate = useNavigate();
const workspaceCtx = tryUseWorkspace();
const workspaces = workspaceCtx?.workspaces;
const currentWorkspace = workspaceCtx?.currentWorkspace;
const [themeSheetOpen, setThemeSheetOpen] = useState(false);
return (
<div className="w-full">
{/* Workspace switcher — only shown when inside WorkspaceProvider */}
{workspaceCtx && (
<DropdownMenu>
<DropdownMenuTrigger
render={
<button
type="button"
className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')}
/>
}
>
<DropdownMenu>
<DropdownMenuTrigger
render={
<button
type="button"
className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')}
/>
}
>
<span className={cn('flex h-6 items-center shrink-0', collapsed ? 'w-6 justify-center' : 'w-6')}>
{currentWorkspace ? (
<Avatar className="h-4 w-4">
@ -48,70 +63,74 @@ export function SidebarSystem({collapsed}: {collapsed: boolean}) {
<LayoutGrid className="h-4 w-4"/>
)}
</span>
{!collapsed && (
<span className="flex-1 truncate text-sm leading-none">
{!collapsed && (
<span className="flex-1 truncate text-sm leading-none">
{currentWorkspace?.name || 'Workspaces'}
</span>
)}
{!collapsed && <ChevronDown className="h-3 w-3 ml-auto shrink-0"/>}
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="start" className="w-56">
<DropdownMenuGroup>
<DropdownMenuLabel>Workspaces</DropdownMenuLabel>
{workspaces?.workspaces.map((ws) => (
<DropdownMenuItem
key={ws.id}
onClick={() => navigate(`/w/${ws.slug}`)}
className="gap-2"
>
<Avatar className="h-5 w-5">
<AvatarImage src={ws.avatar_url || ''}/>
<AvatarFallback className="text-[9px]">
{ws.name.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="flex-1 truncate">{ws.name}</span>
<span className="text-xs text-muted-foreground">@{ws.slug}</span>
)}
{!collapsed && <ChevronDown className="h-3 w-3 ml-auto shrink-0"/>}
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="start" className="w-56">
<DropdownMenuGroup>
<DropdownMenuLabel>Workspaces</DropdownMenuLabel>
{workspaces?.workspaces.map((ws) => (
<DropdownMenuItem
key={ws.id}
onClick={() => navigate(`/w/${ws.slug}`)}
className="gap-2"
>
<Avatar className="h-5 w-5">
<AvatarImage src={ws.avatar_url || ''}/>
<AvatarFallback className="text-[9px]">
{ws.name.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="flex-1 truncate">{ws.name}</span>
<span className="text-xs text-muted-foreground">@{ws.slug}</span>
</DropdownMenuItem>
))}
<DropdownMenuSeparator/>
<DropdownMenuItem onClick={() => navigate('/w/me')} className="gap-2">
<Users className="h-4 w-4"/>
<span>All Workspaces</span>
</DropdownMenuItem>
))}
<DropdownMenuSeparator/>
<DropdownMenuItem onClick={() => navigate('/w/me')} className="gap-2">
<Users className="h-4 w-4"/>
<span>All Workspaces</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => navigate('/init/workspace')} className="gap-2">
<Plus className="h-4 w-4"/>
<span>Create Workspace</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenuItem onClick={() => navigate('/init/workspace')} className="gap-2">
<Plus className="h-4 w-4"/>
<span>Create Workspace</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
<button type="button" className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')} onClick={() => navigate('/')}>
<span className={cn('flex h-6 items-center shrink-0', collapsed ? 'w-6 justify-center' : 'w-6')}>
<button type="button" className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')}
onClick={() => navigate('/')}>
<span className={cn('flex h-6 w-6 items-center justify-center shrink-0')}>
<Home className="h-4 w-4"/>
</span>
{!collapsed && <span className="text-sm leading-none">Home</span>}
</button>
<button type="button" className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')} onClick={() => navigate('/explore')}>
<span className={cn('flex h-6 items-center shrink-0', collapsed ? 'w-6 justify-center' : 'w-6')}>
<Compass className="h-4 w-4" />
<button type="button" className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')}
onClick={() => navigate('/explore')}>
<span className={cn('flex h-6 w-6 items-center justify-center shrink-0')}>
<Compass className="h-4 w-4"/>
</span>
{!collapsed && <span className="text-sm leading-none">Explore</span>}
</button>
<button type="button" className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')} onClick={() => navigate('/market')}>
<span className={cn('flex h-6 items-center shrink-0', collapsed ? 'w-6 justify-center' : 'w-6')}>
<Box className="h-4 w-4" />
<button type="button" className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')}
onClick={() => navigate('/market')}>
<span className={cn('flex h-6 w-6 items-center justify-center shrink-0')}>
<Box className="h-4 w-4"/>
</span>
{!collapsed && <span className="text-sm leading-none">Marketplace</span>}
</button>
<button type="button" className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')} onClick={() => window.open('/docs', '_blank')}>
<span className={cn('flex h-6 items-center shrink-0', collapsed ? 'w-6 justify-center' : 'w-6')}>
<BookOpen className="h-4 w-4" />
<button type="button" className={cn(btnClass, collapsed ? 'justify-center px-0' : 'px-2')}
onClick={() => window.open('/docs', '_blank')}>
<span className={cn('flex h-6 w-6 items-center justify-center shrink-0')}>
<BookOpen className="h-4 w-4"/>
</span>
{!collapsed && <span className="text-sm leading-none">Docs</span>}
</button>
@ -125,13 +144,13 @@ export function SidebarSystem({collapsed}: {collapsed: boolean}) {
/>
}
>
<span className={cn('flex h-6 items-center shrink-0', collapsed ? 'w-6 justify-center' : 'w-6')}>
<span className={cn('flex h-6 w-6 items-center justify-center shrink-0')}>
{theme === 'dark' ? (
<Moon className="h-4 w-4" />
<Moon className="h-4 w-4"/>
) : theme === 'light' ? (
<Sun className="h-4 w-4" />
<Sun className="h-4 w-4"/>
) : (
<Monitor className="h-4 w-4" />
<Monitor className="h-4 w-4"/>
)}
</span>
{!collapsed && <span className="text-sm leading-none">Theme</span>}
@ -139,36 +158,34 @@ export function SidebarSystem({collapsed}: {collapsed: boolean}) {
<DropdownMenuContent side="right" align="start">
{!collapsed && (
<DropdownMenuGroup>
<DropdownMenuLabel>Theme Settings</DropdownMenuLabel>
<DropdownMenuLabel>Theme</DropdownMenuLabel>
<DropdownMenuItem onClick={() => setThemeSheetOpen(true)}>
<Sliders className="mr-2 h-4 w-4"/>
<span>Design System</span>
</DropdownMenuItem>
<DropdownMenuSeparator/>
<DropdownMenuItem onClick={() => setTheme('light')}>
<Sun className="mr-2 h-4 w-4" />
<Sun className="mr-2 h-4 w-4"/>
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<Moon className="mr-2 h-4 w-4" />
<Moon className="mr-2 h-4 w-4"/>
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<Monitor className="mr-2 h-4 w-4" />
<Monitor className="mr-2 h-4 w-4"/>
<span>System</span>
</DropdownMenuItem>
</DropdownMenuGroup>
)}
{collapsed && (
<>
<DropdownMenuItem onClick={() => setTheme('light')}>
<Sun className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<Moon className="mr-2 h-4 w-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<Monitor className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</>
<DropdownMenuItem onClick={() => setThemeSheetOpen(true)}>
<Sliders className="mr-2 h-4 w-4"/>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<ThemeSwitcher open={themeSheetOpen} onOpenChange={setThemeSheetOpen}/>
</div>
);
}
}

View File

@ -6,6 +6,20 @@ import {UserProvider} from '@/contexts';
import {ThemeProvider} from '@/contexts/theme-context';
import './index.css';
import App from './App.tsx';
import {applyPaletteToDOM, loadActivePresetId} from '@/components/room/design-system';
// Restore custom palette on page load (before first render)
const activePreset = loadActivePresetId();
if (activePreset === 'custom') {
const customPalette = localStorage.getItem('theme-custom-palette');
if (customPalette) {
try {
applyPaletteToDOM(JSON.parse(customPalette));
} catch {
// ignore malformed stored palette
}
}
}
const queryClient = new QueryClient();