import { useRef, useState, type RefObject } from "react"; 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, DEFAULT_NODE_COLOR, DEFAULT_NODE_FONT_SIZE, DEFAULT_NODE_TEXT_COLOR, DEFAULT_TASK_BORDER_RADIUS, EVENT_SUBTYPE_ICON, EVENT_SUBTYPE_LABEL, EVENT_SUBTYPES_BY_KIND, TASK_SUBTYPE_ICON, TASK_SUBTYPE_LABEL, } from "./nodes"; import type { DiagramNodeData, EventSubtype, NodeKind, TaskSubtype, } from "./types"; const TASK_SUBTYPES: TaskSubtype[] = [ "none", "user", "service", "script", "manual", "businessRule", "send", "receive", ]; // Every gradient defined in palette.css, across its three "for diagrams / // flowcharts / semantic highlights" sections. Consta's ColorPicker `palette` // prop can't render these — it goes through hexModel, which only understands // flat colors — so these render as our own swatch row instead. const GRADIENT_PRESETS: { name: string; label: string }[] = [ { name: "navy", label: "Тёмно-синий" }, { name: "azure", label: "Лазурный" }, { name: "sky", label: "Небесный" }, { name: "teal-dark", label: "Тёмная бирюза" }, { name: "teal-light", label: "Светлая бирюза" }, { name: "navy-dark", label: "Глубокий синий" }, { name: "ice", label: "Лёд" }, { name: "frost", label: "Иней" }, { name: "purple", label: "Пурпурный" }, { name: "red", label: "Красный" }, { name: "orange", label: "Оранжевый" }, { name: "olive", label: "Оливковый" }, { name: "green", label: "Зелёный" }, { name: "green-dark", label: "Тёмно-зелёный" }, { name: "blue", label: "Синий" }, { name: "cyan", label: "Циан" }, { name: "violet", label: "Виолет" }, { name: "lavender", label: "Лавандовый" }, { name: "pink", label: "Розовый" }, ]; // "Цвета текста" from palette.css. const TEXT_COLOR_PRESETS: { hex: string; label: string }[] = [ { hex: "#0079C2", label: "Синий" }, { hex: "#525A64", label: "Тёмный" }, { hex: "#929AA4", label: "Серый" }, { hex: "#FFFFFF", label: "Белый" }, ]; const TYPE_LABELS: Record = { task: "Действие (Task)", startEvent: "Начальное событие (Start Event)", intermediateEvent: "Промежуточное событие (Intermediate Event)", endEvent: "Конечное событие (End Event)", exclusiveGateway: "Исключающий шлюз (XOR)", parallelGateway: "Параллельный шлюз (AND)", inclusiveGateway: "Включающий шлюз (OR)", complexGateway: "Сложный шлюз (Complex)", eventBasedGateway: "Шлюз на основе событий (Event-based)", hint: "Подсказка", dataObject: "Документ (Data Object)", }; const EVENT_KINDS = new Set([ "startEvent", "intermediateEvent", "endEvent", ]); interface NodeInspectorProps { nodeId: string; nodeType: NodeKind; data: DiagramNodeData; childCount: number; mode: "edit" | "view"; onChange: (patch: Partial) => void; onEnterSubProcess: (nodeId: string) => void; onDuplicate: () => void; onDelete: () => void; onBringToFront: () => void; onSendToBack: () => void; } export function NodeInspector({ nodeId, nodeType, data, childCount, mode, onChange, onEnterSubProcess, onDuplicate, onDelete, onBringToFront, onSendToBack, }: NodeInspectorProps) { const [colorPickerOpen, setColorPickerOpen] = useState(false); // Consta's ref props still use the pre-React-19 non-nullable RefObject shape. const colorButtonRef = useRef( null, ) as RefObject; // 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( null, ) as RefObject; // Text sits directly on the colored fill for tasks (needs a light default) // but on the plain canvas for every other shape's caption (needs a dark one). const defaultTextColor = nodeType === "task" ? DEFAULT_NODE_TEXT_COLOR : DEFAULT_CAPTION_TEXT_COLOR; const currentTextColor = data.textColor ?? defaultTextColor; 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; return (
{TYPE_LABELS[nodeType]}
{subtypeLabel && ( {subtypeLabel} )} {data.label} {data.description && ( {data.description} )} {hasMeta && (
{data.responsible && ( <>
Ответственный
{data.responsible}
)} {data.duration && ( <>
Длительность
{data.duration}
)}
)} {childCount > 0 && (
); } return (
{TYPE_LABELS[nodeType]}
{nodeType === "task" && (
Тип задачи
{TASK_SUBTYPES.map((subtype) => { const Icon = TASK_SUBTYPE_ICON[subtype]; const isActive = (data.subtype ?? "none") === subtype; return ( ); })}
)} {EVENT_KINDS.has(nodeType) && (
Тип триггера
{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 ( ); })}
)}
Цвет фона
{GRADIENT_PRESETS.map(({ name, label }) => { const value = `var(--gradient-${name})`; const isActive = (data.color ?? DEFAULT_NODE_BACKGROUND[nodeType]) === value; return (
{colorPickerOpen && ( onChange({ color: value })} anchorRef={colorButtonRef} open={colorPickerOpen} onOpen={setColorPickerOpen} style={{ zIndex: 100 }} /> )}
Цвет текста
{TEXT_COLOR_PRESETS.map(({ hex, label: presetLabel }) => (
{textColorPickerOpen && ( onChange({ textColor: value })} anchorRef={textColorButtonRef} open={textColorPickerOpen} onOpen={setTextColorPickerOpen} style={{ zIndex: 100 }} /> )}
{ const n = Number(value); if (value && !Number.isNaN(n)) onChange({ fontSize: n }); }} /> onChange({ label: value ?? "" })} /> onChange({ description: value ?? "" })} /> {(nodeType === "task" || nodeType === "hint") && ( { const n = Number(value); if (value && !Number.isNaN(n)) onChange({ borderRadius: n }); }} /> )} {nodeType === "task" && ( <> onChange({ responsible: value ?? "" })} /> onChange({ duration: value ?? "" })} />
); }