Files
houseplan-card/docs/superpowers/specs/2026-08-08-editor-context-overlay-design.md
T

77 KiB
Raw Blame History

HP-UX-11 — стабильная панель редактора и контекстная суб-панель

Статус: реализовано локально 2026-08-09; целевой прогон тестов отложен до следующего пре-релиза по принятому процессу проекта
Дата: 2026-08-08
Область: редактор плана, редактор устройств, редактор подложки и все будущие редакторы House Plan
Тип изменения: UX и архитектура клиентского интерфейса без изменения модели данных

1. Продуктовое решение

Основная панель каждого редактора содержит только постоянные инструменты и постоянные действия этого редактора. Выделение объекта, начало незавершённой операции или появление другого временного контекста не изменяет высоту основной панели.

Все действия и элементы управления, которые появляются только из-за текущего выделения или временного состояния, выводятся в единой контекстной суб-панели:

  • суб-панель появляется непосредственно под основной панелью;
  • располагается поверх рабочей области, а не в её layout-flow;
  • не уменьшает рабочую область;
  • не меняет масштаб, pan, viewBox и координатное преобразование плана;
  • имеет полупрозрачную поверхность с достаточной контрастностью;
  • исчезает вместе с контекстом;
  • используется одинаково во всех редакторах.

Та же поверхность закладывается как общий второй уровень editor chrome. В будущем одна постоянная кнопка основной панели сможет открывать в суб-панели семантически связанную группу инструментов или команд. Например, вместо нескольких соседних кнопок в основной панели остаётся один launcher группы, а её варианты раскрываются под панелью поверх сцены.

Таким образом, у суб-панели есть два равноправных режима вызова:

  1. автоматический контекст — появляется из-за selection/operation/tool state;
  2. явная группа — открывается пользователем по кнопке основной панели как компактное подменю второго уровня.

Это один компонент и один overlay-host, но не одна и та же модель взаимодействия: автоматический контекст не забирает focus, а явно открытая группа управляется как раскрываемая панель и возвращает focus своему launcher.

Кнопка «Закрыть редактор» остаётся постоянной частью основной панели, закрепляется у её логического правого края и не переносится вследствие появления контекстных действий.

Главный инвариант

В пределах одного открытого редактора и неизменной ширины карточки изменение выделения, активного объекта или промежуточного состояния операции не должно менять геометрию рабочей области.

Иными словами, до и после выделения объекта должны оставаться неизменными:

  • верхняя координата рабочей области;
  • её ширина и высота;
  • масштаб плана;
  • положение плана;
  • экранная позиция кнопки выхода из редактора;
  • соответствие экранных координат координатам плана.

2. UX-оценка предложения

Оценка: сильное и приоритетное UX-улучшение.

Проблема не косметическая. Текущая панель участвует в расчёте доступной высоты сцены. Когда «Свойства» и «Удалить» создают новую строку, рабочая область уменьшается, а пользователь видит сразу несколько нежелательных эффектов:

  1. выбранный объект визуально смещается под курсором;
  2. меняется доступный масштаб и композиция плана;
  3. интерфейс выглядит нестабильным;
  4. кнопка выхода перестаёт быть постоянным пространственным ориентиром;
  5. на пограничных ширинах одна небольшая кнопка может вызвать перенос всей группы;
  6. при последовательном выборе объектов с разным набором действий панель «дышит» и отвлекает от геометрической работы.

Предложенная суб-панель устраняет причину, а не маскирует её анимацией высоты. Цена решения умеренная: потребуется общий host-компонент и перенос существующих динамических действий, но модель плана и сами команды не меняются.

Расширение до явно открываемых групп дополнительно повышает ценность решения: вместо одноразового исправления двух кнопок появляется устойчивый второй уровень editor chrome. Это позволит разгружать основную панель без новых dropdown-компонентов в каждом редакторе. Главный UX-риск — чрезмерно спрятать инструменты, поэтому группы создаются только вручную для тесно связанных вариантов, имеют постоянное имя/chevron и не возникают автоматически из-за ширины экрана.

Что не следует выбирать вместо этого

Альтернатива Почему не рекомендуется
Плавно анимировать высоту существующей панели Дёрганье станет мягче, но рабочая область, масштаб и положение объектов всё равно будут изменяться
Всегда резервировать пустую вторую строку Устранит сдвиг, но постоянно отнимет полезную высоту у плана
Спрятать действия в меню … Сэкономит место, но ухудшит обнаруживаемость частых «Свойств» и «Удалить»
Показывать кнопки рядом с выбранным объектом Панель будет перекрывать геометрию, прыгать при pan/zoom и конфликтовать с маленькими объектами
Закрепить действия снизу карточки Слабее связывает действия с редактором и конфликтует с узкими/мобильными раскладками

3. Причина дефекта в текущей архитектуре

Сейчас .editbar является обычным flex-контейнером с flex-wrap: wrap. Контекстные кнопки добавляются в тот же DOM-flow, что и инструменты, параметры и кнопка закрытия.

editorchrome участвует в вертикальной компоновке карточки. Его фактическая высота отслеживается, а высота сцены рассчитывается с учётом этой величины. Поэтому перенос кнопок на следующую строку является не только изменением панели, но и изменением геометрии сцены.

Существующая анимация высоты оправдана при явном переходе между редакторами, когда меняется вся рабочая среда. Она не должна запускаться при выборе объекта внутри одного редактора.

