412 lines
25 KiB
Markdown
412 lines
25 KiB
Markdown
# 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 должны быть читаемы и не прилипать к краям.
|
||
|
||
## Чекеры
|
||
- Для бинарных настроек в 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](/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(
|
||
<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
|
||
```tsx
|
||
<Button className="nodedc-external-primary-button">...</Button>
|
||
```
|
||
|
||
```tsx
|
||
<IconButton className="nodedc-external-icon-button" ... />
|
||
```
|
||
|
||
```tsx
|
||
<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>
|
||
```
|
||
|
||
```tsx
|
||
<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>
|
||
```
|
||
|
||
```tsx
|
||
<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:
|
||
```tsx
|
||
<Button className="nodedc-external-primary-button">...</Button>
|
||
```
|
||
|
||
- Route-aware quick action hide:
|
||
```tsx
|
||
const pathname = usePathname();
|
||
if (pathname?.includes("/external-contours")) return null;
|
||
```
|
||
|
||
- List spacing:
|
||
```tsx
|
||
<div key={resolvedTab} className="space-y-3">
|
||
{filteredRequestIds.map((requestId) => (
|
||
<ExternalContoursListItem key={requestId} ... />
|
||
))}
|
||
</div>
|
||
```
|
||
|
||
- Pending tab anti-flash:
|
||
```tsx
|
||
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:
|
||
```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
|
||
<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:
|
||
```tsx
|
||
<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:
|
||
```tsx
|
||
<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:
|
||
```tsx
|
||
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:
|
||
```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
|
||
<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:
|
||
```tsx
|
||
<div className="nodedc-external-section overflow-visible px-4 py-4">
|
||
...
|
||
</div>
|
||
```
|