/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * The unified sidebar's activity bar (#1208). * * A registry-driven vertical icon rail on the viewport's right edge — the * evolution of the #1200 panel switcher. Icons follow the user's custom order * (`sidebarOrder`) and visible set (`sidebarHiddenIds`), cluster into groups * with dividers, highlight the active docked panel, and flag floating / popped * panels with a dot. The footer toggles customize mode, collapse, and a * layout menu. In customize mode every icon becomes drag-reorderable and * gains an eye toggle inline. */ import { useState } from 'react'; import { SlidersHorizontal, PanelRightClose, PanelRightOpen, EllipsisVertical, RotateCcw, Eye, SquareArrowOutUpRight, MonitorUp, } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { cn } from '@/lib/utils'; import { useViewerStore } from '@/store'; import { usePanelControls } from '@/hooks/usePanelControls'; import { WORKSPACE_PANELS, getPanelDef, type WorkspacePanelId } from '@/lib/panels/registry'; import { CustomizeSidebar } from './CustomizeSidebar'; /** Alt+N hint per panel, by registry index (frozen since #1200): 1-9, then 0. * Only the first ten registry entries get a shortcut; later additions (e.g. * Hierarchy, #1267) have none, so the map is limited to the first ten. */ const ALT_LABEL = new Map( WORKSPACE_PANELS.slice(0, 10).map((p, i) => [p.id, i < 9 ? `Alt+${i + 1}` : 'Alt+0']), ); export function ActivityBar() { const order = useViewerStore((s) => s.sidebarOrder); const hiddenIds = useViewerStore((s) => s.sidebarHiddenIds); const mode = useViewerStore((s) => s.sidebarMode); const customizing = useViewerStore((s) => s.sidebarCustomizing); const setSidebarMode = useViewerStore((s) => s.setSidebarMode); const setSidebarCustomizing = useViewerStore((s) => s.setSidebarCustomizing); const setPanelShownInSidebar = useViewerStore((s) => s.setPanelShownInSidebar); const reorder = useViewerStore((s) => s.reorderSidebarPanel); const resetLayout = useViewerStore((s) => s.resetSidebarLayout); const { isOpen, panelLocation, toggle, openInHome, floatPanel, popOutPanel, activePanel } = usePanelControls(); const hidden = new Set(hiddenIds); const [dragId, setDragId] = useState(null); const [overId, setOverId] = useState(null); // Hidden panels are removed from the rail in every mode (#1263), including // customize. Restoring a hidden panel happens in the Customize popover's // dedicated Hidden section, not by an inline greyed icon here. const visibleIds = order.filter((id) => !hidden.has(id) || id === 'properties'); const onIconClick = (id: WorkspacePanelId) => { const region = getPanelDef(id)?.region; // The left nav panel (Hierarchy, #1267) lives in its own slot, so just // toggle it; it never affects the right-pane sidebar mode. if (region === 'left') { toggle(id); return; } // Bottom-region panels (Script / Schedule / Lists) open in the bottom strip // — their own region — without touching the right-pane sidebar mode. if (region === 'bottom') { toggle(id); return; } if (mode === 'collapsed') { // Collapsed icons open + expand the right pane; they never toggle off. setSidebarMode('expanded'); openInHome(id); return; } toggle(id); }; let prevGroup: string | null = null; return (
{/* Panels */}
{visibleIds.map((id) => { const def = getPanelDef(id); if (!def) return null; const Icon = def.Icon; const loc = panelLocation(id); const active = loc === 'docked'; const open = isOpen(id); const showDivider = prevGroup !== null && def.group !== prevGroup; prevGroup = def.group; // Accessible name: the Radix tooltip is NOT the button's name, and // tooltips don't fire on touch / for many SR users, so set it // explicitly. In customize mode the action is "hide" (only shown // panels render here now, #1263). const ariaLabel = customizing ? `${def.title}, activate to hide from the sidebar` : `${def.title}${loc === 'floating' ? ' (floating)' : loc === 'popped' ? ' (popped out)' : ''}`; return (
{showDivider &&
} {def.title} {customizing ? ' · click to hide' : `${ALT_LABEL.get(id) ? ` · ${ALT_LABEL.get(id)}` : ''}${loc === 'floating' ? ' · floating' : loc === 'popped' ? ' · popped out' : ''}`}
); })}
{/* Footer controls */}
{customizing ? 'Done customizing' : 'Customize sidebar'} setSidebarMode(mode === 'collapsed' ? 'expanded' : 'collapsed')} > {mode === 'collapsed' ? : } Sidebar options setSidebarCustomizing(true)} className="gap-2"> Customize panels… {hiddenIds.length > 0 && ( hiddenIds.forEach((id) => setPanelShownInSidebar(id, true))} className="gap-2" > Show all panels ({hiddenIds.length} hidden) )} resetLayout()} className="gap-2"> Reset layout {/* Keyboard-accessible detach (the grip drag is mouse-only). */} floatPanel(activePanel)} className="gap-2"> Float current panel popOutPanel(activePanel)} className="gap-2"> Pop out to another screen
{customizing && setSidebarCustomizing(false)} />}
); } function FooterButton({ label, active, onClick, children, }: { label: string; active?: boolean; onClick: () => void; children: React.ReactNode; }) { return ( {label} ); }