@almadar/ui 6.5.0 → 6.6.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.
Files changed (46) hide show
  1. package/dist/{EntityBindingContext-0Evn_LcT.d.cts → EntityBindingContext-Bn3ePJQC.d.cts} +1 -1
  2. package/dist/{EntityBindingContext-0Evn_LcT.d.ts → EntityBindingContext-Bn3ePJQC.d.ts} +1 -1
  3. package/dist/{GameAudioProvider-B48iXwz3.d.ts → GameAudioProvider-Ctceau1s.d.ts} +6 -18
  4. package/dist/{GameAudioProvider-CQAdPreB.d.cts → GameAudioProvider-Dk_Y3LN3.d.cts} +6 -18
  5. package/dist/avl/index.cjs +1216 -206
  6. package/dist/avl/index.js +1217 -207
  7. package/dist/{avl-schema-parser-dvJKXn-o.d.ts → avl-schema-parser-DncJLEn9.d.ts} +18 -0
  8. package/dist/{avl-schema-parser-Bt4NzAam.d.cts → avl-schema-parser-FQ1474v8.d.cts} +18 -0
  9. package/dist/{cn-DJSBBm5W.d.cts → cn-CCjFspAo.d.cts} +25 -1
  10. package/dist/{cn-BbhJq_nr.d.ts → cn-sgpqpN0U.d.ts} +25 -1
  11. package/dist/components/index.cjs +1025 -235
  12. package/dist/components/index.d.cts +176 -23
  13. package/dist/components/index.d.ts +176 -23
  14. package/dist/components/index.js +1023 -236
  15. package/dist/context/index.cjs +10 -13
  16. package/dist/context/index.js +10 -13
  17. package/dist/hooks/index.cjs +63 -13
  18. package/dist/hooks/index.d.cts +2 -2
  19. package/dist/hooks/index.d.ts +2 -2
  20. package/dist/hooks/index.js +64 -15
  21. package/dist/lib/drawable/three/index.cjs +126 -13
  22. package/dist/lib/drawable/three/index.d.cts +2 -2
  23. package/dist/lib/drawable/three/index.d.ts +2 -2
  24. package/dist/lib/drawable/three/index.js +127 -14
  25. package/dist/lib/index.cjs +180 -0
  26. package/dist/lib/index.d.cts +1 -1
  27. package/dist/lib/index.d.ts +1 -1
  28. package/dist/lib/index.js +178 -1
  29. package/dist/locales/index.cjs +6 -3
  30. package/dist/locales/index.js +6 -3
  31. package/dist/providers/index.cjs +922 -188
  32. package/dist/providers/index.d.cts +2 -2
  33. package/dist/providers/index.d.ts +2 -2
  34. package/dist/providers/index.js +922 -188
  35. package/dist/runtime/index.cjs +1505 -182
  36. package/dist/runtime/index.d.cts +78 -6
  37. package/dist/runtime/index.d.ts +78 -6
  38. package/dist/runtime/index.js +1504 -184
  39. package/dist/{useEventBus-CQWyAWpK.d.ts → useKeyboardRouter-D84cNWmA.d.ts} +31 -1
  40. package/dist/{useEventBus-Ckr4wqW3.d.cts → useKeyboardRouter-DQEE_9mq.d.cts} +31 -1
  41. package/locales/ar.json +2 -1
  42. package/locales/en.json +2 -1
  43. package/locales/sl.json +2 -1
  44. package/package.json +4 -4
  45. package/themes/comic.css +437 -0
  46. package/themes/index.css +1 -0
@@ -6,7 +6,7 @@ import { createLogger, setNamespaceLevel, isLogLevelEnabled } from '@almadar/log
6
6
  import { perfStore, pushPerfEntry, wrapCallbackForEvent, perfStart, perfEnd, perfGauge, prepareSchemaForPreview, perfTimeAsync, collectTraitRefsFromResolvedTrait, buildOrbitalsByTrait, collectEmbeddedTraits } from '@almadar/runtime/ui';
7
7
  export { PERF_NAMESPACE, adjustSchemaForMockData, buildMockData, clearPerf, perfEnd, perfStart, perfTime, prepareSchemaForPreview, wrapCallbackForEvent } from '@almadar/runtime/ui';
8
8
  import { StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, InMemoryPersistence, normalizeCallSiteConfigToValues, interpolateValue } from '@almadar/runtime';
9
- import { mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, schemaToIR, getPage, clearSchemaCache as clearSchemaCache$1, walkSExpr, buildResolvedTraitConfigs, isRenderBindingMarker, isFileValue, isInlineTrait, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER } from '@almadar/core';
9
+ import { UI_SLOTS, mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, schemaToIR, getPage, clearSchemaCache as clearSchemaCache$1, walkSExpr, buildResolvedTraitConfigs, isRenderBindingMarker, isFileValue, isInlineTrait, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER } from '@almadar/core';
10
10
  import { clsx } from 'clsx';
11
11
  import { twMerge } from 'tailwind-merge';
12
12
  import * as LucideIcons2 from 'lucide-react';
@@ -10427,7 +10427,7 @@ function collectDrawnItems(nodes) {
10427
10427
  for (const n of nodes) {
10428
10428
  switch (n.type) {
10429
10429
  case "draw-sprite":
10430
- if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id, anchor: n.anchor, width: n.width, height: n.height });
10430
+ if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id, anchor: n.anchor, width: n.width, height: n.height, rotation: n.rotation });
10431
10431
  break;
10432
10432
  case "draw-shape":
10433
10433
  case "draw-text":
@@ -10437,7 +10437,7 @@ function collectDrawnItems(nodes) {
10437
10437
  break;
10438
10438
  case "draw-sprite-layer":
10439
10439
  for (const it of n.items) {
10440
- if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id, anchor: it.anchor, width: it.width, height: it.height });
10440
+ if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id, anchor: it.anchor, width: it.width, height: it.height, rotation: it.rotation });
10441
10441
  }
10442
10442
  break;
10443
10443
  case "draw-shape-layer":
@@ -10457,15 +10457,39 @@ function buildHitIndex(items) {
10457
10457
  }
10458
10458
  return m;
10459
10459
  }
10460
+ function withPreviewPosition(nodes, id, pos) {
10461
+ return nodes.map((n) => {
10462
+ if (n.type === "draw-sprite-layer" || n.type === "draw-shape-layer" || n.type === "draw-text-layer") {
10463
+ if (!n.items.some((it) => it.id === id)) return n;
10464
+ return { ...n, items: n.items.map((it) => it.id === id ? { ...it, position: pos } : it) };
10465
+ }
10466
+ if (n.type === "draw-group") {
10467
+ if (!Array.isArray(n.items)) return n;
10468
+ return { ...n, items: withPreviewPosition(n.items, id, pos) };
10469
+ }
10470
+ if (n.id === id && "position" in n) {
10471
+ return { ...n, position: pos };
10472
+ }
10473
+ return n;
10474
+ });
10475
+ }
10460
10476
  function hitTestSprites(items, projector, point) {
10461
10477
  for (let i = items.length - 1; i >= 0; i--) {
10462
10478
  const it = items[i];
10463
10479
  if (it.id === void 0) continue;
10464
10480
  const r = spriteRect(projector, { position: it.pos, anchor: it.anchor, width: it.width, height: it.height });
10465
- if (point.x >= r.x && point.x <= r.x + r.w && point.y >= r.y && point.y <= r.y + r.h) return it.id;
10481
+ const test = it.rotation ? rotatePoint(point, { x: r.x + r.w / 2, y: r.y + r.h / 2 }, -it.rotation) : point;
10482
+ if (test.x >= r.x && test.x <= r.x + r.w && test.y >= r.y && test.y <= r.y + r.h) return it.id;
10466
10483
  }
10467
10484
  return void 0;
10468
10485
  }
10486
+ function rotatePoint(p, center, radians) {
10487
+ const cos = Math.cos(radians);
10488
+ const sin = Math.sin(radians);
10489
+ const dx = p.x - center.x;
10490
+ const dy = p.y - center.y;
10491
+ return { x: center.x + dx * cos - dy * sin, y: center.y + dx * sin + dy * cos };
10492
+ }
10469
10493
  var init_hitTest = __esm({
10470
10494
  "lib/drawable/hitTest.ts"() {
10471
10495
  init_contract();
@@ -10474,6 +10498,41 @@ var init_hitTest = __esm({
10474
10498
  function normalizeBackdrop(bg) {
10475
10499
  return typeof bg === "string" ? { url: bg, role: "decoration", category: "background" } : bg;
10476
10500
  }
10501
+ function selectionOverlayNodes(projector, item) {
10502
+ const r = spriteRect(projector, { position: item.pos, anchor: item.anchor, width: item.width, height: item.height });
10503
+ const tw = projector.tileWidth;
10504
+ const cellTopLeft = projector.anchorPoint(item.pos, "top-left");
10505
+ const offsetX = (r.x - cellTopLeft.x) / tw;
10506
+ const offsetY = (r.y - cellTopLeft.y) / tw;
10507
+ const width = r.w / tw;
10508
+ const height = r.h / tw;
10509
+ const handle = EDIT_HANDLE_SIZE_PX / tw;
10510
+ const ring = {
10511
+ type: "draw-shape",
10512
+ shape: "rect",
10513
+ position: item.pos,
10514
+ anchor: "top-left",
10515
+ offsetX,
10516
+ offsetY,
10517
+ width,
10518
+ height,
10519
+ stroke: EDIT_SELECTION_COLOR,
10520
+ strokeWidth: 2,
10521
+ fill: "none"
10522
+ };
10523
+ const handles = EDIT_SELECTION_CORNERS.map(([cx, cy]) => ({
10524
+ type: "draw-shape",
10525
+ shape: "rect",
10526
+ position: item.pos,
10527
+ anchor: "top-left",
10528
+ offsetX: offsetX + cx * width - handle / 2,
10529
+ offsetY: offsetY + cy * height - handle / 2,
10530
+ width: handle,
10531
+ height: handle,
10532
+ fill: EDIT_SELECTION_COLOR
10533
+ }));
10534
+ return [ring, ...handles];
10535
+ }
10477
10536
  function Canvas2D({
10478
10537
  className,
10479
10538
  isLoading = false,
@@ -10487,6 +10546,12 @@ function Canvas2D({
10487
10546
  tileLeaveEvent,
10488
10547
  keyMap,
10489
10548
  keyUpMap,
10549
+ editable = false,
10550
+ selectedId = null,
10551
+ onSelect,
10552
+ onMove,
10553
+ selectEvent,
10554
+ moveEvent,
10490
10555
  camera = "pan-zoom",
10491
10556
  scale = 0.4,
10492
10557
  tileWidth,
@@ -10744,10 +10809,22 @@ function Canvas2D({
10744
10809
  painter.scale(cam.zoom, cam.zoom);
10745
10810
  painter.translate(-viewportSize.width / 2 - cam.x, -viewportSize.height / 2 - cam.y);
10746
10811
  const dctx = { projector, time: timeMs, invalidate: bumpAtlas };
10747
- for (const node of drawables) paintDrawable(painter, node, dctx);
10812
+ let paintNodes = drawables;
10813
+ if (editable) {
10814
+ const drag = editDragRef.current;
10815
+ if (drag && drag.moved) {
10816
+ paintNodes = withPreviewPosition(paintNodes, drag.id, { x: drag.previewX, y: drag.previewY });
10817
+ }
10818
+ const selectedItem = selectedId != null ? drawnItems.find((it) => it.id === selectedId) : void 0;
10819
+ if (selectedItem) {
10820
+ const overlaySource = drag && drag.moved && drag.id === selectedId ? { ...selectedItem, pos: { x: drag.previewX, y: drag.previewY } } : selectedItem;
10821
+ paintNodes = [...paintNodes, ...selectionOverlayNodes(projector, overlaySource)];
10822
+ }
10823
+ }
10824
+ for (const node of paintNodes) paintDrawable(painter, node, dctx);
10748
10825
  painter.restore();
10749
- scheduleAnimation(drawables);
10750
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
10826
+ scheduleAnimation(paintNodes);
10827
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance, editable, selectedId, drawnItems]);
10751
10828
  useEffect(() => {
10752
10829
  drawTimeRef.current = draw;
10753
10830
  }, [draw]);
@@ -10792,23 +10869,83 @@ function Canvas2D({
10792
10869
  };
10793
10870
  }, [camera, followTarget, lerpToTarget, draw]);
10794
10871
  const singlePointerActiveRef = useRef(false);
10872
+ const editDragRef = useRef(null);
10873
+ const pointerToScene = useCallback((clientX, clientY) => {
10874
+ const canvas = canvasRef.current;
10875
+ if (!canvas) return { x: 0, y: 0 };
10876
+ const world = screenToWorld(clientX, clientY, canvas, viewportSize);
10877
+ const adjustedX = world.x - scaledTileWidth / 2;
10878
+ const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10879
+ return unproject(adjustedX, adjustedY);
10880
+ }, [screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject]);
10795
10881
  const handleCanvasPointerDown = useCallback((e) => {
10796
10882
  singlePointerActiveRef.current = true;
10883
+ if (editable) {
10884
+ if (!canvasRef.current) return;
10885
+ const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
10886
+ const hitId = hitTestSprites(drawnItems, projector, world);
10887
+ if (hitId === void 0) return;
10888
+ const item = [...drawnItems].reverse().find((it) => it.id === hitId);
10889
+ if (!item) return;
10890
+ editDragRef.current = {
10891
+ id: hitId,
10892
+ pointerId: e.pointerId,
10893
+ startClientX: e.clientX,
10894
+ startClientY: e.clientY,
10895
+ startSceneX: item.pos.x,
10896
+ startSceneY: item.pos.y,
10897
+ moved: false,
10898
+ previewX: item.pos.x,
10899
+ previewY: item.pos.y
10900
+ };
10901
+ return;
10902
+ }
10797
10903
  if (enableCamera) handlePointerDown(e);
10798
- }, [enableCamera, handlePointerDown]);
10904
+ }, [editable, screenToWorld, viewportSize, drawnItems, projector, enableCamera, handlePointerDown]);
10799
10905
  const handleCanvasPointerMove = useCallback((e) => {
10906
+ if (editable) {
10907
+ const drag = editDragRef.current;
10908
+ if (!drag || drag.pointerId !== e.pointerId) return;
10909
+ const dxPx = e.clientX - drag.startClientX;
10910
+ const dyPx = e.clientY - drag.startClientY;
10911
+ if (!drag.moved && Math.abs(dxPx) + Math.abs(dyPx) <= 5) return;
10912
+ drag.moved = true;
10913
+ const nowScene = pointerToScene(e.clientX, e.clientY);
10914
+ const startScene = pointerToScene(drag.startClientX, drag.startClientY);
10915
+ drag.previewX = drag.startSceneX + (nowScene.x - startScene.x);
10916
+ drag.previewY = drag.startSceneY + (nowScene.y - startScene.y);
10917
+ draw();
10918
+ return;
10919
+ }
10800
10920
  if (enableCamera) handlePointerMove(e, () => draw());
10801
- }, [enableCamera, handlePointerMove, draw]);
10921
+ }, [editable, pointerToScene, draw, enableCamera, handlePointerMove]);
10802
10922
  const handleCanvasHover = useCallback((e) => {
10803
10923
  if (singlePointerActiveRef.current) return;
10804
10924
  if (!tileHoverEvent || !canvasRef.current) return;
10805
- const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
10806
- const adjustedX = world.x - scaledTileWidth / 2;
10807
- const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10808
- const isoPos = unproject(adjustedX, adjustedY);
10925
+ const isoPos = pointerToScene(e.clientX, e.clientY);
10809
10926
  eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
10810
- }, [screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject, tileHoverEvent, eventBus]);
10927
+ }, [pointerToScene, tileHoverEvent, eventBus]);
10811
10928
  const handleCanvasPointerUp = useCallback((e) => {
10929
+ if (editable) {
10930
+ singlePointerActiveRef.current = false;
10931
+ const drag = editDragRef.current;
10932
+ if (drag && drag.pointerId === e.pointerId) {
10933
+ editDragRef.current = null;
10934
+ if (drag.moved) {
10935
+ onMove?.(drag.id, drag.previewX, drag.previewY);
10936
+ if (moveEvent) eventBus.emit(`UI:${moveEvent}`, { id: drag.id, x: drag.previewX, y: drag.previewY });
10937
+ draw();
10938
+ return;
10939
+ }
10940
+ const next = selectedId === drag.id ? null : drag.id;
10941
+ onSelect?.(next);
10942
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: next });
10943
+ return;
10944
+ }
10945
+ onSelect?.(null);
10946
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: null });
10947
+ return;
10948
+ }
10812
10949
  singlePointerActiveRef.current = false;