4. Термины

  • Основная панель (primary editor toolbar) — постоянная панель инструментов активного редактора.
  • Постоянное действие — действие, доступное независимо от выделения и промежуточной операции: выбор инструмента, Undo/Redo, выход из редактора.
  • Контекстное действие — действие, смысл которого существует только при конкретном выделении или временном состоянии: «Свойства», «Удалить», «Сбросить незавершённый контур».
  • Контекстная суб-панель (context action overlay, далее — context tray) — плавающий контейнер над сценой для временных действий и параметров.
  • Вторичная панель (secondary editor panel) — общее продуктовое назначение context tray: автоматический контекст либо явно открытая группа.
  • Launcher группы — постоянная кнопка основной панели, открывающая семантически связанную группу второго уровня.
  • Group tray — явно открытый вариант вторичной панели с инструментами или командами launcher-а.
  • Параметры инструмента — элементы, которые относятся только к активному инструменту: толщина новой стены, заливка фигуры и подобные настройки.
  • Палитра инструмента — крупный временный набор вариантов, например палитра мебели.
  • Сцена — рабочая область плана под панелями редактора.
  • Контекст — текущее выделение, незавершённая операция, активный инструмент и доступные для них действия.

5. Цели

  1. Полностью исключить изменение геометрии сцены при выборе и снятии выбора.
  2. Сохранить кнопку выхода в постоянной позиции.
  3. Сделать связь действий с выбранным объектом очевидной.
  4. Ввести один масштабируемый паттерн для всех редакторов.
  5. Не допустить возврата проблемы при добавлении новых контекстных кнопок.
  6. Сохранить прямой доступ к частым контекстным действиям объекта; второй уровень использовать для осознанных групп инструментов, а не для сокрытия «Свойства» и «Удалить».
  7. Поддержать длинные RU/EN-подписи и узкие Lovelace-карточки без горизонтального скролла всей страницы.
  8. Сохранить доступность с клавиатуры и prefers-reduced-motion.
  9. Подготовить безопасную основу для группировки нескольких связанных кнопок под одним launcher без увеличения основной панели.

6. Не входит в задачу

  • изменение команд редактирования и их бизнес-логики;
  • изменение command stack, Undo/Redo или правил подтверждения удаления;
  • изменение схемы данных, миграций или backend API;
  • новая модель выделения нескольких объектов;
  • автоматическое перемещение выбранного объекта из-под суб-панели;
  • полноценный touch-паритет редакторов;
  • перенос действий режима просмотра в editor context tray;
  • изменение состава инструментов редакторов;
  • автоматическое сворачивание частых действий в меню ….
  • вложенные подменю третьего и последующих уровней;
  • автоматическая группировка несвязанных действий только ради экономии ширины;
  • превращение всех основных инструментов в скрытые группы без отдельного UX-решения.

7. Информационная архитектура панелей

7.1. Три уровня интерфейса

Уровень Содержимое Участвует в высоте сцены Может появляться динамически
Основная панель Инструменты редактора, Undo/Redo, постоянные настройки, выход Да Нет в пределах одного редактора
Контекстная суб-панель Действия выбранного объекта, временные команды, компактные параметры инструмента Нет Да
Крупная палитра Мебель и будущие большие каталоги вариантов Нет Да

Крупная палитра использует тот же overlay-host, но отдельный вариант компоновки. Она не должна превращать компактную строку «Свойства / Удалить» в высокий каталог и не должна участвовать в измерении header.

7.2. Нормативное правило для новых функций

Новый элемент нельзя добавлять в flow основной панели, если хотя бы одно из условий истинно:

  • он появляется только после выделения объекта;
  • он исчезает после снятия выделения;
  • он доступен только во время незавершённого жеста;
  • он относится только к одному временному типу контекста;
  • его появление потенциально меняет число строк основной панели.

Такой элемент обязан использовать context tray или tool-palette overlay.

Это правило распространяется не только на кнопки. Иначе тот же дефект вернётся через динамический input, checkbox, hint или picker. При этом постоянный контрол может оставаться в основной панели, если его наличие не зависит от выделения и он предусмотрен компоновкой активного редактора.

Контрол, состав или видимость которого зависят от активного инструмента, считается transient tool parameter и всегда размещается во вторичной панели. «Зарезервированная стабильная зона» внутри primary toolbar не используется: она либо отнимает место у всех остальных инструментов, либо снова создаёт разные правила между редакторами. В основной панели остаются только controls, доступные при любом инструменте данного редактора.

7.3. Группы инструментов как второй уровень

Группировка является осознанной информационной архитектурой, а не автоматической реакцией на нехватку места.

Группу можно создавать, если одновременно выполнены условия:

  • в ней не менее двух действий/инструментов;
  • элементы решают одну понятную пользовательскую задачу;
  • группа имеет короткое самостоятельное имя и различимую иконку;
  • пользователю не требуется видеть все элементы группы одновременно с геометрией сцены;
  • скрытие вариантов за одним дополнительным click/tap дешевле, чем постоянное переполнение основной панели.

Нельзя объединять в одну группу несвязанные команды вроде «Удалить», «Толщина», «Undo» и «Добавить устройство». Опасные действия selection не переносятся в launcher-группу только ради экономии места: они остаются в автоматически появляющемся контексте выбранного объекта.

Для первой версии вторичного уровня действует ограничение:

  • только один уровень раскрытия;
  • только одна открытая группа одновременно;
  • рекомендуемый размер группы — 2–6 элементов;
  • более 6 элементов используют palette-вариант или требуют отдельного UX-review;
  • вложенный launcher внутри group tray запрещён.

7.4. Взаимодействие автоматического контекста, явной группы и палитры

Вторичная поверхность физически одна. Приоритет её содержимого:

  1. явно открытая пользователем поверхность — группа либо палитра; если пользователь открывает другую явную поверхность, действует последняя;
  2. автоматические actions выбранного объекта;
  3. параметры активного инструмента;
  4. краткая статусная подсказка.

Открытая группа или палитра временно заменяет автоматический контекст, но не уничтожает selection. После закрытия явной поверхности актуальный автоматический контекст вычисляется заново и возвращается. Нельзя хранить и восстанавливать старый DOM-снимок: выбранный объект мог измениться или исчезнуть.

