feat(tiles): draggable column and row dividers

The grid now places every tile explicitly (grid-column / grid-row, reading
order) with a 6px divider track between columns and between rows, instead of
relying on DOM order and a gap. Track sizes are fractions (grid-template fr
values) that reset to equal whenever the column or row count changes.

Dragging a divider trades size between the two tracks either side, each kept
at the minimum tile size (the pure dragTrackFractions in constants.js, always
computed from the fractions the drag started with, so it cannot drift). The
affected tiles reflow locally at most once per animation frame, with no PTY
resize; each hears exactly one fit (one PTY resize) at pointer-up, and tiles
in other tracks hear nothing. Pointer capture keeps the drag on the divider,
the body locks the resize cursor and text selection for its duration, and
closing the grid or removing a tile mid-drag tears it down, as the split's
divider does. A zoomed grid shows no dividers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-06 17:59:16 +02:00
parent b4618bb853
commit 65e8271fbe
7 changed files with 518 additions and 11 deletions
+31
View File
@@ -1741,6 +1741,36 @@ function sanitizeTileGridState(raw, liveSessions, detachedIds) {
};
}
/**
* New track fractions after a divider drag (grid-template `fr` values): the two
* tracks either side of divider `index` trade `deltaPx` of size, each kept at
* least `minPx` (or half the pair, if the pair cannot give both the minimum).
* Every other track keeps its size. Computed from the fractions the drag
* STARTED with and the pointer's total travel, so a drag never drifts.
*
* @param {number[]} fr - the fractions when the drag started
* @param {number} index - the divider: between track `index` and `index + 1`
* @param {number} deltaPx - pointer travel since the drag started
* @param {number} totalPx - the size the tracks share (dividers and padding excluded)
* @param {number} minPx - the smallest a track may get
* @returns {number[]} new fractions, same length
*/
function dragTrackFractions(fr, index, deltaPx, totalPx, minPx) {
const out = fr.slice();
if (index < 0 || index + 1 >= fr.length || !(totalPx > 0)) return out;
const sum = fr.reduce((a, b) => a + b, 0);
if (!(sum > 0)) return out;
const a = (fr[index] / sum) * totalPx;
const b = (fr[index + 1] / sum) * totalPx;
const pair = a + b;
const lo = Math.min(minPx, pair / 2);
const hi = pair - lo;
const nextA = Math.min(Math.max(a + (Number(deltaPx) || 0), lo), hi);
out[index] = (nextA / totalPx) * sum;
out[index + 1] = ((pair - nextA) / totalPx) * sum;
return out;
}
/**
* The sessions the Tiles picker (and a tile's + menu) offers, in tab order:
* live ones only, never a session popped out to its own window (that window
@@ -2074,6 +2104,7 @@ if (typeof window !== 'undefined') {
tileGridCapacity,
sanitizeTileGridState,
buildTilePickerSessions,
dragTrackFractions,
tileNeighbor,
tileInDirection,
cycleTile,