АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: фундамент редизайна и фиксация UI-техдолга
This commit is contained in:
parent
ecb31a78f9
commit
3f6219fc50
|
|
@ -5,6 +5,7 @@
|
||||||
Связанные документы:
|
Связанные документы:
|
||||||
- архитектурный регламент dropdown-окон: [HDROPDOWN-CANON.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/HDROPDOWN-CANON.md)
|
- архитектурный регламент 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)
|
- экранный аудит и 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)
|
- Основной runtime-конфиг цветов: [design.config.json](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/design.config.json)
|
||||||
|
|
@ -47,7 +48,9 @@
|
||||||
- Все кнопки без жёсткого outline.
|
- Все кнопки без жёсткого outline.
|
||||||
- Primary button:
|
- Primary button:
|
||||||
- фон: акцентный или `active_card_rgb`
|
- фон: акцентный или `active_card_rgb`
|
||||||
- текст: всегда чёрный или очень тёмный, если фон светлый
|
- текст: определяется автоматически по контрасту заливки
|
||||||
|
- если заливка светлая, текст тёмный
|
||||||
|
- если заливка тёмная, текст светлый
|
||||||
- hover: более светлая версия того же цвета
|
- hover: более светлая версия того же цвета
|
||||||
- правило распространяется на все filled CTA:
|
- правило распространяется на все filled CTA:
|
||||||
- `Добавить`
|
- `Добавить`
|
||||||
|
|
@ -58,7 +61,8 @@
|
||||||
- любые акцентные toolbar-кнопки
|
- любые акцентные toolbar-кнопки
|
||||||
- это правило обязательно и для `Внешних контуров`: `Добавить запрос` не может иметь светлый текст на светлом фоне
|
- это правило обязательно и для `Внешних контуров`: `Добавить запрос` не может иметь светлый текст на светлом фоне
|
||||||
- Save/update button:
|
- Save/update button:
|
||||||
- если это зафиксированный green CTA, текст должен быть контрастным и читаемым
|
- если это CTA на `accent_rgb` или `active_card_rgb`, текст не задаётся вручную белым или чёрным
|
||||||
|
- используется системное контрастное значение
|
||||||
- hover осветляет текущий тон, а не уходит в синий
|
- hover осветляет текущий тон, а не уходит в синий
|
||||||
- Secondary button:
|
- Secondary button:
|
||||||
- тёмный glass фон
|
- тёмный glass фон
|
||||||
|
|
@ -110,7 +114,7 @@
|
||||||
- Search shell внутри popup должен использовать тот же стиль, что и сам popup.
|
- Search shell внутри popup должен использовать тот же стиль, что и сам popup.
|
||||||
- Filled CTA внутри popup и модалок подчиняются тому же правилу:
|
- Filled CTA внутри popup и модалок подчиняются тому же правилу:
|
||||||
- светлый акцентный фон
|
- светлый акцентный фон
|
||||||
- тёмный/чёрный текст
|
- контрастный текст от реальной яркости фона
|
||||||
- hover только в более светлый тон того же цвета
|
- hover только в более светлый тон того же цвета
|
||||||
|
|
||||||
### Portal правило
|
### Portal правило
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
Документ фиксирует единый канон dropdown-окон NODE.DC.
|
Документ фиксирует единый канон 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-компоненты
|
- перевести dropdown на переиспользуемые shared-компоненты
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
и
|
и
|
||||||
[HDROPDOWN-CANON.md](/Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/HDROPDOWN-CANON.md).
|
[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-механик
|
- отделить каноничные shared-dropdown от legacy-механик
|
||||||
|
|
@ -245,21 +248,22 @@
|
||||||
### 1. Внутренний контур
|
### 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
|
- добить group headers, spreadsheet header menus, calendar quick add
|
||||||
- унифицировать secondary popup в relations, attachments, comments
|
- унифицировать secondary popup в relations, attachments, comments
|
||||||
|
|
||||||
### 2. Внешние контуры
|
### 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 канон
|
- перевести [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
|
||||||
- проверить все top-toolbar и detail-surface ещё раз по glass/radius/button rules
|
- проверить дополнительное окно информации и related detail-surface ещё раз по glass/radius/button rules
|
||||||
- сравнить все detail-row и popup с эталоном `Внутреннего контура`
|
- сравнить все detail-row и popup с эталоном `Внутреннего контура`
|
||||||
|
|
||||||
### 3. Предложения / Intake
|
### 3. Предложения / Intake
|
||||||
|
|
@ -345,6 +349,14 @@
|
||||||
Это значит:
|
Это значит:
|
||||||
- эти экраны пока не проходили такой же системный канонический прогон, как `Внутренний контур`, `Внешние контуры` и `Intake`
|
- эти экраны пока не проходили такой же системный канонический прогон, как `Внутренний контур`, `Внешние контуры` и `Intake`
|
||||||
- их надо отдельно сверить по dropdown, button, popup, glass shell, toolbar и spacing
|
- их надо отдельно сверить по dropdown, button, popup, glass shell, toolbar и spacing
|
||||||
|
- отдельный приоритет внутри этого блока:
|
||||||
|
- `Analytics overview`
|
||||||
|
- `Workspace dashboard / Home`
|
||||||
|
- `Drafts`
|
||||||
|
- `Profile`
|
||||||
|
- `Stickies`
|
||||||
|
- `Browse / All issues / Workspace view`
|
||||||
|
- `Settings`
|
||||||
|
|
||||||
## Общий список страниц, которые требуют полноценного UI-pass
|
## Общий список страниц, которые требуют полноценного UI-pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ const ProjectsToolbarMenu = observer(function ProjectsToolbarMenu() {
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`nodedc-toolbar-icon-active-dot ${
|
className={`nodedc-toolbar-icon-active-dot ${
|
||||||
pathname.includes("/projects/") ? "bg-[rgb(var(--nodedc-accent-rgb))] text-[#0b1117]" : ""
|
pathname.includes("/projects/") ? "bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))]" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<ProjectIcon className="size-4" />
|
<ProjectIcon className="size-4" />
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,50 @@ import "@fontsource/ibm-plex-mono";
|
||||||
|
|
||||||
const APP_TITLE = "NODE.DC | Self-hosted task management workspace.";
|
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 = {
|
const designConfigStyle = {
|
||||||
"--nodedc-accent-rgb": designConfig.nodedc.accent_rgb.join(" "),
|
"--nodedc-accent-rgb": formatRgbTuple(accentRgb),
|
||||||
"--nodedc-card-passive-rgb": designConfig.nodedc.passive_card_rgb.join(" "),
|
"--nodedc-card-passive-rgb": formatRgbTuple(passiveCardRgb),
|
||||||
"--nodedc-card-active-rgb": designConfig.nodedc.active_card_rgb.join(" "),
|
"--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;
|
} as CSSProperties;
|
||||||
|
|
||||||
export const links: LinksFunction = () => [
|
export const links: LinksFunction = () => [
|
||||||
|
|
|
||||||
|
|
@ -122,12 +122,14 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
const sourceStateIds = useMemo(() => targetOptions?.states?.map((state) => state.id) ?? [], [targetOptions?.states]);
|
const sourceStateIds = useMemo(() => targetOptions?.states?.map((state) => state.id) ?? [], [targetOptions?.states]);
|
||||||
const selectedState = canEditTargetIssue ? getStateById(issue.state_id) : sourceStateMap[issue.state_id ?? ""];
|
const selectedState = canEditTargetIssue ? getStateById(issue.state_id) : sourceStateMap[issue.state_id ?? ""];
|
||||||
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_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 subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]";
|
||||||
const pillBackgroundClasses =
|
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 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");
|
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||||
|
|
||||||
if (!issue) return null;
|
if (!issue) return null;
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,10 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div
|
<div
|
||||||
className={cn("truncate text-[15px] leading-5 font-semibold", isActive ? "text-[#0b1117]" : "text-primary")}
|
className={cn(
|
||||||
|
"truncate text-[15px] leading-5 font-semibold",
|
||||||
|
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-primary"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{requesterName}
|
{requesterName}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,7 +86,12 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
<div className={cn("text-11 font-medium", isActive ? "text-[#17212b]/70" : "text-tertiary")}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"text-11 font-medium",
|
||||||
|
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/70" : "text-tertiary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{issue.project_detail?.identifier || "REQ"}-{issue.sequence_id}
|
{issue.project_detail?.identifier || "REQ"}-{issue.sequence_id}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -97,7 +105,12 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("truncate pl-10 text-[12px] font-medium leading-4", isActive ? "text-[#17212b]/75" : "text-secondary")}>
|
<div
|
||||||
|
className={cn(
|
||||||
|
"truncate pl-10 text-[12px] font-medium leading-4",
|
||||||
|
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/75" : "text-secondary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{contourName}
|
{contourName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -106,7 +119,7 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
|
||||||
<h3
|
<h3
|
||||||
className={cn(
|
className={cn(
|
||||||
"line-clamp-3 w-full max-w-[18.5rem] text-center text-16 leading-7 font-semibold",
|
"line-clamp-3 w-full max-w-[18.5rem] text-center text-16 leading-7 font-semibold",
|
||||||
isActive ? "text-[#0b1117]" : "text-primary"
|
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-primary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{issue.name}
|
{issue.name}
|
||||||
|
|
@ -141,7 +154,7 @@ export const ExternalContoursListItem = observer(function ExternalContoursListIt
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-3 py-1.5 text-12",
|
"rounded-full px-3 py-1.5 text-12",
|
||||||
isActive ? "bg-black/10 text-[#17212b]/80" : "bg-white/6 text-secondary"
|
isActive ? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]/80" : "bg-white/6 text-secondary"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{renderFormattedDate(lastUpdatedAt ?? "")}
|
{renderFormattedDate(lastUpdatedAt ?? "")}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export const FilterDisplayProperties = observer(function FilterDisplayProperties
|
||||||
type="button"
|
type="button"
|
||||||
className={`rounded-full border-0 px-3 py-1.5 text-12 transition-all ${
|
className={`rounded-full border-0 px-3 py-1.5 text-12 transition-all ${
|
||||||
displayProperties?.[displayProperty.key]
|
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"
|
: "bg-white/5 text-secondary hover:bg-white/8"
|
||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export function FilterOption(props: Props) {
|
||||||
<div
|
<div
|
||||||
className={`grid h-4 w-4 flex-shrink-0 place-items-center border-0 ${
|
className={`grid h-4 w-4 flex-shrink-0 place-items-center border-0 ${
|
||||||
isChecked
|
isChecked
|
||||||
? "bg-[rgb(var(--nodedc-accent-rgb))] text-[#0b1117]"
|
? "bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))]"
|
||||||
: "bg-white/6 text-transparent"
|
: "bg-white/6 text-transparent"
|
||||||
} rounded-full`}
|
} rounded-full`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,9 @@ export function LayoutSelection(props: Props) {
|
||||||
layout={layout.key}
|
layout={layout.key}
|
||||||
size={14}
|
size={14}
|
||||||
strokeWidth={2}
|
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"
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export function MobileLayoutSelection({
|
||||||
<SelectionDropdown
|
<SelectionDropdown
|
||||||
menuButton={
|
menuButton={
|
||||||
<div className="nodedc-toolbar-pill relative flex items-center gap-2 px-3">
|
<div className="nodedc-toolbar-pill relative flex items-center gap-2 px-3">
|
||||||
<span className="nodedc-toolbar-icon-active-dot bg-[rgb(var(--nodedc-accent-rgb))] text-[#0b1117]">
|
<span className="nodedc-toolbar-icon-active-dot bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))]">
|
||||||
{activeLayout && (
|
{activeLayout && (
|
||||||
<IssueLayoutIcon layout={activeLayout} size={14} strokeWidth={2} className="h-3.5 w-3.5" />
|
<IssueLayoutIcon layout={activeLayout} size={14} strokeWidth={2} className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,8 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
const selectedState = getStateById(issue.state_id);
|
const selectedState = getStateById(issue.state_id);
|
||||||
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
const projectStateIds = issue.project_id ? getProjectStateIds(issue.project_id) : [];
|
||||||
const { pillBackgroundClasses, iconBubbleClasses } = getNodedcWorkItemCardAppearance(isActive);
|
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 creatorName = creatorDetails?.display_name ?? t("common.none");
|
||||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,13 @@ type TNodedcWorkItemCardProps = {
|
||||||
|
|
||||||
export const getNodedcWorkItemCardAppearance = (isActive: boolean) => ({
|
export const getNodedcWorkItemCardAppearance = (isActive: boolean) => ({
|
||||||
surfaceClassName: isActive
|
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",
|
: "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]",
|
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",
|
iconBubbleClasses: isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -435,7 +435,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
disabled={!isAdmin}
|
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")}
|
{isLoading ? t("updating") : t("common.update_project")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export const ProjectFeatureUpdate = observer(function ProjectFeatureUpdate(props
|
||||||
<Link
|
<Link
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues`}
|
href={`/${workspaceSlug}/projects/${projectId}/issues`}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="nodedc-modal-primary-button inline-flex min-w-[10.5rem] items-center justify-center !text-[#0b1117] hover:!text-[#0b1117]"
|
className="nodedc-modal-primary-button inline-flex min-w-[10.5rem] items-center justify-center"
|
||||||
tabIndex={2}
|
tabIndex={2}
|
||||||
>
|
>
|
||||||
{t("open_project")}
|
{t("open_project")}
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,19 @@
|
||||||
--editor-colors-dark-blue-background: #c9dafb;
|
--editor-colors-dark-blue-background: #c9dafb;
|
||||||
--editor-colors-purple-background: #e3d8fd;
|
--editor-colors-purple-background: #e3d8fd;
|
||||||
--nodedc-accent-rgb: 51 163 255;
|
--nodedc-accent-rgb: 51 163 255;
|
||||||
|
--nodedc-on-accent-rgb: 245 247 251;
|
||||||
--nodedc-card-passive-rgb: 42 43 46;
|
--nodedc-card-passive-rgb: 42 43 46;
|
||||||
|
--nodedc-on-card-passive-rgb: 245 247 251;
|
||||||
--nodedc-card-active-rgb: 195 255 102;
|
--nodedc-card-active-rgb: 195 255 102;
|
||||||
|
--nodedc-on-card-active-rgb: 11 17 23;
|
||||||
--brand-default: rgb(var(--nodedc-accent-rgb));
|
--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);
|
--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 */
|
/* end background colors */
|
||||||
}
|
}
|
||||||
/* background colors */
|
/* background colors */
|
||||||
|
|
@ -507,7 +515,7 @@
|
||||||
|
|
||||||
.nodedc-toolbar-icon-button[data-active="true"] .nodedc-toolbar-icon-active-dot {
|
.nodedc-toolbar-icon-button[data-active="true"] .nodedc-toolbar-icon-active-dot {
|
||||||
background: rgb(var(--nodedc-accent-rgb));
|
background: rgb(var(--nodedc-accent-rgb));
|
||||||
color: #0b1117;
|
color: rgb(var(--nodedc-on-accent-rgb));
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-toolbar-pill {
|
.nodedc-toolbar-pill {
|
||||||
|
|
@ -553,7 +561,7 @@
|
||||||
min-height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
padding-inline: 1.55rem;
|
padding-inline: 1.55rem;
|
||||||
background: rgb(var(--nodedc-accent-rgb)) !important;
|
background: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
color: #0b1117 !important;
|
color: rgb(var(--nodedc-on-accent-rgb)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-toolbar-primary-wide {
|
.nodedc-toolbar-primary-wide {
|
||||||
|
|
@ -587,7 +595,7 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background: rgb(var(--nodedc-accent-rgb)) !important;
|
background: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
color: #0b1117 !important;
|
color: rgb(var(--nodedc-on-accent-rgb)) !important;
|
||||||
padding-inline: 1.25rem !important;
|
padding-inline: 1.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -596,14 +604,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-modal-primary-button,
|
.nodedc-modal-primary-button,
|
||||||
.nodedc-modal-primary-button *,
|
|
||||||
.nodedc-modal-danger-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-primary-button *,
|
.nodedc-settings-primary-button *,
|
||||||
.nodedc-settings-save-button,
|
.nodedc-settings-save-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 {
|
.nodedc-modal-danger-button {
|
||||||
|
|
@ -613,7 +624,7 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background: rgb(var(--nodedc-accent-rgb)) !important;
|
background: rgb(var(--nodedc-accent-rgb)) !important;
|
||||||
color: #0b1117 !important;
|
color: rgb(var(--nodedc-on-accent-rgb)) !important;
|
||||||
padding-inline: 1.25rem !important;
|
padding-inline: 1.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -627,7 +638,7 @@
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
background: rgb(var(--nodedc-accent-rgb)) !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,
|
.nodedc-glass-modal button.bg-danger-primary:hover,
|
||||||
|
|
@ -637,7 +648,7 @@
|
||||||
|
|
||||||
.nodedc-glass-modal button.bg-danger-primary *,
|
.nodedc-glass-modal button.bg-danger-primary *,
|
||||||
.nodedc-glass-modal button.border-danger-strong * {
|
.nodedc-glass-modal button.border-danger-strong * {
|
||||||
color: #0b1117 !important;
|
color: rgb(var(--nodedc-on-accent-rgb)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-modal-chip {
|
.nodedc-modal-chip {
|
||||||
|
|
@ -761,13 +772,13 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background: rgb(var(--nodedc-card-active-rgb)) !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;
|
padding-inline: 1.35rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-settings-primary-button:hover {
|
.nodedc-settings-primary-button:hover {
|
||||||
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
|
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 {
|
.nodedc-settings-save-button {
|
||||||
|
|
@ -777,13 +788,13 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background: rgb(var(--nodedc-card-active-rgb)) !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;
|
padding-inline: 1.45rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-settings-save-button:hover {
|
.nodedc-settings-save-button:hover {
|
||||||
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
|
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 {
|
.nodedc-overlay-button {
|
||||||
|
|
@ -1014,12 +1025,12 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.25rem !important;
|
border-radius: 1.25rem !important;
|
||||||
background: rgb(var(--nodedc-card-active-rgb)) !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 {
|
.nodedc-auth-primary-button:hover {
|
||||||
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
|
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 {
|
.nodedc-error-shell {
|
||||||
|
|
@ -1055,13 +1066,13 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.2rem !important;
|
border-radius: 1.2rem !important;
|
||||||
background: rgb(var(--nodedc-card-active-rgb)) !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;
|
padding-inline: 1.35rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-error-primary:hover {
|
.nodedc-error-primary:hover {
|
||||||
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
|
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 {
|
.nodedc-empty-state-primary {
|
||||||
|
|
@ -1071,13 +1082,13 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.2rem !important;
|
border-radius: 1.2rem !important;
|
||||||
background: rgb(var(--nodedc-card-active-rgb)) !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;
|
padding-inline: 1.35rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-empty-state-primary:hover {
|
.nodedc-empty-state-primary:hover {
|
||||||
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
|
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 {
|
.nodedc-empty-state-secondary {
|
||||||
|
|
@ -1143,14 +1154,14 @@
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.024) 0%, rgba(255, 255, 255, 0.008) 100%),
|
||||||
rgb(var(--nodedc-card-active-rgb)) !important;
|
rgb(var(--nodedc-card-active-rgb)) !important;
|
||||||
color: #0b1117 !important;
|
color: rgb(var(--nodedc-on-card-active-rgb)) !important;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.32),
|
inset 0 0 0 1px rgba(var(--nodedc-accent-rgb), 0.32),
|
||||||
0 12px 32px rgba(0, 0, 0, 0.16) !important;
|
0 12px 32px rgba(0, 0, 0, 0.16) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-external-card[data-active="true"] .text-primary {
|
.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,
|
.nodedc-external-card[data-active="true"] .text-secondary,
|
||||||
|
|
@ -1278,19 +1289,19 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
border-radius: 1.35rem !important;
|
border-radius: 1.35rem !important;
|
||||||
background: rgb(var(--nodedc-card-active-rgb)) !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;
|
padding-inline: 1.6rem !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-external-primary-button,
|
.nodedc-external-primary-button,
|
||||||
.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 {
|
.nodedc-external-primary-button:hover {
|
||||||
background: color-mix(in srgb, rgb(var(--nodedc-card-active-rgb)) 82%, white) !important;
|
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 {
|
.nodedc-external-empty-state {
|
||||||
|
|
|
||||||
|
|
@ -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-компонент, а только потом меняется экран
|
||||||
|
|
||||||
|
Итоговое правило:
|
||||||
|
- редизайн делается поверх канона
|
||||||
|
- канон не ломается ради скорости редизайна
|
||||||
|
|
@ -13,7 +13,7 @@ export const buttonVariants = cva(
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
primary:
|
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":
|
"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",
|
"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":
|
"error-outline":
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export const iconButtonVariants = cva(
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
primary:
|
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":
|
"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",
|
"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":
|
"error-outline":
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,8 @@ const ToolbarSeparator = React.forwardRef(function ToolbarSeparator(
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttonVariants = {
|
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",
|
secondary: "bg-surface-1 text-secondary border border-subtle hover:bg-surface-2 focus:bg-surface-2",
|
||||||
outline:
|
outline:
|
||||||
"border border-accent-strong text-accent-primary bg-transparent hover:bg-accent-primary/10 focus:bg-accent-primary/20",
|
"border border-accent-strong text-accent-primary bg-transparent hover:bg-accent-primary/10 focus:bg-accent-primary/20",
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,9 @@ enum buttonIconStyling {
|
||||||
|
|
||||||
export const buttonStyling: IButtonStyling = {
|
export const buttonStyling: IButtonStyling = {
|
||||||
primary: {
|
primary: {
|
||||||
default: `text-on-color bg-accent-primary`,
|
default: `text-[rgb(var(--nodedc-on-accent-rgb))] bg-accent-primary`,
|
||||||
hover: `hover:bg-accent-primary/80`,
|
hover: `hover:bg-accent-primary-hover`,
|
||||||
pressed: `focus:text-custom-brand-40 focus:bg-accent-primary/80`,
|
pressed: `focus:bg-accent-primary-active`,
|
||||||
disabled: `cursor-not-allowed !bg-layer-1 !text-on-color-disabled`,
|
disabled: `cursor-not-allowed !bg-layer-1 !text-on-color-disabled`,
|
||||||
},
|
},
|
||||||
"accent-primary": {
|
"accent-primary": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue