Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a4f463bf2 | ||
|
|
762f43dad4 |
+1
-1
@@ -97,7 +97,7 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
return <p style={{ padding: 16, fontFamily: 'system-ui, sans-serif' }}>Загрузка диаграммы…</p>;
|
return <div className="diagram-loading">Загрузка диаграммы…</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+116
-49
@@ -1,62 +1,64 @@
|
|||||||
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 { ChoiceGroup } from '@consta/uikit/ChoiceGroup';
|
import { ChoiceGroup } from "@consta/uikit/ChoiceGroup";
|
||||||
import { 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 type { DiagramEdgeData, EdgeLineStyle } from './types';
|
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 }[] = [
|
const LINE_STYLE_ITEMS: { label: string; style: EdgeLineStyle }[] = [
|
||||||
{ label: 'Сплошная', style: 'solid' },
|
{ label: "Сплошная", style: "solid" },
|
||||||
{ label: 'Пунктирная', style: 'dashed' },
|
{ label: "Пунктирная", style: "dashed" },
|
||||||
{ label: 'Точечная', style: 'dotted' },
|
{ label: "Точечная", style: "dotted" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// "Основные цвета для мелкой графики, линий" — two tonal rows (blue, gray)
|
// "Основные цвета для мелкой графики, линий" — two tonal rows (blue, gray)
|
||||||
// meant specifically for lines, shown first.
|
// meant specifically for lines, shown first.
|
||||||
const GRAPHIC_LINE_PRESETS: { hex: string; label: string }[] = [
|
const GRAPHIC_LINE_PRESETS: { hex: string; label: string }[] = [
|
||||||
{ hex: '#0079C2', label: 'Синий 1' },
|
{ hex: "#0079C2", label: "Синий 1" },
|
||||||
{ hex: '#43B7F7', label: 'Синий 2' },
|
{ hex: "#43B7F7", label: "Синий 2" },
|
||||||
{ hex: '#89CFFF', label: 'Синий 3' },
|
{ hex: "#89CFFF", label: "Синий 3" },
|
||||||
{ hex: '#75AAC5', label: 'Синий 4' },
|
{ hex: "#75AAC5", label: "Синий 4" },
|
||||||
{ hex: '#8FD7E4', label: 'Синий 5' },
|
{ hex: "#8FD7E4", label: "Синий 5" },
|
||||||
{ hex: '#004181', label: 'Синий 6' },
|
{ hex: "#004181", label: "Синий 6" },
|
||||||
{ hex: '#929AA4', label: 'Серый 1' },
|
{ hex: "#929AA4", label: "Серый 1" },
|
||||||
{ hex: '#CED2D6', label: 'Серый 2' },
|
{ hex: "#CED2D6", label: "Серый 2" },
|
||||||
{ hex: '#E9EBED', label: 'Серый 3' },
|
{ hex: "#E9EBED", label: "Серый 3" },
|
||||||
{ hex: '#FFFFFF', label: 'Серый 4' },
|
{ hex: "#FFFFFF", label: "Серый 4" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// "Дополнительные цвета" — the section palette.css marks as meant for
|
// "Дополнительные цвета" — the section palette.css marks as meant for
|
||||||
// lines/small graphics too (single accents), shown after the row above.
|
// lines/small graphics too (single accents), shown after the row above.
|
||||||
const ACCENT_LINE_PRESETS: { hex: string; label: string }[] = [
|
const ACCENT_LINE_PRESETS: { hex: string; label: string }[] = [
|
||||||
{ hex: '#B5309A', label: 'Пурпурный' },
|
{ hex: "#B5309A", label: "Пурпурный" },
|
||||||
{ hex: '#EC5665', label: 'Красный' },
|
{ hex: "#EC5665", label: "Красный" },
|
||||||
{ hex: '#FFAB66', label: 'Оранжевый' },
|
{ hex: "#FFAB66", label: "Оранжевый" },
|
||||||
{ hex: '#C2DC5B', label: 'Оливковый' },
|
{ hex: "#C2DC5B", label: "Оливковый" },
|
||||||
{ hex: '#32CA88', label: 'Зелёный' },
|
{ hex: "#32CA88", label: "Зелёный" },
|
||||||
{ hex: '#22926D', label: 'Тёмно-зелёный' },
|
{ hex: "#22926D", label: "Тёмно-зелёный" },
|
||||||
{ hex: '#0EBAD5', label: 'Синий' },
|
{ hex: "#0EBAD5", label: "Синий" },
|
||||||
{ hex: '#4EE7F4', label: 'Циан' },
|
{ hex: "#4EE7F4", label: "Циан" },
|
||||||
{ hex: '#9152E6', label: 'Виолет' },
|
{ hex: "#9152E6", label: "Виолет" },
|
||||||
{ hex: '#9D9AF7', label: 'Лавандовый' },
|
{ hex: "#9D9AF7", label: "Лавандовый" },
|
||||||
{ hex: '#E88FDB', label: 'Розовый' },
|
{ hex: "#E88FDB", label: "Розовый" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const LINE_COLOR_PRESETS = [...GRAPHIC_LINE_PRESETS, ...ACCENT_LINE_PRESETS];
|
const LINE_COLOR_PRESETS = [...GRAPHIC_LINE_PRESETS, ...ACCENT_LINE_PRESETS];
|
||||||
|
|
||||||
// "Цвета текста" from palette.css.
|
// "Цвета текста" from palette.css.
|
||||||
const TEXT_COLOR_PRESETS: { hex: string; label: string }[] = [
|
const TEXT_COLOR_PRESETS: { hex: string; label: string }[] = [
|
||||||
{ hex: '#0079C2', label: 'Синий' },
|
{ hex: "#0079C2", label: "Синий" },
|
||||||
{ hex: '#525A64', label: 'Тёмный' },
|
{ hex: "#525A64", label: "Тёмный" },
|
||||||
{ hex: '#929AA4', label: 'Серый' },
|
{ hex: "#929AA4", label: "Серый" },
|
||||||
{ hex: '#FFFFFF', label: 'Белый' },
|
{ hex: "#FFFFFF", label: "Белый" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const DEFAULT_STROKE_WIDTH = 1;
|
const DEFAULT_STROKE_WIDTH = 1;
|
||||||
const DEFAULT_BORDER_RADIUS = 5;
|
const DEFAULT_BORDER_RADIUS = 5;
|
||||||
const DEFAULT_EDGE_COLOR = '#b1b1b7';
|
const DEFAULT_EDGE_COLOR = "#b1b1b7";
|
||||||
const DEFAULT_TEXT_COLOR = '#FFFFFF';
|
const DEFAULT_TEXT_COLOR = "#FFFFFF";
|
||||||
const DEFAULT_FONT_SIZE = 11;
|
const DEFAULT_FONT_SIZE = 11;
|
||||||
|
|
||||||
interface EdgeInspectorProps {
|
interface EdgeInspectorProps {
|
||||||
@@ -81,11 +83,15 @@ export function EdgeInspector({
|
|||||||
onSendToBack,
|
onSendToBack,
|
||||||
}: EdgeInspectorProps) {
|
}: EdgeInspectorProps) {
|
||||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||||
const colorButtonRef = useRef<HTMLDivElement>(null) as RefObject<HTMLDivElement>;
|
const colorButtonRef = useRef<HTMLDivElement>(
|
||||||
|
null,
|
||||||
|
) as RefObject<HTMLDivElement>;
|
||||||
const currentColor = data.color ?? DEFAULT_EDGE_COLOR;
|
const currentColor = data.color ?? DEFAULT_EDGE_COLOR;
|
||||||
|
|
||||||
const [textColorPickerOpen, setTextColorPickerOpen] = useState(false);
|
const [textColorPickerOpen, setTextColorPickerOpen] = useState(false);
|
||||||
const textColorButtonRef = useRef<HTMLDivElement>(null) as RefObject<HTMLDivElement>;
|
const textColorButtonRef = useRef<HTMLDivElement>(
|
||||||
|
null,
|
||||||
|
) as RefObject<HTMLDivElement>;
|
||||||
const currentTextColor = data.textColor ?? DEFAULT_TEXT_COLOR;
|
const currentTextColor = data.textColor ?? DEFAULT_TEXT_COLOR;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -101,11 +107,26 @@ export function EdgeInspector({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="node-inspector__toolbar">
|
<div className="node-inspector__toolbar">
|
||||||
<Button size="s" view="secondary" label="На передний план" onClick={onBringToFront} />
|
<Button
|
||||||
<Button size="s" view="secondary" label="На задний план" onClick={onSendToBack} />
|
size="s"
|
||||||
|
view="secondary"
|
||||||
|
label="На передний план"
|
||||||
|
onClick={onBringToFront}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="s"
|
||||||
|
view="secondary"
|
||||||
|
label="На задний план"
|
||||||
|
onClick={onSendToBack}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextField label="Подпись" size="s" value={label} onChange={(value) => onLabelChange(value ?? '')} />
|
<TextField
|
||||||
|
label="Подпись"
|
||||||
|
size="s"
|
||||||
|
value={label}
|
||||||
|
onChange={(value) => onLabelChange(value ?? "")}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="node-inspector__field">
|
<div className="node-inspector__field">
|
||||||
<Text size="xs" view="secondary">
|
<Text size="xs" view="secondary">
|
||||||
@@ -118,23 +139,46 @@ export function EdgeInspector({
|
|||||||
type="button"
|
type="button"
|
||||||
title={presetLabel}
|
title={presetLabel}
|
||||||
aria-label={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 }}
|
style={{ background: hex }}
|
||||||
onClick={() => onDataChange({ color: hex })}
|
onClick={() => onDataChange({ color: hex })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{/*
|
||||||
|
Цвет линии заодно красит стрелку и подложку подписи (см.
|
||||||
|
labelBgStyle в DiagramEditor) — прозрачный здесь убирает и её.
|
||||||
|
*/}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Прозрачный"
|
||||||
|
aria-label="Прозрачный"
|
||||||
|
className={`node-inspector__preset node-inspector__preset--none${
|
||||||
|
currentColor === TRANSPARENT_COLOR ?
|
||||||
|
" node-inspector__preset--active"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => onDataChange({ color: TRANSPARENT_COLOR })}
|
||||||
|
>
|
||||||
|
<IconCancel size="xs" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref={colorButtonRef} className="node-inspector__color-trigger">
|
<div ref={colorButtonRef} className="node-inspector__color-trigger">
|
||||||
<Button size="s" view="secondary" label="Настроить цвет" onClick={() => setColorPickerOpen(true)} />
|
<Button
|
||||||
|
size="s"
|
||||||
|
view="secondary"
|
||||||
|
label="Настроить цвет"
|
||||||
|
onClick={() => setColorPickerOpen(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{colorPickerOpen && (
|
{colorPickerOpen && (
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
model={hexModel}
|
model={hexModel}
|
||||||
value={currentColor}
|
value={toPickerHex(currentColor, DEFAULT_EDGE_COLOR)}
|
||||||
onChange={(value) => onDataChange({ color: value })}
|
onChange={(value) => onDataChange({ color: value })}
|
||||||
anchorRef={colorButtonRef}
|
anchorRef={colorButtonRef}
|
||||||
open={colorPickerOpen}
|
open={colorPickerOpen}
|
||||||
onOpen={setColorPickerOpen}
|
onOpen={setColorPickerOpen}
|
||||||
|
style={{ zIndex: 100 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -150,23 +194,42 @@ export function EdgeInspector({
|
|||||||
type="button"
|
type="button"
|
||||||
title={presetLabel}
|
title={presetLabel}
|
||||||
aria-label={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 }}
|
style={{ background: hex }}
|
||||||
onClick={() => onDataChange({ textColor: hex })}
|
onClick={() => onDataChange({ textColor: hex })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Прозрачный"
|
||||||
|
aria-label="Прозрачный"
|
||||||
|
className={`node-inspector__preset node-inspector__preset--none${
|
||||||
|
currentTextColor === TRANSPARENT_COLOR ?
|
||||||
|
" node-inspector__preset--active"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => onDataChange({ textColor: TRANSPARENT_COLOR })}
|
||||||
|
>
|
||||||
|
<IconCancel size="xs" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref={textColorButtonRef} className="node-inspector__color-trigger">
|
<div ref={textColorButtonRef} className="node-inspector__color-trigger">
|
||||||
<Button size="s" view="secondary" label="Настроить цвет" onClick={() => setTextColorPickerOpen(true)} />
|
<Button
|
||||||
|
size="s"
|
||||||
|
view="secondary"
|
||||||
|
label="Настроить цвет"
|
||||||
|
onClick={() => setTextColorPickerOpen(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{textColorPickerOpen && (
|
{textColorPickerOpen && (
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
model={hexModel}
|
model={hexModel}
|
||||||
value={currentTextColor}
|
value={toPickerHex(currentTextColor, DEFAULT_TEXT_COLOR)}
|
||||||
onChange={(value) => onDataChange({ textColor: value })}
|
onChange={(value) => onDataChange({ textColor: value })}
|
||||||
anchorRef={textColorButtonRef}
|
anchorRef={textColorButtonRef}
|
||||||
open={textColorPickerOpen}
|
open={textColorPickerOpen}
|
||||||
onOpen={setTextColorPickerOpen}
|
onOpen={setTextColorPickerOpen}
|
||||||
|
style={{ zIndex: 100 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -205,7 +268,11 @@ export function EdgeInspector({
|
|||||||
size="xs"
|
size="xs"
|
||||||
name={`edge-line-style-${edgeId}`}
|
name={`edge-line-style-${edgeId}`}
|
||||||
items={LINE_STYLE_ITEMS}
|
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 })}
|
onChange={(item) => onDataChange({ lineStyle: item.style })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+61
-20
@@ -3,6 +3,8 @@ import { Button } from "@consta/uikit/Button";
|
|||||||
import { TextField } from "@consta/uikit/TextField";
|
import { TextField } from "@consta/uikit/TextField";
|
||||||
import { 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 { IconCancel } from "@consta/icons/IconCancel";
|
||||||
|
import { TRANSPARENT_COLOR, toPickerHex } from "./colors";
|
||||||
import {
|
import {
|
||||||
DEFAULT_CAPTION_TEXT_COLOR,
|
DEFAULT_CAPTION_TEXT_COLOR,
|
||||||
DEFAULT_NODE_BACKGROUND,
|
DEFAULT_NODE_BACKGROUND,
|
||||||
@@ -16,7 +18,12 @@ import {
|
|||||||
TASK_SUBTYPE_ICON,
|
TASK_SUBTYPE_ICON,
|
||||||
TASK_SUBTYPE_LABEL,
|
TASK_SUBTYPE_LABEL,
|
||||||
} from "./nodes";
|
} from "./nodes";
|
||||||
import type { DiagramNodeData, EventSubtype, NodeKind, TaskSubtype } from "./types";
|
import type {
|
||||||
|
DiagramNodeData,
|
||||||
|
EventSubtype,
|
||||||
|
NodeKind,
|
||||||
|
TaskSubtype,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const TASK_SUBTYPES: TaskSubtype[] = [
|
const TASK_SUBTYPES: TaskSubtype[] = [
|
||||||
"none",
|
"none",
|
||||||
@@ -77,7 +84,11 @@ const TYPE_LABELS: Record<NodeKind, string> = {
|
|||||||
dataObject: "Документ (Data Object)",
|
dataObject: "Документ (Data Object)",
|
||||||
};
|
};
|
||||||
|
|
||||||
const EVENT_KINDS = new Set<NodeKind>(["startEvent", "intermediateEvent", "endEvent"]);
|
const EVENT_KINDS = new Set<NodeKind>([
|
||||||
|
"startEvent",
|
||||||
|
"intermediateEvent",
|
||||||
|
"endEvent",
|
||||||
|
]);
|
||||||
|
|
||||||
interface NodeInspectorProps {
|
interface NodeInspectorProps {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -111,13 +122,9 @@ export function NodeInspector({
|
|||||||
const colorButtonRef = useRef<HTMLDivElement>(
|
const colorButtonRef = useRef<HTMLDivElement>(
|
||||||
null,
|
null,
|
||||||
) as RefObject<HTMLDivElement>;
|
) as RefObject<HTMLDivElement>;
|
||||||
// data.color can be a `var(--gradient-x)` reference from the preset row —
|
// data.color бывает не-hex: `var(--gradient-x)` от пресета-градиента или
|
||||||
// hexModel tries to parse that as a hex color and produces NaN, so the
|
// `transparent` — hexModel такое не разбирает, нужен hex-дефолт.
|
||||||
// hex-only ColorControl/ColorPicker always need a real flat hex fallback.
|
const flatColor = toPickerHex(data.color, DEFAULT_NODE_COLOR[nodeType]);
|
||||||
const flatColor =
|
|
||||||
data.color && !data.color.startsWith("var(") ?
|
|
||||||
data.color
|
|
||||||
: DEFAULT_NODE_COLOR[nodeType];
|
|
||||||
|
|
||||||
const [textColorPickerOpen, setTextColorPickerOpen] = useState(false);
|
const [textColorPickerOpen, setTextColorPickerOpen] = useState(false);
|
||||||
const textColorButtonRef = useRef<HTMLDivElement>(
|
const textColorButtonRef = useRef<HTMLDivElement>(
|
||||||
@@ -132,10 +139,12 @@ export function NodeInspector({
|
|||||||
if (mode === "view") {
|
if (mode === "view") {
|
||||||
const hasMeta = Boolean(data.responsible || data.duration);
|
const hasMeta = Boolean(data.responsible || data.duration);
|
||||||
const subtypeLabel =
|
const subtypeLabel =
|
||||||
nodeType === "task"
|
nodeType === "task" ?
|
||||||
? TASK_SUBTYPE_LABEL[(data.subtype as TaskSubtype | undefined) ?? "none"]
|
TASK_SUBTYPE_LABEL[(data.subtype as TaskSubtype | undefined) ?? "none"]
|
||||||
: EVENT_KINDS.has(nodeType)
|
: EVENT_KINDS.has(nodeType) ?
|
||||||
? EVENT_SUBTYPE_LABEL[(data.subtype as EventSubtype | undefined) ?? "none"]
|
EVENT_SUBTYPE_LABEL[
|
||||||
|
(data.subtype as EventSubtype | undefined) ?? "none"
|
||||||
|
]
|
||||||
: undefined;
|
: undefined;
|
||||||
return (
|
return (
|
||||||
<div className="node-inspector">
|
<div className="node-inspector">
|
||||||
@@ -245,7 +254,9 @@ export function NodeInspector({
|
|||||||
className={`node-inspector__subtype${isActive ? " node-inspector__subtype--active" : ""}`}
|
className={`node-inspector__subtype${isActive ? " node-inspector__subtype--active" : ""}`}
|
||||||
onClick={() => onChange({ subtype })}
|
onClick={() => onChange({ subtype })}
|
||||||
>
|
>
|
||||||
{Icon ? <Icon size="s" /> : <Text size="xs">—</Text>}
|
{Icon ?
|
||||||
|
<Icon size="s" />
|
||||||
|
: <Text size="xs">—</Text>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -259,8 +270,9 @@ export function NodeInspector({
|
|||||||
Тип триггера
|
Тип триггера
|
||||||
</Text>
|
</Text>
|
||||||
<div className="node-inspector__presets">
|
<div className="node-inspector__presets">
|
||||||
{EVENT_SUBTYPES_BY_KIND[nodeType as "startEvent" | "intermediateEvent" | "endEvent"].map(
|
{EVENT_SUBTYPES_BY_KIND[
|
||||||
(subtype: EventSubtype) => {
|
nodeType as "startEvent" | "intermediateEvent" | "endEvent"
|
||||||
|
].map((subtype: EventSubtype) => {
|
||||||
const Icon = EVENT_SUBTYPE_ICON[subtype];
|
const Icon = EVENT_SUBTYPE_ICON[subtype];
|
||||||
const isActive = (data.subtype ?? "none") === subtype;
|
const isActive = (data.subtype ?? "none") === subtype;
|
||||||
return (
|
return (
|
||||||
@@ -272,11 +284,12 @@ export function NodeInspector({
|
|||||||
className={`node-inspector__subtype${isActive ? " node-inspector__subtype--active" : ""}`}
|
className={`node-inspector__subtype${isActive ? " node-inspector__subtype--active" : ""}`}
|
||||||
onClick={() => onChange({ subtype })}
|
onClick={() => onChange({ subtype })}
|
||||||
>
|
>
|
||||||
{Icon ? <Icon size="s" /> : <Text size="xs">—</Text>}
|
{Icon ?
|
||||||
|
<Icon size="s" />
|
||||||
|
: <Text size="xs">—</Text>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
},
|
})}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -302,6 +315,19 @@ export function NodeInspector({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Прозрачный"
|
||||||
|
aria-label="Прозрачный"
|
||||||
|
className={`node-inspector__preset node-inspector__preset--none${
|
||||||
|
data.color === TRANSPARENT_COLOR ?
|
||||||
|
" node-inspector__preset--active"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => onChange({ color: TRANSPARENT_COLOR })}
|
||||||
|
>
|
||||||
|
<IconCancel size="xs" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref={colorButtonRef} className="node-inspector__color-trigger">
|
<div ref={colorButtonRef} className="node-inspector__color-trigger">
|
||||||
<Button
|
<Button
|
||||||
@@ -319,6 +345,7 @@ export function NodeInspector({
|
|||||||
anchorRef={colorButtonRef}
|
anchorRef={colorButtonRef}
|
||||||
open={colorPickerOpen}
|
open={colorPickerOpen}
|
||||||
onOpen={setColorPickerOpen}
|
onOpen={setColorPickerOpen}
|
||||||
|
style={{ zIndex: 100 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -339,6 +366,19 @@ export function NodeInspector({
|
|||||||
onClick={() => onChange({ textColor: hex })}
|
onClick={() => onChange({ textColor: hex })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Прозрачный"
|
||||||
|
aria-label="Прозрачный"
|
||||||
|
className={`node-inspector__preset node-inspector__preset--none${
|
||||||
|
currentTextColor === TRANSPARENT_COLOR ?
|
||||||
|
" node-inspector__preset--active"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => onChange({ textColor: TRANSPARENT_COLOR })}
|
||||||
|
>
|
||||||
|
<IconCancel size="xs" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ref={textColorButtonRef} className="node-inspector__color-trigger">
|
<div ref={textColorButtonRef} className="node-inspector__color-trigger">
|
||||||
<Button
|
<Button
|
||||||
@@ -351,11 +391,12 @@ export function NodeInspector({
|
|||||||
{textColorPickerOpen && (
|
{textColorPickerOpen && (
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
model={hexModel}
|
model={hexModel}
|
||||||
value={currentTextColor}
|
value={toPickerHex(currentTextColor, defaultTextColor)}
|
||||||
onChange={(value) => onChange({ textColor: value })}
|
onChange={(value) => onChange({ textColor: value })}
|
||||||
anchorRef={textColorButtonRef}
|
anchorRef={textColorButtonRef}
|
||||||
open={textColorPickerOpen}
|
open={textColorPickerOpen}
|
||||||
onOpen={setTextColorPickerOpen}
|
onOpen={setTextColorPickerOpen}
|
||||||
|
style={{ zIndex: 100 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -86,6 +86,51 @@
|
|||||||
opacity: 0.4;
|
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 {
|
.diagram-alignment-guide {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: #ff0072;
|
background: #ff0072;
|
||||||
@@ -312,6 +357,24 @@
|
|||||||
outline-offset: 2px;
|
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 {
|
.node-inspector__subtype {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
|||||||
@@ -21,3 +21,20 @@ body,
|
|||||||
.diagram-breadcrumbs:hover {
|
.diagram-breadcrumbs:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Экран ожидания `diagram:init` от хоста. Занимает ровно высоту вьюпорта и
|
||||||
|
* центрирует надпись. Раньше это был <p> с отступом: его собственный
|
||||||
|
* margin выходил за пределы 100%-высоты предка и давал вертикальный скролл
|
||||||
|
* на пустом экране.
|
||||||
|
*/
|
||||||
|
.diagram-loading {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
color: #525a64;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user