@wonderwhy-er/desktop-commander
Version:
MCP server for terminal operations and file editing
108 lines (107 loc) • 4.1 kB
JavaScript
/**
* 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();
},
};
}