/* global React, ReactDOM */ const { clampZoom, shouldShowBridge, shouldShowPractices } = window.AlignmentMapLogic; // ── Geometry ────────────────────────────────────────────────────────────── const R_ANCHOR = 380, R_VALUE_2 = 270, R_VALUE_1 = 170, R_INNER = 70; const ANCHOR_BADGE_R = 26, PRACTICE_ORBIT = 46; const TAU = Math.PI * 2; const toRad = (d) => (d * Math.PI) / 180; function polar(deg, r) { const a = toRad(deg); return { x: Math.cos(a) * r, y: Math.sin(a) * r }; } const DOMAIN_CFG = { inner: { angle: 270, rgb: '130,98,186' }, physical: { angle: 330, rgb: '93,156,118' }, creative: { angle: 30, rgb: '149,163,56' }, interpersonal: { angle: 90, rgb: '212,114,131' }, adventure: { angle: 150, rgb: '71,149,173' }, admin: { angle: 210, rgb: '143,138,136' }, }; function domainColor(id, soft = false) { const map = { inner: soft ? 'var(--dom-inner-soft)' : 'var(--dom-inner)', physical: soft ? 'var(--dom-physical-soft)' : 'var(--dom-physical)', creative: soft ? 'var(--dom-creative-soft)' : 'var(--dom-creative)', interpersonal: soft ? 'var(--dom-interpersonal-soft)' : 'var(--dom-interpersonal)', adventure: soft ? 'var(--dom-adventure-soft)' : 'var(--dom-adventure)', admin: soft ? 'var(--dom-admin-soft)' : 'var(--dom-admin)', }; return map[id] || 'var(--ink-3)'; } const RING_SLOTS = [ { ring: 2, offset: -14 }, { ring: 1, offset: 16 }, { ring: 2, offset: 14 }, { ring: 1, offset: -16 }, ]; function transformAPIData(apiDomains) { const domains = [], values = [], practices = []; apiDomains.forEach(apid => { const cfg = DOMAIN_CFG[apid.id]; if (!cfg) return; domains.push({ id: apid.id, name: apid.label, angle: cfg.angle }); (apid.values || []).forEach((apiv, idx) => { const slot = RING_SLOTS[idx % RING_SLOTS.length]; values.push({ id: apiv.id, name: apiv.name, domain: apid.id, ring: slot.ring, offset: slot.offset, velocity: apiv.velocity || 0, blurb: apiv.description || '', bridge: apiv.secondary_domain && apiv.secondary_domain !== apid.id ? apiv.secondary_domain : null, }); (apiv.practices || []).forEach(apip => { practices.push({ id: apip.id, name: apip.label, value: apiv.id, velocity: apip.velocity || 0 }); }); }); }); return { domains, values, practices }; } // ── SVG helpers ─────────────────────────────────────────────────────────── function starPath(r, n = 0.22) { const p = n * r; return `M 0 ${-r} L ${p} ${-p} L ${r} 0 L ${p} ${p} L 0 ${r} L ${-p} ${p} L ${-r} 0 L ${-p} ${-p} Z`; } function tinyStar(r) { const p = r * 0.30; return `M 0 ${-r} L ${p} ${-p} L ${r} 0 L ${p} ${p} L 0 ${r} L ${-p} ${p} L ${-r} 0 L ${-p} ${-p} Z`; } function velocityState(vel) { if (vel > 0.7) return 'Resonant'; if (vel > 0.4) return 'Active'; if (vel > 0.2) return 'Quiet'; return 'Uncharted'; } // star radius scales with velocity + number of connected practices (capped at 6) function starRadius(velocity, practiceCount) { return 13 + velocity * 6 + Math.min(practiceCount, 6) * 2; } // stable 0..4s pulse phase per id so glows twinkle independently, never strobe in unison function phaseFor(id) { let h = 0; const str = String(id); for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0; return (h % 400) / 100; } const DUST = (() => { const out = []; let s = 9123; const rand = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; }; for (let i = 0; i < 60; i++) { const r = R_INNER + 8 + rand() * (R_ANCHOR - R_INNER - 24), a = rand() * TAU; out.push({ x: Math.cos(a) * r, y: Math.sin(a) * r, r: rand() < 0.88 ? 0.6 : 1.2, o: 0.14 + rand() * 0.22 }); } return out; })(); // ── Graph components ────────────────────────────────────────────────────── function SectorCone({ d, opacity = 1 }) { const a1 = toRad(d.angle - 30), a2 = toRad(d.angle + 30); const rOut = R_ANCHOR + 10, rIn = R_INNER * 0.4; const p1 = { x: Math.cos(a1)*rOut, y: Math.sin(a1)*rOut }; const p2 = { x: Math.cos(a2)*rOut, y: Math.sin(a2)*rOut }; const p3 = { x: Math.cos(a2)*rIn, y: Math.sin(a2)*rIn }; const p4 = { x: Math.cos(a1)*rIn, y: Math.sin(a1)*rIn }; const pt = polar(d.angle, R_ANCHOR); const id = `cone-${d.id}`; const rgb = (DOMAIN_CFG[d.id] || {}).rgb || '150,150,150'; return ( ); } function PolarGrid({ showDust }) { return ( {Array.from({ length: 6 }).map((_, i) => { const a = toRad(-60 + i * 60); return ; })} {[R_INNER, R_VALUE_1, R_VALUE_2, R_ANCHOR].map((r, i) => ( ))} {showDust && DUST.map((d, i) => ( ))} ); } function DomainAnchor({ d, selected, onSelect }) { const { x, y } = polar(d.angle, R_ANCHOR); const out = polar(d.angle, R_ANCHOR + 52); const G = ANCHOR_BADGE_R * 0.55; let glyph; switch (d.id) { case 'inner': glyph = (); break; case 'physical': glyph = (); break; case 'creative': glyph = (); break; case 'interpersonal': glyph = (); break; case 'adventure': glyph = (); break; case 'admin': glyph = (); break; default: glyph = null; } const a = ((d.angle % 360) + 360) % 360; const anchor = (a > 90 && a < 270) ? 'end' : 'start'; return ( { e.stopPropagation(); onSelect(d); }}> {selected && ( )} {glyph} {d.name} ); } function StructuralLine({ v, domain, accent }) { const a = polar(domain.angle, R_ANCHOR - ANCHOR_BADGE_R); const vp = polar(domain.angle + v.offset, v.ring === 1 ? R_VALUE_1 : R_VALUE_2); // control point: pulled inward along domain radial axis for the swoop const ax = polar(domain.angle, (R_ANCHOR - ANCHOR_BADGE_R) * 0.72); const mid = { x: (a.x+vp.x)/2, y: (a.y+vp.y)/2 }; const ctrlX = (mid.x + ax.x) / 2, ctrlY = (mid.y + ax.y) / 2; // perpendicular at the domain-anchor end for taper width const dx = vp.x - a.x, dy = vp.y - a.y; const len = Math.sqrt(dx*dx + dy*dy); const px = -dy/len, py = dx/len; const hw = accent ? 14 : 9; const al = { x: a.x + px*hw, y: a.y + py*hw }; const ar = { x: a.x - px*hw, y: a.y - py*hw }; // both edges share the same control point → tapered teardrop swoop const pathD = `M ${al.x} ${al.y} Q ${ctrlX} ${ctrlY} ${vp.x} ${vp.y} Q ${ctrlX} ${ctrlY} ${ar.x} ${ar.y} Z`; const gid = `sl-grad-${v.id}`; const rgb = (DOMAIN_CFG[domain.id] || {}).rgb || '150,150,150'; return ( ); } function ValueNode({ v, domain, selected, hovered, auraIntensity, practiceCount, scale, onSelect, onHover }) { const ba = domain.angle + v.offset, r = v.ring === 1 ? R_VALUE_1 : R_VALUE_2; const { x, y } = polar(ba, r); const sr = starRadius(v.velocity, practiceCount); // gentle size growth as zoom increases — cube-root curve, not linear const s = scale || 1; const displaySr = sr * (s > 1 ? 1 + 0.18 * Math.cbrt(s - 1) : 1); const auraR = 26 + v.velocity * 26 + Math.min(practiceCount, 6) * 2; const rgb = (DOMAIN_CFG[v.domain] || {}).rgb || '150,150,150'; const iStop = (0.45 + v.velocity * 0.45) * auraIntensity; const mStop = (0.18 + v.velocity * 0.30) * auraIntensity; const gid = `vglow-${v.id}`; const sgid = `vstar-${v.id}`; // gradient opacity grows with zoom but eases off — log2 curve, capped const gradOpacity = Math.min(0.62, 0.08 + 0.22 * Math.log2(Math.max(1, s))); return ( { e.stopPropagation(); onSelect(v); }} onMouseEnter={() => onHover(v.id)} onMouseLeave={() => onHover(null)}> {auraIntensity > 0 && ( 0.6 ? 0.80 : 0.60, '--aura-max': 1.0, animationDelay: `${phaseFor(v.id)}s` }} /> )} {selected && ( )} ); } // angle normalised to (-180, 180] relative to a base function relAngle(a, base) { return ((a - base + 180) % 360 + 360) % 360 - 180; } function practicePos(v, domain, idx, total) { const r = v.ring === 1 ? R_VALUE_1 : R_VALUE_2; const vc = polar(domain.angle + v.offset, r); const sr = starRadius(v.velocity, total); const orbitR = v.ring === 1 ? r - sr - 30 : r + sr + 50; // domain slice is ±28° from domain.angle (leaving 2° buffer at each boundary) const SLICE = 28; // fan width scales with practice count but never exceeds the slice const span = Math.min(SLICE * 1.6, 10 + total * 11); // start fan centered on v.offset within the domain let centerOffset = v.offset; // if this value bridges to a secondary domain, pull the fan toward that boundary if (v.bridge && DOMAIN_CFG[v.bridge]) { const bridgeAngle = DOMAIN_CFG[v.bridge].angle; const diff = relAngle(bridgeAngle, domain.angle); // + = bridge is CW from domain const nearBoundary = diff > 0 ? SLICE : -SLICE; // shift center 35% of the way toward the near boundary centerOffset += (nearBoundary - centerOffset) * 0.35; } // clamp fan so it stays inside the slice const halfSpan = span / 2; const lo = Math.max(-SLICE, centerOffset - halfSpan); const hi = Math.min( SLICE, centerOffset + halfSpan); const actualSpan = hi - lo; const angleOffset = total === 1 ? (lo + hi) / 2 : lo + (actualSpan / (total - 1)) * idx; const angle = domain.angle + angleOffset; const pos = polar(angle, orbitR); return { x: pos.x, y: pos.y, vx: vc.x, vy: vc.y, angle }; } function computeAdjustedPracticePositions(values, domains, practicesByValue) { const SLICE = 28; // per-value safe zone (domain-relative offsets) — practices can't cross // into a neighbouring value's structural stream within the same domain const valueSafeZones = {}; domains.forEach(d => { const dVals = values.filter(v => v.domain === d.id) .sort((a, b) => a.offset - b.offset); dVals.forEach((v, i) => { const prev = dVals[i - 1]; const next = dVals[i + 1]; const lo = prev ? (v.offset + prev.offset) / 2 : -SLICE; const hi = next ? (v.offset + next.offset) / 2 : SLICE; valueSafeZones[v.id] = { lo: Math.max(-SLICE, lo - 1), hi: Math.min(SLICE, hi + 1) }; }); }); // label bounding boxes — what practice dots must avoid const labelBoxes = values.map(v => { const d = domains.find(dd => dd.id === v.domain); if (!d) return null; const ba = d.angle + v.offset; const r = v.ring === 1 ? R_VALUE_1 : R_VALUE_2; const pracs = practicesByValue[v.id] || []; const sr = starRadius(v.velocity, pracs.length); const lp = polar(ba, r + sr + 16); const lines = splitLabel(v.name); const maxLen = lines[1] ? Math.max(lines[0].length, lines[1].length) : lines[0].length; return { x: lp.x, y: lp.y, valueId: v.id, hw: maxLen * 4.5 + 12, hh: lines[1] ? 22 : 12 }; }).filter(Boolean); // initial dot positions + orbit metadata const slots = []; values.forEach(v => { const d = domains.find(dd => dd.id === v.domain); if (!d) return; const pracs = practicesByValue[v.id] || []; const r = v.ring === 1 ? R_VALUE_1 : R_VALUE_2; const sr = starRadius(v.velocity, pracs.length); const orbitR = v.ring === 1 ? r - sr - 30 : r + sr + 50; const vc = polar(d.angle + v.offset, r); pracs.forEach((p, i) => { const raw = practicePos(v, d, i, pracs.length); const angleDeg = Math.atan2(raw.y, raw.x) * 180 / Math.PI; slots.push({ pid: p.id, x: raw.x, y: raw.y, vx: vc.x, vy: vc.y, orbitR, angleDeg, domAngle: d.angle, valueId: v.id }); }); }); // repulsion — push dots away from value labels AND away from each other const MIN_LABEL_DIST = 50; const MIN_DOT_DIST = 24; // 2 * dot radius + comfortable gap for (let iter = 0; iter < 60; iter++) { let moved = false; // label repulsion slots.forEach(s => { labelBoxes.forEach(lb => { const dx = s.x - lb.x, dy = s.y - lb.y; const bx = Math.max(0, Math.abs(dx) - lb.hw); const by = Math.max(0, Math.abs(dy) - lb.hh); const dist = Math.sqrt(bx * bx + by * by); if (dist < MIN_LABEL_DIST) { moved = true; const labelDeg = Math.atan2(lb.y, lb.x) * 180 / Math.PI; const nd = ((s.angleDeg - labelDeg + 180) % 360 + 360) % 360 - 180; s.angleDeg += nd >= 0 ? 3 : -3; const np = polar(s.angleDeg, s.orbitR); s.x = np.x; s.y = np.y; } }); }); // dot-to-dot repulsion for (let i = 0; i < slots.length; i++) { for (let j = i + 1; j < slots.length; j++) { const a = slots[i], b = slots[j]; const dx = a.x - b.x, dy = a.y - b.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < MIN_DOT_DIST && dist > 0) { moved = true; // push each dot along its orbit arc away from the other const toward_b = Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI; const na = relAngle(toward_b, a.angleDeg); a.angleDeg += na >= 0 ? -2 : 2; const ap = polar(a.angleDeg, a.orbitR); a.x = ap.x; a.y = ap.y; const toward_a = Math.atan2(a.y - b.y, a.x - b.x) * 180 / Math.PI; const nb = relAngle(toward_a, b.angleDeg); b.angleDeg += nb >= 0 ? -2 : 2; const bp = polar(b.angleDeg, b.orbitR); b.x = bp.x; b.y = bp.y; } } } if (!moved) break; } // clamp each dot to its value's safe zone (respects domain boundary + no stream-crossing) slots.forEach(s => { const zone = valueSafeZones[s.valueId] || { lo: -SLICE, hi: SLICE }; const offset = relAngle(s.angleDeg, s.domAngle); const clamped = Math.max(zone.lo, Math.min(zone.hi, offset)); if (Math.abs(offset - clamped) > 0.01) { s.angleDeg = s.domAngle + clamped; const np = polar(s.angleDeg, s.orbitR); s.x = np.x; s.y = np.y; } }); const map = {}; slots.forEach(s => { map[s.pid] = { x: s.x, y: s.y, vx: s.vx, vy: s.vy, angle: s.angleDeg }; }); return map; } function PracticeNode({ p, v, domain, idx, total, pos, visible, selected, onSelect }) { const { x, y, vx, vy } = pos || practicePos(v, domain, idx, total); const rgb = (DOMAIN_CFG[v.domain] || {}).rgb || '150,150,150'; const handleClick = onSelect ? (e) => { e.stopPropagation(); onSelect(p); } : undefined; return ( {selected && ( )} 0.6 ? `rgb(${rgb})` : 'var(--ink-3)'} /> ); } function Bridge({ v, fromDomain, toDomain, visible }) { const from = polar(fromDomain.angle + v.offset, v.ring === 1 ? R_VALUE_1 : R_VALUE_2); const to = polar(toDomain.angle, R_ANCHOR - ANCHOR_BADGE_R); const cx = (from.x+to.x)*.3, cy = (from.y+to.y)*.3; return ; } // ── Label helpers ──────────────────────────────────────────────────────────── function splitLabel(text, maxChars = 13) { const words = text.split(' '); if (text.length <= maxChars || words.length < 2) return [text, null]; let best = 1, bestDiff = Infinity; for (let i = 1; i < words.length; i++) { const diff = Math.abs(words.slice(0,i).join(' ').length - words.slice(i).join(' ').length); if (diff < bestDiff) { bestDiff = diff; best = i; } } return [words.slice(0, best).join(' '), words.slice(best).join(' ')]; } // ── Label collision avoidance ───────────────────────────────────────────── function computeRawLabels(values, domains, practicesByValue, practicePositions) { const labels = []; values.forEach(v => { const d = domains.find(dd => dd.id === v.domain); if (!d) return; const ba = d.angle + v.offset; const r = v.ring === 1 ? R_VALUE_1 : R_VALUE_2; const pracs = practicesByValue[v.id] || []; const sr = starRadius(v.velocity, pracs.length); const lp = polar(ba, r + sr + 16); const a = ((ba % 360) + 360) % 360; const lines = splitLabel(v.name); labels.push({ id: `v-${v.id}`, x: lp.x, y: lp.y + (lines[1] ? -8 : 4), text: v.name, lines, anchor: 'middle', cls: 'value__label', type: 'value', charW: 8.5, valueId: v.id, }); pracs.forEach((p, i) => { const { x, y, angle } = (practicePositions && practicePositions[p.id]) || practicePos(v, d, i, pracs.length); const pa = ((angle % 360) + 360) % 360; const anchor = (pa > 90 && pa < 270) ? 'end' : 'start'; labels.push({ id: `p-${p.id}`, x: anchor === 'end' ? x - 14 : x + 14, y: y + 4, text: p.name, anchor, cls: 'practice__label', type: 'practice', charW: 6.2, valueId: v.id, }); }); }); return labels; } function resolveCollisions(labels, iters = 28) { if (!labels.length) return labels; const LINE_H = 18, PAD = 3; const b = labels.map(l => { const maxLen = l.lines ? Math.max(l.lines[0].length, l.lines[1] ? l.lines[1].length : 0) : l.text.length; const w = maxLen * l.charW + PAD * 2; const h = (l.lines && l.lines[1]) ? LINE_H * 2 + 6 : LINE_H; const lx = l.anchor === 'end' ? l.x - w + PAD : l.anchor === 'middle' ? l.x - w / 2 : l.x - PAD; // store original position so we can clamp drift return { ...l, w, h, lx, ty: l.y - LINE_H + 2, ox: l.x, oy: l.y }; }); for (let it = 0; it < iters; it++) { let moved = false; for (let i = 0; i < b.length; i++) { for (let j = i + 1; j < b.length; j++) { const a = b[i], c = b[j]; const ox = Math.min(a.lx + a.w, c.lx + c.w) - Math.max(a.lx, c.lx); const oy = Math.min(a.ty + a.h, c.ty + c.h) - Math.max(a.ty, c.ty); if (ox > 0 && oy > 0) { moved = true; if (oy <= ox) { const push = oy / 2 + 1; if (a.y <= c.y) { b[i].y -= push; b[i].ty -= push; b[j].y += push; b[j].ty += push; } else { b[i].y += push; b[i].ty += push; b[j].y -= push; b[j].ty -= push; } } else { const push = ox / 2 + 1; if (a.lx <= c.lx) { b[i].x -= push; b[i].lx -= push; b[j].x += push; b[j].lx += push; } else { b[i].x += push; b[i].lx += push; b[j].x -= push; b[j].lx -= push; } } } } } if (!moved) break; } // clamp each label to max drift from its home — keeps labels anchored near their star/dot b.forEach(l => { const maxDrift = l.type === 'value' ? 42 : 22; const dx = l.x - l.ox, dy = l.y - l.oy; const dist = Math.sqrt(dx * dx + dy * dy); if (dist > maxDrift) { const s = maxDrift / dist; const nx = l.ox + dx * s, ny = l.oy + dy * s; const ddx = nx - l.x, ddy = ny - l.y; l.x = nx; l.y = ny; l.lx += ddx; l.ty += ddy; } }); return b; } function LabelLayer({ values, domains, practicesByValue, practicePositions, zoom, scale, selectedValueId, hoveredValueId, showValues, showPractices }) { const labels = React.useMemo(() => { const raw = computeRawLabels(values, domains, practicesByValue, practicePositions); return resolveCollisions(raw); }, [values, domains, practicesByValue, practicePositions]); return ( {labels.map(l => { const active = l.valueId === selectedValueId || l.valueId === hoveredValueId; const zoomThreshold = l.type === 'value' ? 1.1 : 1.6; const isPractice = l.type === 'practice'; const visible = isPractice ? showPractices : showValues; const opacity = visible && (active || zoom >= zoomThreshold) ? 1 : 0; const isVal = l.type === 'value'; const fs = (isVal ? 15 : 11) / scale; const sw = (isVal ? 2.5 : 1.5) / scale; const dy = 20 / scale; if (isVal && l.lines && l.lines[1]) { return ( {l.lines[0]} {l.lines[1]} ); } return ( {l.text} ); })} ); } // ── AlignmentGraph ──────────────────────────────────────────────────────── function AlignmentGraph({ domains, values, practices, selectedValueId, selectedDomainId, selectedPracticeId, hoveredValueId, zoom, pan, onZoomChange, onPanChange, showValues, showPractices, onSelectValue, onSelectDomain, onHoverValue, onClearSelection, onSelectPractice, }) { const svgRef = React.useRef(null); const drag = React.useRef(null); const [transitioning, setTransitioning] = React.useState(false); const [size, setSize] = React.useState({ w: 1000, h: 800 }); const practicesByValue = React.useMemo(() => { const map = {}; practices.forEach(p => { if (!map[p.value]) map[p.value] = []; map[p.value].push(p); }); return map; }, [practices]); const practicePositions = React.useMemo(() => computeAdjustedPracticePositions(values, domains, practicesByValue), [values, domains, practicesByValue] ); React.useEffect(() => { const update = () => { if (!svgRef.current) return; const r = svgRef.current.getBoundingClientRect(); setSize({ w: r.width, h: r.height }); }; update(); window.addEventListener('resize', update); return () => window.removeEventListener('resize', update); }, []); React.useEffect(() => { if (!selectedValueId && !selectedDomainId && !selectedPracticeId) return; const autoFit = Math.max(0.5, Math.min(1, (Math.min(size.h/2, size.w/2) - 96) / R_ANCHOR)); const scale = autoFit * zoom; let tx = null, ty = null; if (selectedValueId) { const v = values.find(vv => vv.id === selectedValueId); const d = v && domains.find(dd => dd.id === v.domain); if (v && d) { const p = polar(d.angle + v.offset, v.ring === 1 ? R_VALUE_1 : R_VALUE_2); tx = p.x; ty = p.y; } } else if (selectedDomainId) { const d = domains.find(dd => dd.id === selectedDomainId); if (d) { const p = polar(d.angle, R_ANCHOR * 0.65); tx = p.x; ty = p.y; } } else if (selectedPracticeId) { const p = practices.find(pp => pp.id === selectedPracticeId); const v = p && values.find(vv => vv.id === p.value); const d = v && domains.find(dd => dd.id === v.domain); if (p && v && d) { const pracs = practicesByValue[v.id] || []; const idx = pracs.findIndex(pp => pp.id === p.id); const pos = practicePos(v, d, idx < 0 ? 0 : idx, pracs.length); tx = pos.x; ty = pos.y; } } if (tx !== null) { setTransitioning(true); onPanChange({ x: -tx * scale, y: -ty * scale }); const t = setTimeout(() => setTransitioning(false), 520); return () => clearTimeout(t); } }, [selectedValueId, selectedDomainId, selectedPracticeId]); const onMouseDown = (e) => { if (e.button !== 0) return; drag.current = { x: e.clientX, y: e.clientY, panX: pan.x, panY: pan.y, moved: 0 }; e.currentTarget.classList.add('dragging'); }; const onMouseMove = (e) => { if (!drag.current) return; const dx = e.clientX - drag.current.x, dy = e.clientY - drag.current.y; drag.current.moved = Math.hypot(dx, dy); if (drag.current.moved > 3) setTransitioning(false); onPanChange({ x: drag.current.panX + dx, y: drag.current.panY + dy }); }; const onMouseUp = (e) => { if (!drag.current) return; const moved = drag.current.moved; drag.current = null; e.currentTarget.classList.remove('dragging'); if (moved < 4 && e.target.tagName === 'svg') onClearSelection(); }; const onWheel = (e) => { e.preventDefault(); setTransitioning(false); onZoomChange(clampZoom(zoom * (1 + -e.deltaY * 0.0015))); }; const autoFit = Math.max(0.5, Math.min(1, (Math.min(size.h/2, size.w/2) - 96) / R_ANCHOR)); const scale = autoFit * zoom; const cx = size.w/2 + pan.x, cy = size.h/2 + pan.y; return ( { drag.current = null; }} onWheel={onWheel}> {domains.map(d => )} {showValues && values.map(v => { const d = domains.find(dd => dd.id === v.domain); if (!d) return null; const accent = selectedValueId === v.id || hoveredValueId === v.id || selectedDomainId === v.domain; return ; })} {showValues && values.filter(v => v.bridge).map(v => { const from = domains.find(d => d.id === v.domain); const to = domains.find(d => d.id === v.bridge); if (!from || !to) return null; return ; })} {/* Domain anchors rendered before value/practice nodes so value labels appear on top */} {domains.map(d => ( ))} {showPractices && values.map(v => { const d = domains.find(dd => dd.id === v.domain); if (!d) return null; return (practicesByValue[v.id] || []).map((p, i, arr) => ( )); })} {showValues && values.map(v => { const d = domains.find(dd => dd.id === v.domain); if (!d) return null; return ; })} ); } // ── Navigation sidebar ──────────────────────────────────────────────────── function NavSidebar() { const items = [ { icon: 'dashboard', label: 'Dashboard', href: '/dashboard-ui/' }, { icon: 'star-maps', label: 'Alignment Map', href: '/dashboard-ui/map', active: true }, { icon: 'manuscripts',label: 'Weekly Review', href: '#' }, { icon: 'book', label: 'Monthly Review', href: '#' }, { icon: 'star-maps', label: 'Quarterly Review',href: '#' }, ]; return ( ); } // ── Right panel ─────────────────────────────────────────────────────────── function RightPanel({ selectedValue, selectedDomain, selectedPractice, domains, values, practices, unmappedItems, editMode, onToggleEdit, onClose, pickedUnmappedId, onPickUnmapped, onAssign, totalValues, resonantValues, onSelectValue, onSelectDomain, onSelectPractice, }) { const pickedItem = unmappedItems.find(i => i.id === pickedUnmappedId); let head; if (selectedPractice) { const parentValue = values.find(v => v.id === selectedPractice.value); const parentDomain = parentValue ? domains.find(d => d.id === parentValue.domain) : null; head = (
{parentDomain && } {parentValue ? parentValue.name : 'Practice'} {parentDomain && ·{parentDomain.name}}

