docs: spec for space tab reordering

Written on the owner's product decisions of 2026-08-20: mouse only and only in
the editor modes, one warning about the positional `floor` from #210, no
keyboard alternative.

The spec carries the part that is easy to miss — the order of `config.spaces`
is not decoration. It feeds the marker placement fallback, the swipe
neighbour and the numeric `floor`, so reordering tabs must not move a single
marker. That is a named acceptance criterion with a mutant behind it.

Issue: #220
User-Visible: no
This commit is contained in:
Codex
2026-08-20 23:08:07 +03:00
parent 7763af6b8e
commit 0fd2331d4a
2 changed files with 236 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
# Issue #220 — порядок пространств перетаскиванием вкладок
- **Issue:** https://github.com/Matysh/houseplan-card/issues/220
- **Связанные контракты:** #210 (фиксированный этаж), #170 (fallback-привязка маркера), #3 (комнаты без HA-зоны)
- **Тип:** feature, обычный полный трек
- **Приоритет:** P2
- **Пользовательское изменение:** да
## 1. Сценарий и персона
**Персона:** администратор плана — тот, кто заводит пространства и поддерживает
план в актуальном виде (`docs/SCOPE.md`, job J6).
**Сценарий:** дом рос не по порядку. Сначала завели «Квартиру», через месяц —
«Подвал», потом «Мансарду». Вкладки стоят в порядке создания, а читается дом
снизу вверх. Сегодня переставить их нечем: порядок вкладок — это порядок
массива `config.spaces`, и единственный способ его изменить — удалить
пространство и завести заново, потеряв планировку, устройства и привязки.
**Момент:** администратор в режиме редактора видит панель вкладок, берёт вкладку
мышью и перетаскивает на новое место.
## 2. Что человек увидит до и после
**До:** вкладки стоят в порядке заведения; изменить порядок невозможно.
**После:** в режимах редактора вкладку можно взять мышью и перетащить; во время
перетаскивания видно, куда она встанет; после отпускания порядок сохраняется и
переживает перезагрузку. Свайп между этажами и стрелки киоска идут в новом
порядке. Во View и на сенсорных экранах ничего не меняется: там вкладка
по-прежнему только переключает пространство.
## 3. Подтверждённая причина
Порядок вкладок задан порядком массива: панель рендерится прямым проходом по
модели (`houseplan-card.ts:15717`), `navigationSpaces.map(...)`, отдельного поля
сортировки нет. Запись идёт штатным `_writeConfig` (`:6692`) через
`houseplan/config/set` с `expected_rev`; изменение порядка — обычная правка
массива, схема и миграции не нужны.
**Порядок массива несёт смысл в трёх местах.** Проверено по коду dev:
| Место | Код | Что зависит |
|---|---|---|
| Fallback-пространство маркера | `houseplan-card.ts:3350`, `:4273` → `devices.ts:1058`, `:1063`, `:1249` | `firstSpaceId = _model[0]?.id`: маркеры без явного пространства садятся в первое |
| Свайп и карусель | `logic.ts:1870-1879` | `spaceIds[(i + 1) % n]` — сосед по индексу |
| `floor` числом (#210) | `houseplan-card.ts:1165` `_fixedFloorState` | числовое значение разрешается как позиция, есть ветвь `out-of-range-index` |
Первое — риск потери данных на ровном месте, второе и третье обязаны следовать
новому порядку либо предупреждать.
## 4. Продуктовые решения владельца (2026-08-20)
1. **Перетаскивание — только мышь и только в режимах редактора** (`plan`,
`devices`, `decor`). Во View и киоске поведение вкладок не меняется вовсе.
2. **Числовой `floor` из #210** — после успешной перестановки показать
предупреждение один раз.
3. **Клавиатурная альтернатива не нужна** — `docs/SCOPE.md` честно фиксирует
отсутствие клавиатурной навигации в редакторах.
## 5. Цели
- Порядок пространств меняется без потери данных и переживает перезагрузку.
- Ни один маркер не меняет своё размещение из-за перестановки.
- Навигация (свайп, карусель) следует новому порядку немедленно.
- Скрытая зависимость «позиция в массиве = смысл» становится явной и покрытой
тестами.
## 6. Scope
- Панель вкладок: обработчики `pointerdown`/`pointermove`/`pointerup` на `.tab`,
порог начала перетаскивания, индикатор места вставки, курсор.
- Запись нового порядка `config.spaces` через существующий `_writeConfig`.
- Развязка `firstSpaceId` с позицией в массиве (см. §8).
- Предупреждение о числовом `floor` (§4.2), ключи i18n en+ru.
- `docs/USER-GUIDE.md` и `docs/USER-GUIDE.ru.md`, оба changelog.
## 7. Не входит в задачу
- Перетаскивание на сенсорных экранах и во View — решение владельца §4.1.
- Клавиатурная альтернатива — §4.3.
- Порядок комнат, устройств, вложений, вкладок режимов (`plan`/`devices`/`decor`).
- Сортировка «по имени» и любые автоматические порядки.
- Изменение самого механизма `floor` из #210: числовая адресация остаётся как
есть, задача только предупреждает.
## 8. Контракт поведения
### 8.1. Перетаскивание
- Drag начинается только при `_canEdit`, не в киоске, `this._mode !== 'view'`,
`event.pointerType === 'mouse'` и после смещения ≥ 4 px по горизонтали.
До порога это обычный клик — переключение пространства сохраняется.
- Вкладка «+» (`.tabadd`) в перестановке не участвует и не может стать целью.
- Во время перетаскивания видно место вставки; при отпускании вне панели
порядок не меняется.
- Одно пространство — перетаскивать нечего, обработчики не навешиваются.
### 8.2. Запись
- Новый порядок пишется целиком массивом `spaces` штатным `_writeConfig`
с `expected_rev`; конфликт ревизий обрабатывается как у любой другой правки.
- Активное пространство после перестановки не меняется: перетащили не ту, что
открыта — открытая остаётся открытой; перетащили открытую — она открыта на
новом месте.
### 8.3. Развязка `firstSpaceId`
Fallback-пространство перестаёт зависеть от позиции. `buildDevices` получает
`firstSpaceId` не как «первый по порядку», а как **стабильный якорь**: id
пространства, которое было первым на момент первой сборки конфигурации, и
которое не меняется от перестановки вкладок.
Реализация — на усмотрение автора (§18), обязательное свойство одно: **ни один
маркер не меняет `space` из-за изменения порядка**. Это проверяется AC3 и
мутантом.
### 8.4. Навигация
`swipeTarget` продолжает работать по индексам — он получает уже
переупорядоченный `spaceIds`, поэтому изменений не требует. AC4 фиксирует, что
свайп идёт в новом порядке немедленно, без перезагрузки.
### 8.5. Предупреждение о числовом `floor`
После первой успешной перестановки в текущей сессии карточка показывает тост:
«Порядок пространств изменён. Если где-то этаж карточки задан номером, проверьте
такие панели». Показывается один раз за сессию, независимо от числа
перестановок, и не блокирует работу.
## 9. Данные, i18n, a11y, privacy, security
- **Данные:** только порядок элементов массива `spaces`. Ни одно поле не
добавляется и не удаляется; миграции и compatibility-полей нет.
- **i18n:** один новый ключ тоста (en + ru) и, при необходимости, `title`
вкладки в режиме редактора.
- **a11y:** без изменений — клавиатурной навигации в редакторах нет и не
обещано (`docs/SCOPE.md`).
- **privacy / security:** новых данных и путей записи нет.
## 10. Performance
Панель вкладок — единицы элементов. Обработчики навешиваются только в режимах
редактора и только для мыши. Влияния на бюджеты нет; performance-профили не
затрагиваются.
## 11. Риски
1. **Drag съедает клик.** Порог 4 px и проверка `pointerType` — единственное,
что отделяет перестановку от переключения. AC2 проверяет обе стороны.
2. **Маркеры уезжают.** Главный риск задачи: `firstSpaceId` сегодня буквально
«первый по порядку». Закрывается §8.3 + AC3 + мутант.
3. **Числовой `floor`.** Предупреждение — смягчение, а не защита: чужие панели
отсюда не видны. Записано в USER-GUIDE.
4. **Конкурентная правка.** Перестановка уходит с `expected_rev`; на конфликт
реагирует общий механизм — AC6.
## 12. Acceptance criteria
1. **AC1 — перестановка и запись.** Перетаскивание вкладки меняет её позицию;
порядок сохраняется на сервере и переживает перезагрузку страницы.
**Доказательство:** `smoke`.
2. **AC2 — клик не сломан.** Клик по вкладке без смещения переключает
пространство; смещение ≥ 4 px начинает перетаскивание и клик не срабатывает.
**Доказательство:** `smoke`.
3. **AC3 — маркеры не двигаются.** Конфигурация с маркером, чьё размещение
опирается на fallback-пространство, после перестановки даёт то же `space`
для каждого маркера. Тест красный до развязки §8.3.
**Доказательство:** `unit` (`buildDevices`).
4. **AC4 — навигация следует порядку.** После перестановки `swipeTarget`
возвращает нового соседа немедленно, без перезагрузки.
**Доказательство:** `unit` + `smoke`.
5. **AC5 — границы включения.** Обработчики не навешиваются во View, в киоске,
при `pointerType !== 'mouse'` и при единственном пространстве; вкладка «+»
не участвует. **Доказательство:** `unit` (чистая функция решения) + `smoke`
для View.
6. **AC6 — конкурентная правка.** Перестановка уходит с `expected_rev`;
конфликт ревизий не молчит и не теряет порядок.
**Доказательство:** `unit` либо `smoke` по существующему механизму.
7. **AC7 — предупреждение о числовом `floor`.** После первой перестановки в
сессии показан тост; повторные перестановки его не повторяют.
**Доказательство:** `smoke`.
8. **AC8 — release-артефакты.** Оба changelog, оба USER-GUIDE описывают
перестановку и её ограничения (мышь, редактор, числовой `floor`); `dist`,
demo и integration bundle идентичны друг другу.
**Доказательство:** diff + сверка копий бандла.
## 13. План автотестов
- `test/` — юниты на чистую функцию «можно ли начать перетаскивание» (AC5) и на
стабильность `firstSpaceId` в `buildDevices` (AC3); проверка `swipeTarget` на
переупорядоченном списке (AC4).
- `demo/smoke_space_tab_reorder.mjs` — новый смок: перестановка мышью, запись,
сохранение активного пространства, клик без смещения, отсутствие drag во View,
тост о числовом `floor` (AC1, AC2, AC4, AC5, AC7).
- Существующие смоки навигации и `smoke_fixed_floor` (#210) — прогон без правок.
## 14. Мутационный гейт (`scripts/mutation-gate.mjs`)
| id | Патч | Guard |
|---|---|---|
| `tab-reorder-not-persisted` | перестановка меняет только локальную модель, `_writeConfig` не зовётся | смок AC1 |
| `tab-reorder-eats-click` | убрать порог смещения — любой pointerdown начинает drag | смок AC2 |
| `first-space-follows-order` | вернуть `firstSpaceId = _model[0]?.id` | юнит AC3 |
| `tab-reorder-ignores-pointer-type` | снять проверку `pointerType === 'mouse'` | юнит AC5 |
## 15. Release-артефакты
- `docs/CHANGELOG.md` + `docs/CHANGELOG.ru.md` — `User-Visible: yes`;
- `docs/USER-GUIDE.md` + `docs/USER-GUIDE.ru.md` — раздел про панель вкладок:
перестановка мышью в редакторе, ограничение по тачу, оговорка про числовой
`floor`;
- golden не затрагивается: панель вкладок в матрице не участвует, визуальных
изменений в состоянии покоя нет;
- performance-профили не затрагиваются.
## 16. Откат
Порядок — обычные данные, обратной миграции не требуется: администратор
перетаскивает вкладки назад. Код откатывается снятием обработчиков; развязка
`firstSpaceId` (§8.3) остаётся полезной сама по себе и откату не подлежит.
## 17. Принятые предположения (техническое, менять свободно)
1. **Порог 4 px** взят как минимально заметное намеренное движение; точное
число не продуктовое решение и может быть изменено ревьюером.
2. **Механика вставки** — вставка перетаскиваемой вкладки перед той, над
серединой которой отпущена мышь. Альтернатива (обмен местами) отвергнута:
при переносе через несколько позиций она даёт неожиданный результат.
3. **Способ развязки `firstSpaceId`** (§8.3): предполагается хранить якорь в
`settings` при первой записи конфигурации и читать его вместо `_model[0]`.
Если автор найдёт способ без нового поля — свойство важнее реализации.
4. **Тост о числовом `floor`** показывается всегда, а не только когда числовой
`floor` действительно где-то используется: карточка не видит чужие панели,
поэтому условие проверить нечем.
+1
View File
@@ -60,6 +60,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| [#218](https://github.com/Matysh/houseplan-card/issues/218) Floating-point шум комнаты не гасит Glow пространства | [218-glow-floor-geometry.md](218-glow-floor-geometry.md) |
| [#219](https://github.com/Matysh/houseplan-card/issues/219) Единая палитра замков и glyph на оранжевых подложках | [219-lock-orange-palette.md](219-lock-orange-palette.md) |
| [#205](https://github.com/Matysh/houseplan-card/issues/205) Продолжение следа после короткой остановки пылесоса | [205-vacuum-trail-resume-grace.md](205-vacuum-trail-resume-grace.md) |
| [#220](https://github.com/Matysh/houseplan-card/issues/220) Порядок пространств перетаскиванием вкладок | [220-space-tab-reorder.md](220-space-tab-reorder.md) |
| [#226](https://github.com/Matysh/houseplan-card/issues/226) Entity-marker не дублируется родительским HA-устройством | [226-entity-parent-dedup.md](226-entity-parent-dedup.md) |
| [#223](https://github.com/Matysh/houseplan-card/issues/223) Optimize канонизирует координаты без floating-point шума | [223-optimize-coordinate-canonicalization.md](223-optimize-coordinate-canonicalization.md) |