Группа и палитра взаимно исключают друг друга. Открытие группы закрывает палитру; открытие палитры закрывает группу. Палитра использует те же правила Escape, смены editor/space и потери launcher, что и группа. Click/tap вне палитры по умолчанию закрывает её и поглощается; palette-вариант для повторного размещения может явно получить политику stay-open-on-canvas, при которой валидный click сцены выполняет размещение, а не считается dismiss.

Если действие группы или палитры само меняет tool/selection, после закрытия показывается уже новый вычисленный контекст.

8. Компоновка

8.1. Схема

┌──────────────────────────────────────────────────────────────────────┐
│ [Постоянные инструменты ……………………] [Undo] [Redo] │ [Закрыть] │
└──────────────────────────────────────────────────────────────────────┘
                                      ┌───────────────────────────────┐
                                      │ Объект   [Свойства] [Удалить] │
                                      └───────────────────────────────┘
┌──────────────────────────── рабочая сцена ──────────────────────────┐
│ Суб-панель находится над этой областью и не меняет её размеры.      │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

8.2. Основная панель

Основная панель делится на две независимые зоны:

  1. tool rail — инструменты и постоянные контролы;
  2. pinned end cap — кнопка «Закрыть редактор».

Требования:

  • pinned end cap не участвует в переносе элементов tool rail;
  • кнопка закрытия имеет постоянную доступную область не менее 40 × 40 px, рекомендуемая — 44 × 44 px;
  • между tool rail и кнопкой закрытия допускается вертикальный divider;
  • фон end cap непрозрачен либо достаточно плотный, чтобы под него не просвечивали прокручиваемые инструменты;
  • на узкой ширине прокручивается или адаптируется tool rail, а не кнопка закрытия;
  • выбор объекта не меняет getBoundingClientRect() кнопки закрытия более чем на 1 px из-за округления.

8.3. Контекстная суб-панель

Рекомендуемое положение — под основной панелью, с выравниванием к логическому концу (inline-end). На ширине, где это невозможно, панель занимает доступную ширину с одинаковыми внешними отступами.

Параметр Требование
Позиционирование Абсолютное относительно stage shell или отдельного overlay-host; вне layout-flow
Отступ от основной панели var(--sp-2) / 4 px
Отступ от края карточки не менее var(--sp-4) / 8 px
Внутренний padding var(--sp-3) var(--sp-4) / 6 × 8 px
Gap var(--sp-3) / 6 px
Радиус var(--rad-l) / 12 px
Граница 1 px var(--hp-line) с повышенной видимостью на сложном фоне
Тень var(--shadow-2)
Максимальная ширина calc(100% - 2 * var(--sp-4))
Z-index выше сцены и её интерактивных объектов, ниже меню, picker, toast и dialog

Поверхность:

  • базовый фон строится от var(--hp-bg) / --card-background-color через отдельные theme tokens;
  • рекомендуемый default --hp-editor-tray-bg соответствует приблизительно 86% непрозрачности базового фона;
  • --hp-editor-tray-blur по умолчанию равен 8px, но blur не является обязательным;
  • fallback без backdrop-filter использует отдельный --hp-editor-tray-bg-fallback, по умолчанию приблизительно 94% базового фона;
  • компонент не содержит собственных hardcoded rgba()/процентов: все значения поверхности задаются токенами и могут быть переопределены темой;
  • opacity нельзя применять ко всему контейнеру: текст, иконки и focus ring остаются полностью непрозрачными;
  • контраст текста и controls должен оставаться не ниже WCAG AA в светлой и тёмной теме, в том числе над Glow и яркой подложкой.

Минимальный набор новых design tokens:

Токен Default Назначение
--hp-editor-tray-bg производный от --hp-bg с визуальной плотностью около 86% Фон при поддержке blur
--hp-editor-tray-bg-fallback производный от --hp-bg с визуальной плотностью около 94% Фон без backdrop-filter
--hp-editor-tray-blur 8px Радиус backdrop blur
--hp-editor-tray-border производный от --hp-line Граница над сложным фоном
--hp-editor-tray-shadow var(--shadow-2) Elevation вторичной панели

8.4. Порядок элементов

  1. необязательная компактная метка типа контекста (Перегородка, Колонна, Линия), с обрезкой ellipsis;
  2. недеструктивные параметры;
  3. основное действие «Свойства»;
  4. другие недеструктивные действия;
  5. деструктивное действие «Удалить» — последним.

Метка контекста не должна вытеснять действия. На узкой ширине она скрывается первой, а её текст остаётся доступен через aria-label панели.

8.5. Launcher и явно открытая группа

Launcher находится в tool rail основной панели и визуально отличается от обычной one-shot команды:

  • постоянная локализованная подпись группы;
  • основная иконка группы;
  • небольшой chevron раскрытия;
  • aria-expanded="true|false" и aria-controls;
  • активное оформление, если group tray открыт либо один из её persistent tools активен.

Подпись launcher не меняется на название последнего выбранного дочернего инструмента: стабильное имя группы лучше поддерживает узнаваемость. Активный дочерний инструмент показывается внутри group tray, а в tooltip launcher допустима строка вида «Стены — активна Перегородка».

Group tray на широком экране привязывается к началу launcher и сдвигается/clamp-ится внутрь карточки, если у края недостаточно места. Selection tray по-прежнему выравнивается к inline-end. На узком экране оба варианта используют почти полную доступную ширину.

┌──────────────────────────────────────────────────────────────────────┐
│ [Группа ▾] [Другой tool] ……………………………… [Undo] │ [Закрыть] │
└─────┬────────────────────────────────────────────────────────────────┘
      └─┌────────────────────────────────────────┐
        │ [Вариант A] [Вариант B] [Вариант C]   │  ← group tray
        └────────────────────────────────────────┘

