From 6a4f463bf2be25314641dc19058cb159ec6e8707 Mon Sep 17 00:00:00 2001 From: NikitaKalitov Date: Fri, 28 Aug 2026 09:22:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=B8=20=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20?= =?UTF-8?q?=D1=84=D0=B8=D0=B3=D1=83=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/EdgeInspector.tsx | 165 +++++++++++++++++++++++++++++------------- src/NodeInspector.tsx | 109 +++++++++++++++++++--------- src/colors.ts | 17 +++++ src/diagram.css | 63 ++++++++++++++++ 4 files changed, 271 insertions(+), 83 deletions(-) create mode 100644 src/colors.ts diff --git a/src/EdgeInspector.tsx b/src/EdgeInspector.tsx index 817b478..b326828 100644 --- a/src/EdgeInspector.tsx +++ b/src/EdgeInspector.tsx @@ -1,62 +1,64 @@ -import { useRef, useState, type RefObject } from 'react'; -import { Button } from '@consta/uikit/Button'; -import { TextField } from '@consta/uikit/TextField'; -import { ChoiceGroup } from '@consta/uikit/ChoiceGroup'; -import { ColorPicker, hexModel } from '@consta/uikit/ColorPicker'; -import { Text } from '@consta/uikit/Text'; -import type { DiagramEdgeData, EdgeLineStyle } from './types'; +import { useRef, useState, type RefObject } from "react"; +import { Button } from "@consta/uikit/Button"; +import { TextField } from "@consta/uikit/TextField"; +import { ChoiceGroup } from "@consta/uikit/ChoiceGroup"; +import { ColorPicker, hexModel } from "@consta/uikit/ColorPicker"; +import { Text } from "@consta/uikit/Text"; +import { IconCancel } from "@consta/icons/IconCancel"; +import { TRANSPARENT_COLOR, toPickerHex } from "./colors"; +import type { DiagramEdgeData, EdgeLineStyle } from "./types"; const LINE_STYLE_ITEMS: { label: string; style: EdgeLineStyle }[] = [ - { label: 'Сплошная', style: 'solid' }, - { label: 'Пунктирная', style: 'dashed' }, - { label: 'Точечная', style: 'dotted' }, + { label: "Сплошная", style: "solid" }, + { label: "Пунктирная", style: "dashed" }, + { label: "Точечная", style: "dotted" }, ]; // "Основные цвета для мелкой графики, линий" — two tonal rows (blue, gray) // meant specifically for lines, shown first. const GRAPHIC_LINE_PRESETS: { hex: string; label: string }[] = [ - { hex: '#0079C2', label: 'Синий 1' }, - { hex: '#43B7F7', label: 'Синий 2' }, - { hex: '#89CFFF', label: 'Синий 3' }, - { hex: '#75AAC5', label: 'Синий 4' }, - { hex: '#8FD7E4', label: 'Синий 5' }, - { hex: '#004181', label: 'Синий 6' }, - { hex: '#929AA4', label: 'Серый 1' }, - { hex: '#CED2D6', label: 'Серый 2' }, - { hex: '#E9EBED', label: 'Серый 3' }, - { hex: '#FFFFFF', label: 'Серый 4' }, + { hex: "#0079C2", label: "Синий 1" }, + { hex: "#43B7F7", label: "Синий 2" }, + { hex: "#89CFFF", label: "Синий 3" }, + { hex: "#75AAC5", label: "Синий 4" }, + { hex: "#8FD7E4", label: "Синий 5" }, + { hex: "#004181", label: "Синий 6" }, + { hex: "#929AA4", label: "Серый 1" }, + { hex: "#CED2D6", label: "Серый 2" }, + { hex: "#E9EBED", label: "Серый 3" }, + { hex: "#FFFFFF", label: "Серый 4" }, ]; // "Дополнительные цвета" — the section palette.css marks as meant for // lines/small graphics too (single accents), shown after the row above. const ACCENT_LINE_PRESETS: { hex: string; label: string }[] = [ - { hex: '#B5309A', label: 'Пурпурный' }, - { hex: '#EC5665', label: 'Красный' }, - { hex: '#FFAB66', label: 'Оранжевый' }, - { hex: '#C2DC5B', label: 'Оливковый' }, - { hex: '#32CA88', label: 'Зелёный' }, - { hex: '#22926D', label: 'Тёмно-зелёный' }, - { hex: '#0EBAD5', label: 'Синий' }, - { hex: '#4EE7F4', label: 'Циан' }, - { hex: '#9152E6', label: 'Виолет' }, - { hex: '#9D9AF7', label: 'Лавандовый' }, - { hex: '#E88FDB', label: 'Розовый' }, + { hex: "#B5309A", label: "Пурпурный" }, + { hex: "#EC5665", label: "Красный" }, + { hex: "#FFAB66", label: "Оранжевый" }, + { hex: "#C2DC5B", label: "Оливковый" }, + { hex: "#32CA88", label: "Зелёный" }, + { hex: "#22926D", label: "Тёмно-зелёный" }, + { hex: "#0EBAD5", label: "Синий" }, + { hex: "#4EE7F4", label: "Циан" }, + { hex: "#9152E6", label: "Виолет" }, + { hex: "#9D9AF7", label: "Лавандовый" }, + { hex: "#E88FDB", label: "Розовый" }, ]; const LINE_COLOR_PRESETS = [...GRAPHIC_LINE_PRESETS, ...ACCENT_LINE_PRESETS]; // "Цвета текста" from palette.css. const TEXT_COLOR_PRESETS: { hex: string; label: string }[] = [ - { hex: '#0079C2', label: 'Синий' }, - { hex: '#525A64', label: 'Тёмный' }, - { hex: '#929AA4', label: 'Серый' }, - { hex: '#FFFFFF', label: 'Белый' }, + { hex: "#0079C2", label: "Синий" }, + { hex: "#525A64", label: "Тёмный" }, + { hex: "#929AA4", label: "Серый" }, + { hex: "#FFFFFF", label: "Белый" }, ]; const DEFAULT_STROKE_WIDTH = 1; const DEFAULT_BORDER_RADIUS = 5; -const DEFAULT_EDGE_COLOR = '#b1b1b7'; -const DEFAULT_TEXT_COLOR = '#FFFFFF'; +const DEFAULT_EDGE_COLOR = "#b1b1b7"; +const DEFAULT_TEXT_COLOR = "#FFFFFF"; const DEFAULT_FONT_SIZE = 11; interface EdgeInspectorProps { @@ -81,11 +83,15 @@ export function EdgeInspector({ onSendToBack, }: EdgeInspectorProps) { const [colorPickerOpen, setColorPickerOpen] = useState(false); - const colorButtonRef = useRef(null) as RefObject; + const colorButtonRef = useRef( + null, + ) as RefObject; const currentColor = data.color ?? DEFAULT_EDGE_COLOR; const [textColorPickerOpen, setTextColorPickerOpen] = useState(false); - const textColorButtonRef = useRef(null) as RefObject; + const textColorButtonRef = useRef( + null, + ) as RefObject; const currentTextColor = data.textColor ?? DEFAULT_TEXT_COLOR; return ( @@ -101,11 +107,26 @@ export function EdgeInspector({
-
- onLabelChange(value ?? '')} /> + onLabelChange(value ?? "")} + />
@@ -118,23 +139,46 @@ export function EdgeInspector({ type="button" title={presetLabel} aria-label={presetLabel} - className={`node-inspector__preset${currentColor === hex ? ' node-inspector__preset--active' : ''}`} + className={`node-inspector__preset${currentColor === hex ? " node-inspector__preset--active" : ""}`} style={{ background: hex }} onClick={() => onDataChange({ color: hex })} /> ))} + {/* + Цвет линии заодно красит стрелку и подложку подписи (см. + labelBgStyle в DiagramEditor) — прозрачный здесь убирает и её. + */} +
-
{colorPickerOpen && ( onDataChange({ color: value })} anchorRef={colorButtonRef} open={colorPickerOpen} onOpen={setColorPickerOpen} + style={{ zIndex: 100 }} /> )} @@ -150,23 +194,42 @@ export function EdgeInspector({ type="button" title={presetLabel} aria-label={presetLabel} - className={`node-inspector__preset${currentTextColor === hex ? ' node-inspector__preset--active' : ''}`} + className={`node-inspector__preset${currentTextColor === hex ? " node-inspector__preset--active" : ""}`} style={{ background: hex }} onClick={() => onDataChange({ textColor: hex })} /> ))} +
-
{textColorPickerOpen && ( onDataChange({ textColor: value })} anchorRef={textColorButtonRef} open={textColorPickerOpen} onOpen={setTextColorPickerOpen} + style={{ zIndex: 100 }} /> )} @@ -205,7 +268,11 @@ export function EdgeInspector({ size="xs" name={`edge-line-style-${edgeId}`} items={LINE_STYLE_ITEMS} - value={LINE_STYLE_ITEMS.find((item) => item.style === (data.lineStyle ?? 'solid')) ?? null} + value={ + LINE_STYLE_ITEMS.find( + (item) => item.style === (data.lineStyle ?? "solid"), + ) ?? null + } onChange={(item) => onDataChange({ lineStyle: item.style })} /> diff --git a/src/NodeInspector.tsx b/src/NodeInspector.tsx index 1359cd8..4588f93 100644 --- a/src/NodeInspector.tsx +++ b/src/NodeInspector.tsx @@ -3,6 +3,8 @@ import { Button } from "@consta/uikit/Button"; import { TextField } from "@consta/uikit/TextField"; import { ColorPicker, hexModel } from "@consta/uikit/ColorPicker"; import { Text } from "@consta/uikit/Text"; +import { IconCancel } from "@consta/icons/IconCancel"; +import { TRANSPARENT_COLOR, toPickerHex } from "./colors"; import { DEFAULT_CAPTION_TEXT_COLOR, DEFAULT_NODE_BACKGROUND, @@ -16,7 +18,12 @@ import { TASK_SUBTYPE_ICON, TASK_SUBTYPE_LABEL, } from "./nodes"; -import type { DiagramNodeData, EventSubtype, NodeKind, TaskSubtype } from "./types"; +import type { + DiagramNodeData, + EventSubtype, + NodeKind, + TaskSubtype, +} from "./types"; const TASK_SUBTYPES: TaskSubtype[] = [ "none", @@ -77,7 +84,11 @@ const TYPE_LABELS: Record = { dataObject: "Документ (Data Object)", }; -const EVENT_KINDS = new Set(["startEvent", "intermediateEvent", "endEvent"]); +const EVENT_KINDS = new Set([ + "startEvent", + "intermediateEvent", + "endEvent", +]); interface NodeInspectorProps { nodeId: string; @@ -111,13 +122,9 @@ export function NodeInspector({ const colorButtonRef = useRef( null, ) as RefObject; - // data.color can be a `var(--gradient-x)` reference from the preset row — - // hexModel tries to parse that as a hex color and produces NaN, so the - // hex-only ColorControl/ColorPicker always need a real flat hex fallback. - const flatColor = - data.color && !data.color.startsWith("var(") ? - data.color - : DEFAULT_NODE_COLOR[nodeType]; + // data.color бывает не-hex: `var(--gradient-x)` от пресета-градиента или + // `transparent` — hexModel такое не разбирает, нужен hex-дефолт. + const flatColor = toPickerHex(data.color, DEFAULT_NODE_COLOR[nodeType]); const [textColorPickerOpen, setTextColorPickerOpen] = useState(false); const textColorButtonRef = useRef( @@ -132,11 +139,13 @@ export function NodeInspector({ if (mode === "view") { const hasMeta = Boolean(data.responsible || data.duration); const subtypeLabel = - nodeType === "task" - ? TASK_SUBTYPE_LABEL[(data.subtype as TaskSubtype | undefined) ?? "none"] - : EVENT_KINDS.has(nodeType) - ? EVENT_SUBTYPE_LABEL[(data.subtype as EventSubtype | undefined) ?? "none"] - : undefined; + nodeType === "task" ? + TASK_SUBTYPE_LABEL[(data.subtype as TaskSubtype | undefined) ?? "none"] + : EVENT_KINDS.has(nodeType) ? + EVENT_SUBTYPE_LABEL[ + (data.subtype as EventSubtype | undefined) ?? "none" + ] + : undefined; return (
@@ -245,7 +254,9 @@ export function NodeInspector({ className={`node-inspector__subtype${isActive ? " node-inspector__subtype--active" : ""}`} onClick={() => onChange({ subtype })} > - {Icon ? : —} + {Icon ? + + : —} ); })} @@ -259,24 +270,26 @@ export function NodeInspector({ Тип триггера
- {EVENT_SUBTYPES_BY_KIND[nodeType as "startEvent" | "intermediateEvent" | "endEvent"].map( - (subtype: EventSubtype) => { - const Icon = EVENT_SUBTYPE_ICON[subtype]; - const isActive = (data.subtype ?? "none") === subtype; - return ( - - ); - }, - )} + {EVENT_SUBTYPES_BY_KIND[ + nodeType as "startEvent" | "intermediateEvent" | "endEvent" + ].map((subtype: EventSubtype) => { + const Icon = EVENT_SUBTYPE_ICON[subtype]; + const isActive = (data.subtype ?? "none") === subtype; + return ( + + ); + })}
)} @@ -302,6 +315,19 @@ export function NodeInspector({ /> ); })} +
@@ -339,6 +366,19 @@ export function NodeInspector({ onClick={() => onChange({ textColor: hex })} /> ))} +
diff --git a/src/colors.ts b/src/colors.ts new file mode 100644 index 0000000..42c4676 --- /dev/null +++ b/src/colors.ts @@ -0,0 +1,17 @@ +/** + * «Без заливки». Валидное значение и для CSS `background`/`color`, и для + * SVG `stroke`/`fill`, поэтому одинаково годится фигурам, линиям и подписям. + */ +export const TRANSPARENT_COLOR = "transparent"; + +/** + * Consta's ColorPicker/ColorControl работают через hexModel и понимают только + * плоский hex. В data цвет может лежать и как `var(--gradient-x)` (пресет- + * градиент), и как `transparent` — если отдать такое в hexModel, разбор даст + * NaN и поле покажет «NaNNaNNaN». Поэтому всё, что не hex, подменяем дефолтом. + */ +export function toPickerHex(value: string | undefined, fallback: string): string { + return value && /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value) ? + value + : fallback; +} diff --git a/src/diagram.css b/src/diagram.css index fe518f2..f26b4a9 100644 --- a/src/diagram.css +++ b/src/diagram.css @@ -86,6 +86,51 @@ opacity: 0.4; } +/* + * Зона захвата для изменения размера. По умолчанию xyflow даёт углам маркеры + * 5×5 px, а сторонам — линии толщиной 1 px: попасть курсором почти нереально. + * Видимую графику оставляем скромной, а кликабельную область расширяем + * прозрачным ::after — так фигура не обрастает крупными маркерами. + * + * z-index намеренно не трогаем: точки соединения (.react-flow__handle) идут + * в разметке после ресайзера и потому остаются сверху в местах пересечения — + * на серединах сторон, где расширенная полоса линии проходит поверх них. + * Иначе расширение зоны захвата отобрало бы у них клик и сломало создание + * связей. + */ +.react-flow__resize-control.handle { + width: 8px; + height: 8px; + border-radius: 2px; +} + +.react-flow__resize-control.handle::after { + content: ""; + position: absolute; + inset: -8px; +} + +.react-flow__resize-control.line::after { + content: ""; + position: absolute; +} + +.react-flow__resize-control.line.left::after, +.react-flow__resize-control.line.right::after { + top: 0; + bottom: 0; + left: -6px; + right: -6px; +} + +.react-flow__resize-control.line.top::after, +.react-flow__resize-control.line.bottom::after { + left: 0; + right: 0; + top: -6px; + bottom: -6px; +} + .diagram-alignment-guide { position: absolute; background: #ff0072; @@ -312,6 +357,24 @@ outline-offset: 2px; } +/* + * «Прозрачный» — не цвет, а его отсутствие, поэтому вместо заливки внутри + * значок-перечёркивание. Шахматка под ним — привычная в редакторах пометка + * прозрачности, иначе кружок читался бы просто как белый. + */ +.node-inspector__preset--none { + display: flex; + align-items: center; + justify-content: center; + color: #525a64; + background: + linear-gradient(45deg, #d7dae0 25%, transparent 25%, transparent 75%, #d7dae0 75%), + linear-gradient(45deg, #d7dae0 25%, transparent 25%, transparent 75%, #d7dae0 75%), + #fff; + background-size: 8px 8px; + background-position: 0 0, 4px 4px; +} + .node-inspector__subtype { width: 28px; height: 28px;