UI правки после созвона

This commit is contained in:
NikitaKalitov
2026-07-23 08:52:17 +03:00
parent 6692028045
commit 88997a338b
+111 -65
View File
@@ -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>
); );
} }