Клик по launcher только открывает/закрывает группу и сам по себе не повторяет «последний использованный» инструмент. Split-button в это ТЗ не входит: две зоны на одной компактной кнопке хуже обнаруживаются и сложнее на touch.

9. Адаптивное поведение

Адаптация определяется шириной карточки/контейнера, а не только шириной окна браузера.

Ширина контейнера Context tray
>= 900 px Компактная строка по содержимому, выровнена к inline-end
560–899 px Строка до доступной ширины; внутренние группы могут переноситься внутри overlay
< 560 px Отступ 8 px с обеих сторон; actions остаются одной прокручиваемой по горизонтали строкой

Требования для узкого режима:

  • горизонтальная прокрутка допустима только внутри action rail;
  • карточка и страница не получают горизонтальный scrollbar;
  • «Свойства» и «Удалить» не сжимаются до нечитаемой ширины;
  • деструктивная кнопка не скрывается за clip без возможности прокрутки;
  • появление второй строки внутри крупной палитры разрешено, потому что палитра перекрывает сцену, а не меняет её размер;
  • безопасная зона и системные inset учитываются, если карточка показана fullscreen/kiosk.

Основная панель может изменить базовую высоту при реальном resize контейнера или смене редактора. Это отдельное осознанное событие. Выделение объекта при неизменной ширине не может быть причиной её перераскладки.

Group tray следует за общими breakpoint-правилами. Он не выходит за границы карточки, даже если launcher находится у её правого края; при нехватке места позиция clamp-ится, а не создаёт page overflow. Связь с launcher сохраняется визуально через выравнивание, а не обязательную стрелку-указатель.

10. Анимация

Появление

  • длительность: 140 ms;
  • opacity: 0 → 1;
  • transform: translateY(-4px) → translateY(0);
  • easing: cubic-bezier(0.2, 0.7, 0.2, 1).

Исчезновение

  • длительность: 100 ms;
  • opacity: 1 → 0;
  • transform: translateY(0) → translateY(-2px);
  • действия становятся недоступны сразу после потери контекста, даже если визуальный exit ещё заканчивается.

Смена выбранного объекта

  • внешний контейнер не исчезает, если следующий валидный контекст уже существует;
  • содержимое меняется коротким crossfade до 100 ms;
  • недопустимо проигрывать закрытие и повторное открытие панели между двумя последовательными выделениями;
  • текущая анимация отменяется при поступлении более нового состояния.

Открытие группы

  • используется та же enter/exit-анимация вторичной панели;
  • переключение с автоматического контекста на group tray выполняется одним crossfade без закрытия overlay-host;
  • открытие другой группы заменяет текущую, не создавая второй панели;
  • после выбора persistent tool группа закрывается, а tool-parameters появляются отдельным crossfade;
  • launcher не меняет положение и не вызывает reflow основной панели.

Reduced motion

При prefers-reduced-motion: reduce transform и изменение размеров не используются. Допустима мгновенная смена либо opacity не более 80 ms.

11. Состояния и переходы

Состояние Условие Поведение панели
hidden Нет временного контекста Панель отсутствует и не перехватывает pointer
selection Выбран редактируемый объект Показаны действия выбранного типа
operation Есть незавершённая операция с доступной отменой/сбросом Показаны действия операции
tool-parameters Активному инструменту нужны непостоянные параметры Показаны параметры инструмента
selection+tool Одновременно нужны параметры и действия объекта Один tray с двумя группами, а не две панели
group Пользователь явно открыл launcher основной панели Автоматический контекст временно заменён содержимым группы
palette Активна крупная палитра Overlay-host использует расширенный palette-вариант
blocked Команда временно недоступна Кнопка остаётся видимой disabled с объяснением
exiting Контекст потерян Панель inert, заканчивает exit-анимацию

Одновременно может существовать только один активный context tray. Приоритет UI-слоёв:

  1. dialog;
  2. menu / picker / popover;
  3. toast;
  4. editor context tray / tool palette;
  5. hover-tooltip устройств (_tip), geometry hints и другие scene-owned overlays;
  6. сцена.

Внутри одного overlay-host состояния group и palette являются взаимно исключающими явными поверхностями и имеют приоритет над selection; последнее явно открытое состояние заменяет предыдущее. Dialog/popover всегда выше группы и палитры.

Существующий _tip не может перекрывать tray. Если вторичная панель появляется поверх уже открытого tooltip, tooltip остаётся на scene-уровне и визуально обрезается/оказывается под tray; вход pointer в интерактивную поверхность tray должен очищать stale hover-tooltip, чтобы он не возник повторно без нового hover сцены.

12. Матрица существующих редакторов

12.1. Редактор плана

Текущий динамический элемент Новое место Примечание
«Свойства» выбранной перегородки/колонны/незавершённого контура Context tray Недеструктивное действие перед Delete
«Удалить» выбранный объект Context tray Деструктивное действие последним; текущее подтверждение сохраняется
«Сбросить» незавершённый контур Context tray Контекст операции, не постоянный инструмент
Толщина для «Контур комнаты» / «Перегородка» / «Колонна» Tool-parameters в том же tray Не должна менять строки основной панели при переключении контекста
Динамическая текстовая подсказка инструмента Статусная область tray либо overlay-hint сцены Не должна входить в flow основной панели
Undo / Redo Основная панель Постоянные действия
«Закрыть редактор» Pinned end cap основной панели Никогда не перемещается из-за selection

12.2. Редактор устройств

Текущие постоянные «Добавить», «Показать скрытые», «Правила», Undo/Redo и выход остаются в основной панели, если их доступность не зависит от выделенного marker.

Любые будущие quick actions выбранного устройства — «Свойства», «Скрыть», «Удалить», «Открыть в HA» и подобные — обязаны появляться в context tray. Нельзя добавлять их в общую строку рядом с выходом.

