Приводил к стилу consta UI

This commit is contained in:
NikitaKalitov
2026-07-22 09:42:26 +03:00
parent 52e28e8dc6
commit 4852e55964
7 changed files with 478 additions and 75 deletions
+135 -30
View File
@@ -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