@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
@@ -10501,7 +10501,7 @@ function collectDrawnItems(nodes) {
10501
10501
  for (const n of nodes) {
10502
10502
  switch (n.type) {
10503
10503
  case "draw-sprite":
10504
- if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id, anchor: n.anchor, width: n.width, height: n.height });
10504
+ if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id, anchor: n.anchor, width: n.width, height: n.height, rotation: n.rotation });
10505
10505
  break;
10506
10506
  case "draw-shape":
10507
10507
  case "draw-text":
@@ -10511,7 +10511,7 @@ function collectDrawnItems(nodes) {
10511
10511
  break;
10512
10512
  case "draw-sprite-layer":
10513
10513
  for (const it of n.items) {
10514
- if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id, anchor: it.anchor, width: it.width, height: it.height });
10514
+ if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id, anchor: it.anchor, width: it.width, height: it.height, rotation: it.rotation });
10515
10515
  }
10516
10516
  break;
10517
10517
  case "draw-shape-layer":
@@ -10531,15 +10531,39 @@ function buildHitIndex(items) {
10531
10531
  }
10532
10532
  return m;
10533
10533
  }
10534
+ function withPreviewPosition(nodes, id, pos) {
10535
+ return nodes.map((n) => {
10536
+ if (n.type === "draw-sprite-layer" || n.type === "draw-shape-layer" || n.type === "draw-text-layer") {
10537
+ if (!n.items.some((it) => it.id === id)) return n;
10538
+ return { ...n, items: n.items.map((it) => it.id === id ? { ...it, position: pos } : it) };
10539
+ }
10540
+ if (n.type === "draw-group") {
10541
+ if (!Array.isArray(n.items)) return n;
10542
+ return { ...n, items: withPreviewPosition(n.items, id, pos) };
10543
+ }
10544
+ if (n.id === id && "position" in n) {
10545
+ return { ...n, position: pos };
10546
+ }
10547
+ return n;
10548
+ });
10549
+ }
10534
10550
  function hitTestSprites(items, projector, point) {
10535
10551
  for (let i = items.length - 1; i >= 0; i--) {
10536
10552
  const it = items[i];
10537
10553
  if (it.id === void 0) continue;
10538
10554
  const r = spriteRect(projector, { position: it.pos, anchor: it.anchor, width: it.width, height: it.height });
10539
- if (point.x >= r.x && point.x <= r.x + r.w && point.y >= r.y && point.y <= r.y + r.h) return it.id;
10555
+ const test = it.rotation ? rotatePoint(point, { x: r.x + r.w / 2, y: r.y + r.h / 2 }, -it.rotation) : point;
10556
+ if (test.x >= r.x && test.x <= r.x + r.w && test.y >= r.y && test.y <= r.y + r.h) return it.id;
10540
10557
  }
10541
10558
  return void 0;
10542
10559
  }
10560
+ function rotatePoint(p, center, radians) {
10561
+ const cos = Math.cos(radians);
10562
+ const sin = Math.sin(radians);
10563
+ const dx = p.x - center.x;
10564
+ const dy = p.y - center.y;
10565
+ return { x: center.x + dx * cos - dy * sin, y: center.y + dx * sin + dy * cos };
10566
+ }
10543
10567
  var init_hitTest = __esm({
10544
10568
  "lib/drawable/hitTest.ts"() {
10545
10569
  init_contract();
@@ -10548,6 +10572,41 @@ var init_hitTest = __esm({
10548
10572
  function normalizeBackdrop(bg) {
10549
10573
  return typeof bg === "string" ? { url: bg, role: "decoration", category: "background" } : bg;
10550
10574
  }
10575
+ function selectionOverlayNodes(projector, item) {
10576
+ const r = spriteRect(projector, { position: item.pos, anchor: item.anchor, width: item.width, height: item.height });
10577
+ const tw = projector.tileWidth;
10578
+ const cellTopLeft = projector.anchorPoint(item.pos, "top-left");
10579
+ const offsetX = (r.x - cellTopLeft.x) / tw;
10580
+ const offsetY = (r.y - cellTopLeft.y) / tw;
10581
+ const width = r.w / tw;
10582
+ const height = r.h / tw;
10583
+ const handle = EDIT_HANDLE_SIZE_PX / tw;
10584
+ const ring = {
10585
+ type: "draw-shape",
10586
+ shape: "rect",
10587
+ position: item.pos,
10588
+ anchor: "top-left",
10589
+ offsetX,
10590
+ offsetY,
10591
+ width,
10592
+ height,
10593
+ stroke: EDIT_SELECTION_COLOR,
10594
+ strokeWidth: 2,
10595
+ fill: "none"
10596
+ };
10597
+ const handles = EDIT_SELECTION_CORNERS.map(([cx, cy]) => ({
10598
+ type: "draw-shape",
10599
+ shape: "rect",
10600
+ position: item.pos,
10601
+ anchor: "top-left",
10602
+ offsetX: offsetX + cx * width - handle / 2,
10603
+ offsetY: offsetY + cy * height - handle / 2,
10604
+ width: handle,
10605
+ height: handle,
10606
+ fill: EDIT_SELECTION_COLOR
10607
+ }));
10608
+ return [ring, ...handles];
10609
+ }
10551
10610
  function Canvas2D({
10552
10611
  className,
10553
10612
  isLoading = false,
@@ -10561,6 +10620,12 @@ function Canvas2D({
10561
10620
  tileLeaveEvent,
10562
10621
  keyMap,
10563
10622
  keyUpMap,
10623
+ editable = false,
10624
+ selectedId = null,
10625
+ onSelect,
10626
+ onMove,
10627
+ selectEvent,
10628
+ moveEvent,
10564
10629
  camera = "pan-zoom",
10565
10630
  scale = 0.4,
10566
10631
  tileWidth,
@@ -10818,10 +10883,22 @@ function Canvas2D({
10818
10883
  painter.scale(cam.zoom, cam.zoom);
10819
10884
  painter.translate(-viewportSize.width / 2 - cam.x, -viewportSize.height / 2 - cam.y);
10820
10885
  const dctx = { projector, time: timeMs, invalidate: bumpAtlas };
10821
- for (const node of drawables) paintDrawable(painter, node, dctx);
10886
+ let paintNodes = drawables;
10887
+ if (editable) {
10888
+ const drag = editDragRef.current;
10889
+ if (drag && drag.moved) {
10890
+ paintNodes = withPreviewPosition(paintNodes, drag.id, { x: drag.previewX, y: drag.previewY });
10891
+ }
10892
+ const selectedItem = selectedId != null ? drawnItems.find((it) => it.id === selectedId) : void 0;
10893
+ if (selectedItem) {
10894
+ const overlaySource = drag && drag.moved && drag.id === selectedId ? { ...selectedItem, pos: { x: drag.previewX, y: drag.previewY } } : selectedItem;
10895
+ paintNodes = [...paintNodes, ...selectionOverlayNodes(projector, overlaySource)];
10896
+ }
10897
+ }
10898
+ for (const node of paintNodes) paintDrawable(painter, node, dctx);
10822
10899
  painter.restore();
10823
- scheduleAnimation(drawables);
10824
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
10900
+ scheduleAnimation(paintNodes);
10901
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance, editable, selectedId, drawnItems]);
10825
10902
  React87.useEffect(() => {
10826
10903
  drawTimeRef.current = draw;
10827
10904
  }, [draw]);
@@ -10866,23 +10943,83 @@ function Canvas2D({
10866
10943
  };
10867
10944
  }, [camera, followTarget, lerpToTarget, draw]);
10868
10945
  const singlePointerActiveRef = React87.useRef(false);
10946
+ const editDragRef = React87.useRef(null);
10947
+ const pointerToScene = React87.useCallback((clientX, clientY) => {
10948
+ const canvas = canvasRef.current;
10949
+ if (!canvas) return { x: 0, y: 0 };
10950
+ const world = screenToWorld(clientX, clientY, canvas, viewportSize);
10951
+ const adjustedX = world.x - scaledTileWidth / 2;
10952
+ const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10953
+ return unproject(adjustedX, adjustedY);
10954
+ }, [screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject]);
10869
10955
  const handleCanvasPointerDown = React87.useCallback((e) => {
10870
10956
  singlePointerActiveRef.current = true;
10957
+ if (editable) {
10958
+ if (!canvasRef.current) return;
10959
+ const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
10960
+ const hitId = hitTestSprites(drawnItems, projector, world);
10961
+ if (hitId === void 0) return;
10962
+ const item = [...drawnItems].reverse().find((it) => it.id === hitId);
10963
+ if (!item) return;
10964
+ editDragRef.current = {
10965
+ id: hitId,
10966
+ pointerId: e.pointerId,
10967
+ startClientX: e.clientX,
10968
+ startClientY: e.clientY,
10969
+ startSceneX: item.pos.x,
10970
+ startSceneY: item.pos.y,
10971
+ moved: false,
10972
+ previewX: item.pos.x,
10973
+ previewY: item.pos.y
10974
+ };
10975
+ return;
10976
+ }
10871
10977
  if (enableCamera) handlePointerDown(e);
10872
- }, [enableCamera, handlePointerDown]);
10978
+ }, [editable, screenToWorld, viewportSize, drawnItems, projector, enableCamera, handlePointerDown]);
10873
10979
  const handleCanvasPointerMove = React87.useCallback((e) => {
10980
+ if (editable) {
10981
+ const drag = editDragRef.current;
10982
+ if (!drag || drag.pointerId !== e.pointerId) return;
10983
+ const dxPx = e.clientX - drag.startClientX;
10984
+ const dyPx = e.clientY - drag.startClientY;
10985
+ if (!drag.moved && Math.abs(dxPx) + Math.abs(dyPx) <= 5) return;
10986
+ drag.moved = true;
10987
+ const nowScene = pointerToScene(e.clientX, e.clientY);
10988
+ const startScene = pointerToScene(drag.startClientX, drag.startClientY);
10989
+ drag.previewX = drag.startSceneX + (nowScene.x - startScene.x);
10990
+ drag.previewY = drag.startSceneY + (nowScene.y - startScene.y);
10991
+ draw();
10992
+ return;
10993
+ }
10874
10994
  if (enableCamera) handlePointerMove(e, () => draw());
10875
- }, [enableCamera, handlePointerMove, draw]);
10995
+ }, [editable, pointerToScene, draw, enableCamera, handlePointerMove]);
10876
10996
  const handleCanvasHover = React87.useCallback((e) => {
10877
10997
  if (singlePointerActiveRef.current) return;
10878
10998
  if (!tileHoverEvent || !canvasRef.current) return;
10879
- const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
10880
- const adjustedX = world.x - scaledTileWidth / 2;
10881
- const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10882
- const isoPos = unproject(adjustedX, adjustedY);
10999
+ const isoPos = pointerToScene(e.clientX, e.clientY);
10883
11000
  eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
10884
- }, [screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject, tileHoverEvent, eventBus]);
11001
+ }, [pointerToScene, tileHoverEvent, eventBus]);
10885
11002
  const handleCanvasPointerUp = React87.useCallback((e) => {
11003
+ if (editable) {
11004
+ singlePointerActiveRef.current = false;
11005
+ const drag = editDragRef.current;
11006
+ if (drag && drag.pointerId === e.pointerId) {
11007
+ editDragRef.current = null;
11008
+ if (drag.moved) {
11009
+ onMove?.(drag.id, drag.previewX, drag.previewY);
11010
+ if (moveEvent) eventBus.emit(`UI:${moveEvent}`, { id: drag.id, x: drag.previewX, y: drag.previewY });
11011
+ draw();
11012
+ return;
11013
+ }
11014
+ const next = selectedId === drag.id ? null : drag.id;
11015
+ onSelect?.(next);
11016
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: next });
11017
+ return;
11018
+ }
11019
+ onSelect?.(null);
11020
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: null });
11021
+ return;
11022
+ }
10886
11023
  singlePointerActiveRef.current = false;
10887
11024
  if (enableCamera) handlePointerUp();
10888
11025
  if (dragDistance() > 5) return;
