/* 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 (
{head}
{selectedPractice && (
)}
{!selectedPractice && selectedValue && (
)}
{!selectedPractice && !selectedValue && selectedDomain && (
)}
{!selectedPractice && !selectedValue && !selectedDomain && (
)}
Edit Mode · {editMode ? 'On' : 'Off'}
{editMode ? 'Structural assignments unlocked.' : 'Calm sanctuary. Read-only.'}
);
}
// ── 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
onSelectDomain(bridge)}>
Bridges to {bridge.name}
›
)}
Practices in Orbit · {childPractices.length}
{childPractices.length === 0 ? (
No practices anchored to this value yet.
) : (
{childPractices.map(p => (
onSelectPractice(p)}>
0.6 ? 'marker--filled' : ''}`}>
{p.name}
{velocityState(p.velocity||0)}
›
))}
)}
{editMode && (
Assign Unmapped Practice
{pickedItem ? (
Anchor {pickedItem.name || pickedItem.label} to {v.name} ?
{pickedItem.name || pickedItem.label}
onAssign(pickedItem, v)}>
Anchor to {v.name}
) : (
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 => (
onSelectValue(v)}>
{v.name}
{velocityState(v.velocity||0)}
›
))}
)}
{domainPractices.length > 0 && (
Practices · {domainPractices.length}
{domainPractices.map(p => {
const parentV = values.find(v => v.id === p.value);
return (
onSelectPractice(p)}>
0.6 ? 'marker--filled' : ''}`}>
{p.name}
{parentV && {parentV.name} }
{velocityState(p.velocity||0)}
›
);
})}
)}
{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
onSelectValue(parentValue)}>
{parentValue.name}
›
)}
{parentDomain && (
Domain
onSelectDomain(parentDomain)}>
{parentDomain.name}
›
)}
);
}
// ── 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 => (
selectedDomainId === d.id ? onClearSelection() : onSelectDomain(d)}>
{d.name}
))}
setShowValues(v => !v)}>Values
setShowPractices(v => !v)}>Practices
{(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;