// Draggable node for the tour/transit tool. // Three rendering modes: // - image : node.image is set (uploaded photo) // - icon : node.icon is one of the Lucide icon names from icons.js // - emoji/init : fallback (kept for compatibility / freeform notes) function Node({ node, selected, onSelect, onDrag, onDragEnd, onDelete, dragging, worldZoom, }){ const ref = React.useRef(null); const moveState = React.useRef(null); const onPointerDown = (e) => { if (e.target.closest('.delete-btn')) return; e.stopPropagation(); // shiftKey is forwarded so Canvas can dispatch "copy times from this // node into the currently-selected one" without selecting this node. onSelect(node.id, { shiftKey: !!e.shiftKey }); const startX = e.clientX, startY = e.clientY; const ox = node.x, oy = node.y; moveState.current = { startX, startY, ox, oy, moved:false }; e.currentTarget.setPointerCapture(e.pointerId); }; const onPointerMove = (e) => { if (!moveState.current) return; const { startX, startY, ox, oy } = moveState.current; const dx = (e.clientX - startX) / worldZoom; const dy = (e.clientY - startY) / worldZoom; if (Math.abs(dx)+Math.abs(dy) > 2) moveState.current.moved = true; onDrag(node.id, ox+dx, oy+dy); }; const onPointerUp = (e) => { if (!moveState.current) return; const moved = moveState.current.moved; moveState.current = null; onDragEnd(node.id, moved); try { e.currentTarget.releasePointerCapture(e.pointerId); } catch(_){} }; // (Manual edge-drawing is gone: edges are auto-derived from each node's // arrival/departure times. The connect-handle on the node has been // removed accordingly.) const shape = node.shape || 'square'; // node.highlight is a hex color used for an extra accent ring around the // avatar. Independent of node.color (background). Used to mark "外せない // 商談相手" / "今回の目玉スポット" — visual is also reflected in Schedule. const hi = node.highlight || null; const avatarStyle = { background: node.color || '#FFFFFF', ...(hi ? { borderColor: hi, borderWidth: 3, boxShadow: `0 0 0 3px ${hexAlpha(hi, 0.25)}, 0 2px 4px rgba(15,23,42,.06), 0 4px 12px rgba(15,23,42,.08)`, } : {}), }; let inner; if (node.image){ inner = ; } else if (node.icon && window.TourIcon){ inner = ; } else if (node.emoji){ inner =
{node.emoji}
; } else { const ini = (node.name||'?').trim().charAt(0).toUpperCase(); inner =
{ini}
; } return (
{inner}
{/* Highlight star — rendered as a SIBLING of .avatar (not a child) so it sits on a layer above the avatar and isn't clipped by avatar's overflow:hidden (which would otherwise crop the star's outer edge on circle-shaped avatars). */} {hi && (
)} {(node.name || node.subtitle) ? (
{node.name ?
{node.name}
: null} {node.subtitle ?
{node.subtitle}
: null}
) : null} {/* Delete badge — only the × button remains (manual + connector was removed since edges are now auto-managed from times). */}
{e.stopPropagation(); onDelete(node.id);}} title="削除" > {window.TourIcon ? : '×'}
); } // Convert a hex color #RRGGBB to rgba(...) with given alpha. function hexAlpha(hex, alpha){ const m = /^#?([0-9a-f]{6})$/i.exec(hex || ''); if (!m) return hex; const r = parseInt(m[1].slice(0,2),16); const g = parseInt(m[1].slice(2,4),16); const b = parseInt(m[1].slice(4,6),16); return `rgba(${r},${g},${b},${alpha})`; } function contrastInk(bg){ if (!bg) return '#1F2937'; const m = bg.match(/^#([0-9a-f]{6})$/i); if (!m) return '#1F2937'; const r = parseInt(m[1].slice(0,2),16); const g = parseInt(m[1].slice(2,4),16); const b = parseInt(m[1].slice(4,6),16); const L = (0.299*r+0.587*g+0.114*b)/255; return L>0.6 ? '#1F2937' : '#fff'; } window.Node = Node; window.contrastInk = contrastInk;