NODEDC_TASKMANAGER/HDESIGN-CODE.md

25 KiB
Raw Blame History

HDESIGN CODE

Документ фиксирует канон интерфейса NODE.DC, чтобы не обсуждать одни и те же правила повторно.

Связанные документы:

Источник цветов

Цветовые правила

  • 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 должны быть читаемы и не прилипать к краям.

Чекеры

  • Для бинарных настроек в glass-интерфейсе используется круглый checker в стиле фильтров отображения.
  • Активное состояние:
    • круг залит rgb(var(--nodedc-accent-rgb))
    • внутри маленькая точка rgb(var(--nodedc-on-accent-rgb))
  • Неактивное состояние:
    • круг на мягком white/10
    • без внешнего outline и без синей browser-рамки
  • Текстовый статус рядом с checker может дублировать состояние, но сам визуальный якорь должен оставаться круглым, а не квадратным checkbox.

Toolbar и верхние панели

  • Элементы верхней панели центрируются по одной горизонтальной оси.
  • Активный layout/tool mode выделяется кругом акцентного цвета, не квадратной плашкой.
  • Кнопки Отображение, Аналитика, Добавить рабочий элемент:
    • одинаковая высота
    • каноничные радиусы
    • нормальные горизонтальные paddings, чтобы текст не лип к краям

Карточки

  • Внутренние карточки строятся по симметричным верхним и нижним padding.
  • Верхняя ось:
    • аватар
    • имя
    • вторичная строка
    • action-circle справа должны сидеть на согласованной геометрии
  • Нижняя ось:
    • assignee bubbles
    • дата должны быть симметричны верхней
  • Для списков карточек Внешних контуров используется тот же вертикальный ритм, что и у Внутреннего контура:
    • контейнер списка не плотнее space-y-3
    • нельзя лепить карточки вплотную друг к другу

Dropdown и popup

  • Все dropdown/popup приводятся к единому matte glass канону.
  • Подробный архитектурный и поведенческий регламент dropdown-окон вынесен в 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

{isOpen &&
  typeof document !== "undefined" &&
  createPortal(
    <Combobox.Options className="fixed z-[420]" static>
      <div
        data-prevent-outside-click
        className="nodedc-dropdown-surface nodedc-external-popup-anchor"
      >
        ...
      </div>
    </Combobox.Options>,
    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

<Button className="nodedc-external-primary-button">...</Button>
<IconButton className="nodedc-external-icon-button" ... />
<div className="nodedc-external-readonly-value">
  <SomeIcon className="h-3.5 w-3.5 text-tertiary" />
  <span className="text-12 font-medium text-primary">...</span>
</div>
<div className="nodedc-external-property-row">
  <div className="nodedc-external-property-label">
    <SomeIcon className="h-4 w-4 flex-shrink-0" />
    <span>Label</span>
  </div>
  <div className="nodedc-external-property-value">
    <SomeValue />
  </div>
</div>
<Dropdown
  button={
    <div className="nodedc-external-property-control text-[13px] font-medium">
      <SomeIcon className="h-3.5 w-3.5 flex-shrink-0 text-tertiary" />
      <span className="text-primary">Value</span>
    </div>
  }
/>

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:
<Button className="nodedc-external-primary-button">...</Button>
  • Route-aware quick action hide:
const pathname = usePathname();
if (pathname?.includes("/external-contours")) return null;
  • List spacing:
<div key={resolvedTab} className="space-y-3">
  {filteredRequestIds.map((requestId) => (
    <ExternalContoursListItem key={requestId} ... />
  ))}
</div>
  • Pending tab anti-flash:
const [pendingTab, setPendingTab] = useState<TInboxIssueCurrentTab | null>(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:
.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:
<PriorityDropdown
  placement="top-start"
  buttonContainerClassName="nodedc-external-property-control-shell ..."
  button={
    <div className="nodedc-external-property-control text-[13px] font-medium">
      ...
    </div>
  }
/>
  • Detail toolbar cluster:
<div className="nodedc-external-toolbar-cluster">
  <button type="button" className="nodedc-external-icon-button">...</button>
  <button type="button" className="nodedc-external-icon-button">...</button>
</div>
  • Property control:
<div className="nodedc-external-property-control text-[13px] font-medium">
  <SomeIcon className="h-3.5 w-3.5 flex-shrink-0 text-tertiary" />
  <span className="text-primary">...</span>
</div>
  • Root tab switch without stale flash:
const [pendingTab, setPendingTab] = useState<TInboxIssueCurrentTab | null>(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:
this.requestIds = [];
this.requests = {};
this.loader = "init-loading";
this.currentTab = tab;
  • Portal popup с фиксированной стратегией:
const { styles, attributes } = usePopper(referenceElement, popperElement, {
  strategy: "fixed",
  placement: placement ?? "bottom-start",
});
  • Property popup without boxed artifact:
<IssueLabelSelect
  rootClassName="w-full overflow-visible"
  buttonContainerClassName="nodedc-external-property-control-shell h-full w-full overflow-visible"
  label={
    <div className="nodedc-external-property-control text-[13px] font-medium">
      <LabelPropertyIcon className="h-3.5 w-3.5 flex-shrink-0 text-tertiary" />
      <span className="truncate text-primary">...</span>
    </div>
  }
/>
  • Контейнер секции с trigger:
<div className="nodedc-external-section overflow-visible px-4 py-4">
  ...
</div>