39 lines
4.4 KiB
TypeScript
39 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useContext } from "react";
|
|
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 { NodeEditing, ResizeToggle } from "./transformable-node";
|
|
import {useCanvasOverlay} from "./canvas-overlays";
|
|
import styles from "./canvas.module.css";
|
|
|
|
export function NodeStyleToolbar({ id, data, selected }: NodeProps<CanvasNode>) {
|
|
const overlay=useCanvasOverlay(id,"node-style");
|
|
const reduce = useReducedMotion();
|
|
const editing = useContext(NodeEditing);
|
|
const style = data.textStyle || {};
|
|
const text = data.kind === "text" || data.kind === "script" || data.kind === "plain-text";
|
|
const patch = (value: TextStyle, fontSize?: number) => editing.style(id, value, fontSize);
|
|
return <NodeToolbar style={{ zIndex: 100, pointerEvents: "auto" }} isVisible={true} position={Position.Top} offset={34}>
|
|
<AnimatePresence>{selected && <motion.div ref={overlay} initial={{ opacity: 0, y: reduce ? 0 : 5 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: reduce ? 0 : 5 }} transition={{ duration: reduce ? 0 : 0.14 }} className={`${styles.styleToolbar} nodrag nopan nowheel`} data-node-style={id} role="toolbar" aria-label="元素样式"
|
|
onPointerDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()} onDoubleClick={(event) => event.stopPropagation()}
|
|
onKeyDown={(event) => { if (event.key !== "Escape") event.stopPropagation(); }}>
|
|
{!(["image", "plain-image"].includes(data.kind)) && <label title="文字或图标颜色" className={styles.colorPicker}><span style={{ background: style.color || "#37352f" }} /><input aria-label="文字或图标颜色" type="color" value={style.color || "#37352f"} onChange={(event) => patch({ color: event.target.value })} /></label>}
|
|
{text && <>
|
|
<select aria-label="字体" value={style.fontFamily || "sans"} onChange={(event) => patch({ fontFamily: event.target.value as TextStyle["fontFamily"] })}><option value="sans">正文</option><option value="serif">衬线</option><option value="mono">等宽</option></select>
|
|
<input aria-label="字号" title="字号" type="number" min={8} max={160} value={data.fontSize || (data.kind === "plain-text" ? 24 : 13)} onChange={(event) => { 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]) => <button key={key} aria-label={label} aria-pressed={!!style[key]} title={label} onClick={() => patch({ [key]: !style[key] })}><Icon size={16} /></button>)}
|
|
{([["left", AlignLeft, "左对齐"], ["center", AlignCenter, "居中"], ["right", AlignRight, "右对齐"]] as const).map(([align, Icon, label]) => <button key={align} aria-label={label} aria-pressed={(style.align || "left") === align} title={label} onClick={() => patch({ align })}><Icon size={16} /></button>)}
|
|
<select aria-label="行距" title="行距" value={style.lineHeight || 1.5} onChange={(event) => patch({ lineHeight: Number(event.target.value) })}><option value={1}>1</option><option value={1.25}>1.25</option><option value={1.5}>1.5</option><option value={1.8}>1.8</option><option value={2}>2</option></select>
|
|
</>}
|
|
{!(["image", "plain-image"].includes(data.kind)) && <label className={styles.colorPicker} title="背景色"><span style={{ background: style.background && style.background !== "transparent" ? style.background : "#ffffff" }} /><input aria-label="背景色" type="color" value={style.background && style.background !== "transparent" ? style.background : "#ffffff"} onChange={(event) => patch({ background: event.target.value })} /></label>}
|
|
{!(["image", "plain-image"].includes(data.kind)) && <button title="清除背景" aria-label="清除背景" onClick={() => patch({ background: "transparent" })}>∅</button>}
|
|
{data.kind !== "icon" && <ResizeToggle id={id} />}
|
|
<button data-rotate-toggle={id} title="旋转模式" aria-label="旋转模式" aria-pressed={editing.rotating === id} onClick={() => editing.rotate(editing.rotating === id ? null : id)}><RotateCw size={16} /></button>
|
|
</motion.div>}</AnimatePresence>
|
|
</NodeToolbar>;
|
|
}
|