# HDESIGN CODE Документ фиксирует канон интерфейса NODE.DC, чтобы не обсуждать одни и те же правила повторно. Связанные документы: - архитектурный регламент dropdown-окон: [HDROPDOWN-CANON.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/HDROPDOWN-CANON.md) - экранный аудит и backlog миграции: [HUI-CANON-AUDIT.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/HUI-CANON-AUDIT.md) - активный техдолг по незавершенной миграции dropdown-layer: [plane-src/docs/technical-debts/dropdown-standardization-debt.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/docs/technical-debts/dropdown-standardization-debt.md) ## Источник цветов - Основной runtime-конфиг цветов: [design.config.json](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/design.config.json) - Рабочая web-копия: [plane-src/apps/web/design.config.json](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/apps/web/design.config.json) - В рантайме используются CSS variables: - `--nodedc-accent-rgb` - `--nodedc-card-passive-rgb` - `--nodedc-card-active-rgb` ## Цветовые правила - `accent_rgb`: акцентный цвет интерфейса. - `passive_card_rgb`: пассивные карточки. - `active_card_rgb`: активные карточки и primary CTA в зелёной теме. - Primary/active элементы используют акцентный или `active_card_rgb`. - Secondary элементы не должны иметь ярких outline и цветных рамок без явной причины. ## Радиусы - Главные модалки и большие surface-контейнеры: `1.75rem` - Стандартные glass-карточки и settings-карточки: `1.35rem` - Поля ввода, селекты, secondary/primary кнопки, chip-кнопки: `1.25rem` - Малые круглые action-кнопки: `999px` или полный круг при квадратной коробке ## Outline и рамки - Внешние outline у контролов запрещены. - Синий browser outline должен быть снят и заменён на нормальный hover/focus surface. - Если нужен контур, он должен быть частью дизайна: - мягкий glass border - акцентный border для drag/drop или active-state - Красные технические outline, debug-рамки и случайные browser-box shadow запрещены. ## Glass и фон - Popup, dropdown, modal, sidebar overlays и settings-карточки используют matte black glass. - База: - тёмный полупрозрачный фон - `backdrop-filter: blur(...)` - мягкая стеклянная граница - Popup не должны выглядеть просто прозрачными. Если blur не читается, проблема в слое рендера, а не в одном `rgba`. ## Кнопки - Все кнопки без жёсткого outline. - Primary button: - фон: акцентный или `active_card_rgb` - текст: определяется автоматически по контрасту заливки - если заливка светлая, текст тёмный - если заливка тёмная, текст светлый - hover: более светлая версия того же цвета - правило распространяется на все filled CTA: - `Добавить` - `Сохранить` - `Обновить` - `Принять` - `Добавить запрос` - любые акцентные toolbar-кнопки - это правило обязательно и для `Внешних контуров`: `Добавить запрос` не может иметь светлый текст на светлом фоне - Save/update button: - если это CTA на `accent_rgb` или `active_card_rgb`, текст не задаётся вручную белым или чёрным - используется системное контрастное значение - hover осветляет текущий тон, а не уходит в синий - Secondary button: - тёмный glass фон - без border-outline - hover немного светлее базового surface - Danger button: - без кислотно-красных рамок - мягкий danger surface ## Поля и селекты - Все поля ввода, textarea, select, chip-select: - скруглённые - без внешних outline - glass background - единая вертикальная высота для одного класса контролов - Placeholder и label должны быть читаемы и не прилипать к краям. ## Toolbar и верхние панели - Элементы верхней панели центрируются по одной горизонтальной оси. - Активный layout/tool mode выделяется кругом акцентного цвета, не квадратной плашкой. - Кнопки `Отображение`, `Аналитика`, `Добавить рабочий элемент`: - одинаковая высота - каноничные радиусы - нормальные горизонтальные paddings, чтобы текст не лип к краям ## Карточки - Внутренние карточки строятся по симметричным верхним и нижним padding. - Верхняя ось: - аватар - имя - вторичная строка - action-circle справа должны сидеть на согласованной геометрии - Нижняя ось: - assignee bubbles - дата должны быть симметричны верхней - Для списков карточек `Внешних контуров` используется тот же вертикальный ритм, что и у `Внутреннего контура`: - контейнер списка не плотнее `space-y-3` - нельзя лепить карточки вплотную друг к другу ## 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 - светлый фон, если основной экран тёмный - Search shell внутри popup должен использовать тот же стиль, что и сам popup. - Filled CTA внутри popup и модалок подчиняются тому же правилу: - светлый акцентный фон - контрастный текст от реальной яркости фона - hover только в более светлый тон того же цвета ### Portal правило - Если selector/dropdown открывается внутри: - scroll-контейнера - detail-pane - карточки - properties section - sidebar - sticky header он не должен рендериться inline. - Такой popup обязан рендериться на верхнем слое через `portal` (`document.body` или эквивалент). - Inline popup в ограниченном контейнере считается дефектом, потому что даёт: - клиппинг - налезание на соседние блоки - старую “врезанную” верстку - Для `Свойств` в `Внешних контурах` dropdown по умолчанию открывается вверх: - `placement="top-start"` - причина: блок находится близко к `Активности`, popup не должен падать вниз в соседнюю секцию ### Portal anchor snippet ```tsx {isOpen && typeof document !== "undefined" && createPortal(
...
, document.body )} ``` ### Reusable классы - Accent CTA: - `.nodedc-external-primary-button` - текст внутри всегда `#0b1117` - Secondary action: - `.nodedc-external-action-button` - Secondary icon action: - `.nodedc-external-icon-button` - Readonly property/control surface: - `.nodedc-external-readonly-value` - `.nodedc-modal-field` - External property rows: - `.nodedc-external-property-row` - `.nodedc-external-property-label` - `.nodedc-external-property-value` - `.nodedc-external-property-control` - Dropdown shell: - `.nodedc-dropdown-surface` - `.nodedc-dropdown-search` - `.nodedc-dropdown-option` - External contour card/shell: - `.nodedc-external-card` - `.nodedc-external-section` - `.nodedc-external-content-shell` - Intake filter chips: - `.nodedc-filter-chip` ### Anchor snippets ```tsx ``` ```tsx ``` ```tsx
...
``` ```tsx
Label
``` ```tsx Value } /> ``` ## Drag and drop - Drag overlay использует акцентный контур. - Delete dropzone: - без красного технического свечения и без red-tinted text/fill - текст локализован - акцентный outline обязателен ## Тексты - Пользовательский UI на русском, если экран русифицирован. - Не оставлять смешанные подписи вида `Created at / Updated at / Label / State group`, если экран уже на русском. ## Правило внедрения - Новый экран или popup не стилизуется локально “на глаз”. - Сначала используется существующий shared-класс или shared-component. - Если shared-слоя нет, создаётся reusable-класс/компонент и уже через него приводятся все похожие места. - Цель: не точечная покраска одного окна, а единый системный канон. - Если блок визуально расходится со стилем системы, не добавлять поверх временную wrapper-заплатку. Нужно либо перевести блок на shared-компонент, либо переверстать локальную структуру под shared-классы. - Для экранов со вкладками/переключателями нельзя оставлять flash старой верстки. Перед refetch нужно очищать stale store-data и показывать loading shell. - Если карточки или списки разных модулей должны быть одинаковыми по канону, нельзя лечить это внешней обёрткой. Нужно менять сам внутренний layout item-компонента. - Для `Внешних контуров` это значит: - список карточек правится на уровне `list-item.tsx`, а не через внешний wrapper - gap между карточками должен совпадать с каноном `Внутреннего контура` - актуальный gap списка на текущем каноне: `space-y-3` - при tab switch между `Открытые / Закрытые` нельзя полагаться только на route param; нужен локальный `pendingTab`, чтобы stale layout не мелькал до завершения refetch - toolbar-навигация и inline actions не должны использовать старые квадратные `IconButton` остатки - свойства `Приоритет / Метки / Статус` не должны рисовать внутренние boxed-chip артефакты - popup `Приоритет / Метки` не может визуально жить внутри property-row; если он открывается из blur-shell, он обязан уходить в portal и рендериться над секцией - filled CTA вроде `Добавить запрос` используют `nodedc-external-primary-button` и всегда имеют тёмный текст - filled CTA используют чёрный/почти-чёрный текст всегда; белый текст на светлом акценте запрещён - secondary meta-иконки в карточке списка не должны иметь отдельную серую подложку, если по канону это простой inline icon - empty-state не должен использовать декоративную серую подложку под SVG; media-box прозрачный, SVG выравнивается через `display:flex` и центрирование - detail-toolbar в карточке запроса использует общий glass-cluster для листания `prev/next`, а сами кнопки внутри кластера — круглые, без квадратной подложки - `Добавить запрос` в header `Внешних контуров` — это filled accent CTA с тёмным текстом, каноничным радиусом и hover в более светлый тон того же акцента - 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 старой верстки - карточка списка `Внешних контуров` правится на уровне `list-item.tsx`, а не внешней обёрткой: - верхняя и нижняя оси собираются как у карточки `Внутреннего контура` - gap между карточками совпадает с каноном `Внутреннего контура` - empty-state иконки без декоративной подложки; если иконка визуально “плывёт”, корректируется сам SVG/media-box ### Внешние контуры: code anchors - Header CTA: ```tsx ``` - Route-aware quick action hide: ```tsx const pathname = usePathname(); if (pathname?.includes("/external-contours")) return null; ``` - List spacing: ```tsx
{filteredRequestIds.map((requestId) => ( ))}
``` - Pending tab anti-flash: ```tsx const [pendingTab, setPendingTab] = useState(null); const routeTab = (searchParams.get("currentTab") as TInboxIssueCurrentTab | null) ?? currentTab; const resolvedTab = pendingTab ?? routeTab; const isTabTransitioning = loader === "init-loading" || pendingTab !== null || routeTab !== currentTab; ``` - Card theme source: ```css .nodedc-external-card { background: rgb(var(--nodedc-card-passive-rgb)); } .nodedc-external-card[data-active="true"] { background: rgb(var(--nodedc-card-active-rgb)); color: #0b1117; } ``` - Property popup anchor: ```tsx ... } /> ``` - Detail toolbar cluster: ```tsx
``` - Property control: ```tsx
...
``` - Root tab switch without stale flash: ```tsx const [pendingTab, setPendingTab] = useState(null); const resolvedTab = pendingTab ?? routeTab; const isTabTransitioning = loader === "init-loading" || pendingTab !== null || routeTab !== currentTab; if (resolvedTab !== nextTab) { setPendingTab(nextTab); void handleCurrentTab(workspaceSlug, projectId, nextTab); router.push(`...currentTab=${nextTab}`); } ``` - Store-side tab reset: ```ts this.requestIds = []; this.requests = {}; this.loader = "init-loading"; this.currentTab = tab; ``` - Portal popup с фиксированной стратегией: ```tsx const { styles, attributes } = usePopper(referenceElement, popperElement, { strategy: "fixed", placement: placement ?? "bottom-start", }); ``` - Property popup without boxed artifact: ```tsx ... } /> ``` - Контейнер секции с trigger: ```tsx
...
```