- PlayerBarGlass: use semantic tokens (--player-glass-bg, --player-glass-border) - Replace arbitrary OKLCH with CSS vars; backdrop-blur-md; rounded-xl - Transitions: duration-[var(--duration-*)], ease-[var(--ease-out)] - Sidebar: add border-r border-[var(--sidebar-border)] for depth - Header: border-[var(--glass-border)] for subtle separation - index.css: add --player-glass-bg, --player-glass-border (light + dark) - visual baselines updated (0% diff Playwright) Co-authored-by: Cursor <cursoragent@cursor.com>
294 lines
12 KiB
TypeScript
294 lines
12 KiB
TypeScript
import React from 'react';
|
|
import { useLocation, Link } from 'react-router-dom';
|
|
import {
|
|
Home, Users, Disc, Radio, Settings, LogOut, ShoppingBag,
|
|
GraduationCap, BarChart2, Shield, Box, MessageSquare, Cloud,
|
|
Layers, Cpu, Heart, ListMusic, CreditCard, DollarSign, Terminal,
|
|
ChevronLeft, ChevronRight,
|
|
} from 'lucide-react';
|
|
import { NavItem } from '../../types';
|
|
import { useUIStore } from '@/stores/ui';
|
|
import { useSidebarNavigation } from '@/hooks/useSidebarNavigation';
|
|
import { cn } from '@/lib/utils';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Tooltip } from '@/components/ui/tooltip';
|
|
|
|
interface SidebarProps {
|
|
currentView?: string;
|
|
}
|
|
|
|
const navItems: { section: string; items: NavItem[] }[] = [
|
|
{
|
|
section: 'My Studio',
|
|
items: [
|
|
{ id: 'dashboard', label: 'Command Center', icon: <Home className="w-4 h-4" /> },
|
|
{ id: 'studio', label: 'Cloud Files', icon: <Cloud className="w-4 h-4" /> },
|
|
{ id: 'tracks', label: 'Projects', icon: <Layers className="w-4 h-4" /> },
|
|
{ id: 'gear', label: 'Gear Locker', icon: <Box className="w-4 h-4" /> },
|
|
{ id: 'analytics', label: 'Performance', icon: <BarChart2 className="w-4 h-4" /> },
|
|
],
|
|
},
|
|
{
|
|
section: 'Veza Network',
|
|
items: [
|
|
{ id: 'social', label: 'Community Feed', icon: <Users className="w-4 h-4" /> },
|
|
{ id: 'marketplace', label: 'Marketplace', icon: <ShoppingBag className="w-4 h-4" /> },
|
|
{ id: 'live', label: 'Live Sessions', icon: <Radio className="w-4 h-4" />, badge: 3 },
|
|
{ id: 'chat', label: 'Channels', icon: <MessageSquare className="w-4 h-4" />, badge: 12 },
|
|
{ id: 'education', label: 'Academy', icon: <GraduationCap className="w-4 h-4" /> },
|
|
],
|
|
},
|
|
{
|
|
section: 'Commerce',
|
|
items: [
|
|
{ id: 'sell', label: 'Seller Dashboard', icon: <DollarSign className="w-4 h-4" /> },
|
|
{ id: 'wishlist', label: 'Wishlist', icon: <Heart className="w-4 h-4" /> },
|
|
{ id: 'purchases', label: 'Purchases', icon: <CreditCard className="w-4 h-4" /> },
|
|
],
|
|
},
|
|
{
|
|
section: 'Library',
|
|
items: [
|
|
{ id: 'playlists', label: 'Playlists', icon: <ListMusic className="w-4 h-4" /> },
|
|
{ id: 'queue', label: 'Play Queue', icon: <Disc className="w-4 h-4" /> },
|
|
],
|
|
},
|
|
{
|
|
section: 'System',
|
|
items: [
|
|
{ id: 'developer', label: 'Developer API', icon: <Terminal className="w-4 h-4" /> },
|
|
{ id: 'admin', label: 'Admin Panel', icon: <Shield className="w-4 h-4" /> },
|
|
],
|
|
},
|
|
];
|
|
|
|
const routeMap: Record<string, string> = {
|
|
dashboard: '/dashboard', studio: '/library', tracks: '/library', gear: '/gear',
|
|
analytics: '/analytics', social: '/social', marketplace: '/marketplace', live: '/live',
|
|
chat: '/chat', education: '/education', sell: '/sell', wishlist: '/wishlist',
|
|
purchases: '/purchases', playlists: '/playlists', queue: '/queue', developer: '/developer',
|
|
admin: '/admin', settings: '/settings',
|
|
};
|
|
|
|
const navItemBaseClasses = cn(
|
|
'w-full flex items-center px-3 py-2 rounded-lg text-sm font-medium transition-all duration-[var(--duration-fast)] group relative',
|
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background'
|
|
);
|
|
|
|
const navItemInactiveClasses =
|
|
'text-muted-foreground hover:text-foreground hover:bg-sidebar-accent active:bg-sidebar-accent/80';
|
|
|
|
const navItemActiveClasses = 'bg-primary/10 text-primary sidebar-active-indicator';
|
|
|
|
export const Sidebar: React.FC<SidebarProps> = ({ currentView }) => {
|
|
const location = useLocation();
|
|
const { sidebarOpen, setSidebarOpen } = useUIStore();
|
|
const { handleMobileNav, handleLogout } = useSidebarNavigation();
|
|
|
|
const activeView =
|
|
currentView ||
|
|
Object.keys(routeMap).find((key) => routeMap[key] === location.pathname) ||
|
|
'dashboard';
|
|
|
|
return (
|
|
<>
|
|
{sidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-background/80 backdrop-blur-sm lg:hidden z-sidebar-overlay"
|
|
onClick={() => setSidebarOpen(false)}
|
|
aria-hidden="true"
|
|
role="presentation"
|
|
/>
|
|
)}
|
|
|
|
<aside
|
|
data-testid="app-sidebar"
|
|
className={cn(
|
|
'fixed left-sidebar bottom-sidebar top-sidebar rounded-xl flex flex-col transition-shell z-sidebar overflow-hidden',
|
|
'bg-[var(--sidebar)] backdrop-blur-md border-r border-[var(--sidebar-border)]',
|
|
sidebarOpen ? 'w-sidebar-expanded translate-x-0 opacity-100' : '-translate-x-full lg:translate-x-0 lg:opacity-100 lg:w-sidebar-collapsed'
|
|
)}
|
|
aria-label="Main sidebar"
|
|
>
|
|
{/* Header — minimal Spotify-style */}
|
|
<div className="px-4 py-4 flex items-center gap-3 relative">
|
|
<div className="w-8 h-8 rounded-lg bg-sidebar-accent flex items-center justify-center flex-shrink-0">
|
|
<Cpu className="w-4 h-4 text-muted-foreground" />
|
|
</div>
|
|
|
|
<div className={cn('transition-shell overflow-hidden min-w-0', sidebarOpen ? 'opacity-100' : 'w-0 opacity-0')}>
|
|
<h2 className="text-sm font-semibold text-foreground truncate">
|
|
System Hub
|
|
</h2>
|
|
<div className="flex items-center gap-1.5 mt-0.5">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-primary shrink-0 animate-pulse" aria-hidden="true" />
|
|
<span className="text-xs text-muted-foreground truncate">
|
|
Online
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
className={cn(
|
|
'ml-auto text-muted-foreground hover:text-foreground hidden lg:flex hover:bg-sidebar-accent',
|
|
!sidebarOpen && 'absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2'
|
|
)}
|
|
aria-label={sidebarOpen ? 'Collapse sidebar' : 'Expand sidebar'}
|
|
>
|
|
{sidebarOpen ? <ChevronLeft className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Nav — Discord/Spotify: pill indicator, micro-animations, section dividers */}
|
|
<nav
|
|
className="flex-1 overflow-y-auto custom-scrollbar px-3 py-2"
|
|
role="navigation"
|
|
aria-label="Main navigation"
|
|
>
|
|
{navItems.map((group, idx) => (
|
|
<div key={group.section}>
|
|
{/* Section divider between groups */}
|
|
{idx > 0 && (
|
|
<div
|
|
className={cn(
|
|
'h-px bg-border/50 mx-3 my-1.5 transition-opacity duration-[var(--duration-normal)]',
|
|
!sidebarOpen && 'mx-1'
|
|
)}
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
|
|
<h3
|
|
className={cn(
|
|
'text-xs font-medium text-muted-foreground mb-2 px-3 transition-all duration-[var(--duration-normal)] uppercase tracking-wider',
|
|
!sidebarOpen && 'opacity-0 h-0 overflow-hidden mb-0 px-0'
|
|
)}
|
|
id={`sidebar-section-${group.section.replace(/\s+/g, '-').toLowerCase()}`}
|
|
>
|
|
{group.section}
|
|
</h3>
|
|
|
|
<ul className="space-y-0.5 list-none m-0 p-0" aria-labelledby={`sidebar-section-${group.section.replace(/\s+/g, '-').toLowerCase()}`}>
|
|
{group.items.map((item) => {
|
|
const route = routeMap[item.id] || '/dashboard';
|
|
const isActive = activeView === item.id;
|
|
|
|
return (
|
|
<li key={item.id} className="list-none m-0 p-0">
|
|
<Tooltip content={item.label} position="right" disabled={sidebarOpen}>
|
|
<Link
|
|
to={route}
|
|
onClick={handleMobileNav}
|
|
aria-current={isActive ? 'page' : undefined}
|
|
className={cn(
|
|
navItemBaseClasses,
|
|
isActive ? navItemActiveClasses : navItemInactiveClasses,
|
|
!sidebarOpen && 'justify-center px-0'
|
|
)}
|
|
>
|
|
<div className={cn('flex items-center gap-3 relative z-10 min-w-0', !sidebarOpen && 'justify-center')}>
|
|
<span
|
|
className={cn(
|
|
'shrink-0 transition-all duration-[var(--duration-fast)]',
|
|
'group-hover:scale-110',
|
|
isActive ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'
|
|
)}
|
|
>
|
|
{item.icon}
|
|
</span>
|
|
|
|
<span
|
|
className={cn(
|
|
'transition-all duration-[var(--duration-normal)] whitespace-nowrap truncate',
|
|
sidebarOpen ? 'opacity-100' : 'w-0 opacity-0 overflow-hidden'
|
|
)}
|
|
>
|
|
{item.label}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Badge — expanded: colored pill */}
|
|
{item.badge != null && sidebarOpen && (
|
|
<span className="ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-primary/15 text-primary text-xs font-semibold tabular-nums shrink-0">
|
|
{item.badge}
|
|
</span>
|
|
)}
|
|
|
|
{/* Badge — collapsed: pinging dot */}
|
|
{item.badge != null && !sidebarOpen && (
|
|
<span className="absolute top-1.5 right-1.5 flex h-2 w-2" aria-hidden="true">
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
|
</span>
|
|
)}
|
|
</Link>
|
|
</Tooltip>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</nav>
|
|
|
|
{/* Footer */}
|
|
<div className="p-2 border-t border-sidebar-border space-y-0.5">
|
|
<Tooltip content="Settings" position="right" disabled={sidebarOpen}>
|
|
<Link
|
|
to="/settings"
|
|
onClick={handleMobileNav}
|
|
aria-current={activeView === 'settings' ? 'page' : undefined}
|
|
className={cn(
|
|
navItemBaseClasses,
|
|
activeView === 'settings' ? navItemActiveClasses : navItemInactiveClasses,
|
|
!sidebarOpen && 'justify-center px-0'
|
|
)}
|
|
>
|
|
<Settings
|
|
className={cn(
|
|
'w-4 h-4 shrink-0 transition-all duration-[var(--duration-fast)]',
|
|
'group-hover:scale-110',
|
|
activeView === 'settings' ? 'text-primary' : 'text-muted-foreground group-hover:text-foreground'
|
|
)}
|
|
/>
|
|
<span
|
|
className={cn(
|
|
'truncate transition-all duration-[var(--duration-normal)]',
|
|
sidebarOpen ? 'opacity-100' : 'w-0 opacity-0 overflow-hidden'
|
|
)}
|
|
>
|
|
Settings
|
|
</span>
|
|
</Link>
|
|
</Tooltip>
|
|
|
|
<Tooltip content="Sign Out" position="right" disabled={sidebarOpen}>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={handleLogout}
|
|
className={cn(
|
|
'w-full text-muted-foreground hover:text-destructive hover:bg-destructive/10 gap-3 justify-start rounded-lg group',
|
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
|
!sidebarOpen && 'justify-center px-0'
|
|
)}
|
|
aria-label="Sign out"
|
|
>
|
|
<LogOut className="w-4 h-4 shrink-0 transition-transform duration-[var(--duration-fast)] group-hover:scale-110" />
|
|
<span
|
|
className={cn(
|
|
'whitespace-nowrap transition-all duration-[var(--duration-normal)]',
|
|
sidebarOpen ? 'opacity-100' : 'w-0 opacity-0 overflow-hidden'
|
|
)}
|
|
>
|
|
Sign Out
|
|
</span>
|
|
</Button>
|
|
</Tooltip>
|
|
</div>
|
|
</aside>
|
|
</>
|
|
);
|
|
};
|