231 lines
8.1 KiB
TypeScript
231 lines
8.1 KiB
TypeScript
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;
|
|
data: DiagramEdgeData;
|
|
onLabelChange: (label: string) => void;
|
|
onDataChange: (patch: Partial<DiagramEdgeData>) => void;
|
|
onDelete: () => void;
|
|
onBringToFront: () => void;
|
|
onSendToBack: () => void;
|
|
}
|
|
|
|
export function EdgeInspector({
|
|
edgeId,
|
|
label,
|
|
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">
|
|
<Text size="s" weight="semibold">
|
|
Поток управления (Sequence Flow)
|
|
</Text>
|
|
<Text size="xs" view="secondary" font="mono">
|
|
{edgeId}
|
|
</Text>
|
|
</div>
|
|
|
|
<div className="node-inspector__toolbar">
|
|
<Button size="s" view="secondary" label="Удалить" onClick={onDelete} />
|
|
</div>
|
|
|
|
<div className="node-inspector__toolbar">
|
|
<Button size="s" view="secondary" label="На передний план" onClick={onBringToFront} />
|
|
<Button size="s" view="secondary" label="На задний план" onClick={onSendToBack} />
|
|
</div>
|
|
|
|
<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>
|
|
);
|
|
}
|