// 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 (
はじめかた
・+ノード追加で人物を置こう
・アイコン右上の+をドラッグして相手にドロップ → 関係線ができるよ
・線やノードをクリックで編集
何も選択されていないよ。
キャンバスのノードや線をクリックしてね。
);
}
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 (
<>
ノード編集
アイコン
{node.image && }
{EMOJI_PRESETS.map(em=>(
))}
{NODE_COLORS.map(c=>(
{[['circle','まる'],['square','四角'],['bubble','吹き出し']].map(([v,l])=>(
))}
{/* Name/subtitle position relative to the avatar. Default = bottom.
Useful when nodes are packed close together and labels collide. */}
{[['top','上'], ['left','左'], ['bottom','下'], ['right','右']].map(([v,l])=>(
))}
{/* Group membership — multi-membership is allowed */}
{nodeGroups.length > 0 ? (
🫧 所属グループ ({nodeGroups.length})
{nodeGroups.map(ng => (
{ng.name || '(無名)'}
))}
) : (
)}
>
);
}
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 (
<>
🫧 グループ編集
色
{presets.map(c=>(
メンバー ({members.length})
{members.map(n => (
{n.emoji || (n.name||'?').charAt(0).toUpperCase()}
{n.name || '(無名)'}
))}
>
);
}
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 (
<>
関係を編集
from {from?.name||'?'}
→
to {to?.name||'?'}
{' '}
{RELATIONS.map(r=>(
))}
updateEdge(edge.id,{label:e.target.value})}/>
{STAMPS.map(s=>(
))}
矢印のスタイル
{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+クリック で重なり順を入れ替え
>
);
}
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 || [];
// Resolve a link to a human-readable label for the link list.
const linkLabel = (link) => {
if (link.type === 'node') {
const n = nodes.find(x => x.id === link.id);
return n ? `ノード「${n.name || '?'}」` : '(削除済みのノード)';
}
if (link.type === 'edge') {
const ed = edges.find(x => x.id === link.id);
return ed ? `矢印「${ed.label || ed.relation || ''}」` : '(削除済みの矢印)';
}
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 (
<>
📝 書き込み編集
updateWriting(writing.id, {fontSize: parseInt(e.target.value, 10)}, {skipHistory:true})}
onMouseUp={e=>updateWriting(writing.id, {fontSize: parseInt(e.target.value, 10)})}
onTouchEnd={e=>updateWriting(writing.id, {fontSize: parseInt(e.target.value, 10)})}/>
{[16, 24, 32, 48, 64].map(v => (
))}
updateWriting(writing.id, {rotation: parseInt(e.target.value, 10)}, {skipHistory:true})}
onMouseUp={e=>updateWriting(writing.id, {rotation: parseInt(e.target.value, 10)})}
onTouchEnd={e=>updateWriting(writing.id, {rotation: parseInt(e.target.value, 10)})}/>
{rotation !== 0 && (
)}
文字色(線色も連動)
{presets.map(c => (
リンク
{links.length === 0 ? (
未接続
) : (
{links.map((link, idx) => {
const bent = (link.lineCurve || 0) !== 0;
return (
🔗 {linkLabel(link)}
{bent && (
)}
);
})}
)}
{links.length > 0 && (
)}
>
);
}
window.Inspector = Inspector;