Приводил к стилу consta UI
This commit is contained in:
+25
@@ -106,4 +106,29 @@
|
||||
--accent-violet: rgb(145, 82, 230);
|
||||
--accent-lavender: rgb(157, 154, 247);
|
||||
--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 { EdgeInspector } from './EdgeInspector';
|
||||
import { debounce, serializeDocument } from './serialize';
|
||||
import type { DiagramDocument, DiagramNodeData, NodeKind } from './types';
|
||||
import type { DiagramDocument, DiagramEdgeData, DiagramNodeData, EdgeLineStyle, NodeKind } from './types';
|
||||
import './diagram.css';
|
||||
|
||||
interface DiagramEditorProps {
|
||||
@@ -42,6 +42,16 @@ interface DiagramEditorProps {
|
||||
|
||||
const DEFAULT_EDGE_TYPE = 'smoothstep';
|
||||
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 TYPING_HISTORY_THROTTLE_MS = 1000;
|
||||
|
||||
@@ -240,7 +250,32 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
|
||||
|
||||
const visibleEdges = useMemo(() => {
|
||||
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]);
|
||||
|
||||
const breadcrumbPath = useMemo(() => {
|
||||
@@ -417,6 +452,19 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
|
||||
[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 rect = canvasWrapperRef.current?.getBoundingClientRect();
|
||||
if (!rect) return { x: 0, y: 0 };
|
||||
@@ -696,7 +744,9 @@ function DiagramCanvas({ initialValue, onChange }: DiagramEditorProps) {
|
||||
<EdgeInspector
|
||||
edgeId={selectedEdge.id}
|
||||
label={typeof selectedEdge.label === 'string' ? selectedEdge.label : ''}
|
||||
onChange={updateSelectedEdgeLabel}
|
||||
data={(selectedEdge.data as DiagramEdgeData) ?? {}}
|
||||
onLabelChange={updateSelectedEdgeLabel}
|
||||
onDataChange={updateSelectedEdgeData}
|
||||
onDelete={deleteSelectedEdge}
|
||||
onBringToFront={bringSelectedEdgeToFront}
|
||||
onSendToBack={sendSelectedEdgeToBack}
|
||||
|
||||
+188
-3
@@ -1,11 +1,70 @@
|
||||
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';
|
||||
|
||||
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 {
|
||||
edgeId: string;
|
||||
label: string;
|
||||
onChange: (label: string) => void;
|
||||
data: DiagramEdgeData;
|
||||
onLabelChange: (label: string) => void;
|
||||
onDataChange: (patch: Partial<DiagramEdgeData>) => void;
|
||||
onDelete: () => void;
|
||||
onBringToFront: () => void;
|
||||
onSendToBack: () => void;
|
||||
@@ -14,11 +73,21 @@ interface EdgeInspectorProps {
|
||||
export function EdgeInspector({
|
||||
edgeId,
|
||||
label,
|
||||
onChange,
|
||||
data,
|
||||
onLabelChange,
|
||||
onDataChange,
|
||||
onDelete,
|
||||
onBringToFront,
|
||||
onSendToBack,
|
||||
}: 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 (
|
||||
<div className="node-inspector">
|
||||
<div className="node-inspector__header">
|
||||
@@ -39,7 +108,123 @@ export function EdgeInspector({
|
||||
<Button size="s" view="secondary" label="На задний план" onClick={onSendToBack} />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
+134
-29
@@ -1,26 +1,50 @@
|
||||
import { useRef, useState, type RefObject } from 'react';
|
||||
import { Button } from '@consta/uikit/Button';
|
||||
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 { 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';
|
||||
|
||||
// "Дополнительные цвета" from palette.css — the section the palette itself
|
||||
// marks as meant for single-color accents (as opposed to the gradients used
|
||||
// for shape defaults, which a flat-hex picker can't represent).
|
||||
const NODE_COLOR_PRESETS = [
|
||||
'#B5309A',
|
||||
'#EC5665',
|
||||
'#FFAB66',
|
||||
'#C2DC5B',
|
||||
'#32CA88',
|
||||
'#22926D',
|
||||
'#0EBAD5',
|
||||
'#4EE7F4',
|
||||
'#9152E6',
|
||||
'#9D9AF7',
|
||||
'#E88FDB',
|
||||
// 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<NodeKind, string> = {
|
||||
@@ -62,7 +86,18 @@ export function NodeInspector({
|
||||
}: NodeInspectorProps) {
|
||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||
// 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') {
|
||||
const hasMeta = Boolean(data.responsible || data.duration);
|
||||
@@ -138,28 +173,83 @@ export function NodeInspector({
|
||||
<Text size="xs" view="secondary">
|
||||
Цвет фона
|
||||
</Text>
|
||||
<ColorControl
|
||||
markerRef={colorMarkerRef}
|
||||
model={hexModel}
|
||||
value={data.color ?? DEFAULT_NODE_COLOR[nodeType]}
|
||||
onChange={(value) => onChange({ color: value })}
|
||||
size="s"
|
||||
onClick={() => setColorPickerOpen(true)}
|
||||
<div className="node-inspector__presets">
|
||||
{GRADIENT_PRESETS.map(({ name, label }) => {
|
||||
const value = `var(--gradient-${name})`;
|
||||
const isActive = (data.color ?? DEFAULT_NODE_BACKGROUND[nodeType]) === value;
|
||||
return (
|
||||
<button
|
||||
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 && (
|
||||
<ColorPicker
|
||||
model={hexModel}
|
||||
value={data.color ?? DEFAULT_NODE_COLOR[nodeType]}
|
||||
value={flatColor}
|
||||
onChange={(value) => onChange({ color: value })}
|
||||
anchorRef={colorMarkerRef}
|
||||
anchorRef={colorButtonRef}
|
||||
open={colorPickerOpen}
|
||||
onOpen={setColorPickerOpen}
|
||||
palette={NODE_COLOR_PRESETS}
|
||||
paletteTitle="Пресеты"
|
||||
/>
|
||||
)}
|
||||
</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
|
||||
label="Название"
|
||||
type="textarea"
|
||||
@@ -192,6 +282,21 @@ export function NodeInspector({
|
||||
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)} />
|
||||
<Text size="xs" view="secondary" className="node-inspector__hint">
|
||||
{childCount > 0
|
||||
|
||||
+24
-27
@@ -48,8 +48,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #555;
|
||||
border-radius: 6px;
|
||||
background: canvas;
|
||||
color: canvastext;
|
||||
font-size: 12px;
|
||||
@@ -61,7 +59,6 @@
|
||||
}
|
||||
|
||||
.diagram-node__label {
|
||||
color: white;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -90,9 +87,7 @@
|
||||
transform: translateX(-50%);
|
||||
margin-top: 4px;
|
||||
max-width: 120px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: canvastext;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -101,26 +96,6 @@
|
||||
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 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -134,8 +109,6 @@
|
||||
.diagram-node--diamond-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 1px solid #555;
|
||||
box-sizing: border-box;
|
||||
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
|
||||
}
|
||||
|
||||
@@ -201,6 +174,30 @@
|
||||
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__description {
|
||||
white-space: pre-wrap;
|
||||
|
||||
+36
-11
@@ -35,6 +35,16 @@ export const DEFAULT_NODE_COLOR: Record<NodeKind, string> = {
|
||||
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 }[] = [
|
||||
{ id: 'top', position: Position.Top },
|
||||
{ id: 'right', position: Position.Right },
|
||||
@@ -91,32 +101,42 @@ function ShapeResizer({
|
||||
|
||||
function TaskNode({ id, data, selected }: NodeProps<ShapeNode>) {
|
||||
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} />
|
||||
<ShapeHandles />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
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>) {
|
||||
const kind = (type as 'startEvent' | 'intermediateEvent' | 'endEvent') ?? 'startEvent';
|
||||
return (
|
||||
<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] }}
|
||||
>
|
||||
<ShapeResizer selected={selected} minWidth={24} minHeight={24} />
|
||||
<ShapeHandles />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -149,7 +169,12 @@ function GatewayNode({ id, data, type, selected }: NodeProps<ShapeNode>) {
|
||||
<ShapeHandles />
|
||||
<SubProcessBadge id={id} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
+17
-1
@@ -13,6 +13,10 @@ export interface DiagramNodeData extends Record<string, unknown> {
|
||||
responsible?: string;
|
||||
duration?: string;
|
||||
color?: string;
|
||||
/** Corner rounding, in px — only meaningful for the rectangular task shape. */
|
||||
borderRadius?: number;
|
||||
textColor?: string;
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
export interface DiagramNode {
|
||||
@@ -28,7 +32,19 @@ export interface DiagramNode {
|
||||
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 {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user