Если marker открывает dialog непосредственно двойным кликом, это не отменяет возможность явной кнопки «Свойства» в tray, но состав действий должен быть единым resolver-ом, а не двумя независимыми списками.

12.3. Редактор подложки

Элемент Новое место
Свойства выбранного объекта Context tray
Удаление выбранного объекта Context tray
Динамическая заливка, применимая только к выбранной фигуре/активному tool Tool-parameters tray
«Сбросить подложку», появляющееся только после сдвига Context tray
Цвет, толщина и другие параметры конкретного активного инструмента Tool-parameters tray; зарезервированная зона primary toolbar не используется
Палитра мебели Отдельный palette-вариант того же overlay-host

Палитра мебели может быть выше компактной панели и иметь собственный scroll, но не уменьшает сцену. Закрытие палитры не меняет zoom/pan.

12.4. Будущие редакторы

Каждый редактор предоставляет декларативный набор:

  • постоянных toolbar items;
  • контекстных actions;
  • компактных context controls;
  • при необходимости — одного palette overlay.

Редактор не должен вручную вставлять transient-кнопки в primary toolbar.

Он также может декларативно предоставить группы основной панели. Это разрешает в будущем заменить несколько близких инструментов одной кнопкой без изменения overlay-архитектуры. Конкретный состав каждой группы утверждается отдельным UX-решением: это ТЗ создаёт механизм, но не объединяет существующие кнопки автоматически.

13. Поведение в ключевых сценариях

Выделение и снятие выделения

  • выделение завершено → action model вычислена → tray появляется;
  • клик по пустой сцене снимает выделение по существующим правилам → tray исчезает;
  • появление tray не вызывает auto-pan и не пытается отодвинуть объект;
  • если объект находится под tray, пользователь может продолжить pan/zoom; система не меняет положение плана сама.

Drag и resize

  • новый tray не должен перехватывать завершение pointer-жеста;
  • если выделение создаётся на pointerdown, actions становятся интерактивны только после commit/pointerup текущего жеста;
  • при drag уже выбранного объекта tray может оставаться видимым, но его состав не пересчитывается на каждом pointermove;
  • screen-to-plan mapping не меняется в середине жеста.

Dialog свойств

  • открытие dialog делает tray inert и помещает его под scrim;
  • после закрытия без удаления прежнее выделение и tray восстанавливаются;
  • после сохранения tray отражает актуальный тип/доступность действий;
  • если dialog удалил или заменил объект, stale actions не остаются.

Delete, Undo и Redo

  • подтверждение удаления остаётся обязательным там, где оно уже предусмотрено;
  • после подтверждённого удаления выделение очищается до следующего render actions;
  • Undo, восстановивший объект, не обязан автоматически выделять его, если текущий command stack этого не делает;
  • Redo/внешнее обновление, удалившее выделенный объект, безопасно скрывает tray;
  • Delete остаётся удалением выбранного объекта, только когда focus находится на сцене/editor host и действуют существующие правила выбранного объекта;
  • при focus внутри любой вторичной панели — selection tray, tool-parameters, group или palette — глобальные Delete/Backspace не удаляют объект сцены; для удаления используется явная кнопка либо focus сначала возвращается на сцену;
  • нативное поведение редактируемых input/textarea/contenteditable внутри панели всегда имеет приоритет над editor shortcuts.

Смена editor/space

  • при смене редактора или пространства старый tray немедленно становится inert;
  • context state старого редактора не может мигнуть поверх нового;
  • анимация смены editor chrome продолжает учитывать только постоянную основную панель;
  • overlay нового редактора появляется после активации его актуального контекста.

Быстрая смена выделения

  • используется последний context state;
  • старые callbacks не могут удалить или изменить новый выбранный объект;
  • незавершённая exit-анимация отменяется;
  • не допускается накопление нескольких overlay в DOM.

Явная группа кнопок

  • click/tap по launcher открывает group tray; повторный click/tap закрывает его;
  • группа никогда не открывается по hover;
  • открытие нового launcher закрывает предыдущую группу;
  • Escape закрывает group tray, не снимает selection и возвращает focus launcher-у;
  • click/tap вне явно открытой группы закрывает её; dismiss-click поглощается и не должен одновременно рисовать, удалять или выделять объект на сцене;
  • после закрытия заново вычисляется и показывается актуальный automatic context;
  • выбор дочернего persistent tool активирует tool и по умолчанию закрывает группу;
  • one-shot command закрывает группу перед выполнением команды;
  • toggle/setting может явно получить политику stay-open, если пользователю разумно изменить несколько соседних параметров;
  • на group tray распространяется общее правило вторичных панелей: Delete/Backspace не удаляют выбранный объект сцены;
  • действие, открывающее dialog, закрывает group tray до появления dialog;
  • если group action стала недоступна после открытия, она остаётся видимой disabled с актуальным объяснением;
  • переключение editor/space немедленно закрывает группу без восстановления в новом контексте.

Явная палитра

  • палитра имеет тот же верхний приоритет над автоматическим контекстом, что и явно открытая группа;
  • opening palette закрывает открытую группу, opening group закрывает палитру;
  • изменение selection через keyboard, Undo/Redo или external config не закрывает палитру само по себе, но underlying context пересчитывается и будет показан только после её закрытия;
  • обычный dismiss-click вне палитры закрывает её и не проваливается в сцену;
  • palette с политикой stay-open-on-canvas принимает валидные canvas-clicks активного инструмента для повторного размещения; невалидный внешний click закрывает палитру и поглощается;
  • Escape, смена editor/space, потеря видимого launcher и открытие dialog закрывают палитру;
  • после закрытия палитры automatic context строится заново, без восстановления stale DOM.

14. Архитектурный контракт

