'use client'; import React, { forwardRef } from 'react'; import { cn, Surface, useEffectiveSurface, surfaceClasses, focusRing, } from '../common'; import { tone as toneTokens, ToneKey } from '../tokens'; export interface PixelSidebarItemProps { id: string; label: string; icon?: React.ReactNode; badge?: { label: string; tone?: ToneKey }; href?: string; onSelect?: () => void; active?: boolean; nested?: PixelSidebarItemProps[]; } export interface PixelSidebarSectionProps { /** Canonical section label. */ label?: string; /** * @deprecated Use `label` instead. Retained as alias for one minor. */ title?: string; items: PixelSidebarItemProps[]; } export interface PixelSidebarProps extends React.HTMLAttributes { collapsible?: boolean; defaultCollapsed?: boolean; collapsed?: boolean; onCollapsedChange?: (next: boolean) => void; sections: PixelSidebarSectionProps[]; header?: React.ReactNode; footer?: React.ReactNode; surface?: Surface; } function SidebarBadge({ badge, surface, collapsed, }: { badge: NonNullable; surface: Surface; collapsed: boolean; }) { const s = surfaceClasses(surface); const t = toneTokens[badge.tone ?? 'neutral']; if (collapsed) return null; return ( {badge.label} ); } function SidebarItem({ item, surface, collapsed, depth, }: { item: PixelSidebarItemProps; surface: Surface; collapsed: boolean; depth: number; }) { const s = surfaceClasses(surface); const hasNested = !!item.nested && item.nested.length > 0; const onClick = () => { item.onSelect?.(); }; const isLink = !!item.href; const padLeft = depth === 0 ? 'pl-2' : depth === 1 ? 'pl-6' : 'pl-10'; const sharedClassName = cn( 'group relative flex w-full items-center gap-2 pr-2 py-2 text-xs outline-none', padLeft, s.font, s.radius, s.transition, focusRing, 'focus-visible:ring-retro-cyan/40', item.active ? cn('bg-retro-cyan/15 text-retro-cyan', toneTokens.cyan.border) : 'text-retro-text hover:bg-retro-surface/60', 'border border-transparent', collapsed && 'justify-center pr-0 pl-0', ); const inner = ( <> {item.icon && ( {item.icon} )} {item.label} {item.badge && ( )} ); return ( // Native
  • listitem semantics — role="none" would leave the parent // role="list" with zero owned listitems (axe: aria-required-children).
  • {isLink ? ( {inner} ) : ( )} {hasNested && !collapsed && (
      {item.nested!.map((child) => ( ))}
    )}
  • ); } export const PixelSidebar = forwardRef(function PixelSidebar( { collapsible, defaultCollapsed, collapsed: collapsedProp, onCollapsedChange, sections, header, footer, surface: surfaceProp, className, ...rest }, ref, ) { const surface = useEffectiveSurface(surfaceProp); const s = surfaceClasses(surface); const isControlled = collapsedProp !== undefined; const [internalCollapsed, setInternalCollapsed] = React.useState( defaultCollapsed ?? false, ); const collapsed = isControlled ? !!collapsedProp : internalCollapsed; const toggle = () => { const next = !collapsed; if (!isControlled) setInternalCollapsed(next); onCollapsedChange?.(next); }; return ( ); }); PixelSidebar.displayName = 'PixelSidebar';