diff --git a/HDESIGN-CODE.md b/HDESIGN-CODE.md index 5c28640..3b03d66 100644 --- a/HDESIGN-CODE.md +++ b/HDESIGN-CODE.md @@ -98,6 +98,7 @@ ## Dropdown и popup - Все dropdown/popup приводятся к единому matte glass канону. +- Подробный архитектурный и поведенческий регламент dropdown-окон вынесен в [HDROPDOWN-CANON.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/HDROPDOWN-CANON.md). - Запрещены: - квадратные active-box вокруг круглых кнопок - жёсткие border-outline @@ -167,6 +168,8 @@ - `.nodedc-external-card` - `.nodedc-external-section` - `.nodedc-external-content-shell` +- Intake filter chips: + - `.nodedc-filter-chip` ### Anchor snippets ```tsx @@ -243,6 +246,24 @@ - global sidebar quick action `Новый рабочий элемент` не показывается на маршруте `external-contours`, потому что этот экран уже имеет собственный primary CTA в header - active/passive карточки `Внешних контуров` обязаны брать фон только из `--nodedc-card-active-rgb` и `--nodedc-card-passive-rgb` - header `Внешних контуров` и detail-pane опускаются на единый верхний ритм; нельзя прижимать breadcrumbs, CTA и detail-header к верхней кромке +- Для `Предложений / Intake` это значит: + - правая detail-pane не растягивается на всю свободную ширину экрана; она использует тот же `IssueView` side-peek shell и тот же persisted width, что и `Внутренний контур` + - top-toolbar `Предложений` не верстается отдельной локальной шапкой; используется тот же peek header, что и у `Внутреннего контура`, а intake-специфичные actions добавляются только как slot + - `Открытые / Закрытые` не живут отдельными tab-кнопками внутри левой колонки; для intake статус — это обычный filter, а не отдельный режим layout + - кнопки `Фильтры / Сортировка` не остаются внутри списка intake; они выносятся в верхний header cluster по тому же паттерну, что и у `Внутреннего контура` + - dropdown фильтров и сортировки не могут жить под карточками списка; popup обязан иметь верхний z-layer и не конфликтовать со scroll/list слоями + - search shell внутри intake filter dropdown использует тот же matte glass, что и остальные dropdown/popup + - applied filter chips в intake не используют старые `Tag`-плашки `Plane`; они приводятся к glass-chip канону через `.nodedc-filter-chip` + - intake-list использует тот же shared `nodedc-work-item-card` shell, что и карточка `Внутреннего контура`; intake допускает только контекстные отличия в meta/footer, а не отдельную геометрию карточки + - правая detail-pane `Предложений` не изобретает собственные section-shell; title, description, properties и activity используют тот же peek/details rhythm, что и `Внутреннем контуре` + - режим `full-screen` у detail-pane переводит свойства в правую колонку по тому же принципу, что и в `Внутреннем контуре` + - activity/comment composer внутри узкой detail-pane должен использовать compact peek-канон, а не растянутый page-form вид + - header intake-detail не использует внешнеконтурный toolbar как есть; sequence pill, status pill и decision buttons собираются в один compact peek-row без вылета за край detail-pane + - CTA `Принять / Отклонить` в intake-detail не могут иметь фиксированную ширину, которая ломает side-peek; на светлом accent-fill текст всегда тёмный, hover идёт в более светлый тон того же акцента + - модалка `Создать входящий рабочий элемент` центрируется как остальные create/edit modal, использует glass shell, `nodedc-modal-input`, `nodedc-modal-editor`, `nodedc-modal-primary-button` и `nodedc-modal-secondary-button` + - quick-actions menu по троеточию на карточке обязано открываться из корректного viewport-anchor без оффсета; если локальный card-layer ломает геометрию, menu возвращается в `body` portal, но сохраняет правильный z-layer и привязку к trigger + - quick-actions по троеточию не реализуются как отдельный спец-вид меню; они используют тот же popper/portal dropdown-паттерн, что и рабочие меню `Статус / Приоритет`, чтобы trigger, offset и z-layer вели себя одинаково + - реализация quick-actions выносится в shared `ActionDropdown`; карточки и detail-view не держат собственный `isMenuActive`, локальный outside-click и отдельный anchor-state для `...` - popup выбора `Приоритет / Метки` внутри detail view не рендерится inline в property-row; он обязан уходить в `portal` - секции с dropdown-trigger внутри blur/glass shell обязаны иметь `overflow: visible` и `isolation: isolate`, иначе popup визуально “тонет” внутри блока - при переключении `Открытые / Закрытые` store обязан очистить stale request list до нового fetch, чтобы пользователь не видел flash старой верстки diff --git a/HDROPDOWN-CANON.md b/HDROPDOWN-CANON.md new file mode 100644 index 0000000..5f80d56 --- /dev/null +++ b/HDROPDOWN-CANON.md @@ -0,0 +1,376 @@ +# HDROPDOWN CANON + +Документ фиксирует единый канон dropdown-окон NODE.DC. + +Цель документа: +- убрать локальные самодельные реализации выпадающих окон +- перевести dropdown на переиспользуемые shared-компоненты +- зафиксировать единый контракт для trigger, portal, placement, стилей и поведения +- исключить повторение проблемы, когда соседние controls работают по-разному на одном и том же экране + +## Базовый принцип + +Dropdown в системе не является "локальной маленькой версткой рядом с кнопкой". + +Dropdown это: +- отдельный floating-layer +- отдельный reusable UI-компонент +- предсказуемый контракт открытия и закрытия +- единый визуальный канон + +Если на экране есть несколько выпадающих окон, пользователь не должен видеть разные механики: +- одно окно открывается по тексту +- второе только по стрелке +- третье клипается контейнером +- четвертое открывается с другим типом привязки + +Это считается дефектом стандартизации. + +## Какие dropdown бывают + +В проекте фиксируются три типа выпадающих окон. + +### 1. Selection dropdown + +Используется там, где пользователь выбирает значение. + +Примеры: +- `Статус` +- `Приоритет` +- `Дата` +- `Назначенные` +- `Метки` +- выбор проекта +- выбор модуля + +Смысл: +- пользователь выбирает одно или несколько значений +- у dropdown есть список опций +- часто есть поиск +- текущее значение отображается в trigger + +Канонические reference-реализации: +- [state/base.tsx](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/apps/web/core/components/dropdowns/state/base.tsx:45) +- [priority.tsx](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/apps/web/core/components/dropdowns/priority.tsx:298) + +### 2. Action dropdown + +Используется там, где пользователь вызывает набор действий. + +Примеры: +- `...` на карточке задачи +- `...` в detail-header +- быстрые действия у рабочего элемента + +Смысл: +- dropdown не меняет поле напрямую +- dropdown показывает список команд +- каждая строка запускает `action` + +Канонический shared-компонент: +- [action-dropdown.tsx](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx:109) + +### 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 нужно ответить только на два вопроса: + +1. Это выбор значения или набор действий? +2. Есть ли уже shared-компонент для этого типа? + +Если это выбор значения: +- использовать существующий selection-dropdown +- расширять existing base-component, а не писать новый popup с нуля + +Если это список действий: +- использовать `ActionDropdown` +- не создавать отдельную локальную механику для карточки, detail-view или sidebar + +## Канонический стек для dropdown + +Любой новый dropdown должен опираться на один и тот же стек: +- реальный `trigger` +- `usePopper` +- `portal` +- `outside click close` +- `keyboard close/open` +- `preventOverflow` +- `flip` + +Для action dropdown текущий канон зафиксирован в: +- [action-dropdown.tsx](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/packages/ui/src/dropdowns/action-dropdown.tsx:118) + +Для selection dropdown reference-стек уже зафиксирован в: +- [state/base.tsx](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/apps/web/core/components/dropdowns/state/base.tsx:45) + +## 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](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/apps/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx:126) +- 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 + +Порядок работы: +1. определить тип dropdown +2. выбрать shared-компонент +3. выбрать canonical placement +4. открыть popup через portal +5. применить glass-канон +6. проверить поведение внутри 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 у кнопки `...` + +## Минимальный 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-компонент +- переиспользовать его на всех экранах +- документировать новый вариант сразу в этом файле