// Display label for a node when referenced from another panel (member list,
// note parent link, edge endpoints, etc.). Branches get a "分岐ノード" prefix
// so they're never confused with regular cards even when nameless.
// regular → name OR "(無名)"
// branch → name ? `分岐ノード(${name})` : '分岐ノード'
function nodeDisplayLabel(n){
if (!n) return '?';
const name = (n.name || '').trim();
if (n.variant === 'branch'){
return name ? `分岐ノード(${name})` : '分岐ノード';
}
return name || '(無名)';
}
// 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 (
{/* 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 && (
)}
{/* Group membership — branches are exclusive (single group), regular
cards can be multi-member. Branches show membership info but the
inline ✎ / × actions are hidden because removal would orphan them. */}
{nodeGroups.length > 0 ? (
🫧 所属ゾーン ({nodeGroups.length})
{nodeGroups.map(ng => (
{ng.name || '(無名)'}
{/* Branches can't be removed from their group (single-existence
not allowed). Hide the × so the user only deletes the branch
itself if they want it gone. */}
{!isBranch && (
)}
{n.emoji || (n.name||'?').charAt(0).toUpperCase()}
{nodeDisplayLabel(n)}
{/* Branches can't be removed from a group — single-existence
not allowed. Hide the × so the only path is to delete the
branch itself. */}
{!isBranchMember && (
)}
);
})}
>
);
}
function EdgeInspector({edge, nodes, updateEdge, deleteEdge, customRelations, learnRelation, removeCustomRelation, edgeNotes, addNoteOnEdge, updateNote, deleteNote}){
edgeNotes = edgeNotes || [];
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];
customRelations = customRelations || [];
// Track which chip is "just added" so it can pulse — the silent confirmation
// that a new term got registered.
const prevIdsRef = React.useRef(new Set(customRelations.map(r=>r.id)));
const [justAddedId, setJustAddedId] = React.useState(null);
React.useEffect(()=>{
const currentIds = new Set(customRelations.map(r=>r.id));
const newIds = [...currentIds].filter(id => !prevIdsRef.current.has(id));
prevIdsRef.current = currentIds;
if (newIds.length > 0) {
const id = newIds[newIds.length - 1];
setJustAddedId(id);
const t = setTimeout(()=>setJustAddedId(null), 1300);
return () => clearTimeout(t);
}
}, [customRelations]);
// When a custom preset is clicked we bake its label/color/stamp into the edge
// directly — this keeps customRelations as pure UI shortcuts. Deleting a
// custom later never breaks edges that used it.
const applyCustomPreset = (c) => {
updateEdge(edge.id, { label: c.label, color: c.color, stamp: c.stamp || '', relation: 'custom' });
};
// Explicit "register this term" action — replaces the old auto-learn-on-blur
// behavior because users couldn't tell when learning was happening.
const onRegisterClick = () => {
if (!learnRelation) return;
const v = edge.label ?? rel.label;
learnRelation(v, edge.color || rel.color, edge.stamp ?? rel.stamp);
};
return (
<>
矢印 / アクション編集
from {nodeDisplayLabel(from)}
→to {nodeDisplayLabel(to)}
{' '}
{RELATIONS.map(r=>(
))}
{/* User-learned vocabulary: chips populated automatically when the
label field is edited. × removes only the chip, never the edges
that used it (label/color/stamp are baked in at apply time). */}
{customRelations.length > 0 && (
<>
─── 覚えた用語 ───
{customRelations.map(c => (
))}
>
)}
updateEdge(edge.id,{label:e.target.value})}/>
{STAMPS.map(s=>(
))}
矢印のスタイル
{ARROW_STYLES.map(s=>(
))}
{/* Thickness — "thick" also makes the energy flow faster for visual weight */}
{/* Annotations attached to this arrow. Multi-OK, each is independently
draggable on the canvas and editable here. Mirrors the node-side
解説 UI for consistency. */}
>
);
}
// Shown when a note itself is selected (either by clicking it on the canvas or
// right after creating one). Lets the user edit the text directly and jump back
// to the owning card or arrow.
function NoteInspector({ note, parentNode, parentEdge, nodes, updateNote, deleteNote, setSelection }){
// For edge-attached notes, surface the from/to nodes so the user can see
// which arrow this note describes without having to click around.
const edgeFrom = parentEdge ? nodes.find(n => n.id === parentEdge.from) : null;
const edgeTo = parentEdge ? nodes.find(n => n.id === parentEdge.to) : null;
const placeholder = parentEdge
? 'この矢印アクションの意図・タイミング・条件などを自由に'
: 'このカードの役割・注意点・代替案などを自由に';
return (
<>