feat(ui): add panelResize geometry for edge and corner resizing

Pure box arithmetic for resizing a floating panel from any of the eight
compass handles: the named edges follow the pointer, the opposite edge
stays pinned, and minimum size plus the viewport margin win. Owned by the
same boundary groups as PanelPositionControls, which wires it next.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Fable 5.1 committed 2026-09-23 14:45:54 +01:00
1 parent 0d41b6be54
commit 317c0a7e0c
4 files changed
+133

No files matched your search

+1
View File
@@ -147,6 +147,7 @@
"src/ui/panelRails.js",
"src/ui/leftPanelRail.js",
"src/ui/rightPanelRail.js",
"src/ui/panelResize.test.mjs",
"src/ui/panelMeasurement.js",
"src/ui/panelRailGeometry.js",
"src/ui/panelRails.test.mjs",
+3
View File
@@ -553,6 +553,7 @@
"src/ui/panelLayoutController.js",
"src/ui/panelMeasurement.js",
"src/ui/panelPositionControls.js",
"src/ui/panelResize.js",
"src/ui/panelRailGeometry.js",
"src/ui/panelRails.js",
"src/ui/radio.js",
@@ -1670,6 +1671,7 @@
"src/ui/panelLayoutController.js",
"src/ui/panelMeasurement.js",
"src/ui/panelPositionControls.js",
"src/ui/panelResize.js",
"src/ui/panelRailGeometry.js",
"src/ui/panelRails.js",
"src/ui/radio.js",
@@ -2327,6 +2329,7 @@
"src/ui/panelLayoutController.js",
"src/ui/panelMeasurement.js",
"src/ui/panelPositionControls.js",
"src/ui/panelResize.js",
"src/ui/panelRailGeometry.js",
"src/ui/panelRails.js",
"src/ui/rightPanelRail.js",
+65
View File
@@ -0,0 +1,65 @@
/**
* Pure geometry for resizing a floating panel from any edge or corner. The
* DOM wiring lives in PanelPositionControls; this module only decides boxes.
*/
/** Viewport margin a resized panel keeps clear on every side. */
const EDGE_MARGIN_PX = 6;
/** Resize directions: compass edges first, then corners. */
export const RESIZE_DIRECTIONS = ['n', 's', 'e', 'w', 'ne', 'nw', 'sw', 'se'];
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
/**
* Work out the box a resize gesture produces. Edges named in `dir` move with
* the pointer; the opposite edge stays put, so growing from the left keeps
* the right side pinned. Minimum size and the viewport margin win over the
* pointer.
* @param {{left: number, top: number, width: number, height: number}} box Box at gesture start.
* @param {string} dir One of RESIZE_DIRECTIONS.
* @param {number} dx Pointer travel since gesture start.
* @param {number} dy Pointer travel since gesture start.
* @param {object} [limits]
* @param {number} limits.minWidth
* @param {number} limits.minHeight
* @param {number} [limits.viewportWidth=Infinity]
* @param {number} [limits.viewportHeight=Infinity]
* @param {number} [limits.margin=6]
* @returns {{left: number, top: number, width: number, height: number}}
*/
export function resizeBox(
box,
dir,
dx,
dy,
{
minWidth,
minHeight,
viewportWidth = Infinity,
viewportHeight = Infinity,
margin = EDGE_MARGIN_PX,
} = {},
) {
let { left, top, width, height } = box;
const right = box.left + box.width;
const bottom = box.top + box.height;
if (dir.includes('e'))
width = clamp(box.width + dx, minWidth, viewportWidth - margin - box.left);
if (dir.includes('s'))
height = clamp(
box.height + dy,
minHeight,
viewportHeight - margin - box.top,
);
if (dir.includes('w')) {
width = clamp(box.width - dx, minWidth, right - margin);
left = right - width;
}
if (dir.includes('n')) {
height = clamp(box.height - dy, minHeight, bottom - margin);
top = bottom - height;
}
return { left, top, width, height };
}
+64
View File
@@ -0,0 +1,64 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resizeBox } from './panelResize.js';
const box = { left: 100, top: 100, width: 400, height: 300 };
const limits = {
minWidth: 200,
minHeight: 150,
viewportWidth: 1000,
viewportHeight: 800,
};
test('east and south edges grow away from the anchored corner', () => {
assert.deepEqual(resizeBox(box, 'se', 50, 20, limits), {
left: 100,
top: 100,
width: 450,
height: 320,
});
assert.deepEqual(resizeBox(box, 'e', -30, 999, limits), {
left: 100,
top: 100,
width: 370,
height: 300,
});
});
test('west and north edges keep the opposite edge pinned', () => {
assert.deepEqual(resizeBox(box, 'w', -40, 0, limits), {
left: 60,
top: 100,
width: 440,
height: 300,
});
assert.deepEqual(resizeBox(box, 'nw', 40, 30, limits), {
left: 140,
top: 130,
width: 360,
height: 270,
});
assert.deepEqual(
resizeBox(box, 'n', -50, 0, limits).top,
100,
'horizontal motion does not move a north edge',
);
});
test('minimum size stops the moving edge, not the anchored one', () => {
const small = resizeBox(box, 'w', 350, 0, limits);
assert.deepEqual(small, { left: 300, top: 100, width: 200, height: 300 });
assert.equal(resizeBox(box, 'ne', 0, 400, limits).height, 150);
assert.equal(resizeBox(box, 'ne', 0, 400, limits).top, 250);
});
test('the viewport margin caps growth in every direction', () => {
assert.equal(resizeBox(box, 'e', 2000, 0, limits).width, 1000 - 6 - 100);
assert.equal(resizeBox(box, 's', 0, 2000, limits).height, 800 - 6 - 100);
const west = resizeBox(box, 'w', -2000, 0, limits);
assert.equal(west.left, 6);
assert.equal(west.left + west.width, 500);
const north = resizeBox(box, 'n', 0, -2000, limits);
assert.equal(north.top, 6);
assert.equal(north.top + north.height, 400);
});