feat: reorder space tabs by dragging one to a new position

The order of config.spaces used to be whatever order the spaces were created
in, and there was no way back other than deleting a space and drawing it
again.

The gesture is deliberately narrow — mouse, editors only. The same tabs are
the primary way to switch spaces in View, where touch is first class, so a
drag there would compete with the tap that switches. Recorded in the spec as
"Touch editor: not exposed".

The part that needed care is not the drag. Position in the array feeds three
things: the marker placement fallback, the swipe neighbour and a positional
`floor`. So the write that stores the new order also writes down the
placement that used to depend on it: a marker with neither an explicit space
nor an area that names one gets the space it has right now. Both changes go in
one save; splitting them would leave a window in which markers move on their
own. The positional `floor` cannot be fixed from here, so the card says so
once.

Issue: #220
User-Visible: yes
This commit is contained in:
Codex
2026-08-21 00:27:35 +03:00
parent 30bf18f508
commit 8369c0e839
16 changed files with 718 additions and 128 deletions
+44
View File
@@ -717,6 +717,50 @@ export const MUTANTS = [
replace: ' .dev:not(.unavail):hover {',
}],
},
{
id: 'tab-reorder-not-persisted',
guard: 'node demo/smoke_space_tab_reorder.mjs',
because: 'перестановка вкладок, оставшаяся только в памяти, выглядит рабочей ровно до '
+ 'перезагрузки страницы — смок обязан требовать запись на сервер',
patches: [{
file: 'src/houseplan-card.ts',
find: ' cfg.spaces = applySpaceOrder(cfg.spaces || [], order);\n this._saveConfig();',
replace: ' cfg.spaces = applySpaceOrder(cfg.spaces || [], order);',
}],
},
{
id: 'reorder-skips-materialization',
guard: 'node demo/smoke_space_tab_reorder.mjs',
because: 'без материализации привязки маркер без space и area уезжает вслед за порядком: '
+ 'его пространство решает firstSpaceId, а тот после перестановки другой (#220 §8.3)',
patches: [{
file: 'src/houseplan-card.ts',
find: ' const byId = new Map(pinned.map((entry) => [entry.id, entry.space]));',
replace: ' const byId = new Map();',
}],
},
{
id: 'tab-reorder-eats-click',
guard: 'node --test --test-name-pattern="issue 220" test/space-order.test.mjs',
because: 'нулевой порог превращает обычный клик по вкладке в перетаскивание, и '
+ 'переключение пространств — основное действие панели — перестаёт работать',
patches: [{
file: 'src/space-order.ts',
find: 'export const TAB_DRAG_THRESHOLD_PX = 4;',
replace: 'export const TAB_DRAG_THRESHOLD_PX = 0;',
}],
},
{
id: 'tab-reorder-ignores-pointer-type',
guard: 'node --test --test-name-pattern="issue 220" test/space-order.test.mjs',
because: 'на тач-устройстве вкладки — это навигация View, где тап обязан оставаться тапом; '
+ 'решение владельца «Touch editor: not exposed» держится именно этой проверкой',
patches: [{
file: 'src/space-order.ts',
find: " if (ctx.pointerType !== 'mouse') return false;",
replace: ' if (false) return false;',
}],
},
{
id: 'internal-path-ignores-query',
guard: 'node scripts/backend-test-guard.mjs issue_225',