/* 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/. */ /** * Sidebar customizer popover (#1208, #1263). * * Lets the user reorder the activity bar (drag a row, or the up/down buttons) * and choose which panels appear in it, plus reset to defaults. A self-contained * NON-MODAL popover (role="group") anchored to the activity-bar footer: closes on * an outside click or Escape, moves focus into itself on open and restores it on * close. The same store actions back the inline drag in the activity bar, so the * two stay consistent. * * Hiding REMOVES a panel from the Shown list rather than greying it in place * (#1263): hidden panels move to a separate "Hidden" section whose only control * is Show (restore). So "hide" cleans the rail down to the tools you use, and * restoring is a distinct action, not an inline disabled state. */ import { useEffect, useRef, useState } from 'react'; import { GripVertical, Eye, EyeOff, RotateCcw, Lock, ChevronUp, ChevronDown, Plus } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useViewerStore } from '@/store'; import { getPanelDef, type WorkspacePanelId } from '@/lib/panels/registry'; export function CustomizeSidebar({ onClose }: { onClose: () => void }) { const order = useViewerStore((s) => s.sidebarOrder); const hiddenIds = useViewerStore((s) => s.sidebarHiddenIds); const reorder = useViewerStore((s) => s.reorderSidebarPanel); const setShown = useViewerStore((s) => s.setPanelShownInSidebar); const resetLayout = useViewerStore((s) => s.resetSidebarLayout); const hidden = new Set(hiddenIds); const ref = useRef(null); const restoreFocusRef = useRef(null); const [dragId, setDragId] = useState(null); const [overId, setOverId] = useState(null); // Shown panels keep their rail order; hidden ones live in their own section. const shownIds = order.filter((id) => !hidden.has(id)); const hiddenList = order.filter((id) => hidden.has(id)); // Move focus into the popover on open and restore it to the trigger on close. useEffect(() => { restoreFocusRef.current = (document.activeElement as HTMLElement) ?? null; ref.current?.focus(); return () => { try { restoreFocusRef.current?.focus?.(); } catch (err) { console.debug('[sidebar] focus restore skipped:', err); } }; }, []); // Close on outside click / Escape. The customize toggle button is excluded, // otherwise its own click would close the popover here (mousedown) and then // immediately reopen it (the button's click handler), so it could never close. useEffect(() => { const onDown = (e: MouseEvent) => { const target = e.target as HTMLElement; // Exclude the whole activity bar: its icons (drag-reorder / hide-toggle) // and the customize toggle are part of the same customize surface, so // interacting with them must not dismiss the popover. if ( ref.current && !ref.current.contains(target) && !target.closest('[data-activity-bar]') ) { onClose(); } }; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; // Defer the mousedown listener a tick so the click that opened us doesn't close us. const t = window.setTimeout(() => document.addEventListener('mousedown', onDown), 0); document.addEventListener('keydown', onKey); return () => { window.clearTimeout(t); document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); }; }, [onClose]); return (
Customize sidebar
{/* Shown: the panels in the rail, reorderable, each with a Hide control. */} {shownIds.map((id, index) => { const def = getPanelDef(id); if (!def) return null; const Icon = def.Icon; const locked = id === 'properties'; const prevShown = shownIds[index - 1]; const nextShown = shownIds[index + 1]; return (
setDragId(id)} onDragEnd={() => { setDragId(null); setOverId(null); }} onDragOver={(e) => { e.preventDefault(); if (overId !== id) setOverId(id); }} onDrop={() => { if (dragId && dragId !== id) reorder(dragId as WorkspacePanelId, order.indexOf(id)); setDragId(null); setOverId(null); }} className={cn( 'group flex items-center gap-1.5 px-2 py-1.5 mx-1 rounded-md cursor-grab active:cursor-grabbing', dragId === id && 'opacity-40', overId === id && dragId && dragId !== id && 'ring-1 ring-primary/60', )} > {def.title}
); })} {/* Hidden: removed from the rail; the only control is Show (restore). */} {hiddenList.length > 0 && ( <>
Hidden
{hiddenList.map((id) => { const def = getPanelDef(id); if (!def) return null; const Icon = def.Icon; return (
{def.title}
); })} )}
Drag a row to reorder. Hide moves a panel to Hidden; Show brings it back.
); }