mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
The assumptions section is explicitly labelled "technical, free to change", and it held a requirement that AC3 and a mutant already test as a fact. Read literally, it invited splitting the write in two — reopening the very window in which markers move. The point now states the opposite: everything else in that section is free, this one is normative and lives in section 8.3. Issue: #220 User-Visible: no
283 lines
22 KiB
Markdown
283 lines
22 KiB
Markdown
# 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. Маркеры не двигаются: материализация вместо нового поля
|
||
|
||
Обязательное свойство одно: **ни один маркер не меняет `space` из-за изменения
|
||
порядка**. Достигается оно не хранением якоря, а тем, что перестановка делает
|
||
явной ту привязку, которая до неё держалась на позиции.
|
||
|
||
**Норматив.** В той же транзакции записи, что и новый порядок, каждый маркер,
|
||
чьё пространство сегодня разрешается через `firstSpaceId` — то есть у него нет
|
||
ни `area`, ведущей в пространство, ни собственного `space` — получает явное
|
||
`space`, равное тому пространству, в котором он находится **сейчас**, до
|
||
перестановки. После этого его размещение от порядка не зависит вовсе, и
|
||
`firstSpaceId` перестаёт быть для него значимым.
|
||
|
||
**Почему так, а не якорь в `settings`.** Первая редакция ТЗ предполагала хранить
|
||
id «первого» пространства отдельным полем. Ревью r1 (M2) справедливо указало,
|
||
что это новое поле конфигурации, которое §9 в том же документе отрицал.
|
||
Материализация решает ту же задачу без расширения схемы:
|
||
|
||
- новых полей нет — `CONFIG_SCHEMA`, `scripts/config-field-registry.mjs` и
|
||
`docs/CONFIG-COMPATIBILITY.md` не трогаются;
|
||
- запись идёт одним `config/set` с уже существующими полями маркеров;
|
||
- правка данных минимальна и сохраняет наблюдаемое состояние: маркер остаётся
|
||
ровно там, где пользователь его видел;
|
||
- откат не нужен: явное `space` — валидное и предпочтительное состояние,
|
||
которое карточка и так пишет при любом сохранении маркера.
|
||
|
||
**Граница.** Материализуются только маркеры, разрешавшиеся через
|
||
`firstSpaceId`. Маркеры с `area` или с уже заданным `space` не трогаются —
|
||
проверяется AC3.
|
||
|
||
Если исполнитель обнаружит случай, который материализация не покрывает
|
||
(например, маркер вообще не попал в текущую модель), это не повод возвращать
|
||
якорь молча: такой случай выносится в ревью как находка.
|
||
|
||
### 8.4. Навигация
|
||
|
||
`swipeTarget` продолжает работать по индексам — он получает уже
|
||
переупорядоченный `spaceIds`, поэтому изменений не требует. AC4 фиксирует, что
|
||
свайп идёт в новом порядке немедленно, без перезагрузки.
|
||
|
||
### 8.5. Предупреждение о числовом `floor`
|
||
|
||
После первой успешной перестановки в текущей сессии карточка показывает тост:
|
||
«Порядок пространств изменён. Если где-то этаж карточки задан номером, проверьте
|
||
такие панели». Показывается один раз за сессию, независимо от числа
|
||
перестановок, и не блокирует работу.
|
||
|
||
## 9. Данные, i18n, a11y, touch, privacy, security
|
||
|
||
**Touch editor: not exposed.** Классификация по `docs/TOUCH-SUPPORT.md` §153:
|
||
перетаскивание вкладок на сенсорных экранах не появляется вовсе — ни как
|
||
degraded-вариант, ни как долгое нажатие. Это решение владельца §4.1, а не
|
||
недоделка. Обоснование: вкладки живут во View, где переключение пространств —
|
||
fully supported на тач и release-blocking; любой жест на вкладке рискует съесть
|
||
тап. Порядок пространств меняется на десктопе, как и остальная работа с планом.
|
||
|
||
Safety floor §69 того же документа соблюдён по построению: на тач-устройстве
|
||
поведение вкладок не меняется ни на йоту, значит ни потери данных, ни случайной
|
||
мутации при мультитаче новая функция внести не может.
|
||
|
||
- **Данные:** порядок элементов массива `spaces` плюс материализация неявной
|
||
привязки маркеров (§8.3). Новых полей конфигурации не появляется, схема и
|
||
`CONFIG_SCHEMA` не меняются — см. §8.3 о том, почему выбран путь без нового
|
||
поля и что это значит для `docs/CONFIG-COMPATIBILITY.md`.
|
||
- **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`
|
||
для каждого маркера; такой маркер получает явное `space` в той же записи, а
|
||
маркеры с `area` или с уже заданным `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 |
|
||
| `reorder-skips-materialization` | записывать новый порядок, не материализуя привязку маркеров | юнит AC3 |
|
||
| `materialization-touches-bound-markers` | материализовать `space` и у маркеров с `area` | юнит 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. **Атомарность записи предположением не является.** Требование «порядок и
|
||
материализация уходят одним `config/set`» — норматив §8.3, его проверяет AC3
|
||
и стережёт мутант `reorder-skips-materialization`. Разносить запись на две
|
||
нельзя: между ними возникает окно, где порядок уже новый, а привязка ещё
|
||
старая — ровно тот риск, ради которого §8.3 написан. Здесь пункт оставлен
|
||
только как указатель: свободно меняется всё остальное в этом разделе, но не
|
||
это (находка M3 ревью r2; прежняя редакция §17 держала норматив под
|
||
заголовком «менять свободно»).
|
||
|
||
Историческая справка к §8.3: первая редакция предлагала хранить якорь
|
||
`firstSpaceId` в `settings` — отвергнута по M2 ревью r1 как новое поле
|
||
конфигурации.
|
||
4. **Тост о числовом `floor`** показывается всегда, а не только когда числовой
|
||
`floor` действительно где-то используется: карточка не видит чужие панели,
|
||
поэтому условие проверить нечем.
|