10813
10950
  if (enableCamera) handlePointerUp();
10814
10951
  if (dragDistance() > 5) return;
@@ -10819,16 +10956,14 @@ function Canvas2D({
10819
10956
  eventBus.emit(`UI:${unitClickEvent}`, { unitId: spriteHit });
10820
10957
  return;
10821
10958
  }
10822
- const adjustedX = world.x - scaledTileWidth / 2;
10823
- const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10824
- const isoPos = unproject(adjustedX, adjustedY);
10959
+ const isoPos = pointerToScene(e.clientX, e.clientY);
10825
10960
  const hitId = hitIndex.get(`${isoPos.x},${isoPos.y}`);
10826
10961
  if (hitId !== void 0 && unitClickEvent) {
10827
10962
  eventBus.emit(`UI:${unitClickEvent}`, { unitId: hitId });
10828
10963
  } else if (tileClickEvent) {
10829
10964
  eventBus.emit(`UI:${tileClickEvent}`, { x: isoPos.x, y: isoPos.y });
10830
10965
  }
10831
- }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject, hitIndex, drawnItems, projector, tileClickEvent, unitClickEvent, eventBus]);
10966
+ }, [editable, selectedId, onMove, moveEvent, onSelect, selectEvent, eventBus, draw, enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, drawnItems, projector, tileClickEvent, unitClickEvent, hitIndex, pointerToScene]);
10832
10967
  const handleCanvasPointerLeave = useCallback(() => {
10833
10968
  handleMouseLeave();
10834
10969
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -10847,7 +10982,7 @@ function Canvas2D({
10847
10982
  }, [enableCamera, handlePointerUp]);
10848
10983
  const gestureHandlers = useCanvasGestures({
10849
10984
  canvasRef,
10850
- enabled: enableCamera || !!tileHoverEvent || !!tileClickEvent || !!unitClickEvent,
10985
+ enabled: enableCamera || !!tileHoverEvent || !!tileClickEvent || !!unitClickEvent || editable,
10851
10986
  onPointerDown: handleCanvasPointerDown,
10852
10987
  onPointerMove: handleCanvasPointerMove,
10853
10988
  onPointerUp: handleCanvasPointerUp,
@@ -10979,7 +11114,7 @@ function Canvas2D({
10979
11114
  }
10980
11115
  ) });
10981
11116
  }
10982
- var canvas2DLog;
11117
+ var canvas2DLog, EDIT_SELECTION_COLOR, EDIT_HANDLE_SIZE_PX, EDIT_SELECTION_CORNERS;
10983
11118
  var init_Canvas2D = __esm({
10984
11119
  "components/game/molecules/Canvas2D.tsx"() {
10985
11120
  "use client";
@@ -11004,8 +11139,12 @@ var init_Canvas2D = __esm({
11004
11139
  init_DrawGroup();
11005
11140
  init_registry();
11006
11141
  init_hitTest();
11142
+ init_contract();
11007
11143
  init_isometric();
11008
11144
  canvas2DLog = createLogger("almadar:ui:game-canvas");
11145
+ EDIT_SELECTION_COLOR = "#3b82f6";
11146
+ EDIT_HANDLE_SIZE_PX = 8;
11147
+ EDIT_SELECTION_CORNERS = [[0, 0], [1, 0], [0, 1], [1, 1]];
11009
11148
  Canvas2D.displayName = "Canvas2D";
11010
11149
  }
11011
11150
  });
@@ -11047,6 +11186,12 @@ function Canvas({
11047
11186
  featureClickEvent,
11048
11187
  keyMap,
11049
11188
  keyUpMap,
11189
+ editable,
11190
+ selectedId,
11191
+ onSelect,
11192
+ onMove,
11193
+ selectEvent,
11194
+ moveEvent,
11050
11195
  children
11051
11196
  }) {
11052
11197
  canvasLog.debug("Canvas render", { mode, drawablesCount: drawables?.length, projection, camera: camera ? JSON.stringify(camera) : void 0 });
@@ -11124,6 +11269,12 @@ function Canvas({
11124
11269
  tileLeaveEvent,
11125
11270
  keyMap,
11126
11271
  keyUpMap,
11272
+ editable,
11273
+ selectedId,
11274
+ onSelect,
11275
+ onMove,
11276
+ selectEvent,
11277
+ moveEvent,
11127
11278
  ...children !== void 0 ? { children } : {}
11128
11279
  }
11129
11280
  );
@@ -11145,15 +11296,19 @@ function GameAudioToggle({
11145
11296
  size = "sm",
11146
11297
  className,
11147
11298
  onAsset,
11148
- offAsset
11299
+ offAsset,
11300
+ toggleEvent
11149
11301
  }) {
11150
11302
  const ctx = useGameAudioContextOptional();
11151
11303
  const [localMuted, setLocalMuted] = useState(false);
11152
11304
  const muted = ctx ? ctx.muted : localMuted;
11153
11305
  const setMuted = ctx ? ctx.setMuted : setLocalMuted;
11306
+ const eventBus = useEventBus();
11154
11307
  const handleToggle = useCallback(() => {
11155
- setMuted(!muted);
11156
- }, [muted, setMuted]);
11308
+ const next = !muted;
11309
+ setMuted(next);
11310
+ if (toggleEvent) eventBus.emit(`UI:${toggleEvent}`, { muted: next });
11311
+ }, [muted, setMuted, toggleEvent, eventBus]);
11157
11312
  const activeAsset = muted ? offAsset : onAsset;
11158
11313
  return /* @__PURE__ */ jsx(
11159
11314
  Button,
@@ -11172,10 +11327,319 @@ var init_GameAudioToggle = __esm({
11172
11327
  "use client";
11173
11328
  init_atoms();
11174
11329
  init_cn();
11330
+ init_useEventBus();
11175
11331
  init_GameIcon();
11176
11332
  GameAudioToggle.displayName = "GameAudioToggle";
11177
11333
  }
11178
11334
  });
11335
+ function pickPath(entry) {
11336
+ if (Array.isArray(entry.path)) {
11337
+ return entry.path[Math.floor(Math.random() * entry.path.length)];
11338
+ }
11339
+ return entry.path;
11340
+ }
11341
+ function useGameAudio({
11342
+ manifest,
11343
+ baseUrl = "",
11344
+ initialMuted = false,
11345
+ initialVolume = 1
11346
+ }) {
11347
+ const [muted, setMutedState] = useState(initialMuted);
11348
+ const [masterVolume, setMasterVolumeState] = useState(initialVolume);
11349
+ const mutedRef = useRef(muted);
11350
+ const volumeRef = useRef(masterVolume);
11351
+ const manifestRef = useRef(manifest);
11352
+ mutedRef.current = muted;
11353
+ volumeRef.current = masterVolume;
11354
+ manifestRef.current = manifest;
11355
+ const poolsRef = useRef(/* @__PURE__ */ new Map());
11356
+ const getOrCreateElement = useCallback((key) => {
11357
+ const entry = manifestRef.current[key];
11358
+ if (!entry) return null;
11359
+ let pool = poolsRef.current.get(key);
11360
+ if (!pool) {
11361
+ pool = [];
11362
+ poolsRef.current.set(key, pool);
11363
+ }
11364
+ const maxSize = entry.poolSize ?? 1;
11365
+ for (const audio of pool) {
11366
+ if (audio.paused && (audio.ended || audio.currentTime === 0)) {
11367
+ return audio;
11368
+ }
11369
+ }
11370
+ if (pool.length < maxSize) {
11371
+ const src = baseUrl + pickPath(entry);
11372
+ const audio = new Audio(src);
11373
+ audio.loop = entry.loop ?? false;
11374
+ pool.push(audio);
11375
+ return audio;
11376
+ }
11377
+ if (!entry.loop) {
11378
+ let oldest = pool[0];
11379
+ for (const audio of pool) {
11380
+ if (audio.currentTime > oldest.currentTime) {
11381
+ oldest = audio;
11382
+ }
11383
+ }
11384
+ oldest.pause();
11385
+ oldest.currentTime = 0;
11386
+ return oldest;
11387
+ }
11388
+ return null;
11389
+ }, [baseUrl]);
11390
+ const play = useCallback((key) => {
11391
+ if (mutedRef.current) return;
11392
+ const entry = manifestRef.current[key];
11393
+ if (!entry) return;
11394
+ const audio = getOrCreateElement(key);
11395
+ if (!audio) return;
11396
+ audio.volume = Math.min(1, (entry.volume ?? 1) * volumeRef.current);
11397
+ if (!entry.loop) {
11398
+ audio.currentTime = 0;
11399
+ }
11400
+ const promise = audio.play();
11401
+ if (promise) {
11402
+ promise.catch(() => {
11403
+ });
11404
+ }
11405
+ }, [getOrCreateElement]);
11406
+ const stop = useCallback((key) => {
11407
+ const pool = poolsRef.current.get(key);
11408
+ if (!pool) return;
11409
+ for (const audio of pool) {
11410
+ audio.pause();
11411
+ audio.currentTime = 0;
11412
+ }
11413
+ }, []);
11414
+ const currentMusicKeyRef = useRef(null);
11415
+ const currentMusicElRef = useRef(null);
11416
+ const musicFadeRef = useRef(null);
11417
+ const pendingMusicKeyRef = useRef(null);
11418
+ const clearMusicFade = useCallback(() => {
11419
+ if (musicFadeRef.current) {
11420
+ clearInterval(musicFadeRef.current);
11421
+ musicFadeRef.current = null;
11422
+ }
11423
+ }, []);
11424
+ const playMusic = useCallback((key) => {
11425
+ if (key === currentMusicKeyRef.current) return;
11426
+ pendingMusicKeyRef.current = key;
11427
+ const entry = manifestRef.current[key];
11428
+ if (!entry) return;
11429
+ const fadeDurationMs = entry.crossfadeDurationMs ?? 1500;
11430
+ const stepMs = 50;
11431
+ const totalSteps = Math.max(1, fadeDurationMs / stepMs);
11432
+ const targetVolume = Math.min(1, (entry.volume ?? 1) * volumeRef.current);
11433
+ clearMusicFade();
11434
+ const src = baseUrl + (Array.isArray(entry.path) ? entry.path[0] : entry.path);
11435
+ const incoming = new Audio(src);
11436
+ incoming.loop = true;
11437
+ incoming.volume = 0;
11438
+ const outgoing = currentMusicElRef.current;
11439
+ const outgoingStartVol = outgoing?.volume ?? 0;
11440
+ currentMusicKeyRef.current = key;
11441
+ currentMusicElRef.current = incoming;
11442
+ if (!mutedRef.current) {
11443
+ incoming.play().catch(() => {
11444
+ currentMusicKeyRef.current = null;
11445
+ currentMusicElRef.current = outgoing;
11446
+ });
11447
+ }
11448
+ let step = 0;
11449
+ musicFadeRef.current = setInterval(() => {
11450
+ step++;
11451
+ const progress = Math.min(step / totalSteps, 1);
11452
+ incoming.volume = Math.min(1, targetVolume * progress);
11453
+ if (outgoing) {
11454
+ outgoing.volume = Math.max(0, outgoingStartVol * (1 - progress));
11455
+ }
11456
+ if (progress >= 1) {
11457
+ clearMusicFade();
11458
+ if (outgoing) {
11459
+ outgoing.pause();
11460
+ outgoing.src = "";
11461
+ }
11462
+ }
11463
+ }, stepMs);
11464
+ }, [baseUrl, clearMusicFade]);
11465
+ const stopMusic = useCallback((fadeDurationMs = 1e3) => {
11466
+ const outgoing = currentMusicElRef.current;
11467
+ if (!outgoing) return;
11468
+ currentMusicKeyRef.current = null;
11469
+ currentMusicElRef.current = null;
11470
+ pendingMusicKeyRef.current = null;
11471
+ clearMusicFade();
11472
+ const startVolume = outgoing.volume;
11473
+ const stepMs = 50;
11474
+ const totalSteps = Math.max(1, fadeDurationMs / stepMs);
11475
+ let step = 0;
11476
+ musicFadeRef.current = setInterval(() => {
11477
+ step++;
11478
+ const progress = step / totalSteps;
11479
+ outgoing.volume = Math.max(0, startVolume * (1 - progress));
11480
+ if (progress >= 1) {
11481
+ clearMusicFade();
11482
+ outgoing.pause();
11483
+ outgoing.src = "";
11484
+ }
11485
+ }, stepMs);
11486
+ }, [clearMusicFade]);
11487
+ const stopAll = useCallback(() => {
11488
+ for (const pool of poolsRef.current.values()) {
11489
+ for (const audio of pool) {
11490
+ audio.pause();
11491
+ audio.currentTime = 0;
11492
+ }
11493
+ }
11494
+ stopMusic(0);
11495
+ }, [stopMusic]);
11496
+ const setMuted = useCallback((value) => {
11497
+ setMutedState(value);
11498
+ if (value) {
11499
+ for (const [key, pool] of poolsRef.current.entries()) {
11500
+ if (manifestRef.current[key]?.loop) {
11501
+ for (const audio of pool) {
11502
+ if (!audio.paused) audio.pause();
11503
+ }
11504
+ }
11505
+ }
11506
+ currentMusicElRef.current?.pause();
11507
+ } else {
11508
+ for (const [key, pool] of poolsRef.current.entries()) {
11509
+ const entry = manifestRef.current[key];
11510
+ if (entry?.loop && entry?.autostart) {
11511
+ for (const audio of pool) {
11512
+ if (audio.paused) audio.play().catch(() => {
11513
+ });
11514
+ }
11515
+ }
11516
+ }
11517
+ const musicEl = currentMusicElRef.current;
11518
+ if (musicEl) {
11519
+ musicEl.play().catch(() => {
11520
+ });
11521
+ }
11522
+ }
11523
+ }, []);
11524
+ const setMasterVolume = useCallback((volume) => {
11525
+ const clamped = Math.max(0, Math.min(1, volume));
11526
+ setMasterVolumeState(clamped);
11527
+ for (const [key, pool] of poolsRef.current.entries()) {
11528
+ const entryVol = manifestRef.current[key]?.volume ?? 1;
11529
+ for (const audio of pool) {
11530
+ audio.volume = Math.min(1, entryVol * clamped);
11531
+ }
11532
+ }
11533
+ if (!musicFadeRef.current && currentMusicElRef.current) {
11534
+ const key = currentMusicKeyRef.current;
11535
+ const entryVol = key ? manifestRef.current[key]?.volume ?? 1 : 1;
11536
+ currentMusicElRef.current.volume = Math.min(1, entryVol * clamped);
11537
+ }
11538
+ }, []);
11539
+ const unlockedRef = useRef(false);
11540
+ useEffect(() => {
11541
+ const autoKeys = Object.keys(manifest).filter((k) => manifest[k].autostart);
11542
+ const hasPendingMusic = () => pendingMusicKeyRef.current !== null;
11543
+ const hasAutoStart = autoKeys.length > 0;
11544
+ if (!hasAutoStart && !hasPendingMusic()) return;
11545
+ const unlock = () => {
11546
+ if (unlockedRef.current) return;
11547
+ unlockedRef.current = true;
11548
+ if (!mutedRef.current) {
11549
+ for (const key of autoKeys) {
11550
+ play(key);
11551
+ }
11552
+ const pending = pendingMusicKeyRef.current;
11553
+ if (pending && pending !== currentMusicKeyRef.current) {
11554
+ playMusic(pending);
11555
+ }
11556
+ }
11557
+ };
11558
+ document.addEventListener("click", unlock, { once: true });
11559
+ document.addEventListener("keydown", unlock, { once: true });
11560
+ document.addEventListener("touchstart", unlock, { once: true });
11561
+ return () => {
11562
+ document.removeEventListener("click", unlock);
11563
+ document.removeEventListener("keydown", unlock);
11564
+ document.removeEventListener("touchstart", unlock);
11565
+ };
11566
+ }, [manifest, play, playMusic]);
11567
+ useEffect(() => {
11568
+ return () => {
11569
+ clearMusicFade();
11570
+ for (const pool of poolsRef.current.values()) {
11571
+ for (const audio of pool) {
11572
+ audio.pause();
11573
+ audio.src = "";
11574
+ }
11575
+ }
11576
+ poolsRef.current.clear();
11577
+ if (currentMusicElRef.current) {
11578
+ currentMusicElRef.current.pause();
11579
+ currentMusicElRef.current.src = "";
11580
+ currentMusicElRef.current = null;
11581
+ }
11582
+ };
11583
+ }, [clearMusicFade]);
11584
+ return {
11585
+ play,
11586
+ stop,
11587
+ stopAll,
11588
+ playMusic,
11589
+ stopMusic,
11590
+ muted,
11591
+ setMuted,
11592
+ masterVolume,
11593
+ setMasterVolume
11594
+ };
11595
+ }
11596
+ var init_useGameAudio = __esm({
11597
+ "hooks/useGameAudio.ts"() {
11598
+ "use client";
11599
+ useGameAudio.displayName = "useGameAudio";
11600
+ }
11601
+ });
11602
+ function GameAudioCue({
11603
+ cue,
11604
+ cueSeq,
11605
+ music,
11606
+ muted,
11607
+ volume,
11608
+ manifest,
11609
+ baseUrl
11610
+ }) {
11611
+ const { play, playMusic, stopMusic, setMuted, setMasterVolume } = useGameAudio({
11612
+ manifest,
11613
+ baseUrl,
11614
+ initialMuted: muted,
11615
+ initialVolume: volume
11616
+ });
11617
+ const prevCueSeqRef = useRef(cueSeq);
11618
+ useEffect(() => {
11619
+ if (cue && cueSeq !== void 0 && cueSeq !== prevCueSeqRef.current) {
11620
+ play(cue);
11621
+ }
11622
+ prevCueSeqRef.current = cueSeq;
11623
+ }, [cue, cueSeq, play]);
11624
+ useEffect(() => {
11625
+ if (music) playMusic(music);
11626
+ else stopMusic();
11627
+ }, [music, playMusic, stopMusic]);
11628
+ useEffect(() => {
11629
+ if (muted !== void 0) setMuted(muted);
11630
+ }, [muted, setMuted]);
11631
+ useEffect(() => {
11632
+ if (volume !== void 0) setMasterVolume(volume);
11633
+ }, [volume, setMasterVolume]);
11634
+ return null;
11635
+ }
11636
+ var init_GameAudioCue = __esm({
11637
+ "components/game/atoms/GameAudioCue.tsx"() {
11638
+ "use client";
11639
+ init_useGameAudio();
11640
+ GameAudioCue.displayName = "GameAudioCue";
11641
+ }
11642
+ });
11179
11643
  function isKnownState(s) {
11180
11644
  return s in DEFAULT_STATE_STYLES;
11181
11645
  }
@@ -11874,15 +12338,18 @@ var init_StateJsonView = __esm({
11874
12338
  StateJsonView.displayName = "StateJsonView";
11875
12339
  }
11876
12340
  });
11877
- var GAME_FONTS, GameShell;
11878
- var init_GameShell = __esm({
11879
- "components/game/templates/GameShell.tsx"() {
11880
- init_cn();
11881
- init_Box();
11882
- init_Card();
11883
- init_Typography();
11884
- init_AtlasImage();
11885
- GAME_FONTS = {
12341
+
12342
+ // lib/gameFonts.ts
12343
+ function resolveGameFontFamily(input) {
12344
+ if (!input) return void 0;
12345
+ const resolved = GAME_FONT_KEYS[input];
12346
+ if (resolved) return `'${resolved}', ui-sans-serif, system-ui, sans-serif`;
12347
+ return input;
12348
+ }
12349
+ var GAME_FONT_KEYS;
12350
+ var init_gameFonts = __esm({
12351
+ "lib/gameFonts.ts"() {
12352
+ GAME_FONT_KEYS = {
11886
12353
  fredoka: "Fredoka",
11887
12354
  future: "Kenney Future",
11888
12355
  "future-narrow": "Kenney Future Narrow",
@@ -11890,6 +12357,17 @@ var init_GameShell = __esm({
11890
12357
  blocks: "Kenney Blocks",
11891
12358
  mini: "Kenney Mini"
11892
12359
  };
12360
+ }
12361
+ });
12362
+ var GameShell;
12363
+ var init_GameShell = __esm({
12364
+ "components/game/templates/GameShell.tsx"() {
12365
+ init_cn();
12366
+ init_gameFonts();
12367
+ init_Box();
12368
+ init_Card();
12369
+ init_Typography();
12370
+ init_AtlasImage();
11893
12371
  GameShell = ({
11894
12372
  appName = "Game",
11895
12373
  hud,
@@ -11903,7 +12381,7 @@ var init_GameShell = __esm({
11903
12381
  fontFamily,
11904
12382
  "data-theme": dataTheme
11905
12383
  }) => {
11906
- const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
12384
+ const displayFont = resolveGameFontFamily(fontFamily);
11907
12385
  return /* @__PURE__ */ jsxs(
11908
12386
  Box,
11909
12387
  {
@@ -11922,7 +12400,7 @@ var init_GameShell = __esm({
11922
12400
  // passed — an always-on inline stamp would shadow the orbital's
11923
12401
  // inline-theme font-family-display token (inline style beats the
11924
12402
  // theme provider's vars for the whole shell subtree).
11925
- ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
12403
+ ...displayFont ? { "--font-family-display": displayFont } : {}
11926
12404
  },
11927
12405
  children: [
11928
12406
  backgroundAsset && /* @__PURE__ */ jsx(
@@ -12120,14 +12598,15 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
12120
12598
  ctx.closePath();
12121
12599
  ctx.fill();
12122
12600
  }
12123
- function drawShape(ctx, shape, width, height, allShapes) {
12601
+ function drawShape(ctx, shape, width, height, allShapes, fontFamily) {
12124
12602
  ctx.save();
12125
12603
  const opacity = shape.opacity ?? 1;
12126
12604
  ctx.globalAlpha = opacity;
12127
12605
  const stroke = resolveColor2(shape.color, ctx, "#333333");
12128
12606
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
12129
12607
  ctx.lineWidth = shape.lineWidth ?? 2;
12130
- if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
12608
+ const dashPattern = shape.dash ? DASH_PATTERNS[shape.dash] : void 0;
12609
+ if (dashPattern) ctx.setLineDash([...dashPattern]);
12131
12610
  switch (shape.type) {
12132
12611
  case "grid": {
12133
12612
  const step = shape.step ?? 40;
@@ -12249,7 +12728,7 @@ function drawShape(ctx, shape, width, height, allShapes) {
12249
12728
  case "text": {
12250
12729
  if (shape.x == null || shape.y == null || !shape.text) break;
12251
12730
  ctx.fillStyle = stroke;
12252
- ctx.font = `${shape.fontSize ?? 14}px ${themeBodyFont(ctx.canvas)}`;
12731
+ ctx.font = `${shape.fontSize ?? 14}px ${shape.fontFamily ?? fontFamily ?? themeBodyFont(ctx.canvas)}`;
12253
12732
  ctx.textAlign = shape.align ?? "left";
12254
12733
  ctx.textBaseline = "middle";
12255
12734
  ctx.fillText(shape.text, shape.x, shape.y);
@@ -12291,7 +12770,7 @@ function drawShape(ctx, shape, width, height, allShapes) {
12291
12770
  }
12292
12771
  ctx.restore();
12293
12772
  }
12294
- function readoutShapes(readouts, width) {
12773
+ function readoutShapes(readouts, width, fontFamily) {
12295
12774
  const out = [];
12296
12775
  const chipH = 18;
12297
12776
  const gap = 6;
@@ -12315,13 +12794,14 @@ function readoutShapes(readouts, width) {
12315
12794
  text,
12316
12795
  color: "#ffffff",
12317
12796
  fontSize: 10,
12318
- align: "center"
12797
+ align: "center",
12798
+ fontFamily
12319
12799
  });
12320
12800
  rightEdge = chipX - gap;
12321
12801
  }
12322
12802
  return out;
12323
12803
  }
12324
- function traceShapes(panel, k, width, height) {
12804
+ function traceShapes(panel, k, width, height, fontFamily) {
12325
12805
  const w = panel.width ?? Math.round(width * 0.32);
12326
12806
  const h = panel.height ?? Math.round(height * 0.28);
12327
12807
  const x = panel.x ?? width - w - 8;
@@ -12375,14 +12855,14 @@ function traceShapes(panel, k, width, height) {
12375
12855
  out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
12376
12856
  }
12377
12857
  if (series.label) {
12378
- out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
12858
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9, fontFamily });
12379
12859
  }
12380
12860
  });
12381
12861
  if (panel.yLabel) {
12382
- out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
12862
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right", fontFamily });
12383
12863
  }
12384
12864
  if (panel.xLabel) {
12385
- out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
12865
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right", fontFamily });
12386
12866
  }
12387
12867
  return out;
12388
12868
  }
@@ -12405,13 +12885,14 @@ var init_LearningCanvas = __esm({
12405
12885
  init_useEventBus();
12406
12886
  init_webPainter2d();
12407
12887
  init_paintDispatch();
12408
- DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12888
+ DASH_PATTERNS = { solid: [], dashed: [6, 4], dotted: [2, 3] };
12409
12889
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
12410
12890
  LearningCanvas = ({
12411
12891
  className,
12412
12892
  width = 600,
12413
12893
  height = 400,
12414
12894
  backgroundColor,
12895
+ fontFamily,
12415
12896
  shapes = [],
12416
12897
  drawables,
12417
12898
  projector,
@@ -12447,10 +12928,10 @@ var init_LearningCanvas = __esm({
12447
12928
  }, [shapes]);
12448
12929
  const derivedShapes = useMemo(() => {
12449
12930
  if (!traces?.length && !readouts?.length) return shapes;
12450
- const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
12451
- const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
12931
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height, fontFamily));
12932
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width, fontFamily) : [];
12452
12933
  return [...shapes, ...traceOut, ...readoutOut];
12453
- }, [shapes, traces, readouts, width, height]);
12934
+ }, [shapes, traces, readouts, width, height, fontFamily]);
12454
12935
  const draw = useCallback(() => {
12455
12936
  const _perfT = perfStart("learningcanvas:paint");
12456
12937
  const canvas = canvasRef.current;
@@ -12469,15 +12950,15 @@ var init_LearningCanvas = __esm({
12469
12950
  ctx.fillRect(0, 0, width, height);
12470
12951
  }
12471
12952
  for (const shape of derivedShapes) {
12472
- if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
12953
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes, fontFamily);
12473
12954
  }
12474
12955
  for (const shape of derivedShapes) {
12475
- if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
12956
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes, fontFamily);
12476
12957
  }
12477
12958
  if (drawables?.length && projector) {
12478
12959
  const painter = createWebPainter(ctx, invalidateRef.current);
12479
12960
  const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
12480
- const dctx = { projector, time: timeMs, invalidate: invalidateRef.current, fontFamily: themeBodyFont(canvas) };
12961
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current, fontFamily: fontFamily || themeBodyFont(canvas) };
12481
12962
  for (const node of drawables) {
12482
12963
  paintDrawable(painter, node, dctx);
12483
12964
  }
@@ -17076,6 +17557,250 @@ var init_EmptyState = __esm({
17076
17557
  EmptyState.displayName = "EmptyState";
17077
17558
  }
17078
17559
  });
17560
+
17561
+ // lib/editorMotions.ts
17562
+ function clamp(value, min, max) {
17563
+ return Math.max(min, Math.min(max, value));
17564
+ }
17565
+ function computeLines(text) {
17566
+ const lines = [];
17567
+ let start = 0;
17568
+ for (let i = 0; i <= text.length; i++) {
17569
+ if (i === text.length || text[i] === "\n") {
17570
+ lines.push({ start, end: i });
17571
+ start = i + 1;
17572
+ }
17573
+ }
17574
+ return lines;
17575
+ }
17576
+ function lineIndexAt(lines, pos) {
17577
+ for (let i = 0; i < lines.length; i++) {
17578
+ if (pos <= lines[i].end) return i;
17579
+ }
17580
+ return lines.length - 1;
17581
+ }
17582
+ function findWords(text) {
17583
+ const words = [];
17584
+ const re = /\w+|\S+/g;
17585
+ let m;
17586
+ while ((m = re.exec(text)) !== null) {
17587
+ words.push({ start: m.index, end: m.index + m[0].length });
17588
+ }
17589
+ return words;
17590
+ }
17591
+ function nextWordStart(text, pos) {
17592
+ for (const w of findWords(text)) {
17593
+ if (w.start > pos) return w.start;
17594
+ }
17595
+ return text.length;
17596
+ }
17597
+ function prevWordStart(text, pos) {
17598
+ let result = 0;
17599
+ for (const w of findWords(text)) {
17600
+ if (w.start < pos) result = w.start;
17601
+ else break;
17602
+ }
17603
+ return result;
17604
+ }
17605
+ function nextWordEnd(text, pos) {
17606
+ for (const w of findWords(text)) {
17607
+ const lastChar = w.end - 1;
17608
+ if (lastChar > pos) return lastChar;
17609
+ }
17610
+ return text.length > 0 ? text.length - 1 : 0;
17611
+ }
17612
+ function firstNonBlank(text, line) {
17613
+ let i = line.start;
17614
+ while (i < line.end && (text[i] === " " || text[i] === " ")) i++;
17615
+ return i;
17616
+ }
17617
+ function nextParagraphBoundary(lines, fromLineIdx, textLength) {
17618
+ for (let i = fromLineIdx + 1; i < lines.length; i++) {
17619
+ if (lines[i].start === lines[i].end) return lines[i].start;
17620
+ }
17621
+ return textLength;
17622
+ }
17623
+ function prevParagraphBoundary(lines, fromLineIdx) {
17624
+ for (let i = fromLineIdx - 1; i >= 0; i--) {
17625
+ if (lines[i].start === lines[i].end) return lines[i].start;
17626
+ }
17627
+ return 0;
17628
+ }
17629
+ function applyMotion(text, caret, motion, count) {
17630
+ const n = Math.max(1, count);
17631
+ const lines = computeLines(text);
17632
+ const lineIdx = lineIndexAt(lines, caret);
17633
+ const line = lines[lineIdx];
17634
+ switch (motion) {
17635
+ case "left":
17636
+ return clamp(caret - n, line.start, line.end);
17637
+ case "right":
17638
+ return clamp(caret + n, line.start, line.end);
17639
+ case "up": {
17640
+ const col = caret - line.start;
17641
+ const targetIdx = clamp(lineIdx - n, 0, lines.length - 1);
17642
+ const target = lines[targetIdx];
17643
+ return clamp(target.start + col, target.start, target.end);
17644
+ }
17645
+ case "down": {
17646
+ const col = caret - line.start;
17647
+ const targetIdx = clamp(lineIdx + n, 0, lines.length - 1);
17648
+ const target = lines[targetIdx];
17649
+ return clamp(target.start + col, target.start, target.end);
17650
+ }
17651
+ case "word-forward": {
17652
+ let pos = caret;
17653
+ for (let i = 0; i < n; i++) pos = nextWordStart(text, pos);
17654
+ return pos;
17655
+ }
17656
+ case "word-back": {
17657
+ let pos = caret;
17658
+ for (let i = 0; i < n; i++) pos = prevWordStart(text, pos);
17659
+ return pos;
17660
+ }
17661
+ case "word-end": {
17662
+ let pos = caret;
17663
+ for (let i = 0; i < n; i++) pos = nextWordEnd(text, pos);
17664
+ return pos;
17665
+ }
17666
+ case "line-start":
17667
+ return line.start;
17668
+ case "line-end":
17669
+ return line.end > line.start ? line.end - 1 : line.start;
17670
+ case "first-nonblank":
17671
+ return firstNonBlank(text, line);
17672
+ case "doc-start":
17673
+ return 0;
17674
+ case "doc-end":
17675
+ return text.length;
17676
+ case "paragraph-forward": {
17677
+ let pos = caret;
17678
+ for (let i = 0; i < n; i++) {
17679
+ pos = nextParagraphBoundary(lines, lineIndexAt(lines, pos), text.length);
17680
+ }
17681
+ return pos;
17682
+ }
17683
+ case "paragraph-back": {
17684
+ let pos = caret;
17685
+ for (let i = 0; i < n; i++) {
17686
+ pos = prevParagraphBoundary(lines, lineIndexAt(lines, pos));
17687
+ }
17688
+ return pos;
17689
+ }
17690
+ case "line":
17691
+ case "selection":
17692
+ return caret;
17693
+ default: {
17694
+ const _exhaustive = motion;
17695
+ return _exhaustive;
17696
+ }
17697
+ }
17698
+ }
17699
+ function motionRange(text, caret, motion, count, selection) {
17700
+ if (motion === "selection") {
17701
+ if (!selection) return [caret, caret];
17702
+ return [Math.min(selection[0], selection[1]), Math.max(selection[0], selection[1])];
17703
+ }
17704
+ const lines = computeLines(text);
17705
+ if (motion === "line") {
17706
+ const n = Math.max(1, count);
17707
+ const startIdx = lineIndexAt(lines, caret);
17708
+ const endIdx = clamp(startIdx + n - 1, 0, lines.length - 1);
17709
+ const start2 = lines[startIdx].start;
17710
+ const rawEnd = lines[endIdx].end;
17711
+ const end2 = rawEnd < text.length ? rawEnd + 1 : rawEnd;
17712
+ return [start2, end2];
17713
+ }
17714
+ const newCaret = applyMotion(text, caret, motion, count);
17715
+ let start = Math.min(caret, newCaret);
17716
+ let end = Math.max(caret, newCaret);
17717
+ if (motion === "word-end" || motion === "line-end") {
17718
+ end = Math.min(Math.max(start, newCaret) + 1, text.length);
17719
+ } else if (motion === "word-forward" && newCaret > caret) {
17720
+ const startLine = lineIndexAt(lines, caret);
17721
+ const endLine = lineIndexAt(lines, newCaret);
17722
+ if (endLine !== startLine) {
17723
+ end = lines[startLine].end;
17724
+ }
17725
+ }
17726
+ return [start, end];
17727
+ }
17728
+ function applyOperator(text, range, operator, register) {
17729
+ const start = clamp(range[0], 0, text.length);
17730
+ const end = clamp(range[1], start, text.length);
17731
+ const removed = text.slice(start, end);
17732
+ if (operator === "yank") {
17733
+ return { text, caret: start, register: removed };
17734
+ }
17735
+ return { text: text.slice(0, start) + text.slice(end), caret: start, register: removed };
17736
+ }
17737
+ var init_editorMotions = __esm({
17738
+ "lib/editorMotions.ts"() {
17739
+ }
17740
+ });
17741
+ function isMotionPayload(payload) {
17742
+ return !!payload && typeof payload.editorId === "string" && typeof payload.motion === "string" && typeof payload.count === "number";
17743
+ }
17744
+ function isOperatePayload(payload) {
17745
+ return !!payload && typeof payload.editorId === "string" && typeof payload.operator === "string" && typeof payload.motion === "string" && typeof payload.count === "number";
17746
+ }
17747
+ function isInsertTextPayload(payload) {
17748
+ return !!payload && typeof payload.editorId === "string" && typeof payload.text === "string";
17749
+ }
17750
+ function isSetModePayload(payload) {
17751
+ return !!payload && typeof payload.editorId === "string" && typeof payload.mode === "string" && typeof payload.caret === "string";
17752
+ }
17753
+ function useEditorCapabilities(args) {
17754
+ const [caretMode, setCaretMode] = useState("bar");
17755
+ const registerRef = useRef("");
17756
+ useEventListener(`UI:${args.events.onMotion}`, (evt) => {
17757
+ if (!args.editorId || !isMotionPayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
17758
+ const ta = args.textareaRef.current;
17759
+ if (!ta) return;
17760
+ const { motion, count } = evt.payload;
17761
+ const newCaret = applyMotion(ta.value, ta.selectionStart, motion, count);
17762
+ if (ta.selectionStart !== ta.selectionEnd) {
17763
+ ta.setSelectionRange(ta.selectionStart, newCaret);
17764
+ } else {
17765
+ ta.setSelectionRange(newCaret, newCaret);
17766
+ }
17767
+ });
17768
+ useEventListener(`UI:${args.events.onOperate}`, (evt) => {
17769
+ if (!args.editorId || !isOperatePayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
17770
+ const ta = args.textareaRef.current;
17771
+ if (!ta) return;
17772
+ const { operator, motion, count } = evt.payload;
17773
+ const selection = motion === "selection" ? [ta.selectionStart, ta.selectionEnd] : void 0;
17774
+ const range = motionRange(ta.value, ta.selectionStart, motion, count, selection);
17775
+ const result = applyOperator(ta.value, range, operator, registerRef.current);
17776
+ registerRef.current = result.register;
17777
+ if (operator === "yank") {
17778
+ ta.setSelectionRange(range[0], range[0]);
17779
+ } else {
17780
+ ta.setRangeText("", range[0], range[1], "end");
17781
+ }
17782
+ args.applyChange(ta.value);
17783
+ });
17784
+ useEventListener(`UI:${args.events.onInsertText}`, (evt) => {
17785
+ if (!args.editorId || !isInsertTextPayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
17786
+ const ta = args.textareaRef.current;
17787
+ if (!ta) return;
17788
+ ta.setRangeText(evt.payload.text, ta.selectionStart, ta.selectionEnd, "end");
17789
+ args.applyChange(ta.value);
17790
+ });
17791
+ useEventListener(`UI:${args.events.onSetMode}`, (evt) => {
17792
+ if (!args.editorId || !isSetModePayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
17793
+ setCaretMode(evt.payload.caret);
17794
+ });
17795
+ return { caretMode };
17796
+ }
17797
+ var init_useEditorCapabilities = __esm({
17798
+ "components/core/molecules/markdown/useEditorCapabilities.ts"() {
17799
+ "use client";
17800
+ init_useEventBus();
17801
+ init_editorMotions();
17802
+ }
17803
+ });
17079
17804
  function isLanguageRegistered(lang) {
17080
17805
  return CODE_LANGUAGE_SET.has(lang) || dynamicallyLoaded.has(lang);
17081
17806
  }
@@ -17161,7 +17886,36 @@ function useLanguageReady(language) {
17161
17886
  }, [language]);
17162
17887
  return ready;
17163
17888
  }
17164
- var dynamicallyLoaded, codeLanguageLoader, orbStyleOverrides, orbStyle, loloStyleOverrides, loloStyle, log6, CODE_LANGUAGES, CODE_LANGUAGE_SET, DIFF_STYLES, DIFF_STYLE_FALLBACK, LINE_PROPS_FN, HIDDEN_LINE_NUMBERS, HIGHLIGHT_CAPACITY_BYTES, CodeBlock;
17889
+ function resolveHighlightStyle(lang) {
17890
+ if (lang === "orb") return orbStyle;
17891
+ if (lang === "lolo") return loloStyle;
17892
+ return dark;
17893
+ }
17894
+ function plainCodeColorOf(style) {
17895
+ return style['code[class*="language-"]']?.color ?? "#d4d4d4";
17896
+ }
17897
+ function buildLineProps(errorLines, extraClassName) {
17898
+ return (lineNumber) => {
17899
+ const base = {
17900
+ "data-line": String(lineNumber - 1),
17901
+ ...extraClassName ? { className: extraClassName } : {}
17902
+ };
17903
+ const severity = errorLines?.get(lineNumber);
17904
+ if (!severity) return base;
17905
+ return {
17906
+ ...base,
17907
+ style: {
17908
+ display: "block",
17909
+ backgroundColor: severity === "error" ? "rgba(248, 113, 113, 0.18)" : "rgba(251, 191, 36, 0.18)",
17910
+ // amber-400 @ 18%
17911
+ borderLeft: `3px solid ${severity === "error" ? "#ef4444" : "#f59e0b"}`,
17912
+ paddingLeft: "0.5rem",
17913
+ marginLeft: "-0.5rem"
17914
+ }
17915
+ };
17916
+ };
17917
+ }
17918
+ var dynamicallyLoaded, codeLanguageLoader, orbStyleOverrides, orbStyle, loloStyleOverrides, loloStyle, log6, CODE_LANGUAGES, CODE_LANGUAGE_SET, DIFF_STYLES, DIFF_STYLE_FALLBACK, LINE_PROPS_FN, HIDDEN_LINE_NUMBERS, HIGHLIGHT_CAPACITY_BYTES, MONO_FONT_FAMILY, VIEWER_LINE_NUMBER_STYLE, CodeBlock;
17165
17919
  var init_CodeBlock = __esm({
17166
17920
  "components/core/molecules/markdown/CodeBlock.tsx"() {
17167
17921
  init_cn();
@@ -17177,6 +17931,7 @@ var init_CodeBlock = __esm({
17177
17931
  init_Textarea();
17178
17932
  init_Icon();
17179
17933
  init_useEventBus();
17934
+ init_useEditorCapabilities();
17180
17935
  SyntaxHighlighter.registerLanguage("json", langJson);
17181
17936
  SyntaxHighlighter.registerLanguage("javascript", langJavascript);
17182
17937
  SyntaxHighlighter.registerLanguage("js", langJavascript);
@@ -17395,6 +18150,15 @@ var init_CodeBlock = __esm({
17395
18150
  LINE_PROPS_FN = (n) => ({ "data-line": String(n - 1) });
17396
18151
  HIDDEN_LINE_NUMBERS = { display: "none" };
17397
18152
  HIGHLIGHT_CAPACITY_BYTES = 512 * 1024;
18153
+ MONO_FONT_FAMILY = 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace';
18154
+ VIEWER_LINE_NUMBER_STYLE = {
18155
+ minWidth: "2.5em",
18156
+ paddingRight: "1rem",
18157
+ textAlign: "right",
18158
+ userSelect: "none",
18159
+ opacity: 0.5,
18160
+ fontVariantNumeric: "tabular-nums"
18161
+ };
17398
18162
  CodeBlock = React87__default.memo(
17399
18163
  ({
17400
18164
  code: rawCode,
@@ -17419,15 +18183,39 @@ var init_CodeBlock = __esm({
17419
18183
  actions,
17420
18184
  isLoading = false,
17421
18185
  error,
17422
- showCopy
18186
+ showCopy,
18187
+ // editor capability surface — P1 wires these
18188
+ editorId,
18189
+ onEditorFocus = "EDITOR_FOCUS",
18190
+ onEditorBlur = "EDITOR_BLUR",
18191
+ onMotion = "MOTION",
18192
+ onOperate = "OPERATE",
18193
+ onInsertText = "INSERT_TEXT",
18194
+ onSetMode = "SET_MODE",
18195
+ motions = [
18196
+ "left",
18197
+ "right",
18198
+ "up",
18199
+ "down",
18200
+ "word-forward",
18201
+ "word-back",
18202
+ "word-end",
18203
+ "line-start",
18204
+ "line-end",
18205
+ "first-nonblank",
18206
+ "doc-start",
18207
+ "doc-end",
18208
+ "paragraph-forward",
18209
+ "paragraph-back",
18210
+ "line",
18211
+ "selection"
18212
+ ],
18213
+ operators = ["delete", "yank", "change"]
17423
18214
  }) => {
17424
18215
  const code = typeof rawCode === "string" ? rawCode : String(rawCode ?? "");
17425
- const isOrb = language === "orb";
17426
- const isLolo = language === "lolo";
17427
- const activeStyle = isOrb ? orbStyle : isLolo ? loloStyle : dark;
18216
+ const activeStyle = resolveHighlightStyle(language);
17428
18217
  const overCapacity = code.length > HIGHLIGHT_CAPACITY_BYTES;
17429
- const plainCodeColor = activeStyle['code[class*="language-"]']?.color ?? "#d4d4d4";
17430
- const languageReady = useLanguageReady(language);
18218
+ const plainCodeColor = plainCodeColorOf(activeStyle);
17431
18219
  const eventBus = useEventBus();
17432
18220
  const { t } = useTranslate();
17433
18221
  const scrollRef = useRef(null);
@@ -17439,6 +18227,9 @@ var init_CodeBlock = __esm({
17439
18227
  const activeFile = files?.[activeFileIndex];
17440
18228
  const activeCode = activeFile?.code ?? code;
17441
18229
  const activeLanguage = activeFile?.language ?? language;
18230
+ const languageReady = useLanguageReady(activeLanguage);
18231
+ const viewerStyle = resolveHighlightStyle(activeLanguage);
18232
+ const viewerPlainCodeColor = plainCodeColorOf(viewerStyle);
17442
18233
  const diffLines = useMemo(() => {
17443
18234
  if (propDiff) return propDiff;
17444
18235
  if (mode === "diff" && oldValue !== void 0 && newValue !== void 0) {
@@ -17468,28 +18259,28 @@ var init_CodeBlock = __esm({
17468
18259
  ov.scrollLeft = ta.scrollLeft;
17469
18260
  }
17470
18261
  }, []);
17471
- const errorLineProps = useMemo(() => {
17472
- if (!errorLines || errorLines.size === 0) {
17473
- return LINE_PROPS_FN;
17474
- }
17475
- return (lineNumber) => {
17476
- const severity = errorLines.get(lineNumber);
17477
- if (!severity) {
17478
- return { "data-line": String(lineNumber - 1) };
17479
- }
17480
- return {
17481
- "data-line": String(lineNumber - 1),
17482
- style: {
17483
- display: "block",
17484
- backgroundColor: severity === "error" ? "rgba(248, 113, 113, 0.18)" : "rgba(251, 191, 36, 0.18)",
17485
- // amber-400 @ 18%
17486
- borderLeft: `3px solid ${severity === "error" ? "#ef4444" : "#f59e0b"}`,
17487
- paddingLeft: "0.5rem",
17488
- marginLeft: "-0.5rem"
17489
- }
17490
- };
17491
- };
17492
- }, [errorLines]);
18262
+ const handleEditableChange = useCallback((v) => {
18263
+ lastPropCodeRef.current = v;
18264
+ setEditableValue(v);
18265
+ onChange?.(v);
18266
+ }, [onChange]);
18267
+ const { caretMode } = useEditorCapabilities({
18268
+ editorId: editable ? editorId : void 0,
18269
+ textareaRef: editableTextareaRef,
18270
+ events: { onMotion, onOperate, onInsertText, onSetMode },
18271
+ applyChange: handleEditableChange
18272
+ });
18273
+ const [caretIndex, setCaretIndex] = useState(0);
18274
+ const caretRowCol = useMemo(() => {
18275
+ const before = editableValue.slice(0, Math.min(caretIndex, editableValue.length));
18276
+ const lines = before.split("\n");
18277
+ return { row: lines.length - 1, col: lines[lines.length - 1].length };
18278
+ }, [editableValue, caretIndex]);
18279
+ const errorLineProps = useMemo(() => buildLineProps(errorLines), [errorLines]);
18280
+ const viewerLineProps = useMemo(
18281
+ () => buildLineProps(errorLines, "px-4 py-0.5 hover:bg-muted/50"),
18282
+ [errorLines]
18283
+ );
17493
18284
  const isFoldable = foldableProp ?? true;
17494
18285
  const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
17495
18286
  const foldRegions = useMemo(
@@ -17612,6 +18403,110 @@ var init_CodeBlock = __esm({
17612
18403
  ),
17613
18404
  [code, overCapacity, plainCodeColor, language, activeStyle, languageReady]
17614
18405
  );
18406
+ const viewerOverCapacity = activeCode.length > HIGHLIGHT_CAPACITY_BYTES;
18407
+ const viewerHighlightedElement = useMemo(
18408
+ () => viewerOverCapacity ? /* @__PURE__ */ jsx(
18409
+ "div",
18410
+ {
18411
+ className: "px-4 py-0.5",
18412
+ style: {
18413
+ margin: 0,
18414
+ whiteSpace: wrap ? "pre-wrap" : "pre",
18415
+ wordBreak: wrap ? "break-all" : "normal",
18416
+ color: viewerPlainCodeColor,
18417
+ fontFamily: MONO_FONT_FAMILY,
18418
+ fontSize: "12px",
18419
+ lineHeight: "1.6"
18420
+ },
18421
+ children: activeCode
18422
+ }
18423
+ ) : /* @__PURE__ */ jsx(
18424
+ SyntaxHighlighter,
18425
+ {
18426
+ PreTag: "div",
18427
+ language: activeLanguage,
18428
+ style: viewerStyle,
18429
+ wrapLines: true,
18430
+ wrapLongLines: wrap,
18431
+ showLineNumbers,
18432
+ lineNumberStyle: VIEWER_LINE_NUMBER_STYLE,
18433
+ lineProps: viewerLineProps,
18434
+ customStyle: {
18435
+ backgroundColor: "transparent",
18436
+ borderRadius: 0,
18437
+ padding: "0.25rem 0",
18438
+ margin: 0,
18439
+ whiteSpace: wrap ? "pre-wrap" : "pre",
18440
+ wordBreak: wrap ? "break-all" : "normal",
18441
+ fontFamily: MONO_FONT_FAMILY,
18442
+ fontSize: "12px",
18443
+ lineHeight: "1.6"
18444
+ },
18445
+ codeTagProps: { style: { fontFamily: MONO_FONT_FAMILY, fontSize: "12px", lineHeight: "1.6" } },
18446
+ children: activeCode
18447
+ }
18448
+ ),
18449
+ [activeCode, viewerOverCapacity, viewerPlainCodeColor, activeLanguage, viewerStyle, wrap, showLineNumbers, viewerLineProps, languageReady]
18450
+ );
18451
+ const diffOverCapacity = useMemo(
18452
+ () => !!diffLines && diffLines.reduce((n, l) => n + l.content.length + 1, 0) > HIGHLIGHT_CAPACITY_BYTES,
18453
+ [diffLines]
18454
+ );
18455
+ const diffRowElements = useMemo(() => {
18456
+ if (!diffLines) return null;
18457
+ return diffLines.map((line, idx) => {
18458
+ const style = DIFF_STYLES[line.type] ?? DIFF_STYLE_FALLBACK;
18459
+ return /* @__PURE__ */ jsxs(HStack, { gap: "none", align: "start", className: cn(style.bg, "px-4 py-0.5"), children: [
18460
+ showLineNumbers && /* @__PURE__ */ jsx(
18461
+ Typography,
18462
+ {
18463
+ variant: "caption",
18464
+ color: "secondary",
18465
+ className: "w-8 text-right mr-3 select-none tabular-nums flex-shrink-0",
18466
+ children: line.lineNumber ?? ""
18467
+ }
18468
+ ),
18469
+ /* @__PURE__ */ jsxs(
18470
+ Typography,
18471
+ {
18472
+ variant: "caption",
18473
+ className: cn("font-mono flex-1 min-w-0", style.text, wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
18474
+ children: [
18475
+ /* @__PURE__ */ jsx(Box, { as: "span", className: "select-none opacity-50 mr-2", children: style.prefix }),
18476
+ diffOverCapacity ? line.content || " " : /* @__PURE__ */ jsx(
18477
+ SyntaxHighlighter,
18478
+ {
18479
+ PreTag: "span",
18480
+ CodeTag: "span",
18481
+ language: activeLanguage,
18482
+ style: viewerStyle,
18483
+ customStyle: {
18484
+ display: "inline",
18485
+ background: "transparent",
18486
+ padding: 0,
18487
+ margin: 0,
18488
+ whiteSpace: wrap ? "pre-wrap" : "pre",
18489
+ wordBreak: wrap ? "break-all" : "normal",
18490
+ fontFamily: "inherit",
18491
+ fontSize: "inherit",
18492
+ lineHeight: "inherit"
18493
+ },
18494
+ codeTagProps: {
18495
+ style: {
18496
+ whiteSpace: wrap ? "pre-wrap" : "pre",
18497
+ fontFamily: "inherit",
18498
+ fontSize: "inherit"
18499
+ }
18500
+ },
18501
+ children: line.content || " "
18502
+ }
18503
+ )
18504
+ ]
18505
+ }
18506
+ )
18507
+ ] }, idx);
18508
+ });
18509
+ }, [diffLines, showLineNumbers, wrap, diffOverCapacity, activeLanguage, viewerStyle, languageReady]);
17615
18510
  useLayoutEffect(() => {
17616
18511
  const container = codeRef.current;
17617
18512
  if (!container) return;
@@ -17755,7 +18650,6 @@ var init_CodeBlock = __esm({
17755
18650
  label: file.label,
17756
18651
  content: null
17757
18652
  }));
17758
- const lines = activeCode.split("\n");
17759
18653
  return /* @__PURE__ */ jsx(Card, { className: cn("overflow-hidden", className), children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: [
17760
18654
  tabItems && tabItems.length > 1 && /* @__PURE__ */ jsx(Box, { className: "border-b border-border", children: /* @__PURE__ */ jsx(
17761
18655
  Tabs,
@@ -17818,49 +18712,7 @@ var init_CodeBlock = __esm({
17818
18712
  ]
17819
18713
  }
17820
18714
  ),
17821
- /* @__PURE__ */ jsx(Box, { className: "overflow-auto bg-muted/20", style: { maxHeight }, children: diffLines ? /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column" }, className: "font-mono text-xs", children: diffLines.map((line, idx) => {
17822
- const style = DIFF_STYLES[line.type] ?? DIFF_STYLE_FALLBACK;
17823
- return /* @__PURE__ */ jsxs(HStack, { gap: "none", align: "start", className: cn(style.bg, "px-4 py-0.5"), children: [
17824
- showLineNumbers && /* @__PURE__ */ jsx(
17825
- Typography,
17826
- {
17827
- variant: "caption",
17828
- color: "secondary",
17829
- className: "w-8 text-right mr-3 select-none tabular-nums flex-shrink-0",
17830
- children: line.lineNumber ?? ""
17831
- }
17832
- ),
17833
- /* @__PURE__ */ jsxs(
17834
- Typography,
17835
- {
17836
- variant: "caption",
17837
- className: cn("font-mono flex-1 min-w-0", style.text, wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
17838
- children: [
17839
- /* @__PURE__ */ jsx(Box, { as: "span", className: "select-none opacity-50 mr-2", children: style.prefix }),
17840
- line.content
17841
- ]
17842
- }
17843
- )
17844
- ] }, idx);
17845
- }) }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column" }, className: "font-mono text-xs", children: lines.map((line, idx) => /* @__PURE__ */ jsxs(HStack, { gap: "none", align: "start", className: "px-4 py-0.5 hover:bg-muted/50", children: [
17846
- showLineNumbers && /* @__PURE__ */ jsx(
17847
- Typography,
17848
- {
17849
- variant: "caption",
17850
- color: "secondary",
17851
- className: "w-8 text-right mr-4 select-none tabular-nums flex-shrink-0",
17852
- children: idx + 1
17853
- }
17854
- ),
17855
- /* @__PURE__ */ jsx(
17856
- Typography,
17857
- {
17858
- variant: "caption",
17859
- className: cn("font-mono flex-1 min-w-0", wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
17860
- children: line || " "
17861
- }
17862
- )
17863
- ] }, idx)) }) })
18715
+ /* @__PURE__ */ jsx(Box, { className: "overflow-auto bg-muted/20", style: { maxHeight }, children: diffLines ? /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column" }, className: "font-mono text-xs", children: diffRowElements }) : /* @__PURE__ */ jsx("div", { className: "font-mono text-xs", children: viewerHighlightedElement }) })
17864
18716
  ] }) });
17865
18717
  }
17866
18718
  const hasHeader = showLanguageBadge || effectiveCopy;
@@ -17947,13 +18799,11 @@ var init_CodeBlock = __esm({
17947
18799
  {
17948
18800
  ref: editableTextareaRef,
17949
18801
  defaultValue: code,
17950
- onChange: (e) => {
17951
- const v = e.target.value;
17952
- lastPropCodeRef.current = v;
17953
- setEditableValue(v);
17954
- onChange?.(v);
17955
- },
18802
+ onChange: (e) => handleEditableChange(e.target.value),
17956
18803
  onScroll: handleEditableScroll,
18804
+ onSelect: (e) => setCaretIndex(e.currentTarget.selectionStart),
18805
+ onFocus: editorId ? () => eventBus.emit(`UI:${onEditorFocus}`, { editorId }) : void 0,
18806
+ onBlur: editorId ? () => eventBus.emit(`UI:${onEditorBlur}`, { editorId }) : void 0,
17957
18807
  spellCheck: false,
17958
18808
  style: {
17959
18809
  position: "absolute",
@@ -17968,7 +18818,7 @@ var init_CodeBlock = __esm({
17968
18818
  resize: "none",
17969
18819
  backgroundColor: "transparent",
17970
18820
  color: "transparent",
17971
- caretColor: "#e6e6e6",
18821
+ caretColor: caretMode === "block" ? "transparent" : "#e6e6e6",
17972
18822
  WebkitTextFillColor: "transparent",
17973
18823
  fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace',
17974
18824
  fontSize: "13px",
@@ -17979,6 +18829,22 @@ var init_CodeBlock = __esm({
17979
18829
  }
17980
18830
  },
17981
18831
  editableTextareaKey
18832
+ ),
18833
+ caretMode !== "bar" && /* @__PURE__ */ jsx(
18834
+ "span",
18835
+ {
18836
+ "aria-hidden": true,
18837
+ style: {
18838
+ position: "absolute",
18839
+ top: `calc(1rem + ${caretRowCol.row * 19.5}px)`,
18840
+ left: `calc(1rem + ${caretRowCol.col}ch)`,
18841
+ width: "1ch",
18842
+ height: caretMode === "block" ? "19.5px" : "2px",
18843
+ backgroundColor: caretMode === "block" ? "rgba(230, 230, 230, 0.5)" : void 0,
18844
+ borderBottom: caretMode === "underline" ? "2px solid #e6e6e6" : void 0,
18845
+ pointerEvents: "none"
18846
+ }
18847
+ }
17982
18848
  )
17983
18849
  ]
17984
18850
  }
@@ -18006,55 +18872,175 @@ var init_CodeBlock = __esm({
18006
18872
  )
18007
18873
  ] });
18008
18874
  },
18009
- (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.showCopy === next.showCopy && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable && prev.editable === next.editable && prev.onChange === next.onChange && prev.errorLines === next.errorLines && prev.mode === next.mode && prev.title === next.title && prev.diff === next.diff && prev.files === next.files && prev.actions === next.actions && prev.isLoading === next.isLoading && prev.error === next.error
18875
+ (prev, next) => prev.language === next.language && prev.code === next.code && prev.showCopyButton === next.showCopyButton && prev.showCopy === next.showCopy && prev.maxHeight === next.maxHeight && prev.foldable === next.foldable && prev.editable === next.editable && prev.onChange === next.onChange && prev.errorLines === next.errorLines && prev.mode === next.mode && prev.title === next.title && prev.diff === next.diff && prev.files === next.files && prev.actions === next.actions && prev.isLoading === next.isLoading && prev.error === next.error && prev.editorId === next.editorId && prev.onEditorFocus === next.onEditorFocus && prev.onEditorBlur === next.onEditorBlur && prev.onMotion === next.onMotion && prev.onOperate === next.onOperate && prev.onInsertText === next.onInsertText && prev.onSetMode === next.onSetMode && prev.motions === next.motions && prev.operators === next.operators
18010
18876
  );
18011
18877
  CodeBlock.displayName = "CodeBlock";
18012
18878
  }
18013
18879
  });
18880
+
18881
+ // components/core/molecules/markdown/mermaidSource.ts
18882
+ function isQuoted(label) {
18883
+ const trimmed = label.trim();
18884
+ return trimmed.length === 0 || trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length > 1;
18885
+ }
18886
+ function quote(label) {
18887
+ return `"${label.replace(/"/g, "#quot;")}"`;
18888
+ }
18889
+ function isIdentifierChar(ch) {
18890
+ return /[A-Za-z0-9_\-.]/.test(ch);
18891
+ }
18892
+ function declaredType(code) {
18893
+ for (const line of code.split("\n")) {
18894
+ const trimmed = line.trim();
18895
+ if (trimmed.length === 0 || trimmed.startsWith("%%")) continue;
18896
+ return trimmed;
18897
+ }
18898
+ return "";
18899
+ }
18900
+ function isFlowchart(code) {
18901
+ return FLOWCHART_DIRECTIVE.test(declaredType(code));
18902
+ }
18903
+ function quoteNodeLabels(code) {
18904
+ let out = "";
18905
+ let i = 0;
18906
+ while (i < code.length) {
18907
+ const shape = NODE_SHAPES.find(([open2]) => code.startsWith(open2, i));
18908
+ const precededByIdentifier = i > 0 && isIdentifierChar(code[i - 1] ?? "");
18909
+ if (shape === void 0 || !precededByIdentifier) {
18910
+ out += code[i];
18911
+ i += 1;
18912
+ continue;
18913
+ }
18914
+ const [open, close] = shape;
18915
+ const contentStart = i + open.length;
18916
+ const closeAt = code.indexOf(close, contentStart);
18917
+ const newlineAt = code.indexOf("\n", contentStart);
18918
+ if (closeAt === -1 || newlineAt !== -1 && newlineAt < closeAt) {
18919
+ out += code[i];
18920
+ i += 1;
18921
+ continue;
18922
+ }
18923
+ const label = code.slice(contentStart, closeAt);
18924
+ out += open + (isQuoted(label) ? label : quote(label)) + close;
18925
+ i = closeAt + close.length;
18926
+ }
18927
+ return out;
18928
+ }
18929
+ function quoteEdgeLabels(code) {
18930
+ return code.split("\n").map((line) => {
18931
+ let out = "";
18932
+ let rest = line;
18933
+ for (; ; ) {
18934
+ const open = rest.indexOf("|");
18935
+ if (open === -1) break;
18936
+ const close = rest.indexOf("|", open + 1);
18937
+ if (close === -1) break;
18938
+ const label = rest.slice(open + 1, close);
18939
+ out += rest.slice(0, open + 1) + (isQuoted(label) ? label : quote(label)) + "|";
18940
+ rest = rest.slice(close + 1);
18941
+ }
18942
+ return out + rest;
18943
+ }).join("\n");
18944
+ }
18945
+ function quoteSubgraphTitles(code) {
18946
+ return code.split("\n").map((line) => {
18947
+ const match = /^(\s*subgraph\s+)(.+?)(\s*)$/.exec(line);
18948
+ if (match === null) return line;
18949
+ const [, prefix, title, trailing] = match;
18950
+ if (title === void 0 || prefix === void 0) return line;
18951
+ if (isQuoted(title) || title.includes("[")) return line;
18952
+ return prefix + quote(title) + (trailing ?? "");
18953
+ }).join("\n");
18954
+ }
18955
+ function mermaidRepairCandidates(code) {
18956
+ if (!isFlowchart(code)) return [];
18957
+ const nodes = quoteNodeLabels(code);
18958
+ const nodesAndEdges = quoteEdgeLabels(nodes);
18959
+ const all = quoteSubgraphTitles(nodesAndEdges);
18960
+ const ordered2 = [nodes, nodesAndEdges, all];
18961
+ const seen = /* @__PURE__ */ new Set([code]);
18962
+ const candidates = [];
18963
+ for (const candidate of ordered2) {
18964
+ if (seen.has(candidate)) continue;
18965
+ seen.add(candidate);
18966
+ candidates.push(candidate);
18967
+ }
18968
+ return candidates;
18969
+ }
18970
+ var NODE_SHAPES, FLOWCHART_DIRECTIVE;
18971
+ var init_mermaidSource = __esm({
18972
+ "components/core/molecules/markdown/mermaidSource.ts"() {
18973
+ NODE_SHAPES = [
18974
+ ["[[", "]]"],
18975
+ ["[(", ")]"],
18976
+ ["([", "])"],
18977
+ ["((", "))"],
18978
+ ["{{", "}}"],
18979
+ ["[", "]"],
18980
+ ["(", ")"],
18981
+ ["{", "}"]
18982
+ ];
18983
+ FLOWCHART_DIRECTIVE = /^(?:graph|flowchart)\b/;
18984
+ }
18985
+ });
18014
18986
  function loadMermaid() {
18015
18987
  mermaidModule ?? (mermaidModule = import('mermaid').then((m) => m.default));
18016
18988
  return mermaidModule;
18017
18989
  }
18018
- var mermaidModule, MermaidDiagram;
18990
+ var log7, mermaidModule, MermaidDiagram;
18019
18991
  var init_MermaidDiagram = __esm({
18020
18992
  "components/core/molecules/markdown/MermaidDiagram.tsx"() {
18021
18993
  init_Box();
18022
18994
  init_Typography();
18023
18995
  init_CodeBlock();
18996
+ init_mermaidSource();
18024
18997
  init_cn();
18998
+ log7 = createLogger("almadar:ui:mermaid-diagram");
18025
18999
  mermaidModule = null;
18026
19000
  MermaidDiagram = React87__default.memo(
18027
19001
  ({ code, className }) => {
18028
19002
  const { resolvedMode } = useTheme();
19003
+ const { t } = useTranslate();
18029
19004
  const containerRef = useRef(null);
18030
- const [error, setError] = useState(null);
19005
+ const [unrenderable, setUnrenderable] = useState(false);
18031
19006
  const reactId = useId();
18032
19007
  useEffect(() => {
18033
19008
  let active = true;
18034
19009
  void (async () => {
18035
- try {
18036
- const mermaid = await loadMermaid();
18037
- mermaid.initialize({
18038
- startOnLoad: false,
18039
- securityLevel: "strict",
18040
- theme: resolvedMode === "dark" ? "dark" : "default"
18041
- });
18042
- const domId = `mermaid-${reactId.replace(/[^a-zA-Z0-9]/g, "")}`;
18043
- const { svg } = await mermaid.render(domId, code);
18044
- if (!active || !containerRef.current) return;
18045
- containerRef.current.innerHTML = svg;
18046
- setError(null);
18047
- } catch (err) {
18048
- if (active) setError(err instanceof Error ? err.message : String(err));
19010
+ const mermaid = await loadMermaid();
19011
+ mermaid.initialize({
19012
+ startOnLoad: false,
19013
+ securityLevel: "strict",
19014
+ theme: resolvedMode === "dark" ? "dark" : "default"
19015
+ });
19016
+ const domId = `mermaid-${reactId.replace(/[^a-zA-Z0-9]/g, "")}`;
19017
+ let firstError = null;
19018
+ for (const [index, source] of [code, ...mermaidRepairCandidates(code)].entries()) {
19019
+ try {
19020
+ const { svg } = await mermaid.render(domId, source);
19021
+ if (!active) return;
19022
+ const container = containerRef.current;
19023
+ if (container === null) return;
19024
+ container.innerHTML = svg;
19025
+ container.dataset.mermaidRepaired = String(index > 0);
19026
+ setUnrenderable(false);
19027
+ if (index > 0) log7.debug("mermaid:repaired", { candidate: index });
19028
+ return;
19029
+ } catch (err) {
19030
+ firstError ?? (firstError = err instanceof Error ? err : new Error(String(err)));
19031
+ }
18049
19032
  }
19033
+ if (!active) return;
19034
+ log7.warn("mermaid:unrenderable", { error: firstError?.message ?? "", code });
19035
+ setUnrenderable(true);
18050
19036
  })();
18051
19037
  return () => {
18052
19038
  active = false;
18053
19039
  };
18054
19040
  }, [code, resolvedMode, reactId]);
18055
19041
  return /* @__PURE__ */ jsxs(Box, { className: cn("not-prose my-4", className), children: [
18056
- error !== null && /* @__PURE__ */ jsxs(Box, { className: "space-y-2 mb-2", children: [
18057
- /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-error whitespace-pre-wrap", children: error }),
19042
+ unrenderable && /* @__PURE__ */ jsxs(Box, { className: "space-y-2 mb-2", "data-testid": "mermaid-unrenderable", children: [
19043
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-muted-foreground", children: t("mermaid.unrenderable") }),
18058
19044
  /* @__PURE__ */ jsx(CodeBlock, { code, language: "mermaid" })
18059
19045
  ] }),
18060
19046
  /* @__PURE__ */ jsx(
@@ -18063,7 +19049,7 @@ var init_MermaidDiagram = __esm({
18063
19049
  ref: containerRef,
18064
19050
  "data-testid": "mermaid-diagram",
18065
19051
  className: "overflow-x-auto",
18066
- style: error !== null ? { display: "none" } : void 0
19052
+ style: unrenderable ? { display: "none" } : void 0
18067
19053
  }
18068
19054
  )
18069
19055
  ] });
@@ -21418,14 +22404,14 @@ function useSafeEventBus2() {
21418
22404
  } };
21419
22405
  }
21420
22406
  }
21421
- var log7, lookStyles4, ButtonGroup;
22407
+ var log8, lookStyles4, ButtonGroup;
21422
22408
  var init_ButtonGroup = __esm({
21423
22409
  "components/core/molecules/ButtonGroup.tsx"() {
21424
22410
  "use client";
21425
22411
  init_cn();
21426
22412
  init_atoms();
21427
22413
  init_useEventBus();
21428
- log7 = createLogger("almadar:ui:button-group");
22414
+ log8 = createLogger("almadar:ui:button-group");
21429
22415
  lookStyles4 = {
21430
22416
  "right-aligned-buttons": "",
21431
22417
  "floating-bar": "fixed bottom-section left-1/2 -translate-x-1/2 shadow-elevation-toast bg-card p-card-sm rounded-container",
@@ -21506,7 +22492,7 @@ var init_ButtonGroup = __esm({
21506
22492
  {
21507
22493
  variant: "ghost",
21508
22494
  onClick: () => {
21509
- log7.debug("Filter clicked", { field: filter.field });
22495
+ log8.debug("Filter clicked", { field: filter.field });
21510
22496
  },
21511
22497
  children: filter.label
21512
22498
  },
@@ -24223,6 +25209,12 @@ function commandMatches(command, query) {
24223
25209
  if (matchesQuery(query, command.label)) return true;
24224
25210
  return (command.keywords ?? []).some((keyword) => matchesQuery(query, keyword));
24225
25211
  }
25212
+ function dispatchCommandPaletteCommand(command, deps) {
25213
+ if (command.disabled) return;
25214
+ if (command.event) deps.emit(`UI:${command.event}`, { commandId: command.id });
25215
+ if (command.action) deps.emit(`UI:${command.action}`, command.actionPayload ?? {});
25216
+ deps.onSelect?.(command);
25217
+ }
24226
25218
  var UNGROUPED, CommandPalette;
24227
25219
  var init_CommandPalette = __esm({
24228
25220
  "components/core/molecules/CommandPalette.tsx"() {
@@ -24277,9 +25269,7 @@ var init_CommandPalette = __esm({
24277
25269
  const handleSelect = useCallback(
24278
25270
  (command) => {
24279
25271
  if (command.disabled) return;
24280
- if (command.event) eventBus.emit(`UI:${command.event}`, { commandId: command.id });
24281
- if (command.action) eventBus.emit(`UI:${command.action}`, command.actionPayload ?? {});
24282
- onSelect?.(command);
25272
+ dispatchCommandPaletteCommand(command, { emit: eventBus.emit, onSelect });
24283
25273
  handleClose();
24284
25274
  },
24285
25275
  [eventBus, onSelect, handleClose]
@@ -29074,9 +30064,9 @@ function debug(...args) {
29074
30064
  const [first, ...rest] = args;
29075
30065
  const message = typeof first === "string" ? first : "<debug>";
29076
30066
  if (rest.length === 0 && typeof first === "string") {
29077
- log8.debug(message);
30067
+ log9.debug(message);
29078
30068
  } else {
29079
- log8.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
30069
+ log9.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
29080
30070
  }
29081
30071
  }
29082
30072
  function debugGroup(label) {
@@ -29104,11 +30094,11 @@ function toLogMetaValue(v) {
29104
30094
  }
29105
30095
  return String(v);
29106
30096
  }
29107
- var NAMESPACE, log8;
30097
+ var NAMESPACE, log9;
29108
30098
  var init_debug = __esm({
29109
30099
  "lib/debug.ts"() {
29110
30100
  NAMESPACE = "almadar:ui:debug";
29111
- log8 = createLogger(NAMESPACE);
30101
+ log9 = createLogger(NAMESPACE);
29112
30102
  createLogger("almadar:ui:debug:input");
29113
30103
  createLogger("almadar:ui:debug:collision");
29114
30104
  createLogger("almadar:ui:debug:physics");
@@ -30761,6 +31751,7 @@ var init_MathCanvas = __esm({
30761
31751
  init_perf();
30762
31752
  init_atoms();
30763
31753
  init_Stack();
31754
+ init_gameFonts();
30764
31755
  init_LearningCanvas();
30765
31756
  MathCanvas = ({
30766
31757
  className,
@@ -30780,6 +31771,7 @@ var init_MathCanvas = __esm({
30780
31771
  showTickLabels = false,
30781
31772
  tickLabelFontSize = 10,
30782
31773
  labelFontSize = 12,
31774
+ fontFamily: fontFamilyProp,
30783
31775
  showCurveLabels = false,
30784
31776
  curves = [],
30785
31777
  points = [],
@@ -30802,6 +31794,7 @@ var init_MathCanvas = __esm({
30802
31794
  error
30803
31795
  }) => {
30804
31796
  const eventBus = useEventBus();
31797
+ const fontFamily = resolveGameFontFamily(fontFamilyProp);
30805
31798
  const keyMapKey = keyMap ? JSON.stringify(keyMap) : null;
30806
31799
  const keyUpMapKey = keyUpMap ? JSON.stringify(keyUpMap) : null;
30807
31800
  const stableKeyMap = useMemo(() => keyMap, [keyMapKey]);
@@ -30851,18 +31844,18 @@ var init_MathCanvas = __esm({
30851
31844
  let kx = 0;
30852
31845
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
30853
31846
  if (kx % labelEveryX === 0 && x !== 0) {
30854
- out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: tickLabelFontSize, align: "center" });
31847
+ out.push({ type: "text", fontFamily, x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: tickLabelFontSize, align: "center" });
30855
31848
  }
30856
31849
  }
30857
31850
  const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
30858
31851
  let ky = 0;
30859
31852
  for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
30860
31853
  if (ky % labelEveryY === 0 && y !== 0) {
30861
- out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
31854
+ out.push({ type: "text", fontFamily, x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
30862
31855
  }
30863
31856
  }
30864
31857
  if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
30865
- out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
31858
+ out.push({ type: "text", fontFamily, x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
30866
31859
  }
30867
31860
  }
30868
31861
  for (const region of regions) {
@@ -30889,6 +31882,7 @@ var init_MathCanvas = __esm({
30889
31882
  const mid = Math.floor(region.samples.length / 2);
30890
31883
  out.push({
30891
31884
  type: "text",
31885
+ fontFamily,
30892
31886
  x: mapX((first.x + last.x) / 2),
30893
31887
  y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
30894
31888
  text: region.label,
@@ -30925,14 +31919,14 @@ var init_MathCanvas = __esm({
30925
31919
  const px = mapX(guide.at);
30926
31920
  out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
30927
31921
  if (guide.label) {
30928
- out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color, fontSize: 11 });
31922
+ out.push({ type: "text", fontFamily, x: px + 4, y: margin + 10, text: guide.label, color, fontSize: 11 });
30929
31923
  }
30930
31924
  } else {
30931
31925
  if (guide.at < yMin || guide.at > yMax) continue;
30932
31926
  const py = mapY(guide.at);
30933
31927
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
30934
31928
  if (guide.label) {
30935
- out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color, fontSize: labelFontSize, align: "right" });
31929
+ out.push({ type: "text", fontFamily, x: width - margin - 4, y: py - 8, text: guide.label, color, fontSize: labelFontSize, align: "right" });
30936
31930
  }
30937
31931
  }
30938
31932
  }
@@ -30971,6 +31965,7 @@ var init_MathCanvas = __esm({
30971
31965
  if (showCurveLabels && curve.label && lastInRange) {
30972
31966
  out.push({
30973
31967
  type: "text",
31968
+ fontFamily,
30974
31969
  x: mapX(lastInRange.x) + 6,
30975
31970
  y: mapY(lastInRange.y) - 6,
30976
31971
  text: curve.label,
@@ -31008,6 +32003,7 @@ var init_MathCanvas = __esm({
31008
32003
  if (hop.label) {
31009
32004
  out.push({
31010
32005
  type: "text",
32006
+ fontFamily,
31011
32007
  x: (x1 + x2) / 2,
31012
32008
  y: xAxisY - peak - 8,
31013
32009
  text: hop.label,
@@ -31035,6 +32031,7 @@ var init_MathCanvas = __esm({
31035
32031
  const rad = mid * Math.PI / 180;
31036
32032
  out.push({
31037
32033
  type: "text",
32034
+ fontFamily,
31038
32035
  x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
31039
32036
  y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
31040
32037
  text: angle.label,
@@ -31056,7 +32053,7 @@ var init_MathCanvas = __esm({
31056
32053
  fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
31057
32054
  });
31058
32055
  if (p.label) {
31059
- out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: p.color ?? "#111827", fontSize: labelFontSize });
32056
+ out.push({ type: "text", fontFamily, x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: p.color ?? "#111827", fontSize: labelFontSize });
31060
32057
  }
31061
32058
  }
31062
32059
  for (const v of vectors) {
@@ -31067,7 +32064,7 @@ var init_MathCanvas = __esm({
31067
32064
  const y2 = mapY(v.y + v.vy);
31068
32065
  out.push({ type: "arrow", x1, y1, x2, y2, color: v.color ?? "#7c3aed", lineWidth: 2 });
31069
32066
  if (v.label) {
31070
- out.push({ type: "text", x: x2 + 6, y: y2 - 6, text: v.label, color: v.color ?? "#7c3aed", fontSize: labelFontSize });
32067
+ out.push({ type: "text", fontFamily, x: x2 + 6, y: y2 - 6, text: v.label, color: v.color ?? "#7c3aed", fontSize: labelFontSize });
31071
32068
  }
31072
32069
  }
31073
32070
  out.push(...shapes);
@@ -31088,6 +32085,7 @@ var init_MathCanvas = __esm({
31088
32085
  showTickLabels,
31089
32086
  tickLabelFontSize,
31090
32087
  labelFontSize,
32088
+ fontFamily,
31091
32089
  showCurveLabels,
31092
32090
  curves,
31093
32091
  points,
@@ -31143,6 +32141,7 @@ var init_MathCanvas = __esm({
31143
32141
  width,
31144
32142
  height,
31145
32143
  backgroundColor,
32144
+ fontFamily,
31146
32145
  shapes: derivedShapes,
31147
32146
  drawables,
31148
32147
  projector,
@@ -32375,13 +33374,13 @@ var init_MapView = __esm({
32375
33374
  shadowSize: [41, 41]
32376
33375
  });
32377
33376
  L.Marker.prototype.options.icon = defaultIcon;
32378
- const { useEffect: useEffect64, useRef: useRef65, useCallback: useCallback93, useState: useState99 } = React87__default;
33377
+ const { useEffect: useEffect67, useRef: useRef69, useCallback: useCallback95, useState: useState102 } = React87__default;
32379
33378
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
32380
33379
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
32381
33380
  function MapUpdater({ centerLat, centerLng, zoom }) {
32382
33381
  const map = useMap();
32383
- const prevRef = useRef65({ centerLat, centerLng, zoom });
32384
- useEffect64(() => {
33382
+ const prevRef = useRef69({ centerLat, centerLng, zoom });
33383
+ useEffect67(() => {
32385
33384
  const prev = prevRef.current;
32386
33385
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
32387
33386
  map.setView([centerLat, centerLng], zoom);
@@ -32392,7 +33391,7 @@ var init_MapView = __esm({
32392
33391
  }
32393
33392
  function MapClickHandler({ onMapClick }) {
32394
33393
  const map = useMap();
32395
- useEffect64(() => {
33394
+ useEffect67(() => {
32396
33395
  if (!onMapClick) return;
32397
33396
  const handler = (e) => {
32398
33397
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -32420,8 +33419,8 @@ var init_MapView = __esm({
32420
33419
  showAttribution = true
32421
33420
  }) {
32422
33421
  const eventBus = useEventBus2();
32423
- const [clickedPosition, setClickedPosition] = useState99(null);
32424
- const handleMapClick = useCallback93((lat, lng) => {
33422
+ const [clickedPosition, setClickedPosition] = useState102(null);
33423
+ const handleMapClick = useCallback95((lat, lng) => {
32425
33424
  if (showClickedPin) {
32426
33425
  setClickedPosition({ lat, lng });
32427
33426
  }
@@ -32430,7 +33429,7 @@ var init_MapView = __esm({
32430
33429
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
32431
33430
  }
32432
33431
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
32433
- const handleMarkerClick = useCallback93((marker) => {
33432
+ const handleMarkerClick = useCallback95((marker) => {
32434
33433
  onMarkerClick?.(marker);
32435
33434
  if (markerClickEvent) {
32436
33435
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -47010,7 +48009,7 @@ function getAllEvents(traits2) {
47010
48009
  function EventDispatcherTab({ traits: traits2, schema }) {
47011
48010
  const eventBus = useEventBus();
47012
48011
  const { t } = useTranslate();
47013
- const [log9, setLog] = React87.useState([]);
48012
+ const [log11, setLog] = React87.useState([]);
47014
48013
  const prevStatesRef = React87.useRef(/* @__PURE__ */ new Map());
47015
48014
  React87.useEffect(() => {
47016
48015
  for (const trait of traits2) {
@@ -47074,9 +48073,9 @@ function EventDispatcherTab({ traits: traits2, schema }) {
47074
48073
  /* @__PURE__ */ jsx(Typography, { variant: "small", weight: "medium", className: "text-muted-foreground mb-1", children: t("debug.otherEvents") }),
47075
48074
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: unavailableEvents.map((event) => /* @__PURE__ */ jsx(Badge, { variant: "default", size: "sm", className: "opacity-50", children: event }, event)) })
47076
48075
  ] }),
47077
- log9.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
48076
+ log11.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
47078
48077
  /* @__PURE__ */ jsx(Typography, { variant: "small", weight: "medium", className: "text-muted-foreground mb-1", children: t("debug.recentTransitions") }),
47079
- /* @__PURE__ */ jsx(Stack, { gap: "xs", children: log9.map((entry, i) => /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
48078
+ /* @__PURE__ */ jsx(Stack, { gap: "xs", children: log11.map((entry, i) => /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
47080
48079
  /* @__PURE__ */ jsx("span", { className: "text-primary", children: entry.traitName }),
47081
48080
  " ",
47082
48081
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: entry.from }),
@@ -49267,6 +50266,7 @@ var init_component_registry_generated = __esm({
49267
50266
  init_FormSection();
49268
50267
  init_FormSectionHeader();
49269
50268
  init_FxOverlay();
50269
+ init_GameAudioCue();
49270
50270
  init_GameAudioToggle();
49271
50271
  init_GameHud();
49272
50272
  init_GameIcon();
@@ -49544,6 +50544,7 @@ var init_component_registry_generated = __esm({
49544
50544
  "FormLayout": FormLayout,
49545
50545
  "FormSectionHeader": FormSectionHeader,
49546
50546
  "FxOverlay": FxOverlay,
50547
+ "GameAudioCue": GameAudioCue,
49547
50548
  "GameAudioToggle": GameAudioToggle,
49548
50549
  "GameHud": GameHud,
49549
50550
  "GameIcon": GameIcon,
@@ -51518,7 +52519,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51518
52519
  };
51519
52520
  }, [traitBindings]);
51520
52521
  const executeTransitionEffects = useCallback(async (params) => {
51521
- const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log9 } = params;
52522
+ const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log11 } = params;
51522
52523
  const traitName = binding.trait.name;
51523
52524
  const linkedEntity = binding.linkedEntity || "";
51524
52525
  const entityId = payload?.entityId;
@@ -51647,7 +52648,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51647
52648
  if (sharedKey !== void 0) {
51648
52649
  sharedWrites.push({ field, value });
51649
52650
  }
51650
- log9.debug("set:write", {
52651
+ log11.debug("set:write", {
51651
52652
  traitName,
51652
52653
  field,
51653
52654
  value: JSON.stringify(value),
@@ -51715,7 +52716,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51715
52716
  mergeEntityFrame(sharedEntityStore.getSnapshot(sharedKey), sharedWrites)
51716
52717
  );
51717
52718
  }
51718
- log9.debug("effects:executed", () => ({
52719
+ log11.debug("effects:executed", () => ({
51719
52720
  traitName,
51720
52721
  transition: `${previousState}->${newState}`,
51721
52722
  event: flushEvent,
@@ -51725,7 +52726,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51725
52726
  slotsTouched: Array.from(pendingSlots.keys()).join(",")
51726
52727
  }));
51727
52728
  for (const [slot, patterns] of pendingSlots) {
51728
- log9.debug("flush:slot", {
52729
+ log11.debug("flush:slot", {
51729
52730
  traitName,
51730
52731
  slot,
51731
52732
  patternCount: patterns.length,
@@ -51743,7 +52744,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51743
52744
  publishBindingSnapshot(traitName, liveEntity);
51744
52745
  }
51745
52746
  } catch (error) {
51746
- log9.error("effects:error", {
52747
+ log11.error("effects:error", {
51747
52748
  traitName,
51748
52749
  transition: `${previousState}->${newState}`,
51749
52750
  event: flushEvent,
@@ -52925,11 +53926,20 @@ function BrowserPlayground({
52925
53926
  mode = "mock",
52926
53927
  initialPagePath,
52927
53928
  height,
52928
- className
53929
+ className,
53930
+ paused
52929
53931
  }) {
52930
53932
  const [runtime] = useState(
52931
53933
  () => new OrbitalServerRuntime({ mode, debug: false })
52932
53934
  );
53935
+ useEffect(() => {
53936
+ if (paused === void 0) return;
53937
+ if (paused) {
53938
+ runtime.pauseTicks();
53939
+ } else {
53940
+ runtime.resumeTicks();
53941
+ }
53942
+ }, [runtime, paused]);
52933
53943
  const registrationReady = useMemo(() => {
52934
53944
  const orbitalNames = schema.orbitals.map((o) => o.name);
52935
53945
  playgroundLog.debug("register:start", { schema: schema.name, orbitalNames });
@@ -52984,7 +53994,317 @@ function BrowserPlayground({
52984
53994
  );
52985
53995
  }
52986
53996
 
53997
+ // runtime/OrbitalPluginHost.tsx
53998
+ init_useEventBus();
53999
+ var log10 = createLogger("almadar:ui:plugin-host");
54000
+ var UI_SLOT_SET = new Set(UI_SLOTS);
54001
+ function isUISlot(value) {
54002
+ return UI_SLOT_SET.has(value);
54003
+ }
54004
+ var OrbitalPluginHostContext = createContext(null);
54005
+ function useOrbitalPluginHostContext(caller) {
54006
+ const ctx = useContext(OrbitalPluginHostContext);
54007
+ if (!ctx) {
54008
+ throw new Error(`${caller} must be used within an <OrbitalPluginHost>.`);
54009
+ }
54010
+ return ctx;
54011
+ }
54012
+ function isTraitState(value) {
54013
+ return value !== void 0 && typeof value.currentState === "string";
54014
+ }
54015
+ function toTraitState(runtime, orbital, trait, stateName, lastEvent) {
54016
+ if (runtime) {
54017
+ const raw = runtime.getState(orbital, trait);
54018
+ if (isTraitState(raw)) return raw;
54019
+ }
54020
+ return { traitName: trait, currentState: stateName, previousState: null, lastEvent, context: {} };
54021
+ }
54022
+ function isKeymapValue(value) {
54023
+ if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return false;
54024
+ return Object.values(value).every((entry) => Array.isArray(entry) && entry.every((k) => typeof k === "string"));
54025
+ }
54026
+ function findInlineTrait(schema, orbitalName, traitName) {
54027
+ const orbital = schema.orbitals.find((o) => o.name === orbitalName);
54028
+ if (!orbital) return void 0;
54029
+ for (const ref of orbital.traits) {
54030
+ if (typeof ref === "string") continue;
54031
+ if ("ref" in ref) continue;
54032
+ if (ref.name === traitName) return ref;
54033
+ }
54034
+ return void 0;
54035
+ }
54036
+ function readKeymapConfig(trait, knob) {
54037
+ const declared = trait?.config?.[knob]?.default;
54038
+ return isKeymapValue(declared) ? declared : {};
54039
+ }
54040
+ function traitNamesOf(orbital) {
54041
+ const names = [];
54042
+ for (const ref of orbital.traits) {
54043
+ if (typeof ref === "string") {
54044
+ names.push(ref);
54045
+ } else if ("ref" in ref) {
54046
+ if (ref.name) names.push(ref.name);
54047
+ } else {
54048
+ names.push(ref.name);
54049
+ }
54050
+ }
54051
+ return names;
54052
+ }
54053
+ function useDeclaredCaptureTable(opts) {
54054
+ const { pluginId, orbital, trait, keymapKnob = "keymap", target = "shell" } = opts;
54055
+ const ctx = useOrbitalPluginHostContext("useDeclaredCaptureTable");
54056
+ return useMemo(() => {
54057
+ const schema = ctx.getPluginSchema(pluginId);
54058
+ const inlineTrait = schema ? findInlineTrait(schema, orbital, trait) : void 0;
54059
+ const keymap = readKeymapConfig(inlineTrait, keymapKnob);
54060
+ const currentState = ctx.getState(pluginId, orbital, trait)?.currentState;
54061
+ const keys = currentState !== void 0 ? keymap[currentState] ?? [] : [];
54062
+ return { [target]: { mode: currentState ?? "unknown", keys: new Set(keys) } };
54063
+ }, [ctx, pluginId, orbital, trait, keymapKnob, target]);
54064
+ }
54065
+ function useOrbitalPluginHost() {
54066
+ const ctx = useOrbitalPluginHostContext("useOrbitalPluginHost");
54067
+ return { getState: ctx.getState, lastEvent: ctx.lastEvent, errors: ctx.errors };
54068
+ }
54069
+ function buildMockEffectHandlers(opts) {
54070
+ const { pluginId, denySet, slotsRef, navigateRef, notifyRef } = opts;
54071
+ const handlers = {
54072
+ renderUI: (slot, pattern, props, priority) => {
54073
+ if (!isUISlot(slot)) {
54074
+ log10.warn("renderUI:unknown-slot", { pluginId, slot });
54075
+ return;
54076
+ }
54077
+ if (pattern === null) {
54078
+ slotsRef.current.clear(slot);
54079
+ return;
54080
+ }
54081
+ slotsRef.current.render({ target: slot, pattern: pattern.type, props, priority, sourceTrait: pluginId });
54082
+ }
54083
+ };
54084
+ if (denySet.has("navigate")) {
54085
+ handlers.navigate = (path) => log10.warn("navigate:denied", { pluginId, path });
54086
+ } else if (navigateRef.current) {
54087
+ handlers.navigate = (path) => navigateRef.current?.(path);
54088
+ }
54089
+ if (denySet.has("notify")) {
54090
+ handlers.notify = (message, type) => log10.warn("notify:denied", { pluginId, message, type });
54091
+ } else if (notifyRef.current) {
54092
+ handlers.notify = (message, type) => notifyRef.current?.(message, type);
54093
+ }
54094
+ if (denySet.has("persist")) {
54095
+ handlers.persist = async (action, entityType) => {
54096
+ log10.warn("persist:denied", { pluginId, action, entityType });
54097
+ return void 0;
54098
+ };
54099
+ }
54100
+ if (denySet.has("call-service")) {
54101
+ handlers.callService = async (service, action) => {
54102
+ log10.warn("call-service:denied", { pluginId, service, action });
54103
+ return null;
54104
+ };
54105
+ }
54106
+ return handlers;
54107
+ }
54108
+ function PluginRuntimeMount({
54109
+ plugin,
54110
+ mode,
54111
+ transport,
54112
+ deny,
54113
+ navigate,
54114
+ notify,
54115
+ onTransition,
54116
+ onDispatched,
54117
+ onError
54118
+ }) {
54119
+ const bus = useEventBus();
54120
+ const slots = useUISlots();
54121
+ const slotsRef = useRef(slots);
54122
+ slotsRef.current = slots;
54123
+ const navigateRef = useRef(navigate);
54124
+ navigateRef.current = navigate;
54125
+ const notifyRef = useRef(notify);
54126
+ notifyRef.current = notify;
54127
+ const [mockRuntime] = useState(() => {
54128
+ if (mode !== "mock") return void 0;
54129
+ return new OrbitalServerRuntime({
54130
+ mode: "mock",
54131
+ debug: false,
54132
+ effectHandlers: buildMockEffectHandlers({
54133
+ pluginId: plugin.id,
54134
+ denySet: new Set(deny ?? []),
54135
+ slotsRef,
54136
+ navigateRef,
54137
+ notifyRef
54138
+ })
54139
+ });
54140
+ });
54141
+ const registrationReady = useMemo(() => {
54142
+ if (mockRuntime) return mockRuntime.register(plugin.schema);
54143
+ if (mode === "server" && transport) return transport.register(plugin.schema);
54144
+ return Promise.resolve();
54145
+ }, [mockRuntime, transport, mode, plugin.schema]);
54146
+ useEffect(() => {
54147
+ if (mode === "server" && !transport) {
54148
+ onError(plugin.id, 'OrbitalPluginHost: mode "server" requires a transport');
54149
+ }
54150
+ }, [mode, transport, plugin.id, onError]);
54151
+ const teardownTimerRef = useRef(null);
54152
+ useEffect(() => {
54153
+ if (teardownTimerRef.current !== null) {
54154
+ clearTimeout(teardownTimerRef.current);
54155
+ teardownTimerRef.current = null;
54156
+ }
54157
+ return () => {
54158
+ teardownTimerRef.current = setTimeout(() => {
54159
+ teardownTimerRef.current = null;
54160
+ if (mockRuntime) {
54161
+ mockRuntime.unregisterAll();
54162
+ } else if (mode === "server" && transport) {
54163
+ void transport.unregister();
54164
+ }
54165
+ }, 0);
54166
+ };
54167
+ }, [mockRuntime, transport, mode]);
54168
+ const ownOrbitals = useMemo(
54169
+ () => new Set(plugin.schema.orbitals.map((o) => o.name)),
54170
+ [plugin.schema]
54171
+ );
54172
+ useEffect(() => {
54173
+ let cancelled = false;
54174
+ void registrationReady.then(() => {
54175
+ if (cancelled || !mockRuntime) return;
54176
+ const states = [];
54177
+ for (const orbital of plugin.schema.orbitals) {
54178
+ for (const traitName of traitNamesOf(orbital)) {
54179
+ const raw = mockRuntime.getState(orbital.name, traitName);
54180
+ if (isTraitState(raw)) states.push([orbital.name, traitName, raw]);
54181
+ }
54182
+ }
54183
+ if (states.length > 0) onDispatched(plugin.id, null, states);
54184
+ }).catch((err) => {
54185
+ if (!cancelled) onError(plugin.id, err instanceof Error ? err.message : String(err));
54186
+ });
54187
+ return () => {
54188
+ cancelled = true;
54189
+ };
54190
+ }, [registrationReady, mockRuntime, plugin, onDispatched, onError]);
54191
+ const dispatch = useCallback(
54192
+ async (row, payload) => {
54193
+ await registrationReady;
54194
+ let response;
54195
+ if (mockRuntime) {
54196
+ response = await mockRuntime.processOrbitalEvent(row.orbital, {
54197
+ event: row.trigger,
54198
+ payload,
54199
+ targetTrait: row.trait
54200
+ });
54201
+ } else if (mode === "server" && transport) {
54202
+ response = await transport.sendEvent(row.orbital, row.trigger, payload);
54203
+ } else {
54204
+ return;
54205
+ }
54206
+ if (!response.success) {
54207
+ onError(plugin.id, response.error ?? `${plugin.id}: ${row.trigger} failed`);
54208
+ }
54209
+ for (const entry of response.emittedEvents ?? []) {
54210
+ if (entry.event === row.trigger) continue;
54211
+ const sourceOrbital = entry.source?.orbital;
54212
+ if (sourceOrbital !== void 0 && ownOrbitals.has(sourceOrbital)) {
54213
+ bus.emit(`UI:${entry.event}`, entry.payload, entry.source);
54214
+ }
54215
+ }
54216
+ const states = [];
54217
+ for (const [traitName, stateName] of Object.entries(response.states)) {
54218
+ states.push([row.orbital, traitName, toTraitState(mockRuntime, row.orbital, traitName, stateName, row.trigger)]);
54219
+ onTransition?.(plugin.id, row.orbital, traitName, stateName);
54220
+ }
54221
+ onDispatched(plugin.id, row.trigger, states);
54222
+ },
54223
+ [registrationReady, mockRuntime, mode, transport, plugin, bus, ownOrbitals, onError, onTransition, onDispatched]
54224
+ );
54225
+ useEffect(() => {
54226
+ const unsubs = plugin.inbound.map(
54227
+ (row) => bus.on(`UI:${row.busEvent}`, (event) => {
54228
+ void dispatch(row, event.payload).catch((err) => {
54229
+ onError(plugin.id, err instanceof Error ? err.message : String(err));
54230
+ });
54231
+ })
54232
+ );
54233
+ return () => {
54234
+ for (const unsub of unsubs) unsub();
54235
+ };
54236
+ }, [bus, plugin, dispatch, onError]);
54237
+ return null;
54238
+ }
54239
+ function OrbitalPluginHost({
54240
+ plugins,
54241
+ mode = "mock",
54242
+ transport,
54243
+ deny,
54244
+ navigate,
54245
+ notify,
54246
+ onTransition,
54247
+ children
54248
+ }) {
54249
+ const [tick, setTick] = useState(0);
54250
+ const [errors, setErrors] = useState({});
54251
+ const statesRef = useRef(/* @__PURE__ */ new Map());
54252
+ const lastEventRef = useRef(/* @__PURE__ */ new Map());
54253
+ const pluginSchemas = useMemo(
54254
+ () => new Map(plugins.map((plugin) => [plugin.id, plugin.schema])),
54255
+ [plugins]
54256
+ );
54257
+ const handleDispatched = useCallback(
54258
+ (pluginId, trigger, states) => {
54259
+ if (trigger !== null) {
54260
+ lastEventRef.current.set(pluginId, trigger);
54261
+ }
54262
+ let byKey = statesRef.current.get(pluginId);
54263
+ if (!byKey) {
54264
+ byKey = /* @__PURE__ */ new Map();
54265
+ statesRef.current.set(pluginId, byKey);
54266
+ }
54267
+ for (const [orbital, trait, state] of states) {
54268
+ byKey.set(`${orbital}.${trait}`, state);
54269
+ }
54270
+ setTick((t) => t + 1);
54271
+ },
54272
+ []
54273
+ );
54274
+ const handleError = useCallback((pluginId, message) => {
54275
+ setErrors((prev) => prev[pluginId] === message ? prev : { ...prev, [pluginId]: message });
54276
+ }, []);
54277
+ const contextValue = useMemo(
54278
+ () => ({
54279
+ getState: (pluginId, orbital, trait) => statesRef.current.get(pluginId)?.get(`${orbital}.${trait}`),
54280
+ lastEvent: (pluginId) => lastEventRef.current.get(pluginId),
54281
+ errors,
54282
+ tick,
54283
+ getPluginSchema: (pluginId) => pluginSchemas.get(pluginId)
54284
+ }),
54285
+ [errors, tick, pluginSchemas]
54286
+ );
54287
+ return /* @__PURE__ */ jsxs(OrbitalPluginHostContext.Provider, { value: contextValue, children: [
54288
+ plugins.map((plugin) => /* @__PURE__ */ jsx(
54289
+ PluginRuntimeMount,
54290
+ {
54291
+ plugin,
54292
+ mode,
54293
+ transport,
54294
+ deny,
54295
+ navigate,
54296
+ notify,
54297
+ onTransition,
54298
+ onDispatched: handleDispatched,
54299
+ onError: handleError
54300
+ },
54301
+ plugin.id
54302
+ )),
54303
+ children ?? null
54304
+ ] });
54305
+ }
54306
+
52987
54307
  // runtime/index.ts
52988
54308
  init_perf();
52989
54309
 
52990
- export { BrowserPlayground, OrbPreview, clearSchemaCache, createClientEffectHandlers, profilerOnRender, usePerfBuffer, useResolvedSchema, useTraitStateMachine };
54310
+ export { BrowserPlayground, OrbPreview, OrbitalPluginHost, clearSchemaCache, createClientEffectHandlers, profilerOnRender, useDeclaredCaptureTable, useOrbitalPluginHost, usePerfBuffer, useResolvedSchema, useTraitStateMachine };