2026-01-15 23:27:58 +00:00
|
|
|
import * as React from 'react';
|
|
|
|
|
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
import { Button } from './button';
|
|
|
|
|
import { Card } from './card';
|
|
|
|
|
|
|
|
|
|
export interface SidebarProps {
|
|
|
|
|
/**
|
|
|
|
|
* Content to display in the sidebar
|
|
|
|
|
*/
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Position of the sidebar
|
|
|
|
|
* @default 'left'
|
|
|
|
|
*/
|
|
|
|
|
position?: 'left' | 'right';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Width of the sidebar when open
|
|
|
|
|
* @default 'w-64'
|
|
|
|
|
*/
|
|
|
|
|
width?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether the sidebar is open
|
|
|
|
|
* @default true
|
|
|
|
|
*/
|
|
|
|
|
open?: boolean;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Callback when open state changes
|
|
|
|
|
*/
|
|
|
|
|
onOpenChange?: (open: boolean) => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether the sidebar is collapsible
|
|
|
|
|
* @default true
|
|
|
|
|
*/
|
|
|
|
|
collapsible?: boolean;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Title/header for the sidebar
|
|
|
|
|
*/
|
|
|
|
|
title?: React.ReactNode;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Optional icon to display next to the title
|
|
|
|
|
*/
|
|
|
|
|
icon?: React.ReactNode;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Additional CSS classes for the container
|
|
|
|
|
*/
|
|
|
|
|
className?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Additional CSS classes for the sidebar content
|
|
|
|
|
*/
|
|
|
|
|
contentClassName?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether to show a backdrop on mobile when open
|
|
|
|
|
* @default true
|
|
|
|
|
*/
|
|
|
|
|
showBackdrop?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sidebar - Reusable sidebar component for filters and content
|
|
|
|
|
*
|
|
|
|
|
* A generic sidebar component that can be used to display filters,
|
|
|
|
|
* additional content, or any sidebar-worthy information. Supports
|
|
|
|
|
* collapsible functionality and positioning.
|
|
|
|
|
*
|
|
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* <Sidebar title="Filters" position="left" collapsible>
|
|
|
|
|
* <div>Filter content here</div>
|
|
|
|
|
* </Sidebar>
|
|
|
|
|
* ```
|
|
|
|
|
*
|
|
|
|
|
* @example
|
|
|
|
|
* ```tsx
|
|
|
|
|
* <Sidebar
|
|
|
|
|
* title="Filters"
|
|
|
|
|
* icon={<Filter />}
|
|
|
|
|
* position="right"
|
|
|
|
|
* open={isOpen}
|
|
|
|
|
* onOpenChange={setIsOpen}
|
|
|
|
|
* width="w-80"
|
|
|
|
|
* >
|
|
|
|
|
* <FilterContent />
|
|
|
|
|
* </Sidebar>
|
|
|
|
|
* ```
|
|
|
|
|
*/
|
|
|
|
|
export function Sidebar({
|
|
|
|
|
children,
|
|
|
|
|
position = 'left',
|
|
|
|
|
width = 'w-64',
|
|
|
|
|
open: controlledOpen,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
collapsible = true,
|
|
|
|
|
title,
|
|
|
|
|
icon,
|
|
|
|
|
className,
|
|
|
|
|
contentClassName,
|
|
|
|
|
showBackdrop = true,
|
|
|
|
|
}: SidebarProps) {
|
|
|
|
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(true);
|
|
|
|
|
|
|
|
|
|
// Use controlled state if provided, otherwise use uncontrolled
|
|
|
|
|
const isOpen = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen;
|
|
|
|
|
|
|
|
|
|
const handleToggle = () => {
|
|
|
|
|
const newOpen = !isOpen;
|
|
|
|
|
if (controlledOpen === undefined) {
|
|
|
|
|
setUncontrolledOpen(newOpen);
|
|
|
|
|
}
|
|
|
|
|
onOpenChange?.(newOpen);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ChevronIcon = position === 'left' ? ChevronLeft : ChevronRight;
|
|
|
|
|
const isCollapsed = collapsible && !isOpen;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{/* Mobile Backdrop */}
|
|
|
|
|
{showBackdrop && isOpen && (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 bg-black/60 backdrop-blur-sm lg:hidden z-40"
|
|
|
|
|
onClick={handleToggle}
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<aside
|
|
|
|
|
className={cn(
|
feat(web): UI premium Discord/Spotify-like — tokens, shadows, focus, layout
Plan UI premium 6–8 semaines (design system, shell, Storybook, a11y):
- Design system: DESIGN_TOKENS.md, APP_SHELL.md, FULL_LAYOUT_PAGE.md. Single source
for layout/shell (index.css), shadows (design-system.css), durations/easing.
- Tokens: shadow-cover-depth, shadow-gold-glow, shadow-fab-glow; layout max-height
(max-h-layout-drawer, max-h-layout-panel, max-h-layout-list). All duration-200/300/500
replaced by --duration-fast/normal/slow. Arbitrary shadows replaced by token classes.
- Shell & player: Sidebar, Header, GlobalPlayer, MiniPlayer, PlayerQueue, PlayerControls,
AudioPlayer use tokens; focus-visible on Sidebar, PlayerQueue, DropdownMenuTrigger/Item,
TabsTrigger. Typography: text-[10px]/[9px] → text-xs where applicable.
- ESLint: no-restricted-syntax (warn) for w-/h-/rounded-/shadow-/text-/spacing arbitrary.
- Scripts: report-arbitrary-values.mjs, capture/compare/generate visual; visual-complete.spec.ts.
- Stories full layout: Dashboard, Playlists, Library, Settings, Profile in DashboardLayout.stories.
- .cursorrules + README: DESIGN_TOKENS, APP_SHELL, visual commands, no arbitrary without justification.
- apps/web/.gitignore: e2e test artifacts (test-results-visual, playwright-report-visual).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 16:15:58 +00:00
|
|
|
'flex flex-col transition-all duration-[var(--duration-normal)] ease-in-out',
|
2026-01-15 23:27:58 +00:00
|
|
|
position === 'left' ? 'border-r' : 'border-l',
|
2026-01-15 23:30:04 +00:00
|
|
|
'border-white/10 bg-kodo-ink/40 backdrop-blur-md rounded-xl',
|
|
|
|
|
isCollapsed ? 'w-0 overflow-hidden' : width,
|
2026-01-15 23:27:58 +00:00
|
|
|
className,
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
{(title || collapsible) && (
|
|
|
|
|
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
|
|
|
|
{title && (
|
|
|
|
|
<div className="flex items-center gap-2 flex-1">
|
aesthetic-improvements: reduce decorative cyan in UI components (80/20 rule, batch 9)
- LiveView: decorative chat message username color (text-kodo-cyan → text-kodo-steel)
- Sidebar: decorative section header icon (text-kodo-cyan → text-kodo-steel)
- CartView: decorative promo code link (text-kodo-cyan → text-kodo-steel)
- Total: ~3 files, ~3 instances replaced
- Preserved: Functional links (LoginPage register link, RegisterPage login link, LiveView streamer profile link, LiveView wallet link), design system variants (Spinner default variant, alert.tsx info variant, badge.tsx cyan variant, ErrorDisplay.tsx info variant, Toast.tsx info variant, Alert.tsx info variant - intentional design system options), semantic status indicators (PasswordStrengthIndicator strong password - semantic color), interactive states (radio-group.tsx focus/interaction, select.tsx selected option, dropdown-menu.tsx checked state), primary actions
- Action 11.3.1.3 in progress (ninth batch: UI components decorative elements)
2026-01-16 10:21:33 +00:00
|
|
|
{icon && <div className="text-kodo-steel">{icon}</div>}
|
2026-01-15 23:27:58 +00:00
|
|
|
<h3 className="text-sm font-semibold text-white">{title}</h3>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{collapsible && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={handleToggle}
|
|
|
|
|
className="h-8 w-8"
|
|
|
|
|
aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
|
|
|
|
|
>
|
|
|
|
|
<ChevronIcon className={cn(
|
|
|
|
|
'w-4 h-4 transition-transform',
|
|
|
|
|
isOpen && position === 'right' && 'rotate-180',
|
|
|
|
|
isOpen && position === 'left' && 'rotate-180',
|
|
|
|
|
)} />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex-1 overflow-y-auto custom-scrollbar',
|
|
|
|
|
isCollapsed && 'hidden',
|
|
|
|
|
contentClassName,
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SidebarCard - Sidebar with Card styling
|
|
|
|
|
*
|
|
|
|
|
* A sidebar variant that wraps content in a Card for consistent styling.
|
|
|
|
|
*/
|
|
|
|
|
export interface SidebarCardProps extends Omit<SidebarProps, 'children'> {
|
|
|
|
|
/**
|
|
|
|
|
* Content to display in the sidebar card
|
|
|
|
|
*/
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function SidebarCard({
|
|
|
|
|
children,
|
|
|
|
|
className,
|
|
|
|
|
contentClassName,
|
|
|
|
|
...sidebarProps
|
|
|
|
|
}: SidebarCardProps) {
|
|
|
|
|
return (
|
|
|
|
|
<Sidebar
|
|
|
|
|
className={cn('p-0', className)}
|
|
|
|
|
contentClassName={cn('p-4', contentClassName)}
|
|
|
|
|
{...sidebarProps}
|
|
|
|
|
>
|
|
|
|
|
<Card className="border-0 bg-transparent shadow-none">
|
|
|
|
|
{children}
|
|
|
|
|
</Card>
|
|
|
|
|
</Sidebar>
|
|
|
|
|
);
|
|
|
|
|
}
|