Рекомендуется единый компонент/renderer уровня editor shell, условно hp-editor-context-tray.

Он принимает декларативную модель, а не знает о геометрии конкретного редактора:

type EditorContextAction = {
  id: string;
  label: string;
  icon: string;
  tone?: "normal" | "primary" | "danger";
  disabled?: boolean;
  disabledReason?: string;
  shortcut?: string;
  invoke: () => void;
};

type EditorContextModel = {
  contextId: string;
  kind: "selection" | "operation" | "tool" | "mixed" | "group" | "palette";
  accessibleLabel: string;
  visibleLabel?: string;
  actions: EditorContextAction[];
  controls?: unknown;
};

type EditorToolbarGroup = {
  id: string;
  label: string;
  icon: string;
  activeItemId?: string;
  items: Array<EditorContextAction & {
    role?: "tool" | "command" | "toggle";
    closePolicy?: "on-activate" | "stay-open";
  }>;
};

Точные TypeScript-типы могут отличаться, но обязательны следующие свойства архитектуры:

  1. один host на активный editor shell;
  2. один источник истины для доступных действий;
  3. отсутствие editor-specific transient DOM внутри primary toolbar;
  4. context tray находится вне элемента, чья высота входит в _hdrH;
  5. stage shell является positioning context для overlay;
  6. pointer-events отключены на пустой оболочке overlay и включены только на самой панели;
  7. stale action проверяет актуальность contextId перед мутацией;
  8. dialog, keyboard shortcut и tray вызывают одну и ту же команду.
  9. launcher-группы и автоматический контекст используют один overlay-host и общую систему позиционирования;
  10. явное состояние openGroupId не записывается в конфигурацию и сбрасывается при смене редактора/пространства.

Правило формирования contextId

contextId формируется детерминированно из:

  1. активного editor mode;
  2. kind контекста;
  3. стабильного ID объекта, операции, инструмента, группы или палитры;
  4. монотонного configEpoch, меняющегося при замене config, пространства или исходной коллекции объектов;
  5. локальной revision контекста, если доступность actions изменилась без смены стабильного ID.

Рекомендуемый формат: editor:<mode>:<kind>:<targetId>:<configEpoch>:<revision>. Для явно открытой группы используется её groupId и generation конкретного открытия. Индекс объекта в массиве, DOM-index и локализованная подпись не могут быть частью identity: они нестабильны.

Перед выполнением action host сравнивает сохранённый contextId с актуальным. При несовпадении команда не выполняется, старый tray становится inert и запускается пересчёт текущей модели.

Обязательная граница измерений

ResizeObserver, определяющий высоту header/editor chrome, не должен наблюдать context tray и tool palette. Появление, изменение текста, локализации или перенос строки внутри overlay не может изменить _hdrH.

15. Доступность

  • контейнер действий имеет role="toolbar" и локализованный aria-label, например «Действия: Перегородка»;
  • кнопки остаются нативными button-элементами с видимым focus ring;
  • появление панели после клика не перехватывает focus;
  • для клавиатурного выбора допускается aria-live="polite" с коротким сообщением о выбранном типе, но не с перечислением всех кнопок;
  • Tab включает действия tray в предсказуемом DOM-порядке;
  • если сфокусированная кнопка исчезла вследствие Delete/deselect, focus возвращается на сцену или её устойчивый editor host, а не на body;
  • Escape сначала закрывает верхний dialog/popover, затем по существующим правилам отменяет операцию или снимает выделение;
  • disabled action сообщает причину доступным текстом/tooltip и не зависит только от цвета;
  • danger-состояние различается текстом/иконкой, а не одним красным цветом;
  • prefers-reduced-motion соблюдается;
  • контраст проверяется над светлой, тёмной, цветной и Glow-подложкой.

Для launcher-группы:

  • launcher является button, имеет aria-expanded, aria-controls и локализованное описание группы;
  • сам group tray использует role="toolbar", а не ARIA menu, поскольку содержит инструменты и toggle-состояния, а не классическое application menu;
  • Enter/Space открывают или закрывают группу;
  • ArrowDown на закрытом launcher открывает группу и переводит focus на активный либо первый доступный item;
  • внутри группы действуют Left/Right, Home/End и roving tabindex;
  • Escape закрывает группу и возвращает focus launcher-у;
  • при открытии мышью focus может остаться на launcher, а следующий Tab входит в группу;
  • disabled items пропускаются стрелочной навигацией, но остаются доступны screen reader вместе с причиной;
  • после выбора item focus возвращается на launcher либо на сцену активированного инструмента согласно типу действия, но никогда не теряется на body.

16. Touch-статус

Редакторы House Plan остаются desktop-first / best effort на touch согласно TOUCH-SUPPORT.md. Полноценный паритет сложного редактирования не является целью задачи.

При этом обязательный safety floor сохраняется:

  • tray не должен появляться из-за pinch/multi-touch misclick;
  • доступные на touch действия имеют hit-area не менее 44 × 44 px;
  • pinch/pan вне панели не блокируется её прозрачной оболочкой;
  • нажатие на action не проваливается в сцену;
  • выход из редактора всегда доступен;
  • горизонтальная прокрутка actions не должна случайно перемещать объект плана.
  • launcher открывает группу только по явному tap, не по hover/long-press;
  • dismiss-tap по сцене не проваливается в геометрическую команду.

17. Локализация и copy

Минимальный набор строк поверхности; существующие ключи переиспользуются, новые создаются только при отсутствии эквивалента:

RU EN Назначение
Действия: {object} Actions: {object} aria-label tray
Параметры инструмента: {tool} Tool options: {tool} aria-label tool tray
Свойства Properties Основное действие объекта
Удалить Delete Деструктивное действие
Открыть группу {group} Open {group} group Доступное описание launcher при необходимости
{group}, активен {item} {group}, {item} active Tooltip/ARIA active-child состояния

