@almadar/ui 5.149.0 → 5.151.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -135,7 +135,7 @@ function useEventBus() {
135
135
  return {
136
136
  ...baseBus,
137
137
  emit: (type, payload, source) => {
138
- if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".")) {
138
+ if (typeof type === "string" && type.startsWith("UI:") && !type.slice(3).includes(".") && !source?.trait) {
139
139
  scopeLog.warn("emit:bare-key-no-scope", { type });
140
140
  }
141
141
  baseBus.emit(type, payload, source);
@@ -539,26 +539,6 @@ var init_useDragReorder = __esm({
539
539
  "use client";
540
540
  }
541
541
  });
542
- function useMediaQuery(query) {
543
- const subscribe = useCallback(
544
- (onChange) => {
545
- const mql = window.matchMedia(query);
546
- mql.addEventListener("change", onChange);
547
- return () => mql.removeEventListener("change", onChange);
548
- },
549
- [query]
550
- );
551
- return useSyncExternalStore(
552
- subscribe,
553
- () => window.matchMedia(query).matches,
554
- () => false
555
- );
556
- }
557
- var init_useMediaQuery = __esm({
558
- "hooks/useMediaQuery.ts"() {
559
- "use client";
560
- }
561
- });
562
542
  function useInfiniteScroll(onLoadMore, options = {}) {
563
543
  const { rootMargin = "200px", hasMore = true, isLoading = false } = options;
564
544
  const observerRef = useRef(null);
@@ -11659,6 +11639,14 @@ function shapeBounds(shape) {
11659
11639
  w: shape.radius * 2 + 8,
11660
11640
  h: shape.radius * 2 + 8
11661
11641
  };
11642
+ case "ellipse":
11643
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11644
+ return {
11645
+ x: shape.x - shape.width / 2 - 4,
11646
+ y: shape.y - shape.height / 2 - 4,
11647
+ w: shape.width + 8,
11648
+ h: shape.height + 8
11649
+ };
11662
11650
  case "rect":
11663
11651
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) return null;
11664
11652
  return { x: shape.x - 4, y: shape.y - 4, w: shape.width + 8, h: shape.height + 8 };
@@ -11692,13 +11680,14 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
11692
11680
  ctx.closePath();
11693
11681
  ctx.fill();
11694
11682
  }
