@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
@@ -13787,7 +13787,7 @@ function collectDrawnItems(nodes) {
13787
13787
  for (const n of nodes) {
13788
13788
  switch (n.type) {
13789
13789
  case "draw-sprite":
13790
- if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id, anchor: n.anchor, width: n.width, height: n.height });
13790
+ if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id, anchor: n.anchor, width: n.width, height: n.height, rotation: n.rotation });
13791
13791
  break;
13792
13792
  case "draw-shape":
13793
13793
  case "draw-text":
@@ -13797,7 +13797,7 @@ function collectDrawnItems(nodes) {
13797
13797
  break;
13798
13798
  case "draw-sprite-layer":
13799
13799
  for (const it of n.items) {
13800
- if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id, anchor: it.anchor, width: it.width, height: it.height });
13800
+ if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id, anchor: it.anchor, width: it.width, height: it.height, rotation: it.rotation });
13801
13801
  }
13802
13802
  break;
13803
13803
  case "draw-shape-layer":
@@ -13817,15 +13817,39 @@ function buildHitIndex(items) {
13817
13817
  }
13818
13818
  return m;
13819
13819
  }
13820
+ function withPreviewPosition(nodes, id, pos) {
13821
+ return nodes.map((n) => {
13822
+ if (n.type === "draw-sprite-layer" || n.type === "draw-shape-layer" || n.type === "draw-text-layer") {
13823
+ if (!n.items.some((it) => it.id === id)) return n;
13824
+ return { ...n, items: n.items.map((it) => it.id === id ? { ...it, position: pos } : it) };
13825
+ }
13826
+ if (n.type === "draw-group") {
13827
+ if (!Array.isArray(n.items)) return n;
13828
+ return { ...n, items: withPreviewPosition(n.items, id, pos) };
13829
+ }
13830
+ if (n.id === id && "position" in n) {
13831
+ return { ...n, position: pos };
13832
+ }
13833
+ return n;
13834
+ });
13835
+ }
13820
13836
  function hitTestSprites(items, projector, point) {
13821
13837
  for (let i = items.length - 1; i >= 0; i--) {
13822
13838
  const it = items[i];
13823
13839
  if (it.id === void 0) continue;
13824
13840
  const r2 = spriteRect(projector, { position: it.pos, anchor: it.anchor, width: it.width, height: it.height });
13825
- if (point.x >= r2.x && point.x <= r2.x + r2.w && point.y >= r2.y && point.y <= r2.y + r2.h) return it.id;
13841
+ const test = it.rotation ? rotatePoint(point, { x: r2.x + r2.w / 2, y: r2.y + r2.h / 2 }, -it.rotation) : point;
13842
+ if (test.x >= r2.x && test.x <= r2.x + r2.w && test.y >= r2.y && test.y <= r2.y + r2.h) return it.id;
13826
13843
  }
13827
13844
  return void 0;
13828
13845
  }
13846
+ function rotatePoint(p, center, radians) {
13847
+ const cos = Math.cos(radians);
13848
+ const sin = Math.sin(radians);
13849
+ const dx = p.x - center.x;
13850
+ const dy = p.y - center.y;
13851
+ return { x: center.x + dx * cos - dy * sin, y: center.y + dx * sin + dy * cos };
13852
+ }
13829
13853
  var init_hitTest = __esm({
13830
13854
  "lib/drawable/hitTest.ts"() {
13831
13855
  init_contract();
@@ -13834,6 +13858,41 @@ var init_hitTest = __esm({
13834
13858
  function normalizeBackdrop(bg) {
13835
13859
  return typeof bg === "string" ? { url: bg, role: "decoration", category: "background" } : bg;
13836
13860
  }
13861
+ function selectionOverlayNodes(projector, item) {
13862
+ const r2 = spriteRect(projector, { position: item.pos, anchor: item.anchor, width: item.width, height: item.height });
13863
+ const tw = projector.tileWidth;
13864
+ const cellTopLeft = projector.anchorPoint(item.pos, "top-left");
13865
+ const offsetX = (r2.x - cellTopLeft.x) / tw;
13866
+ const offsetY = (r2.y - cellTopLeft.y) / tw;
13867
+ const width = r2.w / tw;
13868
+ const height = r2.h / tw;
13869
+ const handle = EDIT_HANDLE_SIZE_PX / tw;
13870
+ const ring = {
13871
+ type: "draw-shape",
13872
+ shape: "rect",
13873
+ position: item.pos,
13874
+ anchor: "top-left",
13875
+ offsetX,
13876
+ offsetY,
13877
+ width,
13878
+ height,
13879
+ stroke: EDIT_SELECTION_COLOR,
13880
+ strokeWidth: 2,
13881
+ fill: "none"
13882
+ };
13883
+ const handles = EDIT_SELECTION_CORNERS.map(([cx, cy]) => ({
13884
+ type: "draw-shape",
13885
+ shape: "rect",
13886
+ position: item.pos,
13887
+ anchor: "top-left",
13888
+ offsetX: offsetX + cx * width - handle / 2,
13889
+ offsetY: offsetY + cy * height - handle / 2,
13890
+ width: handle,
13891
+ height: handle,
13892
+ fill: EDIT_SELECTION_COLOR
13893
+ }));
13894
+ return [ring, ...handles];
13895
+ }
13837
13896
  function Canvas2D({
13838
13897
  className,
13839
13898
  isLoading = false,
@@ -13847,6 +13906,12 @@ function Canvas2D({
13847
13906
  tileLeaveEvent,
13848
13907
  keyMap,
13849
13908
  keyUpMap,
13909
+ editable = false,
13910
+ selectedId = null,
13911
+ onSelect,
13912
+ onMove,
13913
+ selectEvent,
13914
+ moveEvent,
13850
13915
  camera = "pan-zoom",
13851
13916
  scale = 0.4,
13852
13917
  tileWidth,
@@ -14104,10 +14169,22 @@ function Canvas2D({
14104
14169
  painter.scale(cam.zoom, cam.zoom);
14105
14170
  painter.translate(-viewportSize.width / 2 - cam.x, -viewportSize.height / 2 - cam.y);
14106
14171
  const dctx = { projector, time: timeMs, invalidate: bumpAtlas };
14107
- for (const node of drawables) paintDrawable(painter, node, dctx);
14172
+ let paintNodes = drawables;
14173
+ if (editable) {
14174
+ const drag = editDragRef.current;
14175
+ if (drag && drag.moved) {
14176
+ paintNodes = withPreviewPosition(paintNodes, drag.id, { x: drag.previewX, y: drag.previewY });
14177
+ }
14178
+ const selectedItem = selectedId != null ? drawnItems.find((it) => it.id === selectedId) : void 0;
14179
+ if (selectedItem) {
14180
+ const overlaySource = drag && drag.moved && drag.id === selectedId ? { ...selectedItem, pos: { x: drag.previewX, y: drag.previewY } } : selectedItem;
14181
+ paintNodes = [...paintNodes, ...selectionOverlayNodes(projector, overlaySource)];
14182
+ }
14183
+ }
14184
+ for (const node of paintNodes) paintDrawable(painter, node, dctx);
14108
14185
  painter.restore();
14109
- scheduleAnimation(drawables);
14110
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
14186
+ scheduleAnimation(paintNodes);
14187
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance, editable, selectedId, drawnItems]);
14111
14188
  React96.useEffect(() => {
14112
14189
  drawTimeRef.current = draw;
14113
14190
  }, [draw]);
@@ -14152,23 +14229,83 @@ function Canvas2D({
14152
14229
  };
14153
14230
  }, [camera, followTarget, lerpToTarget, draw]);
14154
14231
  const singlePointerActiveRef = React96.useRef(false);
14232
+ const editDragRef = React96.useRef(null);
14233
+ const pointerToScene = React96.useCallback((clientX, clientY) => {
14234
+ const canvas = canvasRef.current;
14235
+ if (!canvas) return { x: 0, y: 0 };
14236
+ const world = screenToWorld(clientX, clientY, canvas, viewportSize);
14237
+ const adjustedX = world.x - scaledTileWidth / 2;
14238
+ const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
14239
+ return unproject(adjustedX, adjustedY);
14240
+ }, [screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject]);
14155
14241
  const handleCanvasPointerDown = React96.useCallback((e) => {
14156
14242
  singlePointerActiveRef.current = true;
14243
+ if (editable) {
14244
+ if (!canvasRef.current) return;
14245
+ const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
14246
+ const hitId = hitTestSprites(drawnItems, projector, world);
14247
+ if (hitId === void 0) return;
14248
+ const item = [...drawnItems].reverse().find((it) => it.id === hitId);
14249
+ if (!item) return;
14250
+ editDragRef.current = {
14251
+ id: hitId,
14252
+ pointerId: e.pointerId,
14253
+ startClientX: e.clientX,
14254
+ startClientY: e.clientY,
14255
+ startSceneX: item.pos.x,
14256
+ startSceneY: item.pos.y,
14257
+ moved: false,
14258
+ previewX: item.pos.x,
14259
+ previewY: item.pos.y
14260
+ };
14261
+ return;
14262
+ }
14157
14263
  if (enableCamera) handlePointerDown(e);
14158
- }, [enableCamera, handlePointerDown]);
14264
+ }, [editable, screenToWorld, viewportSize, drawnItems, projector, enableCamera, handlePointerDown]);
14159
14265
  const handleCanvasPointerMove = React96.useCallback((e) => {
14266
+ if (editable) {
14267
+ const drag = editDragRef.current;
14268
+ if (!drag || drag.pointerId !== e.pointerId) return;
14269
+ const dxPx = e.clientX - drag.startClientX;
14270
+ const dyPx = e.clientY - drag.startClientY;
14271
+ if (!drag.moved && Math.abs(dxPx) + Math.abs(dyPx) <= 5) return;
14272
+ drag.moved = true;
14273
+ const nowScene = pointerToScene(e.clientX, e.clientY);
14274
+ const startScene = pointerToScene(drag.startClientX, drag.startClientY);
14275
+ drag.previewX = drag.startSceneX + (nowScene.x - startScene.x);
14276
+ drag.previewY = drag.startSceneY + (nowScene.y - startScene.y);
14277
+ draw();
14278
+ return;
14279
+ }
14160
14280
  if (enableCamera) handlePointerMove(e, () => draw());
14161
- }, [enableCamera, handlePointerMove, draw]);
14281
+ }, [editable, pointerToScene, draw, enableCamera, handlePointerMove]);
14162
14282
  const handleCanvasHover = React96.useCallback((e) => {
14163
14283
  if (singlePointerActiveRef.current) return;
14164
14284
  if (!tileHoverEvent || !canvasRef.current) return;
14165
- const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
14166
- const adjustedX = world.x - scaledTileWidth / 2;
14167
- const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
14168
- const isoPos = unproject(adjustedX, adjustedY);
14285
+ const isoPos = pointerToScene(e.clientX, e.clientY);
14169
14286
  eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
14170
- }, [screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject, tileHoverEvent, eventBus]);
14287
+ }, [pointerToScene, tileHoverEvent, eventBus]);
14171
14288
  const handleCanvasPointerUp = React96.useCallback((e) => {
14289
+ if (editable) {
14290
+ singlePointerActiveRef.current = false;
14291
+ const drag = editDragRef.current;
14292
+ if (drag && drag.pointerId === e.pointerId) {
14293
+ editDragRef.current = null;
14294
+ if (drag.moved) {
14295
+ onMove?.(drag.id, drag.previewX, drag.previewY);
14296
+ if (moveEvent) eventBus.emit(`UI:${moveEvent}`, { id: drag.id, x: drag.previewX, y: drag.previewY });
14297
+ draw();
14298
+ return;
14299
+ }
14300
+ const next = selectedId === drag.id ? null : drag.id;
14301
+ onSelect?.(next);
14302
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: next });
14303
+ return;
14304
+ }
14305
+ onSelect?.(null);
14306
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: null });
14307
+ return;
14308
+ }
14172
14309
  singlePointerActiveRef.current = false;
14173
14310
  if (enableCamera) handlePointerUp();
14174
14311
  if (dragDistance() > 5) return;