{selectedPractice.name}

); } else if (selectedValue) { const dom = domains.find(d => d.id === selectedValue.domain); head = (
{dom && } {dom ? dom.name : 'Value'}

{selectedValue.name}

); } else if (selectedDomain) { head = (
Domain

{selectedDomain.name}

); } else { head = (
Identity Architecture

Overview

); } return ( ); } // ── Panel body components ───────────────────────────────────────────────── function OverviewBody({ totalValues, resonantValues, practices, unmappedItems, editMode, pickedUnmappedId, onPickUnmapped }) { return (
This Cycle
Active Values{totalValues}
Resonant · 7d{resonantValues} of {totalValues}
Practices in Orbit{practices.length}
Unmapped · {unmappedItems.length}
{unmappedItems.length === 0 ? (

All practices are anchored to values.

) : ( unmappedItems.map(item => (
editMode && onPickUnmapped(pickedUnmappedId === item.id ? null : item.id)}>
{item.name || item.label}
{editMode ? 'Click · then select a value on the map' : 'Enable edit mode to assign'}
)) )}

Drag to pan · scroll to zoom. Click a star or domain to inspect it.

); } function ValueBody({ v, domains, practices, editMode, pickedItem, onAssign, onSelectPractice, onSelectDomain }) { const bridge = v.bridge ? domains.find(d => d.id === v.bridge) : null; const childPractices = practices.filter(p => p.value === v.id); return ( {v.blurb &&

{v.blurb}

} {bridge && (
Secondary Domain
)}
Practices in Orbit · {childPractices.length}
{childPractices.length === 0 ? (

No practices anchored to this value yet.

) : (
{childPractices.map(p => ( ))}
)}
{editMode && (
Assign Unmapped Practice
{pickedItem ? (

Anchor {pickedItem.name || pickedItem.label} to {v.name}?

{pickedItem.name || pickedItem.label}
) : (

Select a practice from Unmapped (in Overview) to anchor it here.

)}
)}
); } function DomainBody({ d, values, practices, onSelectValue, onSelectPractice }) { const domainValues = values.filter(v => v.domain === d.id); const domainPractices = practices.filter(p => domainValues.some(v => v.id === p.value)); const DOMAIN_NOTES = { inner: 'The world you build inside — beliefs, emotional landscape, spiritual orientation. Values here shape how you experience everything else.', physical: 'How you inhabit a body and a physical world — movement, health, environment, craft. The most tangible layer of identity.', creative: 'Expression, making, and the drive to bring new things into existence. Where inner vision meets outer form.', interpersonal: 'The relational field — how you connect, love, collaborate, and hold space. Identity shaped through others.', adventure: 'Growth through friction and the unknown — challenge, exploration, expanding the edge of comfort.', admin: 'The infrastructure of a life — resources, systems, obligations. The container that makes everything else possible.', }; return (

{DOMAIN_NOTES[d.id] || 'A sector of the identity architecture.'}

{domainValues.length > 0 && (
Values · {domainValues.length}
{domainValues.map(v => ( ))}
)} {domainPractices.length > 0 && (
Practices · {domainPractices.length}
{domainPractices.map(p => { const parentV = values.find(v => v.id === p.value); return ( ); })}
)} {domainValues.length === 0 && (

No values anchored to this domain yet.

)}
); } function PracticeBody({ p, values, domains, onSelectValue, onSelectDomain }) { const parentValue = values.find(v => v.id === p.value); const parentDomain = parentValue ? domains.find(d => d.id === parentValue.domain) : null; const vel = p.velocity || 0; const state = velocityState(vel); return (
Activity · 7 days
Status 0.7 ? 'v--gold' : ''}`}>{state}
Presence {Math.round(vel * 100)}/100
{parentValue && (
Anchored to Value
)} {parentDomain && (
Domain
)}
); } // ── Icons ───────────────────────────────────────────────────────────────── const ZoomInIcon = () => ; const ZoomOutIcon = () => ; const ResetIcon = () => ; // ── Root ────────────────────────────────────────────────────────────────── function AlignmentMap() { const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [domains, setDomains] = React.useState([]); const [values, setValues] = React.useState([]); const [practices, setPractices] = React.useState([]); const [unmappedItems, setUnmappedItems] = React.useState([]); const [selectedValueId, setSelectedValueId] = React.useState(null); const [selectedDomainId, setSelectedDomainId] = React.useState(null); const [selectedPracticeId, setSelectedPracticeId] = React.useState(null); const [hoveredValueId, setHoveredValueId] = React.useState(null); const [zoom, setZoom] = React.useState(1); const [pan, setPan] = React.useState({ x: 0, y: 0 }); const [editMode, setEditMode] = React.useState(false); const [pickedUnmappedId, setPickedUnmappedId] = React.useState(null); const [showValues, setShowValues] = React.useState(true); const [showPractices, setShowPractices] = React.useState(true); const fetchData = React.useCallback(() => { Promise.all([ fetch('/api/navigation/map').then(r => r.json()), fetch('/api/navigation/landing-bay').then(r => r.json()), ]).then(([mapData, bayData]) => { const { domains: d, values: v, practices: p } = transformAPIData(mapData); setDomains(d); setValues(v); setPractices(p); setUnmappedItems(bayData); setLoading(false); }).catch(err => { setError(err.message); setLoading(false); }); }, []); React.useEffect(() => { fetchData(); }, [fetchData]); const selectedValue = React.useMemo(() => values.find(v => v.id === selectedValueId), [values, selectedValueId]); const selectedDomain = React.useMemo(() => domains.find(d => d.id === selectedDomainId), [domains, selectedDomainId]); const selectedPractice = React.useMemo(() => practices.find(p => p.id === selectedPracticeId), [practices, selectedPracticeId]); const onSelectValue = (v) => { setSelectedValueId(v.id); setSelectedDomainId(null); setSelectedPracticeId(null); }; const onSelectDomain = (d) => { setSelectedDomainId(d.id); setSelectedValueId(null); setSelectedPracticeId(null); }; const onSelectPractice = (p) => { setSelectedPracticeId(p.id); setSelectedValueId(null); setSelectedDomainId(null); }; const onClearSelection = () => { setSelectedValueId(null); setSelectedDomainId(null); setSelectedPracticeId(null); }; const onAssign = (practice, targetValue) => { fetch('/api/navigation/assign', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ practice_id: practice.id, value_id: targetValue.id }), }).then(() => { fetchData(); setPickedUnmappedId(null); }); }; const onZoomIn = () => setZoom(z => clampZoom(+(z * 1.2).toFixed(2))); const onZoomOut = () => setZoom(z => clampZoom(+(z / 1.2).toFixed(2))); const onResetView = () => { setZoom(1); setPan({ x: 0, y: 0 }); }; const totalValues = values.length; const resonantValues = values.filter(v => (v.velocity || 0) >= 0.7).length; if (loading) return
Plotting structural coordinates…
; if (error) return
Navigation offline · {error}
; return (
Life Domains
{domains.map(d => ( ))}
{(zoom * 100).toFixed(0)}%
{ setEditMode(e => !e); setPickedUnmappedId(null); }} onClose={onClearSelection} pickedUnmappedId={pickedUnmappedId} onPickUnmapped={setPickedUnmappedId} onAssign={onAssign} totalValues={totalValues} resonantValues={resonantValues} onSelectValue={onSelectValue} onSelectDomain={onSelectDomain} onSelectPractice={onSelectPractice} />
); } window.AlignmentMap = AlignmentMap;