11695
- function drawShape(ctx, shape, width, height) {
11683
+ function drawShape(ctx, shape, width, height, allShapes) {
11696
11684
  ctx.save();
11697
11685
  const opacity = shape.opacity ?? 1;
11698
11686
  ctx.globalAlpha = opacity;
11699
11687
  const stroke = resolveColor2(shape.color, ctx, "#333333");
11700
11688
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
11701
11689
  ctx.lineWidth = shape.lineWidth ?? 2;
11690
+ if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
11702
11691
  switch (shape.type) {
11703
11692
  case "grid": {
11704
11693
  const step = shape.step ?? 40;
@@ -11764,6 +11753,20 @@ function drawShape(ctx, shape, width, height) {
11764
11753
  ctx.stroke();
11765
11754
  break;
11766
11755
  }
11756
+ case "ellipse": {
11757
+ if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11758
+ const startAngle = (shape.startAngle ?? 0) * Math.PI / 180;
11759
+ const endAngle = (shape.endAngle ?? 360) * Math.PI / 180;
11760
+ ctx.beginPath();
11761
+ ctx.ellipse(shape.x, shape.y, shape.width / 2, shape.height / 2, 0, startAngle, endAngle);
11762
+ if (fill) {
11763
+ ctx.fillStyle = fill;
11764
+ ctx.fill();
11765
+ }
11766
+ ctx.strokeStyle = stroke;
11767
+ ctx.stroke();
11768
+ break;
11769
+ }
11767
11770
  case "rect": {
11768
11771
  if (shape.x == null || shape.y == null || shape.width == null || shape.height == null) break;
11769
11772
  if (fill) {
@@ -11810,21 +11813,153 @@ function drawShape(ctx, shape, width, height) {
11810
11813
  ctx.fillText(shape.text, shape.x, shape.y);
11811
11814
  break;
11812
11815
  }
11816
+ case "venn-region": {
11817
+ const resolveCircles = (ids) => (ids ?? []).flatMap((id) => {
11818
+ const c = allShapes.find((s) => s.type === "circle" && s.id === id);
11819
+ return c && c.x != null && c.y != null && c.radius != null ? [{ x: c.x, y: c.y, radius: c.radius }] : [];
11820
+ });
11821
+ const inside = resolveCircles(shape.inside);
11822
+ if (inside.length === 0) break;
11823
+ const outside = resolveCircles(shape.outside);
11824
+ const off = document.createElement("canvas");
11825
+ off.width = ctx.canvas.width;
11826
+ off.height = ctx.canvas.height;
11827
+ const octx = off.getContext("2d");
11828
+ if (!octx) break;
11829
+ octx.setTransform(ctx.getTransform());
11830
+ for (const c of inside) {
11831
+ const p = new Path2D();
11832
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11833
+ octx.clip(p);
11834
+ }
11835
+ octx.fillStyle = fill ?? stroke;
11836
+ octx.fillRect(0, 0, width, height);
11837
+ octx.globalCompositeOperation = "destination-out";
11838
+ for (const c of outside) {
11839
+ const p = new Path2D();
11840
+ p.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
11841
+ octx.fill(p);
11842
+ }
11843
+ ctx.save();
11844
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
11845
+ ctx.drawImage(off, 0, 0);
11846
+ ctx.restore();
11847
+ break;
11848
+ }
11813
11849
  }
11814
11850
  ctx.restore();
11815
11851
  }
11816
- var LearningCanvas;
11852
+ function readoutShapes(readouts, width) {
11853
+ const out = [];
11854
+ const chipH = 18;
11855
+ const gap = 6;
11856
+ let rightEdge = width - 6;
11857
+ let rowY = 6;
11858
+ for (const readout of readouts) {
11859
+ const text = `${readout.label}: ${String(readout.value)}`;
11860
+ const chipW = Math.min(170, Math.max(34, text.length * 6 + 12));
11861
+ let chipX = rightEdge - chipW;
11862
+ if (chipX < 4) {
11863
+ rowY += chipH + 4;
11864
+ rightEdge = width - 6;
11865
+ chipX = rightEdge - chipW;
11866
+ }
11867
+ const color = readout.color ?? "#334155";
11868
+ out.push({ type: "rect", x: chipX, y: rowY, width: chipW, height: chipH, color, fill: color });
11869
+ out.push({
11870
+ type: "text",
11871
+ x: chipX + chipW / 2,
11872
+ y: rowY + chipH / 2,
11873
+ text,
11874
+ color: "#ffffff",
11875
+ fontSize: 10,
11876
+ align: "center"
11877
+ });
11878
+ rightEdge = chipX - gap;
11879
+ }
11880
+ return out;
11881
+ }
11882
+ function traceShapes(panel, k, width, height) {
11883
+ const w = panel.width ?? Math.round(width * 0.32);
11884
+ const h = panel.height ?? Math.round(height * 0.28);
11885
+ const x = panel.x ?? width - w - 8;
11886
+ const y = panel.y ?? height - h - 8 - k * (h + 8);
11887
+ const allSamples = panel.series.flatMap((series) => series.samples);
11888
+ let xLo = Math.min(...allSamples.map((p) => p.x));
11889
+ let xHi = Math.max(...allSamples.map((p) => p.x));
11890
+ let yLo = Math.min(...allSamples.map((p) => p.y));
11891
+ let yHi = Math.max(...allSamples.map((p) => p.y));
11892
+ if (xLo === xHi) {
11893
+ xLo -= 1;
11894
+ xHi += 1;
11895
+ }
11896
+ if (yLo === yHi) {
11897
+ yLo -= 1;
11898
+ yHi += 1;
11899
+ }
11900
+ const backgroundColor = panel.backgroundColor ?? "#ffffff";
11901
+ const frameColor = panel.frameColor ?? "#94a3b8";
11902
+ const out = [];
11903
+ out.push({
11904
+ type: "rect",
11905
+ x,
11906
+ y,
11907
+ width: w,
11908
+ height: h,
11909
+ color: backgroundColor,
11910
+ fill: backgroundColor,
11911
+ opacity: panel.backgroundOpacity ?? 0.85
11912
+ });
11913
+ out.push({ type: "rect", x, y, width: w, height: h, color: frameColor, lineWidth: 1 });
11914
+ panel.series.forEach((series, j) => {
11915
+ const color = series.color ?? TRACE_SERIES_COLORS[j % TRACE_SERIES_COLORS.length];
11916
+ const mapped = series.samples.map((p) => ({
11917
+ x: x + 4 + (p.x - xLo) / (xHi - xLo) * (w - 8),
11918
+ y: y + h - 4 - (p.y - yLo) / (yHi - yLo) * (h - 8)
11919
+ }));
11920
+ for (let i = 1; i < mapped.length; i++) {
11921
+ out.push({
11922
+ type: "line",
11923
+ x1: mapped[i - 1].x,
11924
+ y1: mapped[i - 1].y,
11925
+ x2: mapped[i].x,
11926
+ y2: mapped[i].y,
11927
+ color,
11928
+ lineWidth: 1.5
11929
+ });
11930
+ }
11931
+ if (mapped.length > 0) {
11932
+ const last = mapped[mapped.length - 1];
11933
+ out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
11934
+ }
11935
+ if (series.label) {
11936
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
11937
+ }
11938
+ });
11939
+ if (panel.yLabel) {
11940
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
11941
+ }
11942
+ if (panel.xLabel) {
11943
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
11944
+ }
11945
+ return out;
11946
+ }
11947
+ var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
11817
11948
  var init_LearningCanvas = __esm({
11818
11949
  "components/learning/atoms/LearningCanvas.tsx"() {
11819
11950
  "use client";
11820
11951
  init_cn();
11821
11952
  init_useEventBus();
11953
+ DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
11954
+ TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
11822
11955
  LearningCanvas = ({
11823
11956
  className,
11824
11957
  width = 600,
11825
11958
  height = 400,
11826
11959
  backgroundColor,
11827
11960
  shapes = [],
11961
+ readouts,
11962
+ traces,
11828
11963
  interactive = false,
11829
11964
  animate = false,
11830
11965
  onShapeClick,
@@ -11850,6 +11985,12 @@ var init_LearningCanvas = __esm({
11850
11985
  }
11851
11986
  return -1;
11852
11987
  }, [shapes]);
11988
+ const derivedShapes = useMemo(() => {
11989
+ if (!traces?.length && !readouts?.length) return shapes;
11990
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
11991
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
11992
+ return [...shapes, ...traceOut, ...readoutOut];
11993
+ }, [shapes, traces, readouts, width, height]);
11853
11994
  const draw = useCallback(() => {
11854
11995
  const canvas = canvasRef.current;
11855
11996
  if (!canvas) return;
@@ -11866,13 +12007,13 @@ var init_LearningCanvas = __esm({
11866
12007
  ctx.fillStyle = backgroundColor;
11867
12008
  ctx.fillRect(0, 0, width, height);
11868
12009
  }
11869
- for (const shape of shapes) {
11870
- if (shape.type !== "text") drawShape(ctx, shape, width, height);
12010
+ for (const shape of derivedShapes) {
12011
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
11871
12012
  }
11872
- for (const shape of shapes) {
11873
- if (shape.type === "text") drawShape(ctx, shape, width, height);
12013
+ for (const shape of derivedShapes) {
12014
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
11874
12015
  }
11875
- }, [width, height, backgroundColor, shapes]);
12016
+ }, [width, height, backgroundColor, derivedShapes]);
11876
12017
  useEffect(() => {
11877
12018
  draw();
11878
12019
  }, [draw]);
@@ -13422,7 +13563,363 @@ var init_ComponentPatterns = __esm({
13422
13563
  AlertPattern.displayName = "AlertPattern";
13423
13564
  }
13424
13565
  });
13425
- var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, AlgorithmCanvas;
13566
+ function layoutCircle(nodes, width, height) {
13567
+ const cx = width / 2;
13568
+ const cy = height / 2;
13569
+ const radius = Math.max(10, Math.min(cx, cy) - 40);
13570
+ const positions = /* @__PURE__ */ new Map();
13571
+ const n = nodes.length;
13572
+ nodes.forEach((node, i) => {
13573
+ const angle = 2 * Math.PI * i / Math.max(n, 1) - Math.PI / 2;
13574
+ positions.set(node.id, { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) });
13575
+ });
13576
+ return positions;
13577
+ }
13578
+ function layoutTree(nodes, edges, root, width, height) {
13579
+ const nodeIds = nodes.map((n) => n.id);
13580
+ const idSet = new Set(nodeIds);
13581
+ const childrenOf = /* @__PURE__ */ new Map();
13582
+ const hasIncoming = /* @__PURE__ */ new Set();
13583
+ for (const e of edges) {
13584
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13585
+ const list = childrenOf.get(e.from) ?? [];
13586
+ list.push(e.to);
13587
+ childrenOf.set(e.from, list);
13588
+ hasIncoming.add(e.to);
13589
+ }
13590
+ const depth = /* @__PURE__ */ new Map();
13591
+ const treeChildren = /* @__PURE__ */ new Map();
13592
+ const visited = /* @__PURE__ */ new Set();
13593
+ const bfsFrom = (start) => {
13594
+ if (visited.has(start)) return;
13595
+ visited.add(start);
13596
+ depth.set(start, 0);
13597
+ const queue = [start];
13598
+ while (queue.length > 0) {
13599
+ const u = queue.shift();
13600
+ for (const v of childrenOf.get(u) ?? []) {
13601
+ if (visited.has(v)) continue;
13602
+ visited.add(v);
13603
+ depth.set(v, (depth.get(u) ?? 0) + 1);
13604
+ const list = treeChildren.get(u) ?? [];
13605
+ list.push(v);
13606
+ treeChildren.set(u, list);
13607
+ queue.push(v);
13608
+ }
13609
+ }
13610
+ };
13611
+ const primaryRoot = root && idSet.has(root) ? root : nodeIds.find((id) => !hasIncoming.has(id)) ?? nodeIds[0];
13612
+ const rootsOrder = [];
13613
+ if (primaryRoot !== void 0) {
13614
+ bfsFrom(primaryRoot);
13615
+ rootsOrder.push(primaryRoot);
13616
+ }
13617
+ for (const id of nodeIds) {
13618
+ if (!visited.has(id)) {
13619
+ bfsFrom(id);
13620
+ rootsOrder.push(id);
13621
+ }
13622
+ }
13623
+ let leafCounter = 0;
13624
+ const xSlot = /* @__PURE__ */ new Map();
13625
+ const assignXSlot = (u) => {
13626
+ const children = treeChildren.get(u) ?? [];
13627
+ if (children.length === 0) {
13628
+ const slot = leafCounter++;
13629
+ xSlot.set(u, slot);
13630
+ return slot;
13631
+ }
13632
+ const childSlots = children.map(assignXSlot);
13633
+ const avg = childSlots.reduce((a, b) => a + b, 0) / childSlots.length;
13634
+ xSlot.set(u, avg);
13635
+ return avg;
13636
+ };
13637
+ for (const r of rootsOrder) assignXSlot(r);
13638
+ let maxDepth = 0;
13639
+ for (const d of depth.values()) maxDepth = Math.max(maxDepth, d);
13640
+ const colWidth = width / Math.max(1, leafCounter);
13641
+ const rowHeight = height / (maxDepth + 1);
13642
+ const positions = /* @__PURE__ */ new Map();
13643
+ for (const id of nodeIds) {
13644
+ const slot = xSlot.get(id) ?? 0;
13645
+ const d = depth.get(id) ?? 0;
13646
+ positions.set(id, { x: slot * colWidth + colWidth / 2, y: d * rowHeight + rowHeight / 2 });
13647
+ }
13648
+ return positions;
13649
+ }
13650
+ function layoutLayered(nodes, edges, width, height) {
13651
+ const nodeIds = nodes.map((n) => n.id);
13652
+ const idSet = new Set(nodeIds);
13653
+ const adj = /* @__PURE__ */ new Map();
13654
+ const remainingIndegree = /* @__PURE__ */ new Map();
13655
+ for (const id of nodeIds) remainingIndegree.set(id, 0);
13656
+ for (const e of edges) {
13657
+ if (!idSet.has(e.from) || !idSet.has(e.to)) continue;
13658
+ const list = adj.get(e.from) ?? [];
13659
+ list.push(e.to);
13660
+ adj.set(e.from, list);
13661
+ remainingIndegree.set(e.to, (remainingIndegree.get(e.to) ?? 0) + 1);
13662
+ }
13663
+ const layer = /* @__PURE__ */ new Map();
13664
+ const dequeued = /* @__PURE__ */ new Set();
13665
+ const queue = [];
13666
+ for (const id of nodeIds) {
13667
+ if ((remainingIndegree.get(id) ?? 0) === 0) {
13668
+ layer.set(id, 0);
13669
+ queue.push(id);
13670
+ }
13671
+ }
13672
+ while (queue.length > 0) {
13673
+ const u = queue.shift();
13674
+ dequeued.add(u);
13675
+ for (const v of adj.get(u) ?? []) {
13676
+ const candidate = (layer.get(u) ?? 0) + 1;
13677
+ layer.set(v, Math.max(layer.get(v) ?? 0, candidate));
13678
+ remainingIndegree.set(v, (remainingIndegree.get(v) ?? 0) - 1);
13679
+ if ((remainingIndegree.get(v) ?? 0) === 0 && !dequeued.has(v)) {
13680
+ queue.push(v);
13681
+ }
13682
+ }
13683
+ }
13684
+ let baseMaxLayer = 0;
13685
+ for (const id of nodeIds) {
13686
+ if (dequeued.has(id)) baseMaxLayer = Math.max(baseMaxLayer, layer.get(id) ?? 0);
13687
+ }
13688
+ const cycleLayer = baseMaxLayer + 1;
13689
+ let maxLayer = baseMaxLayer;
13690
+ for (const id of nodeIds) {
13691
+ if (!dequeued.has(id)) {
13692
+ layer.set(id, cycleLayer);
13693
+ maxLayer = cycleLayer;
13694
+ }
13695
+ }
13696
+ const colWidth = width / Math.max(1, maxLayer + 1);
13697
+ const byLayer = /* @__PURE__ */ new Map();
13698
+ for (const id of nodeIds) {
13699
+ const l = layer.get(id) ?? 0;
13700
+ const list = byLayer.get(l) ?? [];
13701
+ list.push(id);
13702
+ byLayer.set(l, list);
13703
+ }
13704
+ const positions = /* @__PURE__ */ new Map();
13705
+ for (const [l, ids] of byLayer) {
13706
+ const rowHeight = height / ids.length;
13707
+ ids.forEach((id, i) => {
13708
+ positions.set(id, { x: l * colWidth + colWidth / 2, y: i * rowHeight + rowHeight / 2 });
13709
+ });
13710
+ }
13711
+ return positions;
13712
+ }
13713
+ function computePositions(nodes, edges, layout, root, width, height) {
13714
+ switch (layout) {
13715
+ case "circle":
13716
+ return layoutCircle(nodes, width, height);
13717
+ case "tree":
13718
+ return layoutTree(nodes, edges, root, width, height);
13719
+ case "layered":
13720
+ return layoutLayered(nodes, edges, width, height);
13721
+ case "manual":
13722
+ default: {
13723
+ const positions = /* @__PURE__ */ new Map();
13724
+ for (const n of nodes) positions.set(n.id, { x: n.x ?? 0, y: n.y ?? 0 });
13725
+ return positions;
13726
+ }
13727
+ }
13728
+ }
13729
+ var NODE_STATE_COLOR, EDGE_STATE_COLOR, DEFAULT_NODE_RADIUS, AlgoGraphCanvas;
13730
+ var init_AlgoGraphCanvas = __esm({
13731
+ "components/learning/molecules/AlgoGraphCanvas.tsx"() {
13732
+ "use client";
13733
+ init_atoms();
13734
+ init_Stack();
13735
+ init_LearningCanvas();
13736
+ NODE_STATE_COLOR = {
13737
+ unvisited: "#cbd5e1",
13738
+ frontier: "#f59e0b",
13739
+ current: "#ef4444",
13740
+ visited: "#22c55e",
13741
+ goal: "#8b5cf6",
13742
+ path: "#0ea5e9"
13743
+ };
13744
+ EDGE_STATE_COLOR = {
13745
+ default: "#9ca3af",
13746
+ tree: "#16a34a",
13747
+ relaxed: "#f59e0b",
13748
+ candidate: "#38bdf8",
13749
+ path: "#dc2626"
13750
+ };
13751
+ DEFAULT_NODE_RADIUS = 18;
13752
+ AlgoGraphCanvas = ({
13753
+ className,
13754
+ width = 600,
13755
+ height = 400,
13756
+ title,
13757
+ backgroundColor,
13758
+ nodes = [],
13759
+ edges = [],
13760
+ layout = "manual",
13761
+ root,
13762
+ shapes = [],
13763
+ interactive = false,
13764
+ animate = false,
13765
+ onShapeClick,
13766
+ onNodeClick,
13767
+ isLoading,
13768
+ error
13769
+ }) => {
13770
+ const nodeById = useMemo(() => {
13771
+ const m = /* @__PURE__ */ new Map();
13772
+ for (const n of nodes) m.set(n.id, n);
13773
+ return m;
13774
+ }, [nodes]);
13775
+ const nodeIndexById = useMemo(() => {
13776
+ const m = /* @__PURE__ */ new Map();
13777
+ nodes.forEach((n, i) => m.set(n.id, i));
13778
+ return m;
13779
+ }, [nodes]);
13780
+ const derivedShapes = useMemo(() => {
13781
+ const out = [];
13782
+ const positions = computePositions(nodes, edges, layout, root, width, height);
13783
+ const edgeGeoms = [];
13784
+ for (const e of edges) {
13785
+ const a = nodeById.get(e.from);
13786
+ const b = nodeById.get(e.to);
13787
+ const posA = positions.get(e.from);
13788
+ const posB = positions.get(e.to);
13789
+ if (!a || !b || !posA || !posB) continue;
13790
+ const rA = a.radius ?? DEFAULT_NODE_RADIUS;
13791
+ const rB = b.radius ?? DEFAULT_NODE_RADIUS;
13792
+ const dx = posB.x - posA.x;
13793
+ const dy = posB.y - posA.y;
13794
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
13795
+ const ux = dx / dist;
13796
+ const uy = dy / dist;
13797
+ edgeGeoms.push({
13798
+ directed: e.directed ?? false,
13799
+ x1: posA.x + ux * rA,
13800
+ y1: posA.y + uy * rA,
13801
+ x2: posB.x - ux * rB,
13802
+ y2: posB.y - uy * rB,
13803
+ color: e.color ?? EDGE_STATE_COLOR[e.state ?? "default"],
13804
+ label: e.label ?? (e.weight != null ? String(e.weight) : void 0)
13805
+ });
13806
+ }
13807
+ for (const g of edgeGeoms) {
13808
+ out.push({
13809
+ type: g.directed ? "arrow" : "line",
13810
+ x1: g.x1,
13811
+ y1: g.y1,
13812
+ x2: g.x2,
13813
+ y2: g.y2,
13814
+ color: g.color,
13815
+ lineWidth: 2
13816
+ });
13817
+ }
13818
+ for (const g of edgeGeoms) {
13819
+ if (g.label === void 0) continue;
13820
+ const midX = (g.x1 + g.x2) / 2;
13821
+ const midY = (g.y1 + g.y2) / 2;
13822
+ const dx = g.x2 - g.x1;
13823
+ const dy = g.y2 - g.y1;
13824
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
13825
+ const perpX = -(dy / dist);
13826
+ const perpY = dx / dist;
13827
+ out.push({
13828
+ type: "text",
13829
+ x: midX + perpX * 10,
13830
+ y: midY + perpY * 10,
13831
+ text: g.label,
13832
+ fontSize: 11,
13833
+ align: "center",
13834
+ color: "#374151"
13835
+ });
13836
+ }
13837
+ const nodeGeoms = [];
13838
+ for (const n of nodes) {
13839
+ const pos = positions.get(n.id);
13840
+ if (!pos) continue;
13841
+ nodeGeoms.push({
13842
+ id: n.id,
13843
+ x: pos.x,
13844
+ y: pos.y,
13845
+ radius: n.radius ?? DEFAULT_NODE_RADIUS,
13846
+ color: n.color ?? NODE_STATE_COLOR[n.state ?? "unvisited"],
13847
+ label: n.label,
13848
+ badge: n.badge
13849
+ });
13850
+ }
13851
+ for (const g of nodeGeoms) {
13852
+ out.push({ type: "circle", id: g.id, x: g.x, y: g.y, radius: g.radius, color: g.color, fill: `${g.color}33` });
13853
+ }
13854
+ const badgeGeoms = [];
13855
+ for (const g of nodeGeoms) {
13856
+ if (!g.badge) continue;
13857
+ const w = Math.min(42, Math.max(18, g.badge.text.length * 6 + 10));
13858
+ badgeGeoms.push({
13859
+ cx: g.x + g.radius * 0.75,
13860
+ cy: g.y - g.radius * 0.75,
13861
+ w,
13862
+ h: 14,
13863
+ // Borderless pill: same color drives both stroke and fill.
13864
+ color: g.badge.color ?? "#1e293b",
13865
+ text: g.badge.text
13866
+ });
13867
+ }
13868
+ for (const b of badgeGeoms) {
13869
+ out.push({ type: "rect", x: b.cx - b.w / 2, y: b.cy - b.h / 2, width: b.w, height: b.h, color: b.color, fill: b.color });
13870
+ }
13871
+ for (const b of badgeGeoms) {
13872
+ out.push({ type: "text", x: b.cx, y: b.cy, text: b.text, fontSize: 9, align: "center", color: "#ffffff" });
13873
+ }
13874
+ for (const g of nodeGeoms) {
13875
+ if (g.label === void 0) continue;
13876
+ out.push({
13877
+ type: "text",
13878
+ x: g.x,
13879
+ y: g.y + g.radius + 14,
13880
+ text: g.label,
13881
+ fontSize: 12,
13882
+ align: "center",
13883
+ color: "#111827"
13884
+ });
13885
+ }
13886
+ out.push(...shapes);
13887
+ return out;
13888
+ }, [nodes, edges, layout, root, width, height, nodeById, shapes]);
13889
+ const handleShapeClick = useCallback(
13890
+ (payload) => {
13891
+ if (payload.type === "circle" && payload.id) {
13892
+ const node = nodeById.get(payload.id);
13893
+ const idx = nodeIndexById.get(payload.id);
13894
+ if (node && idx !== void 0) {
13895
+ onNodeClick?.({ id: node.id, label: node.label, index: idx });
13896
+ }
13897
+ }
13898
+ onShapeClick?.(payload);
13899
+ },
13900
+ [nodeById, nodeIndexById, onNodeClick, onShapeClick]
13901
+ );
13902
+ return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
13903
+ title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
13904
+ /* @__PURE__ */ jsx(
13905
+ LearningCanvas,
13906
+ {
13907
+ width,
13908
+ height,
13909
+ backgroundColor,
13910
+ shapes: derivedShapes,
13911
+ interactive,
13912
+ animate,
13913
+ onShapeClick: onShapeClick || onNodeClick ? handleShapeClick : void 0,
13914
+ isLoading,
13915
+ error
13916
+ }
13917
+ )
13918
+ ] }) });
13919
+ };
13920
+ }
13921
+ });
13922
+ var DEFAULT_BAR_COLOR, DEFAULT_CELL_COLOR, DEFAULT_POINTER_COLOR, POINTER_BAND, TOP_PAD, PANEL_FAMILY_ORDER, RANGE_COLOR_DEFAULT, RANGE_FILL_OPACITY, BRACKET_TOP_OFFSET, BRACKET_ROW_H, BRACKET_TICK_H, BRACKET_LABEL_OFFSET, SLOT_EMPTY_FILL, SLOT_EMPTY_STROKE, SLOT_FILLED_STROKE, SLOT_HIGHLIGHT_DEFAULT, SLOT_VALUE_TEXT_COLOR, FRAME_ACTIVE_COLOR, FRAME_RETURNING_COLOR, FRAME_DONE_COLOR, FRAME_LABEL_COLOR, FRAME_DETAIL_COLOR, FRAME_TWO_LINE_MIN_H, BUCKET_INDEX_FILL, BUCKET_INDEX_STROKE, BUCKET_INDEX_TEXT, BUCKET_ENTRY_TEXT, BUCKET_ENTRY_DEFAULT, BUCKET_ENTRY_HIGHLIGHT, BUCKET_ENTRY_PROBING, BUCKET_ENTRY_MIN_W, BUCKET_ENTRY_MAX_W, AXIS_LABEL_COLOR, AXIS_LABEL_FONT_SIZE, CORNER_TEXT_COLOR, CORNER_FONT_SIZE, CORNER_MIN_CELL, CORNER_INSET_X, CORNER_INSET_Y, AUX_PRIMARY_RATIO, AUX_LABEL_BAND, AUX_BASELINE_PAD, AlgorithmCanvas;
13426
13923
  var init_AlgorithmCanvas = __esm({
13427
13924
  "components/learning/molecules/AlgorithmCanvas.tsx"() {
13428
13925
  "use client";
@@ -13434,6 +13931,43 @@ var init_AlgorithmCanvas = __esm({
13434
13931
  DEFAULT_POINTER_COLOR = "#dc2626";
13435
13932
  POINTER_BAND = 34;
13436
13933
  TOP_PAD = 26;
13934
+ PANEL_FAMILY_ORDER = ["bars", "slots", "cells", "buckets", "frames"];
13935
+ RANGE_COLOR_DEFAULT = "#3b82f6";
13936
+ RANGE_FILL_OPACITY = 0.15;
13937
+ BRACKET_TOP_OFFSET = 16;
13938
+ BRACKET_ROW_H = 14;
13939
+ BRACKET_TICK_H = 6;
13940
+ BRACKET_LABEL_OFFSET = 6;
13941
+ SLOT_EMPTY_FILL = "#f1f5f9";
13942
+ SLOT_EMPTY_STROKE = "#cbd5e1";
13943
+ SLOT_FILLED_STROKE = "#9ca3af";
13944
+ SLOT_HIGHLIGHT_DEFAULT = "#f59e0b";
13945
+ SLOT_VALUE_TEXT_COLOR = "#ffffff";
13946
+ FRAME_ACTIVE_COLOR = "#3b82f6";
13947
+ FRAME_RETURNING_COLOR = "#f59e0b";
13948
+ FRAME_DONE_COLOR = "#94a3b8";
13949
+ FRAME_LABEL_COLOR = "#ffffff";
13950
+ FRAME_DETAIL_COLOR = "#e2e8f0";
13951
+ FRAME_TWO_LINE_MIN_H = 22;
13952
+ BUCKET_INDEX_FILL = "#e2e8f0";
13953
+ BUCKET_INDEX_STROKE = "#9ca3af";
13954
+ BUCKET_INDEX_TEXT = "#374151";
13955
+ BUCKET_ENTRY_TEXT = "#ffffff";
13956
+ BUCKET_ENTRY_DEFAULT = "#3b82f6";
13957
+ BUCKET_ENTRY_HIGHLIGHT = "#f59e0b";
13958
+ BUCKET_ENTRY_PROBING = "#38bdf8";
13959
+ BUCKET_ENTRY_MIN_W = 24;
13960
+ BUCKET_ENTRY_MAX_W = 64;
13961
+ AXIS_LABEL_COLOR = "#6b7280";
13962
+ AXIS_LABEL_FONT_SIZE = 10;
13963
+ CORNER_TEXT_COLOR = "#111827";
13964
+ CORNER_FONT_SIZE = 7;
13965
+ CORNER_MIN_CELL = 28;
13966
+ CORNER_INSET_X = 3;
13967
+ CORNER_INSET_Y = 6;
13968
+ AUX_PRIMARY_RATIO = 0.6;
13969
+ AUX_LABEL_BAND = 18;
13970
+ AUX_BASELINE_PAD = 8;
13437
13971
  AlgorithmCanvas = ({
13438
13972
  className,
13439
13973
  width = 600,
@@ -13443,6 +13977,14 @@ var init_AlgorithmCanvas = __esm({
13443
13977
  bars = [],
13444
13978
  cells = [],
13445
13979
  pointers = [],
13980
+ ranges = [],
13981
+ slots = [],
13982
+ slotOrientation = "horizontal",
13983
+ frames = [],
13984
+ buckets = [],
13985
+ auxBars = [],
13986
+ rowLabels = [],
13987
+ colLabels = [],
13446
13988
  shapes = [],
13447
13989
  interactive = false,
13448
13990
  animate = false,
@@ -13452,12 +13994,35 @@ var init_AlgorithmCanvas = __esm({
13452
13994
  }) => {
13453
13995
  const derivedShapes = useMemo(() => {
13454
13996
  const out = [];
13997
+ const presence = {
13998
+ bars: bars.length > 0,
13999
+ slots: slots.length > 0,
14000
+ cells: cells.length > 0,
14001
+ buckets: buckets.length > 0,
14002
+ frames: frames.length > 0
14003
+ };
14004
+ const panelCount = PANEL_FAMILY_ORDER.filter((f3) => presence[f3]).length;
14005
+ const panelHeight = height / Math.max(1, panelCount);
14006
+ const panelY = { bars: 0, slots: 0, cells: 0, buckets: 0, frames: 0 };
14007
+ let compactIndex = 0;
14008
+ PANEL_FAMILY_ORDER.forEach((f3) => {
14009
+ if (presence[f3]) {
14010
+ panelY[f3] = compactIndex * panelHeight;
14011
+ compactIndex += 1;
14012
+ }
14013
+ });
13455
14014
  if (bars.length > 0) {
14015
+ const panelYBars = panelY.bars;
13456
14016
  const slot = width / bars.length;
13457
14017
  const barW = slot * 0.8;
13458
14018
  const gap = slot * 0.1;
13459
- const baseline = height - POINTER_BAND;
13460
- const usableH = baseline - TOP_PAD;
14019
+ const bracketRanges = ranges.filter((r) => r.kind === "bracket");
14020
+ const bracketCount = bracketRanges.length;
14021
+ const bracketHeadroom = bracketCount > 0 ? BRACKET_TOP_OFFSET + bracketCount * BRACKET_ROW_H : 0;
14022
+ const hasAux = auxBars.length > 0;
14023
+ const primaryH = hasAux ? panelHeight * AUX_PRIMARY_RATIO : panelHeight;
14024
+ const baseline = panelYBars + primaryH - POINTER_BAND;
14025
+ const usableH = baseline - (panelYBars + TOP_PAD + bracketHeadroom);
13461
14026
  const maxV = Math.max(1, ...bars.map((b) => Number.isFinite(b.value) ? b.value : 0));
13462
14027
  bars.forEach((bar, i) => {
13463
14028
  const v = Number.isFinite(bar.value) ? bar.value : 0;
@@ -13487,6 +14052,89 @@ var init_AlgorithmCanvas = __esm({
13487
14052
  });
13488
14053
  }
13489
14054
  });
14055
+ ranges.forEach((r) => {
14056
+ const kind = r.kind ?? "fill";
14057
+ if (kind !== "fill") return;
14058
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14059
+ out.push({
14060
+ type: "rect",
14061
+ x: r.from * slot,
14062
+ y: panelYBars,
14063
+ width: (r.to - r.from + 1) * slot,
14064
+ height: primaryH,
14065
+ color,
14066
+ fill: color,
14067
+ opacity: RANGE_FILL_OPACITY
14068
+ });
14069
+ if (r.label) {
14070
+ out.push({
14071
+ type: "text",
14072
+ x: r.from * slot + 4,
14073
+ // Sits below the bracket block (if any) so fill and bracket labels never collide.
14074
+ y: panelYBars + 10 + bracketHeadroom,
14075
+ text: r.label,
14076
+ color,
14077
+ fontSize: 10,
14078
+ align: "left"
14079
+ });
14080
+ }
14081
+ });
14082
+ bracketRanges.forEach((r, i) => {
14083
+ const bracketY = panelYBars + BRACKET_TOP_OFFSET + i * BRACKET_ROW_H;
14084
+ const x1 = r.from * slot + slot * 0.1;
14085
+ const x2 = (r.to + 1) * slot - slot * 0.1;
14086
+ const color = r.color ?? RANGE_COLOR_DEFAULT;
14087
+ out.push({ type: "line", x1, y1: bracketY, x2, y2: bracketY, color, lineWidth: 2 });
14088
+ out.push({ type: "line", x1, y1: bracketY, x2: x1, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14089
+ out.push({ type: "line", x1: x2, y1: bracketY, x2, y2: bracketY + BRACKET_TICK_H, color, lineWidth: 2 });
14090
+ if (r.label) {
14091
+ out.push({
14092
+ type: "text",
14093
+ x: (x1 + x2) / 2,
14094
+ y: bracketY - BRACKET_LABEL_OFFSET,
14095
+ text: r.label,
14096
+ color,
14097
+ fontSize: 10,
14098
+ align: "center"
14099
+ });
14100
+ }
14101
+ });
14102
+ if (hasAux) {
14103
+ const auxH = panelHeight - primaryH;
14104
+ const slot2 = width / auxBars.length;
14105
+ const auxBaseline = panelYBars + primaryH + auxH - AUX_BASELINE_PAD;
14106
+ const auxUsableH = auxBaseline - (panelYBars + primaryH + AUX_LABEL_BAND);
14107
+ const maxAuxV = Math.max(1, ...auxBars.map((b) => Number.isFinite(b.value) ? b.value : 0));
14108
+ auxBars.forEach((bar, i) => {
14109
+ const v = Number.isFinite(bar.value) ? bar.value : 0;
14110
+ const bh = Math.max(0, v / maxAuxV * auxUsableH);
14111
+ const x = i * slot2 + slot2 * 0.1;
14112
+ const w = slot2 * 0.8;
14113
+ const color = bar.color ?? DEFAULT_BAR_COLOR;
14114
+ out.push({
14115
+ type: "rect",
14116
+ id: `auxbar-${i}`,
14117
+ x,
14118
+ y: auxBaseline - bh,
14119
+ width: w,
14120
+ height: bh,
14121
+ color,
14122
+ fill: color
14123
+ });
14124
+ const label = bar.label ?? (auxBars.length <= 24 ? String(v) : void 0);
14125
+ if (label) {
14126
+ out.push({
14127
+ type: "text",
14128
+ x: x + w / 2,
14129
+ y: auxBaseline - bh - 8,
14130
+ text: label,
14131
+ color: "#374151",
14132
+ fontSize: 11,
14133
+ align: "center"
14134
+ });
14135
+ }
14136
+ });
14137
+ }
13490
14138
  pointers.forEach((p) => {
13491
14139
  if (p.index < 0 || p.index >= bars.length) return;
13492
14140
  const cx = p.index * slot + slot / 2;
@@ -13494,7 +14142,7 @@ var init_AlgorithmCanvas = __esm({
13494
14142
  out.push({
13495
14143
  type: "arrow",
13496
14144
  x1: cx,
13497
- y1: height - 6,
14145
+ y1: panelYBars + primaryH - 18,
13498
14146
  x2: cx,
13499
14147
  y2: baseline + 4,
13500
14148
  color,
@@ -13504,7 +14152,7 @@ var init_AlgorithmCanvas = __esm({
13504
14152
  out.push({
13505
14153
  type: "text",
13506
14154
  x: cx,
13507
- y: height - 22,
14155
+ y: panelYBars + primaryH - 8,
13508
14156
  text: p.label,
13509
14157
  color,
13510
14158
  fontSize: 11,
@@ -13513,14 +14161,111 @@ var init_AlgorithmCanvas = __esm({
13513
14161
  }
13514
14162
  });
13515
14163
  }
14164
+ if (slots.length > 0) {
14165
+ const panelYSlots = panelY.slots;
14166
+ const n = slots.length;
14167
+ const vertical = slotOrientation === "vertical";
14168
+ const vBoxH = panelHeight / n;
14169
+ const vBoxW = Math.min(width * 0.5, 120);
14170
+ const vBoxX = (width - vBoxW) / 2;
14171
+ const hCellW = width / n;
14172
+ const hBoxW = hCellW * 0.82;
14173
+ const hBoxH = Math.min(panelHeight * 0.6, 48);
14174
+ const hBoxY = panelYSlots + (panelHeight - hBoxH) / 2;
14175
+ const slotBox = (i) => vertical ? { x: vBoxX, y: panelYSlots + panelHeight - (i + 1) * vBoxH, width: vBoxW, height: vBoxH } : { x: i * hCellW + (hCellW - hBoxW) / 2, y: hBoxY, width: hBoxW, height: hBoxH };
14176
+ slots.forEach((s, i) => {
14177
+ const box = slotBox(i);
14178
+ const state = s.state ?? "filled";
14179
+ const fill = state === "empty" ? SLOT_EMPTY_FILL : state === "highlight" ? s.color ?? SLOT_HIGHLIGHT_DEFAULT : s.color ?? DEFAULT_BAR_COLOR;
14180
+ const stroke = state === "empty" ? SLOT_EMPTY_STROKE : SLOT_FILLED_STROKE;
14181
+ out.push({
14182
+ type: "rect",
14183
+ id: `slot-${i}`,
14184
+ x: box.x,
14185
+ y: box.y,
14186
+ width: box.width,
14187
+ height: box.height,
14188
+ color: stroke,
14189
+ fill
14190
+ });
14191
+ if (s.value != null && state !== "empty") {
14192
+ out.push({
14193
+ type: "text",
14194
+ x: box.x + box.width / 2,
14195
+ y: box.y + box.height / 2,
14196
+ text: String(s.value),
14197
+ color: SLOT_VALUE_TEXT_COLOR,
14198
+ fontSize: 12,
14199
+ align: "center"
14200
+ });
14201
+ }
14202
+ });
14203
+ if (bars.length === 0) {
14204
+ pointers.forEach((p) => {
14205
+ if (p.index < 0 || p.index >= slots.length) return;
14206
+ const box = slotBox(p.index);
14207
+ const color = p.color ?? DEFAULT_POINTER_COLOR;
14208
+ if (vertical) {
14209
+ const cy = box.y + box.height / 2;
14210
+ out.push({
14211
+ type: "arrow",
14212
+ x1: box.x + box.width + 34,
14213
+ y1: cy,
14214
+ x2: box.x + box.width + 4,
14215
+ y2: cy,
14216
+ color,
14217
+ lineWidth: 2
14218
+ });
14219
+ if (p.label) {
14220
+ out.push({
14221
+ type: "text",
14222
+ x: box.x + box.width + 38,
14223
+ y: cy,
14224
+ text: p.label,
14225
+ color,
14226
+ fontSize: 11,
14227
+ align: "left"
14228
+ });
14229
+ }
14230
+ } else {
14231
+ const cx = box.x + box.width / 2;
14232
+ out.push({
14233
+ type: "arrow",
14234
+ x1: cx,
14235
+ y1: panelYSlots + panelHeight - 18,
14236
+ x2: cx,
14237
+ y2: box.y + box.height + 4,
14238
+ color,
14239
+ lineWidth: 2
14240
+ });
14241
+ if (p.label) {
14242
+ out.push({
14243
+ type: "text",
14244
+ x: cx,
14245
+ y: panelYSlots + panelHeight - 8,
14246
+ text: p.label,
14247
+ color,
14248
+ fontSize: 11,
14249
+ align: "center"
14250
+ });
14251
+ }
14252
+ }
14253
+ });
14254
+ }
14255
+ }
13516
14256
  if (cells.length > 0) {
14257
+ const panelYCells = panelY.cells;
13517
14258
  const maxCol = Math.max(0, ...cells.map((c) => c.col)) + 1;
13518
14259
  const maxRow = Math.max(0, ...cells.map((c) => c.row)) + 1;
13519
- const cw = width / maxCol;
13520
- const ch = height / maxRow;
14260
+ const colLabelH = colLabels.length > 0 ? 16 : 0;
14261
+ const rowLabelW = rowLabels.length > 0 ? 20 : 0;
14262
+ const gridX0 = rowLabelW;
14263
+ const gridY0 = panelYCells + colLabelH;
14264
+ const cw = (width - rowLabelW) / maxCol;
14265
+ const ch = (panelHeight - colLabelH) / maxRow;
13521
14266
  cells.forEach((c, i) => {
13522
- const x = c.col * cw;
13523
- const y = c.row * ch;
14267
+ const x = gridX0 + c.col * cw;
14268
+ const y = gridY0 + c.row * ch;
13524
14269
  const color = c.color ?? DEFAULT_CELL_COLOR;
13525
14270
  out.push({
13526
14271
  type: "rect",
@@ -13544,11 +14289,207 @@ var init_AlgorithmCanvas = __esm({
13544
14289
  align: "center"
13545
14290
  });
13546
14291
  }
14292
+ if (c.corner && cw >= CORNER_MIN_CELL && ch >= CORNER_MIN_CELL) {
14293
+ const { tl, tr, bl, br } = c.corner;
14294
+ if (tl) {
14295
+ out.push({
14296
+ type: "text",
14297
+ x: x + CORNER_INSET_X,
14298
+ y: y + CORNER_INSET_Y,
14299
+ text: tl,
14300
+ color: CORNER_TEXT_COLOR,
14301
+ fontSize: CORNER_FONT_SIZE,
14302
+ align: "left"
14303
+ });
14304
+ }
14305
+ if (tr) {
14306
+ out.push({
14307
+ type: "text",
14308
+ x: x + cw - CORNER_INSET_X,
14309
+ y: y + CORNER_INSET_Y,
14310
+ text: tr,
14311
+ color: CORNER_TEXT_COLOR,
14312
+ fontSize: CORNER_FONT_SIZE,
14313
+ align: "right"
14314
+ });
14315
+ }
14316
+ if (bl) {
14317
+ out.push({
14318
+ type: "text",
14319
+ x: x + CORNER_INSET_X,
14320
+ y: y + ch - CORNER_INSET_Y,
14321
+ text: bl,
14322
+ color: CORNER_TEXT_COLOR,
14323
+ fontSize: CORNER_FONT_SIZE,
14324
+ align: "left"
14325
+ });
14326
+ }
14327
+ if (br) {
14328
+ out.push({
14329
+ type: "text",
14330
+ x: x + cw - CORNER_INSET_X,
14331
+ y: y + ch - CORNER_INSET_Y,
14332
+ text: br,
14333
+ color: CORNER_TEXT_COLOR,
14334
+ fontSize: CORNER_FONT_SIZE,
14335
+ align: "right"
14336
+ });
14337
+ }
14338
+ }
14339
+ });
14340
+ colLabels.forEach((l) => {
14341
+ out.push({
14342
+ type: "text",
14343
+ x: gridX0 + l.index * cw + cw / 2,
14344
+ y: panelYCells + colLabelH / 2,
14345
+ text: l.text,
14346
+ color: l.color ?? AXIS_LABEL_COLOR,
14347
+ fontSize: AXIS_LABEL_FONT_SIZE,
14348
+ align: "center"
14349
+ });
14350
+ });
14351
+ rowLabels.forEach((l) => {
14352
+ out.push({
14353
+ type: "text",
14354
+ x: rowLabelW - 6,
14355
+ y: gridY0 + l.index * ch + ch / 2,
14356
+ text: l.text,
14357
+ color: l.color ?? AXIS_LABEL_COLOR,
14358
+ fontSize: AXIS_LABEL_FONT_SIZE,
14359
+ align: "right"
14360
+ });
14361
+ });
14362
+ }
14363
+ if (buckets.length > 0) {
14364
+ const panelYBuckets = panelY.buckets;
14365
+ const bucketCount = Math.max(0, ...buckets.map((b) => b.index)) + 1;
14366
+ const rowH = panelHeight / bucketCount;
14367
+ const indexColW = Math.min(width * 0.12, 40);
14368
+ const maxChainLen = Math.max(1, ...buckets.map((b) => b.entries.length));
14369
+ const entryW = Math.min(BUCKET_ENTRY_MAX_W, Math.max(BUCKET_ENTRY_MIN_W, (width - indexColW - 8) / maxChainLen));
14370
+ const maxVisible = Math.floor((width - indexColW - 4) / entryW);
14371
+ buckets.forEach((b) => {
14372
+ const rowY = panelYBuckets + b.index * rowH;
14373
+ out.push({
14374
+ type: "rect",
14375
+ id: `bucket-index-${b.index}`,
14376
+ x: 2,
14377
+ y: rowY + 2,
14378
+ width: indexColW - 4,
14379
+ height: rowH - 4,
14380
+ color: BUCKET_INDEX_STROKE,
14381
+ fill: BUCKET_INDEX_FILL
14382
+ });
14383
+ out.push({
14384
+ type: "text",
14385
+ x: 2 + (indexColW - 4) / 2,
14386
+ y: rowY + rowH / 2,
14387
+ text: String(b.index),
14388
+ color: BUCKET_INDEX_TEXT,
14389
+ fontSize: 10,
14390
+ align: "center"
14391
+ });
14392
+ const overflow = b.entries.length > maxVisible;
14393
+ const visibleCount = overflow ? Math.max(0, maxVisible - 1) : b.entries.length;
14394
+ for (let j = 0; j < visibleCount; j++) {
14395
+ const entry = b.entries[j];
14396
+ const ex = indexColW + 4 + j * entryW;
14397
+ const state = entry.state ?? "default";
14398
+ const fill = state === "highlight" ? entry.color ?? BUCKET_ENTRY_HIGHLIGHT : state === "probing" ? entry.color ?? BUCKET_ENTRY_PROBING : entry.color ?? BUCKET_ENTRY_DEFAULT;
14399
+ out.push({
14400
+ type: "rect",
14401
+ id: `bucket-${b.index}-${j}`,
14402
+ x: ex,
14403
+ y: rowY + 2,
14404
+ width: entryW - 2,
14405
+ height: rowH - 4,
14406
+ color: fill,
14407
+ fill
14408
+ });
14409
+ if (entryW >= 20 && rowH >= 16) {
14410
+ out.push({
14411
+ type: "text",
14412
+ x: ex + (entryW - 2) / 2,
14413
+ y: rowY + rowH / 2,
14414
+ text: entry.label,
14415
+ color: BUCKET_ENTRY_TEXT,
14416
+ fontSize: 10,
14417
+ align: "center"
14418
+ });
14419
+ }
14420
+ }
14421
+ if (overflow) {
14422
+ const ex = indexColW + 4 + visibleCount * entryW;
14423
+ out.push({
14424
+ type: "rect",
14425
+ id: `bucket-${b.index}-overflow`,
14426
+ x: ex,
14427
+ y: rowY + 2,
14428
+ width: entryW - 2,
14429
+ height: rowH - 4,
14430
+ color: BUCKET_ENTRY_DEFAULT,
14431
+ fill: BUCKET_ENTRY_DEFAULT
14432
+ });
14433
+ out.push({
14434
+ type: "text",
14435
+ x: ex + (entryW - 2) / 2,
14436
+ y: rowY + rowH / 2,
14437
+ text: `+${b.entries.length - visibleCount}`,
14438
+ color: BUCKET_ENTRY_TEXT,
14439
+ fontSize: 10,
14440
+ align: "center"
14441
+ });
14442
+ }
14443
+ });
14444
+ }
14445
+ if (frames.length > 0) {
14446
+ const panelYFrames = panelY.frames;
14447
+ const n = frames.length;
14448
+ const frameH = panelHeight / n;
14449
+ const x = 8;
14450
+ const w = width - 16;
14451
+ frames.forEach((f3, i) => {
14452
+ const y = panelYFrames + panelHeight - (i + 1) * frameH;
14453
+ const state = f3.state ?? "active";
14454
+ const fill = state === "returning" ? f3.color ?? FRAME_RETURNING_COLOR : state === "done" ? f3.color ?? FRAME_DONE_COLOR : f3.color ?? FRAME_ACTIVE_COLOR;
14455
+ out.push({ type: "rect", id: `frame-${i}`, x, y, width: w, height: frameH, color: fill, fill });
14456
+ if (frameH >= FRAME_TWO_LINE_MIN_H) {
14457
+ out.push({
14458
+ type: "text",
14459
+ x: 16,
14460
+ y: y + frameH * 0.35,
14461
+ text: f3.label,
14462
+ color: FRAME_LABEL_COLOR,
14463
+ fontSize: 10,
14464
+ align: "left"
14465
+ });
14466
+ if (f3.detail) {
14467
+ out.push({
14468
+ type: "text",
14469
+ x: 16,
14470
+ y: y + frameH * 0.7,
14471
+ text: f3.detail,
14472
+ color: FRAME_DETAIL_COLOR,
14473
+ fontSize: 10,
14474
+ align: "left"
14475
+ });
14476
+ }
14477
+ } else {
14478
+ out.push({
14479
+ type: "text",
14480
+ x: 16,
14481
+ y: y + frameH / 2,
14482
+ text: f3.label,
14483
+ color: FRAME_LABEL_COLOR,
14484
+ fontSize: 10,
14485
+ align: "left"
14486
+ });
14487
+ }
13547
14488
  });
13548
14489
  }
13549
14490
  out.push(...shapes);
13550
14491
  return out;
13551
- }, [bars, cells, pointers, shapes, width, height]);
14492
+ }, [bars, cells, pointers, ranges, slots, slotOrientation, frames, buckets, auxBars, rowLabels, colLabels, shapes, width, height]);
13552
14493
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
13553
14494
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
13554
14495
  /* @__PURE__ */ jsx(
@@ -14562,6 +15503,12 @@ function LearningScene3D({
14562
15503
  const unitId = event.payload?.unitId;
14563
15504
  if (typeof unitId === "string") onItemClickRef.current?.(unitId);
14564
15505
  });
15506
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && post?.bloom) {
15507
+ const unknownKeys = Object.keys(post.bloom).filter((k) => !KNOWN_BLOOM_KEYS.has(k));
15508
+ if (unknownKeys.length > 0) {
15509
+ sceneLog.debug("post.bloom has unrecognized keys \u2014 only intensity/threshold/smoothing are read", { unknownKeys });
15510
+ }
15511
+ }
14565
15512
  const props3d = {
14566
15513
  drawables,
14567
15514
  isLoading,
@@ -14627,7 +15574,7 @@ function cylinderBetween(from, to, radius, color) {
14627
15574
  material: { color }
14628
15575
  };
14629
15576
  }
14630
- function arrowBetween(from, to, color, shaftRadius = 0.08) {
15577
+ function arrowBetween(from, to, color, shaftRadius = 0.08, id) {
14631
15578
  const len = segmentLength(from, to);
14632
15579
  if (len < 1e-6) return null;
14633
15580
  const tipLen = Math.min(shaftRadius * 8, len * 0.35);
@@ -14655,6 +15602,7 @@ function arrowBetween(from, to, color, shaftRadius = 0.08) {
14655
15602
  };
14656
15603
  return {
14657
15604
  type: "draw-group",
15605
+ ...id !== void 0 ? { id } : {},
14658
15606
  position: { x: from[0], y: from[1], z: from[2] },
14659
15607
  items: tipLenActual < 1e-6 ? shaft ? [shaft] : [] : shaft ? [shaft, tip] : [tip]
14660
15608
  };
@@ -14681,20 +15629,228 @@ function get3DClickPayload(onShapeClick, idToIndex) {
14681
15629
  if (!onShapeClick) return void 0;
14682
15630
  return (id) => onShapeClick({ id, index: idToIndex.get(id) ?? -1 });
14683
15631
  }
14684
- var Canvas3DHost2;
15632
+ function polylineTube(points, radius, color, opts) {
15633
+ const maxSegments = opts?.maxSegments ?? 128;
15634
+ let pts = points;
15635
+ if (pts.length - 1 > maxSegments) {
15636
+ const step = (pts.length - 1) / maxSegments;
15637
+ const kept = [pts[0]];
15638
+ for (let s = 1; s < maxSegments; s++) kept.push(pts[Math.round(s * step)]);
15639
+ kept.push(pts[pts.length - 1]);
15640
+ pts = kept;
15641
+ }
15642
+ const out = [];
15643
+ for (let i = 0; i < pts.length - 1; i++) {
15644
+ const seg = cylinderBetween(pts[i], pts[i + 1], radius, color);
15645
+ if (seg) out.push(opts?.opacity !== void 0 ? { ...seg, opacity: opts.opacity } : seg);
15646
+ }
15647
+ return out;
15648
+ }
15649
+ function heightFieldMesh(spec) {
15650
+ const { nx, ny, heights, spacing = 1, x = 0, y = 0 } = spec;
15651
+ const flatShading = spec.flatShading ?? true;
15652
+ const vertices = [];
15653
+ for (let iy = 0; iy < ny; iy++) {
15654
+ for (let ix = 0; ix < nx; ix++) {
15655
+ vertices.push([
15656
+ x + (ix - (nx - 1) / 2) * spacing,
15657
+ y + (iy - (ny - 1) / 2) * spacing,
15658
+ heights[iy * nx + ix] ?? 0
15659
+ ]);
15660
+ }
15661
+ }
15662
+ const bands = [...spec.bands ?? []].sort((a, b) => (a.min ?? -Infinity) - (b.min ?? -Infinity));
15663
+ const facesByBand = /* @__PURE__ */ new Map();
15664
+ for (let iy = 0; iy < ny - 1; iy++) {
15665
+ for (let ix = 0; ix < nx - 1; ix++) {
15666
+ const v00 = iy * nx + ix;
15667
+ const v10 = iy * nx + ix + 1;
15668
+ const v01 = (iy + 1) * nx + ix;
15669
+ const v11 = (iy + 1) * nx + ix + 1;
15670
+ for (const face of [[v00, v10, v01], [v10, v11, v01]]) {
15671
+ const centroid = (vertices[face[0]][2] + vertices[face[1]][2] + vertices[face[2]][2]) / 3;
15672
+ let band = null;
15673
+ for (const b of bands) {
15674
+ if ((b.min ?? -Infinity) <= centroid) band = b;
15675
+ }
15676
+ const key = bands.length > 0 ? band : null;
15677
+ const list = facesByBand.get(key) ?? [];
15678
+ list.push(face);
15679
+ facesByBand.set(key, list);
15680
+ }
15681
+ }
15682
+ }
15683
+ const out = [];
15684
+ for (const [band, faces] of facesByBand) {
15685
+ if (faces.length === 0) continue;
15686
+ out.push({
15687
+ type: "draw-mesh",
15688
+ shape: "polyhedron",
15689
+ position: { x: 0, y: 0, z: 0 },
15690
+ vertices,
15691
+ faces,
15692
+ pivot: "center",
15693
+ material: { color: band?.color ?? spec.color ?? "#64748b", flatShading, side: "double" },
15694
+ ...spec.opacity !== void 0 ? { opacity: spec.opacity } : {}
15695
+ });
15696
+ }
15697
+ return out;
15698
+ }
15699
+ function arrowField(vectors, opts) {
15700
+ const scale = opts?.scale ?? 1;
15701
+ const out = [];
15702
+ for (const v of vectors) {
15703
+ const to = [
15704
+ v.from[0] + v.delta[0] * scale,
15705
+ v.from[1] + v.delta[1] * scale,
15706
+ v.from[2] + v.delta[2] * scale
15707
+ ];
15708
+ const arrow = arrowBetween(v.from, to, v.color ?? "#dc2626", v.width, v.id);
15709
+ if (arrow) out.push(arrow);
15710
+ if (v.label) out.push(billboardLabel(v.label, to[0], to[1], to[2], { color: opts?.labelColor }));
15711
+ }
15712
+ return out;
15713
+ }
15714
+ function helixDrawables(spec, opts) {
15715
+ const count = spec.count ?? spec.rungs?.length ?? 0;
15716
+ const rungs = Array.from({ length: count }, (_, i) => spec.rungs?.[i] ?? {});
15717
+ const radius = spec.radius ?? 1;
15718
+ const rise = spec.rise ?? 0.34;
15719
+ const twistRad = (spec.twistDeg ?? 36) * (Math.PI / 180);
15720
+ const strandAColor = spec.strandAColor ?? "#38bdf8";
15721
+ const strandBColor = spec.strandBColor ?? "#fb923c";
15722
+ const backboneRadius = spec.backboneRadius ?? 0.16;
15723
+ const rungRadius = spec.rungRadius ?? 0.12;
15724
+ const cx = spec.x ?? 0;
15725
+ const cy = spec.y ?? 0;
15726
+ const cz = spec.z ?? 0;
15727
+ const unwoundCount = spec.unwoundCount ?? 0;
15728
+ const unwindSpread = spec.unwindSpread ?? 1.8;
15729
+ const strandA = [];
15730
+ const strandB = [];
15731
+ for (let i = 0; i < count; i++) {
15732
+ const yi = cy + (i - (count - 1) / 2) * rise;
15733
+ const theta = i * twistRad;
15734
+ const s = i < unwoundCount ? unwindSpread : 1;
15735
+ strandA.push([cx + s * radius * Math.cos(theta), yi, cz + s * radius * Math.sin(theta)]);
15736
+ strandB.push([cx + s * radius * Math.cos(theta + Math.PI), yi, cz + s * radius * Math.sin(theta + Math.PI)]);
15737
+ }
15738
+ const out = [];
15739
+ for (let i = 0; i < count; i++) {
15740
+ out.push(meshSphere(`hx-a-${i}`, strandA[i][0], strandA[i][1], strandA[i][2], backboneRadius, strandAColor));
15741
+ out.push(meshSphere(`hx-b-${i}`, strandB[i][0], strandB[i][1], strandB[i][2], backboneRadius, strandBColor));
15742
+ if (i > 0) {
15743
+ const segA = cylinderBetween(strandA[i - 1], strandA[i], backboneRadius, strandAColor);
15744
+ if (segA) out.push(segA);
15745
+ const segB = cylinderBetween(strandB[i - 1], strandB[i], backboneRadius, strandBColor);
15746
+ if (segB) out.push(segB);
15747
+ }
15748
+ const rung = rungs[i];
15749
+ const rungColor = rung.color ?? "#94a3b8";
15750
+ const rod = cylinderBetween(strandA[i], strandB[i], rungRadius, rungColor);
15751
+ if (rod) out.push(rod);
15752
+ const mid = [
15753
+ (strandA[i][0] + strandB[i][0]) / 2,
15754
+ (strandA[i][1] + strandB[i][1]) / 2,
15755
+ (strandA[i][2] + strandB[i][2]) / 2
15756
+ ];
15757
+ const markerRadius = rung.radius ?? rungRadius;
15758
+ out.push(meshSphere(rung.id, mid[0], mid[1], mid[2], markerRadius, rungColor));
15759
+ if (rung.label) out.push(billboardLabel(rung.label, mid[0], mid[1], mid[2] + markerRadius, { color: opts?.labelColor }));
15760
+ }
15761
+ return out;
15762
+ }
15763
+ function latticeDrawables(spec, opts) {
15764
+ const nx = spec.nx ?? 2;
15765
+ const ny = spec.ny ?? 2;
15766
+ const nz = spec.nz ?? 2;
15767
+ const latticeConstant = spec.latticeConstant ?? 2;
15768
+ const bondRadius = spec.bondRadius ?? 0.06;
15769
+ const highlightCell = spec.highlightCell ?? false;
15770
+ const dimColor = spec.dimColor ?? "#475569";
15771
+ const showLabels = spec.showLabels ?? false;
15772
+ const selectedColor = spec.selectedColor ?? "#f59e0b";
15773
+ const posByKey = /* @__PURE__ */ new Map();
15774
+ const inCellByKey = /* @__PURE__ */ new Map();
15775
+ const out = [];
15776
+ for (const site of spec.basis) {
15777
+ const snx = site.xEdge ? nx + 1 : nx;
15778
+ const sny = site.yEdge ? ny + 1 : ny;
15779
+ const snz = site.zEdge ? nz + 1 : nz;
15780
+ for (let i = 0; i < snx; i++) {
15781
+ for (let j = 0; j < sny; j++) {
15782
+ for (let k = 0; k < snz; k++) {
15783
+ const key = `${site.key}-${i}-${j}-${k}`;
15784
+ const inCell = i + site.dx <= 1 && j + site.dy <= 1 && k + site.dz <= 1;
15785
+ const pos = [
15786
+ (i + site.dx) * latticeConstant - nx * latticeConstant / 2,
15787
+ (j + site.dy) * latticeConstant - ny * latticeConstant / 2,
15788
+ (k + site.dz) * latticeConstant - nz * latticeConstant / 2
15789
+ ];
15790
+ posByKey.set(key, pos);
15791
+ inCellByKey.set(key, inCell);
15792
+ const isSelected = spec.selectedId === `lat-${key}`;
15793
+ const color = isSelected ? selectedColor : highlightCell && !inCell ? dimColor : site.color ?? "#2563eb";
15794
+ const radius = (site.radius ?? 0.3) * (isSelected ? 1.4 : 1);
15795
+ out.push(meshSphere(`lat-${key}`, pos[0], pos[1], pos[2], radius, color));
15796
+ if (showLabels && site.element) {
15797
+ out.push(billboardLabel(site.element, pos[0], pos[1], pos[2] + radius, { color: opts?.labelColor }));
15798
+ }
15799
+ }
15800
+ }
15801
+ }
15802
+ }
15803
+ const basisByKey = new Map(spec.basis.map((s) => [s.key, s]));
15804
+ for (const bond of spec.bonds ?? []) {
15805
+ const fromSite = basisByKey.get(bond.from);
15806
+ const toSite = basisByKey.get(bond.to);
15807
+ if (!fromSite || !toSite) continue;
15808
+ const fnx = fromSite.xEdge ? nx + 1 : nx;
15809
+ const fny = fromSite.yEdge ? ny + 1 : ny;
15810
+ const fnz = fromSite.zEdge ? nz + 1 : nz;
15811
+ const tnx = toSite.xEdge ? nx + 1 : nx;
15812
+ const tny = toSite.yEdge ? ny + 1 : ny;
15813
+ const tnz = toSite.zEdge ? nz + 1 : nz;
15814
+ const bdx = bond.dx ?? 0;
15815
+ const bdy = bond.dy ?? 0;
15816
+ const bdz = bond.dz ?? 0;
15817
+ for (let i = 0; i < fnx; i++) {
15818
+ for (let j = 0; j < fny; j++) {
15819
+ for (let k = 0; k < fnz; k++) {
15820
+ const ti = i + bdx;
15821
+ const tj = j + bdy;
15822
+ const tk = k + bdz;
15823
+ if (ti < 0 || ti >= tnx || tj < 0 || tj >= tny || tk < 0 || tk >= tnz) continue;
15824
+ const fromKey = `${fromSite.key}-${i}-${j}-${k}`;
15825
+ const toKey = `${toSite.key}-${ti}-${tj}-${tk}`;
15826
+ const fromPos = posByKey.get(fromKey);
15827
+ const toPos = posByKey.get(toKey);
15828
+ if (!fromPos || !toPos) continue;
15829
+ const dimmed = highlightCell && !(inCellByKey.get(fromKey) && inCellByKey.get(toKey));
15830
+ const seg = cylinderBetween(fromPos, toPos, bondRadius, dimmed ? dimColor : bond.color ?? "#6b7280");
15831
+ if (seg) out.push(seg);
15832
+ }
15833
+ }
15834
+ }
15835
+ }
15836
+ return out;
15837
+ }
15838
+ var sceneLog, KNOWN_BLOOM_KEYS, Canvas3DHost2;
14685
15839
  var init_learningScene3D = __esm({
14686
15840
  "components/learning/molecules/learningScene3D.tsx"() {
14687
15841
  "use client";
14688
15842
  init_atoms();
14689
15843
  init_Stack();
14690
15844
  init_useEventBus();
15845
+ sceneLog = createLogger("almadar:ui:learning-scene-3d");
15846
+ KNOWN_BLOOM_KEYS = /* @__PURE__ */ new Set(["intensity", "threshold", "smoothing"]);
14691
15847
  Canvas3DHost2 = lazy(
14692
15848
  () => import('@almadar/ui/components/molecules/game/three').then((m) => ({ default: m.Canvas3DHost }))
14693
15849
  );
14694
15850
  LearningScene3D.displayName = "LearningScene3D";
14695
15851
  }
14696
15852
  });
14697
- var biologyLog, BiologyCanvas;
15853
+ var biologyLog, BIO_BAND_COLORS, BIO_STAGE_FILL, BIO_STAGE_TEXT, BiologyCanvas;
14698
15854
  var init_BiologyCanvas = __esm({
14699
15855
  "components/learning/molecules/BiologyCanvas.tsx"() {
14700
15856
  "use client";
@@ -14703,6 +15859,17 @@ var init_BiologyCanvas = __esm({
14703
15859
  init_LearningCanvas();
14704
15860
  init_learningScene3D();
14705
15861
  biologyLog = createLogger("almadar:ui:biology-canvas");
15862
+ BIO_BAND_COLORS = ["#dcfce7", "#fef9c3", "#fee2e2", "#e0e7ff"];
15863
+ BIO_STAGE_FILL = {
15864
+ pending: "#e2e8f0",
15865
+ active: "#3b82f6",
15866
+ done: "#94a3b8"
15867
+ };
15868
+ BIO_STAGE_TEXT = {
15869
+ pending: "#64748b",
15870
+ active: "#ffffff",
15871
+ done: "#ffffff"
15872
+ };
14706
15873
  BiologyCanvas = ({
14707
15874
  className,
14708
15875
  width = 600,
@@ -14715,7 +15882,15 @@ var init_BiologyCanvas = __esm({
14715
15882
  post,
14716
15883
  nodes = [],
14717
15884
  edges = [],
15885
+ compartments = [],
15886
+ bands = [],
15887
+ stages = [],
15888
+ stageStyle = "timeline",
15889
+ helix,
15890
+ helix3d,
14718
15891
  shapes = [],
15892
+ readouts,
15893
+ traces,
14719
15894
  showGrid,
14720
15895
  shadows,
14721
15896
  interactive,
@@ -14730,19 +15905,148 @@ var init_BiologyCanvas = __esm({
14730
15905
  for (const n of nodes) {
14731
15906
  if (n.id) nodeById.set(n.id, n);
14732
15907
  }
15908
+ const bandCount = bands.length;
15909
+ for (let i = 0; i < bandCount; i++) {
15910
+ const band = bands[i];
15911
+ const bandColor = band.color ?? BIO_BAND_COLORS[i % BIO_BAND_COLORS.length];
15912
+ const bandY = i * height / bandCount;
15913
+ const bandH = height / bandCount;
15914
+ out.push({
15915
+ type: "rect",
15916
+ x: 0,
15917
+ y: bandY,
15918
+ width,
15919
+ height: bandH,
15920
+ color: bandColor,
15921
+ fill: bandColor,
15922
+ opacity: 0.45
15923
+ });
15924
+ if (band.label) {
15925
+ out.push({
15926
+ type: "text",
15927
+ x: 8,
15928
+ y: bandY + 14,
15929
+ text: band.label,
15930
+ color: "#6b7280",
15931
+ fontSize: 10
15932
+ });
15933
+ }
15934
+ }
15935
+ for (const c of compartments) {
15936
+ const color = c.color ?? "#16a34a";
15937
+ out.push({
15938
+ type: "ellipse",
15939
+ x: c.x,
15940
+ y: c.y,
15941
+ width: c.width,
15942
+ height: c.height,
15943
+ color,
15944
+ fill: c.fill ?? `${color}1A`,
15945
+ lineWidth: c.lineWidth ?? 2,
15946
+ ...c.dash ? { dash: c.dash } : {}
15947
+ });
15948
+ if (c.label) {
15949
+ out.push({
15950
+ type: "text",
15951
+ x: c.x,
15952
+ y: c.y - c.height / 2 + 14,
15953
+ text: c.label,
15954
+ color: "#111827",
15955
+ fontSize: 11,
15956
+ align: "center"
15957
+ });
15958
+ }
15959
+ }
15960
+ if (helix) {
15961
+ const hx = helix.x ?? 24;
15962
+ const hy = helix.y ?? height * 0.25;
15963
+ const hw = helix.width ?? width - 48;
15964
+ const hh = helix.height ?? height * 0.5;
15965
+ const rungs = helix.rungs;
15966
+ const n = rungs.length;
15967
+ const cy = hy + hh / 2;
15968
+ const colorA = helix.colorA ?? "#2563eb";
15969
+ const colorB = helix.colorB ?? "#dc2626";
15970
+ const rungColor = helix.rungColor ?? "#94a3b8";
15971
+ const fork = helix.fork ?? 0;
15972
+ const maxSep = Math.min(hh - 8, 96);
15973
+ const strandA = [];
15974
+ const strandB = [];
15975
+ const rungGeoms = [];
15976
+ for (let i = 0; i < n; i++) {
15977
+ const rx = hx + (i + 0.5) * hw / n;
15978
+ const t = (i + 0.5) / n;
15979
+ const paired = t >= fork;
15980
+ const sep = paired ? 28 : 28 + (maxSep - 28) * ((fork - t) / fork);
15981
+ strandA.push({ x: rx, y: cy - sep / 2 });
15982
+ strandB.push({ x: rx, y: cy + sep / 2 });
15983
+ rungGeoms.push({ rx, sep, rung: rungs[i], paired });
15984
+ }
15985
+ for (let i = 1; i < n; i++) {
15986
+ out.push({ type: "line", x1: strandA[i - 1].x, y1: strandA[i - 1].y, x2: strandA[i].x, y2: strandA[i].y, color: colorA, lineWidth: 3 });
15987
+ }
15988
+ for (let i = 1; i < n; i++) {
15989
+ out.push({ type: "line", x1: strandB[i - 1].x, y1: strandB[i - 1].y, x2: strandB[i].x, y2: strandB[i].y, color: colorB, lineWidth: 3 });
15990
+ }
15991
+ for (const g of rungGeoms) {
15992
+ const rColor = g.rung.color ?? (g.rung.state === "new" ? "#16a34a" : rungColor);
15993
+ const topY = cy - g.sep / 2;
15994
+ const bottomY = cy + g.sep / 2;
15995
+ if (g.paired) {
15996
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: bottomY, color: rColor });
15997
+ if (g.rung.a) {
15998
+ out.push({ type: "text", x: g.rx, y: cy - g.sep / 4, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
15999
+ }
16000
+ if (g.rung.b) {
16001
+ out.push({ type: "text", x: g.rx, y: cy + g.sep / 4, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16002
+ }
16003
+ } else {
16004
+ const stubTopY = topY + 8;
16005
+ const stubBottomY = bottomY - 8;
16006
+ out.push({ type: "line", x1: g.rx, y1: topY, x2: g.rx, y2: stubTopY, color: rColor });
16007
+ out.push({ type: "line", x1: g.rx, y1: bottomY, x2: g.rx, y2: stubBottomY, color: rColor });
16008
+ if (g.rung.a) {
16009
+ out.push({ type: "text", x: g.rx, y: stubTopY + 6, text: g.rung.a, fontSize: 9, align: "center", color: "#374151" });
16010
+ }
16011
+ if (g.rung.b) {
16012
+ out.push({ type: "text", x: g.rx, y: stubBottomY - 6, text: g.rung.b, fontSize: 9, align: "center", color: "#374151" });
16013
+ }
16014
+ }
16015
+ }
16016
+ }
14733
16017
  for (const e of edges) {
14734
16018
  const a = nodeById.get(e.from);
14735
16019
  const b = nodeById.get(e.to);
14736
16020
  if (!a || !b) continue;
14737
- out.push({
14738
- type: "line",
14739
- x1: a.x,
14740
- y1: a.y,
14741
- x2: b.x,
14742
- y2: b.y,
14743
- color: e.color ?? "#9ca3af",
14744
- lineWidth: 2
14745
- });
16021
+ const color = e.color ?? "#9ca3af";
16022
+ if (e.directed) {
16023
+ const rA = a.radius ?? 16;
16024
+ const rB = b.radius ?? 16;
16025
+ const dx = b.x - a.x;
16026
+ const dy = b.y - a.y;
16027
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16028
+ const ux = dx / dist;
16029
+ const uy = dy / dist;
16030
+ out.push({
16031
+ type: "arrow",
16032
+ x1: a.x + ux * rA,
16033
+ y1: a.y + uy * rA,
16034
+ x2: b.x - ux * rB,
16035
+ y2: b.y - uy * rB,
16036
+ color,
16037
+ lineWidth: 2
16038
+ });
16039
+ } else {
16040
+ out.push({
16041
+ type: "line",
16042
+ x1: a.x,
16043
+ y1: a.y,
16044
+ x2: b.x,
16045
+ y2: b.y,
16046
+ color,
16047
+ lineWidth: 2
16048
+ });
16049
+ }
14746
16050
  if (e.label) {
14747
16051
  out.push({
14748
16052
  type: "text",
@@ -14755,6 +16059,8 @@ var init_BiologyCanvas = __esm({
14755
16059
  }
14756
16060
  }
14757
16061
  for (const n of nodes) {
16062
+ const state = n.state ?? "default";
16063
+ const muted = state === "muted";
14758
16064
  out.push({
14759
16065
  type: "circle",
14760
16066
  x: n.x,
@@ -14762,28 +16068,127 @@ var init_BiologyCanvas = __esm({
14762
16068
  radius: n.radius ?? 16,
14763
16069
  color: n.color ?? "#16a34a",
14764
16070
  fill: `${n.color ?? "#16a34a"}33`,
14765
- id: n.id
16071
+ id: n.id,
16072
+ ...muted ? { opacity: 0.35 } : {}
14766
16073
  });
16074
+ if (state === "highlight") {
16075
+ out.push({
16076
+ type: "circle",
16077
+ x: n.x,
16078
+ y: n.y,
16079
+ radius: (n.radius ?? 16) + 4,
16080
+ color: "#f59e0b",
16081
+ lineWidth: 2
16082
+ });
16083
+ }
14767
16084
  if (n.label) {
14768
16085
  out.push({
14769
16086
  type: "text",
14770
16087
  x: n.x,
14771
16088
  y: n.y + (n.radius ?? 16) + 14,
14772
16089
  text: n.label,
16090
+ ...muted ? { opacity: 0.35 } : {},
14773
16091
  color: "#111827",
14774
16092
  fontSize: 12,
14775
16093
  align: "center"
14776
16094
  });
14777
16095
  }
14778
16096
  }
16097
+ const stageCount = stages.length;
16098
+ if (stageCount > 0) {
16099
+ if (stageStyle === "ring") {
16100
+ const cx = width / 2;
16101
+ const cy = height / 2;
16102
+ const R = Math.min(width, height) / 2 - 48;
16103
+ const ringPoints = [];
16104
+ for (let i = 0; i < stageCount; i++) {
16105
+ const angleRad = (-90 + 360 * i / stageCount) * Math.PI / 180;
16106
+ ringPoints.push({ x: cx + R * Math.cos(angleRad), y: cy + R * Math.sin(angleRad) });
16107
+ }
16108
+ if (stageCount >= 2) {
16109
+ for (let i = 0; i < stageCount - 1; i++) {
16110
+ const p1 = ringPoints[i];
16111
+ const p2 = ringPoints[i + 1];
16112
+ const dx = p2.x - p1.x;
16113
+ const dy = p2.y - p1.y;
16114
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
16115
+ const ux = dx / dist;
16116
+ const uy = dy / dist;
16117
+ out.push({
16118
+ type: "arrow",
16119
+ x1: p1.x + ux * 46,
16120
+ y1: p1.y + uy * 46,
16121
+ x2: p2.x - ux * 46,
16122
+ y2: p2.y - uy * 46,
16123
+ color: "#94a3b8"
16124
+ });
16125
+ }
16126
+ }
16127
+ for (let i = 0; i < stageCount; i++) {
16128
+ const stage = stages[i];
16129
+ const state = stage.state ?? "pending";
16130
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16131
+ const w = Math.max(26, Math.min(84, stage.label.length * 6 + 10));
16132
+ const h = 18;
16133
+ const p = ringPoints[i];
16134
+ out.push({ type: "rect", x: p.x - w / 2, y: p.y - h / 2, width: w, height: h, color: fill, fill });
16135
+ out.push({ type: "text", x: p.x, y: p.y, text: stage.label, color: BIO_STAGE_TEXT[state], fontSize: 10, align: "center" });
16136
+ }
16137
+ } else {
16138
+ const stripY = height - 32;
16139
+ const slotW = (width - 16) / stageCount;
16140
+ const chipGeoms = [];
16141
+ for (let i = 0; i < stageCount; i++) {
16142
+ chipGeoms.push({ x: 8 + i * slotW + 5, w: slotW - 10 });
16143
+ }
16144
+ for (let i = 0; i < stageCount - 1; i++) {
16145
+ const midY = stripY + 13;
16146
+ out.push({
16147
+ type: "arrow",
16148
+ x1: chipGeoms[i].x + chipGeoms[i].w,
16149
+ y1: midY,
16150
+ x2: chipGeoms[i + 1].x,
16151
+ y2: midY,
16152
+ color: "#94a3b8"
16153
+ });
16154
+ }
16155
+ for (let i = 0; i < stageCount; i++) {
16156
+ const stage = stages[i];
16157
+ const state = stage.state ?? "pending";
16158
+ const fill = stage.color ?? BIO_STAGE_FILL[state];
16159
+ const g = chipGeoms[i];
16160
+ out.push({ type: "rect", x: g.x, y: stripY, width: g.w, height: 26, color: fill, fill });
16161
+ out.push({
16162
+ type: "text",
16163
+ x: g.x + g.w / 2,
16164
+ y: stripY + 13,
16165
+ text: stage.label,
16166
+ color: BIO_STAGE_TEXT[state],
16167
+ fontSize: 10,
16168
+ align: "center"
16169
+ });
16170
+ }
16171
+ }
16172
+ }
14779
16173
  out.push(...shapes);
14780
16174
  return out;
14781
- }, [nodes, edges, shapes]);
16175
+ }, [nodes, edges, compartments, bands, stages, stageStyle, helix, shapes, width, height]);
14782
16176
  const drawables3D = useMemo(() => {
14783
16177
  if (mode !== "3d") return [];
14784
16178
  if (shapes.length > 0) {
14785
16179
  biologyLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
14786
16180
  }
16181
+ if (compartments.length > 0 || bands.length > 0 || stages.length > 0 || helix) {
16182
+ biologyLog.debug("2D-only families ignored in 3D mode (pixel-authored 2D vocabulary)", {
16183
+ compartments: compartments.length,
16184
+ bands: bands.length,
16185
+ stages: stages.length,
16186
+ helix: helix != null
16187
+ });
16188
+ }
16189
+ if (animate) {
16190
+ biologyLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
16191
+ }
14787
16192
  const out = [];
14788
16193
  const labelColor = labelColorForBackground(backgroundColor);
14789
16194
  const nodeById = /* @__PURE__ */ new Map();
@@ -14817,15 +16222,21 @@ var init_BiologyCanvas = __esm({
14817
16222
  out.push(billboardLabel(n.label, n.x, n.y, nz + radius, { color: labelColor }));
14818
16223
  }
14819
16224
  }
16225
+ if (helix3d) {
16226
+ out.push(...helixDrawables(helix3d, { labelColor }));
16227
+ }
14820
16228
  return out;
14821
- }, [mode, nodes, edges, shapes, backgroundColor]);
16229
+ }, [mode, nodes, edges, shapes, compartments, bands, stages, helix, helix3d, animate, backgroundColor]);
14822
16230
  const nodeIndexById = useMemo(() => {
14823
16231
  const m = /* @__PURE__ */ new Map();
16232
+ (helix3d?.rungs ?? []).forEach((rung, i) => {
16233
+ if (rung.id) m.set(rung.id, i);
16234
+ });
14824
16235
  nodes.forEach((n, i) => {
14825
16236
  if (n.id) m.set(n.id, i);
14826
16237
  });
14827
16238
  return m;
14828
- }, [nodes]);
16239
+ }, [nodes, helix3d]);
14829
16240
  if (mode === "3d") {
14830
16241
  return /* @__PURE__ */ jsx(
14831
16242
  LearningScene3D,
@@ -14857,6 +16268,8 @@ var init_BiologyCanvas = __esm({
14857
16268
  height,
14858
16269
  backgroundColor,
14859
16270
  shapes: derivedShapes,
16271
+ readouts,
16272
+ traces,
14860
16273
  interactive: interactive ?? false,
14861
16274
  animate,
14862
16275
  onShapeClick,
@@ -21589,7 +23002,7 @@ function bondPerpendicular(a, b) {
21589
23002
  if (len < 1e-6) return [1, 0, 0];
21590
23003
  return [px / len, py / len, 0];
21591
23004
  }
21592
- var chemistryLog, ChemistryCanvas;
23005
+ var chemistryLog, CHEM_BOND_STATE_COLOR, LONE_PAIR_ANGLES, ChemistryCanvas;
21593
23006
  var init_ChemistryCanvas = __esm({
21594
23007
  "components/learning/molecules/ChemistryCanvas.tsx"() {
21595
23008
  "use client";
@@ -21598,6 +23011,13 @@ var init_ChemistryCanvas = __esm({
21598
23011
  init_LearningCanvas();
21599
23012
  init_learningScene3D();
21600
23013
  chemistryLog = createLogger("almadar:ui:chemistry-canvas");
23014
+ CHEM_BOND_STATE_COLOR = {
23015
+ default: "#6b7280",
23016
+ forming: "#16a34a",
23017
+ breaking: "#dc2626",
23018
+ highlight: "#f59e0b"
23019
+ };
23020
+ LONE_PAIR_ANGLES = [-90, 0, 90, 180];
21601
23021
  ChemistryCanvas = ({
21602
23022
  className,
21603
23023
  width = 600,
@@ -21611,7 +23031,14 @@ var init_ChemistryCanvas = __esm({
21611
23031
  atoms = [],
21612
23032
  bonds = [],
21613
23033
  arrows = [],
23034
+ bondStyle = "thick",
23035
+ containers = [],
23036
+ equation,
23037
+ equationColor,
23038
+ lattice3d,
21614
23039
  shapes = [],
23040
+ readouts,
23041
+ traces,
21615
23042
  showGrid,
21616
23043
  shadows,
21617
23044
  interactive,
@@ -21626,21 +23053,118 @@ var init_ChemistryCanvas = __esm({
21626
23053
  for (const a of atoms) {
21627
23054
  if (a.id) atomById.set(a.id, a);
21628
23055
  }
23056
+ for (const c of containers) {
23057
+ const color = c.color ?? "#64748b";
23058
+ if (c.level != null) {
23059
+ const lv = c.level;
23060
+ out.push({
23061
+ type: "rect",
23062
+ x: c.x + 1,
23063
+ y: c.y + c.height * (1 - lv),
23064
+ width: c.width - 2,
23065
+ height: c.height * lv - 1,
23066
+ color: c.levelColor ?? "#60a5fa",
23067
+ fill: c.levelColor ?? "#60a5fa",
23068
+ opacity: 0.5
23069
+ });
23070
+ }
23071
+ out.push({
23072
+ type: "rect",
23073
+ x: c.x,
23074
+ y: c.y,
23075
+ width: c.width,
23076
+ height: c.height,
23077
+ color,
23078
+ fill: c.fill,
23079
+ lineWidth: c.lineWidth ?? 2
23080
+ });
23081
+ const divider = c.divider ?? "none";
23082
+ if (divider !== "none") {
23083
+ out.push({
23084
+ type: "line",
23085
+ x1: c.x + c.width / 2,
23086
+ y1: c.y,
23087
+ x2: c.x + c.width / 2,
23088
+ y2: c.y + c.height,
23089
+ color: c.dividerColor ?? color,
23090
+ ...divider === "dashed" || divider === "dotted" ? { dash: divider } : {}
23091
+ });
23092
+ }
23093
+ if (c.leftLabel) {
23094
+ out.push({
23095
+ type: "text",
23096
+ x: c.x + c.width * 0.25,
23097
+ y: c.y + 12,
23098
+ text: c.leftLabel,
23099
+ color: "#374151",
23100
+ fontSize: 11,
23101
+ align: "center"
23102
+ });
23103
+ }
23104
+ if (c.rightLabel) {
23105
+ out.push({
23106
+ type: "text",
23107
+ x: c.x + c.width * 0.75,
23108
+ y: c.y + 12,
23109
+ text: c.rightLabel,
23110
+ color: "#374151",
23111
+ fontSize: 11,
23112
+ align: "center"
23113
+ });
23114
+ }
23115
+ if (c.label) {
23116
+ out.push({
23117
+ type: "text",
23118
+ x: c.x + c.width / 2,
23119
+ y: c.y + c.height + 12,
23120
+ text: c.label,
23121
+ color: "#111827",
23122
+ fontSize: 12,
23123
+ align: "center"
23124
+ });
23125
+ }
23126
+ }
21629
23127
  for (const b of bonds) {
21630
23128
  const a = atomById.get(b.from);
21631
23129
  const c = atomById.get(b.to);
21632
23130
  if (!a || !c) continue;
21633
- const color = b.color ?? "#6b7280";
21634
- const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
21635
- out.push({
21636
- type: "line",
21637
- x1: a.x,
21638
- y1: a.y,
21639
- x2: c.x,
21640
- y2: c.y,
21641
- color,
21642
- lineWidth: strokeWidth
21643
- });
23131
+ const state = b.state ?? "default";
23132
+ const color = b.color ?? CHEM_BOND_STATE_COLOR[state];
23133
+ const dash = state === "forming" || state === "breaking" ? "dashed" : void 0;
23134
+ if (bondStyle === "parallel") {
23135
+ const dx = c.x - a.x;
23136
+ const dy = c.y - a.y;
23137
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
23138
+ const ux = dx / dist;
23139
+ const uy = dy / dist;
23140
+ const px = -uy;
23141
+ const py = ux;
23142
+ const offsets = b.type === "double" ? [-3, 3] : b.type === "triple" ? [-4, 0, 4] : [0];
23143
+ for (const off of offsets) {
23144
+ out.push({
23145
+ type: "line",
23146
+ x1: a.x + px * off,
23147
+ y1: a.y + py * off,
23148
+ x2: c.x + px * off,
23149
+ y2: c.y + py * off,
23150
+ color,
23151
+ lineWidth: 2,
23152
+ ...dash ? { dash } : {}
23153
+ });
23154
+ }
23155
+ } else {
23156
+ const strokeWidth = b.type === "double" ? 4 : b.type === "triple" ? 6 : 2;
23157
+ out.push({
23158
+ type: "line",
23159
+ x1: a.x,
23160
+ y1: a.y,
23161
+ x2: c.x,
23162
+ y2: c.y,
23163
+ color,
23164
+ lineWidth: strokeWidth,
23165
+ ...dash ? { dash } : {}
23166
+ });
23167
+ }
21644
23168
  }
21645
23169
  for (const a of arrows) {
21646
23170
  const angle = (a.angle ?? 0) * (Math.PI / 180);
@@ -21689,15 +23213,62 @@ var init_ChemistryCanvas = __esm({
21689
23213
  align: "center"
21690
23214
  });
21691
23215
  }
23216
+ const r = a.radius ?? 14;
23217
+ if (a.charge) {
23218
+ out.push({
23219
+ type: "text",
23220
+ x: a.x + r * 0.85,
23221
+ y: a.y - r * 0.85,
23222
+ text: a.charge,
23223
+ color: "#111827",
23224
+ fontSize: 9,
23225
+ align: "left"
23226
+ });
23227
+ }
23228
+ const lonePairs = Math.max(0, Math.min(4, a.lonePairs ?? 0));
23229
+ for (let k = 0; k < lonePairs; k++) {
23230
+ const angleRad = LONE_PAIR_ANGLES[k] * Math.PI / 180;
23231
+ const cx = a.x + (r + 6) * Math.cos(angleRad);
23232
+ const cy = a.y + (r + 6) * Math.sin(angleRad);
23233
+ const perpX = -Math.sin(angleRad);
23234
+ const perpY = Math.cos(angleRad);
23235
+ for (const sign of [1, -1]) {
23236
+ out.push({
23237
+ type: "circle",
23238
+ x: cx + perpX * 2.5 * sign,
23239
+ y: cy + perpY * 2.5 * sign,
23240
+ radius: 1.5,
23241
+ color: "#374151",
23242
+ fill: "#374151"
23243
+ });
23244
+ }
23245
+ }
23246
+ }
23247
+ if (equation) {
23248
+ out.push({
23249
+ type: "text",
23250
+ x: width / 2,
23251
+ y: 14,
23252
+ text: equation,
23253
+ color: equationColor ?? "#111827",
23254
+ fontSize: 13,
23255
+ align: "center"
23256
+ });
21692
23257
  }
21693
23258
  out.push(...shapes);
21694
23259
  return out;
21695
- }, [atoms, bonds, arrows, shapes]);
23260
+ }, [atoms, bonds, arrows, bondStyle, containers, equation, equationColor, shapes, width]);
21696
23261
  const drawables3D = useMemo(() => {
21697
23262
  if (mode !== "3d") return [];
21698
23263
  if (shapes.length > 0) {
21699
23264
  chemistryLog.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
21700
23265
  }
23266
+ if (containers.length > 0) {
23267
+ chemistryLog.debug("containers ignored in 3D mode (pixel-authored 2D vocabulary)", { count: containers.length });
23268
+ }
23269
+ if (animate) {
23270
+ chemistryLog.debug("animate ignored in 3D mode (motion is entity-state driven)");
23271
+ }
21701
23272
  const out = [];
21702
23273
  const labelColor = labelColorForBackground(backgroundColor);
21703
23274
  const atomById = /* @__PURE__ */ new Map();
@@ -21744,8 +23315,11 @@ var init_ChemistryCanvas = __esm({
21744
23315
  out.push(billboardLabel(a.element, a.x, a.y, az + radius, { color: labelColor }));
21745
23316
  }
21746
23317
  }
23318
+ if (lattice3d) {
23319
+ out.push(...latticeDrawables(lattice3d, { labelColor }));
23320
+ }
21747
23321
  return out;
21748
- }, [mode, atoms, bonds, arrows, shapes, backgroundColor]);
23322
+ }, [mode, atoms, bonds, arrows, shapes, containers, lattice3d, animate, backgroundColor]);
21749
23323
  const atomIndexById = useMemo(() => {
21750
23324
  const m = /* @__PURE__ */ new Map();
21751
23325
  atoms.forEach((a, i) => {
@@ -21784,6 +23358,8 @@ var init_ChemistryCanvas = __esm({
21784
23358
  height,
21785
23359
  backgroundColor,
21786
23360
  shapes: derivedShapes,
23361
+ readouts,
23362
+ traces,
21787
23363
  interactive: interactive ?? false,
21788
23364
  animate,
21789
23365
  onShapeClick,
@@ -27895,6 +29471,10 @@ var init_ProgressDots = __esm({
27895
29471
  ProgressDots.displayName = "ProgressDots";
27896
29472
  }
27897
29473
  });
29474
+ function formatTick(v) {
29475
+ if (Number.isInteger(v)) return String(v);
29476
+ return v.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
29477
+ }
27898
29478
  var MathCanvas;
27899
29479
  var init_MathCanvas = __esm({
27900
29480
  "components/learning/molecules/MathCanvas.tsx"() {
@@ -27914,10 +29494,19 @@ var init_MathCanvas = __esm({
27914
29494
  showAxes = true,
27915
29495
  showGrid = true,
27916
29496
  gridStep = 1,
29497
+ showTickLabels = false,
29498
+ showCurveLabels = false,
27917
29499
  curves = [],
27918
29500
  points = [],
27919
29501
  vectors = [],
29502
+ regions = [],
29503
+ bars = [],
29504
+ guides = [],
29505
+ angles = [],
29506
+ hops = [],
27920
29507
  shapes = [],
29508
+ readouts,
29509
+ traces,
27921
29510
  interactive = false,
27922
29511
  animate = false,
27923
29512
  onShapeClick,
@@ -27931,6 +29520,8 @@ var init_MathCanvas = __esm({
27931
29520
  const plotH = height - margin * 2;
27932
29521
  const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
27933
29522
  const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
29523
+ const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
29524
+ const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
27934
29525
  if (showGrid) {
27935
29526
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep) {
27936
29527
  const px = mapX(x);
@@ -27941,14 +29532,99 @@ var init_MathCanvas = __esm({
27941
29532
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color: "#e5e7eb", lineWidth: 1 });
27942
29533
  }
27943
29534
  }
29535
+ if (showTickLabels) {
29536
+ const labelEveryX = Math.max(1, Math.ceil((xMax - xMin) / gridStep / Math.floor(plotW / 40)));
29537
+ let kx = 0;
29538
+ for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
29539
+ if (kx % labelEveryX === 0 && x !== 0) {
29540
+ out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: 10, align: "center" });
29541
+ }
29542
+ }
29543
+ const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
29544
+ let ky = 0;
29545
+ for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
29546
+ if (ky % labelEveryY === 0 && y !== 0) {
29547
+ out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: 10, align: "right" });
29548
+ }
29549
+ }
29550
+ if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
29551
+ out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: 10, align: "right" });
29552
+ }
29553
+ }
29554
+ for (const region of regions) {
29555
+ if (!region.samples || region.samples.length === 0) continue;
29556
+ const baseline = region.baseline ?? 0;
29557
+ const clampedPoint = (p) => ({
29558
+ x: mapX(Math.min(xMax, Math.max(xMin, p.x))),
29559
+ y: mapY(Math.min(yMax, Math.max(yMin, p.y)))
29560
+ });
29561
+ const upper = region.samples.map(clampedPoint);
29562
+ const first = region.samples[0];
29563
+ const last = region.samples[region.samples.length - 1];
29564
+ const closing = region.samples2 && region.samples2.length > 0 ? [...region.samples2].reverse().map(clampedPoint) : [clampedPoint({ x: last.x, y: baseline }), clampedPoint({ x: first.x, y: baseline })];
29565
+ const color = region.color ?? "#2563eb";
29566
+ out.push({
29567
+ type: "polygon",
29568
+ points: [...upper, ...closing],
29569
+ fill: color,
29570
+ color,
29571
+ opacity: region.opacity ?? 0.2,
29572
+ lineWidth: 1
29573
+ });
29574
+ if (region.label) {
29575
+ const mid = Math.floor(region.samples.length / 2);
29576
+ out.push({
29577
+ type: "text",
29578
+ x: mapX((first.x + last.x) / 2),
29579
+ y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
29580
+ text: region.label,
29581
+ color: "#111827",
29582
+ fontSize: 11
29583
+ });
29584
+ }
29585
+ }
29586
+ for (const bar of bars) {
29587
+ if (bar.x + bar.width < xMin || bar.x > xMax) continue;
29588
+ const y0 = bar.y0 ?? 0;
29589
+ const color = bar.color ?? "#93c5fd";
29590
+ out.push({
29591
+ type: "rect",
29592
+ x: mapX(bar.x),
29593
+ y: mapY(Math.max(y0, bar.y1)),
29594
+ width: mapX(bar.x + bar.width) - mapX(bar.x),
29595
+ height: Math.abs(mapY(bar.y1) - mapY(y0)),
29596
+ color,
29597
+ fill: color,
29598
+ opacity: bar.opacity ?? 0.5,
29599
+ lineWidth: 1
29600
+ });
29601
+ }
27944
29602
  if (showAxes) {
27945
- const xAxisY = Math.max(margin, Math.min(height - margin, mapY(0)));
27946
- const yAxisX = Math.max(margin, Math.min(width - margin, mapX(0)));
27947
29603
  out.push({ type: "line", x1: margin, y1: xAxisY, x2: width - margin, y2: xAxisY, color: "#374151", lineWidth: 2 });
27948
29604
  out.push({ type: "line", x1: yAxisX, y1: margin, x2: yAxisX, y2: height - margin, color: "#374151", lineWidth: 2 });
27949
29605
  }
29606
+ for (const guide of guides) {
29607
+ const color = guide.color ?? "#9ca3af";
29608
+ const dash = guide.dash ?? "dashed";
29609
+ if (guide.kind === "vline") {
29610
+ if (guide.at < xMin || guide.at > xMax) continue;
29611
+ const px = mapX(guide.at);
29612
+ out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
29613
+ if (guide.label) {
29614
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
29615
+ }
29616
+ } else {
29617
+ if (guide.at < yMin || guide.at > yMax) continue;
29618
+ const py = mapY(guide.at);
29619
+ out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
29620
+ if (guide.label) {
29621
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
29622
+ }
29623
+ }
29624
+ }
27950
29625
  for (const curve of curves) {
27951
29626
  if (!curve.samples || curve.samples.length < 2) continue;
29627
+ let lastInRange;
27952
29628
  for (let i = 1; i < curve.samples.length; i++) {
27953
29629
  const a = curve.samples[i - 1];
27954
29630
  const b = curve.samples[i];
@@ -27960,19 +29636,97 @@ var init_MathCanvas = __esm({
27960
29636
  x2: mapX(b.x),
27961
29637
  y2: mapY(b.y),
27962
29638
  color: curve.color ?? "#2563eb",
27963
- lineWidth: 2
29639
+ lineWidth: 2,
29640
+ dash: curve.dash
29641
+ });
29642
+ lastInRange = b;
29643
+ }
29644
+ if (showCurveLabels && curve.label && lastInRange) {
29645
+ out.push({
29646
+ type: "text",
29647
+ x: mapX(lastInRange.x) + 6,
29648
+ y: mapY(lastInRange.y) - 6,
29649
+ text: curve.label,
29650
+ color: curve.color ?? "#2563eb",
29651
+ fontSize: 11
29652
+ });
29653
+ }
29654
+ }
29655
+ for (const hop of hops) {
29656
+ const x1 = mapX(hop.from);
29657
+ const x2 = mapX(hop.to);
29658
+ const peak = Math.min(36, plotH * 0.3);
29659
+ const color = hop.color ?? "#7c3aed";
29660
+ out.push({
29661
+ type: "ellipse",
29662
+ x: (x1 + x2) / 2,
29663
+ y: xAxisY,
29664
+ width: Math.abs(x2 - x1),
29665
+ height: 2 * peak,
29666
+ startAngle: 180,
29667
+ endAngle: 360,
29668
+ color
29669
+ });
29670
+ const s = Math.sign(hop.to - hop.from);
29671
+ out.push({
29672
+ type: "polygon",
29673
+ points: [
29674
+ { x: x2, y: xAxisY },
29675
+ { x: x2 - 4 * s, y: xAxisY - 7 },
29676
+ { x: x2 + 2 * s, y: xAxisY - 7 }
29677
+ ],
29678
+ fill: color,
29679
+ color
29680
+ });
29681
+ if (hop.label) {
29682
+ out.push({
29683
+ type: "text",
29684
+ x: (x1 + x2) / 2,
29685
+ y: xAxisY - peak - 8,
29686
+ text: hop.label,
29687
+ color: "#111827",
29688
+ fontSize: 10,
29689
+ align: "center"
29690
+ });
29691
+ }
29692
+ }
29693
+ for (const angle of angles) {
29694
+ const radius = angle.radius ?? 0.8;
29695
+ const color = angle.color ?? "#0ea5e9";
29696
+ out.push({
29697
+ type: "ellipse",
29698
+ x: mapX(angle.x),
29699
+ y: mapY(angle.y),
29700
+ width: 2 * radius * plotW / (xMax - xMin),
29701
+ height: 2 * radius * plotH / (yMax - yMin),
29702
+ startAngle: -angle.to,
29703
+ endAngle: -angle.from,
29704
+ color
29705
+ });
29706
+ if (angle.label) {
29707
+ const mid = (angle.from + angle.to) / 2;
29708
+ const rad = mid * Math.PI / 180;
29709
+ out.push({
29710
+ type: "text",
29711
+ x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
29712
+ y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
29713
+ text: angle.label,
29714
+ color: "#111827",
29715
+ fontSize: 11,
29716
+ align: "center"
27964
29717
  });
27965
29718
  }
27966
29719
  }
27967
29720
  for (const p of points) {
27968
29721
  if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue;
29722
+ const isOpen = p.style === "open";
27969
29723
  out.push({
27970
29724
  type: "circle",
27971
29725
  x: mapX(p.x),
27972
29726
  y: mapY(p.y),
27973
29727
  radius: p.radius ?? 4,
27974
29728
  color: p.color ?? "#dc2626",
27975
- fill: p.color ?? "#dc2626"
29729
+ fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
27976
29730
  });
27977
29731
  if (p.label) {
27978
29732
  out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
@@ -27991,7 +29745,28 @@ var init_MathCanvas = __esm({
27991
29745
  }
27992
29746
  out.push(...shapes);
27993
29747
  return out;
27994
- }, [width, height, xMin, xMax, yMin, yMax, showAxes, showGrid, gridStep, curves, points, vectors, shapes]);
29748
+ }, [
29749
+ width,
29750
+ height,
29751
+ xMin,
29752
+ xMax,
29753
+ yMin,
29754
+ yMax,
29755
+ showAxes,
29756
+ showGrid,
29757
+ gridStep,
29758
+ showTickLabels,
29759
+ showCurveLabels,
29760
+ curves,
29761
+ points,
29762
+ vectors,
29763
+ regions,
29764
+ bars,
29765
+ guides,
29766
+ angles,
29767
+ hops,
29768
+ shapes
29769
+ ]);
27995
29770
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
27996
29771
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
27997
29772
  /* @__PURE__ */ jsx(
@@ -28000,6 +29775,8 @@ var init_MathCanvas = __esm({
28000
29775
  width,
28001
29776
  height,
28002
29777
  shapes: derivedShapes,
29778
+ readouts,
29779
+ traces,
28003
29780
  interactive,
28004
29781
  animate,
28005
29782
  onShapeClick,
@@ -28011,7 +29788,315 @@ var init_MathCanvas = __esm({
28011
29788
  };
28012
29789
  }
28013
29790
  });
28014
- var physicsLog2, PhysicsCanvas;
29791
+ function formatMeterValue(v) {
29792
+ return Number.isInteger(v) ? String(v) : String(Number(v.toFixed(2)));
29793
+ }
29794
+ function sceneObjectShapes(obj, canvasWidth, canvasHeight) {
29795
+ const out = [];
29796
+ const color = obj.color ?? "#334155";
29797
+ switch (obj.kind) {
29798
+ case "ground": {
29799
+ const xStart = obj.x1 ?? 0;
29800
+ const xEnd = obj.x2 ?? canvasWidth;
29801
+ const y = obj.y ?? 0;
29802
+ out.push({ type: "line", x1: xStart, y1: y, x2: xEnd, y2: y, color, lineWidth: 2 });
29803
+ for (let hx = xStart + 7; hx <= xEnd; hx += 14) {
29804
+ out.push({ type: "line", x1: hx, y1: y, x2: hx - 7, y2: y + 7, color, lineWidth: 1 });
29805
+ }
29806
+ if (obj.label) {
29807
+ out.push({
29808
+ type: "text",
29809
+ x: (xStart + xEnd) / 2,
29810
+ y: y - 10,
29811
+ text: obj.label,
29812
+ color: PHYSICS_LABEL_COLOR,
29813
+ fontSize: 11,
29814
+ align: "center"
29815
+ });
29816
+ }
29817
+ break;
29818
+ }
29819
+ case "wall": {
29820
+ const yStart = obj.y1 ?? 0;
29821
+ const yEnd = obj.y2 ?? canvasHeight;
29822
+ const x = obj.x ?? 0;
29823
+ out.push({ type: "line", x1: x, y1: yStart, x2: x, y2: yEnd, color, lineWidth: 2 });
29824
+ for (let hy = yStart + 7; hy <= yEnd; hy += 14) {
29825
+ out.push({ type: "line", x1: x, y1: hy, x2: x - 7, y2: hy + 7, color, lineWidth: 1 });
29826
+ }
29827
+ if (obj.label) {
29828
+ out.push({
29829
+ type: "text",
29830
+ x: x + 12,
29831
+ y: (yStart + yEnd) / 2,
29832
+ text: obj.label,
29833
+ color: PHYSICS_LABEL_COLOR,
29834
+ fontSize: 11,
29835
+ align: "left"
29836
+ });
29837
+ }
29838
+ break;
29839
+ }
29840
+ case "ramp": {
29841
+ const x1 = obj.x1 ?? 0;
29842
+ const y1 = obj.y1 ?? 0;
29843
+ const x2 = obj.x2 ?? canvasWidth;
29844
+ const y2 = obj.y2 ?? canvasHeight;
29845
+ out.push({
29846
+ type: "polygon",
29847
+ points: [
29848
+ { x: x1, y: y1 },
29849
+ { x: x2, y: y2 },
29850
+ { x: x1, y: y2 }
29851
+ ],
29852
+ color,
29853
+ fill: obj.fill ?? "#e2e8f0",
29854
+ lineWidth: 2
29855
+ });
29856
+ if (obj.label) {
29857
+ out.push({
29858
+ type: "text",
29859
+ x: (2 * x1 + x2) / 3,
29860
+ y: (y1 + 2 * y2) / 3,
29861
+ text: obj.label,
29862
+ color: PHYSICS_LABEL_COLOR,
29863
+ fontSize: 11,
29864
+ align: "center"
29865
+ });
29866
+ }
29867
+ break;
29868
+ }
29869
+ case "box": {
29870
+ const x = obj.x ?? 0;
29871
+ const y = obj.y ?? 0;
29872
+ const w = obj.width ?? 40;
29873
+ const h = obj.height ?? 40;
29874
+ out.push({ type: "rect", x, y, width: w, height: h, color, fill: obj.fill, lineWidth: 2 });
29875
+ if (obj.label) {
29876
+ out.push({
29877
+ type: "text",
29878
+ x: x + w / 2,
29879
+ y: y + h / 2,
29880
+ text: obj.label,
29881
+ color: PHYSICS_LABEL_COLOR,
29882
+ fontSize: 11,
29883
+ align: "center"
29884
+ });
29885
+ }
29886
+ break;
29887
+ }
29888
+ case "pivot": {
29889
+ const x = obj.x ?? 0;
29890
+ const y = obj.y ?? 0;
29891
+ out.push({ type: "circle", x, y, radius: 5, color, fill: color });
29892
+ out.push({ type: "line", x1: x - 14, y1: y - 8, x2: x + 14, y2: y - 8, color, lineWidth: 1 });
29893
+ for (let k = 0; k < 5; k++) {
29894
+ const hx = x - 14 + 7 * k;
29895
+ out.push({ type: "line", x1: hx, y1: y - 8, x2: hx - 6, y2: y - 14, color, lineWidth: 1 });
29896
+ }
29897
+ if (obj.label) {
29898
+ out.push({
29899
+ type: "text",
29900
+ x,
29901
+ y: y - 20,
29902
+ text: obj.label,
29903
+ color: PHYSICS_LABEL_COLOR,
29904
+ fontSize: 11,
29905
+ align: "center"
29906
+ });
29907
+ }
29908
+ break;
29909
+ }
29910
+ }
29911
+ return out;
29912
+ }
29913
+ function trailShapes(trail) {
29914
+ const n = trail.points.length;
29915
+ if (n < 2) return [];
29916
+ const color = trail.color ?? "#94a3b8";
29917
+ const lineWidth = trail.width ?? 2;
29918
+ const fade = trail.fade ?? true;
29919
+ const globalOpacity = trail.opacity ?? 1;
29920
+ const out = [];
29921
+ for (let i = 0; i < n - 1; i++) {
29922
+ const a = trail.points[i];
29923
+ const b = trail.points[i + 1];
29924
+ const segmentOpacity = fade ? 0.12 + 0.68 * i / (n - 1) : 0.6;
29925
+ out.push({
29926
+ type: "line",
29927
+ x1: a.x,
29928
+ y1: a.y,
29929
+ x2: b.x,
29930
+ y2: b.y,
29931
+ color,
29932
+ lineWidth,
29933
+ opacity: segmentOpacity * globalOpacity
29934
+ });
29935
+ }
29936
+ return out;
29937
+ }
29938
+ function constraintShapes(c, a, b) {
29939
+ const color = c.color ?? "#9ca3af";
29940
+ const kind = c.kind ?? "rod";
29941
+ if (kind === "rod") {
29942
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2 }];
29943
+ }
29944
+ if (kind === "string") {
29945
+ return [{ type: "line", x1: a.x, y1: a.y, x2: b.x, y2: b.y, color, lineWidth: 2, dash: "dashed" }];
29946
+ }
29947
+ const COILS = 8;
29948
+ const AMP = 7;
29949
+ const LEAD = 10;
29950
+ const dx = b.x - a.x;
29951
+ const dy = b.y - a.y;
29952
+ const dist = Math.max(1e-6, Math.hypot(dx, dy));
29953
+ const ux = dx / dist;
29954
+ const uy = dy / dist;
29955
+ const perpX = -uy;
29956
+ const perpY = ux;
29957
+ const aPrime = { x: a.x + LEAD * ux, y: a.y + LEAD * uy };
29958
+ const bPrime = { x: b.x - LEAD * ux, y: b.y - LEAD * uy };
29959
+ const m = 2 * COILS;
29960
+ const polyline = [{ x: a.x, y: a.y }, aPrime];
29961
+ for (let j = 1; j <= m; j++) {
29962
+ const t = j / (m + 1);
29963
+ const baseX = aPrime.x + t * (bPrime.x - aPrime.x);
29964
+ const baseY = aPrime.y + t * (bPrime.y - aPrime.y);
29965
+ const sign = j % 2 === 0 ? 1 : -1;
29966
+ polyline.push({ x: baseX + sign * AMP * perpX, y: baseY + sign * AMP * perpY });
29967
+ }
29968
+ polyline.push(bPrime, { x: b.x, y: b.y });
29969
+ const out = [];
29970
+ for (let i = 1; i < polyline.length; i++) {
29971
+ out.push({
29972
+ type: "line",
29973
+ x1: polyline[i - 1].x,
29974
+ y1: polyline[i - 1].y,
29975
+ x2: polyline[i].x,
29976
+ y2: polyline[i].y,
29977
+ color,
29978
+ lineWidth: 2
29979
+ });
29980
+ }
29981
+ return out;
29982
+ }
29983
+ function vectorShapes(v, bodyById) {
29984
+ let ax;
29985
+ let ay;
29986
+ if (v.body) {
29987
+ const anchor = bodyById.get(v.body);
29988
+ if (!anchor) return [];
29989
+ ax = anchor.x;
29990
+ ay = anchor.y;
29991
+ } else {
29992
+ ax = v.x ?? 0;
29993
+ ay = v.y ?? 0;
29994
+ }
29995
+ const scale = v.scale ?? 1;
29996
+ const color = v.color ?? "#dc2626";
29997
+ const tx = ax + v.dx * scale;
29998
+ const ty = ay + v.dy * scale;
29999
+ const out = [{ type: "arrow", x1: ax, y1: ay, x2: tx, y2: ty, color, lineWidth: 2, dash: v.dash }];
30000
+ if (v.label) {
30001
+ const dist = Math.max(1e-6, Math.hypot(tx - ax, ty - ay));
30002
+ const ux = (tx - ax) / dist;
30003
+ const uy = (ty - ay) / dist;
30004
+ out.push({
30005
+ type: "text",
30006
+ x: tx + 8 * ux,
30007
+ y: ty + 8 * uy,
30008
+ text: v.label,
30009
+ color,
30010
+ fontSize: 11,
30011
+ align: "center"
30012
+ });
30013
+ }
30014
+ return out;
30015
+ }
30016
+ function angleMarkerShapes(a) {
30017
+ const radius = a.radius ?? 26;
30018
+ const color = a.color ?? "#0ea5e9";
30019
+ const out = [
30020
+ {
30021
+ type: "ellipse",
30022
+ x: a.x,
30023
+ y: a.y,
30024
+ width: radius * 2,
30025
+ height: radius * 2,
30026
+ startAngle: a.from,
30027
+ endAngle: a.to,
30028
+ color,
30029
+ lineWidth: 2
30030
+ }
30031
+ ];
30032
+ if (a.label) {
30033
+ const mid = (a.from + a.to) / 2 * (Math.PI / 180);
30034
+ out.push({
30035
+ type: "text",
30036
+ x: a.x + (radius + 13) * Math.cos(mid),
30037
+ y: a.y + (radius + 13) * Math.sin(mid),
30038
+ text: a.label,
30039
+ color,
30040
+ fontSize: 11,
30041
+ align: "center"
30042
+ });
30043
+ }
30044
+ return out;
30045
+ }
30046
+ function fieldShapes(field, canvasWidth, canvasHeight) {
30047
+ const spacing = field.spacing ?? 48;
30048
+ const size = field.size ?? 14;
30049
+ const color = field.color ?? "#94a3b8";
30050
+ const regionX = field.x ?? 0;
30051
+ const regionY = field.y ?? 0;
30052
+ const regionW = field.width ?? canvasWidth;
30053
+ const regionH = field.height ?? canvasHeight;
30054
+ const out = [];
30055
+ for (let gx = regionX + spacing / 2; gx < regionX + regionW; gx += spacing) {
30056
+ for (let gy = regionY + spacing / 2; gy < regionY + regionH; gy += spacing) {
30057
+ if (field.kind === "arrows") {
30058
+ const rad = (field.angle ?? 0) * Math.PI / 180;
30059
+ const hx = Math.cos(rad) * size / 2;
30060
+ const hy = Math.sin(rad) * size / 2;
30061
+ out.push({ type: "arrow", x1: gx - hx, y1: gy - hy, x2: gx + hx, y2: gy + hy, color, lineWidth: 2 });
30062
+ } else if (field.kind === "into") {
30063
+ const r = size / 3;
30064
+ const d = 0.6 * r * Math.SQRT1_2;
30065
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30066
+ out.push({ type: "line", x1: gx - d, y1: gy - d, x2: gx + d, y2: gy + d, color, lineWidth: 1 });
30067
+ out.push({ type: "line", x1: gx - d, y1: gy + d, x2: gx + d, y2: gy - d, color, lineWidth: 1 });
30068
+ } else {
30069
+ const r = size / 3;
30070
+ out.push({ type: "circle", x: gx, y: gy, radius: r, color });
30071
+ out.push({ type: "circle", x: gx, y: gy, radius: 1.5, color, fill: color });
30072
+ }
30073
+ }
30074
+ }
30075
+ return out;
30076
+ }
30077
+ function meterShapes(meters, canvasHeight) {
30078
+ const n = meters.length;
30079
+ const out = [];
30080
+ const sharedMax = Math.max(1e-6, ...meters.map((m) => m.value));
30081
+ meters.forEach((meter, i) => {
30082
+ const rowY = canvasHeight - 10 - 16 * (n - i);
30083
+ const color = meter.color ?? "#3b82f6";
30084
+ const M = meter.max ?? sharedMax;
30085
+ const w = Math.round(Math.min(1, Math.max(0, meter.value / M)) * 110);
30086
+ out.push({ type: "text", x: 8, y: rowY + 8, text: meter.label, color: PHYSICS_LABEL_COLOR, fontSize: 10 });
30087
+ out.push({ type: "rect", x: 52, y: rowY, width: w, height: 10, color, fill: color });
30088
+ out.push({
30089
+ type: "text",
30090
+ x: 166,
30091
+ y: rowY + 8,
30092
+ text: formatMeterValue(meter.value),
30093
+ color: "#6b7280",
30094
+ fontSize: 9
30095
+ });
30096
+ });
30097
+ return out;
30098
+ }
30099
+ var physicsLog2, PHYSICS_LABEL_COLOR, PhysicsCanvas;
28015
30100
  var init_PhysicsCanvas = __esm({
28016
30101
  "components/learning/molecules/PhysicsCanvas.tsx"() {
28017
30102
  "use client";
@@ -28020,6 +30105,7 @@ var init_PhysicsCanvas = __esm({
28020
30105
  init_LearningCanvas();
28021
30106
  init_learningScene3D();
28022
30107
  physicsLog2 = createLogger("almadar:ui:physics-canvas");
30108
+ PHYSICS_LABEL_COLOR = "#374151";
28023
30109
  PhysicsCanvas = ({
28024
30110
  className,
28025
30111
  width = 600,
@@ -28036,7 +30122,18 @@ var init_PhysicsCanvas = __esm({
28036
30122
  showForces = false,
28037
30123
  velocityScale = 20,
28038
30124
  forceScale = 20,
30125
+ sceneObjects = [],
30126
+ trails = [],
30127
+ vectors = [],
30128
+ surface3d,
30129
+ vectors3d = [],
30130
+ vectorScale = 1,
30131
+ angles = [],
30132
+ field,
30133
+ meters = [],
28039
30134
  shapes = [],
30135
+ readouts,
30136
+ traces,
28040
30137
  showGrid,
28041
30138
  shadows,
28042
30139
  interactive,
@@ -28051,19 +30148,14 @@ var init_PhysicsCanvas = __esm({
28051
30148
  for (const b of bodies) {
28052
30149
  if (b.id) bodyById.set(b.id, b);
28053
30150
  }
30151
+ if (field) out.push(...fieldShapes(field, width, height));
30152
+ for (const obj of sceneObjects) out.push(...sceneObjectShapes(obj, width, height));
30153
+ for (const trail of trails) out.push(...trailShapes(trail));
28054
30154
  for (const c of constraints) {
28055
30155
  const a = bodyById.get(c.from);
28056
30156
  const b = bodyById.get(c.to);
28057
30157
  if (!a || !b) continue;
28058
- out.push({
28059
- type: "line",
28060
- x1: a.x,
28061
- y1: a.y,
28062
- x2: b.x,
28063
- y2: b.y,
28064
- color: c.color ?? "#9ca3af",
28065
- lineWidth: 2
28066
- });
30158
+ out.push(...constraintShapes(c, a, b));
28067
30159
  }
28068
30160
  for (const b of bodies) {
28069
30161
  out.push({
@@ -28108,14 +30200,51 @@ var init_PhysicsCanvas = __esm({
28108
30200
  });
28109
30201
  }
28110
30202
  }
30203
+ for (const v of vectors) out.push(...vectorShapes(v, bodyById));
30204
+ for (const a of angles) out.push(...angleMarkerShapes(a));
30205
+ if (meters.length > 0) out.push(...meterShapes(meters, height));
28111
30206
  out.push(...shapes);
28112
30207
  return out;
28113
- }, [bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes]);
30208
+ }, [
30209
+ bodies,
30210
+ constraints,
30211
+ showVelocity,
30212
+ showForces,
30213
+ velocityScale,
30214
+ forceScale,
30215
+ sceneObjects,
30216
+ trails,
30217
+ vectors,
30218
+ angles,
30219
+ field,
30220
+ meters,
30221
+ shapes,
30222
+ width,
30223
+ height
30224
+ ]);
28114
30225
  const drawables3D = useMemo(() => {
28115
30226
  if (mode !== "3d") return [];
28116
30227
  if (shapes.length > 0) {
28117
30228
  physicsLog2.debug("shapes ignored in 3D mode (pixel-authored 2D vocabulary)", { count: shapes.length });
28118
30229
  }
30230
+ if (sceneObjects.length > 0) {
30231
+ physicsLog2.debug("sceneObjects ignored in 3D mode (pixel-authored 2D vocabulary)", { count: sceneObjects.length });
30232
+ }
30233
+ if (vectors.length > 0) {
30234
+ physicsLog2.debug("vectors ignored in 3D mode (pixel-authored 2D vocabulary)", { count: vectors.length });
30235
+ }
30236
+ if (angles.length > 0) {
30237
+ physicsLog2.debug("angles ignored in 3D mode (pixel-authored 2D vocabulary)", { count: angles.length });
30238
+ }
30239
+ if (field) {
30240
+ physicsLog2.debug("field ignored in 3D mode (pixel-authored 2D vocabulary)");
30241
+ }
30242
+ if (meters.length > 0) {
30243
+ physicsLog2.debug("meters ignored in 3D mode (pixel-authored 2D vocabulary)", { count: meters.length });
30244
+ }
30245
+ if (animate) {
30246
+ physicsLog2.debug("animate ignored in 3D mode (motion is entity-state driven)");
30247
+ }
28119
30248
  const out = [];
28120
30249
  const labelColor = labelColorForBackground(backgroundColor);
28121
30250
  const bodyById = /* @__PURE__ */ new Map();
@@ -28163,15 +30292,67 @@ var init_PhysicsCanvas = __esm({
28163
30292
  if (arrow) out.push(arrow);
28164
30293
  }
28165
30294
  }
30295
+ for (const trail of trails) {
30296
+ if (trail.fade !== void 0) {
30297
+ physicsLog2.debug("trail.fade ignored in 3D mode (2D-only fade curve \u2014 3D draws an opaque tube)", { id: trail.id });
30298
+ }
30299
+ const points = trail.points.map((p) => [p.x, p.y, p.z ?? 0]);
30300
+ out.push(
30301
+ ...polylineTube(points, trail.width ?? 0.05, trail.color ?? "#94a3b8", {
30302
+ ...trail.opacity !== void 0 ? { opacity: trail.opacity } : {}
30303
+ })
30304
+ );
30305
+ }
30306
+ if (surface3d) {
30307
+ out.push(...heightFieldMesh(surface3d));
30308
+ }
30309
+ if (vectors3d.length > 0) {
30310
+ out.push(
30311
+ ...arrowField(
30312
+ vectors3d.map((v) => ({
30313
+ id: v.id,
30314
+ from: [v.x, v.y, v.z ?? 0],
30315
+ delta: [v.dx, v.dy, v.dz ?? 0],
30316
+ color: v.color,
30317
+ label: v.label,
30318
+ width: v.width
30319
+ })),
30320
+ { scale: vectorScale, labelColor }
30321
+ )
30322
+ );
30323
+ }
28166
30324
  return out;
28167
- }, [mode, bodies, constraints, showVelocity, showForces, velocityScale, forceScale, shapes, backgroundColor]);
30325
+ }, [
30326
+ mode,
30327
+ bodies,
30328
+ constraints,
30329
+ showVelocity,
30330
+ showForces,
30331
+ velocityScale,
30332
+ forceScale,
30333
+ shapes,
30334
+ sceneObjects,
30335
+ trails,
30336
+ vectors,
30337
+ surface3d,
30338
+ vectors3d,
30339
+ vectorScale,
30340
+ angles,
30341
+ field,
30342
+ meters,
30343
+ animate,
30344
+ backgroundColor
30345
+ ]);
28168
30346
  const bodyIndexById = useMemo(() => {
28169
30347
  const m = /* @__PURE__ */ new Map();
30348
+ vectors3d.forEach((v, i) => {
30349
+ if (v.id) m.set(v.id, i);
30350
+ });
28170
30351
  bodies.forEach((b, i) => {
28171
30352
  if (b.id) m.set(b.id, i);
28172
30353
  });
28173
30354
  return m;
28174
- }, [bodies]);
30355
+ }, [bodies, vectors3d]);
28175
30356
  if (mode === "3d") {
28176
30357
  return /* @__PURE__ */ jsx(
28177
30358
  LearningScene3D,
@@ -28203,6 +30384,8 @@ var init_PhysicsCanvas = __esm({
28203
30384
  height,
28204
30385
  backgroundColor,
28205
30386
  shapes: derivedShapes,
30387
+ readouts,
30388
+ traces,
28206
30389
  interactive: interactive ?? false,
28207
30390
  animate,
28208
30391
  onShapeClick,
@@ -28353,7 +30536,7 @@ function layoutFlow(nodeIds, adjacency, roots, width, height, margin) {
28353
30536
  }
28354
30537
  return nodeIds.map((id) => positions.get(id));
28355
30538
  }
28356
- function layoutTree(nodeIds, adjacency, roots, width, height, margin) {
30539
+ function layoutTree2(nodeIds, adjacency, roots, width, height, margin) {
28357
30540
  const effectiveRoots = roots.length > 0 ? roots : [nodeIds[0]];
28358
30541
  const layers = assignLayers(nodeIds, adjacency, effectiveRoots);
28359
30542
  const maxLayer = Math.max(...Array.from(layers.values()));
@@ -28409,7 +30592,7 @@ function computeStaticLayout(mode, input) {
28409
30592
  const adjacency = buildAdjacency(nodeIds, edges);
28410
30593
  const roots = findRoots(nodeIds, adjacency);
28411
30594
  if (mode === "flow") return layoutFlow(nodeIds, adjacency, roots, width, height, margin);
28412
- if (mode === "tree") return layoutTree(nodeIds, adjacency, roots, width, height, margin);
30595
+ if (mode === "tree") return layoutTree2(nodeIds, adjacency, roots, width, height, margin);
28413
30596
  return layoutRadial(nodeIds, adjacency, roots, width, height, margin);
28414
30597
  }
28415
30598
  var init_graphViewLayouts = __esm({
@@ -29690,8 +31873,8 @@ function TableView({
29690
31873
  columns,
29691
31874
  fields,
29692
31875
  itemActions,
29693
- maxInlineActions,
29694
- itemClickEvent,
31876
+ maxInlineActions: _maxInlineActions,
31877
+ itemClickEvent = "",
29695
31878
  selectable = false,
29696
31879
  selectEvent,
29697
31880
  selectedIds,
@@ -29741,7 +31924,6 @@ function TableView({
29741
31924
  const hasMore = pageSize > 0 && visibleCount < ordered2.length;
29742
31925
  const hasRenderProp = typeof children === "function";
29743
31926
  const idField = dndItemIdField ?? "id";
29744
- const isCoarsePointer = useMediaQuery("(pointer: coarse)");
29745
31927
  React85__default.useEffect(() => {
29746
31928
  tableViewLog.debug("render", {
29747
31929
  rowCount: data.length,
@@ -29783,21 +31965,14 @@ function TableView({
29783
31965
  const dir = sortColumn === (col.field ?? col.key) && sortDirection === "asc" ? "desc" : "asc";
29784
31966
  eventBus.emit(`UI:${sortEvent}`, { column: col.field ?? col.key, direction: dir });
29785
31967
  };
29786
- const handleActionClick = (action, row) => (e) => {
29787
- e.stopPropagation();
29788
- const payload = {
29789
- id: row.id,
29790
- row
29791
- };
29792
- eventBus.emit(`UI:${action.event}`, payload);
29793
- };
31968
+ const rowClickEvent = itemClickEvent || actionDefs.find((a) => a.variant !== "danger")?.event;
29794
31969
  const handleRowClick = (row) => () => {
29795
- if (!itemClickEvent) return;
31970
+ if (!rowClickEvent) return;
29796
31971
  const payload = {
29797
31972
  id: row.id,
29798
31973
  row
29799
31974
  };
29800
- eventBus.emit(`UI:${itemClickEvent}`, payload);
31975
+ eventBus.emit(`UI:${rowClickEvent}`, payload);
29801
31976
  };
29802
31977
  const colFloors = React85__default.useMemo(
29803
31978
  () => colDefs.map((col) => {
@@ -29813,10 +31988,7 @@ function TableView({
29813
31988
  const statusNode = isLoading ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: t("loading.items") }) }) : error ? /* @__PURE__ */ jsx(Box, { className: "text-center py-8", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "error", children: error.message }) }) : data.length === 0 ? /* @__PURE__ */ jsx(Box, { className: "text-center py-12", children: /* @__PURE__ */ jsx(Typography, { variant: "body", color: "secondary", children: emptyMessage || t("empty.noItems") }) }) : null;
29814
31989
  const lk = LOOKS[look];
29815
31990
  const hasActions = actionDefs.length > 0;
29816
- const effectiveMaxInline = isCoarsePointer ? 0 : maxInlineActions;
29817
- const inlineActionCount = hasActions ? effectiveMaxInline != null ? Math.min(actionDefs.length, effectiveMaxInline) : actionDefs.length : 0;
29818
- const hasOverflowActions = hasActions && effectiveMaxInline != null && actionDefs.length > effectiveMaxInline;
29819
- const actionsTrack = hasActions ? `${inlineActionCount * 6 + (hasOverflowActions ? 3 : 0)}rem` : null;
31991
+ const actionsTrack = hasActions ? "3rem" : null;
29820
31992
  const gridTemplateColumns = [
29821
31993
  selectable ? "auto" : null,
29822
31994
  ...colDefs.map((c, i) => c.width ?? `minmax(${colFloors[i]}ch, 1fr)`),
@@ -29863,7 +32035,7 @@ function TableView({
29863
32035
  col.key
29864
32036
  );
29865
32037
  }),
29866
- hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)]" })
32038
+ hasActions && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, className: "sticky right-0 bg-[var(--color-surface-subtle)] border-l border-[var(--color-border)] h-full" })
29867
32039
  ]
29868
32040
  }
29869
32041
  );
@@ -29875,12 +32047,12 @@ function TableView({
29875
32047
  role: "row",
29876
32048
  "data-entity-row": true,
29877
32049
  "data-entity-id": id,
29878
- onClick: itemClickEvent ? handleRowClick(row) : void 0,
32050
+ onClick: rowClickEvent ? handleRowClick(row) : void 0,
29879
32051
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
29880
32052
  className: cn(
29881
32053
  "group items-center gap-3 transition-colors duration-fast",
29882
32054
  hasRenderProp ? "flex" : "grid",
29883
- itemClickEvent && "cursor-pointer",
32055
+ rowClickEvent && "cursor-pointer",
29884
32056
  lk.rowPad,
29885
32057
  lk.divider && "border-b border-[var(--color-border)]",
29886
32058
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -29888,7 +32060,7 @@ function TableView({
29888
32060
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
29889
32061
  ),
29890
32062
  children: [
29891
- selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
32063
+ selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
29892
32064
  Checkbox,
29893
32065
  {
29894
32066
  checked: selected.has(id),
@@ -29909,53 +32081,37 @@ function TableView({
29909
32081
  }
29910
32082
  return /* @__PURE__ */ jsx(Box, { role: "cell", className: cellBase, children: /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: formatCell(raw, col.format) }) }, col.key);
29911
32083
  }),
29912
- hasActions && /* @__PURE__ */ jsxs(
32084
+ hasActions && /* @__PURE__ */ jsx(
29913
32085
  HStack,
29914
32086
  {
29915
32087
  gap: "xs",
29916
- onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
32088
+ onClick: rowClickEvent ? (e) => e.stopPropagation() : void 0,
29917
32089
  className: cn(
29918
32090
  // Pinned: the fixed column tracks routinely overflow the caller's
29919
- // scroll container, which used to leave the actions off-screen.
29920
- // Opaque so scrolled cells pass underneath it.
32091
+ // scroll container, which would leave the kebab off-screen.
32092
+ // Opaque + hairline edge so it reads as a pinned column, not a
32093
+ // floating control, while scrolled cells pass underneath.
29921
32094
  "justify-end flex-shrink-0 sticky right-0 z-[1] transition-colors",
32095
+ "border-l border-[var(--color-border)]",
29922
32096
  lk.striped && index % 2 === 1 ? "bg-[var(--color-surface-subtle)]" : "bg-[var(--color-card)] group-hover:bg-[var(--color-surface-subtle)]"
29923
32097
  ),
29924
- children: [
29925
- (effectiveMaxInline != null ? actionDefs.slice(0, effectiveMaxInline) : actionDefs).map((action, i) => /* @__PURE__ */ jsxs(
29926
- Button,
29927
- {
29928
- variant: action.variant === "primary" ? "primary" : "ghost",
29929
- size: "sm",
29930
- onClick: handleActionClick(action, row),
29931
- "data-testid": `action-${action.event}`,
29932
- "data-row-id": String(row.id),
29933
- className: cn(action.variant === "danger" && "text-error hover:text-error hover:bg-error/10"),
29934
- children: [
29935
- action.icon && renderIconInput3(action.icon, { size: "xs", className: "mr-1" }),
29936
- action.label
29937
- ]
29938
- },
29939
- i
29940
- )),
29941
- effectiveMaxInline != null && actionDefs.length > effectiveMaxInline && /* @__PURE__ */ jsx(
29942
- Menu,
29943
- {
29944
- position: "bottom-end",
29945
- trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
29946
- items: actionDefs.slice(effectiveMaxInline).map((action) => ({
29947
- label: action.label,
29948
- icon: action.icon,
29949
- event: action.event,
29950
- variant: action.variant === "danger" ? "danger" : "default",
29951
- onClick: () => eventBus.emit(`UI:${action.event}`, {
29952
- id: row.id,
29953
- row
29954
- })
29955
- }))
29956
- }
29957
- )
29958
- ]
32098
+ children: /* @__PURE__ */ jsx(
32099
+ Menu,
32100
+ {
32101
+ position: "bottom-end",
32102
+ trigger: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", "data-row-id": String(row.id), children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
32103
+ items: actionDefs.map((action) => ({
32104
+ label: action.label,
32105
+ icon: action.icon,
32106
+ event: action.event,
32107
+ variant: action.variant === "danger" ? "danger" : "default",
32108
+ onClick: () => eventBus.emit(`UI:${action.event}`, {
32109
+ id: row.id,
32110
+ row
32111
+ })
32112
+ }))
32113
+ }
32114
+ )
29959
32115
  }
29960
32116
  )
29961
32117
  ]
@@ -29998,7 +32154,6 @@ var init_TableView = __esm({
29998
32154
  init_format();
29999
32155
  init_getNestedValue();
30000
32156
  init_useEventBus();
30001
- init_useMediaQuery();
30002
32157
  init_Box();
30003
32158
  init_Stack();
30004
32159
  init_Typography();
@@ -44938,6 +47093,7 @@ var init_component_registry_generated = __esm({
44938
47093
  init_ActionTile();
44939
47094
  init_ActivationBlock();
44940
47095
  init_ComponentPatterns();
47096
+ init_AlgoGraphCanvas();
44941
47097
  init_AlgorithmCanvas();
44942
47098
  init_AnimatedCounter();
44943
47099
  init_AnimatedGraphic();
@@ -45201,6 +47357,7 @@ var init_component_registry_generated = __esm({
45201
47357
  "ActivationBlock": ActivationBlock,
45202
47358
  "Alert": AlertPattern,
45203
47359
  "AlertPattern": AlertPattern,
47360
+ "AlgoGraphCanvas": AlgoGraphCanvas,
45204
47361
  "AlgorithmCanvas": AlgorithmCanvas,
45205
47362
  "AnimatedCounter": AnimatedCounter,
45206
47363
  "AnimatedGraphic": AnimatedGraphic,
@@ -46527,9 +48684,9 @@ var log3 = createLogger("almadar:ui:effects:client-handlers");
46527
48684
  function createClientEffectHandlers(options) {
46528
48685
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
46529
48686
  return {
46530
- emit: (event, payload) => {
48687
+ emit: (event, payload, source) => {
46531
48688
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
46532
- eventBus.emit(prefixedEvent, payload);
48689
+ eventBus.emit(prefixedEvent, payload, source);
46533
48690
  },
46534
48691
  persist: async () => {
46535
48692
  log3.warn("persist is server-side only, ignored on client");
@@ -46864,7 +49021,7 @@ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
46864
49021
  }
46865
49022
  };
46866
49023
  ctx.emit = (event, payload) => {
46867
- emit(event, payload);
49024
+ emit(event, payload, { trait: traitName, tick: tick.name });
46868
49025
  };
46869
49026
  if (tick.guard !== void 0 && !evaluateGuard(tick.guard, ctx)) {
46870
49027
  tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
@@ -47316,6 +49473,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47316
49473
  }
47317
49474
  const effectContext = {
47318
49475
  traitName,
49476
+ orbitalName: orbitalsByTrait?.[traitName],
47319
49477
  state: previousState,
47320
49478
  transition: `${previousState}->${newState}`,
47321
49479
  linkedEntity,
@@ -47392,7 +49550,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47392
49550
  });
47393
49551
  }
47394
49552
  return emittedDuringExec;
47395
- }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
49553
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot, orbitalsByTrait]);
47396
49554
  const runTickEffects = useCallback((tick, binding) => {
47397
49555
  const traitName = binding.trait.name;
47398
49556
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -47428,9 +49586,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47428
49586
  log: tickLog
47429
49587
  });
47430
49588
  }, [executeTransitionEffects, sharedEntityStore]);
47431
- const emitFromSharedWriter = useCallback((event, payload) => {
49589
+ const emitFromSharedWriter = useCallback((event, payload, source) => {
47432
49590
  const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
47433
- eventBus.emit(prefixedEvent, payload);
49591
+ eventBus.emit(prefixedEvent, payload, source);
47434
49592
  }, [eventBus]);
47435
49593
  useEffect(() => {
47436
49594
  const scheduler = createTickScheduler();
@@ -48284,11 +50442,11 @@ function SchemaRunner({ schema, serverUrl, transport, mockData, pageName, routeP
48284
50442
  for (const orb of schema.orbitals) {
48285
50443
  for (const pageRef of orb.pages ?? []) {
48286
50444
  const name = typeof pageRef === "object" && pageRef !== null ? pageRef.name : void 0;
48287
- if (name === pageName) return orb.theme;
50445
+ if (name === pageName) return orb.theme ?? schema.theme;
48288
50446
  }
48289
50447
  }
48290
50448
  }
48291
- return schema.orbitals[0]?.theme;
50449
+ return schema.orbitals[0]?.theme ?? schema.theme;
48292
50450
  }, [schema, pageName]);
48293
50451
  const inner = /* @__PURE__ */ jsx(VerificationProvider, { enabled: true, children: /* @__PURE__ */ jsx(
48294
50452
  EntitySchemaProvider,