/** * 选中节点的样式浮层。把颜色、文字排版、背景与旋转/拉伸模式转换成作用域编辑动作,不在此保存样式副本。 */ "use client"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { NodeToolbar, Position, type NodeProps } from "@xyflow/react"; import { AlignCenter, AlignLeft, AlignRight, Bold, Italic, RotateCw, Underline } from "lucide-react"; import type { CanvasNode } from "@/lib/canvas"; import type { TextStyle } from "@/lib/canvas-elements"; import { ResizeToggle } from "./transformable-node"; import {useCanvasOverlay} from "./canvas-overlays"; import {useScopedNodeEditing} from "./node-editing"; import {canvasKeyContext} from "@/lib/canvas-bindings/input-router"; import styles from "./canvas.module.css"; /** NodeToolbar负责定位,AnimatePresence负责选中/取消选中时的进出动画,减少动态效果时关闭位移与时长。 * 浮层注册到overlay索引,保证Portal中的输入也能归属于正确视图与元素。 */ export function NodeStyleToolbar({ id, data, selected }: NodeProps) { const overlay=useCanvasOverlay(id,"node-style"); const reduce = useReducedMotion(); const editing = useScopedNodeEditing(id,data.__headlessTarget); const style = data.textStyle || {}; // 只有text/script/plain-text展示文字排版;图片不展示前景/背景色,图标可调颜色但不展示拉伸开关。 const text = data.kind === "text" || data.kind === "script" || data.kind === "plain-text"; /** 统一样式动作入口;字号作为独立可选参数,避免把它混进textStyle的字段空间。 */ const patch = (value: TextStyle, fontSize?: number) => editing.style(id, value, fontSize); // 颜色输入用现有值或默认色;字号仅提交8—160范围,透明背景在取色器中显示白色但不自动写回模型。 // 加粗/斜体/下划线按当前值取反,对齐和行高直接提交所选值;清除背景明确写transparent。 // 常规事件停止冒泡;Escape只在非组合输入、非文本编辑且未被处理时关闭当前节点的变换模式。 return {selected && event.stopPropagation()} onClick={(event) => event.stopPropagation()} onDoubleClick={(event) => event.stopPropagation()} onKeyDown={(event) => { // 其它按键留给当前控件并阻断全局快捷键;Escape让输入上下文先决定是否可用于退出模式。 if(event.key!=="Escape"){event.stopPropagation();return;} if(event.defaultPrevented||event.nativeEvent.isComposing||canvasKeyContext(event.nativeEvent).textEditing)return; // The registered overlay owns its mode dismissal; global shortcuts stay out. editing.rotate(null);editing.resize(null);event.preventDefault();event.stopPropagation(); }}> {!(["image", "plain-image"].includes(data.kind)) && } {text && <> { const size = Number(event.target.value); if (size >= 8 && size <= 160) patch({}, size); }} /> {([["bold", Bold, "加粗"], ["italic", Italic, "斜体"], ["underline", Underline, "下划线"]] as const).map(([key, Icon, label]) => )} {([["left", AlignLeft, "左对齐"], ["center", AlignCenter, "居中"], ["right", AlignRight, "右对齐"]] as const).map(([align, Icon, label]) => )} } {!(["image", "plain-image"].includes(data.kind)) && } {!(["image", "plain-image"].includes(data.kind)) && } {data.kind !== "icon" && } }; }