From 3f6219fc507cc4ab840a7048588d4874a358ebe4 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Wed, 22 Apr 2026 14:30:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A0=D0=A5=20-=20=D0=9C=D0=95=D0=96?= =?UTF-8?q?=D0=9F=D0=A0=D0=9E=D0=95=D0=9A=D0=A2=D0=9D=D0=90=D0=AF=20=D0=9A?= =?UTF-8?q?=D0=9E=D0=9C=D0=9C=D0=A3=D0=9D=D0=98=D0=9A=D0=90=D0=A6=D0=98?= =?UTF-8?q?=D0=AF:=20=D1=84=D1=83=D0=BD=D0=B4=D0=B0=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=20=D1=80=D0=B5=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B8=20=D1=84=D0=B8=D0=BA=D1=81=D0=B0=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?UI-=D1=82=D0=B5=D1=85=D0=B4=D0=BE=D0=BB=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HDESIGN-CODE.md | 10 +- HDROPDOWN-CANON.md | 3 + HUI-CANON-AUDIT.md | 22 +- .../(projects)/project-shell-top-toolbar.tsx | 2 +- plane-src/apps/web/app/root.tsx | 46 +++- .../projects/external-contours/board-item.tsx | 8 +- .../projects/external-contours/list-item.tsx | 23 +- .../display-filters/display-properties.tsx | 2 +- .../filters/header/helpers/filter-option.tsx | 2 +- .../filters/header/layout-selection.tsx | 4 +- .../header/mobile-layout-selection.tsx | 2 +- .../kanban/internal-contour-card.tsx | 3 +- .../shared/nodedc-work-item-card.tsx | 8 +- .../apps/web/core/components/project/form.tsx | 2 +- .../project/project-feature-update.tsx | 2 +- plane-src/apps/web/styles/globals.css | 61 +++-- .../dropdown-standardization-debt.md | 233 ++++++++++++++++++ .../packages/propel/src/button/helper.tsx | 2 +- .../propel/src/icon-button/helper.tsx | 2 +- .../packages/propel/src/toolbar/toolbar.tsx | 3 +- plane-src/packages/ui/src/button/helper.tsx | 6 +- 21 files changed, 385 insertions(+), 61 deletions(-) create mode 100644 plane-src/docs/technical-debts/dropdown-standardization-debt.md diff --git a/HDESIGN-CODE.md b/HDESIGN-CODE.md index f11a904..8e8ab11 100644 --- a/HDESIGN-CODE.md +++ b/HDESIGN-CODE.md @@ -5,6 +5,7 @@ Связанные документы: - архитектурный регламент 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) @@ -47,7 +48,9 @@ - Все кнопки без жёсткого outline. - Primary button: - фон: акцентный или `active_card_rgb` - - текст: всегда чёрный или очень тёмный, если фон светлый + - текст: определяется автоматически по контрасту заливки + - если заливка светлая, текст тёмный + - если заливка тёмная, текст светлый - hover: более светлая версия того же цвета - правило распространяется на все filled CTA: - `Добавить` @@ -58,7 +61,8 @@ - любые акцентные toolbar-кнопки - это правило обязательно и для `Внешних контуров`: `Добавить запрос` не может иметь светлый текст на светлом фоне - Save/update button: - - если это зафиксированный green CTA, текст должен быть контрастным и читаемым + - если это CTA на `accent_rgb` или `active_card_rgb`, текст не задаётся вручную белым или чёрным + - используется системное контрастное значение - hover осветляет текущий тон, а не уходит в синий - Secondary button: - тёмный glass фон @@ -110,7 +114,7 @@ - Search shell внутри popup должен использовать тот же стиль, что и сам popup. - Filled CTA внутри popup и модалок подчиняются тому же правилу: - светлый акцентный фон - - тёмный/чёрный текст + - контрастный текст от реальной яркости фона - hover только в более светлый тон того же цвета ### Portal правило diff --git a/HDROPDOWN-CANON.md b/HDROPDOWN-CANON.md index 5f80d56..5a7021b 100644 --- a/HDROPDOWN-CANON.md +++ b/HDROPDOWN-CANON.md @@ -2,6 +2,9 @@ Документ фиксирует единый канон dropdown-окон NODE.DC. +Связанный техдолг: +- [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) + Цель документа: - убрать локальные самодельные реализации выпадающих окон - перевести dropdown на переиспользуемые shared-компоненты diff --git a/HUI-CANON-AUDIT.md b/HUI-CANON-AUDIT.md index 79cc413..f4d6d34 100644 --- a/HUI-CANON-AUDIT.md +++ b/HUI-CANON-AUDIT.md @@ -5,6 +5,9 @@ и [HDROPDOWN-CANON.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/HDROPDOWN-CANON.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) + Цель: - увидеть все экраны проекта в одном месте - отделить каноничные shared-dropdown от legacy-механик @@ -245,21 +248,22 @@ ### 1. Внутренний контур Статус: -- основной эталон +- основной эталон по `board + detail-shell + cards + activity + properties` Осталось: -- дочистить legacy action-menu вне основной карточки +- дочистить legacy action-menu вне основной карточки и detail-secondary слоёв +- добить альтернативные view `list / calendar / gantt / spreadsheet` - добить group headers, spreadsheet header menus, calendar quick add - унифицировать secondary popup в relations, attachments, comments ### 2. Внешние контуры Статус: -- частично приведён +- cards и detail-shell близки к канону, но модуль ещё не закрыт полностью Осталось: -- перевести [actions-menu.tsx](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/apps/web/ce/components/projects/external-contours/actions-menu.tsx:1) на тот же action-dropdown канон -- проверить все top-toolbar и detail-surface ещё раз по glass/radius/button rules +- перевести [actions-menu.tsx](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src/apps/web/ce/components/projects/external-contours/actions-menu.tsx:1) на тот же action-dropdown канон, если там ещё остались legacy-path +- проверить дополнительное окно информации и related detail-surface ещё раз по glass/radius/button rules - сравнить все detail-row и popup с эталоном `Внутреннего контура` ### 3. Предложения / Intake @@ -345,6 +349,14 @@ Это значит: - эти экраны пока не проходили такой же системный канонический прогон, как `Внутренний контур`, `Внешние контуры` и `Intake` - их надо отдельно сверить по dropdown, button, popup, glass shell, toolbar и spacing +- отдельный приоритет внутри этого блока: + - `Analytics overview` + - `Workspace dashboard / Home` + - `Drafts` + - `Profile` + - `Stickies` + - `Browse / All issues / Workspace view` + - `Settings` ## Общий список страниц, которые требуют полноценного UI-pass diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/project-shell-top-toolbar.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/project-shell-top-toolbar.tsx index a7f9127..8b7513d 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/project-shell-top-toolbar.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/project-shell-top-toolbar.tsx @@ -116,7 +116,7 @@ const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu() { > diff --git a/plane-src/apps/web/app/root.tsx b/plane-src/apps/web/app/root.tsx index 236064a..3c38ddb 100644 --- a/plane-src/apps/web/app/root.tsx +++ b/plane-src/apps/web/app/root.tsx @@ -36,10 +36,50 @@ import "@fontsource/ibm-plex-mono"; const APP_TITLE = "NODE.DC | Self-hosted task management workspace."; +const DARK_TEXT_RGB = [11, 17, 23] as const; +const LIGHT_TEXT_RGB = [245, 247, 251] as const; + +const formatRgbTuple = (rgb: readonly number[]) => rgb.join(" "); +const formatCssRgb = (rgb: readonly number[]) => `rgb(${rgb.join(" ")})`; + +const blendRgb = (rgb: readonly number[], target: number, ratio: number) => + rgb.map((channel) => Math.round(channel * (1 - ratio) + target * ratio)) as [number, number, number]; + +const toRelativeLuminance = (rgb: readonly number[]) => { + const [r, g, b] = rgb.map((channel) => { + const normalized = channel / 255; + return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4; + }); + + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +}; + +const getReadableTextRgb = (rgb: readonly number[]) => (toRelativeLuminance(rgb) > 0.52 ? DARK_TEXT_RGB : LIGHT_TEXT_RGB); + +const accentRgb = designConfig.nodedc.accent_rgb as [number, number, number]; +const activeCardRgb = designConfig.nodedc.active_card_rgb as [number, number, number]; +const passiveCardRgb = designConfig.nodedc.passive_card_rgb as [number, number, number]; +const accentHoverRgb = blendRgb(accentRgb, 255, 0.18); +const accentActiveRgb = blendRgb(accentRgb, 0, 0.1); +const onAccentRgb = getReadableTextRgb(accentRgb); +const onActiveCardRgb = getReadableTextRgb(activeCardRgb); +const onPassiveCardRgb = getReadableTextRgb(passiveCardRgb); + const designConfigStyle = { - "--nodedc-accent-rgb": designConfig.nodedc.accent_rgb.join(" "), - "--nodedc-card-passive-rgb": designConfig.nodedc.passive_card_rgb.join(" "), - "--nodedc-card-active-rgb": designConfig.nodedc.active_card_rgb.join(" "), + "--nodedc-accent-rgb": formatRgbTuple(accentRgb), + "--nodedc-card-passive-rgb": formatRgbTuple(passiveCardRgb), + "--nodedc-card-active-rgb": formatRgbTuple(activeCardRgb), + "--nodedc-on-accent-rgb": formatRgbTuple(onAccentRgb), + "--nodedc-on-card-active-rgb": formatRgbTuple(onActiveCardRgb), + "--nodedc-on-card-passive-rgb": formatRgbTuple(onPassiveCardRgb), + "--brand-default": formatCssRgb(accentRgb), + "--brand-300": formatCssRgb(blendRgb(accentRgb, 255, 0.35)), + "--brand-700": formatCssRgb(blendRgb(accentRgb, 0, 0.25)), + "--bg-accent-primary": formatCssRgb(accentRgb), + "--bg-accent-primary-hover": formatCssRgb(accentHoverRgb), + "--bg-accent-primary-active": formatCssRgb(accentActiveRgb), + "--txt-on-color": formatCssRgb(onAccentRgb), + "--txt-icon-on-color": formatCssRgb(onAccentRgb), } as CSSProperties; export const links: LinksFunction = () => [ diff --git a/plane-src/apps/web/ce/components/projects/external-contours/board-item.tsx b/plane-src/apps/web/ce/components/projects/external-contours/board-item.tsx index 574a0b9..a9d62b9 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/board-item.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/board-item.tsx @@ -122,12 +122,14 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard const sourceStateIds = useMemo(() => targetOptions?.states?.map((state) => state.id) ?? [], [targetOptions?.states]); const selectedState = canEditTargetIssue ? getStateById(issue.state_id) : sourceStateMap[issue.state_id ?? ""]; const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : []; - const foregroundClasses = isActive ? "text-[#111111]" : "text-white"; + const foregroundClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white"; const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]"; const pillBackgroundClasses = - isActive ? "bg-black/10 text-[#111111]" : "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white"; + isActive + ? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]" + : "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white"; const iconBubbleClasses = isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white"; - const statusIconColor = selectedState?.color ?? (isActive ? "#111111" : "var(--text-color-primary)"); + const statusIconColor = selectedState?.color ?? (isActive ? "rgb(var(--nodedc-on-card-active-rgb))" : "var(--text-color-primary)"); const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none"); if (!issue) return null; diff --git a/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx b/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx index 27b7284..ece6d8d 100644 --- a/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx +++ b/plane-src/apps/web/ce/components/projects/external-contours/list-item.tsx @@ -75,7 +75,10 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt />
{requesterName}
@@ -83,7 +86,12 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
-
+
{issue.project_detail?.identifier || "REQ"}-{issue.sequence_id}
@@ -97,7 +105,12 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
-
+
{contourName}
@@ -106,7 +119,7 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt

