import { ChevronDown, ChevronUp, Eye, EyeOff, LayoutList, MoreHorizontal, MoreVertical, PanelTop, } from 'lucide-react'; import React, { createContext, useCallback, useContext, useState } from 'react'; import type { ToolbarItemPlacement } from '../../domain/toolbarItemLayout'; import { cn } from '../../lib/utils'; import { Button } from './button'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, } from './context-menu'; import { Popover, PopoverContent, PopoverTrigger } from './popover'; import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip'; const ToolbarOverflowCloseContext = createContext<(() => void) | null>(null); /** Close the parent ⋮ overflow menu after a leaf action (no-op outside overflow). */ export function useToolbarOverflowClose(): () => void { return useContext(ToolbarOverflowCloseContext) ?? (() => {}); } export type ToolbarCustomizeItem = { id: string; label: string; /** Icon shown before the item label in the customize menu. */ icon?: React.ReactNode; /** When true, hide option is disabled (locked items). */ locked?: boolean; /** When false, collapse is not offered (item is show/hide only). Default true. */ supportsCollapse?: boolean; }; export type ToolbarCustomizeContextMenuProps = { items: ToolbarCustomizeItem[]; placementOf: (id: string) => ToolbarItemPlacement; onSetPlacement: (id: string, placement: ToolbarItemPlacement) => unknown; onMove?: (id: string, direction: 'earlier' | 'later') => void; onReset: () => void; t: (key: string, params?: Record) => string; children: React.ReactNode; /** Optional className for the trigger wrapper. */ className?: string; /** Optional inline style for the trigger wrapper (chrome theming). */ style?: React.CSSProperties; /** Optional data-section marker for custom CSS / tests. */ dataSection?: string; /** When false, right-click is disabled (e.g. compact mode). Default true. */ enabled?: boolean; }; const PLACEMENT_LABEL_KEYS: Record = { show: 'toolbar.layout.show', collapse: 'toolbar.layout.collapse', hide: 'toolbar.layout.hide', }; const PLACEMENT_ICONS: Record = { show: , collapse: , hide: , }; /** * Right-click the toolbar region to configure each action as show / collapse / hide. * Optional move earlier/later reorders the full item list. */ export const ToolbarCustomizeContextMenu: React.FC = ({ items, placementOf, onSetPlacement, onMove, onReset, t, children, className, style, dataSection, enabled = true, }) => { if (!enabled || items.length === 0) { return (
{children}
); } return (
{children}
{t('toolbar.layout.customize')} {items.map((item, index) => { const placement = placementOf(item.id); return ( {item.icon ? ( {item.icon} ) : null} {item.label} {PLACEMENT_ICONS[placement]} {t(PLACEMENT_LABEL_KEYS[placement])} { if (value === 'show' || value === 'collapse' || value === 'hide') { if (value === 'collapse' && item.supportsCollapse === false) return; onSetPlacement(item.id, value); } }} > {t('toolbar.layout.show')} {item.supportsCollapse !== false && ( {t('toolbar.layout.collapse')} )} {t('toolbar.layout.hide')} {onMove && ( <> { e.preventDefault(); onMove(item.id, 'earlier'); }} > {t('toolbar.layout.moveEarlier')} { e.preventDefault(); onMove(item.id, 'later'); }} > {t('toolbar.layout.moveLater')} )} ); })} onReset()}>{t('toolbar.layout.reset')}
); }; export type ToolbarOverflowMenuProps = { /** When empty, the ⋮ trigger is not rendered. */ hasItems: boolean; label: string; children: React.ReactNode; /** Icon orientation; terminal uses vertical, sftp horizontal. */ orientation?: 'horizontal' | 'vertical'; buttonClassName?: string; contentClassName?: string; align?: 'start' | 'center' | 'end'; /** Optional access to the persistent trigger, e.g. for restoring focus after a nested dialog. */ triggerRef?: React.Ref; }; /** * ⋮ button that opens the collapsed-item region. Hidden when nothing is collapsed. * Uses controlled Popover so leaf actions can close via useToolbarOverflowClose(). * Nested portaled menus (encoding, bookmark list) use data-toolbar-nested-menu * to stay open while the nested panel is used. */ export const ToolbarOverflowMenu: React.FC = ({ hasItems, label, children, orientation = 'horizontal', buttonClassName, contentClassName, align = 'end', triggerRef, }) => { const [open, setOpen] = useState(false); const close = useCallback(() => setOpen(false), []); if (!hasItems) return null; const Icon = orientation === 'vertical' ? MoreVertical : MoreHorizontal; return ( {label} { const target = e.target as Element | null; if (target?.closest('[data-toolbar-nested-menu="true"]')) { e.preventDefault(); } }} onClick={(e) => { // Leaf clicks close; nested openers keep the menu for the child panel. const target = e.target as Element | null; if (!target) return; if (target.closest('[data-toolbar-overflow-keep-open="true"]')) return; if (target.closest('[data-toolbar-nested-menu="true"]')) return; if (target.closest('button, [role="menuitem"], a')) { // Defer so the leaf onClick still runs first. requestAnimationFrame(() => setOpen(false)); } }} > {children} ); };