Существующие подписи переиспользуются, если их семантика совпадает. Текст не собирается конкатенацией, нарушающей порядок слов в других языках.

Для «Свойства» и «Удалить» обязательно переиспользуются существующие ключи btn.properties и btn.delete из src/i18n/ru.json и src/i18n/en.json; дублирующие tray-specific ключи не создаются. Новые строки добавляются только для aria-label самой панели и launcher-групп.

18. Эдж-кейсы

№ Ситуация Нормативное поведение Основной раздел
1 Карточка находится ровно на ширине прежнего переноса кнопок Selection меняет только overlay; размеры сцены и primary toolbar остаются теми же с допуском 1 px §1, §19
2 Очень длинное название типа или локализованная подпись Видимая context-метка получает ellipsis и скрывается первой; полное имя остаётся в ARIA; actions прокручиваются только внутри rail §8.4, §9, §15
3 Одновременно нужны tool parameters и actions объекта Рендерится один tray с двумя семантическими группами; при дефиците ширины scroll/wrap происходит внутри overlay §11
4 Появилось более четырёх actions Actions сохраняют приоритет и явную доступность; до шести допустим внутренний rail, свыше шести требуется отдельный UX-review, без автоматического overflow §7.3 и правило ниже
5 Выбранный объект находится под overlay Объект автоматически не перемещается; пользователь может pan/zoom, а overlay не меняет viewBox §13 «Выделение»
6 Выбранный объект удалён external config update contextId инвалидируется, actions немедленно становятся inert, selection очищается или пересчитывается §13, §14
7 Undo/Redo меняет тип или существование объекта Context model строится заново; исчезнувший объект не остаётся selected, восстановленный не выбирается автоматически без текущего правила command stack §13 «Delete, Undo и Redo»
8 Dialog открыт во время смены пространства Старый tray становится inert и не восстанавливается в новом space; dialog закрывается/переназначается по существующим правилам смены пространства без stale callback §13 «Смена editor/space»
9 Быстрые selection-clicks идут во время enter/exit Предыдущая animation отменяется, применяется последний context state, в DOM остаётся один host §10, §13 «Быстрая смена»
10 Pointer capture активен при появлении контекста Actions не становятся интерактивными до commit/pointerup; mapping сцены не меняется §13 «Drag и resize»
11 Из tool controls открыт color picker/popover Picker находится выше tray; tray остаётся якорем и inert только в зоне взаимодействия picker; закрытие picker возвращает focus вызвавшему control §11, §15
12 backdrop-filter не поддерживается Используется тематический fallback-токен более плотного фона; контент не меняет opacity §8.3
13 Light/dark, Glow или яркая картинка под tray Используются theme tokens и проверенный AA-контраст; opacity не применяется ко всему контейнеру §8.3, §19
14 Browser zoom 125–200% Позиция clamp-ится внутри карточки; page overflow отсутствует, controls сохраняют hit-area §9, §15
15 RTL-интерфейс Применяются logical properties и inline-start/inline-end; порядок danger-action остаётся семантически последним §8, §9
16 Fullscreen/kiosk с safe-area inset Внешние отступы overlay увеличиваются на соответствующий env(safe-area-inset-*); tray не перекрывает системную область §9
17 Touch-scroll по action rail рядом со сценой Gesture принадлежит rail, не двигает объект/план и не проваливается в сцену §16
18 prefers-reduced-motion: reduce Transform отключён; используется мгновенная смена или opacity до 80 ms §10
19 Повторно выбран тот же объект без смены actions Сохраняется тот же contextId; enter/crossfade не перезапускается §10, §14
20 Context инвалидирован между render и нажатием Action повторно проверяет contextId, ничего не мутирует и инициирует пересчёт актуального контекста §14
21 Group launcher у края, tray не помещается Group tray clamp-ится внутрь карточки без изменения ширины страницы §8.5, §9
22 Под group/palette изменился или исчез selection Явная поверхность остаётся до штатного закрытия, underlying context пересчитывается; после закрытия stale selection не возвращается §7.4, §13
23 Две launcher-группы открыты быстрыми кликами Последняя явная группа атомарно заменяет предыдущую; второй host не создаётся §7.4, §13
24 Активный дочерний tool стал недоступен Active item снимается или становится disabled по актуальной модели; launcher не показывает ложное active-состояние §8.5, §13
25 В группе смешаны tool, command и toggle Каждый item использует явные role и closePolicy; default — on-activate, stay-open разрешён только toggle/setting §13, §14
26 Dismiss-click попал поверх интерактивной геометрии Group/palette закрывается, click поглощается и не выполняет selection/draw/delete; исключение — объявленная palette-политика stay-open-on-canvas §13
27 Launcher скрыт responsive layout или полностью ушёл из видимой области прокручиваемого tool rail Связанная group/palette немедленно закрывается. Если focus был внутри неё или на ставшем невидимым launcher, focus переводится на ближайший видимый toolbar item, затем на toolbar host как fallback §8.5, §9, §15

Если действий становится больше четырёх, они группируются по приоритету, но автоматически прятать их в overflow нельзя. Состав свыше шести видимых действий требует отдельного UX-review: это обычно признак слишком широкого контекста.

19. Критерии приёмки

Геометрия

  1. На каждой контрольной ширине высота и верхняя координата сцены до и после selection совпадают с допуском 1 px.
  2. viewBox, zoom и pan не меняются.
  3. Кнопка закрытия остаётся в той же позиции с допуском 1 px.
  4. Появление tray не вызывает ResizeObserver-обновление _hdrH.
  5. Страница и карточка не получают новый горизонтальный scrollbar.

