feat(tiles): drag a tile by its header to move it

Owner request: "give me the option to move the tiles around". A tile's
header (its free area, not the buttons or the rename input) is now a
native drag handle: dropped on another tile the two trade places, dropped
on an empty slot it moves there (the last cell; the tiles after it close
up). Escape or a drop anywhere else is the browser's own cancel and
moves nothing.

Every move goes through one path, _reorderTiles: the header drag, a
tab of a tiled session dropped on a tile or a slot, and (next commit)
the Move Tile chords. Nothing is remounted, reconnected or reloaded.
Divider sizes belong to the cells, so a moved tile takes its new
cell's size: each tile whose cell size changed fits once (one PTY
resize, #464) and every other tile is left alone, in place of the
debounced refit of every tile the swap used to schedule.

The drag reuses the tab drop targets (capture phase, stopped before
xterm), carries a type of its own and never text, and is not
draggedTabId, so neither a text field nor the tab strip takes it.
Moving is off while a tile is zoomed (draggable off, and a tab drag
of a tiled session onto the zoomed tile is refused too) and with a
single tile. The handle shows a grab cursor and says it drags in its
tooltip (zh-CN included); the dragged tile is dimmed and the target
highlighted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 23:33:16 +02:00
parent 9209922ea4
commit 2b20288ca0
6 changed files with 628 additions and 39 deletions
+20 -1
View File
@@ -133,6 +133,8 @@
'Restore the grid': '恢复平铺网格',
'Remove tile (the session keeps running)': '移除窗格(会话继续运行)',
'Drop a tab here': '将标签页拖放到此处',
// A tile header's tooltip while tiles can move (with the state above it: a pattern below).
'Drag to move the tile': '拖动可移动窗格',
'Resize tile columns': '调整窗格列宽',
'Resize tile rows': '调整窗格行高',
Attach: '附加',
@@ -1012,6 +1014,17 @@
return value.replace(/\{([a-zA-Z][\w]*)\}/g, (_match, key) => String(variables[key] ?? ''));
}
// The six-state words of a tile header's tooltip (tile-grid.js _paintTileHandle).
const TILE_STATE_ZH = {
'needs you': '需要你',
error: '错误',
waiting: '等待中',
working: '工作中',
idle: '空闲',
done: '已完成',
exited: '已退出',
};
function translateDynamic(source) {
const patterns = [
[/^(\d+) tokens?$/, (_m, count) => `${count} 个 Token`],
@@ -1087,8 +1100,14 @@
// state strings on other surfaces (see mobile-overview.js).
[
/^(needs you|error|waiting|working|idle|done|exited) (<1m|\d+[dhm](?: \d+[hm])?)$/,
(_m, state, duration) => `${TILE_STATE_ZH[state]} ${duration}`,
],
// The same while tiles can move, with the drag hint on a second line.
// Anchored on the hint, so a bare state word is safe here.
[
/^(needs you|error|waiting|working|idle|done|exited)(?: (<1m|\d+[dhm](?: \d+[hm])?))?\nDrag to move the tile$/,
(_m, state, duration) =>
`${{ 'needs you': '需要你', error: '错误', waiting: '等待中', working: '工作中', idle: '空闲', done: '已完成', exited: '已退出' }[state]} ${duration}`,
`${TILE_STATE_ZH[state]}${duration ? ` ${duration}` : ''}\n${ZH_CN['Drag to move the tile']}`,
],
];
for (const [pattern, replacement] of patterns) {