// Display label for a node when referenced from another panel (member list, // note parent link, edge endpoints, etc.). Branches get a "Branch node" prefix // so they're never confused with regular cards even when nameless. // regular → name OR "(untitled)" // branch → name ? `Branch node (${name})` : 'Branch node' function nodeDisplayLabel(n){ if (!n) return '?'; const name = (n.name || '').trim(); if (n.variant === 'branch'){ return name ? `Branch node (${name})` : 'Branch node'; } return name || '(untitled)'; } // Right-side inspector for either the selected node, edge, group or note function Inspector({ selection, setSelection, nodes, edges, groups, notes, customRelations, learnRelation, removeCustomRelation, updateNode, updateEdge, deleteNode, deleteEdge, duplicateNode, addNote, addNoteOnEdge, updateNote, deleteNote, addNode, clearAll, onLoadDemo, createGroupFromNode, updateGroup, deleteGroup, removeNodeFromGroup, composingGroupId, setComposingGroupId, }){ groups = groups || []; notes = notes || []; customRelations = customRelations || []; if (!selection){ return (

Getting started

· + Card to drop a card on the board
· Drag the on a card onto another card → draws an action arrow
· Click an arrow or card to fine-tune color, style, thickness, and more
· Use ● Branch node to drop a small waypoint on an arrow OR inside a zone — great for forks, merging lines, or marking in-zone state
· Add an ✨ Effect (buff / damage / heal, etc.) to any card to show its state
Nothing selected.
Click a card or an arrow.
); } if (selection.type === 'node'){ const node = nodes.find(n=>n.id===selection.id); if (!node) return null; const nodeGroups = groups.filter(g => (g.nodeIds || []).includes(node.id)); const nodeNotes = notes.filter(n => n.nodeId === node.id); return (
); } if (selection.type === 'note'){ const note = notes.find(n => n.id === selection.id); if (!note) return null; const parentNode = note.nodeId ? nodes.find(n => n.id === note.nodeId) : null; const parentEdge = note.targetEdgeId ? edges.find(e => e.id === note.targetEdgeId) : null; return (
); } if (selection.type === 'edge'){ const edge = edges.find(e=>e.id===selection.id); if (!edge) return null; const edgeNotes = notes.filter(n => n.targetEdgeId === edge.id); return (
); } if (selection.type === 'group'){ const group = groups.find(g=>g.id===selection.id); if (!group) return null; return (
); } return null; } function NodeInspector({node, updateNode, deleteNode, duplicateNode, nodeGroups, nodeNotes, addNote, updateNote, deleteNote, createGroupFromNode, updateGroup, deleteGroup, removeNodeFromGroup, setComposingGroupId, setSelection}){ nodeGroups = nodeGroups || []; nodeNotes = nodeNotes || []; const isBranch = node.variant === 'branch'; const isEdgeAttached = isBranch && !!node.attachedEdgeId; const fileRef = React.useRef(null); const onFile = async (e) => { const f = e.target.files?.[0]; if (!f) return; try { const dataUrl = await compressImage(f); updateNode(node.id, { image: dataUrl }); } catch(err) { console.error('Image compression failed', err); } e.target.value = ''; }; return ( <>

{isBranch ? 'Edit branch node' : 'Edit card'}

updateNode(node.id,{name:e.target.value})} placeholder="Card name"/>
updateNode(node.id,{subtitle:e.target.value})} placeholder="e.g. Normal Monster / Hand / Field"/>

{isBranch ? 'Appearance' : 'Card image'}

{!isBranch && (
{node.image && }
)}
{EMOJI_PRESETS.map(em=>( ))}
{NODE_COLORS.map(c=>(
{!isBranch && (
{[['square','Card'],['circle','Circle'],['bubble','Bubble']].map(([v,l])=>( ))}
)} {/* Name/subtitle placement — default is below the card. Useful for dense layouts where the label would collide with another card or arrow. */}
{[['top','Top'], ['left','Left'], ['bottom','Bottom'], ['right','Right']].map(([v,l])=>( ))}
{/* Effect: rising particles that convey buff / damage / status on the card. Migrated from legacy node.aura (boolean) — if aura is true treat it as 'buff'. Hidden for branch nodes — the small avatar can't host visible particles. */} {!isBranch && (
{[ ['none', 'None'], ['buff', '✨ Buff'], ['damage', '💥 Damage'], ['status', '🌀 Status'], ['heal', '💚 Heal'], ['shield', '🛡 Shield'], ].map(([v,l])=>{ const cur = node.effect || (node.aura ? 'buff' : 'none'); return ( ); })}
)}
{/* Annotations attached to this card. Multi-OK, each is independently draggable on the canvas and editable here. */}
💬 Notes {nodeNotes.length > 0 && `(${nodeNotes.length})`}
{nodeNotes.map(n => (