Поведение

  1. «Свойства» открывает тот же dialog и тот же объект, что текущая команда.
  2. «Удалить» сохраняет существующие правила подтверждения и command stack.
  3. Снятие выделения скрывает actions.
  4. Смена объектов обновляет actions без закрытия/открытия внешнего контейнера.
  5. Смена редактора не оставляет stale overlay.
  6. Только один tray существует одновременно.
  7. Overlay не препятствует pan/zoom за пределами своей видимой поверхности.
  8. Launcher открывает только одну группу и не меняет высоту основной панели.
  9. Закрытие группы восстанавливает заново вычисленный automatic context.
  10. Dismiss-click не проваливается в сцену.
  11. Выбор дочернего tool отражается активным состоянием launcher без смены его постоянной подписи.

Визуал и доступность

  1. Поверхность читаема в светлой/тёмной теме и над Glow.
  2. RU/EN помещаются на контрольных ширинах без page overflow.
  3. Focus не теряется при появлении или удалении панели.
  4. Все действия доступны с клавиатуры.
  5. Reduced motion не использует transform-анимацию.
  6. Touch safety floor соблюдён без требования полного editor parity.

20. План проверки при будущей реализации

Автоматические проверки

  1. Unit-тест resolver-а context actions для каждого редактора и типа selection.
  2. Browser smoke: измерить stage.getBoundingClientRect() и кнопку закрытия до/после selection.
  3. Browser smoke: быстрая смена двух объектов во время animation.
  4. Browser smoke: Delete → Undo → Redo без stale actions.
  5. Browser smoke: modal properties поверх tray.
  6. Browser smoke: узкий контейнер без горизонтального scroll страницы.
  7. Golden images для wide/medium/narrow, RU/EN, light/dark.
  8. Reduced-motion сценарий.
  9. Launcher: open/close, смена двух групп, выбор tool, one-shot command и stay-open toggle.
  10. Selection tray → group tray → восстановление selection tray без stale object.
  11. Keyboard matrix launcher/toolbar: Enter, Space, ArrowDown, Left/Right, Home/End, Escape, Tab.
  12. Ревизия demo/smoke_editor_tabs.mjs: editorSwapAnimatesHeight продолжает проверять осознанную анимацию между редакторами, но selection/group/palette внутри одного редактора отдельно подтверждаются как layout-invariant.
  13. Ревизия demo/smoke_preloader_lifecycle.mjs и связанных preloader smoke: новый overlay не входит в settle-измерения header, не продлевает boot veil и не воспринимается как поздний layout shift.
  14. Регрессионная проверка существующих selectors .editbar, .barclose и .editorchrome-inner либо их осознанное обновление во всех smoke, использующих старую структуру панели.

Рекомендуемая матрица ширин

  • 420 px;
  • 559 px;
  • 560 px;
  • 719/720/721 px как текущая чувствительная зона;
  • 899/900 px;
  • 1200 px.

На каждой ширине проверяются редактор плана, устройств и подложки минимум в состояниях hidden, selection и наиболее крупном контексте.

Ручная проверка

  • mouse: select, double click, drag, delete, properties;
  • keyboard: Tab, Enter, Escape, Delete, Ctrl+Z/Ctrl+Shift+Z;
  • touch best effort: single tap, action tap, горизонтальный scroll actions, pinch рядом с tray;
  • browser zoom 100%, 125%, 200%;
  • отсутствие визуального скачка при повторном выборе.

Полный прогон выполняется перед полноценным релизом; для beta — минимально необходимая матрица согласно принятому процессу проекта.

21. Последовательность будущей реализации

  1. Добавить stage overlay-host, не входящий в измерение header.
  2. Разделить primary toolbar на tool rail и pinned close end cap.
  3. Создать общий декларативный context tray.
  4. Сразу заложить в host явный group-режим и API launcher-групп, даже если первая миграция использует только selection actions.
  5. Перенести selection actions редактора плана.
  6. Перенести transient controls и operation actions редактора плана.
  7. Подключить тот же контракт к редактору подложки и palette-варианту.
  8. Подключить редактор устройств и зафиксировать API для будущих actions.
  9. Добавить keyboard/focus semantics автоматического и явно открытого режимов.
  10. Добавить геометрические browser assertions, launcher/group tests и golden fixtures.
  11. Обновить ARCHITECTURE.md, UX-MODES.md, DECOR-EDITOR.md, TOUCH-SUPPORT.md, TESTING.md, RU/EN пользовательскую документацию и CHANGELOG.md / CHANGELOG.ru.md.

22. Зафиксированные допущения

  1. Суб-панель выравнивается к правому/логическому краю, потому что там находятся object actions и выход; на узкой ширине становится почти полноширинной.
  2. Правило распространяется на весь transient UI, а не только на кнопки. Это необходимо, чтобы такой же layout shift не вернулся через input, hint или picker.
  3. Метка типа выбранного объекта рекомендуется, но скрывается первой при дефиците места.
  4. Объект под overlay не перемещается автоматически.
  5. Основная панель может менять высоту при смене редактора или resize контейнера, но не при selection/context changes внутри редактора.
  6. Модель данных, команды и результаты редактирования остаются прежними.
  7. Суб-панель проектируется как универсальный второй уровень editor chrome, а не как частный контейнер двух selection-кнопок.
  8. Одна кнопка основной панели может в будущем открыть группу из 2–6 связанных инструментов/команд.
  9. Группа открывается только явно и временно имеет приоритет над автоматическим контекстом; закрытие возвращает актуальный контекст.
  10. Первый архитектурный этап не поддерживает вложенные группы и не объединяет существующие инструменты без отдельного продуктового решения.

Блокирующих продуктовых вопросов для начала реализации нет. Если владелец продукта хочет ограничить первый этап только кнопками «Свойства / Удалить», это допустимо как поэтапный rollout, но архитектурный запрет на transient UI в primary toolbar всё равно должен быть заложен сразу.