Files
ace-step-ui/components/Sidebar.tsx
T
fspecii 325046eaf2 Apply i18n to all components, add collapsible sidebar, playback speed control, inline title editing, and ConfirmDialog for deletions
Phase 4: i18n all 15 components from PR #19. Sidebar gains collapse/expand
toggle. Player gets playback speed selector (0.25x-2.0x) fixing bug B1
(hardcoded Chinese '正常'). SongList adds inline title editing and model
version badge. Delete actions now use ConfirmDialog instead of
window.confirm. Volume persisted to localStorage. Training nav item
deferred to Phase 7.
2026-02-08 18:35:09 +02:00

211 lines
8.4 KiB
TypeScript

import React from 'react';
import { Library, Disc, Search, User, LogIn, LogOut, Sun, Moon } from 'lucide-react';
import { View } from '../types';
import { useI18n } from '../context/I18nContext';
interface SidebarProps {
currentView: View;
onNavigate: (view: View) => void;
theme: 'light' | 'dark';
onToggleTheme: () => void;
user?: { username: string; isAdmin?: boolean; avatar_url?: string } | null;
onLogin?: () => void;
onLogout?: () => void;
onOpenSettings?: () => void;
isOpen?: boolean;
onToggle?: () => void;
}
export const Sidebar: React.FC<SidebarProps> = ({
currentView,
onNavigate,
theme,
onToggleTheme,
user,
onLogin,
onLogout,
onOpenSettings,
isOpen = true,
onToggle,
}) => {
const { t } = useI18n();
return (
<>
{/* Backdrop for mobile - only when expanded */}
{isOpen && onToggle && (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 md:hidden"
onClick={onToggle}
/>
)}
{/* Sidebar */}
<div className={`
flex flex-col h-full bg-white dark:bg-suno-sidebar border-r border-zinc-200 dark:border-white/5 flex-shrink-0 py-4 overflow-y-auto scrollbar-hide transition-all duration-300
fixed left-0 top-0 z-50 md:relative
${isOpen ? 'w-[200px]' : 'w-[72px]'}
`}>
{/* Logo & Brand */}
<div className="px-3 mb-8 flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-full bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center cursor-pointer shadow-lg hover:scale-105 transition-transform flex-shrink-0"
onClick={() => onNavigate('create')}
title={t('aceStepUI')}
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-white">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
{isOpen && (
<span className="text-lg font-bold text-zinc-900 dark:text-white whitespace-nowrap">ACE Step</span>
)}
</div>
{/* Collapse/Expand Button */}
{onToggle && (
<button
onClick={onToggle}
className="w-8 h-8 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 flex items-center justify-center text-zinc-500 dark:text-zinc-400 hover:text-black dark:hover:text-white transition-colors flex-shrink-0"
title={isOpen ? t('collapseSidebar') : t('expandSidebar')}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{isOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
)}
</svg>
</button>
)}
</div>
<nav className="flex-1 flex flex-col gap-2 w-full px-3">
<NavItem
icon={<Disc size={20} />}
label={t('create')}
active={currentView === 'create'}
onClick={() => onNavigate('create')}
isExpanded={isOpen}
/>
<NavItem
icon={<Library size={20} />}
label={t('library')}
active={currentView === 'library'}
onClick={() => onNavigate('library')}
isExpanded={isOpen}
/>
<NavItem
icon={<Search size={20} />}
label={t('search')}
active={currentView === 'search'}
onClick={() => onNavigate('search')}
isExpanded={isOpen}
/>
<div className="mt-auto flex flex-col gap-2">
{/* Theme Toggle */}
<button
onClick={onToggleTheme}
className={`
w-full rounded-xl flex items-center gap-3 transition-all duration-200 text-zinc-500 dark:text-zinc-400 hover:text-black dark:hover:text-white hover:bg-zinc-100 dark:hover:bg-white/5
${isOpen ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
`}
title={theme === 'dark' ? t('lightMode') : t('darkMode')}
>
<div className="flex-shrink-0">{theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />}</div>
{isOpen && (
<span className="text-sm font-medium whitespace-nowrap">
{theme === 'dark' ? t('lightMode') : t('darkMode')}
</span>
)}
</button>
{user ? (
<>
{/* User Settings */}
<button
onClick={onOpenSettings}
className={`
w-full rounded-xl flex items-center gap-3 transition-all duration-200 text-zinc-500 dark:text-zinc-400 hover:text-black dark:hover:text-white hover:bg-zinc-100 dark:hover:bg-white/5
${isOpen ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
`}
title={`${user.username} - ${t('settings')}`}
>
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center text-white text-xs font-bold border border-white/20 overflow-hidden flex-shrink-0">
{user.avatar_url ? (
<img src={user.avatar_url} alt={user.username} className="w-full h-full object-cover" />
) : (
user.username.charAt(0).toUpperCase()
)}
</div>
{isOpen && (
<span className="text-sm font-medium whitespace-nowrap truncate flex-1 text-left">
{user.username}
</span>
)}
</button>
{/* Logout */}
<button
onClick={onLogout}
className={`
w-full rounded-xl flex items-center gap-3 transition-all duration-200 text-zinc-500 hover:text-red-500 hover:bg-red-500/10
${isOpen ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
`}
title={t('signOut')}
>
<div className="flex-shrink-0"><LogOut size={20} /></div>
{isOpen && (
<span className="text-sm font-medium whitespace-nowrap">{t('signOut')}</span>
)}
</button>
</>
) : (
<button
onClick={onLogin}
className={`
w-full rounded-xl flex items-center gap-3 transition-all duration-200 text-zinc-500 dark:text-zinc-400 hover:text-pink-500 hover:bg-zinc-100 dark:hover:bg-white/5
${isOpen ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
`}
title={t('signIn')}
>
<div className="flex-shrink-0"><LogIn size={20} /></div>
{isOpen && (
<span className="text-sm font-medium whitespace-nowrap">{t('signIn')}</span>
)}
</button>
)}
</div>
</nav>
</div>
</>
);
};
interface NavItemProps {
icon: React.ReactNode;
label: string;
active?: boolean;
onClick: () => void;
isExpanded?: boolean;
}
const NavItem: React.FC<NavItemProps> = ({ icon, label, active, onClick, isExpanded }) => (
<button
onClick={onClick}
className={`
w-full rounded-xl flex items-center gap-3 transition-all duration-200 group relative overflow-hidden
${isExpanded ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
${active ? 'bg-zinc-100 dark:bg-white/10 text-black dark:text-white' : 'text-zinc-500 hover:text-black dark:hover:text-white hover:bg-zinc-100 dark:hover:bg-white/5'}
`}
title={label}
>
{active && <div className="absolute left-0 top-1/2 -translate-y-1/2 h-8 w-1 bg-pink-500 rounded-r-full"></div>}
<div className="flex-shrink-0">{icon}</div>
{isExpanded && (
<span className="text-sm font-medium whitespace-nowrap">{label}</span>
)}
</button>
);