Files
Endoleap/src/components/canvas/edge-toolbar.tsx
T

33 lines
5.6 KiB
TypeScript

"use client";
import { EdgeToolbar } from "@xyflow/react";
import { Plus, Trash2 } from "lucide-react";
import type { EdgeData, EdgeMarker, MarkerShape } from "@/lib/canvas-v2";
import styles from "./canvas.module.css";
export function EdgeToolbarControls({ id, x, y, data, change, add, remove, count, knotMode, changeKnotMode }: { id: string; x: number; y: number; data: EdgeData; change: (data: EdgeData) => void; add: () => void; remove: () => void; count: number; knotMode?: string; changeKnotMode: (mode: "corner" | "smooth" | "symmetric") => void }) {
const a = data.appearance || {}, animation = data.animation || {};
const appearance = (patch: NonNullable<EdgeData["appearance"]>) => change({ ...data, appearance: { ...a, ...patch } });
const animate = (patch: NonNullable<EdgeData["animation"]>) => change({ ...data, animation: { ...animation, ...patch } });
const markers: [MarkerShape, string][] = [["none", "无"], ["open-arrow", "开放箭头"], ["triangle", "三角箭头"], ["circle", "圆形"], ["diamond", "菱形"], ["square", "方形"]];
return <EdgeToolbar alignY="bottom" edgeId={id} x={x} y={y} isVisible style={{ zIndex: 115, pointerEvents: "auto" }}><div role="toolbar" aria-label="连线编辑" className={`${styles.edgeToolbar} nodrag nopan nowheel`}
onPointerDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()} onDoubleClick={(event) => event.stopPropagation()} onKeyDown={(event) => event.stopPropagation()}>
<label>颜色<input aria-label="连线颜色" type="color" value={a.color || "#aa968b"} onChange={(event) => appearance({ color: event.target.value })} /></label>
<label>粗细<input aria-label="连线粗细" type="number" min={.5} max={20} step={.5} value={a.width ?? 1.5} onChange={(event) => { const value = +event.target.value; if (value >= .5 && value <= 20) appearance({ width: value }); }} /></label>
<label>线条<select aria-label="线条类型" value={a.line || "solid"} onChange={(event) => appearance({ line: event.target.value as typeof a.line })}><option value="solid">实线</option><option value="dashed">虚线</option><option value="dotted">点线</option></select></label>
{(["head", "tail"] as const).map((end) => <label key={end}>{end === "head" ? "箭头" : "箭尾"}<select aria-label={end === "head" ? "箭头类型" : "箭尾类型"} value={a[end]?.type || "none"} onChange={(event) => appearance({ [end]: { ...a[end], type: event.target.value as EdgeMarker["type"] } })}>{markers.map(([id, label]) => <option key={id} value={id}>{label}</option>)}</select>
<input aria-label={end === "head" ? "箭头大小" : "箭尾大小"} type="number" min={4} max={32} value={a[end]?.size || 12} onChange={(event) => { const size = +event.target.value; if (size >= 4 && size <= 32) appearance({ [end]: { ...a[end], type: a[end]?.type || "none", size } }); }} />
<input aria-label={end === "head" ? "填充箭头" : "填充箭尾"} type="checkbox" checked={a[end]?.filled ?? true} onChange={(event) => appearance({ [end]: { ...a[end], type: a[end]?.type || "none", filled: event.target.checked } })} /></label>)}
<label>透明度<input aria-label="连线透明度" type="range" min={.05} max={1} step={.05} value={a.opacity ?? 1} onChange={(event) => appearance({ opacity: +event.target.value })} /></label>
<label>动画<select aria-label="动画播放时机" value={animation.mode || "off"} onChange={(event) => animate({ mode: event.target.value as typeof animation.mode })}><option value="off">关闭</option><option value="always">始终播放</option><option value="executing">仅执行时</option></select></label>
<label>效果<select aria-label="连线动画效果" value={animation.effect || "flow"} onChange={(event) => animate({ effect: event.target.value as typeof animation.effect })}><option value="flow">流光</option><option value="dash">流动虚线</option><option value="pulse">脉冲</option></select></label>
<label>方向<select aria-label="动画方向" value={animation.direction || "forward"} onChange={(event) => animate({ direction: event.target.value as typeof animation.direction })}><option value="forward">正向</option><option value="reverse">反向</option></select></label>
<label>时长<input aria-label="动画时长" title="毫秒" type="number" min={250} max={30000} step={250} value={animation.durationMs || 1600} onChange={(event) => { const durationMs = +event.target.value; if (durationMs >= 250 && durationMs <= 30000) animate({ durationMs }); }} /></label>
<label>光色<input aria-label="动画颜色" type="color" value={animation.color || "#ffffff"} onChange={(event) => animate({ color: event.target.value })} /></label>
<label><input aria-label="参与下游传播" type="checkbox" checked={data.execution?.enabled !== false} onChange={(event) => change({ ...data, execution: { enabled: event.target.checked } })} />传递</label>
<label>曲线点<select aria-label="曲线点控制模式" disabled={!count} value={knotMode || "smooth"} onChange={(event) => changeKnotMode(event.target.value as "corner" | "smooth" | "symmetric")}><option value="corner">独立控制柄</option><option value="smooth">平滑</option><option value="symmetric">镜像</option></select></label>
<button type="button" aria-label="添加曲线点" title="也可双击连线添加曲线点" disabled={count >= 64} onClick={add}><Plus size={14} /></button>
<button type="button" aria-label="删除选中曲线点" disabled={!count} onClick={remove}><Trash2 size={14} /></button>
<small>拖动锚点或控制柄 · 双击添加 · Shift 镜像控制柄</small>
</div></EdgeToolbar>;
}