// Right-side inspector for either the selected node or edge function Inspector({ selection, setSelection, nodes, edges, groups, writings, updateNode, updateEdge, updateWriting, deleteNode, deleteEdge, deleteWriting, addNode, addWriting, clearAll, onLoadDemo, createGroupFromNode, updateGroup, deleteGroup, composingGroupId, setComposingGroupId, }){ groups = groups || []; writings = writings || []; if (!selection){ return (

Getting started

・Click + Node to add a character
・Drag the on the top-right of an avatar onto another character to create a relationship line
・Click a line or a node to edit it
Nothing selected.
Click a node or a line on the canvas.
); } 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)); return (
); } if (selection.type === 'edge'){ const edge = edges.find(e=>e.id===selection.id); if (!edge) return null; return (
); } if (selection.type === 'writing'){ const writing = writings.find(w => w.id === selection.id); if (!writing) return null; 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, nodeGroups, createGroupFromNode, updateGroup, deleteGroup, setComposingGroupId, setSelection}){ nodeGroups = nodeGroups || []; 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 ( <>

Edit node

updateNode(node.id,{name:e.target.value})} placeholder="Character name"/>
updateNode(node.id,{subtitle:e.target.value})} placeholder="Protagonist / Freshman / etc."/>

Avatar

{node.image && }
{EMOJI_PRESETS.map(em=>( ))}
{NODE_COLORS.map(c=>(
{[['circle','Circle'],['square','Square'],['bubble','Bubble']].map(([v,l])=>( ))}
{/* Name/subtitle position relative to the avatar. Default = bottom. Useful when nodes are packed close together and labels collide. */}
{[['top','Top'], ['left','Left'], ['bottom','Bottom'], ['right','Right']].map(([v,l])=>( ))}
{/* Group membership — multi-membership is allowed */} {nodeGroups.length > 0 ? (
🫧 Groups ({nodeGroups.length})
{nodeGroups.map(ng => (
{ng.name || '(Untitled)'}
))}
) : ( )} ); } function GroupInspector({group, nodes, updateGroup, deleteGroup, setComposingGroupId, setSelection}){ const members = (group.nodeIds || []) .map(nid => nodes.find(n => n.id === nid)) .filter(Boolean); const presets = ['#FFB3C7','#FFD3A8','#FFE8A3','#C7EFC0','#B5D9F2','#C9BEF5','#E8BCE8']; return ( <>

🫧 Edit group

updateGroup(group.id, {name:e.target.value})} placeholder="e.g. Team A / Main Cast"/>

Color

{presets.map(c=>(

Members ({members.length})

{members.map(n => (
{n.emoji || (n.name||'?').charAt(0).toUpperCase()} {n.name || '(Unnamed)'}
))}
); } function EdgeInspector({edge, nodes, updateEdge, deleteEdge}){ const from = nodes.find(n=>n.id===edge.from); const to = nodes.find(n=>n.id===edge.to); const rel = RELATIONS.find(r=>r.id===edge.relation) || RELATIONS[0]; return ( <>

Edit relationship

from {from?.name||'?'} to {to?.name||'?'} {' '}
{RELATIONS.map(r=>( ))}
updateEdge(edge.id,{label:e.target.value})}/>
{STAMPS.map(s=>( ))}

Arrow style

{ARROW_STYLES.map(s=>( ))}
{(() => { const presets = ['#F56A92','#F77F6E','#FFB86B','#7DD8B4','#8CCBF0','#B7A0EE','#7a6a7a','#3a2c3a']; return ( <> {presets.map(c=>(
{edge.customOffset !== undefined && edge.customOffset !== 0 && ( )}
💡 Shift+click another arrow to swap their stacking order
); } function WritingInspector({writing, nodes, edges, updateWriting, deleteWriting}){ const presets = ['#F56A92','#F77F6E','#FFB86B','#FFD97A','#7DD8B4','#8CCBF0','#B7A0EE','#3a2c3a']; const fontSize = writing.fontSize || 28; const rotation = writing.rotation || 0; const links = writing.links || []; const linkLabel = (link) => { if (link.type === 'node') { const n = nodes.find(x => x.id === link.id); return n ? `Node "${n.name || '?'}"` : '(deleted node)'; } if (link.type === 'edge') { const ed = edges.find(x => x.id === link.id); return ed ? `Arrow "${ed.label || ed.relation || ''}"` : '(deleted arrow)'; } return '?'; }; const removeLinkAt = (idx) => { const next = links.filter((_, i) => i !== idx); updateWriting(writing.id, { links: next }); }; const resetCurveAt = (idx) => { const next = links.map((l, i) => i === idx ? { ...l, lineCurve: 0 } : l); updateWriting(writing.id, { links: next }); }; const detachAll = () => updateWriting(writing.id, { links: [] }); return ( <>

📝 Edit writing