UI правки после созвона
This commit is contained in:
+111
-65
@@ -1,8 +1,8 @@
|
|||||||
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 { 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 {
|
import {
|
||||||
DEFAULT_CAPTION_TEXT_COLOR,
|
DEFAULT_CAPTION_TEXT_COLOR,
|
||||||
DEFAULT_NODE_BACKGROUND,
|
DEFAULT_NODE_BACKGROUND,
|
||||||
@@ -10,51 +10,51 @@ import {
|
|||||||
DEFAULT_NODE_FONT_SIZE,
|
DEFAULT_NODE_FONT_SIZE,
|
||||||
DEFAULT_NODE_TEXT_COLOR,
|
DEFAULT_NODE_TEXT_COLOR,
|
||||||
DEFAULT_TASK_BORDER_RADIUS,
|
DEFAULT_TASK_BORDER_RADIUS,
|
||||||
} from './nodes';
|
} from "./nodes";
|
||||||
import type { DiagramNodeData, NodeKind } from './types';
|
import type { DiagramNodeData, NodeKind } from "./types";
|
||||||
|
|
||||||
// Every gradient defined in palette.css, across its three "for diagrams /
|
// Every gradient defined in palette.css, across its three "for diagrams /
|
||||||
// flowcharts / semantic highlights" sections. Consta's ColorPicker `palette`
|
// flowcharts / semantic highlights" sections. Consta's ColorPicker `palette`
|
||||||
// prop can't render these — it goes through hexModel, which only understands
|
// prop can't render these — it goes through hexModel, which only understands
|
||||||
// flat colors — so these render as our own swatch row instead.
|
// flat colors — so these render as our own swatch row instead.
|
||||||
const GRADIENT_PRESETS: { name: string; label: string }[] = [
|
const GRADIENT_PRESETS: { name: string; label: string }[] = [
|
||||||
{ name: 'navy', label: 'Тёмно-синий' },
|
{ name: "navy", label: "Тёмно-синий" },
|
||||||
{ name: 'azure', label: 'Лазурный' },
|
{ name: "azure", label: "Лазурный" },
|
||||||
{ name: 'sky', label: 'Небесный' },
|
{ name: "sky", label: "Небесный" },
|
||||||
{ name: 'teal-dark', label: 'Тёмная бирюза' },
|
{ name: "teal-dark", label: "Тёмная бирюза" },
|
||||||
{ name: 'teal-light', label: 'Светлая бирюза' },
|
{ name: "teal-light", label: "Светлая бирюза" },
|
||||||
{ name: 'navy-dark', label: 'Глубокий синий' },
|
{ name: "navy-dark", label: "Глубокий синий" },
|
||||||
{ name: 'ice', label: 'Лёд' },
|
{ name: "ice", label: "Лёд" },
|
||||||
{ name: 'frost', label: 'Иней' },
|
{ name: "frost", label: "Иней" },
|
||||||
{ name: 'purple', label: 'Пурпурный' },
|
{ name: "purple", label: "Пурпурный" },
|
||||||
{ name: 'red', label: 'Красный' },
|
{ name: "red", label: "Красный" },
|
||||||
{ name: 'orange', label: 'Оранжевый' },
|
{ name: "orange", label: "Оранжевый" },
|
||||||
{ name: 'olive', label: 'Оливковый' },
|
{ name: "olive", label: "Оливковый" },
|
||||||
{ name: 'green', label: 'Зелёный' },
|
{ name: "green", label: "Зелёный" },
|
||||||
{ name: 'green-dark', label: 'Тёмно-зелёный' },
|
{ name: "green-dark", label: "Тёмно-зелёный" },
|
||||||
{ name: 'blue', label: 'Синий' },
|
{ name: "blue", label: "Синий" },
|
||||||
{ name: 'cyan', label: 'Циан' },
|
{ name: "cyan", label: "Циан" },
|
||||||
{ name: 'violet', label: 'Виолет' },
|
{ name: "violet", label: "Виолет" },
|
||||||
{ name: 'lavender', label: 'Лавандовый' },
|
{ name: "lavender", label: "Лавандовый" },
|
||||||
{ name: 'pink', label: 'Розовый' },
|
{ name: "pink", label: "Розовый" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// "Цвета текста" 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 TYPE_LABELS: Record<NodeKind, string> = {
|
const TYPE_LABELS: Record<NodeKind, string> = {
|
||||||
task: 'Задача',
|
task: "Задача",
|
||||||
startEvent: 'Начальное событие',
|
startEvent: "Начальное событие",
|
||||||
intermediateEvent: 'Промежуточное событие',
|
intermediateEvent: "Промежуточное событие",
|
||||||
endEvent: 'Конечное событие',
|
endEvent: "Конечное событие",
|
||||||
exclusiveGateway: 'Исключающий шлюз',
|
exclusiveGateway: "Исключающий шлюз",
|
||||||
parallelGateway: 'Параллельный шлюз',
|
parallelGateway: "Параллельный шлюз",
|
||||||
inclusiveGateway: 'Включающий шлюз',
|
inclusiveGateway: "Включающий шлюз",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NodeInspectorProps {
|
interface NodeInspectorProps {
|
||||||
@@ -62,7 +62,7 @@ interface NodeInspectorProps {
|
|||||||
nodeType: NodeKind;
|
nodeType: NodeKind;
|
||||||
data: DiagramNodeData;
|
data: DiagramNodeData;
|
||||||
childCount: number;
|
childCount: number;
|
||||||
mode: 'edit' | 'view';
|
mode: "edit" | "view";
|
||||||
onChange: (patch: Partial<DiagramNodeData>) => void;
|
onChange: (patch: Partial<DiagramNodeData>) => void;
|
||||||
onEnterSubProcess: (nodeId: string) => void;
|
onEnterSubProcess: (nodeId: string) => void;
|
||||||
onDuplicate: () => void;
|
onDuplicate: () => void;
|
||||||
@@ -86,20 +86,28 @@ 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 colorButtonRef = useRef<HTMLDivElement>(null) as RefObject<HTMLDivElement>;
|
const colorButtonRef = useRef<HTMLDivElement>(
|
||||||
|
null,
|
||||||
|
) as RefObject<HTMLDivElement>;
|
||||||
// data.color can be a `var(--gradient-x)` reference from the preset row —
|
// 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
|
// 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.
|
// 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 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>(null) as RefObject<HTMLDivElement>;
|
const textColorButtonRef = useRef<HTMLDivElement>(
|
||||||
|
null,
|
||||||
|
) as RefObject<HTMLDivElement>;
|
||||||
// Text sits directly on the colored fill for tasks (needs a light default)
|
// 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).
|
// 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 defaultTextColor =
|
||||||
|
nodeType === "task" ? DEFAULT_NODE_TEXT_COLOR : DEFAULT_CAPTION_TEXT_COLOR;
|
||||||
const currentTextColor = data.textColor ?? defaultTextColor;
|
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);
|
||||||
return (
|
return (
|
||||||
<div className="node-inspector">
|
<div className="node-inspector">
|
||||||
@@ -114,7 +122,11 @@ export function NodeInspector({
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{data.description && (
|
{data.description && (
|
||||||
<Text size="s" view="secondary" className="node-inspector__description">
|
<Text
|
||||||
|
size="s"
|
||||||
|
view="secondary"
|
||||||
|
className="node-inspector__description"
|
||||||
|
>
|
||||||
{data.description}
|
{data.description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -160,13 +172,28 @@ export function NodeInspector({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="node-inspector__toolbar">
|
<div className="node-inspector__toolbar">
|
||||||
<Button size="s" view="secondary" label="Дублировать" onClick={onDuplicate} />
|
<Button
|
||||||
|
size="s"
|
||||||
|
view="secondary"
|
||||||
|
label="Дублировать"
|
||||||
|
onClick={onDuplicate}
|
||||||
|
/>
|
||||||
<Button size="s" view="secondary" label="Удалить" onClick={onDelete} />
|
<Button size="s" view="secondary" label="Удалить" onClick={onDelete} />
|
||||||
</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>
|
||||||
|
|
||||||
<div className="node-inspector__field">
|
<div className="node-inspector__field">
|
||||||
@@ -176,14 +203,15 @@ export function NodeInspector({
|
|||||||
<div className="node-inspector__presets">
|
<div className="node-inspector__presets">
|
||||||
{GRADIENT_PRESETS.map(({ name, label }) => {
|
{GRADIENT_PRESETS.map(({ name, label }) => {
|
||||||
const value = `var(--gradient-${name})`;
|
const value = `var(--gradient-${name})`;
|
||||||
const isActive = (data.color ?? DEFAULT_NODE_BACKGROUND[nodeType]) === value;
|
const isActive =
|
||||||
|
(data.color ?? DEFAULT_NODE_BACKGROUND[nodeType]) === value;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={name}
|
key={name}
|
||||||
type="button"
|
type="button"
|
||||||
title={label}
|
title={label}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
className={`node-inspector__preset${isActive ? ' node-inspector__preset--active' : ''}`}
|
className={`node-inspector__preset${isActive ? " node-inspector__preset--active" : ""}`}
|
||||||
style={{ background: value }}
|
style={{ background: value }}
|
||||||
onClick={() => onChange({ color: value })}
|
onClick={() => onChange({ color: value })}
|
||||||
/>
|
/>
|
||||||
@@ -191,7 +219,12 @@ export function NodeInspector({
|
|||||||
})}
|
})}
|
||||||
</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
|
||||||
@@ -216,14 +249,19 @@ export function NodeInspector({
|
|||||||
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={() => onChange({ textColor: hex })}
|
onClick={() => onChange({ textColor: hex })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</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
|
||||||
@@ -256,7 +294,7 @@ export function NodeInspector({
|
|||||||
rows={2}
|
rows={2}
|
||||||
size="s"
|
size="s"
|
||||||
value={data.label}
|
value={data.label}
|
||||||
onChange={(value) => onChange({ label: value ?? '' })}
|
onChange={(value) => onChange({ label: value ?? "" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -264,25 +302,26 @@ export function NodeInspector({
|
|||||||
type="textarea"
|
type="textarea"
|
||||||
rows={3}
|
rows={3}
|
||||||
size="s"
|
size="s"
|
||||||
value={data.description ?? ''}
|
value={data.description ?? ""}
|
||||||
onChange={(value) => onChange({ description: value ?? '' })}
|
onChange={(value) => onChange({ description: value ?? "" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{nodeType === "task" && (
|
||||||
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
label="Ответственный"
|
label="Ответственный"
|
||||||
size="s"
|
size="s"
|
||||||
value={data.responsible ?? ''}
|
value={data.responsible ?? ""}
|
||||||
onChange={(value) => onChange({ responsible: value ?? '' })}
|
onChange={(value) => onChange({ responsible: value ?? "" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Длительность"
|
label="Длительность"
|
||||||
size="s"
|
size="s"
|
||||||
value={data.duration ?? ''}
|
value={data.duration ?? ""}
|
||||||
onChange={(value) => onChange({ duration: value ?? '' })}
|
onChange={(value) => onChange({ duration: value ?? "" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{nodeType === 'task' && (
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Скругление углов"
|
label="Скругление углов"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -295,14 +334,21 @@ export function NodeInspector({
|
|||||||
if (value && !Number.isNaN(n)) onChange({ borderRadius: n });
|
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 ?
|
||||||
? `Внутри уже ${childCount} узл(ов).`
|
`Внутри уже ${childCount} узл(ов).`
|
||||||
: 'Пока пусто — зайдя внутрь, можно добавить узлы детального разбора этого шага.'}
|
: "Пока пусто — зайдя внутрь, можно добавить узлы детального разбора этого шага."
|
||||||
|
}
|
||||||
</Text>
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user