mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
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:
@@ -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` действительно где-то используется: карточка не видит чужие панели,
|
||||
поэтому условие проверить нечем.
|
||||
@@ -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) |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user