import type { MindNode, Op, StylePreset } from "@mind-map/shared"; import { useEffect, useMemo, useRef, useState } from "react"; import { commitField, fieldEditorSeed, hiddenAddableFields, visiblePropRows, } from "./registry"; import type { PropFieldDef, PropFieldId } from "./types"; import { uploadImageFile } from "./upload-image"; import "./node-shell.css"; type ApplyOps = (ops: Op[], summary?: string) => void | Promise; export function NodePropEditor({ node, applyOps, accent, variant = "overlay", onDragHandlePointerDown, showStylePreset, }: { node: MindNode; applyOps: ApplyOps; accent?: string; variant?: "overlay" | "panel"; onDragHandlePointerDown?: (e: React.PointerEvent) => void; showStylePreset?: boolean; }) { const [forced, setForced] = useState>(() => new Set()); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { setForced(new Set()); setMenuOpen(false); }, [node.id]); const rows = useMemo( () => visiblePropRows(node, forced), [node, forced], ); const addable = useMemo( () => hiddenAddableFields(node, forced), [node, forced], ); const commit = (id: PropFieldId, raw: string) => { const ops = commitField(node, id, raw); if (!ops.length) return; void applyOps(ops, `prop ${id}`); if (!raw.trim() && id !== "text") { setForced((prev) => { const next = new Set(prev); next.delete(id); return next; }); } }; return (
e.stopPropagation()} onBlur={(e) => { if (e.target.value !== node.text) commit("text", e.target.value); }} onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} />
{showStylePreset ? (
类型
) : null}
强调色
{ const v = e.target.value.trim(); const next = v || null; if ((node.accentColor ?? null) === next) return; void applyOps([ { type: "update_node_meta", nodeId: node.id, patch: { accentColor: next as never }, }, ]); }} />
图标
{rows .filter((r) => r.def.id !== "text") .map(({ def }) => ( commit(def.id, raw)} onClear={() => { commit(def.id, ""); setForced((prev) => { const next = new Set(prev); next.delete(def.id); return next; }); }} /> ))}
{addable.length > 0 ? (
e.stopPropagation()} > {menuOpen ? (
{addable.map((f) => ( ))}
) : null}
) : null}
); } function FieldRow({ def, node, onCommit, onClear, }: { def: PropFieldDef; node: MindNode; onCommit: (raw: string) => void; onClear: () => void; }) { const seed = fieldEditorSeed(node, def.id); const canClear = !def.always; const [uploading, setUploading] = useState(false); const [uploadErr, setUploadErr] = useState(null); const fileRef = useRef(null); if (def.kind === "image") { return (
{def.label}
e.stopPropagation()}> {seed ? : null}
{ const file = e.target.files?.[0]; e.target.value = ""; if (!file) return; setUploadErr(null); setUploading(true); void uploadImageFile(file) .then((url) => onCommit(url)) .catch((err) => setUploadErr(String(err?.message || err || "上传失败")), ) .finally(() => setUploading(false)); }} /> { const v = e.target.value.trim(); if (v !== (seed || "")) onCommit(v); }} onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} /> {canClear ? ( ) : null}
{uploadErr ?
{uploadErr}
: null}
支持本地选图或粘贴 https://… 链接
); } return (
{def.label}
e.stopPropagation()}> {def.kind === "textarea" || def.kind === "links" ? (