UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

108 lines (107 loc) 4.1 kB
/** * Shared shell behavior for collapsible sections and common UI affordances. It keeps repeated interaction patterns consistent across tool apps. */ const EXPAND_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 10l5 5 5-5z"></path></svg>'; const COLLAPSE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 14l5-5 5 5z"></path></svg>'; function syncExpandButton(toggleButton, expanded) { if (!toggleButton) { return; } const label = expanded ? 'Collapse' : 'Expand'; toggleButton.title = label; toggleButton.setAttribute('aria-label', label); toggleButton.innerHTML = expanded ? COLLAPSE_ICON : EXPAND_ICON; } function syncShellClasses(shell, expanded) { if (!shell) { return; } shell.classList.toggle('expanded', expanded); shell.classList.toggle('collapsed', !expanded); } export function createToolShellController(options) { const { shell, toggleButton, initialExpanded, onToggle, onScrollAfterExpand, onRender } = options; let isExpanded = initialExpanded; let scrollTrackedForCurrentExpand = false; const shouldTrackScroll = typeof onScrollAfterExpand === 'function'; const applyExpandedState = (nextExpanded) => { const wasExpanded = isExpanded; isExpanded = nextExpanded; syncShellClasses(shell, isExpanded); syncExpandButton(toggleButton, isExpanded); if (!wasExpanded && isExpanded) { scrollTrackedForCurrentExpand = false; } }; const toggle = () => { applyExpandedState(!isExpanded); onToggle?.(isExpanded); onRender?.(); }; const handleScroll = (event) => { if (!isExpanded || scrollTrackedForCurrentExpand) { return; } const targetNode = event?.target instanceof Node ? event.target : null; if (targetNode && shell && !shell.contains(targetNode)) { return; } scrollTrackedForCurrentExpand = true; onScrollAfterExpand?.(); }; applyExpandedState(isExpanded); toggleButton?.addEventListener('click', toggle); if (shouldTrackScroll) { shell?.addEventListener('scroll', handleScroll, { passive: true }); document.addEventListener('scroll', handleScroll, { passive: true, capture: true }); } return { getExpanded: () => isExpanded, setExpanded: applyExpandedState, toggle, dispose: () => { toggleButton?.removeEventListener('click', toggle); if (shouldTrackScroll) { shell?.removeEventListener('scroll', handleScroll); document.removeEventListener('scroll', handleScroll, true); } }, }; } export function createCompactRowShellController(options) { const { shell, compactRow, initialExpanded, onToggle, onScrollAfterExpand, onRender } = options; const controller = createToolShellController({ shell, toggleButton: null, initialExpanded, onToggle: (expanded) => { compactRow?.setAttribute('aria-expanded', String(expanded)); onToggle?.(expanded); }, onScrollAfterExpand, onRender, }); compactRow?.setAttribute('aria-expanded', String(initialExpanded)); const handleCompactClick = () => { controller.toggle(); }; const handleCompactKeydown = (event) => { if (event.key !== 'Enter' && event.key !== ' ') { return; } event.preventDefault(); controller.toggle(); }; compactRow?.addEventListener('click', handleCompactClick); compactRow?.addEventListener('keydown', handleCompactKeydown); return { getExpanded: controller.getExpanded, setExpanded: controller.setExpanded, toggle: controller.toggle, dispose: () => { compactRow?.removeEventListener('click', handleCompactClick); compactRow?.removeEventListener('keydown', handleCompactKeydown); controller.dispose(); }, }; }