{issue.name} @@ -141,7 +154,7 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
{renderFormattedDate(lastUpdatedAt ?? "")} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx index 612c6b1..f6a42d9 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/display-filters/display-properties.tsx @@ -73,7 +73,7 @@ export const FilterDisplayProperties = observer(function FilterDisplayProperties type="button" className={`rounded-full border-0 px-3 py-1.5 text-12 transition-all ${ displayProperties?.[displayProperty.key] - ? "bg-[rgb(var(--nodedc-accent-rgb))] text-[#0b1117]" + ? "bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))]" : "bg-white/5 text-secondary hover:bg-white/8" }`} onClick={() => diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx index 7626b12..53666d4 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/helpers/filter-option.tsx @@ -29,7 +29,7 @@ export function FilterOption(props: Props) {
diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx index 6fe568b..8a6d9d2 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx @@ -48,7 +48,9 @@ export function LayoutSelection(props: Props) { layout={layout.key} size={14} strokeWidth={2} - className={`size-3.5 ${selectedLayout == layout.key ? "text-[#0b1117]" : "text-secondary"}`} + className={`size-3.5 ${ + selectedLayout == layout.key ? "text-[rgb(var(--nodedc-on-accent-rgb))]" : "text-secondary" + }`} /> diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/mobile-layout-selection.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/mobile-layout-selection.tsx index c59cfbb..e0a902a 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/mobile-layout-selection.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/mobile-layout-selection.tsx @@ -27,7 +27,7 @@ export function MobileLayoutSelection({ - + {activeLayout && ( )} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx index c55d2cb..f16a21f 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx @@ -75,7 +75,8 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban const selectedState = getStateById(issue.state_id); const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : []; const { pillBackgroundClasses, iconBubbleClasses } = getNodedcWorkItemCardAppearance(isActive); - const statusIconColor = selectedState?.color ?? (isActive ? "#111111" : "var(--text-color-primary)"); + const statusIconColor = + selectedState?.color ?? (isActive ? "rgb(var(--nodedc-on-card-active-rgb))" : "var(--text-color-primary)"); const creatorName = creatorDetails?.display_name ?? t("common.none"); const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none"); diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/shared/nodedc-work-item-card.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/shared/nodedc-work-item-card.tsx index dd51c46..0dd1a89 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/shared/nodedc-work-item-card.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/shared/nodedc-work-item-card.tsx @@ -23,11 +23,13 @@ type TNodedcWorkItemCardProps = { export const getNodedcWorkItemCardAppearance = (isActive: boolean) => ({ surfaceClassName: isActive - ? "bg-[rgb(var(--nodedc-card-active-rgb))] text-[#111111]" + ? "bg-[rgb(var(--nodedc-card-active-rgb))] text-[rgb(var(--nodedc-on-card-active-rgb))]" : "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white", - foregroundClasses: isActive ? "text-[#111111]" : "text-white", + foregroundClasses: isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-[rgb(var(--nodedc-on-card-passive-rgb))]", subtleTextClasses: isActive ? "text-[#2F4721]" : "text-[#B3B3B8]", - pillBackgroundClasses: isActive ? "bg-black/10 text-[#111111]" : "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white", + pillBackgroundClasses: isActive + ? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]" + : "bg-[rgb(var(--nodedc-card-passive-rgb))] text-[rgb(var(--nodedc-on-card-passive-rgb))]", iconBubbleClasses: isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white", }); diff --git a/plane-src/apps/web/core/components/project/form.tsx b/plane-src/apps/web/core/components/project/form.tsx index 2e44bc6..9d39519 100644 --- a/plane-src/apps/web/core/components/project/form.tsx +++ b/plane-src/apps/web/core/components/project/form.tsx @@ -435,7 +435,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) { type="submit" loading={isLoading} disabled={!isAdmin} - className="nodedc-settings-save-button min-w-[11.5rem] !text-[#0b1117] hover:!text-[#0b1117]" + className="nodedc-settings-save-button min-w-[11.5rem]" > {isLoading ? t("updating") : t("common.update_project")} diff --git a/plane-src/apps/web/core/components/project/project-feature-update.tsx b/plane-src/apps/web/core/components/project/project-feature-update.tsx index ff83f66..95f8df4 100644 --- a/plane-src/apps/web/core/components/project/project-feature-update.tsx +++ b/plane-src/apps/web/core/components/project/project-feature-update.tsx @@ -57,7 +57,7 @@ export const ProjectFeatureUpdate = observer(function ProjectFeatureUpdate(props {t("open_project")} diff --git a/plane-src/apps/web/styles/globals.css b/plane-src/apps/web/styles/globals.css index dfe68de..3f93de6 100644 --- a/plane-src/apps/web/styles/globals.css +++ b/plane-src/apps/web/styles/globals.css @@ -28,11 +28,19 @@ --editor-colors-dark-blue-background: #c9dafb; --editor-colors-purple-background: #e3d8fd; --nodedc-accent-rgb: 51 163 255; + --nodedc-on-accent-rgb: 245 247 251; --nodedc-card-passive-rgb: 42 43 46; + --nodedc-on-card-passive-rgb: 245 247 251; --nodedc-card-active-rgb: 195 255 102; + --nodedc-on-card-active-rgb: 11 17 23; --brand-default: rgb(var(--nodedc-accent-rgb)); - --brand-300: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 35%, white); + --brand-300: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 65%, white); --brand-700: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 75%, black); + --bg-accent-primary: rgb(var(--nodedc-accent-rgb)); + --bg-accent-primary-hover: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 82%, white); + --bg-accent-primary-active: color-mix(in srgb, rgb(var(--nodedc-accent-rgb)) 90%, black); + --txt-on-color: rgb(var(--nodedc-on-accent-rgb)); + --txt-icon-on-color: rgb(var(--nodedc-on-accent-rgb)); /* end background colors */ } /* background colors */ @@ -507,7 +515,7 @@ .nodedc-toolbar-icon-button[data-active="true"] .nodedc-toolbar-icon-active-dot { background: rgb(var(--nodedc-accent-rgb)); - color: #0b1117; + color: rgb(var(--nodedc-on-accent-rgb)); } .nodedc-toolbar-pill { @@ -553,7 +561,7 @@ min-height: 2.5rem; padding-inline: 1.55rem; background: rgb(var(--nodedc-accent-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-accent-rgb)) !important; } .nodedc-toolbar-primary-wide { @@ -587,7 +595,7 @@ box-shadow: none !important; border-radius: 1.25rem !important; background: rgb(var(--nodedc-accent-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-accent-rgb)) !important; padding-inline: 1.25rem !important; } @@ -596,14 +604,17 @@ } .nodedc-modal-primary-button, - .nodedc-modal-primary-button *, .nodedc-modal-danger-button, - .nodedc-modal-danger-button *, + .nodedc-modal-primary-button *, + .nodedc-modal-danger-button * { + color: rgb(var(--nodedc-on-accent-rgb)) !important; + } + .nodedc-settings-primary-button, .nodedc-settings-primary-button *, .nodedc-settings-save-button, .nodedc-settings-save-button * { - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-modal-danger-button { @@ -613,7 +624,7 @@ box-shadow: none !important; border-radius: 1.25rem !important; background: rgb(var(--nodedc-accent-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-accent-rgb)) !important; padding-inline: 1.25rem !important; } @@ -627,7 +638,7 @@ outline: none !important; box-shadow: none !important; background: rgb(var(--nodedc-accent-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-accent-rgb)) !important; } .nodedc-glass-modal button.bg-danger-primary:hover, @@ -637,7 +648,7 @@ .nodedc-glass-modal button.bg-danger-primary *, .nodedc-glass-modal button.border-danger-strong * { - color: #0b1117 !important; + color: rgb(var(--nodedc-on-accent-rgb)) !important; } .nodedc-modal-chip { @@ -761,13 +772,13 @@ box-shadow: none !important; border-radius: 1.25rem !important; background: rgb(var(--nodedc-card-active-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; padding-inline: 1.35rem !important; } .nodedc-settings-primary-button:hover { background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-settings-save-button { @@ -777,13 +788,13 @@ box-shadow: none !important; border-radius: 1.25rem !important; background: rgb(var(--nodedc-card-active-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; padding-inline: 1.45rem !important; } .nodedc-settings-save-button:hover { background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-overlay-button { @@ -1014,12 +1025,12 @@ box-shadow: none !important; border-radius: 1.25rem !important; background: rgb(var(--nodedc-card-active-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-auth-primary-button:hover { background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-error-shell { @@ -1055,13 +1066,13 @@ box-shadow: none !important; border-radius: 1.2rem !important; background: rgb(var(--nodedc-card-active-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; padding-inline: 1.35rem !important; } .nodedc-error-primary:hover { background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-empty-state-primary { @@ -1071,13 +1082,13 @@ box-shadow: none !important; border-radius: 1.2rem !important; background: rgb(var(--nodedc-card-active-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; padding-inline: 1.35rem !important; } .nodedc-empty-state-primary:hover { background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-empty-state-secondary { @@ -1143,14 +1154,14 @@ background: linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%), rgb(var(--nodedc-card-active-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; box-shadow: inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.32), 0 12px 32px rgba(0, 0, 0, 0.16) !important; } .nodedc-external-card[data-active="true"] .text-primary { - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-external-card[data-active="true"] .text-secondary, @@ -1278,19 +1289,19 @@ box-shadow: none !important; border-radius: 1.35rem !important; background: rgb(var(--nodedc-card-active-rgb)) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; padding-inline: 1.6rem !important; font-weight: 600 !important; } .nodedc-external-primary-button, .nodedc-external-primary-button * { - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-external-primary-button:hover { background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important; - color: #0b1117 !important; + color: rgb(var(--nodedc-on-card-active-rgb)) !important; } .nodedc-external-empty-state { diff --git a/plane-src/docs/technical-debts/dropdown-standardization-debt.md b/plane-src/docs/technical-debts/dropdown-standardization-debt.md new file mode 100644 index 0000000..dc143ff --- /dev/null +++ b/plane-src/docs/technical-debts/dropdown-standardization-debt.md @@ -0,0 +1,233 @@ +# Dropdown Standardization Debt + +Дата фиксации: `2026-04-22` + +Статус: `active` + +Связанные документы: +- [HDESIGN-CODE.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/HDESIGN-CODE.md) +- [HDROPDOWN-CANON.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/HDROPDOWN-CANON.md) +- [HUI-CANON-AUDIT.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/HUI-CANON-AUDIT.md) + +## Зачем фиксируется этот техдолг + +В проекте уже выполнена большая часть канонизации dropdown-окон, но этап еще не завершен. + +Это означает: +- общий контракт для dropdown уже определен +- значимая часть экранов уже переведена на shared-компоненты +- legacy-механики еще не вычищены полностью +- визуальный слой новых dropdown и связанных элементов еще не доведен на всех экранах до одного и того же дизайн-эталона + +Этот документ нужен, чтобы: +- не потерять текущий контекст миграции +- не откатиться обратно к локальным menu-wrapper решениям +- иметь единый технический ориентир перед следующим большим этапом редизайна +- отделить архитектурную канонизацию dropdown-stack от визуальной доводки экранов + +## Что уже считается сделанным + +На текущем этапе уже зафиксированы и частично внедрены следующие принципы: + +### 1. Типизация dropdown по смыслу + +В системе признаются только три вида выпадающих окон: +- `Selection dropdown` +- `Action dropdown` +- `Context menu` + +Правило: +- если пользователь выбирает значение, используется `SelectionDropdown` +- если пользователь вызывает список действий, используется `ActionDropdown` +- `ContextMenu` допустим только как secondary/right-click механизм + +### 2. Общий канон поведения + +Dropdown больше не считается локальной версткой рядом с кнопкой. + +Dropdown должен быть: +- отдельным floating-layer +- привязанным к реальному trigger-элементу +- открываемым через общий popper/portal стек +- одинаковым по контракту открытия, закрытия и позиционирования + +### 3. Уже мигрированные слои + +На общий канон уже переведены крупные блоки: +- project/module breadcrumbs +- quick-actions карточек и detail action-menu +- desktop header action-menu +- desktop sorting dropdown +- mobile selection/menu слой для части экранов +- searchable select-пикеры +- form/settings select-пикеры + +### 4. Уже определен визуальный канон + +Для dropdown зафиксирован matte black glass подход: +- темный glass surface +- blur +- мягкая граница +- единый popup-shell +- единый ритм option-row +- запрет на случайные outline, clip и локальные подложки + +## В чем состоит незакрытый долг + +Долг не в том, что dropdown “вообще не стандартизированы”. + +Долг в том, что стандартизация завершена не до конца на уровне всей системы. + +Проблема сейчас состоит из четырех частей. + +### 1. Не все legacy dropdown переведены на shared-канон + +В проекте еще остаются места, где используются старые механики: +- `CustomMenu` +- `CustomSelect` +- `CustomSearchSelect` +- локальные menu-wrapper решения вокруг существующих control-компонентов + +Это критично, потому что: +- соседние controls могут вести себя по-разному на одном экране +- фиксы начинают делаться точечно, а не системно +- новая UI-логика начинает разъезжаться по разным слоям + +### 2. Не все dropdown доведены до одной и той же схемы переиспользования + +Даже там, где поведение уже близко к канону, еще встречаются расхождения: +- разные trigger-shell +- разные popup-wrapper +- разная схема option-render +- разные локальные классы для похожих dropdown + +Это опасно тем, что код выглядит “уже почти каноничным”, но реально еще не является одним и тем же reusable-механизмом. + +### 3. Визуальный канон еще не протянут через все экраны + +Архитектурный слой во многих местах уже выровнен, но визуально часть экранов еще живет на смешанном состоянии: +- popup уже новый, а surrounding layout еще старый +- dropdown уже каноничный, а рядом карточка, detail-pane или modal сверстаны по старому ритму +- glass shell и spacing формально есть, но не совпадают с эталоном `Внутреннего контура` + +### 4. Нет финального закрытия этапа по критерию Definition of Done + +Этап можно считать закрытым только тогда, когда: +- legacy dropdown-механики перестают быть обязательным рабочим слоем +- новые экраны не изобретают свои popup-решения +- существующие экраны используют только зафиксированные shared-компоненты +- UI доведен до одного и того же канона не только функционально, но и визуально + +Сейчас этот критерий еще не выполнен. + +## Что запрещено до полного закрытия долга + +До закрытия этого техдолга запрещено: +- добавлять новый dropdown через локальный `isOpen` и `absolute` popup +- делать новый `...` через отдельный компонент, если уже есть `ActionDropdown` +- использовать `CustomMenu` как “быструю временную затычку” для нового action-menu +- использовать `CustomSelect` или `CustomSearchSelect` для новых экранов, если их можно закрыть каноничными shared-компонентами +- лечить проблемы positioning случайными `left/right translate` без исправления placement-контракта +- исправлять визуальный разнобой только внешней оберткой, если проблема находится внутри базового reusable-компонента + +## Что обязательно делать при продолжении этапа + +Любая новая работа по dropdown и связанным UI-элементам должна идти в таком порядке: + +1. Определить тип dropdown: + `selection`, `action` или `context`. +2. Проверить, есть ли shared-компонент для этого сценария. +3. Если shared-компонент есть: + дорабатывать его, а не создавать новый локальный wrapper. +4. Если shared-компонент не покрывает кейс полностью: + расширять shared-контракт, чтобы изменение переиспользовалось и на других экранах. +5. После архитектурного изменения дотягивать экран до дизайн-канона целиком, а не только dropdown. + +## Граница между архитектурой и редизайном + +Важно разделять два разных этапа. + +### Текущий этап + +Это этап архитектурной канонизации dropdown-layer. + +Его задача: +- вычистить legacy popup-механики +- выровнять trigger/popup/portal/placement слой +- свести похожие выпадающие окна к одним и тем же shared-компонентам + +### Следующий большой этап + +Это этап редизайна UI-элементов под новые каноны. + +Его задача: +- пройтись по живым экранам проекта +- довести карточки, detail-pane, filters, modal, toolbar и list layouts до одного визуального эталона +- использовать уже зафиксированный dropdown-канон как инфраструктурную основу, а не спорить заново о popup-поведении + +Именно этот второй этап теперь считается более приоритетным. + +## Какие области еще требуют возврата + +На момент фиксации документа к этапу нужно вернуться минимум по следующим направлениям: + +### 1. Legacy select/search layer + +Нужно дочистить остатки старых select-механик: +- `automation` +- `export` +- `pages modal` +- `api-token` +- `publish-project` +- `rich-filters` + +Цель: +- убрать остаточные прямые зависимости от legacy select/search dropdown-слоя +- завершить переход на `SelectionDropdown` и `SearchSelectionDropdown` + +### 2. Legacy action/context layer + +Нужно проверить, не остались ли вторичные места, где visible action-menu еще живет на старом menu-engine. + +Цель: +- не допустить, чтобы `ActionDropdown` существовал как “еще один способ” +- закрепить его как основной стандарт для action popup + +### 3. Экранный визуальный проход + +Нужно отдельно пройтись по: +- `Внутреннему контуру` +- `Внешним контурам` +- `Предложениям` +- `Модулям` +- `Циклам` +- `Видам` +- `Страницам` +- workspace/sidebar/settings flows + +Цель: +- довести не только сами dropdown, но и surrounding UI до общего дизайн-ритма + +## Признак завершения техдолга + +Этот документ можно считать закрытым только если одновременно выполнены все условия: + +- на новых экранах больше не появляются локальные dropdown-механики +- legacy `CustomMenu` не используется как основной visible action dropdown +- legacy `CustomSelect` и `CustomSearchSelect` перестают быть рабочим стандартом для новых задач +- общие shared dropdown покрывают реальные product-сценарии без точечных обходов +- визуально dropdown, filters, action menu и related cards/modals подчиняются одному и тому же канону +- команда может вернуться к экрану через несколько недель и продолжить работу без повторного архитектурного переизобретения + +## Что делать перед стартом большого этапа редизайна + +Перед стартом следующего большого этапа нужно считать зафиксированными следующие вводные: + +- dropdown-канон уже существует и описан +- техдолг по миграции еще активен +- редизайн не должен плодить новые popup-механики +- если при редизайне находится dropdown-кейс, которого нет в shared-компонентах, сначала расширяется shared-компонент, а только потом меняется экран + +Итоговое правило: +- редизайн делается поверх канона +- канон не ломается ради скорости редизайна diff --git a/plane-src/packages/propel/src/button/helper.tsx b/plane-src/packages/propel/src/button/helper.tsx index 18130b1..941b4e2 100644 --- a/plane-src/packages/propel/src/button/helper.tsx +++ b/plane-src/packages/propel/src/button/helper.tsx @@ -13,7 +13,7 @@ export const buttonVariants = cva( variants: { variant: { primary: - "bg-accent-primary text-on-color hover:bg-accent-primary-hover active:bg-accent-primary-active disabled:bg-layer-disabled disabled:text-on-color-disabled", + "bg-accent-primary text-[rgb(var(--nodedc-on-accent-rgb))] hover:bg-accent-primary-hover active:bg-accent-primary-active disabled:bg-layer-disabled disabled:text-on-color-disabled", "error-fill": "bg-danger-primary text-on-color hover:bg-danger-primary-hover active:bg-danger-primary-active disabled:bg-layer-disabled disabled:text-disabled", "error-outline": diff --git a/plane-src/packages/propel/src/icon-button/helper.tsx b/plane-src/packages/propel/src/icon-button/helper.tsx index a4e421c..58e0363 100644 --- a/plane-src/packages/propel/src/icon-button/helper.tsx +++ b/plane-src/packages/propel/src/icon-button/helper.tsx @@ -14,7 +14,7 @@ export const iconButtonVariants = cva( variants: { variant: { primary: - "bg-accent-primary text-on-color hover:bg-accent-primary-hover focus:bg-accent-primary-active active:bg-accent-primary-active disabled:bg-layer-disabled disabled:text-on-color-disabled", + "bg-accent-primary text-[rgb(var(--nodedc-on-accent-rgb))] hover:bg-accent-primary-hover focus:bg-accent-primary-active active:bg-accent-primary-active disabled:bg-layer-disabled disabled:text-on-color-disabled", "error-fill": "bg-danger-primary text-on-color hover:bg-danger-primary-hover focus:bg-danger-primary-active active:bg-danger-primary-active disabled:bg-layer-disabled disabled:text-disabled", "error-outline": diff --git a/plane-src/packages/propel/src/toolbar/toolbar.tsx b/plane-src/packages/propel/src/toolbar/toolbar.tsx index ca7565c..92321f1 100644 --- a/plane-src/packages/propel/src/toolbar/toolbar.tsx +++ b/plane-src/packages/propel/src/toolbar/toolbar.tsx @@ -130,7 +130,8 @@ const ToolbarSeparator = React.forwardRef(function ToolbarSeparator( }); const buttonVariants = { - primary: "bg-accent-primary text-on-color hover:bg-accent-primary/80 focus:bg-accent-primary/80", + primary: + "bg-accent-primary text-[rgb(var(--nodedc-on-accent-rgb))] hover:bg-accent-primary-hover focus:bg-accent-primary-active", secondary: "bg-surface-1 text-secondary border border-subtle hover:bg-surface-2 focus:bg-surface-2", outline: "border border-accent-strong text-accent-primary bg-transparent hover:bg-accent-primary/10 focus:bg-accent-primary/20", diff --git a/plane-src/packages/ui/src/button/helper.tsx b/plane-src/packages/ui/src/button/helper.tsx index 95ac851..abf243d 100644 --- a/plane-src/packages/ui/src/button/helper.tsx +++ b/plane-src/packages/ui/src/button/helper.tsx @@ -44,9 +44,9 @@ enum buttonIconStyling { export const buttonStyling: IButtonStyling = { primary: { - default: `text-on-color bg-accent-primary`, - hover: `hover:bg-accent-primary/80`, - pressed: `focus:text-custom-brand-40 focus:bg-accent-primary/80`, + default: `text-[rgb(var(--nodedc-on-accent-rgb))] bg-accent-primary`, + hover: `hover:bg-accent-primary-hover`, + pressed: `focus:bg-accent-primary-active`, disabled: `cursor-not-allowed !bg-layer-1 !text-on-color-disabled`, }, "accent-primary": {