SOURCE / PINNED RELEASE
Made of little things.
Black Site Map Editor
- Release
- 32a57733b7e9…
- Author-recorded commit
- 3f54890f542f…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/black-site-map-editor
Archive hash verified: 7a4b1abbac7db5b0…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Draggable, keyboard-accessible splitters for the workspace side panes.
// Widths live as --left-w / --right-w CSS variables on the workspace grid; the
// canvas ResizeObserver refits the map as the panes change size.
export interface ResizablePane {
/** Handle element mounted on an edge of the canvas pane. */
handle: HTMLElement;
/** Pane element whose grid column the handle controls. */
pane: HTMLElement;
/** Width in px matching the CSS fallback, used for resets. */
defaultWidth: number;
/** Smallest useful width in px. */
min: number;
/** Largest useful width in px. */
max: number;
}
export function mountPaneResizers(
workspace: HTMLElement,
sides: { left: ResizablePane; right: ResizablePane },
) {
const currentWidth = (side: ResizablePane) => side.pane.getBoundingClientRect().width;
const cssVariable = (side: ResizablePane) => (side === sides.left ? '--left-w' : '--right-w');
function setWidth(side: ResizablePane, width: number) {
const value = Math.round(Math.max(side.min, Math.min(side.max, width)));
workspace.style.setProperty(cssVariable(side), `${value}px`);
side.handle.setAttribute('aria-valuenow', String(value));
}
for (const side of [sides.left, sides.right]) {
const handle = side.handle;
// The left pane grows towards the right edge; the right pane the other way.
const growsWithRightwardDrag = side === sides.left;
let dragFrom: { width: number; x: number } | undefined;
const dragTo = (event: PointerEvent) => {
if (!dragFrom) return;
const delta = (event.clientX - dragFrom.x) * (growsWithRightwardDrag ? 1 : -1);
setWidth(side, dragFrom.width + delta);
};
const stop = (event: PointerEvent) => {
if (!handle.hasPointerCapture(event.pointerId)) return;
dragFrom = undefined;
handle.releasePointerCapture(event.pointerId);
handle.classList.remove('dragging');
document.body.classList.remove('pane-resizing');
};
handle.setAttribute('aria-valuemin', String(side.min));
handle.setAttribute('aria-valuemax', String(side.max));
handle.setAttribute('aria-valuenow', String(currentWidth(side) || side.defaultWidth));
handle.addEventListener('pointerdown', (event) => {
if (event.button !== 0) return;
event.preventDefault();
dragFrom = { width: currentWidth(side), x: event.clientX };
handle.setPointerCapture(event.pointerId);
handle.classList.add('dragging');
document.body.classList.add('pane-resizing');
});
handle.addEventListener('pointermove', (event) => {
if (handle.hasPointerCapture(event.pointerId)) dragTo(event);
});
handle.addEventListener('pointerup', stop);
handle.addEventListener('pointercancel', stop);
handle.addEventListener('lostpointercapture', () => {
dragFrom = undefined;
handle.classList.remove('dragging');
document.body.classList.remove('pane-resizing');
});
handle.addEventListener('dblclick', () => setWidth(side, side.defaultWidth));
handle.addEventListener('keydown', (event) => {
if (event.key === 'Home') {
event.preventDefault();
setWidth(side, side.defaultWidth);
return;
}
const step = event.shiftKey ? 48 : 16;
let delta = 0;
if (event.key === 'ArrowLeft') delta = growsWithRightwardDrag ? -step : step;
else if (event.key === 'ArrowRight') delta = growsWithRightwardDrag ? step : -step;
else return;
event.preventDefault();
setWidth(side, currentWidth(side) + delta);
});
}
}