@@ -10893,16 +11030,14 @@ function Canvas2D({
10893
11030
  eventBus.emit(`UI:${unitClickEvent}`, { unitId: spriteHit });
10894
11031
  return;
10895
11032
  }
10896
- const adjustedX = world.x - scaledTileWidth / 2;
10897
- const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10898
- const isoPos = unproject(adjustedX, adjustedY);
11033
+ const isoPos = pointerToScene(e.clientX, e.clientY);
10899
11034
  const hitId = hitIndex.get(`${isoPos.x},${isoPos.y}`);
10900
11035
  if (hitId !== void 0 && unitClickEvent) {
10901
11036
  eventBus.emit(`UI:${unitClickEvent}`, { unitId: hitId });
10902
11037
  } else if (tileClickEvent) {
10903
11038
  eventBus.emit(`UI:${tileClickEvent}`, { x: isoPos.x, y: isoPos.y });
10904
11039
  }
10905
- }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject, hitIndex, drawnItems, projector, tileClickEvent, unitClickEvent, eventBus]);
11040
+ }, [editable, selectedId, onMove, moveEvent, onSelect, selectEvent, eventBus, draw, enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, drawnItems, projector, tileClickEvent, unitClickEvent, hitIndex, pointerToScene]);
10906
11041
  const handleCanvasPointerLeave = React87.useCallback(() => {
10907
11042
  handleMouseLeave();
10908
11043
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -10921,7 +11056,7 @@ function Canvas2D({
10921
11056
  }, [enableCamera, handlePointerUp]);
10922
11057
  const gestureHandlers = useCanvasGestures({
10923
11058
  canvasRef,
10924
- enabled: enableCamera || !!tileHoverEvent || !!tileClickEvent || !!unitClickEvent,
11059
+ enabled: enableCamera || !!tileHoverEvent || !!tileClickEvent || !!unitClickEvent || editable,
10925
11060
  onPointerDown: handleCanvasPointerDown,
10926
11061
  onPointerMove: handleCanvasPointerMove,
10927
11062
  onPointerUp: handleCanvasPointerUp,
@@ -11053,7 +11188,7 @@ function Canvas2D({
11053
11188
  }
11054
11189
  ) });
11055
11190
  }
11056
- var canvas2DLog;
11191
+ var canvas2DLog, EDIT_SELECTION_COLOR, EDIT_HANDLE_SIZE_PX, EDIT_SELECTION_CORNERS;
11057
11192
  var init_Canvas2D = __esm({
11058
11193
  "components/game/molecules/Canvas2D.tsx"() {
11059
11194
  "use client";
@@ -11078,8 +11213,12 @@ var init_Canvas2D = __esm({
11078
11213
  init_DrawGroup();
11079
11214
  init_registry();
11080
11215
  init_hitTest();
11216
+ init_contract();
11081
11217
  init_isometric();
11082
11218
  canvas2DLog = logger.createLogger("almadar:ui:game-canvas");
11219
+ EDIT_SELECTION_COLOR = "#3b82f6";
11220
+ EDIT_HANDLE_SIZE_PX = 8;
11221
+ EDIT_SELECTION_CORNERS = [[0, 0], [1, 0], [0, 1], [1, 1]];
11083
11222
  Canvas2D.displayName = "Canvas2D";
11084
11223
  }
11085
11224
  });
@@ -11121,6 +11260,12 @@ function Canvas({
11121
11260
  featureClickEvent,
11122
11261
  keyMap,
11123
11262
  keyUpMap,
11263
+ editable,
11264
+ selectedId,
11265
+ onSelect,
11266
+ onMove,
11267
+ selectEvent,
11268
+ moveEvent,
11124
11269
  children
11125
11270
  }) {
11126
11271
  canvasLog.debug("Canvas render", { mode, drawablesCount: drawables?.length, projection, camera: camera ? JSON.stringify(camera) : void 0 });
@@ -11198,6 +11343,12 @@ function Canvas({
11198
11343
  tileLeaveEvent,
11199
11344
  keyMap,
11200
11345
  keyUpMap,
11346
+ editable,
11347
+ selectedId,
11348
+ onSelect,
11349
+ onMove,
11350
+ selectEvent,
11351
+ moveEvent,
11201
11352
  ...children !== void 0 ? { children } : {}
11202
11353
  }
11203
11354
  );
@@ -11219,15 +11370,19 @@ function GameAudioToggle({
11219
11370
  size = "sm",
11220
11371
  className,
11221
11372
  onAsset,
11222
- offAsset
11373
+ offAsset,
11374
+ toggleEvent
11223
11375
  }) {
11224
11376
  const ctx = providers.useGameAudioContextOptional();
11225
11377
  const [localMuted, setLocalMuted] = React87.useState(false);
11226
11378
  const muted = ctx ? ctx.muted : localMuted;
11227
11379
  const setMuted = ctx ? ctx.setMuted : setLocalMuted;
11380
+ const eventBus = useEventBus();
11228
11381
  const handleToggle = React87.useCallback(() => {
11229
- setMuted(!muted);
11230
- }, [muted, setMuted]);
11382
+ const next = !muted;
11383
+ setMuted(next);
11384
+ if (toggleEvent) eventBus.emit(`UI:${toggleEvent}`, { muted: next });
11385
+ }, [muted, setMuted, toggleEvent, eventBus]);
11231
11386
  const activeAsset = muted ? offAsset : onAsset;
11232
11387
  return /* @__PURE__ */ jsxRuntime.jsx(
11233
11388
  Button,
@@ -11246,10 +11401,319 @@ var init_GameAudioToggle = __esm({
11246
11401
  "use client";
11247
11402
  init_atoms();
11248
11403
  init_cn();
11404
+ init_useEventBus();
11249
11405
  init_GameIcon();
11250
11406
  GameAudioToggle.displayName = "GameAudioToggle";
11251
11407
  }
11252
11408
  });
11409
+ function pickPath(entry) {
11410
+ if (Array.isArray(entry.path)) {
11411
+ return entry.path[Math.floor(Math.random() * entry.path.length)];
11412
+ }
11413
+ return entry.path;
11414
+ }
11415
+ function useGameAudio({
11416
+ manifest,
11417
+ baseUrl = "",
11418
+ initialMuted = false,
11419
+ initialVolume = 1
11420
+ }) {
11421
+ const [muted, setMutedState] = React87.useState(initialMuted);
11422
+ const [masterVolume, setMasterVolumeState] = React87.useState(initialVolume);
11423
+ const mutedRef = React87.useRef(muted);
11424
+ const volumeRef = React87.useRef(masterVolume);
11425
+ const manifestRef = React87.useRef(manifest);
11426
+ mutedRef.current = muted;
11427
+ volumeRef.current = masterVolume;
11428
+ manifestRef.current = manifest;
11429
+ const poolsRef = React87.useRef(/* @__PURE__ */ new Map());
11430
+ const getOrCreateElement = React87.useCallback((key) => {
11431
+ const entry = manifestRef.current[key];
11432
+ if (!entry) return null;
11433
+ let pool = poolsRef.current.get(key);
11434
+ if (!pool) {
11435
+ pool = [];
11436
+ poolsRef.current.set(key, pool);
11437
+ }
11438
+ const maxSize = entry.poolSize ?? 1;
11439
+ for (const audio of pool) {
11440
+ if (audio.paused && (audio.ended || audio.currentTime === 0)) {
11441
+ return audio;
11442
+ }
11443
+ }
11444
+ if (pool.length < maxSize) {
11445
+ const src = baseUrl + pickPath(entry);
11446
+ const audio = new Audio(src);
11447
+ audio.loop = entry.loop ?? false;
11448
+ pool.push(audio);
11449
+ return audio;
11450
+ }
11451
+ if (!entry.loop) {
11452
+ let oldest = pool[0];
11453
+ for (const audio of pool) {
11454
+ if (audio.currentTime > oldest.currentTime) {
11455
+ oldest = audio;
11456
+ }
11457
+ }
11458
+ oldest.pause();
11459
+ oldest.currentTime = 0;
11460
+ return oldest;
11461
+ }
11462
+ return null;
11463
+ }, [baseUrl]);
11464
+ const play = React87.useCallback((key) => {
11465
+ if (mutedRef.current) return;
11466
+ const entry = manifestRef.current[key];
11467
+ if (!entry) return;
11468
+ const audio = getOrCreateElement(key);
11469
+ if (!audio) return;
11470
+ audio.volume = Math.min(1, (entry.volume ?? 1) * volumeRef.current);
11471
+ if (!entry.loop) {
11472
+ audio.currentTime = 0;
11473
+ }
11474
+ const promise = audio.play();
11475
+ if (promise) {
11476
+ promise.catch(() => {
11477
+ });
11478
+ }
11479
+ }, [getOrCreateElement]);
11480
+ const stop = React87.useCallback((key) => {
11481
+ const pool = poolsRef.current.get(key);
11482
+ if (!pool) return;
11483
+ for (const audio of pool) {
11484
+ audio.pause();
11485
+ audio.currentTime = 0;
11486
+ }
11487
+ }, []);
11488
+ const currentMusicKeyRef = React87.useRef(null);
11489
+ const currentMusicElRef = React87.useRef(null);
11490
+ const musicFadeRef = React87.useRef(null);
11491
+ const pendingMusicKeyRef = React87.useRef(null);
11492
+ const clearMusicFade = React87.useCallback(() => {
11493
+ if (musicFadeRef.current) {
11494
+ clearInterval(musicFadeRef.current);
11495
+ musicFadeRef.current = null;
11496
+ }
11497
+ }, []);
11498
+ const playMusic = React87.useCallback((key) => {
11499
+ if (key === currentMusicKeyRef.current) return;
11500
+ pendingMusicKeyRef.current = key;
11501
+ const entry = manifestRef.current[key];
11502
+ if (!entry) return;
11503
+ const fadeDurationMs = entry.crossfadeDurationMs ?? 1500;
11504
+ const stepMs = 50;
11505
+ const totalSteps = Math.max(1, fadeDurationMs / stepMs);
11506
+ const targetVolume = Math.min(1, (entry.volume ?? 1) * volumeRef.current);
11507
+ clearMusicFade();
11508
+ const src = baseUrl + (Array.isArray(entry.path) ? entry.path[0] : entry.path);
11509
+ const incoming = new Audio(src);
11510
+ incoming.loop = true;
11511
+ incoming.volume = 0;
11512
+ const outgoing = currentMusicElRef.current;
11513
+ const outgoingStartVol = outgoing?.volume ?? 0;
11514
+ currentMusicKeyRef.current = key;
11515
+ currentMusicElRef.current = incoming;
11516
+ if (!mutedRef.current) {
11517
+ incoming.play().catch(() => {
11518
+ currentMusicKeyRef.current = null;
11519
+ currentMusicElRef.current = outgoing;
11520
+ });
11521
+ }
11522
+ let step = 0;
11523
+ musicFadeRef.current = setInterval(() => {
11524
+ step++;
11525
+ const progress = Math.min(step / totalSteps, 1);
11526
+ incoming.volume = Math.min(1, targetVolume * progress);
11527
+ if (outgoing) {
11528
+ outgoing.volume = Math.max(0, outgoingStartVol * (1 - progress));
11529
+ }
11530
+ if (progress >= 1) {
11531
+ clearMusicFade();
11532
+ if (outgoing) {
11533
+ outgoing.pause();
11534
+ outgoing.src = "";
11535
+ }
11536
+ }
11537
+ }, stepMs);
11538
+ }, [baseUrl, clearMusicFade]);
11539
+ const stopMusic = React87.useCallback((fadeDurationMs = 1e3) => {
11540
+ const outgoing = currentMusicElRef.current;
11541
+ if (!outgoing) return;
11542
+ currentMusicKeyRef.current = null;
11543
+ currentMusicElRef.current = null;
11544
+ pendingMusicKeyRef.current = null;
11545
+ clearMusicFade();
11546
+ const startVolume = outgoing.volume;
11547
+ const stepMs = 50;
11548
+ const totalSteps = Math.max(1, fadeDurationMs / stepMs);
11549
+ let step = 0;
11550
+ musicFadeRef.current = setInterval(() => {
11551
+ step++;
11552
+ const progress = step / totalSteps;
11553
+ outgoing.volume = Math.max(0, startVolume * (1 - progress));
11554
+ if (progress >= 1) {
11555
+ clearMusicFade();
11556
+ outgoing.pause();
11557
+ outgoing.src = "";
11558
+ }
11559
+ }, stepMs);
11560
+ }, [clearMusicFade]);
11561
+ const stopAll = React87.useCallback(() => {
11562
+ for (const pool of poolsRef.current.values()) {
11563
+ for (const audio of pool) {
11564
+ audio.pause();
11565
+ audio.currentTime = 0;
11566
+ }
11567
+ }
11568
+ stopMusic(0);
11569
+ }, [stopMusic]);
11570
+ const setMuted = React87.useCallback((value) => {
11571
+ setMutedState(value);
11572
+ if (value) {
11573
+ for (const [key, pool] of poolsRef.current.entries()) {
11574
+ if (manifestRef.current[key]?.loop) {
11575
+ for (const audio of pool) {
11576
+ if (!audio.paused) audio.pause();
11577
+ }
11578
+ }
11579
+ }
11580
+ currentMusicElRef.current?.pause();
11581
+ } else {
11582
+ for (const [key, pool] of poolsRef.current.entries()) {
11583
+ const entry = manifestRef.current[key];
11584
+ if (entry?.loop && entry?.autostart) {
11585
+ for (const audio of pool) {
11586
+ if (audio.paused) audio.play().catch(() => {
11587
+ });
11588
+ }
11589
+ }
11590
+ }
11591
+ const musicEl = currentMusicElRef.current;
11592
+ if (musicEl) {
11593
+ musicEl.play().catch(() => {
11594
+ });
11595
+ }
11596
+ }
11597
+ }, []);
11598
+ const setMasterVolume = React87.useCallback((volume) => {
11599
+ const clamped = Math.max(0, Math.min(1, volume));
11600
+ setMasterVolumeState(clamped);
11601
+ for (const [key, pool] of poolsRef.current.entries()) {
11602
+ const entryVol = manifestRef.current[key]?.volume ?? 1;
11603
+ for (const audio of pool) {
11604
+ audio.volume = Math.min(1, entryVol * clamped);
11605
+ }
11606
+ }
11607
+ if (!musicFadeRef.current && currentMusicElRef.current) {
11608
+ const key = currentMusicKeyRef.current;
11609
+ const entryVol = key ? manifestRef.current[key]?.volume ?? 1 : 1;
11610
+ currentMusicElRef.current.volume = Math.min(1, entryVol * clamped);
11611
+ }
11612
+ }, []);
11613
+ const unlockedRef = React87.useRef(false);
11614
+ React87.useEffect(() => {
11615
+ const autoKeys = Object.keys(manifest).filter((k) => manifest[k].autostart);
11616
+ const hasPendingMusic = () => pendingMusicKeyRef.current !== null;
11617
+ const hasAutoStart = autoKeys.length > 0;
11618
+ if (!hasAutoStart && !hasPendingMusic()) return;
11619
+ const unlock = () => {
11620
+ if (unlockedRef.current) return;
11621
+ unlockedRef.current = true;
11622
+ if (!mutedRef.current) {
11623
+ for (const key of autoKeys) {
11624
+ play(key);
11625
+ }
11626
+ const pending = pendingMusicKeyRef.current;
11627
+ if (pending && pending !== currentMusicKeyRef.current) {
11628
+ playMusic(pending);
11629
+ }
11630
+ }
11631
+ };
11632
+ document.addEventListener("click", unlock, { once: true });
11633
+ document.addEventListener("keydown", unlock, { once: true });
11634
+ document.addEventListener("touchstart", unlock, { once: true });
11635
+ return () => {
11636
+ document.removeEventListener("click", unlock);
11637
+ document.removeEventListener("keydown", unlock);
11638
+ document.removeEventListener("touchstart", unlock);
11639
+ };
11640
+ }, [manifest, play, playMusic]);
11641
+ React87.useEffect(() => {
11642
+ return () => {
11643
+ clearMusicFade();
11644
+ for (const pool of poolsRef.current.values()) {
11645
+ for (const audio of pool) {
11646
+ audio.pause();
11647
+ audio.src = "";
11648
+ }
11649
+ }
11650
+ poolsRef.current.clear();
11651
+ if (currentMusicElRef.current) {
11652
+ currentMusicElRef.current.pause();
11653
+ currentMusicElRef.current.src = "";
11654
+ currentMusicElRef.current = null;
11655
+ }
11656
+ };
11657
+ }, [clearMusicFade]);
11658
+ return {
11659
+ play,
11660
+ stop,
11661
+ stopAll,
11662
+ playMusic,
11663
+ stopMusic,
11664
+ muted,
11665
+ setMuted,
11666
+ masterVolume,
11667
+ setMasterVolume
11668
+ };
11669
+ }
11670
+ var init_useGameAudio = __esm({
11671
+ "hooks/useGameAudio.ts"() {
11672
+ "use client";
11673
+ useGameAudio.displayName = "useGameAudio";
11674
+ }
11675
+ });
11676
+ function GameAudioCue({
11677
+ cue,
11678
+ cueSeq,
11679
+ music,
11680
+ muted,
11681
+ volume,
11682
+ manifest,
11683
+ baseUrl
11684
+ }) {
11685
+ const { play, playMusic, stopMusic, setMuted, setMasterVolume } = useGameAudio({
11686
+ manifest,
11687
+ baseUrl,
11688
+ initialMuted: muted,
11689
+ initialVolume: volume
11690
+ });
11691
+ const prevCueSeqRef = React87.useRef(cueSeq);
11692
+ React87.useEffect(() => {
11693
+ if (cue && cueSeq !== void 0 && cueSeq !== prevCueSeqRef.current) {
11694
+ play(cue);
11695
+ }
11696
+ prevCueSeqRef.current = cueSeq;
11697
+ }, [cue, cueSeq, play]);
11698
+ React87.useEffect(() => {
11699
+ if (music) playMusic(music);
11700
+ else stopMusic();
11701
+ }, [music, playMusic, stopMusic]);
11702
+ React87.useEffect(() => {
11703
+ if (muted !== void 0) setMuted(muted);
11704
+ }, [muted, setMuted]);
11705
+ React87.useEffect(() => {
11706
+ if (volume !== void 0) setMasterVolume(volume);
11707
+ }, [volume, setMasterVolume]);
11708
+ return null;
11709
+ }
11710
+ var init_GameAudioCue = __esm({
11711
+ "components/game/atoms/GameAudioCue.tsx"() {
11712
+ "use client";
11713
+ init_useGameAudio();
11714
+ GameAudioCue.displayName = "GameAudioCue";
11715
+ }
11716
+ });
11253
11717
  function isKnownState(s) {
11254
11718
  return s in DEFAULT_STATE_STYLES;
11255
11719
  }
@@ -11948,15 +12412,18 @@ var init_StateJsonView = __esm({
11948
12412
  StateJsonView.displayName = "StateJsonView";
11949
12413
  }
11950
12414
  });
11951
- var GAME_FONTS, GameShell;
11952
- var init_GameShell = __esm({
11953
- "components/game/templates/GameShell.tsx"() {
11954
- init_cn();
11955
- init_Box();
11956
- init_Card();
11957
- init_Typography();
11958
- init_AtlasImage();
11959
- GAME_FONTS = {
12415
+
12416
+ // lib/gameFonts.ts
12417
+ function resolveGameFontFamily(input) {
12418
+ if (!input) return void 0;
12419
+ const resolved = GAME_FONT_KEYS[input];
12420
+ if (resolved) return `'${resolved}', ui-sans-serif, system-ui, sans-serif`;
12421
+ return input;
12422
+ }
12423
+ var GAME_FONT_KEYS;
12424
+ var init_gameFonts = __esm({
12425
+ "lib/gameFonts.ts"() {
12426
+ GAME_FONT_KEYS = {
11960
12427
  fredoka: "Fredoka",
11961
12428
  future: "Kenney Future",
11962
12429
  "future-narrow": "Kenney Future Narrow",
@@ -11964,6 +12431,17 @@ var init_GameShell = __esm({
11964
12431
  blocks: "Kenney Blocks",
11965
12432
  mini: "Kenney Mini"
11966
12433
  };
12434
+ }
12435
+ });
12436
+ var GameShell;
12437
+ var init_GameShell = __esm({
12438
+ "components/game/templates/GameShell.tsx"() {
12439
+ init_cn();
12440
+ init_gameFonts();
12441
+ init_Box();
12442
+ init_Card();
12443
+ init_Typography();
12444
+ init_AtlasImage();
11967
12445
  GameShell = ({
11968
12446
  appName = "Game",
11969
12447
  hud,
@@ -11977,7 +12455,7 @@ var init_GameShell = __esm({
11977
12455
  fontFamily,
11978
12456
  "data-theme": dataTheme
11979
12457
  }) => {
11980
- const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
12458
+ const displayFont = resolveGameFontFamily(fontFamily);
11981
12459
  return /* @__PURE__ */ jsxRuntime.jsxs(
11982
12460
  Box,
11983
12461
  {
@@ -11996,7 +12474,7 @@ var init_GameShell = __esm({
11996
12474
  // passed — an always-on inline stamp would shadow the orbital's
11997
12475
  // inline-theme font-family-display token (inline style beats the
11998
12476
  // theme provider's vars for the whole shell subtree).
11999
- ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
12477
+ ...displayFont ? { "--font-family-display": displayFont } : {}
12000
12478
  },
12001
12479
  children: [
12002
12480
  backgroundAsset && /* @__PURE__ */ jsxRuntime.jsx(
@@ -12194,14 +12672,15 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
12194
12672
  ctx.closePath();
12195
12673
  ctx.fill();
12196
12674
  }
12197
- function drawShape(ctx, shape, width, height, allShapes) {
12675
+ function drawShape(ctx, shape, width, height, allShapes, fontFamily) {
12198
12676
  ctx.save();
12199
12677
  const opacity = shape.opacity ?? 1;
12200
12678
  ctx.globalAlpha = opacity;
12201
12679
  const stroke = resolveColor2(shape.color, ctx, "#333333");
12202
12680
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
12203
12681
  ctx.lineWidth = shape.lineWidth ?? 2;
12204
- if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
12682
+ const dashPattern = shape.dash ? DASH_PATTERNS[shape.dash] : void 0;
12683
+ if (dashPattern) ctx.setLineDash([...dashPattern]);
12205
12684
  switch (shape.type) {
12206
12685
  case "grid": {
12207
12686
  const step = shape.step ?? 40;
@@ -12323,7 +12802,7 @@ function drawShape(ctx, shape, width, height, allShapes) {
12323
12802
  case "text": {
12324
12803
  if (shape.x == null || shape.y == null || !shape.text) break;
12325
12804
  ctx.fillStyle = stroke;
12326
- ctx.font = `${shape.fontSize ?? 14}px ${themeBodyFont(ctx.canvas)}`;
12805
+ ctx.font = `${shape.fontSize ?? 14}px ${shape.fontFamily ?? fontFamily ?? themeBodyFont(ctx.canvas)}`;
12327
12806
  ctx.textAlign = shape.align ?? "left";
12328
12807
  ctx.textBaseline = "middle";
12329
12808
  ctx.fillText(shape.text, shape.x, shape.y);
@@ -12365,7 +12844,7 @@ function drawShape(ctx, shape, width, height, allShapes) {
12365
12844
  }
12366
12845
  ctx.restore();
12367
12846
  }
12368
- function readoutShapes(readouts, width) {
12847
+ function readoutShapes(readouts, width, fontFamily) {
12369
12848
  const out = [];
12370
12849
  const chipH = 18;
12371
12850
  const gap = 6;
@@ -12389,13 +12868,14 @@ function readoutShapes(readouts, width) {
12389
12868
  text,
12390
12869
  color: "#ffffff",
12391
12870
  fontSize: 10,
12392
- align: "center"
12871
+ align: "center",
12872
+ fontFamily
12393
12873
  });
12394
12874
  rightEdge = chipX - gap;
12395
12875
  }
12396
12876
  return out;
12397
12877
  }
12398
- function traceShapes(panel, k, width, height) {
12878
+ function traceShapes(panel, k, width, height, fontFamily) {
12399
12879
  const w = panel.width ?? Math.round(width * 0.32);
12400
12880
  const h = panel.height ?? Math.round(height * 0.28);
12401
12881
  const x = panel.x ?? width - w - 8;
@@ -12449,14 +12929,14 @@ function traceShapes(panel, k, width, height) {
12449
12929
  out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
12450
12930
  }
12451
12931
  if (series.label) {
12452
- out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
12932
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9, fontFamily });
12453
12933
  }
12454
12934
  });
12455
12935
  if (panel.yLabel) {
12456
- out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
12936
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right", fontFamily });
12457
12937
  }
12458
12938
  if (panel.xLabel) {
12459
- out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
12939
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right", fontFamily });
12460
12940
  }
12461
12941
  return out;
12462
12942
  }
@@ -12479,13 +12959,14 @@ var init_LearningCanvas = __esm({
12479
12959
  init_useEventBus();
12480
12960
  init_webPainter2d();
12481
12961
  init_paintDispatch();
12482
- DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12962
+ DASH_PATTERNS = { solid: [], dashed: [6, 4], dotted: [2, 3] };
12483
12963
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
12484
12964
  LearningCanvas = ({
12485
12965
  className,
12486
12966
  width = 600,
12487
12967
  height = 400,
12488
12968
  backgroundColor,
12969
+ fontFamily,
12489
12970
  shapes = [],
12490
12971
  drawables,
12491
12972
  projector,
@@ -12521,10 +13002,10 @@ var init_LearningCanvas = __esm({
12521
13002
  }, [shapes]);
12522
13003
  const derivedShapes = React87.useMemo(() => {
12523
13004
  if (!traces?.length && !readouts?.length) return shapes;
12524
- const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
12525
- const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
13005
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height, fontFamily));
13006
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width, fontFamily) : [];
12526
13007
  return [...shapes, ...traceOut, ...readoutOut];
12527
- }, [shapes, traces, readouts, width, height]);
13008
+ }, [shapes, traces, readouts, width, height, fontFamily]);
12528
13009
  const draw = React87.useCallback(() => {
12529
13010
  const _perfT = ui.perfStart("learningcanvas:paint");
12530
13011
  const canvas = canvasRef.current;
@@ -12543,15 +13024,15 @@ var init_LearningCanvas = __esm({
12543
13024
  ctx.fillRect(0, 0, width, height);
12544
13025
  }
12545
13026
  for (const shape of derivedShapes) {
12546
- if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
13027
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes, fontFamily);
12547
13028
  }
12548
13029
  for (const shape of derivedShapes) {
12549
- if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
13030
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes, fontFamily);
12550
13031
  }
12551
13032
  if (drawables?.length && projector) {
12552
13033
  const painter = createWebPainter(ctx, invalidateRef.current);
12553
13034
  const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
12554
- const dctx = { projector, time: timeMs, invalidate: invalidateRef.current, fontFamily: themeBodyFont(canvas) };
13035
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current, fontFamily: fontFamily || themeBodyFont(canvas) };
12555
13036
  for (const node of drawables) {
12556
13037
  paintDrawable(painter, node, dctx);
12557
13038
  }
@@ -17150,6 +17631,250 @@ var init_EmptyState = __esm({
17150
17631
  EmptyState.displayName = "EmptyState";
17151
17632
  }
17152
17633
  });
17634
+
17635
+ // lib/editorMotions.ts
17636
+ function clamp(value, min, max) {
17637
+ return Math.max(min, Math.min(max, value));
17638
+ }
17639
+ function computeLines(text) {
17640
+ const lines = [];
17641
+ let start = 0;
17642
+ for (let i = 0; i <= text.length; i++) {
17643
+ if (i === text.length || text[i] === "\n") {
17644
+ lines.push({ start, end: i });
17645
+ start = i + 1;
17646
+ }
17647
+ }
17648
+ return lines;
17649
+ }
17650
+ function lineIndexAt(lines, pos) {
17651
+ for (let i = 0; i < lines.length; i++) {
17652
+ if (pos <= lines[i].end) return i;
17653
+ }
17654
+ return lines.length - 1;
17655
+ }
17656
+ function findWords(text) {
17657
+ const words = [];
17658
+ const re = /\w+|\S+/g;
17659
+ let m;
17660
+ while ((m = re.exec(text)) !== null) {
17661
+ words.push({ start: m.index, end: m.index + m[0].length });
17662
+ }
17663
+ return words;
17664
+ }
17665
+ function nextWordStart(text, pos) {
17666
+ for (const w of findWords(text)) {
17667
+ if (w.start > pos) return w.start;
17668
+ }
17669
+ return text.length;
17670
+ }
17671
+ function prevWordStart(text, pos) {
17672
+ let result = 0;
17673
+ for (const w of findWords(text)) {
17674
+ if (w.start < pos) result = w.start;
17675
+ else break;
17676
+ }
17677
+ return result;
17678
+ }
17679
+ function nextWordEnd(text, pos) {
17680
+ for (const w of findWords(text)) {
17681
+ const lastChar = w.end - 1;
17682
+ if (lastChar > pos) return lastChar;
17683
+ }
17684
+ return text.length > 0 ? text.length - 1 : 0;
17685
+ }
17686
+ function firstNonBlank(text, line) {
17687
+ let i = line.start;
17688
+ while (i < line.end && (text[i] === " " || text[i] === " ")) i++;
17689
+ return i;
17690
+ }
17691
+ function nextParagraphBoundary(lines, fromLineIdx, textLength) {
17692
+ for (let i = fromLineIdx + 1; i < lines.length; i++) {
17693
+ if (lines[i].start === lines[i].end) return lines[i].start;
17694
+ }
17695
+ return textLength;
17696
+ }
17697
+ function prevParagraphBoundary(lines, fromLineIdx) {
17698
+ for (let i = fromLineIdx - 1; i >= 0; i--) {
17699
+ if (lines[i].start === lines[i].end) return lines[i].start;
17700
+ }
17701
+ return 0;
17702
+ }
17703
+ function applyMotion(text, caret, motion, count) {
17704
+ const n = Math.max(1, count);
17705
+ const lines = computeLines(text);
17706
+ const lineIdx = lineIndexAt(lines, caret);
17707
+ const line = lines[lineIdx];
17708
+ switch (motion) {
17709
+ case "left":
17710
+ return clamp(caret - n, line.start, line.end);
17711
+ case "right":
17712
+ return clamp(caret + n, line.start, line.end);
17713
+ case "up": {
17714
+ const col = caret - line.start;
17715
+ const targetIdx = clamp(lineIdx - n, 0, lines.length - 1);
17716
+ const target = lines[targetIdx];
17717
+ return clamp(target.start + col, target.start, target.end);
17718
+ }
17719
+ case "down": {
17720
+ const col = caret - line.start;
17721
+ const targetIdx = clamp(lineIdx + n, 0, lines.length - 1);
17722
+ const target = lines[targetIdx];
17723
+ return clamp(target.start + col, target.start, target.end);
17724
+ }
17725
+ case "word-forward": {
17726
+ let pos = caret;
17727
+ for (let i = 0; i < n; i++) pos = nextWordStart(text, pos);
17728
+ return pos;
17729
+ }
17730
+ case "word-back": {
17731
+ let pos = caret;
17732
+ for (let i = 0; i < n; i++) pos = prevWordStart(text, pos);
17733
+ return pos;
17734
+ }
17735
+ case "word-end": {
17736
+ let pos = caret;
17737
+ for (let i = 0; i < n; i++) pos = nextWordEnd(text, pos);
17738
+ return pos;
17739
+ }
17740
+ case "line-start":
17741
+ return line.start;
17742
+ case "line-end":
17743
+ return line.end > line.start ? line.end - 1 : line.start;
17744
+ case "first-nonblank":
17745
+ return firstNonBlank(text, line);
17746
+ case "doc-start":
17747
+ return 0;
17748
+ case "doc-end":
17749
+ return text.length;
17750
+ case "paragraph-forward": {
17751
+ let pos = caret;
17752
+ for (let i = 0; i < n; i++) {
17753
+ pos = nextParagraphBoundary(lines, lineIndexAt(lines, pos), text.length);
17754
+ }
17755
+ return pos;
17756
+ }
17757
+ case "paragraph-back": {
17758
+ let pos = caret;
17759
+ for (let i = 0; i < n; i++) {
17760
+ pos = prevParagraphBoundary(lines, lineIndexAt(lines, pos));
17761
+ }
17762
+ return pos;
17763
+ }
17764
+ case "line":
17765
+ case "selection":
17766
+ return caret;
17767
+ default: {
17768
+ const _exhaustive = motion;
17769
+ return _exhaustive;
17770
+ }
17771
+ }
17772
+ }
17773
+ function motionRange(text, caret, motion, count, selection) {
17774
+ if (motion === "selection") {
17775
+ if (!selection) return [caret, caret];
17776
+ return [Math.min(selection[0], selection[1]), Math.max(selection[0], selection[1])];
17777
+ }
17778
+ const lines = computeLines(text);
17779
+ if (motion === "line") {
17780
+ const n = Math.max(1, count);
17781
+ const startIdx = lineIndexAt(lines, caret);
17782
+ const endIdx = clamp(startIdx + n - 1, 0, lines.length - 1);
17783
+ const start2 = lines[startIdx].start;
17784
+ const rawEnd = lines[endIdx].end;
17785
+ const end2 = rawEnd < text.length ? rawEnd + 1 : rawEnd;
17786
+ return [start2, end2];
17787
+ }
17788
+ const newCaret = applyMotion(text, caret, motion, count);
17789
+ let start = Math.min(caret, newCaret);
17790
+ let end = Math.max(caret, newCaret);
17791
+ if (motion === "word-end" || motion === "line-end") {
17792
+ end = Math.min(Math.max(start, newCaret) + 1, text.length);
17793
+ } else if (motion === "word-forward" && newCaret > caret) {
17794
+ const startLine = lineIndexAt(lines, caret);
17795
+ const endLine = lineIndexAt(lines, newCaret);
17796
+ if (endLine !== startLine) {
17797
+ end = lines[startLine].end;
17798
+ }
17799
+ }
17800
+ return [start, end];
17801
+ }
17802
+ function applyOperator(text, range, operator, register) {
17803
+ const start = clamp(range[0], 0, text.length);
17804
+ const end = clamp(range[1], start, text.length);
17805
+ const removed = text.slice(start, end);
17806
+ if (operator === "yank") {
17807
+ return { text, caret: start, register: removed };
17808
+ }
17809
+ return { text: text.slice(0, start) + text.slice(end), caret: start, register: removed };
17810
+ }
17811
+ var init_editorMotions = __esm({
17812
+ "lib/editorMotions.ts"() {
17813
+ }
17814
+ });
17815
+ function isMotionPayload(payload) {
17816
+ return !!payload && typeof payload.editorId === "string" && typeof payload.motion === "string" && typeof payload.count === "number";
17817
+ }
17818
+ function isOperatePayload(payload) {
17819
+ return !!payload && typeof payload.editorId === "string" && typeof payload.operator === "string" && typeof payload.motion === "string" && typeof payload.count === "number";
17820
+ }
17821
+ function isInsertTextPayload(payload) {
17822
+ return !!payload && typeof payload.editorId === "string" && typeof payload.text === "string";
17823
+ }
17824
+ function isSetModePayload(payload) {
17825
+ return !!payload && typeof payload.editorId === "string" && typeof payload.mode === "string" && typeof payload.caret === "string";
17826
+ }
17827
+ function useEditorCapabilities(args) {
17828
+ const [caretMode, setCaretMode] = React87.useState("bar");
17829
+ const registerRef = React87.useRef("");
17830
+ useEventListener(`UI:${args.events.onMotion}`, (evt) => {
17831
+ if (!args.editorId || !isMotionPayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
17832
+ const ta = args.textareaRef.current;
17833
+ if (!ta) return;
17834
+ const { motion, count } = evt.payload;
17835
+ const newCaret = applyMotion(ta.value, ta.selectionStart, motion, count);
17836
+ if (ta.selectionStart !== ta.selectionEnd) {
17837
+ ta.setSelectionRange(ta.selectionStart, newCaret);
17838
+ } else {
17839
+ ta.setSelectionRange(newCaret, newCaret);
17840
+ }
17841
+ });
17842
+ useEventListener(`UI:${args.events.onOperate}`, (evt) => {
17843
+ if (!args.editorId || !isOperatePayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
17844
+ const ta = args.textareaRef.current;
17845
+ if (!ta) return;
17846
+ const { operator, motion, count } = evt.payload;
17847
+ const selection = motion === "selection" ? [ta.selectionStart, ta.selectionEnd] : void 0;
17848
+ const range = motionRange(ta.value, ta.selectionStart, motion, count, selection);
17849
+ const result = applyOperator(ta.value, range, operator, registerRef.current);
17850
+ registerRef.current = result.register;
17851
+ if (operator === "yank") {
17852
+ ta.setSelectionRange(range[0], range[0]);
17853
+ } else {
17854
+ ta.setRangeText("", range[0], range[1], "end");
17855
+ }
17856
+ args.applyChange(ta.value);
17857
+ });
17858
+ useEventListener(`UI:${args.events.onInsertText}`, (evt) => {
17859
+ if (!args.editorId || !isInsertTextPayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
17860
+ const ta = args.textareaRef.current;
17861
+ if (!ta) return;
17862
+ ta.setRangeText(evt.payload.text, ta.selectionStart, ta.selectionEnd, "end");
17863
+ args.applyChange(ta.value);
17864
+ });
17865
+ useEventListener(`UI:${args.events.onSetMode}`, (evt) => {
17866
+ if (!args.editorId || !isSetModePayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
17867
+ setCaretMode(evt.payload.caret);
17868
+ });
17869
+ return { caretMode };
17870
+ }
17871
+ var init_useEditorCapabilities = __esm({
17872
+ "components/core/molecules/markdown/useEditorCapabilities.ts"() {
17873
+ "use client";
17874
+ init_useEventBus();
17875
+ init_editorMotions();
17876
+ }
17877
+ });
17153
17878
  function isLanguageRegistered(lang) {
17154
17879
  return CODE_LANGUAGE_SET.has(lang) || dynamicallyLoaded.has(lang);
17155
17880
  }
@@ -17235,7 +17960,36 @@ function useLanguageReady(language) {
17235
17960
  }, [language]);
17236
17961
  return ready;
17237
17962
  }
17238
- 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;
17963
+ function resolveHighlightStyle(lang) {
17964
+ if (lang === "orb") return orbStyle;
17965
+ if (lang === "lolo") return loloStyle;
17966
+ return dark__default.default;
17967
+ }
17968
+ function plainCodeColorOf(style) {
17969
+ return style['code[class*="language-"]']?.color ?? "#d4d4d4";
17970
+ }
17971
+ function buildLineProps(errorLines, extraClassName) {
17972
+ return (lineNumber) => {
17973
+ const base = {
17974
+ "data-line": String(lineNumber - 1),
17975
+ ...extraClassName ? { className: extraClassName } : {}
17976
+ };
17977
+ const severity = errorLines?.get(lineNumber);
17978
+ if (!severity) return base;
17979
+ return {
17980
+ ...base,
17981
+ style: {
17982
+ display: "block",
17983
+ backgroundColor: severity === "error" ? "rgba(248, 113, 113, 0.18)" : "rgba(251, 191, 36, 0.18)",
17984
+ // amber-400 @ 18%
17985
+ borderLeft: `3px solid ${severity === "error" ? "#ef4444" : "#f59e0b"}`,
17986
+ paddingLeft: "0.5rem",
17987
+ marginLeft: "-0.5rem"
17988
+ }
17989
+ };
17990
+ };
17991
+ }
17992
+ 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;
17239
17993
  var init_CodeBlock = __esm({
17240
17994
  "components/core/molecules/markdown/CodeBlock.tsx"() {
17241
17995
  init_cn();
@@ -17251,6 +18005,7 @@ var init_CodeBlock = __esm({
17251
18005
  init_Textarea();
17252
18006
  init_Icon();
17253
18007
  init_useEventBus();
18008
+ init_useEditorCapabilities();
17254
18009
  SyntaxHighlighter__default.default.registerLanguage("json", langJson__default.default);
17255
18010
  SyntaxHighlighter__default.default.registerLanguage("javascript", langJavascript__default.default);
17256
18011
  SyntaxHighlighter__default.default.registerLanguage("js", langJavascript__default.default);
@@ -17469,6 +18224,15 @@ var init_CodeBlock = __esm({
17469
18224
  LINE_PROPS_FN = (n) => ({ "data-line": String(n - 1) });
17470
18225
  HIDDEN_LINE_NUMBERS = { display: "none" };
17471
18226
  HIGHLIGHT_CAPACITY_BYTES = 512 * 1024;
18227
+ MONO_FONT_FAMILY = 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace';
18228
+ VIEWER_LINE_NUMBER_STYLE = {
18229
+ minWidth: "2.5em",
18230
+ paddingRight: "1rem",
18231
+ textAlign: "right",
18232
+ userSelect: "none",
18233
+ opacity: 0.5,
18234
+ fontVariantNumeric: "tabular-nums"
18235
+ };
17472
18236
  CodeBlock = React87__namespace.default.memo(
17473
18237
  ({
17474
18238
  code: rawCode,
@@ -17493,15 +18257,39 @@ var init_CodeBlock = __esm({
17493
18257
  actions,
17494
18258
  isLoading = false,
17495
18259
  error,
17496
- showCopy
18260
+ showCopy,
18261
+ // editor capability surface — P1 wires these
18262
+ editorId,
18263
+ onEditorFocus = "EDITOR_FOCUS",
18264
+ onEditorBlur = "EDITOR_BLUR",
18265
+ onMotion = "MOTION",
18266
+ onOperate = "OPERATE",
18267
+ onInsertText = "INSERT_TEXT",
18268
+ onSetMode = "SET_MODE",
18269
+ motions = [
18270
+ "left",
18271
+ "right",
18272
+ "up",
18273
+ "down",
18274
+ "word-forward",
18275
+ "word-back",
18276
+ "word-end",
18277
+ "line-start",
18278
+ "line-end",
18279
+ "first-nonblank",
18280
+ "doc-start",
18281
+ "doc-end",
18282
+ "paragraph-forward",
18283
+ "paragraph-back",
18284
+ "line",
18285
+ "selection"
18286
+ ],
18287
+ operators = ["delete", "yank", "change"]
17497
18288
  }) => {
17498
18289
  const code = typeof rawCode === "string" ? rawCode : String(rawCode ?? "");
17499
- const isOrb = language === "orb";
17500
- const isLolo = language === "lolo";
17501
- const activeStyle = isOrb ? orbStyle : isLolo ? loloStyle : dark__default.default;
18290
+ const activeStyle = resolveHighlightStyle(language);
17502
18291
  const overCapacity = code.length > HIGHLIGHT_CAPACITY_BYTES;
17503
- const plainCodeColor = activeStyle['code[class*="language-"]']?.color ?? "#d4d4d4";
17504
- const languageReady = useLanguageReady(language);
18292
+ const plainCodeColor = plainCodeColorOf(activeStyle);
17505
18293
  const eventBus = useEventBus();
17506
18294
  const { t } = hooks.useTranslate();
17507
18295
  const scrollRef = React87.useRef(null);
@@ -17513,6 +18301,9 @@ var init_CodeBlock = __esm({
17513
18301
  const activeFile = files?.[activeFileIndex];
17514
18302
  const activeCode = activeFile?.code ?? code;
17515
18303
  const activeLanguage = activeFile?.language ?? language;
18304
+ const languageReady = useLanguageReady(activeLanguage);
18305
+ const viewerStyle = resolveHighlightStyle(activeLanguage);
18306
+ const viewerPlainCodeColor = plainCodeColorOf(viewerStyle);
17516
18307
  const diffLines = React87.useMemo(() => {
17517
18308
  if (propDiff) return propDiff;
17518
18309
  if (mode === "diff" && oldValue !== void 0 && newValue !== void 0) {
@@ -17542,28 +18333,28 @@ var init_CodeBlock = __esm({
17542
18333
  ov.scrollLeft = ta.scrollLeft;
17543
18334
  }
17544
18335
  }, []);
17545
- const errorLineProps = React87.useMemo(() => {
17546
- if (!errorLines || errorLines.size === 0) {
17547
- return LINE_PROPS_FN;
17548
- }
17549
- return (lineNumber) => {
17550
- const severity = errorLines.get(lineNumber);
17551
- if (!severity) {
17552
- return { "data-line": String(lineNumber - 1) };
17553
- }
17554
- return {
17555
- "data-line": String(lineNumber - 1),
17556
- style: {
17557
- display: "block",
17558
- backgroundColor: severity === "error" ? "rgba(248, 113, 113, 0.18)" : "rgba(251, 191, 36, 0.18)",
17559
- // amber-400 @ 18%
17560
- borderLeft: `3px solid ${severity === "error" ? "#ef4444" : "#f59e0b"}`,
17561
- paddingLeft: "0.5rem",
17562
- marginLeft: "-0.5rem"
17563
- }
17564
- };
17565
- };
17566
- }, [errorLines]);
18336
+ const handleEditableChange = React87.useCallback((v) => {
18337
+ lastPropCodeRef.current = v;
18338
+ setEditableValue(v);
18339
+ onChange?.(v);
18340
+ }, [onChange]);
18341
+ const { caretMode } = useEditorCapabilities({
18342
+ editorId: editable ? editorId : void 0,
18343
+ textareaRef: editableTextareaRef,
18344
+ events: { onMotion, onOperate, onInsertText, onSetMode },
18345
+ applyChange: handleEditableChange
18346
+ });
18347
+ const [caretIndex, setCaretIndex] = React87.useState(0);
18348
+ const caretRowCol = React87.useMemo(() => {
18349
+ const before = editableValue.slice(0, Math.min(caretIndex, editableValue.length));
18350
+ const lines = before.split("\n");
18351
+ return { row: lines.length - 1, col: lines[lines.length - 1].length };
18352
+ }, [editableValue, caretIndex]);
18353
+ const errorLineProps = React87.useMemo(() => buildLineProps(errorLines), [errorLines]);
18354
+ const viewerLineProps = React87.useMemo(
18355
+ () => buildLineProps(errorLines, "px-4 py-0.5 hover:bg-muted/50"),
18356
+ [errorLines]
18357
+ );
17567
18358
  const isFoldable = foldableProp ?? true;
17568
18359
  const [collapsed, setCollapsed] = React87.useState(() => /* @__PURE__ */ new Set());
17569
18360
  const foldRegions = React87.useMemo(
@@ -17686,6 +18477,110 @@ var init_CodeBlock = __esm({
17686
18477
  ),
17687
18478
  [code, overCapacity, plainCodeColor, language, activeStyle, languageReady]
17688
18479
  );
18480
+ const viewerOverCapacity = activeCode.length > HIGHLIGHT_CAPACITY_BYTES;
18481
+ const viewerHighlightedElement = React87.useMemo(
18482
+ () => viewerOverCapacity ? /* @__PURE__ */ jsxRuntime.jsx(
18483
+ "div",
18484
+ {
18485
+ className: "px-4 py-0.5",
18486
+ style: {
18487
+ margin: 0,
18488
+ whiteSpace: wrap ? "pre-wrap" : "pre",
18489
+ wordBreak: wrap ? "break-all" : "normal",
18490
+ color: viewerPlainCodeColor,
18491
+ fontFamily: MONO_FONT_FAMILY,
18492
+ fontSize: "12px",
18493
+ lineHeight: "1.6"
18494
+ },
18495
+ children: activeCode
18496
+ }
18497
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
18498
+ SyntaxHighlighter__default.default,
18499
+ {
18500
+ PreTag: "div",
18501
+ language: activeLanguage,
18502
+ style: viewerStyle,
18503
+ wrapLines: true,
18504
+ wrapLongLines: wrap,
18505
+ showLineNumbers,
18506
+ lineNumberStyle: VIEWER_LINE_NUMBER_STYLE,
18507
+ lineProps: viewerLineProps,
18508
+ customStyle: {
18509
+ backgroundColor: "transparent",
18510
+ borderRadius: 0,
18511
+ padding: "0.25rem 0",
18512
+ margin: 0,
18513
+ whiteSpace: wrap ? "pre-wrap" : "pre",
18514
+ wordBreak: wrap ? "break-all" : "normal",
18515
+ fontFamily: MONO_FONT_FAMILY,
18516
+ fontSize: "12px",
18517
+ lineHeight: "1.6"
18518
+ },
18519
+ codeTagProps: { style: { fontFamily: MONO_FONT_FAMILY, fontSize: "12px", lineHeight: "1.6" } },
18520
+ children: activeCode
18521
+ }
18522
+ ),
18523
+ [activeCode, viewerOverCapacity, viewerPlainCodeColor, activeLanguage, viewerStyle, wrap, showLineNumbers, viewerLineProps, languageReady]
18524
+ );
18525
+ const diffOverCapacity = React87.useMemo(
18526
+ () => !!diffLines && diffLines.reduce((n, l) => n + l.content.length + 1, 0) > HIGHLIGHT_CAPACITY_BYTES,
18527
+ [diffLines]
18528
+ );
18529
+ const diffRowElements = React87.useMemo(() => {
18530
+ if (!diffLines) return null;
18531
+ return diffLines.map((line, idx) => {
18532
+ const style = DIFF_STYLES[line.type] ?? DIFF_STYLE_FALLBACK;
18533
+ return /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "none", align: "start", className: cn(style.bg, "px-4 py-0.5"), children: [
18534
+ showLineNumbers && /* @__PURE__ */ jsxRuntime.jsx(
18535
+ Typography,
18536
+ {
18537
+ variant: "caption",
18538
+ color: "secondary",
18539
+ className: "w-8 text-right mr-3 select-none tabular-nums flex-shrink-0",
18540
+ children: line.lineNumber ?? ""
18541
+ }
18542
+ ),
18543
+ /* @__PURE__ */ jsxRuntime.jsxs(
18544
+ Typography,
18545
+ {
18546
+ variant: "caption",
18547
+ className: cn("font-mono flex-1 min-w-0", style.text, wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
18548
+ children: [
18549
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "span", className: "select-none opacity-50 mr-2", children: style.prefix }),
18550
+ diffOverCapacity ? line.content || " " : /* @__PURE__ */ jsxRuntime.jsx(
18551
+ SyntaxHighlighter__default.default,
18552
+ {
18553
+ PreTag: "span",
18554
+ CodeTag: "span",
18555
+ language: activeLanguage,
18556
+ style: viewerStyle,
18557
+ customStyle: {
18558
+ display: "inline",
18559
+ background: "transparent",
18560
+ padding: 0,
18561
+ margin: 0,
18562
+ whiteSpace: wrap ? "pre-wrap" : "pre",
18563
+ wordBreak: wrap ? "break-all" : "normal",
18564
+ fontFamily: "inherit",
18565
+ fontSize: "inherit",
18566
+ lineHeight: "inherit"
18567
+ },
18568
+ codeTagProps: {
18569
+ style: {
18570
+ whiteSpace: wrap ? "pre-wrap" : "pre",
18571
+ fontFamily: "inherit",
18572
+ fontSize: "inherit"
18573
+ }
18574
+ },
18575
+ children: line.content || " "
18576
+ }
18577
+ )
18578
+ ]
18579
+ }
18580
+ )
18581
+ ] }, idx);
18582
+ });
18583
+ }, [diffLines, showLineNumbers, wrap, diffOverCapacity, activeLanguage, viewerStyle, languageReady]);
17689
18584
  React87.useLayoutEffect(() => {
17690
18585
  const container = codeRef.current;
17691
18586
  if (!container) return;
@@ -17829,7 +18724,6 @@ var init_CodeBlock = __esm({
17829
18724
  label: file.label,
17830
18725
  content: null
17831
18726
  }));
17832
- const lines = activeCode.split("\n");
17833
18727
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className: cn("overflow-hidden", className), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: [
17834
18728
  tabItems && tabItems.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
17835
18729
  Tabs,
@@ -17892,49 +18786,7 @@ var init_CodeBlock = __esm({
17892
18786
  ]
17893
18787
  }
17894
18788
  ),
17895
- /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "overflow-auto bg-muted/20", style: { maxHeight }, children: diffLines ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column" }, className: "font-mono text-xs", children: diffLines.map((line, idx) => {
17896
- const style = DIFF_STYLES[line.type] ?? DIFF_STYLE_FALLBACK;
17897
- return /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "none", align: "start", className: cn(style.bg, "px-4 py-0.5"), children: [
17898
- showLineNumbers && /* @__PURE__ */ jsxRuntime.jsx(
17899
- Typography,
17900
- {
17901
- variant: "caption",
17902
- color: "secondary",
17903
- className: "w-8 text-right mr-3 select-none tabular-nums flex-shrink-0",
17904
- children: line.lineNumber ?? ""
17905
- }
17906
- ),
17907
- /* @__PURE__ */ jsxRuntime.jsxs(
17908
- Typography,
17909
- {
17910
- variant: "caption",
17911
- className: cn("font-mono flex-1 min-w-0", style.text, wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
17912
- children: [
17913
- /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "span", className: "select-none opacity-50 mr-2", children: style.prefix }),
17914
- line.content
17915
- ]
17916
- }
17917
- )
17918
- ] }, idx);
17919
- }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column" }, className: "font-mono text-xs", children: lines.map((line, idx) => /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "none", align: "start", className: "px-4 py-0.5 hover:bg-muted/50", children: [
17920
- showLineNumbers && /* @__PURE__ */ jsxRuntime.jsx(
17921
- Typography,
17922
- {
17923
- variant: "caption",
17924
- color: "secondary",
17925
- className: "w-8 text-right mr-4 select-none tabular-nums flex-shrink-0",
17926
- children: idx + 1
17927
- }
17928
- ),
17929
- /* @__PURE__ */ jsxRuntime.jsx(
17930
- Typography,
17931
- {
17932
- variant: "caption",
17933
- className: cn("font-mono flex-1 min-w-0", wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
17934
- children: line || " "
17935
- }
17936
- )
17937
- ] }, idx)) }) })
18789
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "overflow-auto bg-muted/20", style: { maxHeight }, children: diffLines ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column" }, className: "font-mono text-xs", children: diffRowElements }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-mono text-xs", children: viewerHighlightedElement }) })
17938
18790
  ] }) });
17939
18791
  }
17940
18792
  const hasHeader = showLanguageBadge || effectiveCopy;
@@ -18021,13 +18873,11 @@ var init_CodeBlock = __esm({
18021
18873
  {
18022
18874
  ref: editableTextareaRef,
18023
18875
  defaultValue: code,
18024
- onChange: (e) => {
18025
- const v = e.target.value;
18026
- lastPropCodeRef.current = v;
18027
- setEditableValue(v);
18028
- onChange?.(v);
18029
- },
18876
+ onChange: (e) => handleEditableChange(e.target.value),
18030
18877
  onScroll: handleEditableScroll,
18878
+ onSelect: (e) => setCaretIndex(e.currentTarget.selectionStart),
18879
+ onFocus: editorId ? () => eventBus.emit(`UI:${onEditorFocus}`, { editorId }) : void 0,
18880
+ onBlur: editorId ? () => eventBus.emit(`UI:${onEditorBlur}`, { editorId }) : void 0,
18031
18881
  spellCheck: false,
18032
18882
  style: {
18033
18883
  position: "absolute",
@@ -18042,7 +18892,7 @@ var init_CodeBlock = __esm({
18042
18892
  resize: "none",
18043
18893
  backgroundColor: "transparent",
18044
18894
  color: "transparent",
18045
- caretColor: "#e6e6e6",
18895
+ caretColor: caretMode === "block" ? "transparent" : "#e6e6e6",
18046
18896
  WebkitTextFillColor: "transparent",
18047
18897
  fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace',
18048
18898
  fontSize: "13px",
@@ -18053,6 +18903,22 @@ var init_CodeBlock = __esm({
18053
18903
  }
18054
18904
  },
18055
18905
  editableTextareaKey
18906
+ ),
18907
+ caretMode !== "bar" && /* @__PURE__ */ jsxRuntime.jsx(
18908
+ "span",
18909
+ {
18910
+ "aria-hidden": true,
18911
+ style: {
18912
+ position: "absolute",
18913
+ top: `calc(1rem + ${caretRowCol.row * 19.5}px)`,
18914
+ left: `calc(1rem + ${caretRowCol.col}ch)`,
18915
+ width: "1ch",
18916
+ height: caretMode === "block" ? "19.5px" : "2px",
18917
+ backgroundColor: caretMode === "block" ? "rgba(230, 230, 230, 0.5)" : void 0,
18918
+ borderBottom: caretMode === "underline" ? "2px solid #e6e6e6" : void 0,
18919
+ pointerEvents: "none"
18920
+ }
18921
+ }
18056
18922
  )
18057
18923
  ]
18058
18924
  }
@@ -18080,55 +18946,175 @@ var init_CodeBlock = __esm({
18080
18946
  )
18081
18947
  ] });
18082
18948
  },
18083
- (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
18949
+ (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
18084
18950
  );
18085
18951
  CodeBlock.displayName = "CodeBlock";
18086
18952
  }
18087
18953
  });
18954
+
18955
+ // components/core/molecules/markdown/mermaidSource.ts
18956
+ function isQuoted(label) {
18957
+ const trimmed = label.trim();
18958
+ return trimmed.length === 0 || trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length > 1;
18959
+ }
18960
+ function quote(label) {
18961
+ return `"${label.replace(/"/g, "#quot;")}"`;
18962
+ }
18963
+ function isIdentifierChar(ch) {
18964
+ return /[A-Za-z0-9_\-.]/.test(ch);
18965
+ }
18966
+ function declaredType(code) {
18967
+ for (const line of code.split("\n")) {
18968
+ const trimmed = line.trim();
18969
+ if (trimmed.length === 0 || trimmed.startsWith("%%")) continue;
18970
+ return trimmed;
18971
+ }
18972
+ return "";
18973
+ }
18974
+ function isFlowchart(code) {
18975
+ return FLOWCHART_DIRECTIVE.test(declaredType(code));
18976
+ }
18977
+ function quoteNodeLabels(code) {
18978
+ let out = "";
18979
+ let i = 0;
18980
+ while (i < code.length) {
18981
+ const shape = NODE_SHAPES.find(([open2]) => code.startsWith(open2, i));
18982
+ const precededByIdentifier = i > 0 && isIdentifierChar(code[i - 1] ?? "");
18983
+ if (shape === void 0 || !precededByIdentifier) {
18984
+ out += code[i];
18985
+ i += 1;
18986
+ continue;
18987
+ }
18988
+ const [open, close] = shape;
18989
+ const contentStart = i + open.length;
18990
+ const closeAt = code.indexOf(close, contentStart);
18991
+ const newlineAt = code.indexOf("\n", contentStart);
18992
+ if (closeAt === -1 || newlineAt !== -1 && newlineAt < closeAt) {
18993
+ out += code[i];
18994
+ i += 1;
18995
+ continue;
18996
+ }
18997
+ const label = code.slice(contentStart, closeAt);
18998
+ out += open + (isQuoted(label) ? label : quote(label)) + close;
18999
+ i = closeAt + close.length;
19000
+ }
19001
+ return out;
19002
+ }
19003
+ function quoteEdgeLabels(code) {
19004
+ return code.split("\n").map((line) => {
19005
+ let out = "";
19006
+ let rest = line;
19007
+ for (; ; ) {
19008
+ const open = rest.indexOf("|");
19009
+ if (open === -1) break;
19010
+ const close = rest.indexOf("|", open + 1);
19011
+ if (close === -1) break;
19012
+ const label = rest.slice(open + 1, close);
19013
+ out += rest.slice(0, open + 1) + (isQuoted(label) ? label : quote(label)) + "|";
19014
+ rest = rest.slice(close + 1);
19015
+ }
19016
+ return out + rest;
19017
+ }).join("\n");
19018
+ }
19019
+ function quoteSubgraphTitles(code) {
19020
+ return code.split("\n").map((line) => {
19021
+ const match = /^(\s*subgraph\s+)(.+?)(\s*)$/.exec(line);
19022
+ if (match === null) return line;
19023
+ const [, prefix, title, trailing] = match;
19024
+ if (title === void 0 || prefix === void 0) return line;
19025
+ if (isQuoted(title) || title.includes("[")) return line;
19026
+ return prefix + quote(title) + (trailing ?? "");
19027
+ }).join("\n");
19028
+ }
19029
+ function mermaidRepairCandidates(code) {
19030
+ if (!isFlowchart(code)) return [];
19031
+ const nodes = quoteNodeLabels(code);
19032
+ const nodesAndEdges = quoteEdgeLabels(nodes);
19033
+ const all = quoteSubgraphTitles(nodesAndEdges);
19034
+ const ordered2 = [nodes, nodesAndEdges, all];
19035
+ const seen = /* @__PURE__ */ new Set([code]);
19036
+ const candidates = [];
19037
+ for (const candidate of ordered2) {
19038
+ if (seen.has(candidate)) continue;
19039
+ seen.add(candidate);
19040
+ candidates.push(candidate);
19041
+ }
19042
+ return candidates;
19043
+ }
19044
+ var NODE_SHAPES, FLOWCHART_DIRECTIVE;
19045
+ var init_mermaidSource = __esm({
19046
+ "components/core/molecules/markdown/mermaidSource.ts"() {
19047
+ NODE_SHAPES = [
19048
+ ["[[", "]]"],
19049
+ ["[(", ")]"],
19050
+ ["([", "])"],
19051
+ ["((", "))"],
19052
+ ["{{", "}}"],
19053
+ ["[", "]"],
19054
+ ["(", ")"],
19055
+ ["{", "}"]
19056
+ ];
19057
+ FLOWCHART_DIRECTIVE = /^(?:graph|flowchart)\b/;
19058
+ }
19059
+ });
18088
19060
  function loadMermaid() {
18089
19061
  mermaidModule ?? (mermaidModule = import('mermaid').then((m) => m.default));
18090
19062
  return mermaidModule;
18091
19063
  }
18092
- var mermaidModule, MermaidDiagram;
19064
+ var log7, mermaidModule, MermaidDiagram;
18093
19065
  var init_MermaidDiagram = __esm({
18094
19066
  "components/core/molecules/markdown/MermaidDiagram.tsx"() {
18095
19067
  init_Box();
18096
19068
  init_Typography();
18097
19069
  init_CodeBlock();
19070
+ init_mermaidSource();
18098
19071
  init_cn();
19072
+ log7 = logger.createLogger("almadar:ui:mermaid-diagram");
18099
19073
  mermaidModule = null;
18100
19074
  MermaidDiagram = React87__namespace.default.memo(
18101
19075
  ({ code, className }) => {
18102
19076
  const { resolvedMode } = context.useTheme();
19077
+ const { t } = hooks.useTranslate();
18103
19078
  const containerRef = React87.useRef(null);
18104
- const [error, setError] = React87.useState(null);
19079
+ const [unrenderable, setUnrenderable] = React87.useState(false);
18105
19080
  const reactId = React87.useId();
18106
19081
  React87.useEffect(() => {
18107
19082
  let active = true;
18108
19083
  void (async () => {
18109
- try {
18110
- const mermaid = await loadMermaid();
18111
- mermaid.initialize({
18112
- startOnLoad: false,
18113
- securityLevel: "strict",
18114
- theme: resolvedMode === "dark" ? "dark" : "default"
18115
- });
18116
- const domId = `mermaid-${reactId.replace(/[^a-zA-Z0-9]/g, "")}`;
18117
- const { svg } = await mermaid.render(domId, code);
18118
- if (!active || !containerRef.current) return;
18119
- containerRef.current.innerHTML = svg;
18120
- setError(null);
18121
- } catch (err) {
18122
- if (active) setError(err instanceof Error ? err.message : String(err));
19084
+ const mermaid = await loadMermaid();
19085
+ mermaid.initialize({
19086
+ startOnLoad: false,
19087
+ securityLevel: "strict",
19088
+ theme: resolvedMode === "dark" ? "dark" : "default"
19089
+ });
19090
+ const domId = `mermaid-${reactId.replace(/[^a-zA-Z0-9]/g, "")}`;
19091
+ let firstError = null;
19092
+ for (const [index, source] of [code, ...mermaidRepairCandidates(code)].entries()) {
19093
+ try {
19094
+ const { svg } = await mermaid.render(domId, source);
19095
+ if (!active) return;
19096
+ const container = containerRef.current;
19097
+ if (container === null) return;
19098
+ container.innerHTML = svg;
19099
+ container.dataset.mermaidRepaired = String(index > 0);
19100
+ setUnrenderable(false);
19101
+ if (index > 0) log7.debug("mermaid:repaired", { candidate: index });
19102
+ return;
19103
+ } catch (err) {
19104
+ firstError ?? (firstError = err instanceof Error ? err : new Error(String(err)));
19105
+ }
18123
19106
  }
19107
+ if (!active) return;
19108
+ log7.warn("mermaid:unrenderable", { error: firstError?.message ?? "", code });
19109
+ setUnrenderable(true);
18124
19110
  })();
18125
19111
  return () => {
18126
19112
  active = false;
18127
19113
  };
18128
19114
  }, [code, resolvedMode, reactId]);
18129
19115
  return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: cn("not-prose my-4", className), children: [
18130
- error !== null && /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "space-y-2 mb-2", children: [
18131
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-error whitespace-pre-wrap", children: error }),
19116
+ unrenderable && /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "space-y-2 mb-2", "data-testid": "mermaid-unrenderable", children: [
19117
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-muted-foreground", children: t("mermaid.unrenderable") }),
18132
19118
  /* @__PURE__ */ jsxRuntime.jsx(CodeBlock, { code, language: "mermaid" })
18133
19119
  ] }),
18134
19120
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -18137,7 +19123,7 @@ var init_MermaidDiagram = __esm({
18137
19123
  ref: containerRef,
18138
19124
  "data-testid": "mermaid-diagram",
18139
19125
  className: "overflow-x-auto",
18140
- style: error !== null ? { display: "none" } : void 0
19126
+ style: unrenderable ? { display: "none" } : void 0
18141
19127
  }
18142
19128
  )
18143
19129
  ] });
@@ -21492,14 +22478,14 @@ function useSafeEventBus2() {
21492
22478
  } };
21493
22479
  }
21494
22480
  }
21495
- var log7, lookStyles4, ButtonGroup;
22481
+ var log8, lookStyles4, ButtonGroup;
21496
22482
  var init_ButtonGroup = __esm({
21497
22483
  "components/core/molecules/ButtonGroup.tsx"() {
21498
22484
  "use client";
21499
22485
  init_cn();
21500
22486
  init_atoms();
21501
22487
  init_useEventBus();
21502
- log7 = logger.createLogger("almadar:ui:button-group");
22488
+ log8 = logger.createLogger("almadar:ui:button-group");
21503
22489
  lookStyles4 = {
21504
22490
  "right-aligned-buttons": "",
21505
22491
  "floating-bar": "fixed bottom-section left-1/2 -translate-x-1/2 shadow-elevation-toast bg-card p-card-sm rounded-container",
@@ -21580,7 +22566,7 @@ var init_ButtonGroup = __esm({
21580
22566
  {
21581
22567
  variant: "ghost",
21582
22568
  onClick: () => {
21583
- log7.debug("Filter clicked", { field: filter.field });
22569
+ log8.debug("Filter clicked", { field: filter.field });
21584
22570
  },
21585
22571
  children: filter.label
21586
22572
  },
@@ -24297,6 +25283,12 @@ function commandMatches(command, query) {
24297
25283
  if (matchesQuery(query, command.label)) return true;
24298
25284
  return (command.keywords ?? []).some((keyword) => matchesQuery(query, keyword));
24299
25285
  }
25286
+ function dispatchCommandPaletteCommand(command, deps) {
25287
+ if (command.disabled) return;
25288
+ if (command.event) deps.emit(`UI:${command.event}`, { commandId: command.id });
25289
+ if (command.action) deps.emit(`UI:${command.action}`, command.actionPayload ?? {});
25290
+ deps.onSelect?.(command);
25291
+ }
24300
25292
  var UNGROUPED, CommandPalette;
24301
25293
  var init_CommandPalette = __esm({
24302
25294
  "components/core/molecules/CommandPalette.tsx"() {
@@ -24351,9 +25343,7 @@ var init_CommandPalette = __esm({
24351
25343
  const handleSelect = React87.useCallback(
24352
25344
  (command) => {
24353
25345
  if (command.disabled) return;
24354
- if (command.event) eventBus.emit(`UI:${command.event}`, { commandId: command.id });
24355
- if (command.action) eventBus.emit(`UI:${command.action}`, command.actionPayload ?? {});
24356
- onSelect?.(command);
25346
+ dispatchCommandPaletteCommand(command, { emit: eventBus.emit, onSelect });
24357
25347
  handleClose();
24358
25348
  },
24359
25349
  [eventBus, onSelect, handleClose]
@@ -29148,9 +30138,9 @@ function debug(...args) {
29148
30138
  const [first, ...rest] = args;
29149
30139
  const message = typeof first === "string" ? first : "<debug>";
29150
30140
  if (rest.length === 0 && typeof first === "string") {
29151
- log8.debug(message);
30141
+ log9.debug(message);
29152
30142
  } else {
29153
- log8.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
30143
+ log9.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
29154
30144
  }
29155
30145
  }
29156
30146
  function debugGroup(label) {
@@ -29178,11 +30168,11 @@ function toLogMetaValue(v) {
29178
30168
  }
29179
30169
  return String(v);
29180
30170
  }
29181
- var NAMESPACE, log8;
30171
+ var NAMESPACE, log9;
29182
30172
  var init_debug = __esm({
29183
30173
  "lib/debug.ts"() {
29184
30174
  NAMESPACE = "almadar:ui:debug";
29185
- log8 = logger.createLogger(NAMESPACE);
30175
+ log9 = logger.createLogger(NAMESPACE);
29186
30176
  logger.createLogger("almadar:ui:debug:input");
29187
30177
  logger.createLogger("almadar:ui:debug:collision");
29188
30178
  logger.createLogger("almadar:ui:debug:physics");
@@ -30835,6 +31825,7 @@ var init_MathCanvas = __esm({
30835
31825
  init_perf();
30836
31826
  init_atoms();
30837
31827
  init_Stack();
31828
+ init_gameFonts();
30838
31829
  init_LearningCanvas();
30839
31830
  MathCanvas = ({
30840
31831
  className,
@@ -30854,6 +31845,7 @@ var init_MathCanvas = __esm({
30854
31845
  showTickLabels = false,
30855
31846
  tickLabelFontSize = 10,
30856
31847
  labelFontSize = 12,
31848
+ fontFamily: fontFamilyProp,
30857
31849
  showCurveLabels = false,
30858
31850
  curves = [],
30859
31851
  points = [],
@@ -30876,6 +31868,7 @@ var init_MathCanvas = __esm({
30876
31868
  error
30877
31869
  }) => {
30878
31870
  const eventBus = useEventBus();
31871
+ const fontFamily = resolveGameFontFamily(fontFamilyProp);
30879
31872
  const keyMapKey = keyMap ? JSON.stringify(keyMap) : null;
30880
31873
  const keyUpMapKey = keyUpMap ? JSON.stringify(keyUpMap) : null;
30881
31874
  const stableKeyMap = React87.useMemo(() => keyMap, [keyMapKey]);
@@ -30925,18 +31918,18 @@ var init_MathCanvas = __esm({
30925
31918
  let kx = 0;
30926
31919
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
30927
31920
  if (kx % labelEveryX === 0 && x !== 0) {
30928
- out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: tickLabelFontSize, align: "center" });
31921
+ out.push({ type: "text", fontFamily, x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: tickLabelFontSize, align: "center" });
30929
31922
  }
30930
31923
  }
30931
31924
  const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
30932
31925
  let ky = 0;
30933
31926
  for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
30934
31927
  if (ky % labelEveryY === 0 && y !== 0) {
30935
- out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
31928
+ out.push({ type: "text", fontFamily, x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
30936
31929
  }
30937
31930
  }
30938
31931
  if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
30939
- out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
31932
+ out.push({ type: "text", fontFamily, x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
30940
31933
  }
30941
31934
  }
30942
31935
  for (const region of regions) {
@@ -30963,6 +31956,7 @@ var init_MathCanvas = __esm({
30963
31956
  const mid = Math.floor(region.samples.length / 2);
30964
31957
  out.push({
30965
31958
  type: "text",
31959
+ fontFamily,
30966
31960
  x: mapX((first.x + last.x) / 2),
30967
31961
  y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
30968
31962
  text: region.label,
@@ -30999,14 +31993,14 @@ var init_MathCanvas = __esm({
30999
31993
  const px = mapX(guide.at);
31000
31994
  out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
31001
31995
  if (guide.label) {
31002
- out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color, fontSize: 11 });
31996
+ out.push({ type: "text", fontFamily, x: px + 4, y: margin + 10, text: guide.label, color, fontSize: 11 });
31003
31997
  }
31004
31998
  } else {
31005
31999
  if (guide.at < yMin || guide.at > yMax) continue;
31006
32000
  const py = mapY(guide.at);
31007
32001
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
31008
32002
  if (guide.label) {
31009
- out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color, fontSize: labelFontSize, align: "right" });
32003
+ out.push({ type: "text", fontFamily, x: width - margin - 4, y: py - 8, text: guide.label, color, fontSize: labelFontSize, align: "right" });
31010
32004
  }
31011
32005
  }
31012
32006
  }
@@ -31045,6 +32039,7 @@ var init_MathCanvas = __esm({
31045
32039
  if (showCurveLabels && curve.label && lastInRange) {
31046
32040
  out.push({
31047
32041
  type: "text",
32042
+ fontFamily,
31048
32043
  x: mapX(lastInRange.x) + 6,
31049
32044
  y: mapY(lastInRange.y) - 6,
31050
32045
  text: curve.label,
@@ -31082,6 +32077,7 @@ var init_MathCanvas = __esm({
31082
32077
  if (hop.label) {
31083
32078
  out.push({
31084
32079
  type: "text",
32080
+ fontFamily,
31085
32081
  x: (x1 + x2) / 2,
31086
32082
  y: xAxisY - peak - 8,
31087
32083
  text: hop.label,
@@ -31109,6 +32105,7 @@ var init_MathCanvas = __esm({
31109
32105
  const rad = mid * Math.PI / 180;
31110
32106
  out.push({
31111
32107
  type: "text",
32108
+ fontFamily,
31112
32109
  x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
31113
32110
  y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
31114
32111
  text: angle.label,
@@ -31130,7 +32127,7 @@ var init_MathCanvas = __esm({
31130
32127
  fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
31131
32128
  });
31132
32129
  if (p.label) {
31133
- out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: p.color ?? "#111827", fontSize: labelFontSize });
32130
+ out.push({ type: "text", fontFamily, x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: p.color ?? "#111827", fontSize: labelFontSize });
31134
32131
  }
31135
32132
  }
31136
32133
  for (const v of vectors) {
@@ -31141,7 +32138,7 @@ var init_MathCanvas = __esm({
31141
32138
  const y2 = mapY(v.y + v.vy);
31142
32139
  out.push({ type: "arrow", x1, y1, x2, y2, color: v.color ?? "#7c3aed", lineWidth: 2 });
31143
32140
  if (v.label) {
31144
- out.push({ type: "text", x: x2 + 6, y: y2 - 6, text: v.label, color: v.color ?? "#7c3aed", fontSize: labelFontSize });
32141
+ out.push({ type: "text", fontFamily, x: x2 + 6, y: y2 - 6, text: v.label, color: v.color ?? "#7c3aed", fontSize: labelFontSize });
31145
32142
  }
31146
32143
  }
31147
32144
  out.push(...shapes);
@@ -31162,6 +32159,7 @@ var init_MathCanvas = __esm({
31162
32159
  showTickLabels,
31163
32160
  tickLabelFontSize,
31164
32161
  labelFontSize,
32162
+ fontFamily,
31165
32163
  showCurveLabels,
31166
32164
  curves,
31167
32165
  points,
@@ -31217,6 +32215,7 @@ var init_MathCanvas = __esm({
31217
32215
  width,
31218
32216
  height,
31219
32217
  backgroundColor,
32218
+ fontFamily,
31220
32219
  shapes: derivedShapes,
31221
32220
  drawables,
31222
32221
  projector,
@@ -32449,13 +33448,13 @@ var init_MapView = __esm({
32449
33448
  shadowSize: [41, 41]
32450
33449
  });
32451
33450
  L.Marker.prototype.options.icon = defaultIcon;
32452
- const { useEffect: useEffect64, useRef: useRef65, useCallback: useCallback93, useState: useState99 } = React87__namespace.default;
33451
+ const { useEffect: useEffect67, useRef: useRef69, useCallback: useCallback95, useState: useState102 } = React87__namespace.default;
32453
33452
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
32454
33453
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
32455
33454
  function MapUpdater({ centerLat, centerLng, zoom }) {
32456
33455
  const map = useMap();
32457
- const prevRef = useRef65({ centerLat, centerLng, zoom });
32458
- useEffect64(() => {
33456
+ const prevRef = useRef69({ centerLat, centerLng, zoom });
33457
+ useEffect67(() => {
32459
33458
  const prev = prevRef.current;
32460
33459
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
32461
33460
  map.setView([centerLat, centerLng], zoom);
@@ -32466,7 +33465,7 @@ var init_MapView = __esm({
32466
33465
  }
32467
33466
  function MapClickHandler({ onMapClick }) {
32468
33467
  const map = useMap();
32469
- useEffect64(() => {
33468
+ useEffect67(() => {
32470
33469
  if (!onMapClick) return;
32471
33470
  const handler = (e) => {
32472
33471
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -32494,8 +33493,8 @@ var init_MapView = __esm({
32494
33493
  showAttribution = true
32495
33494
  }) {
32496
33495
  const eventBus = useEventBus2();
32497
- const [clickedPosition, setClickedPosition] = useState99(null);
32498
- const handleMapClick = useCallback93((lat, lng) => {
33496
+ const [clickedPosition, setClickedPosition] = useState102(null);
33497
+ const handleMapClick = useCallback95((lat, lng) => {
32499
33498
  if (showClickedPin) {
32500
33499
  setClickedPosition({ lat, lng });
32501
33500
  }
@@ -32504,7 +33503,7 @@ var init_MapView = __esm({
32504
33503
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
32505
33504
  }
32506
33505
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
32507
- const handleMarkerClick = useCallback93((marker) => {
33506
+ const handleMarkerClick = useCallback95((marker) => {
32508
33507
  onMarkerClick?.(marker);
32509
33508
  if (markerClickEvent) {
32510
33509
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -47084,7 +48083,7 @@ function getAllEvents(traits2) {
47084
48083
  function EventDispatcherTab({ traits: traits2, schema }) {
47085
48084
  const eventBus = useEventBus();
47086
48085
  const { t } = hooks.useTranslate();
47087
- const [log9, setLog] = React87__namespace.useState([]);
48086
+ const [log11, setLog] = React87__namespace.useState([]);
47088
48087
  const prevStatesRef = React87__namespace.useRef(/* @__PURE__ */ new Map());
47089
48088
  React87__namespace.useEffect(() => {
47090
48089
  for (const trait of traits2) {
@@ -47148,9 +48147,9 @@ function EventDispatcherTab({ traits: traits2, schema }) {
47148
48147
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", weight: "medium", className: "text-muted-foreground mb-1", children: t("debug.otherEvents") }),
47149
48148
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1", children: unavailableEvents.map((event) => /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "default", size: "sm", className: "opacity-50", children: event }, event)) })
47150
48149
  ] }),
47151
- log9.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
48150
+ log11.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
47152
48151
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", weight: "medium", className: "text-muted-foreground mb-1", children: t("debug.recentTransitions") }),
47153
- /* @__PURE__ */ jsxRuntime.jsx(Stack, { gap: "xs", children: log9.map((entry, i) => /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
48152
+ /* @__PURE__ */ jsxRuntime.jsx(Stack, { gap: "xs", children: log11.map((entry, i) => /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
47154
48153
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-primary", children: entry.traitName }),
47155
48154
  " ",
47156
48155
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: entry.from }),
@@ -49341,6 +50340,7 @@ var init_component_registry_generated = __esm({
49341
50340
  init_FormSection();
49342
50341
  init_FormSectionHeader();
49343
50342
  init_FxOverlay();
50343
+ init_GameAudioCue();
49344
50344
  init_GameAudioToggle();
49345
50345
  init_GameHud();
49346
50346
  init_GameIcon();
@@ -49618,6 +50618,7 @@ var init_component_registry_generated = __esm({
49618
50618
  "FormLayout": FormLayout,
49619
50619
  "FormSectionHeader": FormSectionHeader,
49620
50620
  "FxOverlay": FxOverlay,
50621
+ "GameAudioCue": GameAudioCue,
49621
50622
  "GameAudioToggle": GameAudioToggle,
49622
50623
  "GameHud": GameHud,
49623
50624
  "GameIcon": GameIcon,
@@ -51592,7 +52593,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51592
52593
  };
51593
52594
  }, [traitBindings]);
51594
52595
  const executeTransitionEffects = React87.useCallback(async (params) => {
51595
- const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log9 } = params;
52596
+ const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log11 } = params;
51596
52597
  const traitName = binding.trait.name;
51597
52598
  const linkedEntity = binding.linkedEntity || "";
51598
52599
  const entityId = payload?.entityId;
@@ -51721,7 +52722,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51721
52722
  if (sharedKey !== void 0) {
51722
52723
  sharedWrites.push({ field, value });
51723
52724
  }
51724
- log9.debug("set:write", {
52725
+ log11.debug("set:write", {
51725
52726
  traitName,
51726
52727
  field,
51727
52728
  value: JSON.stringify(value),
@@ -51789,7 +52790,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51789
52790
  core.mergeEntityFrame(sharedEntityStore.getSnapshot(sharedKey), sharedWrites)
51790
52791
  );
51791
52792
  }
51792
- log9.debug("effects:executed", () => ({
52793
+ log11.debug("effects:executed", () => ({
51793
52794
  traitName,
51794
52795
  transition: `${previousState}->${newState}`,
51795
52796
  event: flushEvent,
@@ -51799,7 +52800,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51799
52800
  slotsTouched: Array.from(pendingSlots.keys()).join(",")
51800
52801
  }));
51801
52802
  for (const [slot, patterns] of pendingSlots) {
51802
- log9.debug("flush:slot", {
52803
+ log11.debug("flush:slot", {
51803
52804
  traitName,
51804
52805
  slot,
51805
52806
  patternCount: patterns.length,
@@ -51817,7 +52818,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51817
52818
  publishBindingSnapshot(traitName, liveEntity);
51818
52819
  }
51819
52820
  } catch (error) {
51820
- log9.error("effects:error", {
52821
+ log11.error("effects:error", {
51821
52822
  traitName,
51822
52823
  transition: `${previousState}->${newState}`,
51823
52824
  event: flushEvent,
@@ -52999,11 +54000,20 @@ function BrowserPlayground({
52999
54000
  mode = "mock",
53000
54001
  initialPagePath,
53001
54002
  height,
53002
- className
54003
+ className,
54004
+ paused
53003
54005
  }) {
53004
54006
  const [runtime] = React87.useState(
53005
54007
  () => new OrbitalServerRuntime.OrbitalServerRuntime({ mode, debug: false })
53006
54008
  );
54009
+ React87.useEffect(() => {
54010
+ if (paused === void 0) return;
54011
+ if (paused) {
54012
+ runtime.pauseTicks();
54013
+ } else {
54014
+ runtime.resumeTicks();
54015
+ }
54016
+ }, [runtime, paused]);
53007
54017
  const registrationReady = React87.useMemo(() => {
53008
54018
  const orbitalNames = schema.orbitals.map((o) => o.name);
53009
54019
  playgroundLog.debug("register:start", { schema: schema.name, orbitalNames });
@@ -53058,6 +54068,316 @@ function BrowserPlayground({
53058
54068
  );
53059
54069
  }
53060
54070
 
54071
+ // runtime/OrbitalPluginHost.tsx
54072
+ init_useEventBus();
54073
+ var log10 = logger.createLogger("almadar:ui:plugin-host");
54074
+ var UI_SLOT_SET = new Set(core.UI_SLOTS);
54075
+ function isUISlot(value) {
54076
+ return UI_SLOT_SET.has(value);
54077
+ }
54078
+ var OrbitalPluginHostContext = React87.createContext(null);
54079
+ function useOrbitalPluginHostContext(caller) {
54080
+ const ctx = React87.useContext(OrbitalPluginHostContext);
54081
+ if (!ctx) {
54082
+ throw new Error(`${caller} must be used within an <OrbitalPluginHost>.`);
54083
+ }
54084
+ return ctx;
54085
+ }
54086
+ function isTraitState(value) {
54087
+ return value !== void 0 && typeof value.currentState === "string";
54088
+ }
54089
+ function toTraitState(runtime, orbital, trait, stateName, lastEvent) {
54090
+ if (runtime) {
54091
+ const raw = runtime.getState(orbital, trait);
54092
+ if (isTraitState(raw)) return raw;
54093
+ }
54094
+ return { traitName: trait, currentState: stateName, previousState: null, lastEvent, context: {} };
54095
+ }
54096
+ function isKeymapValue(value) {
54097
+ if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return false;
54098
+ return Object.values(value).every((entry) => Array.isArray(entry) && entry.every((k) => typeof k === "string"));
54099
+ }
54100
+ function findInlineTrait(schema, orbitalName, traitName) {
54101
+ const orbital = schema.orbitals.find((o) => o.name === orbitalName);
54102
+ if (!orbital) return void 0;
54103
+ for (const ref of orbital.traits) {
54104
+ if (typeof ref === "string") continue;
54105
+ if ("ref" in ref) continue;
54106
+ if (ref.name === traitName) return ref;
54107
+ }
54108
+ return void 0;
54109
+ }
54110
+ function readKeymapConfig(trait, knob) {
54111
+ const declared = trait?.config?.[knob]?.default;
54112
+ return isKeymapValue(declared) ? declared : {};
54113
+ }
54114
+ function traitNamesOf(orbital) {
54115
+ const names = [];
54116
+ for (const ref of orbital.traits) {
54117
+ if (typeof ref === "string") {
54118
+ names.push(ref);
54119
+ } else if ("ref" in ref) {
54120
+ if (ref.name) names.push(ref.name);
54121
+ } else {
54122
+ names.push(ref.name);
54123
+ }
54124
+ }
54125
+ return names;
54126
+ }
54127
+ function useDeclaredCaptureTable(opts) {
54128
+ const { pluginId, orbital, trait, keymapKnob = "keymap", target = "shell" } = opts;
54129
+ const ctx = useOrbitalPluginHostContext("useDeclaredCaptureTable");
54130
+ return React87.useMemo(() => {
54131
+ const schema = ctx.getPluginSchema(pluginId);
54132
+ const inlineTrait = schema ? findInlineTrait(schema, orbital, trait) : void 0;
54133
+ const keymap = readKeymapConfig(inlineTrait, keymapKnob);
54134
+ const currentState = ctx.getState(pluginId, orbital, trait)?.currentState;
54135
+ const keys = currentState !== void 0 ? keymap[currentState] ?? [] : [];
54136
+ return { [target]: { mode: currentState ?? "unknown", keys: new Set(keys) } };
54137
+ }, [ctx, pluginId, orbital, trait, keymapKnob, target]);
54138
+ }
54139
+ function useOrbitalPluginHost() {
54140
+ const ctx = useOrbitalPluginHostContext("useOrbitalPluginHost");
54141
+ return { getState: ctx.getState, lastEvent: ctx.lastEvent, errors: ctx.errors };
54142
+ }
54143
+ function buildMockEffectHandlers(opts) {
54144
+ const { pluginId, denySet, slotsRef, navigateRef, notifyRef } = opts;
54145
+ const handlers = {
54146
+ renderUI: (slot, pattern, props, priority) => {
54147
+ if (!isUISlot(slot)) {
54148
+ log10.warn("renderUI:unknown-slot", { pluginId, slot });
54149
+ return;
54150
+ }
54151
+ if (pattern === null) {
54152
+ slotsRef.current.clear(slot);
54153
+ return;
54154
+ }
54155
+ slotsRef.current.render({ target: slot, pattern: pattern.type, props, priority, sourceTrait: pluginId });
54156
+ }
54157
+ };
54158
+ if (denySet.has("navigate")) {
54159
+ handlers.navigate = (path) => log10.warn("navigate:denied", { pluginId, path });
54160
+ } else if (navigateRef.current) {
54161
+ handlers.navigate = (path) => navigateRef.current?.(path);
54162
+ }
54163
+ if (denySet.has("notify")) {
54164
+ handlers.notify = (message, type) => log10.warn("notify:denied", { pluginId, message, type });
54165
+ } else if (notifyRef.current) {
54166
+ handlers.notify = (message, type) => notifyRef.current?.(message, type);
54167
+ }
54168
+ if (denySet.has("persist")) {
54169
+ handlers.persist = async (action, entityType) => {
54170
+ log10.warn("persist:denied", { pluginId, action, entityType });
54171
+ return void 0;
54172
+ };
54173
+ }
54174
+ if (denySet.has("call-service")) {
54175
+ handlers.callService = async (service, action) => {
54176
+ log10.warn("call-service:denied", { pluginId, service, action });
54177
+ return null;
54178
+ };
54179
+ }
54180
+ return handlers;
54181
+ }
54182
+ function PluginRuntimeMount({
54183
+ plugin,
54184
+ mode,
54185
+ transport,
54186
+ deny,
54187
+ navigate,
54188
+ notify,
54189
+ onTransition,
54190
+ onDispatched,
54191
+ onError
54192
+ }) {
54193
+ const bus = useEventBus();
54194
+ const slots = context.useUISlots();
54195
+ const slotsRef = React87.useRef(slots);
54196
+ slotsRef.current = slots;
54197
+ const navigateRef = React87.useRef(navigate);
54198
+ navigateRef.current = navigate;
54199
+ const notifyRef = React87.useRef(notify);
54200
+ notifyRef.current = notify;
54201
+ const [mockRuntime] = React87.useState(() => {
54202
+ if (mode !== "mock") return void 0;
54203
+ return new OrbitalServerRuntime.OrbitalServerRuntime({
54204
+ mode: "mock",
54205
+ debug: false,
54206
+ effectHandlers: buildMockEffectHandlers({
54207
+ pluginId: plugin.id,
54208
+ denySet: new Set(deny ?? []),
54209
+ slotsRef,
54210
+ navigateRef,
54211
+ notifyRef
54212
+ })
54213
+ });
54214
+ });
54215
+ const registrationReady = React87.useMemo(() => {
54216
+ if (mockRuntime) return mockRuntime.register(plugin.schema);
54217
+ if (mode === "server" && transport) return transport.register(plugin.schema);
54218
+ return Promise.resolve();
54219
+ }, [mockRuntime, transport, mode, plugin.schema]);
54220
+ React87.useEffect(() => {
54221
+ if (mode === "server" && !transport) {
54222
+ onError(plugin.id, 'OrbitalPluginHost: mode "server" requires a transport');
54223
+ }
54224
+ }, [mode, transport, plugin.id, onError]);
54225
+ const teardownTimerRef = React87.useRef(null);
54226
+ React87.useEffect(() => {
54227
+ if (teardownTimerRef.current !== null) {
54228
+ clearTimeout(teardownTimerRef.current);
54229
+ teardownTimerRef.current = null;
54230
+ }
54231
+ return () => {
54232
+ teardownTimerRef.current = setTimeout(() => {
54233
+ teardownTimerRef.current = null;
54234
+ if (mockRuntime) {
54235
+ mockRuntime.unregisterAll();
54236
+ } else if (mode === "server" && transport) {
54237
+ void transport.unregister();
54238
+ }
54239
+ }, 0);
54240
+ };
54241
+ }, [mockRuntime, transport, mode]);
54242
+ const ownOrbitals = React87.useMemo(
54243
+ () => new Set(plugin.schema.orbitals.map((o) => o.name)),
54244
+ [plugin.schema]
54245
+ );
54246
+ React87.useEffect(() => {
54247
+ let cancelled = false;
54248
+ void registrationReady.then(() => {
54249
+ if (cancelled || !mockRuntime) return;
54250
+ const states = [];
54251
+ for (const orbital of plugin.schema.orbitals) {
54252
+ for (const traitName of traitNamesOf(orbital)) {
54253
+ const raw = mockRuntime.getState(orbital.name, traitName);
54254
+ if (isTraitState(raw)) states.push([orbital.name, traitName, raw]);
54255
+ }
54256
+ }
54257
+ if (states.length > 0) onDispatched(plugin.id, null, states);
54258
+ }).catch((err) => {
54259
+ if (!cancelled) onError(plugin.id, err instanceof Error ? err.message : String(err));
54260
+ });
54261
+ return () => {
54262
+ cancelled = true;
54263
+ };
54264
+ }, [registrationReady, mockRuntime, plugin, onDispatched, onError]);
54265
+ const dispatch = React87.useCallback(
54266
+ async (row, payload) => {
54267
+ await registrationReady;
54268
+ let response;
54269
+ if (mockRuntime) {
54270
+ response = await mockRuntime.processOrbitalEvent(row.orbital, {
54271
+ event: row.trigger,
54272
+ payload,
54273
+ targetTrait: row.trait
54274
+ });
54275
+ } else if (mode === "server" && transport) {
54276
+ response = await transport.sendEvent(row.orbital, row.trigger, payload);
54277
+ } else {
54278
+ return;
54279
+ }
54280
+ if (!response.success) {
54281
+ onError(plugin.id, response.error ?? `${plugin.id}: ${row.trigger} failed`);
54282
+ }
54283
+ for (const entry of response.emittedEvents ?? []) {
54284
+ if (entry.event === row.trigger) continue;
54285
+ const sourceOrbital = entry.source?.orbital;
54286
+ if (sourceOrbital !== void 0 && ownOrbitals.has(sourceOrbital)) {
54287
+ bus.emit(`UI:${entry.event}`, entry.payload, entry.source);
54288
+ }
54289
+ }
54290
+ const states = [];
54291
+ for (const [traitName, stateName] of Object.entries(response.states)) {
54292
+ states.push([row.orbital, traitName, toTraitState(mockRuntime, row.orbital, traitName, stateName, row.trigger)]);
54293
+ onTransition?.(plugin.id, row.orbital, traitName, stateName);
54294
+ }
54295
+ onDispatched(plugin.id, row.trigger, states);
54296
+ },
54297
+ [registrationReady, mockRuntime, mode, transport, plugin, bus, ownOrbitals, onError, onTransition, onDispatched]
54298
+ );
54299
+ React87.useEffect(() => {
54300
+ const unsubs = plugin.inbound.map(
54301
+ (row) => bus.on(`UI:${row.busEvent}`, (event) => {
54302
+ void dispatch(row, event.payload).catch((err) => {
54303
+ onError(plugin.id, err instanceof Error ? err.message : String(err));
54304
+ });
54305
+ })
54306
+ );
54307
+ return () => {
54308
+ for (const unsub of unsubs) unsub();
54309
+ };
54310
+ }, [bus, plugin, dispatch, onError]);
54311
+ return null;
54312
+ }
54313
+ function OrbitalPluginHost({
54314
+ plugins,
54315
+ mode = "mock",
54316
+ transport,
54317
+ deny,
54318
+ navigate,
54319
+ notify,
54320
+ onTransition,
54321
+ children
54322
+ }) {
54323
+ const [tick, setTick] = React87.useState(0);
54324
+ const [errors, setErrors] = React87.useState({});
54325
+ const statesRef = React87.useRef(/* @__PURE__ */ new Map());
54326
+ const lastEventRef = React87.useRef(/* @__PURE__ */ new Map());
54327
+ const pluginSchemas = React87.useMemo(
54328
+ () => new Map(plugins.map((plugin) => [plugin.id, plugin.schema])),
54329
+ [plugins]
54330
+ );
54331
+ const handleDispatched = React87.useCallback(
54332
+ (pluginId, trigger, states) => {
54333
+ if (trigger !== null) {
54334
+ lastEventRef.current.set(pluginId, trigger);
54335
+ }
54336
+ let byKey = statesRef.current.get(pluginId);
54337
+ if (!byKey) {
54338
+ byKey = /* @__PURE__ */ new Map();
54339
+ statesRef.current.set(pluginId, byKey);
54340
+ }
54341
+ for (const [orbital, trait, state] of states) {
54342
+ byKey.set(`${orbital}.${trait}`, state);
54343
+ }
54344
+ setTick((t) => t + 1);
54345
+ },
54346
+ []
54347
+ );
54348
+ const handleError = React87.useCallback((pluginId, message) => {
54349
+ setErrors((prev) => prev[pluginId] === message ? prev : { ...prev, [pluginId]: message });
54350
+ }, []);
54351
+ const contextValue = React87.useMemo(
54352
+ () => ({
54353
+ getState: (pluginId, orbital, trait) => statesRef.current.get(pluginId)?.get(`${orbital}.${trait}`),
54354
+ lastEvent: (pluginId) => lastEventRef.current.get(pluginId),
54355
+ errors,
54356
+ tick,
54357
+ getPluginSchema: (pluginId) => pluginSchemas.get(pluginId)
54358
+ }),
54359
+ [errors, tick, pluginSchemas]
54360
+ );
54361
+ return /* @__PURE__ */ jsxRuntime.jsxs(OrbitalPluginHostContext.Provider, { value: contextValue, children: [
54362
+ plugins.map((plugin) => /* @__PURE__ */ jsxRuntime.jsx(
54363
+ PluginRuntimeMount,
54364
+ {
54365
+ plugin,
54366
+ mode,
54367
+ transport,
54368
+ deny,
54369
+ navigate,
54370
+ notify,
54371
+ onTransition,
54372
+ onDispatched: handleDispatched,
54373
+ onError: handleError
54374
+ },
54375
+ plugin.id
54376
+ )),
54377
+ children ?? null
54378
+ ] });
54379
+ }
54380
+
53061
54381
  // runtime/index.ts
53062
54382
  init_perf();
53063
54383
 
@@ -53135,8 +54455,11 @@ Object.defineProperty(exports, "wrapCallbackForEvent", {
53135
54455
  });
53136
54456
  exports.BrowserPlayground = BrowserPlayground;
53137
54457
  exports.OrbPreview = OrbPreview;
54458
+ exports.OrbitalPluginHost = OrbitalPluginHost;
53138
54459
  exports.clearSchemaCache = clearSchemaCache;
53139
54460
  exports.createClientEffectHandlers = createClientEffectHandlers;
54461
+ exports.useDeclaredCaptureTable = useDeclaredCaptureTable;
54462
+ exports.useOrbitalPluginHost = useOrbitalPluginHost;
53140
54463
  exports.usePerfBuffer = usePerfBuffer;
53141
54464
  exports.useResolvedSchema = useResolvedSchema;
53142
54465
  exports.useTraitStateMachine = useTraitStateMachine;