From 0fd2331d4acbbc5c9420e9928146e48ca9b676a5 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 20 Aug 2026 23:08:07 +0300 Subject: [PATCH] docs: spec for space tab reordering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/specs/220-space-tab-reorder.md | 235 ++++++++++++++++++++++++++++ docs/specs/README.md | 1 + 2 files changed, 236 insertions(+) create mode 100644 docs/specs/220-space-tab-reorder.md diff --git a/docs/specs/220-space-tab-reorder.md b/docs/specs/220-space-tab-reorder.md new file mode 100644 index 00000000..37875eb1 --- /dev/null +++ b/docs/specs/220-space-tab-reorder.md @@ -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` действительно где-то используется: карточка не видит чужие панели, + поэтому условие проверить нечем. diff --git a/docs/specs/README.md b/docs/specs/README.md index 7e8645fa..fb62ed82 100644 --- a/docs/specs/README.md +++ b/docs/specs/README.md @@ -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) |