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

View File

@ -6,6 +6,20 @@ import {UserProvider} from '@/contexts';
import {ThemeProvider} from '@/contexts/theme-context'; import {ThemeProvider} from '@/contexts/theme-context';
import './index.css'; import './index.css';
import App from './App.tsx'; 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(); const queryClient = new QueryClient();