23 KiB
23 KiB
HDESIGN CODE
Документ фиксирует канон интерфейса NODE.DC, чтобы не обсуждать одни и те же правила повторно.
Источник цветов
- Основной runtime-конфиг цветов: design.config.json
- Рабочая web-копия: 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:
- если это зафиксированный green CTA, текст должен быть контрастным и читаемым
- 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.
- Запрещены:
- квадратные 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 не растягивается на всю свободную ширину экрана; она использует тот же
IssueViewside-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-cardshell, что и карточкаВнутреннего контура; 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 возвращается в
bodyportal, но сохраняет правильный z-layer и привязку к trigger - quick-actions по троеточию не реализуются как отдельный спец-вид меню; они используют тот же popper/portal dropdown-паттерн, что и рабочие меню
Статус / Приоритет, чтобы trigger, offset и z-layer вели себя одинаково - реализация quick-actions выносится в shared
ActionDropdown; карточки и detail-view не держат собственныйisMenuActive, локальный outside-click и отдельный anchor-state для...
- правая detail-pane не растягивается на всю свободную ширину экрана; она использует тот же
- 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>