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
22 KiB
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)
- Перетаскивание — только мышь и только в режимах редактора (
plan,devices,decor). Во View и киоске поведение вкладок не меняется вовсе. - Числовой
floorиз #210 — после успешной перестановки показать предупреждение один раз. - Клавиатурная альтернатива не нужна —
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. Риски
- Drag съедает клик. Порог 4 px и проверка
pointerType— единственное, что отделяет перестановку от переключения. AC2 проверяет обе стороны. - Маркеры уезжают. Главный риск задачи:
firstSpaceIdсегодня буквально «первый по порядку». Закрывается §8.3 + AC3 + мутант. - Числовой
floor. Предупреждение — смягчение, а не защита: чужие панели отсюда не видны. Записано в USER-GUIDE. - Конкурентная правка. Перестановка уходит с
expected_rev; на конфликт реагирует общий механизм — AC6.
12. Acceptance criteria
- AC1 — перестановка и запись. Перетаскивание вкладки меняет её позицию;
порядок сохраняется на сервере и переживает перезагрузку страницы.
Доказательство:
smoke. - AC2 — клик не сломан. Клик по вкладке без смещения переключает
пространство; смещение ≥ 4 px начинает перетаскивание и клик не срабатывает.
Доказательство:
smoke. - AC3 — маркеры не двигаются. Конфигурация с маркером, чьё размещение
опирается на fallback-пространство, после перестановки даёт то же
spaceдля каждого маркера; такой маркер получает явноеspaceв той же записи, а маркеры сareaили с уже заданнымspaceостаются побитово прежними. Тест красный до §8.3. Доказательство:unit(buildDevices+ запись). - AC4 — навигация следует порядку. После перестановки
swipeTargetвозвращает нового соседа немедленно, без перезагрузки. Доказательство:unit+smoke. - AC5 — границы включения. Обработчики не навешиваются во View, в киоске,
при
pointerType !== 'mouse'и при единственном пространстве; вкладка «+» не участвует. Доказательство:unit(чистая функция решения) +smokeдля View. - AC6 — конкурентная правка. Перестановка уходит с
expected_rev; конфликт ревизий не молчит и не теряет порядок. Доказательство:unitлибоsmokeпо существующему механизму. - AC7 — предупреждение о числовом
floor. После первой перестановки в сессии показан тост; повторные перестановки его не повторяют. Доказательство:smoke. - 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. Принятые предположения (техническое, менять свободно)
-
Порог 4 px взят как минимально заметное намеренное движение; точное число не продуктовое решение и может быть изменено ревьюером.
-
Механика вставки — вставка перетаскиваемой вкладки перед той, над серединой которой отпущена мышь. Альтернатива (обмен местами) отвергнута: при переносе через несколько позиций она даёт неожиданный результат.
-
Атомарность записи предположением не является. Требование «порядок и материализация уходят одним
config/set» — норматив §8.3, его проверяет AC3 и стережёт мутантreorder-skips-materialization. Разносить запись на две нельзя: между ними возникает окно, где порядок уже новый, а привязка ещё старая — ровно тот риск, ради которого §8.3 написан. Здесь пункт оставлен только как указатель: свободно меняется всё остальное в этом разделе, но не это (находка M3 ревью r2; прежняя редакция §17 держала норматив под заголовком «менять свободно»).Историческая справка к §8.3: первая редакция предлагала хранить якорь
firstSpaceIdвsettings— отвергнута по M2 ревью r1 как новое поле конфигурации. -
Тост о числовом
floorпоказывается всегда, а не только когда числовойfloorдействительно где-то используется: карточка не видит чужие панели, поэтому условие проверить нечем.