@@ -14179,16 +14316,14 @@ function Canvas2D({
14179
14316
  eventBus.emit(`UI:${unitClickEvent}`, { unitId: spriteHit });
14180
14317
  return;
14181
14318
  }
14182
- const adjustedX = world.x - scaledTileWidth / 2;
14183
- const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
14184
- const isoPos = unproject(adjustedX, adjustedY);
14319
+ const isoPos = pointerToScene(e.clientX, e.clientY);
14185
14320
  const hitId = hitIndex.get(`${isoPos.x},${isoPos.y}`);
14186
14321
  if (hitId !== void 0 && unitClickEvent) {
14187
14322
  eventBus.emit(`UI:${unitClickEvent}`, { unitId: hitId });
14188
14323
  } else if (tileClickEvent) {
14189
14324
  eventBus.emit(`UI:${tileClickEvent}`, { x: isoPos.x, y: isoPos.y });
14190
14325
  }
14191
- }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject, hitIndex, drawnItems, projector, tileClickEvent, unitClickEvent, eventBus]);
14326
+ }, [editable, selectedId, onMove, moveEvent, onSelect, selectEvent, eventBus, draw, enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, drawnItems, projector, tileClickEvent, unitClickEvent, hitIndex, pointerToScene]);
14192
14327
  const handleCanvasPointerLeave = React96.useCallback(() => {
14193
14328
  handleMouseLeave();
14194
14329
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -14207,7 +14342,7 @@ function Canvas2D({
14207
14342
  }, [enableCamera, handlePointerUp]);
14208
14343
  const gestureHandlers = useCanvasGestures({
14209
14344
  canvasRef,
14210
- enabled: enableCamera || !!tileHoverEvent || !!tileClickEvent || !!unitClickEvent,
14345
+ enabled: enableCamera || !!tileHoverEvent || !!tileClickEvent || !!unitClickEvent || editable,
14211
14346
  onPointerDown: handleCanvasPointerDown,
14212
14347
  onPointerMove: handleCanvasPointerMove,
14213
14348
  onPointerUp: handleCanvasPointerUp,
@@ -14339,7 +14474,7 @@ function Canvas2D({
14339
14474
  }
14340
14475
  ) });
14341
14476
  }
14342
- var canvas2DLog;
14477
+ var canvas2DLog, EDIT_SELECTION_COLOR, EDIT_HANDLE_SIZE_PX, EDIT_SELECTION_CORNERS;
14343
14478
  var init_Canvas2D = __esm({
14344
14479
  "components/game/molecules/Canvas2D.tsx"() {
14345
14480
  "use client";
@@ -14364,8 +14499,12 @@ var init_Canvas2D = __esm({
14364
14499
  init_DrawGroup();
14365
14500
  init_registry();
14366
14501
  init_hitTest();
14502
+ init_contract();
14367
14503
  init_isometric();
14368
14504
  canvas2DLog = logger.createLogger("almadar:ui:game-canvas");
14505
+ EDIT_SELECTION_COLOR = "#3b82f6";
14506
+ EDIT_HANDLE_SIZE_PX = 8;
14507
+ EDIT_SELECTION_CORNERS = [[0, 0], [1, 0], [0, 1], [1, 1]];
14369
14508
  Canvas2D.displayName = "Canvas2D";
14370
14509
  }
14371
14510
  });
@@ -14407,6 +14546,12 @@ function Canvas({
14407
14546
  featureClickEvent,
14408
14547
  keyMap,
14409
14548
  keyUpMap,
14549
+ editable,
14550
+ selectedId,
14551
+ onSelect,
14552
+ onMove,
14553
+ selectEvent,
14554
+ moveEvent,
14410
14555
  children
14411
14556
  }) {
14412
14557
  canvasLog.debug("Canvas render", { mode, drawablesCount: drawables?.length, projection, camera: camera ? JSON.stringify(camera) : void 0 });
@@ -14484,6 +14629,12 @@ function Canvas({
14484
14629
  tileLeaveEvent,
14485
14630
  keyMap,
14486
14631
  keyUpMap,
14632
+ editable,
14633
+ selectedId,
14634
+ onSelect,
14635
+ onMove,
14636
+ selectEvent,
14637
+ moveEvent,
14487
14638
  ...children !== void 0 ? { children } : {}
14488
14639
  }
14489
14640
  );
@@ -14505,15 +14656,19 @@ function GameAudioToggle({
14505
14656
  size = "sm",
14506
14657
  className,
14507
14658
  onAsset,
14508
- offAsset
14659
+ offAsset,
14660
+ toggleEvent
14509
14661
  }) {
14510
14662
  const ctx = providers.useGameAudioContextOptional();
14511
14663
  const [localMuted, setLocalMuted] = React96.useState(false);
14512
14664
  const muted = ctx ? ctx.muted : localMuted;
14513
14665
  const setMuted = ctx ? ctx.setMuted : setLocalMuted;
14666
+ const eventBus = useEventBus();
14514
14667
  const handleToggle = React96.useCallback(() => {
14515
- setMuted(!muted);
14516
- }, [muted, setMuted]);
14668
+ const next = !muted;
14669
+ setMuted(next);
14670
+ if (toggleEvent) eventBus.emit(`UI:${toggleEvent}`, { muted: next });
14671
+ }, [muted, setMuted, toggleEvent, eventBus]);
14517
14672
  const activeAsset = muted ? offAsset : onAsset;
14518
14673
  return /* @__PURE__ */ jsxRuntime.jsx(
14519
14674
  Button,
@@ -14532,10 +14687,319 @@ var init_GameAudioToggle = __esm({
14532
14687
  "use client";
14533
14688
  init_atoms();
14534
14689
  init_cn();
14690
+ init_useEventBus();
14535
14691
  init_GameIcon();
14536
14692
  GameAudioToggle.displayName = "GameAudioToggle";
14537
14693
  }
14538
14694
  });
14695
+ function pickPath(entry) {
14696
+ if (Array.isArray(entry.path)) {
14697
+ return entry.path[Math.floor(Math.random() * entry.path.length)];
14698
+ }
14699
+ return entry.path;
14700
+ }
14701
+ function useGameAudio({
14702
+ manifest,
14703
+ baseUrl = "",
14704
+ initialMuted = false,
14705
+ initialVolume = 1
14706
+ }) {
14707
+ const [muted, setMutedState] = React96.useState(initialMuted);
14708
+ const [masterVolume, setMasterVolumeState] = React96.useState(initialVolume);
14709
+ const mutedRef = React96.useRef(muted);
14710
+ const volumeRef = React96.useRef(masterVolume);
14711
+ const manifestRef = React96.useRef(manifest);
14712
+ mutedRef.current = muted;
14713
+ volumeRef.current = masterVolume;
14714
+ manifestRef.current = manifest;
14715
+ const poolsRef = React96.useRef(/* @__PURE__ */ new Map());
14716
+ const getOrCreateElement = React96.useCallback((key) => {
14717
+ const entry = manifestRef.current[key];
14718
+ if (!entry) return null;
14719
+ let pool = poolsRef.current.get(key);
14720
+ if (!pool) {
14721
+ pool = [];
14722
+ poolsRef.current.set(key, pool);
14723
+ }
14724
+ const maxSize = entry.poolSize ?? 1;
14725
+ for (const audio of pool) {
14726
+ if (audio.paused && (audio.ended || audio.currentTime === 0)) {
14727
+ return audio;
14728
+ }
14729
+ }
14730
+ if (pool.length < maxSize) {
14731
+ const src = baseUrl + pickPath(entry);
14732
+ const audio = new Audio(src);
14733
+ audio.loop = entry.loop ?? false;
14734
+ pool.push(audio);
14735
+ return audio;
14736
+ }
14737
+ if (!entry.loop) {
14738
+ let oldest = pool[0];
14739
+ for (const audio of pool) {
14740
+ if (audio.currentTime > oldest.currentTime) {
14741
+ oldest = audio;
14742
+ }
14743
+ }
14744
+ oldest.pause();
14745
+ oldest.currentTime = 0;
14746
+ return oldest;
14747
+ }
14748
+ return null;
14749
+ }, [baseUrl]);
14750
+ const play = React96.useCallback((key) => {
14751
+ if (mutedRef.current) return;
14752
+ const entry = manifestRef.current[key];
14753
+ if (!entry) return;
14754
+ const audio = getOrCreateElement(key);
14755
+ if (!audio) return;
14756
+ audio.volume = Math.min(1, (entry.volume ?? 1) * volumeRef.current);
14757
+ if (!entry.loop) {
14758
+ audio.currentTime = 0;
14759
+ }
14760
+ const promise = audio.play();
14761
+ if (promise) {
14762
+ promise.catch(() => {
14763
+ });
14764
+ }
14765
+ }, [getOrCreateElement]);
14766
+ const stop = React96.useCallback((key) => {
14767
+ const pool = poolsRef.current.get(key);
14768
+ if (!pool) return;
14769
+ for (const audio of pool) {
14770
+ audio.pause();
14771
+ audio.currentTime = 0;
14772
+ }
14773
+ }, []);
14774
+ const currentMusicKeyRef = React96.useRef(null);
14775
+ const currentMusicElRef = React96.useRef(null);
14776
+ const musicFadeRef = React96.useRef(null);
14777
+ const pendingMusicKeyRef = React96.useRef(null);
14778
+ const clearMusicFade = React96.useCallback(() => {
14779
+ if (musicFadeRef.current) {
14780
+ clearInterval(musicFadeRef.current);
14781
+ musicFadeRef.current = null;
14782
+ }
14783
+ }, []);
14784
+ const playMusic = React96.useCallback((key) => {
14785
+ if (key === currentMusicKeyRef.current) return;
14786
+ pendingMusicKeyRef.current = key;
14787
+ const entry = manifestRef.current[key];
14788
+ if (!entry) return;
14789
+ const fadeDurationMs = entry.crossfadeDurationMs ?? 1500;
14790
+ const stepMs = 50;
14791
+ const totalSteps = Math.max(1, fadeDurationMs / stepMs);
14792
+ const targetVolume = Math.min(1, (entry.volume ?? 1) * volumeRef.current);
14793
+ clearMusicFade();
14794
+ const src = baseUrl + (Array.isArray(entry.path) ? entry.path[0] : entry.path);
14795
+ const incoming = new Audio(src);
14796
+ incoming.loop = true;
14797
+ incoming.volume = 0;
14798
+ const outgoing = currentMusicElRef.current;
14799
+ const outgoingStartVol = outgoing?.volume ?? 0;
14800
+ currentMusicKeyRef.current = key;
14801
+ currentMusicElRef.current = incoming;
14802
+ if (!mutedRef.current) {
14803
+ incoming.play().catch(() => {
14804
+ currentMusicKeyRef.current = null;
14805
+ currentMusicElRef.current = outgoing;
14806
+ });
14807
+ }
14808
+ let step = 0;
14809
+ musicFadeRef.current = setInterval(() => {
14810
+ step++;
14811
+ const progress = Math.min(step / totalSteps, 1);
14812
+ incoming.volume = Math.min(1, targetVolume * progress);
14813
+ if (outgoing) {
14814
+ outgoing.volume = Math.max(0, outgoingStartVol * (1 - progress));
14815
+ }
14816
+ if (progress >= 1) {
14817
+ clearMusicFade();
14818
+ if (outgoing) {
14819
+ outgoing.pause();
14820
+ outgoing.src = "";
14821
+ }
14822
+ }
14823
+ }, stepMs);
14824
+ }, [baseUrl, clearMusicFade]);
14825
+ const stopMusic = React96.useCallback((fadeDurationMs = 1e3) => {
14826
+ const outgoing = currentMusicElRef.current;
14827
+ if (!outgoing) return;
14828
+ currentMusicKeyRef.current = null;
14829
+ currentMusicElRef.current = null;
14830
+ pendingMusicKeyRef.current = null;
14831
+ clearMusicFade();
14832
+ const startVolume = outgoing.volume;
14833
+ const stepMs = 50;
14834
+ const totalSteps = Math.max(1, fadeDurationMs / stepMs);
14835
+ let step = 0;
14836
+ musicFadeRef.current = setInterval(() => {
14837
+ step++;
14838
+ const progress = step / totalSteps;
14839
+ outgoing.volume = Math.max(0, startVolume * (1 - progress));
14840
+ if (progress >= 1) {
14841
+ clearMusicFade();
14842
+ outgoing.pause();
14843
+ outgoing.src = "";
14844
+ }
14845
+ }, stepMs);
14846
+ }, [clearMusicFade]);
14847
+ const stopAll = React96.useCallback(() => {
14848
+ for (const pool of poolsRef.current.values()) {
14849
+ for (const audio of pool) {
14850
+ audio.pause();
14851
+ audio.currentTime = 0;
14852
+ }
14853
+ }
14854
+ stopMusic(0);
14855
+ }, [stopMusic]);
14856
+ const setMuted = React96.useCallback((value) => {
14857
+ setMutedState(value);
14858
+ if (value) {
14859
+ for (const [key, pool] of poolsRef.current.entries()) {
14860
+ if (manifestRef.current[key]?.loop) {
14861
+ for (const audio of pool) {
14862
+ if (!audio.paused) audio.pause();
14863
+ }
14864
+ }
14865
+ }
14866
+ currentMusicElRef.current?.pause();
14867
+ } else {
14868
+ for (const [key, pool] of poolsRef.current.entries()) {
14869
+ const entry = manifestRef.current[key];
14870
+ if (entry?.loop && entry?.autostart) {
14871
+ for (const audio of pool) {
14872
+ if (audio.paused) audio.play().catch(() => {
14873
+ });
14874
+ }
14875
+ }
14876
+ }
14877
+ const musicEl = currentMusicElRef.current;
14878
+ if (musicEl) {
14879
+ musicEl.play().catch(() => {
14880
+ });
14881
+ }
14882
+ }
14883
+ }, []);
14884
+ const setMasterVolume = React96.useCallback((volume) => {
14885
+ const clamped = Math.max(0, Math.min(1, volume));
14886
+ setMasterVolumeState(clamped);
14887
+ for (const [key, pool] of poolsRef.current.entries()) {
14888
+ const entryVol = manifestRef.current[key]?.volume ?? 1;
14889
+ for (const audio of pool) {
14890
+ audio.volume = Math.min(1, entryVol * clamped);
14891
+ }
14892
+ }
14893
+ if (!musicFadeRef.current && currentMusicElRef.current) {
14894
+ const key = currentMusicKeyRef.current;
14895
+ const entryVol = key ? manifestRef.current[key]?.volume ?? 1 : 1;
14896
+ currentMusicElRef.current.volume = Math.min(1, entryVol * clamped);
14897
+ }
14898
+ }, []);
14899
+ const unlockedRef = React96.useRef(false);
14900
+ React96.useEffect(() => {
14901
+ const autoKeys = Object.keys(manifest).filter((k) => manifest[k].autostart);
14902
+ const hasPendingMusic = () => pendingMusicKeyRef.current !== null;
14903
+ const hasAutoStart = autoKeys.length > 0;
14904
+ if (!hasAutoStart && !hasPendingMusic()) return;
14905
+ const unlock = () => {
14906
+ if (unlockedRef.current) return;
14907
+ unlockedRef.current = true;
14908
+ if (!mutedRef.current) {
14909
+ for (const key of autoKeys) {
14910
+ play(key);
14911
+ }
14912
+ const pending = pendingMusicKeyRef.current;
14913
+ if (pending && pending !== currentMusicKeyRef.current) {
14914
+ playMusic(pending);
14915
+ }
14916
+ }
14917
+ };
14918
+ document.addEventListener("click", unlock, { once: true });
14919
+ document.addEventListener("keydown", unlock, { once: true });
14920
+ document.addEventListener("touchstart", unlock, { once: true });
14921
+ return () => {
14922
+ document.removeEventListener("click", unlock);
14923
+ document.removeEventListener("keydown", unlock);
14924
+ document.removeEventListener("touchstart", unlock);
14925
+ };
14926
+ }, [manifest, play, playMusic]);
14927
+ React96.useEffect(() => {
14928
+ return () => {
14929
+ clearMusicFade();
14930
+ for (const pool of poolsRef.current.values()) {
14931
+ for (const audio of pool) {
14932
+ audio.pause();
14933
+ audio.src = "";
14934
+ }
14935
+ }
14936
+ poolsRef.current.clear();
14937
+ if (currentMusicElRef.current) {
14938
+ currentMusicElRef.current.pause();
14939
+ currentMusicElRef.current.src = "";
14940
+ currentMusicElRef.current = null;
14941
+ }
14942
+ };
14943
+ }, [clearMusicFade]);
14944
+ return {
14945
+ play,
14946
+ stop,
14947
+ stopAll,
14948
+ playMusic,
14949
+ stopMusic,
14950
+ muted,
14951
+ setMuted,
14952
+ masterVolume,
14953
+ setMasterVolume
14954
+ };
14955
+ }
14956
+ var init_useGameAudio = __esm({
14957
+ "hooks/useGameAudio.ts"() {
14958
+ "use client";
14959
+ useGameAudio.displayName = "useGameAudio";
14960
+ }
14961
+ });
14962
+ function GameAudioCue({
14963
+ cue,
14964
+ cueSeq,
14965
+ music,
14966
+ muted,
14967
+ volume,
14968
+ manifest,
14969
+ baseUrl
14970
+ }) {
14971
+ const { play, playMusic, stopMusic, setMuted, setMasterVolume } = useGameAudio({
14972
+ manifest,
14973
+ baseUrl,
14974
+ initialMuted: muted,
14975
+ initialVolume: volume
14976
+ });
14977
+ const prevCueSeqRef = React96.useRef(cueSeq);
14978
+ React96.useEffect(() => {
14979
+ if (cue && cueSeq !== void 0 && cueSeq !== prevCueSeqRef.current) {
14980
+ play(cue);
14981
+ }
14982
+ prevCueSeqRef.current = cueSeq;
14983
+ }, [cue, cueSeq, play]);
14984
+ React96.useEffect(() => {
14985
+ if (music) playMusic(music);
14986
+ else stopMusic();
14987
+ }, [music, playMusic, stopMusic]);
14988
+ React96.useEffect(() => {
14989
+ if (muted !== void 0) setMuted(muted);
14990
+ }, [muted, setMuted]);
14991
+ React96.useEffect(() => {
14992
+ if (volume !== void 0) setMasterVolume(volume);
14993
+ }, [volume, setMasterVolume]);
14994
+ return null;
14995
+ }
14996
+ var init_GameAudioCue = __esm({
14997
+ "components/game/atoms/GameAudioCue.tsx"() {
14998
+ "use client";
14999
+ init_useGameAudio();
15000
+ GameAudioCue.displayName = "GameAudioCue";
15001
+ }
15002
+ });
14539
15003
  function isKnownState(s) {
14540
15004
  return s in DEFAULT_STATE_STYLES;
14541
15005
  }
@@ -15234,15 +15698,18 @@ var init_StateJsonView = __esm({
15234
15698
  StateJsonView.displayName = "StateJsonView";
15235
15699
  }
15236
15700
  });
15237
- var GAME_FONTS, GameShell;
15238
- var init_GameShell = __esm({
15239
- "components/game/templates/GameShell.tsx"() {
15240
- init_cn();
15241
- init_Box();
15242
- init_Card();
15243
- init_Typography();
15244
- init_AtlasImage();
15245
- GAME_FONTS = {
15701
+
15702
+ // lib/gameFonts.ts
15703
+ function resolveGameFontFamily(input) {
15704
+ if (!input) return void 0;
15705
+ const resolved = GAME_FONT_KEYS[input];
15706
+ if (resolved) return `'${resolved}', ui-sans-serif, system-ui, sans-serif`;
15707
+ return input;
15708
+ }
15709
+ var GAME_FONT_KEYS;
15710
+ var init_gameFonts = __esm({
15711
+ "lib/gameFonts.ts"() {
15712
+ GAME_FONT_KEYS = {
15246
15713
  fredoka: "Fredoka",
15247
15714
  future: "Kenney Future",
15248
15715
  "future-narrow": "Kenney Future Narrow",
@@ -15250,6 +15717,17 @@ var init_GameShell = __esm({
15250
15717
  blocks: "Kenney Blocks",
15251
15718
  mini: "Kenney Mini"
15252
15719
  };
15720
+ }
15721
+ });
15722
+ var GameShell;
15723
+ var init_GameShell = __esm({
15724
+ "components/game/templates/GameShell.tsx"() {
15725
+ init_cn();
15726
+ init_gameFonts();
15727
+ init_Box();
15728
+ init_Card();
15729
+ init_Typography();
15730
+ init_AtlasImage();
15253
15731
  GameShell = ({
15254
15732
  appName = "Game",
15255
15733
  hud,
@@ -15263,7 +15741,7 @@ var init_GameShell = __esm({
15263
15741
  fontFamily,
15264
15742
  "data-theme": dataTheme
15265
15743
  }) => {
15266
- const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
15744
+ const displayFont = resolveGameFontFamily(fontFamily);
15267
15745
  return /* @__PURE__ */ jsxRuntime.jsxs(
15268
15746
  Box,
15269
15747
  {
@@ -15282,7 +15760,7 @@ var init_GameShell = __esm({
15282
15760
  // passed — an always-on inline stamp would shadow the orbital's
15283
15761
  // inline-theme font-family-display token (inline style beats the
15284
15762
  // theme provider's vars for the whole shell subtree).
15285
- ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
15763
+ ...displayFont ? { "--font-family-display": displayFont } : {}
15286
15764
  },
15287
15765
  children: [
15288
15766
  backgroundAsset && /* @__PURE__ */ jsxRuntime.jsx(
@@ -15493,14 +15971,15 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
15493
15971
  ctx.closePath();
15494
15972
  ctx.fill();
15495
15973
  }
15496
- function drawShape(ctx, shape, width, height, allShapes) {
15974
+ function drawShape(ctx, shape, width, height, allShapes, fontFamily) {
15497
15975
  ctx.save();
15498
15976
  const opacity = shape.opacity ?? 1;
15499
15977
  ctx.globalAlpha = opacity;
15500
15978
  const stroke = resolveColor2(shape.color, ctx, "#333333");
15501
15979
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
15502
15980
  ctx.lineWidth = shape.lineWidth ?? 2;
15503
- if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
15981
+ const dashPattern = shape.dash ? DASH_PATTERNS[shape.dash] : void 0;
15982
+ if (dashPattern) ctx.setLineDash([...dashPattern]);
15504
15983
  switch (shape.type) {
15505
15984
  case "grid": {
15506
15985
  const step = shape.step ?? 40;
@@ -15622,7 +16101,7 @@ function drawShape(ctx, shape, width, height, allShapes) {
15622
16101
  case "text": {
15623
16102
  if (shape.x == null || shape.y == null || !shape.text) break;
15624
16103
  ctx.fillStyle = stroke;
15625
- ctx.font = `${shape.fontSize ?? 14}px ${themeBodyFont(ctx.canvas)}`;
16104
+ ctx.font = `${shape.fontSize ?? 14}px ${shape.fontFamily ?? fontFamily ?? themeBodyFont(ctx.canvas)}`;
15626
16105
  ctx.textAlign = shape.align ?? "left";
15627
16106
  ctx.textBaseline = "middle";
15628
16107
  ctx.fillText(shape.text, shape.x, shape.y);
@@ -15664,7 +16143,7 @@ function drawShape(ctx, shape, width, height, allShapes) {
15664
16143
  }
15665
16144
  ctx.restore();
15666
16145
  }
15667
- function readoutShapes(readouts, width) {
16146
+ function readoutShapes(readouts, width, fontFamily) {
15668
16147
  const out = [];
15669
16148
  const chipH = 18;
15670
16149
  const gap = 6;
@@ -15688,13 +16167,14 @@ function readoutShapes(readouts, width) {
15688
16167
  text,
15689
16168
  color: "#ffffff",
15690
16169
  fontSize: 10,
15691
- align: "center"
16170
+ align: "center",
16171
+ fontFamily
15692
16172
  });
15693
16173
  rightEdge = chipX - gap;
15694
16174
  }
15695
16175
  return out;
15696
16176
  }
15697
- function traceShapes(panel, k, width, height) {
16177
+ function traceShapes(panel, k, width, height, fontFamily) {
15698
16178
  const w = panel.width ?? Math.round(width * 0.32);
15699
16179
  const h = panel.height ?? Math.round(height * 0.28);
15700
16180
  const x = panel.x ?? width - w - 8;
@@ -15748,14 +16228,14 @@ function traceShapes(panel, k, width, height) {
15748
16228
  out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
15749
16229
  }
15750
16230
  if (series.label) {
15751
- out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
16231
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9, fontFamily });
15752
16232
  }
15753
16233
  });
15754
16234
  if (panel.yLabel) {
15755
- out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
16235
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right", fontFamily });
15756
16236
  }
15757
16237
  if (panel.xLabel) {
15758
- out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
16238
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right", fontFamily });
15759
16239
  }
15760
16240
  return out;
15761
16241
  }
@@ -15778,13 +16258,14 @@ var init_LearningCanvas = __esm({
15778
16258
  init_useEventBus();
15779
16259
  init_webPainter2d();
15780
16260
  init_paintDispatch();
15781
- DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
16261
+ DASH_PATTERNS = { solid: [], dashed: [6, 4], dotted: [2, 3] };
15782
16262
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15783
16263
  LearningCanvas = ({
15784
16264
  className,
15785
16265
  width = 600,
15786
16266
  height = 400,
15787
16267
  backgroundColor,
16268
+ fontFamily,
15788
16269
  shapes = [],
15789
16270
  drawables,
15790
16271
  projector,
@@ -15820,10 +16301,10 @@ var init_LearningCanvas = __esm({
15820
16301
  }, [shapes]);
15821
16302
  const derivedShapes = React96.useMemo(() => {
15822
16303
  if (!traces?.length && !readouts?.length) return shapes;
15823
- const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
15824
- const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
16304
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height, fontFamily));
16305
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width, fontFamily) : [];
15825
16306
  return [...shapes, ...traceOut, ...readoutOut];
15826
- }, [shapes, traces, readouts, width, height]);
16307
+ }, [shapes, traces, readouts, width, height, fontFamily]);
15827
16308
  const draw = React96.useCallback(() => {
15828
16309
  const _perfT = ui.perfStart("learningcanvas:paint");
15829
16310
  const canvas = canvasRef.current;
@@ -15842,15 +16323,15 @@ var init_LearningCanvas = __esm({
15842
16323
  ctx.fillRect(0, 0, width, height);
15843
16324
  }
15844
16325
  for (const shape of derivedShapes) {
15845
- if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
16326
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes, fontFamily);
15846
16327
  }
15847
16328
  for (const shape of derivedShapes) {
15848
- if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
16329
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes, fontFamily);
15849
16330
  }
15850
16331
  if (drawables?.length && projector) {
15851
16332
  const painter = createWebPainter(ctx, invalidateRef.current);
15852
16333
  const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
15853
- const dctx = { projector, time: timeMs, invalidate: invalidateRef.current, fontFamily: themeBodyFont(canvas) };
16334
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current, fontFamily: fontFamily || themeBodyFont(canvas) };
15854
16335
  for (const node of drawables) {
15855
16336
  paintDrawable(painter, node, dctx);
15856
16337
  }
@@ -19622,6 +20103,250 @@ var init_EmptyState = __esm({
19622
20103
  EmptyState.displayName = "EmptyState";
19623
20104
  }
19624
20105
  });
20106
+
20107
+ // lib/editorMotions.ts
20108
+ function clamp(value, min, max) {
20109
+ return Math.max(min, Math.min(max, value));
20110
+ }
20111
+ function computeLines(text) {
20112
+ const lines = [];
20113
+ let start = 0;
20114
+ for (let i = 0; i <= text.length; i++) {
20115
+ if (i === text.length || text[i] === "\n") {
20116
+ lines.push({ start, end: i });
20117
+ start = i + 1;
20118
+ }
20119
+ }
20120
+ return lines;
20121
+ }
20122
+ function lineIndexAt(lines, pos) {
20123
+ for (let i = 0; i < lines.length; i++) {
20124
+ if (pos <= lines[i].end) return i;
20125
+ }
20126
+ return lines.length - 1;
20127
+ }
20128
+ function findWords(text) {
20129
+ const words = [];
20130
+ const re = /\w+|\S+/g;
20131
+ let m;
20132
+ while ((m = re.exec(text)) !== null) {
20133
+ words.push({ start: m.index, end: m.index + m[0].length });
20134
+ }
20135
+ return words;
20136
+ }
20137
+ function nextWordStart(text, pos) {
20138
+ for (const w of findWords(text)) {
20139
+ if (w.start > pos) return w.start;
20140
+ }
20141
+ return text.length;
20142
+ }
20143
+ function prevWordStart(text, pos) {
20144
+ let result = 0;
20145
+ for (const w of findWords(text)) {
20146
+ if (w.start < pos) result = w.start;
20147
+ else break;
20148
+ }
20149
+ return result;
20150
+ }
20151
+ function nextWordEnd(text, pos) {
20152
+ for (const w of findWords(text)) {
20153
+ const lastChar = w.end - 1;
20154
+ if (lastChar > pos) return lastChar;
20155
+ }
20156
+ return text.length > 0 ? text.length - 1 : 0;
20157
+ }
20158
+ function firstNonBlank(text, line) {
20159
+ let i = line.start;
20160
+ while (i < line.end && (text[i] === " " || text[i] === " ")) i++;
20161
+ return i;
20162
+ }
20163
+ function nextParagraphBoundary(lines, fromLineIdx, textLength) {
20164
+ for (let i = fromLineIdx + 1; i < lines.length; i++) {
20165
+ if (lines[i].start === lines[i].end) return lines[i].start;
20166
+ }
20167
+ return textLength;
20168
+ }
20169
+ function prevParagraphBoundary(lines, fromLineIdx) {
20170
+ for (let i = fromLineIdx - 1; i >= 0; i--) {
20171
+ if (lines[i].start === lines[i].end) return lines[i].start;
20172
+ }
20173
+ return 0;
20174
+ }
20175
+ function applyMotion(text, caret, motion, count) {
20176
+ const n = Math.max(1, count);
20177
+ const lines = computeLines(text);
20178
+ const lineIdx = lineIndexAt(lines, caret);
20179
+ const line = lines[lineIdx];
20180
+ switch (motion) {
20181
+ case "left":
20182
+ return clamp(caret - n, line.start, line.end);
20183
+ case "right":
20184
+ return clamp(caret + n, line.start, line.end);
20185
+ case "up": {
20186
+ const col = caret - line.start;
20187
+ const targetIdx = clamp(lineIdx - n, 0, lines.length - 1);
20188
+ const target = lines[targetIdx];
20189
+ return clamp(target.start + col, target.start, target.end);
20190
+ }
20191
+ case "down": {
20192
+ const col = caret - line.start;
20193
+ const targetIdx = clamp(lineIdx + n, 0, lines.length - 1);
20194
+ const target = lines[targetIdx];
20195
+ return clamp(target.start + col, target.start, target.end);
20196
+ }
20197
+ case "word-forward": {
20198
+ let pos = caret;
20199
+ for (let i = 0; i < n; i++) pos = nextWordStart(text, pos);
20200
+ return pos;
20201
+ }
20202
+ case "word-back": {
20203
+ let pos = caret;
20204
+ for (let i = 0; i < n; i++) pos = prevWordStart(text, pos);
20205
+ return pos;
20206
+ }
20207
+ case "word-end": {
20208
+ let pos = caret;
20209
+ for (let i = 0; i < n; i++) pos = nextWordEnd(text, pos);
20210
+ return pos;
20211
+ }
20212
+ case "line-start":
20213
+ return line.start;
20214
+ case "line-end":
20215
+ return line.end > line.start ? line.end - 1 : line.start;
20216
+ case "first-nonblank":
20217
+ return firstNonBlank(text, line);
20218
+ case "doc-start":
20219
+ return 0;
20220
+ case "doc-end":
20221
+ return text.length;
20222
+ case "paragraph-forward": {
20223
+ let pos = caret;
20224
+ for (let i = 0; i < n; i++) {
20225
+ pos = nextParagraphBoundary(lines, lineIndexAt(lines, pos), text.length);
20226
+ }
20227
+ return pos;
20228
+ }
20229
+ case "paragraph-back": {
20230
+ let pos = caret;
20231
+ for (let i = 0; i < n; i++) {
20232
+ pos = prevParagraphBoundary(lines, lineIndexAt(lines, pos));
20233
+ }
20234
+ return pos;
20235
+ }
20236
+ case "line":
20237
+ case "selection":
20238
+ return caret;
20239
+ default: {
20240
+ const _exhaustive = motion;
20241
+ return _exhaustive;
20242
+ }
20243
+ }
20244
+ }
20245
+ function motionRange(text, caret, motion, count, selection) {
20246
+ if (motion === "selection") {
20247
+ if (!selection) return [caret, caret];
20248
+ return [Math.min(selection[0], selection[1]), Math.max(selection[0], selection[1])];
20249
+ }
20250
+ const lines = computeLines(text);
20251
+ if (motion === "line") {
20252
+ const n = Math.max(1, count);
20253
+ const startIdx = lineIndexAt(lines, caret);
20254
+ const endIdx = clamp(startIdx + n - 1, 0, lines.length - 1);
20255
+ const start2 = lines[startIdx].start;
20256
+ const rawEnd = lines[endIdx].end;
20257
+ const end2 = rawEnd < text.length ? rawEnd + 1 : rawEnd;
20258
+ return [start2, end2];
20259
+ }
20260
+ const newCaret = applyMotion(text, caret, motion, count);
20261
+ let start = Math.min(caret, newCaret);
20262
+ let end = Math.max(caret, newCaret);
20263
+ if (motion === "word-end" || motion === "line-end") {
20264
+ end = Math.min(Math.max(start, newCaret) + 1, text.length);
20265
+ } else if (motion === "word-forward" && newCaret > caret) {
20266
+ const startLine = lineIndexAt(lines, caret);
20267
+ const endLine = lineIndexAt(lines, newCaret);
20268
+ if (endLine !== startLine) {
20269
+ end = lines[startLine].end;
20270
+ }
20271
+ }
20272
+ return [start, end];
20273
+ }
20274
+ function applyOperator(text, range, operator, register) {
20275
+ const start = clamp(range[0], 0, text.length);
20276
+ const end = clamp(range[1], start, text.length);
20277
+ const removed = text.slice(start, end);
20278
+ if (operator === "yank") {
20279
+ return { text, caret: start, register: removed };
20280
+ }
20281
+ return { text: text.slice(0, start) + text.slice(end), caret: start, register: removed };
20282
+ }
20283
+ var init_editorMotions = __esm({
20284
+ "lib/editorMotions.ts"() {
20285
+ }
20286
+ });
20287
+ function isMotionPayload(payload) {
20288
+ return !!payload && typeof payload.editorId === "string" && typeof payload.motion === "string" && typeof payload.count === "number";
20289
+ }
20290
+ function isOperatePayload(payload) {
20291
+ return !!payload && typeof payload.editorId === "string" && typeof payload.operator === "string" && typeof payload.motion === "string" && typeof payload.count === "number";
20292
+ }
20293
+ function isInsertTextPayload(payload) {
20294
+ return !!payload && typeof payload.editorId === "string" && typeof payload.text === "string";
20295
+ }
20296
+ function isSetModePayload(payload) {
20297
+ return !!payload && typeof payload.editorId === "string" && typeof payload.mode === "string" && typeof payload.caret === "string";
20298
+ }
20299
+ function useEditorCapabilities(args) {
20300
+ const [caretMode, setCaretMode] = React96.useState("bar");
20301
+ const registerRef = React96.useRef("");
20302
+ useEventListener(`UI:${args.events.onMotion}`, (evt) => {
20303
+ if (!args.editorId || !isMotionPayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
20304
+ const ta = args.textareaRef.current;
20305
+ if (!ta) return;
20306
+ const { motion, count } = evt.payload;
20307
+ const newCaret = applyMotion(ta.value, ta.selectionStart, motion, count);
20308
+ if (ta.selectionStart !== ta.selectionEnd) {
20309
+ ta.setSelectionRange(ta.selectionStart, newCaret);
20310
+ } else {
20311
+ ta.setSelectionRange(newCaret, newCaret);
20312
+ }
20313
+ });
20314
+ useEventListener(`UI:${args.events.onOperate}`, (evt) => {
20315
+ if (!args.editorId || !isOperatePayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
20316
+ const ta = args.textareaRef.current;
20317
+ if (!ta) return;
20318
+ const { operator, motion, count } = evt.payload;
20319
+ const selection = motion === "selection" ? [ta.selectionStart, ta.selectionEnd] : void 0;
20320
+ const range = motionRange(ta.value, ta.selectionStart, motion, count, selection);
20321
+ const result = applyOperator(ta.value, range, operator, registerRef.current);
20322
+ registerRef.current = result.register;
20323
+ if (operator === "yank") {
20324
+ ta.setSelectionRange(range[0], range[0]);
20325
+ } else {
20326
+ ta.setRangeText("", range[0], range[1], "end");
20327
+ }
20328
+ args.applyChange(ta.value);
20329
+ });
20330
+ useEventListener(`UI:${args.events.onInsertText}`, (evt) => {
20331
+ if (!args.editorId || !isInsertTextPayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
20332
+ const ta = args.textareaRef.current;
20333
+ if (!ta) return;
20334
+ ta.setRangeText(evt.payload.text, ta.selectionStart, ta.selectionEnd, "end");
20335
+ args.applyChange(ta.value);
20336
+ });
20337
+ useEventListener(`UI:${args.events.onSetMode}`, (evt) => {
20338
+ if (!args.editorId || !isSetModePayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
20339
+ setCaretMode(evt.payload.caret);
20340
+ });
20341
+ return { caretMode };
20342
+ }
20343
+ var init_useEditorCapabilities = __esm({
20344
+ "components/core/molecules/markdown/useEditorCapabilities.ts"() {
20345
+ "use client";
20346
+ init_useEventBus();
20347
+ init_editorMotions();
20348
+ }
20349
+ });
19625
20350
  function isLanguageRegistered(lang) {
19626
20351
  return CODE_LANGUAGE_SET.has(lang) || dynamicallyLoaded.has(lang);
19627
20352
  }
@@ -19707,7 +20432,36 @@ function useLanguageReady(language) {
19707
20432
  }, [language]);
19708
20433
  return ready;
19709
20434
  }
19710
- var dynamicallyLoaded, codeLanguageLoader, orbStyleOverrides, orbStyle, loloStyleOverrides, loloStyle, log4, CODE_LANGUAGES, CODE_LANGUAGE_SET, DIFF_STYLES, DIFF_STYLE_FALLBACK, LINE_PROPS_FN, HIDDEN_LINE_NUMBERS, HIGHLIGHT_CAPACITY_BYTES, CodeBlock;
20435
+ function resolveHighlightStyle(lang) {
20436
+ if (lang === "orb") return orbStyle;
20437
+ if (lang === "lolo") return loloStyle;
20438
+ return dark__default.default;
20439
+ }
20440
+ function plainCodeColorOf(style) {
20441
+ return style['code[class*="language-"]']?.color ?? "#d4d4d4";
20442
+ }
20443
+ function buildLineProps(errorLines, extraClassName) {
20444
+ return (lineNumber) => {
20445
+ const base = {
20446
+ "data-line": String(lineNumber - 1),
20447
+ ...extraClassName ? { className: extraClassName } : {}
20448
+ };
20449
+ const severity = errorLines?.get(lineNumber);
20450
+ if (!severity) return base;
20451
+ return {
20452
+ ...base,
20453
+ style: {
20454
+ display: "block",
20455
+ backgroundColor: severity === "error" ? "rgba(248, 113, 113, 0.18)" : "rgba(251, 191, 36, 0.18)",
20456
+ // amber-400 @ 18%
20457
+ borderLeft: `3px solid ${severity === "error" ? "#ef4444" : "#f59e0b"}`,
20458
+ paddingLeft: "0.5rem",
20459
+ marginLeft: "-0.5rem"
20460
+ }
20461
+ };
20462
+ };
20463
+ }
20464
+ var dynamicallyLoaded, codeLanguageLoader, orbStyleOverrides, orbStyle, loloStyleOverrides, loloStyle, log4, 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;
19711
20465
  var init_CodeBlock = __esm({
19712
20466
  "components/core/molecules/markdown/CodeBlock.tsx"() {
19713
20467
  init_cn();
@@ -19723,6 +20477,7 @@ var init_CodeBlock = __esm({
19723
20477
  init_Textarea();
19724
20478
  init_Icon();
19725
20479
  init_useEventBus();
20480
+ init_useEditorCapabilities();
19726
20481
  SyntaxHighlighter__default.default.registerLanguage("json", langJson__default.default);
19727
20482
  SyntaxHighlighter__default.default.registerLanguage("javascript", langJavascript__default.default);
19728
20483
  SyntaxHighlighter__default.default.registerLanguage("js", langJavascript__default.default);
@@ -19941,6 +20696,15 @@ var init_CodeBlock = __esm({
19941
20696
  LINE_PROPS_FN = (n) => ({ "data-line": String(n - 1) });
19942
20697
  HIDDEN_LINE_NUMBERS = { display: "none" };
19943
20698
  HIGHLIGHT_CAPACITY_BYTES = 512 * 1024;
20699
+ MONO_FONT_FAMILY = 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace';
20700
+ VIEWER_LINE_NUMBER_STYLE = {
20701
+ minWidth: "2.5em",
20702
+ paddingRight: "1rem",
20703
+ textAlign: "right",
20704
+ userSelect: "none",
20705
+ opacity: 0.5,
20706
+ fontVariantNumeric: "tabular-nums"
20707
+ };
19944
20708
  CodeBlock = React96__namespace.default.memo(
19945
20709
  ({
19946
20710
  code: rawCode,
@@ -19965,15 +20729,39 @@ var init_CodeBlock = __esm({
19965
20729
  actions,
19966
20730
  isLoading = false,
19967
20731
  error,
19968
- showCopy
20732
+ showCopy,
20733
+ // editor capability surface — P1 wires these
20734
+ editorId,
20735
+ onEditorFocus = "EDITOR_FOCUS",
20736
+ onEditorBlur = "EDITOR_BLUR",
20737
+ onMotion = "MOTION",
20738
+ onOperate = "OPERATE",
20739
+ onInsertText = "INSERT_TEXT",
20740
+ onSetMode = "SET_MODE",
20741
+ motions = [
20742
+ "left",
20743
+ "right",
20744
+ "up",
20745
+ "down",
20746
+ "word-forward",
20747
+ "word-back",
20748
+ "word-end",
20749
+ "line-start",
20750
+ "line-end",
20751
+ "first-nonblank",
20752
+ "doc-start",
20753
+ "doc-end",
20754
+ "paragraph-forward",
20755
+ "paragraph-back",
20756
+ "line",
20757
+ "selection"
20758
+ ],
20759
+ operators = ["delete", "yank", "change"]
19969
20760
  }) => {
19970
20761
  const code = typeof rawCode === "string" ? rawCode : String(rawCode ?? "");
19971
- const isOrb = language === "orb";
19972
- const isLolo = language === "lolo";
19973
- const activeStyle = isOrb ? orbStyle : isLolo ? loloStyle : dark__default.default;
20762
+ const activeStyle = resolveHighlightStyle(language);
19974
20763
  const overCapacity = code.length > HIGHLIGHT_CAPACITY_BYTES;
19975
- const plainCodeColor = activeStyle['code[class*="language-"]']?.color ?? "#d4d4d4";
19976
- const languageReady = useLanguageReady(language);
20764
+ const plainCodeColor = plainCodeColorOf(activeStyle);
19977
20765
  const eventBus = useEventBus();
19978
20766
  const { t } = hooks.useTranslate();
19979
20767
  const scrollRef = React96.useRef(null);
@@ -19985,6 +20773,9 @@ var init_CodeBlock = __esm({
19985
20773
  const activeFile = files?.[activeFileIndex];
19986
20774
  const activeCode = activeFile?.code ?? code;
19987
20775
  const activeLanguage = activeFile?.language ?? language;
20776
+ const languageReady = useLanguageReady(activeLanguage);
20777
+ const viewerStyle = resolveHighlightStyle(activeLanguage);
20778
+ const viewerPlainCodeColor = plainCodeColorOf(viewerStyle);
19988
20779
  const diffLines = React96.useMemo(() => {
19989
20780
  if (propDiff) return propDiff;
19990
20781
  if (mode === "diff" && oldValue !== void 0 && newValue !== void 0) {
@@ -20014,28 +20805,28 @@ var init_CodeBlock = __esm({
20014
20805
  ov.scrollLeft = ta.scrollLeft;
20015
20806
  }
20016
20807
  }, []);
20017
- const errorLineProps = React96.useMemo(() => {
20018
- if (!errorLines || errorLines.size === 0) {
20019
- return LINE_PROPS_FN;
20020
- }
20021
- return (lineNumber) => {
20022
- const severity = errorLines.get(lineNumber);
20023
- if (!severity) {
20024
- return { "data-line": String(lineNumber - 1) };
20025
- }
20026
- return {
20027
- "data-line": String(lineNumber - 1),
20028
- style: {
20029
- display: "block",
20030
- backgroundColor: severity === "error" ? "rgba(248, 113, 113, 0.18)" : "rgba(251, 191, 36, 0.18)",
20031
- // amber-400 @ 18%
20032
- borderLeft: `3px solid ${severity === "error" ? "#ef4444" : "#f59e0b"}`,
20033
- paddingLeft: "0.5rem",
20034
- marginLeft: "-0.5rem"
20035
- }
20036
- };
20037
- };
20038
- }, [errorLines]);
20808
+ const handleEditableChange = React96.useCallback((v) => {
20809
+ lastPropCodeRef.current = v;
20810
+ setEditableValue(v);
20811
+ onChange?.(v);
20812
+ }, [onChange]);
20813
+ const { caretMode } = useEditorCapabilities({
20814
+ editorId: editable ? editorId : void 0,
20815
+ textareaRef: editableTextareaRef,
20816
+ events: { onMotion, onOperate, onInsertText, onSetMode },
20817
+ applyChange: handleEditableChange
20818
+ });
20819
+ const [caretIndex, setCaretIndex] = React96.useState(0);
20820
+ const caretRowCol = React96.useMemo(() => {
20821
+ const before = editableValue.slice(0, Math.min(caretIndex, editableValue.length));
20822
+ const lines = before.split("\n");
20823
+ return { row: lines.length - 1, col: lines[lines.length - 1].length };
20824
+ }, [editableValue, caretIndex]);
20825
+ const errorLineProps = React96.useMemo(() => buildLineProps(errorLines), [errorLines]);
20826
+ const viewerLineProps = React96.useMemo(
20827
+ () => buildLineProps(errorLines, "px-4 py-0.5 hover:bg-muted/50"),
20828
+ [errorLines]
20829
+ );
20039
20830
  const isFoldable = foldableProp ?? true;
20040
20831
  const [collapsed, setCollapsed] = React96.useState(() => /* @__PURE__ */ new Set());
20041
20832
  const foldRegions = React96.useMemo(
@@ -20158,6 +20949,110 @@ var init_CodeBlock = __esm({
20158
20949
  ),
20159
20950
  [code, overCapacity, plainCodeColor, language, activeStyle, languageReady]
20160
20951
  );
20952
+ const viewerOverCapacity = activeCode.length > HIGHLIGHT_CAPACITY_BYTES;
20953
+ const viewerHighlightedElement = React96.useMemo(
20954
+ () => viewerOverCapacity ? /* @__PURE__ */ jsxRuntime.jsx(
20955
+ "div",
20956
+ {
20957
+ className: "px-4 py-0.5",
20958
+ style: {
20959
+ margin: 0,
20960
+ whiteSpace: wrap ? "pre-wrap" : "pre",
20961
+ wordBreak: wrap ? "break-all" : "normal",
20962
+ color: viewerPlainCodeColor,
20963
+ fontFamily: MONO_FONT_FAMILY,
20964
+ fontSize: "12px",
20965
+ lineHeight: "1.6"
20966
+ },
20967
+ children: activeCode
20968
+ }
20969
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
20970
+ SyntaxHighlighter__default.default,
20971
+ {
20972
+ PreTag: "div",
20973
+ language: activeLanguage,
20974
+ style: viewerStyle,
20975
+ wrapLines: true,
20976
+ wrapLongLines: wrap,
20977
+ showLineNumbers,
20978
+ lineNumberStyle: VIEWER_LINE_NUMBER_STYLE,
20979
+ lineProps: viewerLineProps,
20980
+ customStyle: {
20981
+ backgroundColor: "transparent",
20982
+ borderRadius: 0,
20983
+ padding: "0.25rem 0",
20984
+ margin: 0,
20985
+ whiteSpace: wrap ? "pre-wrap" : "pre",
20986
+ wordBreak: wrap ? "break-all" : "normal",
20987
+ fontFamily: MONO_FONT_FAMILY,
20988
+ fontSize: "12px",
20989
+ lineHeight: "1.6"
20990
+ },
20991
+ codeTagProps: { style: { fontFamily: MONO_FONT_FAMILY, fontSize: "12px", lineHeight: "1.6" } },
20992
+ children: activeCode
20993
+ }
20994
+ ),
20995
+ [activeCode, viewerOverCapacity, viewerPlainCodeColor, activeLanguage, viewerStyle, wrap, showLineNumbers, viewerLineProps, languageReady]
20996
+ );
20997
+ const diffOverCapacity = React96.useMemo(
20998
+ () => !!diffLines && diffLines.reduce((n, l) => n + l.content.length + 1, 0) > HIGHLIGHT_CAPACITY_BYTES,
20999
+ [diffLines]
21000
+ );
21001
+ const diffRowElements = React96.useMemo(() => {
21002
+ if (!diffLines) return null;
21003
+ return diffLines.map((line, idx) => {
21004
+ const style = DIFF_STYLES[line.type] ?? DIFF_STYLE_FALLBACK;
21005
+ return /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "none", align: "start", className: cn(style.bg, "px-4 py-0.5"), children: [
21006
+ showLineNumbers && /* @__PURE__ */ jsxRuntime.jsx(
21007
+ Typography,
21008
+ {
21009
+ variant: "caption",
21010
+ color: "secondary",
21011
+ className: "w-8 text-right mr-3 select-none tabular-nums flex-shrink-0",
21012
+ children: line.lineNumber ?? ""
21013
+ }
21014
+ ),
21015
+ /* @__PURE__ */ jsxRuntime.jsxs(
21016
+ Typography,
21017
+ {
21018
+ variant: "caption",
21019
+ className: cn("font-mono flex-1 min-w-0", style.text, wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
21020
+ children: [
21021
+ /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "span", className: "select-none opacity-50 mr-2", children: style.prefix }),
21022
+ diffOverCapacity ? line.content || " " : /* @__PURE__ */ jsxRuntime.jsx(
21023
+ SyntaxHighlighter__default.default,
21024
+ {
21025
+ PreTag: "span",
21026
+ CodeTag: "span",
21027
+ language: activeLanguage,
21028
+ style: viewerStyle,
21029
+ customStyle: {
21030
+ display: "inline",
21031
+ background: "transparent",
21032
+ padding: 0,
21033
+ margin: 0,
21034
+ whiteSpace: wrap ? "pre-wrap" : "pre",
21035
+ wordBreak: wrap ? "break-all" : "normal",
21036
+ fontFamily: "inherit",
21037
+ fontSize: "inherit",
21038
+ lineHeight: "inherit"
21039
+ },
21040
+ codeTagProps: {
21041
+ style: {
21042
+ whiteSpace: wrap ? "pre-wrap" : "pre",
21043
+ fontFamily: "inherit",
21044
+ fontSize: "inherit"
21045
+ }
21046
+ },
21047
+ children: line.content || " "
21048
+ }
21049
+ )
21050
+ ]
21051
+ }
21052
+ )
21053
+ ] }, idx);
21054
+ });
21055
+ }, [diffLines, showLineNumbers, wrap, diffOverCapacity, activeLanguage, viewerStyle, languageReady]);
20161
21056
  React96.useLayoutEffect(() => {
20162
21057
  const container = codeRef.current;
20163
21058
  if (!container) return;
@@ -20301,7 +21196,6 @@ var init_CodeBlock = __esm({
20301
21196
  label: file.label,
20302
21197
  content: null
20303
21198
  }));
20304
- const lines = activeCode.split("\n");
20305
21199
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className: cn("overflow-hidden", className), children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: [
20306
21200
  tabItems && tabItems.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(Box, { className: "border-b border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
20307
21201
  Tabs,
@@ -20364,49 +21258,7 @@ var init_CodeBlock = __esm({
20364
21258
  ]
20365
21259
  }
20366
21260
  ),
20367
- /* @__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) => {
20368
- const style = DIFF_STYLES[line.type] ?? DIFF_STYLE_FALLBACK;
20369
- return /* @__PURE__ */ jsxRuntime.jsxs(HStack, { gap: "none", align: "start", className: cn(style.bg, "px-4 py-0.5"), children: [
20370
- showLineNumbers && /* @__PURE__ */ jsxRuntime.jsx(
20371
- Typography,
20372
- {
20373
- variant: "caption",
20374
- color: "secondary",
20375
- className: "w-8 text-right mr-3 select-none tabular-nums flex-shrink-0",
20376
- children: line.lineNumber ?? ""
20377
- }
20378
- ),
20379
- /* @__PURE__ */ jsxRuntime.jsxs(
20380
- Typography,
20381
- {
20382
- variant: "caption",
20383
- className: cn("font-mono flex-1 min-w-0", style.text, wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
20384
- children: [
20385
- /* @__PURE__ */ jsxRuntime.jsx(Box, { as: "span", className: "select-none opacity-50 mr-2", children: style.prefix }),
20386
- line.content
20387
- ]
20388
- }
20389
- )
20390
- ] }, idx);
20391
- }) }) : /* @__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: [
20392
- showLineNumbers && /* @__PURE__ */ jsxRuntime.jsx(
20393
- Typography,
20394
- {
20395
- variant: "caption",
20396
- color: "secondary",
20397
- className: "w-8 text-right mr-4 select-none tabular-nums flex-shrink-0",
20398
- children: idx + 1
20399
- }
20400
- ),
20401
- /* @__PURE__ */ jsxRuntime.jsx(
20402
- Typography,
20403
- {
20404
- variant: "caption",
20405
- className: cn("font-mono flex-1 min-w-0", wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
20406
- children: line || " "
20407
- }
20408
- )
20409
- ] }, idx)) }) })
21261
+ /* @__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 }) })
20410
21262
  ] }) });
20411
21263
  }
20412
21264
  const hasHeader = showLanguageBadge || effectiveCopy;
@@ -20493,13 +21345,11 @@ var init_CodeBlock = __esm({
20493
21345
  {
20494
21346
  ref: editableTextareaRef,
20495
21347
  defaultValue: code,
20496
- onChange: (e) => {
20497
- const v = e.target.value;
20498
- lastPropCodeRef.current = v;
20499
- setEditableValue(v);
20500
- onChange?.(v);
20501
- },
21348
+ onChange: (e) => handleEditableChange(e.target.value),
20502
21349
  onScroll: handleEditableScroll,
21350
+ onSelect: (e) => setCaretIndex(e.currentTarget.selectionStart),
21351
+ onFocus: editorId ? () => eventBus.emit(`UI:${onEditorFocus}`, { editorId }) : void 0,
21352
+ onBlur: editorId ? () => eventBus.emit(`UI:${onEditorBlur}`, { editorId }) : void 0,
20503
21353
  spellCheck: false,
20504
21354
  style: {
20505
21355
  position: "absolute",
@@ -20514,7 +21364,7 @@ var init_CodeBlock = __esm({
20514
21364
  resize: "none",
20515
21365
  backgroundColor: "transparent",
20516
21366
  color: "transparent",
20517
- caretColor: "#e6e6e6",
21367
+ caretColor: caretMode === "block" ? "transparent" : "#e6e6e6",
20518
21368
  WebkitTextFillColor: "transparent",
20519
21369
  fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace',
20520
21370
  fontSize: "13px",
@@ -20525,6 +21375,22 @@ var init_CodeBlock = __esm({
20525
21375
  }
20526
21376
  },
20527
21377
  editableTextareaKey
21378
+ ),
21379
+ caretMode !== "bar" && /* @__PURE__ */ jsxRuntime.jsx(
21380
+ "span",
21381
+ {
21382
+ "aria-hidden": true,
21383
+ style: {
21384
+ position: "absolute",
21385
+ top: `calc(1rem + ${caretRowCol.row * 19.5}px)`,
21386
+ left: `calc(1rem + ${caretRowCol.col}ch)`,
21387
+ width: "1ch",
21388
+ height: caretMode === "block" ? "19.5px" : "2px",
21389
+ backgroundColor: caretMode === "block" ? "rgba(230, 230, 230, 0.5)" : void 0,
21390
+ borderBottom: caretMode === "underline" ? "2px solid #e6e6e6" : void 0,
21391
+ pointerEvents: "none"
21392
+ }
21393
+ }
20528
21394
  )
20529
21395
  ]
20530
21396
  }
@@ -20552,55 +21418,175 @@ var init_CodeBlock = __esm({
20552
21418
  )
20553
21419
  ] });
20554
21420
  },
20555
- (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
21421
+ (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
20556
21422
  );
20557
21423
  CodeBlock.displayName = "CodeBlock";
20558
21424
  }
20559
21425
  });
21426
+
21427
+ // components/core/molecules/markdown/mermaidSource.ts
21428
+ function isQuoted(label) {
21429
+ const trimmed = label.trim();
21430
+ return trimmed.length === 0 || trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length > 1;
21431
+ }
21432
+ function quote(label) {
21433
+ return `"${label.replace(/"/g, "#quot;")}"`;
21434
+ }
21435
+ function isIdentifierChar(ch) {
21436
+ return /[A-Za-z0-9_\-.]/.test(ch);
21437
+ }
21438
+ function declaredType(code) {
21439
+ for (const line of code.split("\n")) {
21440
+ const trimmed = line.trim();
21441
+ if (trimmed.length === 0 || trimmed.startsWith("%%")) continue;
21442
+ return trimmed;
21443
+ }
21444
+ return "";
21445
+ }
21446
+ function isFlowchart(code) {
21447
+ return FLOWCHART_DIRECTIVE.test(declaredType(code));
21448
+ }
21449
+ function quoteNodeLabels(code) {
21450
+ let out = "";
21451
+ let i = 0;
21452
+ while (i < code.length) {
21453
+ const shape = NODE_SHAPES.find(([open2]) => code.startsWith(open2, i));
21454
+ const precededByIdentifier = i > 0 && isIdentifierChar(code[i - 1] ?? "");
21455
+ if (shape === void 0 || !precededByIdentifier) {
21456
+ out += code[i];
21457
+ i += 1;
21458
+ continue;
21459
+ }
21460
+ const [open, close] = shape;
21461
+ const contentStart = i + open.length;
21462
+ const closeAt = code.indexOf(close, contentStart);
21463
+ const newlineAt = code.indexOf("\n", contentStart);
21464
+ if (closeAt === -1 || newlineAt !== -1 && newlineAt < closeAt) {
21465
+ out += code[i];
21466
+ i += 1;
21467
+ continue;
21468
+ }
21469
+ const label = code.slice(contentStart, closeAt);
21470
+ out += open + (isQuoted(label) ? label : quote(label)) + close;
21471
+ i = closeAt + close.length;
21472
+ }
21473
+ return out;
21474
+ }
21475
+ function quoteEdgeLabels(code) {
21476
+ return code.split("\n").map((line) => {
21477
+ let out = "";
21478
+ let rest = line;
21479
+ for (; ; ) {
21480
+ const open = rest.indexOf("|");
21481
+ if (open === -1) break;
21482
+ const close = rest.indexOf("|", open + 1);
21483
+ if (close === -1) break;
21484
+ const label = rest.slice(open + 1, close);
21485
+ out += rest.slice(0, open + 1) + (isQuoted(label) ? label : quote(label)) + "|";
21486
+ rest = rest.slice(close + 1);
21487
+ }
21488
+ return out + rest;
21489
+ }).join("\n");
21490
+ }
21491
+ function quoteSubgraphTitles(code) {
21492
+ return code.split("\n").map((line) => {
21493
+ const match = /^(\s*subgraph\s+)(.+?)(\s*)$/.exec(line);
21494
+ if (match === null) return line;
21495
+ const [, prefix, title, trailing] = match;
21496
+ if (title === void 0 || prefix === void 0) return line;
21497
+ if (isQuoted(title) || title.includes("[")) return line;
21498
+ return prefix + quote(title) + (trailing ?? "");
21499
+ }).join("\n");
21500
+ }
21501
+ function mermaidRepairCandidates(code) {
21502
+ if (!isFlowchart(code)) return [];
21503
+ const nodes = quoteNodeLabels(code);
21504
+ const nodesAndEdges = quoteEdgeLabels(nodes);
21505
+ const all = quoteSubgraphTitles(nodesAndEdges);
21506
+ const ordered2 = [nodes, nodesAndEdges, all];
21507
+ const seen = /* @__PURE__ */ new Set([code]);
21508
+ const candidates = [];
21509
+ for (const candidate of ordered2) {
21510
+ if (seen.has(candidate)) continue;
21511
+ seen.add(candidate);
21512
+ candidates.push(candidate);
21513
+ }
21514
+ return candidates;
21515
+ }
21516
+ var NODE_SHAPES, FLOWCHART_DIRECTIVE;
21517
+ var init_mermaidSource = __esm({
21518
+ "components/core/molecules/markdown/mermaidSource.ts"() {
21519
+ NODE_SHAPES = [
21520
+ ["[[", "]]"],
21521
+ ["[(", ")]"],
21522
+ ["([", "])"],
21523
+ ["((", "))"],
21524
+ ["{{", "}}"],
21525
+ ["[", "]"],
21526
+ ["(", ")"],
21527
+ ["{", "}"]
21528
+ ];
21529
+ FLOWCHART_DIRECTIVE = /^(?:graph|flowchart)\b/;
21530
+ }
21531
+ });
20560
21532
  function loadMermaid() {
20561
21533
  mermaidModule ?? (mermaidModule = import('mermaid').then((m) => m.default));
20562
21534
  return mermaidModule;
20563
21535
  }
20564
- var mermaidModule, MermaidDiagram;
21536
+ var log5, mermaidModule, MermaidDiagram;
20565
21537
  var init_MermaidDiagram = __esm({
20566
21538
  "components/core/molecules/markdown/MermaidDiagram.tsx"() {
20567
21539
  init_Box();
20568
21540
  init_Typography();
20569
21541
  init_CodeBlock();
21542
+ init_mermaidSource();
20570
21543
  init_cn();
21544
+ log5 = logger.createLogger("almadar:ui:mermaid-diagram");
20571
21545
  mermaidModule = null;
20572
21546
  MermaidDiagram = React96__namespace.default.memo(
20573
21547
  ({ code, className }) => {
20574
21548
  const { resolvedMode } = context.useTheme();
21549
+ const { t } = hooks.useTranslate();
20575
21550
  const containerRef = React96.useRef(null);
20576
- const [error, setError] = React96.useState(null);
21551
+ const [unrenderable, setUnrenderable] = React96.useState(false);
20577
21552
  const reactId = React96.useId();
20578
21553
  React96.useEffect(() => {
20579
21554
  let active = true;
20580
21555
  void (async () => {
20581
- try {
20582
- const mermaid = await loadMermaid();
20583
- mermaid.initialize({
20584
- startOnLoad: false,
20585
- securityLevel: "strict",
20586
- theme: resolvedMode === "dark" ? "dark" : "default"
20587
- });
20588
- const domId = `mermaid-${reactId.replace(/[^a-zA-Z0-9]/g, "")}`;
20589
- const { svg } = await mermaid.render(domId, code);
20590
- if (!active || !containerRef.current) return;
20591
- containerRef.current.innerHTML = svg;
20592
- setError(null);
20593
- } catch (err) {
20594
- if (active) setError(err instanceof Error ? err.message : String(err));
21556
+ const mermaid = await loadMermaid();
21557
+ mermaid.initialize({
21558
+ startOnLoad: false,
21559
+ securityLevel: "strict",
21560
+ theme: resolvedMode === "dark" ? "dark" : "default"
21561
+ });
21562
+ const domId = `mermaid-${reactId.replace(/[^a-zA-Z0-9]/g, "")}`;
21563
+ let firstError = null;
21564
+ for (const [index, source] of [code, ...mermaidRepairCandidates(code)].entries()) {
21565
+ try {
21566
+ const { svg } = await mermaid.render(domId, source);
21567
+ if (!active) return;
21568
+ const container = containerRef.current;
21569
+ if (container === null) return;
21570
+ container.innerHTML = svg;
21571
+ container.dataset.mermaidRepaired = String(index > 0);
21572
+ setUnrenderable(false);
21573
+ if (index > 0) log5.debug("mermaid:repaired", { candidate: index });
21574
+ return;
21575
+ } catch (err) {
21576
+ firstError ?? (firstError = err instanceof Error ? err : new Error(String(err)));
21577
+ }
20595
21578
  }
21579
+ if (!active) return;
21580
+ log5.warn("mermaid:unrenderable", { error: firstError?.message ?? "", code });
21581
+ setUnrenderable(true);
20596
21582
  })();
20597
21583
  return () => {
20598
21584
  active = false;
20599
21585
  };
20600
21586
  }, [code, resolvedMode, reactId]);
20601
21587
  return /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: cn("not-prose my-4", className), children: [
20602
- error !== null && /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "space-y-2 mb-2", children: [
20603
- /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-error whitespace-pre-wrap", children: error }),
21588
+ unrenderable && /* @__PURE__ */ jsxRuntime.jsxs(Box, { className: "space-y-2 mb-2", "data-testid": "mermaid-unrenderable", children: [
21589
+ /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "caption", className: "text-muted-foreground", children: t("mermaid.unrenderable") }),
20604
21590
  /* @__PURE__ */ jsxRuntime.jsx(CodeBlock, { code, language: "mermaid" })
20605
21591
  ] }),
20606
21592
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -20609,7 +21595,7 @@ var init_MermaidDiagram = __esm({
20609
21595
  ref: containerRef,
20610
21596
  "data-testid": "mermaid-diagram",
20611
21597
  className: "overflow-x-auto",
20612
- style: error !== null ? { display: "none" } : void 0
21598
+ style: unrenderable ? { display: "none" } : void 0
20613
21599
  }
20614
21600
  )
20615
21601
  ] });
@@ -24014,14 +25000,14 @@ function useSafeEventBus2() {
24014
25000
  } };
24015
25001
  }
24016
25002
  }
24017
- var log5, lookStyles4, ButtonGroup;
25003
+ var log6, lookStyles4, ButtonGroup;
24018
25004
  var init_ButtonGroup = __esm({
24019
25005
  "components/core/molecules/ButtonGroup.tsx"() {
24020
25006
  "use client";
24021
25007
  init_cn();
24022
25008
  init_atoms();
24023
25009
  init_useEventBus();
24024
- log5 = logger.createLogger("almadar:ui:button-group");
25010
+ log6 = logger.createLogger("almadar:ui:button-group");
24025
25011
  lookStyles4 = {
24026
25012
  "right-aligned-buttons": "",
24027
25013
  "floating-bar": "fixed bottom-section left-1/2 -translate-x-1/2 shadow-elevation-toast bg-card p-card-sm rounded-container",
@@ -24102,7 +25088,7 @@ var init_ButtonGroup = __esm({
24102
25088
  {
24103
25089
  variant: "ghost",
24104
25090
  onClick: () => {
24105
- log5.debug("Filter clicked", { field: filter.field });
25091
+ log6.debug("Filter clicked", { field: filter.field });
24106
25092
  },
24107
25093
  children: filter.label
24108
25094
  },
@@ -26886,6 +27872,12 @@ function commandMatches(command, query) {
26886
27872
  if (matchesQuery(query, command.label)) return true;
26887
27873
  return (command.keywords ?? []).some((keyword) => matchesQuery(query, keyword));
26888
27874
  }
27875
+ function dispatchCommandPaletteCommand(command, deps) {
27876
+ if (command.disabled) return;
27877
+ if (command.event) deps.emit(`UI:${command.event}`, { commandId: command.id });
27878
+ if (command.action) deps.emit(`UI:${command.action}`, command.actionPayload ?? {});
27879
+ deps.onSelect?.(command);
27880
+ }
26889
27881
  var UNGROUPED, CommandPalette;
26890
27882
  var init_CommandPalette = __esm({
26891
27883
  "components/core/molecules/CommandPalette.tsx"() {
@@ -26940,9 +27932,7 @@ var init_CommandPalette = __esm({
26940
27932
  const handleSelect = React96.useCallback(
26941
27933
  (command) => {
26942
27934
  if (command.disabled) return;
26943
- if (command.event) eventBus.emit(`UI:${command.event}`, { commandId: command.id });
26944
- if (command.action) eventBus.emit(`UI:${command.action}`, command.actionPayload ?? {});
26945
- onSelect?.(command);
27935
+ dispatchCommandPaletteCommand(command, { emit: eventBus.emit, onSelect });
26946
27936
  handleClose();
26947
27937
  },
26948
27938
  [eventBus, onSelect, handleClose]
@@ -31921,9 +32911,9 @@ function debug(...args) {
31921
32911
  const [first, ...rest] = args;
31922
32912
  const message = typeof first === "string" ? first : "<debug>";
31923
32913
  if (rest.length === 0 && typeof first === "string") {
31924
- log6.debug(message);
32914
+ log7.debug(message);
31925
32915
  } else {
31926
- log6.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
32916
+ log7.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
31927
32917
  }
31928
32918
  }
31929
32919
  function debugGroup(label) {
@@ -31951,11 +32941,11 @@ function toLogMetaValue(v) {
31951
32941
  }
31952
32942
  return String(v);
31953
32943
  }
31954
- var NAMESPACE, log6;
32944
+ var NAMESPACE, log7;
31955
32945
  var init_debug = __esm({
31956
32946
  "lib/debug.ts"() {
31957
32947
  NAMESPACE = "almadar:ui:debug";
31958
- log6 = logger.createLogger(NAMESPACE);
32948
+ log7 = logger.createLogger(NAMESPACE);
31959
32949
  logger.createLogger("almadar:ui:debug:input");
31960
32950
  logger.createLogger("almadar:ui:debug:collision");
31961
32951
  logger.createLogger("almadar:ui:debug:physics");
@@ -33608,6 +34598,7 @@ var init_MathCanvas = __esm({
33608
34598
  init_perf();
33609
34599
  init_atoms();
33610
34600
  init_Stack();
34601
+ init_gameFonts();
33611
34602
  init_LearningCanvas();
33612
34603
  MathCanvas = ({
33613
34604
  className,
@@ -33627,6 +34618,7 @@ var init_MathCanvas = __esm({
33627
34618
  showTickLabels = false,
33628
34619
  tickLabelFontSize = 10,
33629
34620
  labelFontSize = 12,
34621
+ fontFamily: fontFamilyProp,
33630
34622
  showCurveLabels = false,
33631
34623
  curves = [],
33632
34624
  points = [],
@@ -33649,6 +34641,7 @@ var init_MathCanvas = __esm({
33649
34641
  error
33650
34642
  }) => {
33651
34643
  const eventBus = useEventBus();
34644
+ const fontFamily = resolveGameFontFamily(fontFamilyProp);
33652
34645
  const keyMapKey = keyMap ? JSON.stringify(keyMap) : null;
33653
34646
  const keyUpMapKey = keyUpMap ? JSON.stringify(keyUpMap) : null;
33654
34647
  const stableKeyMap = React96.useMemo(() => keyMap, [keyMapKey]);
@@ -33698,18 +34691,18 @@ var init_MathCanvas = __esm({
33698
34691
  let kx = 0;
33699
34692
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
33700
34693
  if (kx % labelEveryX === 0 && x !== 0) {
33701
- out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: tickLabelFontSize, align: "center" });
34694
+ out.push({ type: "text", fontFamily, x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: tickLabelFontSize, align: "center" });
33702
34695
  }
33703
34696
  }
33704
34697
  const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
33705
34698
  let ky = 0;
33706
34699
  for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
33707
34700
  if (ky % labelEveryY === 0 && y !== 0) {
33708
- out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
34701
+ out.push({ type: "text", fontFamily, x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
33709
34702
  }
33710
34703
  }
33711
34704
  if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
33712
- out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
34705
+ out.push({ type: "text", fontFamily, x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
33713
34706
  }
33714
34707
  }
33715
34708
  for (const region of regions) {
@@ -33736,6 +34729,7 @@ var init_MathCanvas = __esm({
33736
34729
  const mid = Math.floor(region.samples.length / 2);
33737
34730
  out.push({
33738
34731
  type: "text",
34732
+ fontFamily,
33739
34733
  x: mapX((first.x + last.x) / 2),
33740
34734
  y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
33741
34735
  text: region.label,
@@ -33772,14 +34766,14 @@ var init_MathCanvas = __esm({
33772
34766
  const px = mapX(guide.at);
33773
34767
  out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
33774
34768
  if (guide.label) {
33775
- out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color, fontSize: 11 });
34769
+ out.push({ type: "text", fontFamily, x: px + 4, y: margin + 10, text: guide.label, color, fontSize: 11 });
33776
34770
  }
33777
34771
  } else {
33778
34772
  if (guide.at < yMin || guide.at > yMax) continue;
33779
34773
  const py = mapY(guide.at);
33780
34774
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
33781
34775
  if (guide.label) {
33782
- out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color, fontSize: labelFontSize, align: "right" });
34776
+ out.push({ type: "text", fontFamily, x: width - margin - 4, y: py - 8, text: guide.label, color, fontSize: labelFontSize, align: "right" });
33783
34777
  }
33784
34778
  }
33785
34779
  }
@@ -33818,6 +34812,7 @@ var init_MathCanvas = __esm({
33818
34812
  if (showCurveLabels && curve.label && lastInRange) {
33819
34813
  out.push({
33820
34814
  type: "text",
34815
+ fontFamily,
33821
34816
  x: mapX(lastInRange.x) + 6,
33822
34817
  y: mapY(lastInRange.y) - 6,
33823
34818
  text: curve.label,
@@ -33855,6 +34850,7 @@ var init_MathCanvas = __esm({
33855
34850
  if (hop.label) {
33856
34851
  out.push({
33857
34852
  type: "text",
34853
+ fontFamily,
33858
34854
  x: (x1 + x2) / 2,
33859
34855
  y: xAxisY - peak - 8,
33860
34856
  text: hop.label,
@@ -33882,6 +34878,7 @@ var init_MathCanvas = __esm({
33882
34878
  const rad = mid * Math.PI / 180;
33883
34879
  out.push({
33884
34880
  type: "text",
34881
+ fontFamily,
33885
34882
  x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
33886
34883
  y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
33887
34884
  text: angle.label,
@@ -33903,7 +34900,7 @@ var init_MathCanvas = __esm({
33903
34900
  fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
33904
34901
  });
33905
34902
  if (p.label) {
33906
- out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: p.color ?? "#111827", fontSize: labelFontSize });
34903
+ out.push({ type: "text", fontFamily, x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: p.color ?? "#111827", fontSize: labelFontSize });
33907
34904
  }
33908
34905
  }
33909
34906
  for (const v of vectors) {
@@ -33914,7 +34911,7 @@ var init_MathCanvas = __esm({
33914
34911
  const y2 = mapY(v.y + v.vy);
33915
34912
  out.push({ type: "arrow", x1, y1, x2, y2, color: v.color ?? "#7c3aed", lineWidth: 2 });
33916
34913
  if (v.label) {
33917
- out.push({ type: "text", x: x2 + 6, y: y2 - 6, text: v.label, color: v.color ?? "#7c3aed", fontSize: labelFontSize });
34914
+ out.push({ type: "text", fontFamily, x: x2 + 6, y: y2 - 6, text: v.label, color: v.color ?? "#7c3aed", fontSize: labelFontSize });
33918
34915
  }
33919
34916
  }
33920
34917
  out.push(...shapes);
@@ -33935,6 +34932,7 @@ var init_MathCanvas = __esm({
33935
34932
  showTickLabels,
33936
34933
  tickLabelFontSize,
33937
34934
  labelFontSize,
34935
+ fontFamily,
33938
34936
  showCurveLabels,
33939
34937
  curves,
33940
34938
  points,
@@ -33990,6 +34988,7 @@ var init_MathCanvas = __esm({
33990
34988
  width,
33991
34989
  height,
33992
34990
  backgroundColor,
34991
+ fontFamily,
33993
34992
  shapes: derivedShapes,
33994
34993
  drawables,
33995
34994
  projector,
@@ -35222,13 +36221,13 @@ var init_MapView = __esm({
35222
36221
  shadowSize: [41, 41]
35223
36222
  });
35224
36223
  L.Marker.prototype.options.icon = defaultIcon;
35225
- const { useEffect: useEffect68, useRef: useRef67, useCallback: useCallback99, useState: useState107 } = React96__namespace.default;
36224
+ const { useEffect: useEffect70, useRef: useRef70, useCallback: useCallback100, useState: useState109 } = React96__namespace.default;
35226
36225
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
35227
36226
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
35228
36227
  function MapUpdater({ centerLat, centerLng, zoom }) {
35229
36228
  const map = useMap();
35230
- const prevRef = useRef67({ centerLat, centerLng, zoom });
35231
- useEffect68(() => {
36229
+ const prevRef = useRef70({ centerLat, centerLng, zoom });
36230
+ useEffect70(() => {
35232
36231
  const prev = prevRef.current;
35233
36232
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
35234
36233
  map.setView([centerLat, centerLng], zoom);
@@ -35239,7 +36238,7 @@ var init_MapView = __esm({
35239
36238
  }
35240
36239
  function MapClickHandler({ onMapClick }) {
35241
36240
  const map = useMap();
35242
- useEffect68(() => {
36241
+ useEffect70(() => {
35243
36242
  if (!onMapClick) return;
35244
36243
  const handler = (e) => {
35245
36244
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -35267,8 +36266,8 @@ var init_MapView = __esm({
35267
36266
  showAttribution = true
35268
36267
  }) {
35269
36268
  const eventBus = useEventBus2();
35270
- const [clickedPosition, setClickedPosition] = useState107(null);
35271
- const handleMapClick = useCallback99((lat, lng) => {
36269
+ const [clickedPosition, setClickedPosition] = useState109(null);
36270
+ const handleMapClick = useCallback100((lat, lng) => {
35272
36271
  if (showClickedPin) {
35273
36272
  setClickedPosition({ lat, lng });
35274
36273
  }
@@ -35277,7 +36276,7 @@ var init_MapView = __esm({
35277
36276
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
35278
36277
  }
35279
36278
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
35280
- const handleMarkerClick = useCallback99((marker) => {
36279
+ const handleMarkerClick = useCallback100((marker) => {
35281
36280
  onMarkerClick?.(marker);
35282
36281
  if (markerClickEvent) {
35283
36282
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -49586,7 +50585,7 @@ function getAllEvents(traits2) {
49586
50585
  function EventDispatcherTab({ traits: traits2, schema }) {
49587
50586
  const eventBus = useEventBus();
49588
50587
  const { t } = hooks.useTranslate();
49589
- const [log11, setLog] = React96__namespace.useState([]);
50588
+ const [log12, setLog] = React96__namespace.useState([]);
49590
50589
  const prevStatesRef = React96__namespace.useRef(/* @__PURE__ */ new Map());
49591
50590
  React96__namespace.useEffect(() => {
49592
50591
  for (const trait of traits2) {
@@ -49650,9 +50649,9 @@ function EventDispatcherTab({ traits: traits2, schema }) {
49650
50649
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", weight: "medium", className: "text-muted-foreground mb-1", children: t("debug.otherEvents") }),
49651
50650
  /* @__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)) })
49652
50651
  ] }),
49653
- log11.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
50652
+ log12.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
49654
50653
  /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", weight: "medium", className: "text-muted-foreground mb-1", children: t("debug.recentTransitions") }),
49655
- /* @__PURE__ */ jsxRuntime.jsx(Stack, { gap: "xs", children: log11.map((entry, i) => /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
50654
+ /* @__PURE__ */ jsxRuntime.jsx(Stack, { gap: "xs", children: log12.map((entry, i) => /* @__PURE__ */ jsxRuntime.jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
49656
50655
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-primary", children: entry.traitName }),
49657
50656
  " ",
49658
50657
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: entry.from }),
@@ -51843,6 +52842,7 @@ var init_component_registry_generated = __esm({
51843
52842
  init_FormSection();
51844
52843
  init_FormSectionHeader();
51845
52844
  init_FxOverlay();
52845
+ init_GameAudioCue();
51846
52846
  init_GameAudioToggle();
51847
52847
  init_GameHud();
51848
52848
  init_GameIcon();
@@ -52120,6 +53120,7 @@ var init_component_registry_generated = __esm({
52120
53120
  "FormLayout": FormLayout,
52121
53121
  "FormSectionHeader": FormSectionHeader,
52122
53122
  "FxOverlay": FxOverlay,
53123
+ "GameAudioCue": GameAudioCue,
52123
53124
  "GameAudioToggle": GameAudioToggle,
52124
53125
  "GameHud": GameHud,
52125
53126
  "GameIcon": GameIcon,
@@ -56916,7 +57917,7 @@ function convertFnFormLambdasInProps(props) {
56916
57917
 
56917
57918
  // hooks/index.ts
56918
57919
  init_useEventBus();
56919
- var log7 = logger.createLogger("almadar:ui:shared-entity-store");
57920
+ var log8 = logger.createLogger("almadar:ui:shared-entity-store");
56920
57921
  var EMPTY_ENTITY_STATE = {};
56921
57922
  function createSharedEntityStore() {
56922
57923
  const states = /* @__PURE__ */ new Map();
@@ -56946,7 +57947,7 @@ function createSharedEntityStore() {
56946
57947
  try {
56947
57948
  callback();
56948
57949
  } catch (error) {
56949
- log7.error("Shared entity subscriber error", {
57950
+ log8.error("Shared entity subscriber error", {
56950
57951
  entityId,
56951
57952
  error: error instanceof Error ? error : String(error)
56952
57953
  });
@@ -56977,7 +57978,7 @@ function runTickFrame(entityId, orderedWriters, store) {
56977
57978
  store.commit(entityId, scratch);
56978
57979
  return scratch;
56979
57980
  }
56980
- var log8 = logger.createLogger("almadar:ui:effects:client-handlers");
57981
+ var log9 = logger.createLogger("almadar:ui:effects:client-handlers");
56981
57982
  function createClientEffectHandlers(options) {
56982
57983
  const { eventBus, slotSetter, navigate, navigateBack, notify, callService, liveEntity } = options;
56983
57984
  return {
@@ -56986,12 +57987,12 @@ function createClientEffectHandlers(options) {
56986
57987
  eventBus.emit(prefixedEvent, payload, source);
56987
57988
  },
56988
57989
  persist: async () => {
56989
- log8.warn("persist is server-side only, ignored on client");
57990
+ log9.warn("persist is server-side only, ignored on client");
56990
57991
  },
56991
57992
  // @almadar/runtime EffectHandlers.set types value:unknown — should be FieldValue (upstream fix queued)
56992
57993
  set: ((_entityId, field, value) => {
56993
57994
  if (!liveEntity) {
56994
- log8.warn("set is server-side only, ignored on client (no live entity)");
57995
+ log9.warn("set is server-side only, ignored on client (no live entity)");
56995
57996
  return;
56996
57997
  }
56997
57998
  liveEntity[field] = value;
@@ -57027,13 +58028,13 @@ function createClientEffectHandlers(options) {
57027
58028
  window.location.href = path;
57028
58029
  return;
57029
58030
  }
57030
- log8.warn("No navigate handler, ignoring", { path });
58031
+ log9.warn("No navigate handler, ignoring", { path });
57031
58032
  }),
57032
58033
  navigateBack: navigateBack ?? (() => {
57033
- log8.warn("No navigate-back handler, ignoring");
58034
+ log9.warn("No navigate-back handler, ignoring");
57034
58035
  }),
57035
58036
  notify: notify ?? ((msg, type) => {
57036
- log8.debug("notify", { type, message: msg });
58037
+ log9.debug("notify", { type, message: msg });
57037
58038
  })
57038
58039
  };
57039
58040
  }
@@ -57468,7 +58469,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57468
58469
  };
57469
58470
  }, [traitBindings]);
57470
58471
  const executeTransitionEffects = React96.useCallback(async (params) => {
57471
- const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log11 } = params;
58472
+ const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log12 } = params;
57472
58473
  const traitName = binding.trait.name;
57473
58474
  const linkedEntity = binding.linkedEntity || "";
57474
58475
  const entityId = payload?.entityId;
@@ -57597,7 +58598,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57597
58598
  if (sharedKey !== void 0) {
57598
58599
  sharedWrites.push({ field, value });
57599
58600
  }
57600
- log11.debug("set:write", {
58601
+ log12.debug("set:write", {
57601
58602
  traitName,
57602
58603
  field,
57603
58604
  value: JSON.stringify(value),
@@ -57665,7 +58666,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57665
58666
  core.mergeEntityFrame(sharedEntityStore.getSnapshot(sharedKey), sharedWrites)
57666
58667
  );
57667
58668
  }
57668
- log11.debug("effects:executed", () => ({
58669
+ log12.debug("effects:executed", () => ({
57669
58670
  traitName,
57670
58671
  transition: `${previousState}->${newState}`,
57671
58672
  event: flushEvent,
@@ -57675,7 +58676,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57675
58676
  slotsTouched: Array.from(pendingSlots.keys()).join(",")
57676
58677
  }));
57677
58678
  for (const [slot, patterns] of pendingSlots) {
57678
- log11.debug("flush:slot", {
58679
+ log12.debug("flush:slot", {
57679
58680
  traitName,
57680
58681
  slot,
57681
58682
  patternCount: patterns.length,
@@ -57693,7 +58694,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57693
58694
  publishBindingSnapshot(traitName, liveEntity);
57694
58695
  }
57695
58696
  } catch (error) {
57696
- log11.error("effects:error", {
58697
+ log12.error("effects:error", {
57697
58698
  traitName,
57698
58699
  transition: `${previousState}->${newState}`,
57699
58700
  event: flushEvent,
@@ -58772,11 +59773,20 @@ function BrowserPlayground({
58772
59773
  mode = "mock",
58773
59774
  initialPagePath,
58774
59775
  height,
58775
- className
59776
+ className,
59777
+ paused
58776
59778
  }) {
58777
59779
  const [runtime] = React96.useState(
58778
59780
  () => new OrbitalServerRuntime.OrbitalServerRuntime({ mode, debug: false })
58779
59781
  );
59782
+ React96.useEffect(() => {
59783
+ if (paused === void 0) return;
59784
+ if (paused) {
59785
+ runtime.pauseTicks();
59786
+ } else {
59787
+ runtime.resumeTicks();
59788
+ }
59789
+ }, [runtime, paused]);
58780
59790
  const registrationReady = React96.useMemo(() => {
58781
59791
  const orbitalNames = schema.orbitals.map((o) => o.name);
58782
59792
  playgroundLog.debug("register:start", { schema: schema.name, orbitalNames });
@@ -58837,7 +59847,7 @@ init_useEventBus();
58837
59847
  // components/avl/hooks/useCanvasDnd.tsx
58838
59848
  init_useEventBus();
58839
59849
  init_useAlmadarDndCollision();
58840
- var log9 = logger.createLogger("almadar:ui:canvas-dnd");
59850
+ var log10 = logger.createLogger("almadar:ui:canvas-dnd");
58841
59851
  function useCanvasDraggable({
58842
59852
  id,
58843
59853
  payload,
@@ -58875,7 +59885,7 @@ function defaultEmit(eventBus, drop) {
58875
59885
  if (payload.kind === "pattern") {
58876
59886
  const patternType = payload.data["type"];
58877
59887
  if (typeof patternType !== "string") {
58878
- log9.warn("default-emit:pattern:missing-type");
59888
+ log10.warn("default-emit:pattern:missing-type");
58879
59889
  return;
58880
59890
  }
58881
59891
  const out = { patternType, containerNode: target.containerNode };
@@ -58884,26 +59894,26 @@ function defaultEmit(eventBus, drop) {
58884
59894
  out.index = resolved.index;
58885
59895
  }
58886
59896
  eventBus.emit("UI:PATTERN_DROP", out);
58887
- log9.info("default-emit:pattern", { patternType, level: target.level });
59897
+ log10.info("default-emit:pattern", { patternType, level: target.level });
58888
59898
  return;
58889
59899
  }
58890
59900
  if (payload.kind === "behavior") {
58891
59901
  const behaviorName = payload.data["name"];
58892
59902
  if (typeof behaviorName !== "string") {
58893
- log9.warn("default-emit:behavior:missing-name");
59903
+ log10.warn("default-emit:behavior:missing-name");
58894
59904
  return;
58895
59905
  }
58896
59906
  eventBus.emit("UI:BEHAVIOR_DROP", {
58897
59907
  behaviorName,
58898
59908
  containerNode: target.containerNode
58899
59909
  });
58900
- log9.info("default-emit:behavior", { behaviorName, level: target.level });
59910
+ log10.info("default-emit:behavior", { behaviorName, level: target.level });
58901
59911
  return;
58902
59912
  }
58903
59913
  if (payload.kind === "pattern-instance") {
58904
59914
  const fromPath = payload.data["fromPath"];
58905
59915
  if (typeof fromPath !== "string") {
58906
- log9.warn("default-emit:pattern-instance:missing-fromPath");
59916
+ log10.warn("default-emit:pattern-instance:missing-fromPath");
58907
59917
  return;
58908
59918
  }
58909
59919
  const out = { fromPath, loc: payload.data["loc"] };
@@ -58912,10 +59922,10 @@ function defaultEmit(eventBus, drop) {
58912
59922
  out.toIndex = resolved.index;
58913
59923
  }
58914
59924
  eventBus.emit("UI:PATTERN_MOVE", out);
58915
- log9.info("default-emit:pattern-instance", { fromPath, level: target.level });
59925
+ log10.info("default-emit:pattern-instance", { fromPath, level: target.level });
58916
59926
  return;
58917
59927
  }
58918
- log9.debug("default-emit:unhandled-kind", { kind: payload.kind });
59928
+ log10.debug("default-emit:unhandled-kind", { kind: payload.kind });
58919
59929
  }
58920
59930
  function CanvasDndProvider({
58921
59931
  children,
@@ -58938,9 +59948,9 @@ function CanvasDndProvider({
58938
59948
  if (payload) {
58939
59949
  setActivePayload(payload);
58940
59950
  eventBus.emit("UI:DRAG_START", { kind: payload.kind, data: payload.data });
58941
- log9.info("dragStart", { id: e.active.id, kind: payload.kind });
59951
+ log10.info("dragStart", { id: e.active.id, kind: payload.kind });
58942
59952
  } else {
58943
- log9.warn("dragStart:missing-payload", { id: e.active.id });
59953
+ log10.warn("dragStart:missing-payload", { id: e.active.id });
58944
59954
  }
58945
59955
  }, [eventBus, trackPointer]);
58946
59956
  const handleDragEnd = React96__namespace.default.useCallback((e) => {
@@ -58951,7 +59961,7 @@ function CanvasDndProvider({
58951
59961
  const overData = e.over?.data.current;
58952
59962
  const target = overData?.target;
58953
59963
  const accepts = overData?.accepts;
58954
- log9.info("dragEnd", {
59964
+ log10.info("dragEnd", {
58955
59965
  activeId: e.active.id,
58956
59966
  overId: e.over?.id,
58957
59967
  hasPayload: !!payload,
@@ -58963,12 +59973,12 @@ function CanvasDndProvider({
58963
59973
  }
58964
59974
  if (!payload || !target) return;
58965
59975
  if (accepts && !accepts.includes(payload.kind)) {
58966
- log9.debug("dragEnd:rejected:kind", { kind: payload.kind, accepts: [...accepts] });
59976
+ log10.debug("dragEnd:rejected:kind", { kind: payload.kind, accepts: [...accepts] });
58967
59977
  return;
58968
59978
  }
58969
59979
  const cursor = lastPointerRef.current;
58970
59980
  const resolved = target.resolvePath && cursor ? target.resolvePath(cursor) : null;
58971
- log9.debug("dragEnd:resolve", {
59981
+ log10.debug("dragEnd:resolve", {
58972
59982
  hasResolver: !!target.resolvePath,
58973
59983
  cursorX: cursor?.x,
58974
59984
  cursorY: cursor?.y,
@@ -58982,7 +59992,7 @@ function CanvasDndProvider({
58982
59992
  const handleDragCancel = React96__namespace.default.useCallback(() => {
58983
59993
  setActivePayload(null);
58984
59994
  document.removeEventListener("pointermove", trackPointer);
58985
- log9.info("dragCancel");
59995
+ log10.info("dragCancel");
58986
59996
  }, [trackPointer]);
58987
59997
  return /* @__PURE__ */ jsxRuntime.jsxs(
58988
59998
  core$1.DndContext,
@@ -61042,7 +62052,7 @@ init_AvlTransitionLane();
61042
62052
  init_AvlSwimLane();
61043
62053
  init_avl_atom_types();
61044
62054
  init_avl_elk_layout();
61045
- var log10 = logger.createLogger("almadar:ui:avl:trait-scene");
62055
+ var log11 = logger.createLogger("almadar:ui:avl:trait-scene");
61046
62056
  var SWIM_GUTTER2 = 120;
61047
62057
  var CENTER_W2 = 360;
61048
62058
  var AvlTraitScene = ({
@@ -61055,7 +62065,7 @@ var AvlTraitScene = ({
61055
62065
  const dataKey = React96.useMemo(() => JSON.stringify(data), [data]);
61056
62066
  React96.useEffect(() => {
61057
62067
  computeTraitLayout(data).then(setLayout).catch((error) => {
61058
- log10.error("computeTraitLayout failed", { error: error instanceof Error ? error : String(error) });
62068
+ log11.error("computeTraitLayout failed", { error: error instanceof Error ? error : String(error) });
61059
62069
  });
61060
62070
  }, [dataKey]);
61061
62071
  if (!layout) {