18 KiB
HDROPDOWN CANON
Документ фиксирует единый канон dropdown-окон NODE.DC.
Цель документа:
- убрать локальные самодельные реализации выпадающих окон
- перевести dropdown на переиспользуемые shared-компоненты
- зафиксировать единый контракт для trigger, portal, placement, стилей и поведения
- исключить повторение проблемы, когда соседние controls работают по-разному на одном и том же экране
Базовый принцип
Dropdown в системе не является "локальной маленькой версткой рядом с кнопкой".
Dropdown это:
- отдельный floating-layer
- отдельный reusable UI-компонент
- предсказуемый контракт открытия и закрытия
- единый визуальный канон
Если на экране есть несколько выпадающих окон, пользователь не должен видеть разные механики:
- одно окно открывается по тексту
- второе только по стрелке
- третье клипается контейнером
- четвертое открывается с другим типом привязки
Это считается дефектом стандартизации.
Какие dropdown бывают
В проекте фиксируются три типа выпадающих окон.
1. Selection dropdown
Используется там, где пользователь выбирает значение.
Примеры:
СтатусПриоритетДатаНазначенныеМетки- выбор проекта
- выбор модуля
Смысл:
- пользователь выбирает одно или несколько значений
- у dropdown есть список опций
- часто есть поиск
- текущее значение отображается в trigger
Канонические reference-реализации:
2. Action dropdown
Используется там, где пользователь вызывает набор действий.
Примеры:
...на карточке задачи...в detail-header- быстрые действия у рабочего элемента
Смысл:
- dropdown не меняет поле напрямую
- dropdown показывает список команд
- каждая строка запускает
action
Канонический shared-компонент:
3. Context menu
Используется только как контекстное меню, а не как основной UI для кнопки ....
Примеры:
- правый клик
- context-actions, привязанные к
parentRef
Правило:
ContextMenuне заменяет основной dropdown по клику на кнопку- если у карточки есть кнопка
..., основной visible menu должен открываться черезActionDropdown ContextMenuдопустим как дополнительный способ вызвать те же действия через right click
Что запрещено
Запрещено делать новый dropdown через:
- локальный
isOpenбез shared-компонента - локальный
useOutsideClickвнутри карточки - inline absolute-блок внутри карточки
- popup, который живет внутри scroll-контейнера
- отдельную реализацию
..., если рядом уже есть рабочий shared-dropdown - другой trigger-контракт для соседних controls на одной оси
Запрещено:
- делать одну механику для
Статуса, а другую для... - открывать меню только по стрелке, если весь control должен быть кликабельным
- класть popup внутрь карточки, detail-pane, sidebar или sticky-header без portal
- компенсировать неверную архитектуру случайными
left/right translate
Главный стандарт переиспользования
Перед созданием нового dropdown нужно ответить только на два вопроса:
- Это выбор значения или набор действий?
- Есть ли уже shared-компонент для этого типа?
Если это выбор значения:
- использовать существующий selection-dropdown
- расширять existing base-component, а не писать новый popup с нуля
Если это список действий:
- использовать
ActionDropdown - не создавать отдельную локальную механику для карточки, detail-view или sidebar
Канонический стек для dropdown
Любой новый dropdown должен опираться на один и тот же стек:
- реальный
trigger usePopperportaloutside click closekeyboard close/openpreventOverflowflip
Для action dropdown текущий канон зафиксирован в:
Для selection dropdown reference-стек уже зафиксирован в:
Trigger-правила
Trigger должен быть реальным интерактивным элементом.
Правила:
- dropdown привязывается к настоящей кнопке или surface-контролу
- нельзя заворачивать trigger в лишний интерактивный wrapper без причины
- нельзя делать
buttonвнутриbutton - нельзя вешать отдельный click-interceptor на родительскую карточку так, чтобы он ломал trigger dropdown
Если control живет внутри кликабельной карточки:
- интерактивная зона обязана быть помечена как ignore-area для клика карточки
- карточка не должна перехватывать клик у dropdown trigger
Пример текущего канона для вложенной интерактивной зоны карточки:
data-control-link-ignore="true"
Portal-правила
Dropdown нельзя рендерить inline, если control находится внутри:
- карточки
- kanban-колонки
- list-item
- detail-pane
- sidebar
- properties section
- sticky toolbar
- scrollable container
В этих случаях popup обязан рендериться через portal на верхнем слое.
Базовое правило:
document.bodyявляется стандартным portal target, если нет отдельного системного слоя
Причина:
- иначе будут клиппинг, неправильный z-index, обрезание соседними контейнерами и ложные оффсеты
Placement-правила
Placement выбирается не "на глаз", а по канону control-type.
Канон для selection dropdown
По умолчанию:
bottom-start
Это уже используется соседними controls:
СтатусПриоритетДата
Канон для action dropdown карточек
Для quick-actions карточек используется тот же базовый anchor-канон, что и у соседних dropdown на той же верхней оси.
Текущая фиксация для kanban:
- base-kanban-root.tsx
- quick-actions в kanban теперь идут через
bottom-start
Смысл:
- меню раскрывается относительно trigger по той же логике, что и соседние controls
- мы не подгоняем offset вручную под один экран
- мы нормализуем сам placement-contract
Когда допустим другой placement
Другой placement допустим только если это диктует layout:
top-start, если dropdown открывается из нижней части detail-pane и не должен врезаться в секцию нижеtop-end, если control находится у правой кромки и popup иначе уходит за экран
Но это должно быть обосновано layout-контекстом, а не случайной визуальной подгонкой.
Offset-правило
Offset нужен только для вертикального зазора между trigger и popup.
Канон:
- вертикальный gap небольшой и стабильный
- боковой offset по умолчанию не используется
Если кажется, что popup "смотрит не туда":
- сначала проверяется правильность
placement - потом anchor element
- и только в последнюю очередь ручной skidding
Правило:
- нельзя лечить неверный
placementслучайнымleft/right offset
Визуальный канон
Все dropdown и popup подчиняются общему matte glass стилю.
Обязательные свойства:
- темный glass surface
- blur
- мягкая стеклянная граница
- без технических outline
- без браузерного синего focus ring
- без светлой инородной подложки на темном экране
Внутренние элементы dropdown:
- search shell визуально того же семейства, что и popup
- option row того же радиуса, что и системные popup-option
- hover мягкий, не кислотный
- active/selected state не ломает общую палитру
Правила для списка действий
Action dropdown обязан принимать данные в виде menu items.
Каждый item:
- имеет
key - может иметь
icon - может иметь
title - может иметь
description - может иметь
disabled - может иметь
action
Правило:
- визуальный рендер item делается внутри shared action-dropdown
- вызывающий код не рисует popup shell вручную
Правила для карточек и detail-view
Для карточек рабочего элемента:
...всегда action dropdownСтатусиПриоритетвсегда selection dropdown- все три control-а на одной оси должны использовать совместимую trigger-геометрию и близкий placement-contract
Для detail-view:
- quick actions сверху не должны использовать отдельный popup-engine
- если действия те же, используется тот же
ActionDropdown
Для Внутреннего контура:
- quick-actions карточек больше не живут на отдельной локальной механике
- они нормализуются под тот же shared dropdown-stack, что и другие controls
Правила для хлебных крошек
Breadcrumb dropdown тоже подчиняется этому документу.
Правило:
- project breadcrumb это dropdown проектов
- module breadcrumb это dropdown модулей проекта
- весь breadcrumb-item кликабелен, а не только стрелка
- стрелка не может быть единственной интерактивной зоной
Если breadcrumb открывает список выбора:
- это selection dropdown
- trigger-контракт должен совпадать на всех проектных экранах
Правила для новых экранов
Новый экран не имеет права изобретать свой тип dropdown, если:
- уже существует shared action dropdown
- уже существует shared selection dropdown
Порядок работы:
- определить тип dropdown
- выбрать shared-компонент
- выбрать canonical placement
- открыть popup через portal
- применить glass-канон
- проверить поведение внутри scroll/sticky/detail contexts
Что делать, если нужен новый вариант
Если текущих shared-вариантов не хватает:
- сначала расширяется shared-component
- потом новый вариант документируется в этом файле
- только после этого он применяется на экранах
Что нельзя делать:
- сделать новый локальный dropdown "временно"
- спрятать временную логику внутри конкретной карточки
- оставлять два разных menu-engine для одинаковой задачи
Legacy-правило
Если в проекте еще остались старые dropdown-механики:
- они считаются legacy
- новые карточечные quick-actions на них не строятся
- при доработке экрана предпочтение отдается переводу на shared-канон, а не лечению локального бага поверх legacy-кода
Это особенно касается:
- локальных
CustomMenu-вариантов для карточечных... - inline popup внутри карточек
- отдельных
isMenuActiveсостояний в item-компонентах
Reference-матрица переиспользования
Использовать:
-
ActionDropdown- для
... - для быстрых действий карточки
- для action-menu detail-header
- для
-
StateDropdown- для выбора состояния
-
PriorityDropdown- для выбора приоритета
-
DateDropdown- для выбора даты
-
MemberDropdown- для выбора участников
-
ContextMenu- только как дополнительное context-menu по
parentRef - не как основной visible dropdown у кнопки
...
- только как дополнительное context-menu по
Минимальный check-list перед merge
Перед завершением задачи с dropdown надо проверить:
- popup открывается по клику на весь intended trigger
- popup не клипается родительским контейнером
- popup не уезжает за экран
- popup не живет на отдельном локальном menu-engine без причины
Статус / Приоритет / ...на карточке не конфликтуют по click-handling- карточка не перехватывает клик у trigger
- popup визуально соответствует matte glass канону
- hover, active и selected состояния выглядят как часть одной системы
Канон внедрения
С этого момента правило простое:
- одинаковая задача = одинаковый dropdown-engine
- одинаковый control-type = одинаковый placement-contract
- одинаковый popup-type = одинаковый visual shell
Нельзя:
- лечить оффсет одного окна отдельной заплаткой
- оставлять соседние dropdown на разных механиках
- дублировать menu-логику в карточке, detail-pane и sidebar
Нужно:
- расширять shared-компонент
- переиспользовать его на всех экранах
- документировать новый вариант сразу в этом файле