Приводил к стилу consta UI
This commit is contained in:
+25
@@ -106,4 +106,29 @@
|
|||||||
--accent-violet: rgb(145, 82, 230);
|
--accent-violet: rgb(145, 82, 230);
|
||||||
--accent-lavender: rgb(157, 154, 247);
|
--accent-lavender: rgb(157, 154, 247);
|
||||||
--accent-pink: rgb(232, 143, 219);
|
--accent-pink: rgb(232, 143, 219);
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
ЦВЕТА ТЕКСТА
|
||||||
|
============================================================ */
|
||||||
|
--text-color-blue: rgb(0, 121, 194);
|
||||||
|
--text-color-dark: rgb(82, 90, 100);
|
||||||
|
--text-color-gray: rgb(146, 154, 164);
|
||||||
|
--text-color-white: rgb(255, 255, 255);
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
ОСНОВНЫЕ ЦВЕТА ДЛЯ МЕЛКОЙ ГРАФИКИ, ЛИНИЙ
|
||||||
|
Два тональных ряда: синий (от --text-color-blue) и серый
|
||||||
|
(от --text-color-gray до белого)
|
||||||
|
============================================================ */
|
||||||
|
--graphic-blue-1: rgb(0, 121, 194); /* = --text-color-blue */
|
||||||
|
--graphic-blue-2: rgb(67, 183, 247);
|
||||||
|
--graphic-blue-3: rgb(137, 207, 255);
|
||||||
|
--graphic-blue-4: rgb(117, 170, 197);
|
||||||
|
--graphic-blue-5: rgb(143, 215, 228);
|
||||||
|
--graphic-blue-6: rgb(0, 65, 129);
|
||||||
|
|
||||||
|
--graphic-gray-1: rgb(146, 154, 164); /* = --text-color-gray */
|
||||||
|
--graphic-gray-2: rgb(206, 210, 214);
|
||||||
|
--graphic-gray-3: rgb(233, 235, 237);
|
||||||
|
--graphic-gray-4: rgb(255, 255, 255); /* = --text-color-white */
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-3
@@ -32,7 +32,7 @@ import { nodeTypes, NodesWithChildrenContext, ModeContext } from './nodes';
|
|||||||
import { NodeInspector } from './NodeInspector';
|
import { NodeInspector } from './NodeInspector';
|
||||||
import { EdgeInspector } from './EdgeInspector';
|
import { EdgeInspector } from './EdgeInspector';
|
||||||
import { debounce, serializeDocument } from './serialize';
|
import { debounce, serializeDocument } from './serialize';
|
||||||
import type { DiagramDocument, DiagramNodeData, NodeKind } from './types';
|
import type { DiagramDocument, DiagramEdgeData, DiagramNodeData, EdgeLineStyle, NodeKind } from './types';
|
||||||
import './diagram.css';
|
import './diagram.css';
|
||||||
|
|
||||||
interface DiagramEditorProps {
|
interface DiagramEditorProps {
|
||||||
@@ -42,6 +42,16 @@ interface DiagramEditorProps {
|
|||||||
|
|
||||||
const DEFAULT_EDGE_TYPE = 'smoothstep';
|
const DEFAULT_EDGE_TYPE = 'smoothstep';
|
||||||
const DEFAULT_MARKER_END = { type: MarkerType.ArrowClosed };
|
const DEFAULT_MARKER_END = { type: MarkerType.ArrowClosed };
|
||||||
|
const DEFAULT_EDGE_STROKE_WIDTH = 1;
|
||||||
|
const DEFAULT_EDGE_BORDER_RADIUS = 5; // matches getSmoothStepPath's own default
|
||||||
|
const DEFAULT_EDGE_COLOR = '#b1b1b7'; // xyflow's own default edge color
|
||||||
|
const DEFAULT_EDGE_TEXT_COLOR = '#FFFFFF';
|
||||||
|
const DEFAULT_EDGE_FONT_SIZE = 11;
|
||||||
|
const EDGE_DASH_ARRAY: Record<EdgeLineStyle, string | undefined> = {
|
||||||
|
solid: undefined,
|
||||||
|
dashed: '8 4',
|
||||||
|
dotted: '1.5 4',
|
||||||
|
};
|
||||||
const MAX_HISTORY = 50;
|
const MAX_HISTORY = 50;
|
||||||
const TYPING_HISTORY_THROTTLE_MS = 1000;
|
const TYPING_HISTORY_THROTTLE_MS = 1000;
|
||||||
|
|
||||||
@@ -240,7 +250,32 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
|
|||||||
|
|
||||||
const visibleEdges = useMemo(() => {
|
const visibleEdges = useMemo(() => {
|
||||||
const visibleIds = new Set(visibleNodes.map((node) => node.id));
|
const visibleIds = new Set(visibleNodes.map((node) => node.id));
|
||||||
return edges.filter((edge) => visibleIds.has(edge.source) && visibleIds.has(edge.target));
|
return edges
|
||||||
|
.filter((edge) => visibleIds.has(edge.source) && visibleIds.has(edge.target))
|
||||||
|
.map((edge) => {
|
||||||
|
const edgeData = edge.data as DiagramEdgeData | undefined;
|
||||||
|
const edgeColor = edgeData?.color ?? DEFAULT_EDGE_COLOR;
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
style: {
|
||||||
|
...edge.style,
|
||||||
|
stroke: edgeColor,
|
||||||
|
strokeWidth: edgeData?.strokeWidth ?? DEFAULT_EDGE_STROKE_WIDTH,
|
||||||
|
strokeDasharray: EDGE_DASH_ARRAY[edgeData?.lineStyle ?? 'solid'],
|
||||||
|
},
|
||||||
|
pathOptions: { borderRadius: edgeData?.borderRadius ?? DEFAULT_EDGE_BORDER_RADIUS },
|
||||||
|
markerEnd: { type: MarkerType.ArrowClosed, color: edgeColor },
|
||||||
|
// Label renders as its own small chip — same color as the line, configurable text color.
|
||||||
|
labelBgStyle: { fill: edgeColor },
|
||||||
|
labelStyle: {
|
||||||
|
fill: edgeData?.textColor ?? DEFAULT_EDGE_TEXT_COLOR,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: edgeData?.fontSize ?? DEFAULT_EDGE_FONT_SIZE,
|
||||||
|
},
|
||||||
|
labelBgPadding: [6, 4] as [number, number],
|
||||||
|
labelBgBorderRadius: 4,
|
||||||
|
};
|
||||||
|
});
|
||||||
}, [edges, visibleNodes]);
|
}, [edges, visibleNodes]);
|
||||||
|
|
||||||
const breadcrumbPath = useMemo(() => {
|
const breadcrumbPath = useMemo(() => {
|
||||||
@@ -417,6 +452,19 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
|
|||||||
[selectedEdgeId, nodes, edges, emitChange, pushHistoryThrottled],
|
[selectedEdgeId, nodes, edges, emitChange, pushHistoryThrottled],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateSelectedEdgeData = useCallback(
|
||||||
|
(patch: Partial<DiagramEdgeData>) => {
|
||||||
|
if (!selectedEdgeId) return;
|
||||||
|
pushHistoryThrottled(nodes, edges);
|
||||||
|
const next = edges.map((edge) =>
|
||||||
|
edge.id === selectedEdgeId ? { ...edge, data: { ...edge.data, ...patch } } : edge,
|
||||||
|
);
|
||||||
|
setEdges(next);
|
||||||
|
emitChange(nodes, next);
|
||||||
|
},
|
||||||
|
[selectedEdgeId, nodes, edges, emitChange, pushHistoryThrottled],
|
||||||
|
);
|
||||||
|
|
||||||
const getViewportCenter = useCallback(() => {
|
const getViewportCenter = useCallback(() => {
|
||||||
const rect = canvasWrapperRef.current?.getBoundingClientRect();
|
const rect = canvasWrapperRef.current?.getBoundingClientRect();
|
||||||
if (!rect) return { x: 0, y: 0 };
|
if (!rect) return { x: 0, y: 0 };
|
||||||
@@ -696,7 +744,9 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
|
|||||||
<EdgeInspector
|
<EdgeInspector
|
||||||
edgeId={selectedEdge.id}
|
edgeId={selectedEdge.id}
|
||||||
label={typeof selectedEdge.label === 'string' ? selectedEdge.label : ''}
|
label={typeof selectedEdge.label === 'string' ? selectedEdge.label : ''}
|
||||||
onChange={updateSelectedEdgeLabel}
|
data={(selectedEdge.data as DiagramEdgeData) ?? {}}
|
||||||
|
onLabelChange={updateSelectedEdgeLabel}
|
||||||
|
onDataChange={updateSelectedEdgeData}
|
||||||
onDelete={deleteSelectedEdge}
|
onDelete={deleteSelectedEdge}
|
||||||
onBringToFront={bringSelectedEdgeToFront}
|
onBringToFront={bringSelectedEdgeToFront}
|
||||||
onSendToBack={sendSelectedEdgeToBack}
|
onSendToBack={sendSelectedEdgeToBack}
|
||||||
|
|||||||
+188
-3
@@ -1,11 +1,70 @@
|
|||||||
|
import { useRef, useState, type RefObject } from 'react';
|
||||||
import { Button } from '@consta/uikit/Button';
|
import { Button } from '@consta/uikit/Button';
|
||||||
import { TextField } from '@consta/uikit/TextField';
|
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 { Text } from '@consta/uikit/Text';
|
||||||
|
import type { DiagramEdgeData, EdgeLineStyle } from './types';
|
||||||
|
|
||||||
|
const LINE_STYLE_ITEMS: { label: string; style: EdgeLineStyle }[] = [
|
||||||
|
{ 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' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// "Дополнительные цвета" — 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: 'Розовый' },
|
||||||
|
];
|
||||||
|
|
||||||
|
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: 'Белый' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_STROKE_WIDTH = 1;
|
||||||
|
const DEFAULT_BORDER_RADIUS = 5;
|
||||||
|
const DEFAULT_EDGE_COLOR = '#b1b1b7';
|
||||||
|
const DEFAULT_TEXT_COLOR = '#FFFFFF';
|
||||||
|
const DEFAULT_FONT_SIZE = 11;
|
||||||
|
|
||||||
interface EdgeInspectorProps {
|
interface EdgeInspectorProps {
|
||||||
edgeId: string;
|
edgeId: string;
|
||||||
label: string;
|
label: string;
|
||||||
onChange: (label: string) => void;
|
data: DiagramEdgeData;
|
||||||
|
onLabelChange: (label: string) => void;
|
||||||
|
onDataChange: (patch: Partial<DiagramEdgeData>) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onBringToFront: () => void;
|
onBringToFront: () => void;
|
||||||
onSendToBack: () => void;
|
onSendToBack: () => void;
|
||||||
@@ -14,11 +73,21 @@ interface EdgeInspectorProps {
|
|||||||
export function EdgeInspector({
|
export function EdgeInspector({
|
||||||
edgeId,
|
edgeId,
|
||||||
label,
|
label,
|
||||||
onChange,
|
data,
|
||||||
|
onLabelChange,
|
||||||
|
onDataChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
onBringToFront,
|
onBringToFront,
|
||||||
onSendToBack,
|
onSendToBack,
|
||||||
}: EdgeInspectorProps) {
|
}: EdgeInspectorProps) {
|
||||||
|
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||||
|
const colorButtonRef = useRef<HTMLDivElement>(null) as RefObject<HTMLDivElement>;
|
||||||
|
const currentColor = data.color ?? DEFAULT_EDGE_COLOR;
|
||||||
|
|
||||||
|
const [textColorPickerOpen, setTextColorPickerOpen] = useState(false);
|
||||||
|
const textColorButtonRef = useRef<HTMLDivElement>(null) as RefObject<HTMLDivElement>;
|
||||||
|
const currentTextColor = data.textColor ?? DEFAULT_TEXT_COLOR;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="node-inspector">
|
<div className="node-inspector">
|
||||||
<div className="node-inspector__header">
|
<div className="node-inspector__header">
|
||||||
@@ -39,7 +108,123 @@ export function EdgeInspector({
|
|||||||
<Button size="s" view="secondary" label="На задний план" onClick={onSendToBack} />
|
<Button size="s" view="secondary" label="На задний план" onClick={onSendToBack} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextField label="Подпись" size="s" value={label} onChange={(value) => onChange(value ?? '')} />
|
<TextField label="Подпись" size="s" value={label} onChange={(value) => onLabelChange(value ?? '')} />
|
||||||
|
|
||||||
|
<div className="node-inspector__field">
|
||||||
|
<Text size="xs" view="secondary">
|
||||||
|
Цвет линии
|
||||||
|
</Text>
|
||||||
|
<div className="node-inspector__presets">
|
||||||
|
{LINE_COLOR_PRESETS.map(({ hex, label: presetLabel }) => (
|
||||||
|
<button
|
||||||
|
key={hex}
|
||||||
|
type="button"
|
||||||
|
title={presetLabel}
|
||||||
|
aria-label={presetLabel}
|
||||||
|
className={`node-inspector__preset${currentColor === hex ? ' node-inspector__preset--active' : ''}`}
|
||||||
|
style={{ background: hex }}
|
||||||
|
onClick={() => onDataChange({ color: hex })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div ref={colorButtonRef} className="node-inspector__color-trigger">
|
||||||
|
<Button size="s" view="secondary" label="Настроить цвет" onClick={() => setColorPickerOpen(true)} />
|
||||||
|
</div>
|
||||||
|
{colorPickerOpen && (
|
||||||
|
<ColorPicker
|
||||||
|
model={hexModel}
|
||||||
|
value={currentColor}
|
||||||
|
onChange={(value) => onDataChange({ color: value })}
|
||||||
|
anchorRef={colorButtonRef}
|
||||||
|
open={colorPickerOpen}
|
||||||
|
onOpen={setColorPickerOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="node-inspector__field">
|
||||||
|
<Text size="xs" view="secondary">
|
||||||
|
Цвет текста
|
||||||
|
</Text>
|
||||||
|
<div className="node-inspector__presets">
|
||||||
|
{TEXT_COLOR_PRESETS.map(({ hex, label: presetLabel }) => (
|
||||||
|
<button
|
||||||
|
key={hex}
|
||||||
|
type="button"
|
||||||
|
title={presetLabel}
|
||||||
|
aria-label={presetLabel}
|
||||||
|
className={`node-inspector__preset${currentTextColor === hex ? ' node-inspector__preset--active' : ''}`}
|
||||||
|
style={{ background: hex }}
|
||||||
|
onClick={() => onDataChange({ textColor: hex })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div ref={textColorButtonRef} className="node-inspector__color-trigger">
|
||||||
|
<Button size="s" view="secondary" label="Настроить цвет" onClick={() => setTextColorPickerOpen(true)} />
|
||||||
|
</div>
|
||||||
|
{textColorPickerOpen && (
|
||||||
|
<ColorPicker
|
||||||
|
model={hexModel}
|
||||||
|
value={currentTextColor}
|
||||||
|
onChange={(value) => onDataChange({ textColor: value })}
|
||||||
|
anchorRef={textColorButtonRef}
|
||||||
|
open={textColorPickerOpen}
|
||||||
|
onOpen={setTextColorPickerOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Размер текста"
|
||||||
|
type="number"
|
||||||
|
size="s"
|
||||||
|
min={8}
|
||||||
|
max={32}
|
||||||
|
value={String(data.fontSize ?? DEFAULT_FONT_SIZE)}
|
||||||
|
onChange={(value) => {
|
||||||
|
const n = Number(value);
|
||||||
|
if (value && !Number.isNaN(n)) onDataChange({ fontSize: n });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Толщина линии"
|
||||||
|
type="number"
|
||||||
|
size="s"
|
||||||
|
min={1}
|
||||||
|
max={8}
|
||||||
|
value={String(data.strokeWidth ?? DEFAULT_STROKE_WIDTH)}
|
||||||
|
onChange={(value) => {
|
||||||
|
const n = Number(value);
|
||||||
|
if (value && !Number.isNaN(n)) onDataChange({ strokeWidth: n });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="node-inspector__field">
|
||||||
|
<Text size="xs" view="secondary">
|
||||||
|
Стиль линии
|
||||||
|
</Text>
|
||||||
|
<ChoiceGroup
|
||||||
|
size="xs"
|
||||||
|
name={`edge-line-style-${edgeId}`}
|
||||||
|
items={LINE_STYLE_ITEMS}
|
||||||
|
value={LINE_STYLE_ITEMS.find((item) => item.style === (data.lineStyle ?? 'solid')) ?? null}
|
||||||
|
onChange={(item) => onDataChange({ lineStyle: item.style })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Скругление углов"
|
||||||
|
type="number"
|
||||||
|
size="s"
|
||||||
|
min={0}
|
||||||
|
max={20}
|
||||||
|
value={String(data.borderRadius ?? DEFAULT_BORDER_RADIUS)}
|
||||||
|
onChange={(value) => {
|
||||||
|
const n = Number(value);
|
||||||
|
if (value && !Number.isNaN(n)) onDataChange({ borderRadius: n });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-30
@@ -1,26 +1,50 @@
|
|||||||
import { useRef, useState, type RefObject } from 'react';
|
import { useRef, useState, type RefObject } from 'react';
|
||||||
import { Button } from '@consta/uikit/Button';
|
import { Button } from '@consta/uikit/Button';
|
||||||
import { TextField } from '@consta/uikit/TextField';
|
import { TextField } from '@consta/uikit/TextField';
|
||||||
import { ColorControl, ColorPicker, hexModel } from '@consta/uikit/ColorPicker';
|
import { ColorPicker, hexModel } from '@consta/uikit/ColorPicker';
|
||||||
import { Text } from '@consta/uikit/Text';
|
import { Text } from '@consta/uikit/Text';
|
||||||
import { DEFAULT_NODE_COLOR } from './nodes';
|
import {
|
||||||
|
DEFAULT_CAPTION_TEXT_COLOR,
|
||||||
|
DEFAULT_NODE_BACKGROUND,
|
||||||
|
DEFAULT_NODE_COLOR,
|
||||||
|
DEFAULT_NODE_FONT_SIZE,
|
||||||
|
DEFAULT_NODE_TEXT_COLOR,
|
||||||
|
DEFAULT_TASK_BORDER_RADIUS,
|
||||||
|
} from './nodes';
|
||||||
import type { DiagramNodeData, NodeKind } from './types';
|
import type { DiagramNodeData, NodeKind } from './types';
|
||||||
|
|
||||||
// "Дополнительные цвета" from palette.css — the section the palette itself
|
// Every gradient defined in palette.css, across its three "for diagrams /
|
||||||
// marks as meant for single-color accents (as opposed to the gradients used
|
// flowcharts / semantic highlights" sections. Consta's ColorPicker `palette`
|
||||||
// for shape defaults, which a flat-hex picker can't represent).
|
// prop can't render these — it goes through hexModel, which only understands
|
||||||
const NODE_COLOR_PRESETS = [
|
// flat colors — so these render as our own swatch row instead.
|
||||||
'#B5309A',
|
const GRADIENT_PRESETS: { name: string; label: string }[] = [
|
||||||
'#EC5665',
|
{ name: 'navy', label: 'Тёмно-синий' },
|
||||||
'#FFAB66',
|
{ name: 'azure', label: 'Лазурный' },
|
||||||
'#C2DC5B',
|
{ name: 'sky', label: 'Небесный' },
|
||||||
'#32CA88',
|
{ name: 'teal-dark', label: 'Тёмная бирюза' },
|
||||||
'#22926D',
|
{ name: 'teal-light', label: 'Светлая бирюза' },
|
||||||
'#0EBAD5',
|
{ name: 'navy-dark', label: 'Глубокий синий' },
|
||||||
'#4EE7F4',
|
{ name: 'ice', label: 'Лёд' },
|
||||||
'#9152E6',
|
{ name: 'frost', label: 'Иней' },
|
||||||
'#9D9AF7',
|
{ name: 'purple', label: 'Пурпурный' },
|
||||||
'#E88FDB',
|
{ 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<NodeKind, string> = {
|
const TYPE_LABELS: Record<NodeKind, string> = {
|
||||||
@@ -62,7 +86,18 @@ export function NodeInspector({
|
|||||||
}: NodeInspectorProps) {
|
}: NodeInspectorProps) {
|
||||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||||
// Consta's ref props still use the pre-React-19 non-nullable RefObject shape.
|
// Consta's ref props still use the pre-React-19 non-nullable RefObject shape.
|
||||||
const colorMarkerRef = useRef<HTMLButtonElement>(null) as RefObject<HTMLButtonElement>;
|
const colorButtonRef = useRef<HTMLDivElement>(null) as RefObject<HTMLDivElement>;
|
||||||
|
// 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];
|
||||||
|
|
||||||
|
const [textColorPickerOpen, setTextColorPickerOpen] = useState(false);
|
||||||
|
const textColorButtonRef = useRef<HTMLDivElement>(null) as RefObject<HTMLDivElement>;
|
||||||
|
// 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') {
|
if (mode === 'view') {
|
||||||
const hasMeta = Boolean(data.responsible || data.duration);
|
const hasMeta = Boolean(data.responsible || data.duration);
|
||||||
@@ -138,28 +173,83 @@ export function NodeInspector({
|
|||||||
<Text size="xs" view="secondary">
|
<Text size="xs" view="secondary">
|
||||||
Цвет фона
|
Цвет фона
|
||||||
</Text>
|
</Text>
|
||||||
<ColorControl
|
<div className="node-inspector__presets">
|
||||||
markerRef={colorMarkerRef}
|
{GRADIENT_PRESETS.map(({ name, label }) => {
|
||||||
model={hexModel}
|
const value = `var(--gradient-${name})`;
|
||||||
value={data.color ?? DEFAULT_NODE_COLOR[nodeType]}
|
const isActive = (data.color ?? DEFAULT_NODE_BACKGROUND[nodeType]) === value;
|
||||||
onChange={(value) => onChange({ color: value })}
|
return (
|
||||||
size="s"
|
<button
|
||||||
onClick={() => setColorPickerOpen(true)}
|
key={name}
|
||||||
/>
|
type="button"
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
className={`node-inspector__preset${isActive ? ' node-inspector__preset--active' : ''}`}
|
||||||
|
style={{ background: value }}
|
||||||
|
onClick={() => onChange({ color: value })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div ref={colorButtonRef} className="node-inspector__color-trigger">
|
||||||
|
<Button size="s" view="secondary" label="Настроить цвет" onClick={() => setColorPickerOpen(true)} />
|
||||||
|
</div>
|
||||||
{colorPickerOpen && (
|
{colorPickerOpen && (
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
model={hexModel}
|
model={hexModel}
|
||||||
value={data.color ?? DEFAULT_NODE_COLOR[nodeType]}
|
value={flatColor}
|
||||||
onChange={(value) => onChange({ color: value })}
|
onChange={(value) => onChange({ color: value })}
|
||||||
anchorRef={colorMarkerRef}
|
anchorRef={colorButtonRef}
|
||||||
open={colorPickerOpen}
|
open={colorPickerOpen}
|
||||||
onOpen={setColorPickerOpen}
|
onOpen={setColorPickerOpen}
|
||||||
palette={NODE_COLOR_PRESETS}
|
|
||||||
paletteTitle="Пресеты"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="node-inspector__field">
|
||||||
|
<Text size="xs" view="secondary">
|
||||||
|
Цвет текста
|
||||||
|
</Text>
|
||||||
|
<div className="node-inspector__presets">
|
||||||
|
{TEXT_COLOR_PRESETS.map(({ hex, label: presetLabel }) => (
|
||||||
|
<button
|
||||||
|
key={hex}
|
||||||
|
type="button"
|
||||||
|
title={presetLabel}
|
||||||
|
aria-label={presetLabel}
|
||||||
|
className={`node-inspector__preset${currentTextColor === hex ? ' node-inspector__preset--active' : ''}`}
|
||||||
|
style={{ background: hex }}
|
||||||
|
onClick={() => onChange({ textColor: hex })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div ref={textColorButtonRef} className="node-inspector__color-trigger">
|
||||||
|
<Button size="s" view="secondary" label="Настроить цвет" onClick={() => setTextColorPickerOpen(true)} />
|
||||||
|
</div>
|
||||||
|
{textColorPickerOpen && (
|
||||||
|
<ColorPicker
|
||||||
|
model={hexModel}
|
||||||
|
value={currentTextColor}
|
||||||
|
onChange={(value) => onChange({ textColor: value })}
|
||||||
|
anchorRef={textColorButtonRef}
|
||||||
|
open={textColorPickerOpen}
|
||||||
|
onOpen={setTextColorPickerOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Размер текста"
|
||||||
|
type="number"
|
||||||
|
size="s"
|
||||||
|
min={8}
|
||||||
|
max={32}
|
||||||
|
value={String(data.fontSize ?? DEFAULT_NODE_FONT_SIZE)}
|
||||||
|
onChange={(value) => {
|
||||||
|
const n = Number(value);
|
||||||
|
if (value && !Number.isNaN(n)) onChange({ fontSize: n });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Название"
|
label="Название"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
@@ -192,6 +282,21 @@ export function NodeInspector({
|
|||||||
onChange={(value) => onChange({ duration: value ?? '' })}
|
onChange={(value) => onChange({ duration: value ?? '' })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{nodeType === 'task' && (
|
||||||
|
<TextField
|
||||||
|
label="Скругление углов"
|
||||||
|
type="number"
|
||||||
|
size="s"
|
||||||
|
min={0}
|
||||||
|
max={25}
|
||||||
|
value={String(data.borderRadius ?? DEFAULT_TASK_BORDER_RADIUS)}
|
||||||
|
onChange={(value) => {
|
||||||
|
const n = Number(value);
|
||||||
|
if (value && !Number.isNaN(n)) onChange({ borderRadius: n });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button size="s" view="ghost" label="Войти в под-процесс →" onClick={() => onEnterSubProcess(nodeId)} />
|
<Button size="s" view="ghost" label="Войти в под-процесс →" onClick={() => onEnterSubProcess(nodeId)} />
|
||||||
<Text size="xs" view="secondary" className="node-inspector__hint">
|
<Text size="xs" view="secondary" className="node-inspector__hint">
|
||||||
{childCount > 0
|
{childCount > 0
|
||||||
|
|||||||
+24
-27
@@ -48,8 +48,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: canvas;
|
background: canvas;
|
||||||
color: canvastext;
|
color: canvastext;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -61,7 +59,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.diagram-node__label {
|
.diagram-node__label {
|
||||||
color: white;
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
@@ -90,9 +87,7 @@
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
max-width: 120px;
|
max-width: 120px;
|
||||||
font-size: 11px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: canvastext;
|
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,26 +96,6 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diagram-node--startEvent {
|
|
||||||
border-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagram-node--endEvent {
|
|
||||||
border-width: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagram-node--intermediateEvent {
|
|
||||||
border-width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagram-node--intermediateEvent::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 3px;
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagram-node--gateway-outer {
|
.diagram-node--gateway-outer {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -134,8 +109,6 @@
|
|||||||
.diagram-node--diamond-bg {
|
.diagram-node--diamond-bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
border: 1px solid #555;
|
|
||||||
box-sizing: border-box;
|
|
||||||
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
|
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +174,30 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-inspector__presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-inspector__preset {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid color-mix(in srgb, canvastext 25%, transparent);
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-inspector__preset--active {
|
||||||
|
outline: 2px solid canvastext;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-inspector__color-trigger {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.node-inspector__label,
|
.node-inspector__label,
|
||||||
.node-inspector__description {
|
.node-inspector__description {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
+36
-11
@@ -35,6 +35,16 @@ export const DEFAULT_NODE_COLOR: Record<NodeKind, string> = {
|
|||||||
inclusiveGateway: '#BBBD39',
|
inclusiveGateway: '#BBBD39',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_TASK_BORDER_RADIUS = 6;
|
||||||
|
|
||||||
|
// Text on the colored shape body (task label, gateway icon) needs a light
|
||||||
|
// default; the caption below events/gateways sits on the plain canvas
|
||||||
|
// background instead and needs a dark one. Both are overridable via the
|
||||||
|
// same `data.textColor`/`data.fontSize`.
|
||||||
|
export const DEFAULT_NODE_TEXT_COLOR = '#FFFFFF';
|
||||||
|
export const DEFAULT_CAPTION_TEXT_COLOR = '#525A64';
|
||||||
|
export const DEFAULT_NODE_FONT_SIZE = 12;
|
||||||
|
|
||||||
const HANDLE_POSITIONS: { id: string; position: Position }[] = [
|
const HANDLE_POSITIONS: { id: string; position: Position }[] = [
|
||||||
{ id: 'top', position: Position.Top },
|
{ id: 'top', position: Position.Top },
|
||||||
{ id: 'right', position: Position.Right },
|
{ id: 'right', position: Position.Right },
|
||||||
@@ -91,32 +101,42 @@ function ShapeResizer({
|
|||||||
|
|
||||||
function TaskNode({ id, data, selected }: NodeProps<ShapeNode>) {
|
function TaskNode({ id, data, selected }: NodeProps<ShapeNode>) {
|
||||||
return (
|
return (
|
||||||
<div className="diagram-node diagram-node--task" style={{ background: data.color ?? DEFAULT_NODE_BACKGROUND.task }}>
|
<div
|
||||||
|
className="diagram-node diagram-node--task"
|
||||||
|
style={{
|
||||||
|
background: data.color ?? DEFAULT_NODE_BACKGROUND.task,
|
||||||
|
borderRadius: data.borderRadius ?? DEFAULT_TASK_BORDER_RADIUS,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ShapeResizer selected={selected} minWidth={70} minHeight={32} keepAspectRatio={false} />
|
<ShapeResizer selected={selected} minWidth={70} minHeight={32} keepAspectRatio={false} />
|
||||||
<ShapeHandles />
|
<ShapeHandles />
|
||||||
<SubProcessBadge id={id} />
|
<SubProcessBadge id={id} />
|
||||||
<span className="diagram-node__label">{data.label}</span>
|
<span
|
||||||
|
className="diagram-node__label"
|
||||||
|
style={{ color: data.textColor ?? DEFAULT_NODE_TEXT_COLOR, fontSize: data.fontSize ?? DEFAULT_NODE_FONT_SIZE }}
|
||||||
|
>
|
||||||
|
{data.label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EVENT_BORDER_CLASS: Record<'startEvent' | 'intermediateEvent' | 'endEvent', string> = {
|
|
||||||
startEvent: 'diagram-node--startEvent',
|
|
||||||
intermediateEvent: 'diagram-node--intermediateEvent',
|
|
||||||
endEvent: 'diagram-node--endEvent',
|
|
||||||
};
|
|
||||||
|
|
||||||
function EventNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
|
function EventNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
|
||||||
const kind = (type as 'startEvent' | 'intermediateEvent' | 'endEvent') ?? 'startEvent';
|
const kind = (type as 'startEvent' | 'intermediateEvent' | 'endEvent') ?? 'startEvent';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`diagram-node diagram-node--circle ${EVENT_BORDER_CLASS[kind]}`}
|
className="diagram-node diagram-node--circle"
|
||||||
style={{ background: data.color ?? DEFAULT_NODE_BACKGROUND[kind] }}
|
style={{ background: data.color ?? DEFAULT_NODE_BACKGROUND[kind] }}
|
||||||
>
|
>
|
||||||
<ShapeResizer selected={selected} minWidth={24} minHeight={24} />
|
<ShapeResizer selected={selected} minWidth={24} minHeight={24} />
|
||||||
<ShapeHandles />
|
<ShapeHandles />
|
||||||
<SubProcessBadge id={id} />
|
<SubProcessBadge id={id} />
|
||||||
<span className="diagram-node__caption">{data.label}</span>
|
<span
|
||||||
|
className="diagram-node__caption"
|
||||||
|
style={{ color: data.textColor ?? DEFAULT_CAPTION_TEXT_COLOR, fontSize: data.fontSize ?? DEFAULT_NODE_FONT_SIZE }}
|
||||||
|
>
|
||||||
|
{data.label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -149,7 +169,12 @@ function GatewayNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
|
|||||||
<ShapeHandles />
|
<ShapeHandles />
|
||||||
<SubProcessBadge id={id} />
|
<SubProcessBadge id={id} />
|
||||||
<span className="diagram-node__icon">{GATEWAY_ICON[kind]}</span>
|
<span className="diagram-node__icon">{GATEWAY_ICON[kind]}</span>
|
||||||
<span className="diagram-node__caption">{data.label}</span>
|
<span
|
||||||
|
className="diagram-node__caption"
|
||||||
|
style={{ color: data.textColor ?? DEFAULT_CAPTION_TEXT_COLOR, fontSize: data.fontSize ?? DEFAULT_NODE_FONT_SIZE }}
|
||||||
|
>
|
||||||
|
{data.label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -13,6 +13,10 @@ export interface DiagramNodeData extends Record<string, unknown> {
|
|||||||
responsible?: string;
|
responsible?: string;
|
||||||
duration?: string;
|
duration?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
|
/** Corner rounding, in px — only meaningful for the rectangular task shape. */
|
||||||
|
borderRadius?: number;
|
||||||
|
textColor?: string;
|
||||||
|
fontSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiagramNode {
|
export interface DiagramNode {
|
||||||
@@ -28,7 +32,19 @@ export interface DiagramNode {
|
|||||||
zIndex?: number;
|
zIndex?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DiagramEdgeData extends Record<string, unknown> {}
|
export type EdgeLineStyle = 'solid' | 'dashed' | 'dotted';
|
||||||
|
|
||||||
|
export interface DiagramEdgeData extends Record<string, unknown> {
|
||||||
|
strokeWidth?: number;
|
||||||
|
lineStyle?: EdgeLineStyle;
|
||||||
|
/** Corner rounding for the smoothstep path, in px. */
|
||||||
|
borderRadius?: number;
|
||||||
|
/** Stroke color — also tints the arrowhead marker and the label's background chip. */
|
||||||
|
color?: string;
|
||||||
|
/** Label text color — the chip's own background still follows `color`. */
|
||||||
|
textColor?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DiagramEdge {
|
export interface DiagramEdge {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user