@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
package/dist/avl/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import * as React96 from 'react';
3
- import React96__default, { createContext, useState, useMemo, useRef, useEffect, useContext, useCallback, Suspense, useLayoutEffect, Profiler, useReducer, useSyncExternalStore, lazy, useId } from 'react';
3
+ import React96__default, { createContext, useState, useEffect, useMemo, useRef, useContext, useCallback, Suspense, useLayoutEffect, Profiler, useReducer, useSyncExternalStore, lazy, useId } from 'react';
4
4
  import { getAllPages, matchPathAmong, CurrentPagePathProvider, NavStackProvider, OrbitalProvider, EventBusContext, useTraitScopeChain, useNavStack, ServerBridgeProvider, VerificationProvider, EntitySchemaProvider, OrbitalThemeProvider, useServerBridge, EntityBindingContext, useEntitySchema, RenderSlotProvider, useEntitySchemaOptional, useEntityBindingSnapshot, TraitScopeProvider, useTraitScope, useCurrentPagePath, useRenderSlot, useGameAudioContextOptional } from '@almadar/ui/providers';
5
5
  import { createLogger, setNamespaceLevel, isLogLevelEnabled } from '@almadar/logger';
6
6
  import ELK from 'elkjs/lib/elk.bundled.js';
@@ -13711,7 +13711,7 @@ function collectDrawnItems(nodes) {
13711
13711
  for (const n of nodes) {
13712
13712
  switch (n.type) {
13713
13713
  case "draw-sprite":
13714
- if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id, anchor: n.anchor, width: n.width, height: n.height });
13714
+ if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id, anchor: n.anchor, width: n.width, height: n.height, rotation: n.rotation });
13715
13715
  break;
13716
13716
  case "draw-shape":
13717
13717
  case "draw-text":
@@ -13721,7 +13721,7 @@ function collectDrawnItems(nodes) {
13721
13721
  break;
13722
13722
  case "draw-sprite-layer":
13723
13723
  for (const it of n.items) {
13724
- if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id, anchor: it.anchor, width: it.width, height: it.height });
13724
+ if (isValidScenePos(it.position)) out.push({ pos: it.position, id: it.id, anchor: it.anchor, width: it.width, height: it.height, rotation: it.rotation });
13725
13725
  }
13726
13726
  break;
13727
13727
  case "draw-shape-layer":
@@ -13741,15 +13741,39 @@ function buildHitIndex(items) {
13741
13741
  }
13742
13742
  return m;
13743
13743
  }
13744
+ function withPreviewPosition(nodes, id, pos) {
13745
+ return nodes.map((n) => {
13746
+ if (n.type === "draw-sprite-layer" || n.type === "draw-shape-layer" || n.type === "draw-text-layer") {
13747
+ if (!n.items.some((it) => it.id === id)) return n;
13748
+ return { ...n, items: n.items.map((it) => it.id === id ? { ...it, position: pos } : it) };
13749
+ }
13750
+ if (n.type === "draw-group") {
13751
+ if (!Array.isArray(n.items)) return n;
13752
+ return { ...n, items: withPreviewPosition(n.items, id, pos) };
13753
+ }
13754
+ if (n.id === id && "position" in n) {
13755
+ return { ...n, position: pos };
13756
+ }
13757
+ return n;
13758
+ });
13759
+ }
13744
13760
  function hitTestSprites(items, projector, point) {
13745
13761
  for (let i = items.length - 1; i >= 0; i--) {
13746
13762
  const it = items[i];
13747
13763
  if (it.id === void 0) continue;
13748
13764
  const r2 = spriteRect(projector, { position: it.pos, anchor: it.anchor, width: it.width, height: it.height });
13749
- if (point.x >= r2.x && point.x <= r2.x + r2.w && point.y >= r2.y && point.y <= r2.y + r2.h) return it.id;
13765
+ const test = it.rotation ? rotatePoint(point, { x: r2.x + r2.w / 2, y: r2.y + r2.h / 2 }, -it.rotation) : point;
13766
+ if (test.x >= r2.x && test.x <= r2.x + r2.w && test.y >= r2.y && test.y <= r2.y + r2.h) return it.id;
13750
13767
  }
13751
13768
  return void 0;
13752
13769
  }
13770
+ function rotatePoint(p, center, radians) {
13771
+ const cos = Math.cos(radians);
13772
+ const sin = Math.sin(radians);
13773
+ const dx = p.x - center.x;
13774
+ const dy = p.y - center.y;
13775
+ return { x: center.x + dx * cos - dy * sin, y: center.y + dx * sin + dy * cos };
13776
+ }
13753
13777
  var init_hitTest = __esm({
13754
13778
  "lib/drawable/hitTest.ts"() {
13755
13779
  init_contract();
@@ -13758,6 +13782,41 @@ var init_hitTest = __esm({
13758
13782
  function normalizeBackdrop(bg) {
13759
13783
  return typeof bg === "string" ? { url: bg, role: "decoration", category: "background" } : bg;
13760
13784
  }
13785
+ function selectionOverlayNodes(projector, item) {
13786
+ const r2 = spriteRect(projector, { position: item.pos, anchor: item.anchor, width: item.width, height: item.height });
13787
+ const tw = projector.tileWidth;
13788
+ const cellTopLeft = projector.anchorPoint(item.pos, "top-left");
13789
+ const offsetX = (r2.x - cellTopLeft.x) / tw;
13790
+ const offsetY = (r2.y - cellTopLeft.y) / tw;
13791
+ const width = r2.w / tw;
13792
+ const height = r2.h / tw;
13793
+ const handle = EDIT_HANDLE_SIZE_PX / tw;
13794
+ const ring = {
13795
+ type: "draw-shape",
13796
+ shape: "rect",
13797
+ position: item.pos,
13798
+ anchor: "top-left",
13799
+ offsetX,
13800
+ offsetY,
13801
+ width,
13802
+ height,
13803
+ stroke: EDIT_SELECTION_COLOR,
13804
+ strokeWidth: 2,
13805
+ fill: "none"
13806
+ };
13807
+ const handles = EDIT_SELECTION_CORNERS.map(([cx, cy]) => ({
13808
+ type: "draw-shape",
13809
+ shape: "rect",
13810
+ position: item.pos,
13811
+ anchor: "top-left",
13812
+ offsetX: offsetX + cx * width - handle / 2,
13813
+ offsetY: offsetY + cy * height - handle / 2,
13814
+ width: handle,
13815
+ height: handle,
13816
+ fill: EDIT_SELECTION_COLOR
13817
+ }));
13818
+ return [ring, ...handles];
13819
+ }
13761
13820
  function Canvas2D({
13762
13821
  className,
13763
13822
  isLoading = false,
@@ -13771,6 +13830,12 @@ function Canvas2D({
13771
13830
  tileLeaveEvent,
13772
13831
  keyMap,
13773
13832
  keyUpMap,
13833
+ editable = false,
13834
+ selectedId = null,
13835
+ onSelect,
13836
+ onMove,
13837
+ selectEvent,
13838
+ moveEvent,
13774
13839
  camera = "pan-zoom",
13775
13840
  scale = 0.4,
13776
13841
  tileWidth,
@@ -14028,10 +14093,22 @@ function Canvas2D({
14028
14093
  painter.scale(cam.zoom, cam.zoom);
14029
14094
  painter.translate(-viewportSize.width / 2 - cam.x, -viewportSize.height / 2 - cam.y);
14030
14095
  const dctx = { projector, time: timeMs, invalidate: bumpAtlas };
14031
- for (const node of drawables) paintDrawable(painter, node, dctx);
14096
+ let paintNodes = drawables;
14097
+ if (editable) {
14098
+ const drag = editDragRef.current;
14099
+ if (drag && drag.moved) {
14100
+ paintNodes = withPreviewPosition(paintNodes, drag.id, { x: drag.previewX, y: drag.previewY });
14101
+ }
14102
+ const selectedItem = selectedId != null ? drawnItems.find((it) => it.id === selectedId) : void 0;
14103
+ if (selectedItem) {
14104
+ const overlaySource = drag && drag.moved && drag.id === selectedId ? { ...selectedItem, pos: { x: drag.previewX, y: drag.previewY } } : selectedItem;
14105
+ paintNodes = [...paintNodes, ...selectionOverlayNodes(projector, overlaySource)];
14106
+ }
14107
+ }
14108
+ for (const node of paintNodes) paintDrawable(painter, node, dctx);
14032
14109
  painter.restore();
14033
- scheduleAnimation(drawables);
14034
- }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance]);
14110
+ scheduleAnimation(paintNodes);
14111
+ }, [viewportSize, backgroundImage, bgColor, drawables, projector, cameraRef, bumpAtlas, getImage, cameraPos, defaultGridFocus, camera, dragDistance, editable, selectedId, drawnItems]);
14035
14112
  useEffect(() => {
14036
14113
  drawTimeRef.current = draw;
14037
14114
  }, [draw]);
@@ -14076,23 +14153,83 @@ function Canvas2D({
14076
14153
  };
14077
14154
  }, [camera, followTarget, lerpToTarget, draw]);
14078
14155
  const singlePointerActiveRef = useRef(false);
14156
+ const editDragRef = useRef(null);
14157
+ const pointerToScene = useCallback((clientX, clientY) => {
14158
+ const canvas = canvasRef.current;
14159
+ if (!canvas) return { x: 0, y: 0 };
14160
+ const world = screenToWorld(clientX, clientY, canvas, viewportSize);
14161
+ const adjustedX = world.x - scaledTileWidth / 2;
14162
+ const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
14163
+ return unproject(adjustedX, adjustedY);
14164
+ }, [screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject]);
14079
14165
  const handleCanvasPointerDown = useCallback((e) => {
14080
14166
  singlePointerActiveRef.current = true;
14167
+ if (editable) {
14168
+ if (!canvasRef.current) return;
14169
+ const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
14170
+ const hitId = hitTestSprites(drawnItems, projector, world);
14171
+ if (hitId === void 0) return;
14172
+ const item = [...drawnItems].reverse().find((it) => it.id === hitId);
14173
+ if (!item) return;
14174
+ editDragRef.current = {
14175
+ id: hitId,
14176
+ pointerId: e.pointerId,
14177
+ startClientX: e.clientX,
14178
+ startClientY: e.clientY,
14179
+ startSceneX: item.pos.x,
14180
+ startSceneY: item.pos.y,
14181
+ moved: false,
14182
+ previewX: item.pos.x,
14183
+ previewY: item.pos.y
14184
+ };
14185
+ return;
14186
+ }
14081
14187
  if (enableCamera) handlePointerDown(e);
14082
- }, [enableCamera, handlePointerDown]);
14188
+ }, [editable, screenToWorld, viewportSize, drawnItems, projector, enableCamera, handlePointerDown]);
14083
14189
  const handleCanvasPointerMove = useCallback((e) => {
14190
+ if (editable) {
14191
+ const drag = editDragRef.current;
14192
+ if (!drag || drag.pointerId !== e.pointerId) return;
14193
+ const dxPx = e.clientX - drag.startClientX;
14194
+ const dyPx = e.clientY - drag.startClientY;
14195
+ if (!drag.moved && Math.abs(dxPx) + Math.abs(dyPx) <= 5) return;
14196
+ drag.moved = true;
14197
+ const nowScene = pointerToScene(e.clientX, e.clientY);
14198
+ const startScene = pointerToScene(drag.startClientX, drag.startClientY);
14199
+ drag.previewX = drag.startSceneX + (nowScene.x - startScene.x);
14200
+ drag.previewY = drag.startSceneY + (nowScene.y - startScene.y);
14201
+ draw();
14202
+ return;
14203
+ }
14084
14204
  if (enableCamera) handlePointerMove(e, () => draw());
14085
- }, [enableCamera, handlePointerMove, draw]);
14205
+ }, [editable, pointerToScene, draw, enableCamera, handlePointerMove]);
14086
14206
  const handleCanvasHover = useCallback((e) => {
14087
14207
  if (singlePointerActiveRef.current) return;
14088
14208
  if (!tileHoverEvent || !canvasRef.current) return;
14089
- const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
14090
- const adjustedX = world.x - scaledTileWidth / 2;
14091
- const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
14092
- const isoPos = unproject(adjustedX, adjustedY);
14209
+ const isoPos = pointerToScene(e.clientX, e.clientY);
14093
14210
  eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
14094
- }, [screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject, tileHoverEvent, eventBus]);
14211
+ }, [pointerToScene, tileHoverEvent, eventBus]);
14095
14212
  const handleCanvasPointerUp = useCallback((e) => {
14213
+ if (editable) {
14214
+ singlePointerActiveRef.current = false;
14215
+ const drag = editDragRef.current;
14216
+ if (drag && drag.pointerId === e.pointerId) {
14217
+ editDragRef.current = null;
14218
+ if (drag.moved) {
14219
+ onMove?.(drag.id, drag.previewX, drag.previewY);
14220
+ if (moveEvent) eventBus.emit(`UI:${moveEvent}`, { id: drag.id, x: drag.previewX, y: drag.previewY });
14221
+ draw();
14222
+ return;
14223
+ }
14224
+ const next = selectedId === drag.id ? null : drag.id;
14225
+ onSelect?.(next);
14226
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: next });
14227
+ return;
14228
+ }
14229
+ onSelect?.(null);
14230
+ if (selectEvent) eventBus.emit(`UI:${selectEvent}`, { id: null });
14231
+ return;
14232
+ }
14096
14233
  singlePointerActiveRef.current = false;
14097
14234
  if (enableCamera) handlePointerUp();
14098
14235
  if (dragDistance() > 5) return;
@@ -14103,16 +14240,14 @@ function Canvas2D({
14103
14240
  eventBus.emit(`UI:${unitClickEvent}`, { unitId: spriteHit });
14104
14241
  return;
14105
14242
  }
14106
- const adjustedX = world.x - scaledTileWidth / 2;
14107
- const adjustedY = squareGrid ? world.y - scaledTileWidth / 2 : world.y - scaledDiamondTopY - scaledFloorHeight / 2;
14108
- const isoPos = unproject(adjustedX, adjustedY);
14243
+ const isoPos = pointerToScene(e.clientX, e.clientY);
14109
14244
  const hitId = hitIndex.get(`${isoPos.x},${isoPos.y}`);
14110
14245
  if (hitId !== void 0 && unitClickEvent) {
14111
14246
  eventBus.emit(`UI:${unitClickEvent}`, { unitId: hitId });
14112
14247
  } else if (tileClickEvent) {
14113
14248
  eventBus.emit(`UI:${tileClickEvent}`, { x: isoPos.x, y: isoPos.y });
14114
14249
  }
14115
- }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, squareGrid, scaledDiamondTopY, scaledFloorHeight, unproject, hitIndex, drawnItems, projector, tileClickEvent, unitClickEvent, eventBus]);
14250
+ }, [editable, selectedId, onMove, moveEvent, onSelect, selectEvent, eventBus, draw, enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, drawnItems, projector, tileClickEvent, unitClickEvent, hitIndex, pointerToScene]);
14116
14251
  const handleCanvasPointerLeave = useCallback(() => {
14117
14252
  handleMouseLeave();
14118
14253
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -14131,7 +14266,7 @@ function Canvas2D({
14131
14266
  }, [enableCamera, handlePointerUp]);
14132
14267
  const gestureHandlers = useCanvasGestures({
14133
14268
  canvasRef,
14134
- enabled: enableCamera || !!tileHoverEvent || !!tileClickEvent || !!unitClickEvent,
14269
+ enabled: enableCamera || !!tileHoverEvent || !!tileClickEvent || !!unitClickEvent || editable,
14135
14270
  onPointerDown: handleCanvasPointerDown,
14136
14271
  onPointerMove: handleCanvasPointerMove,
14137
14272
  onPointerUp: handleCanvasPointerUp,
@@ -14263,7 +14398,7 @@ function Canvas2D({
14263
14398
  }
14264
14399
  ) });
14265
14400
  }
14266
- var canvas2DLog;
14401
+ var canvas2DLog, EDIT_SELECTION_COLOR, EDIT_HANDLE_SIZE_PX, EDIT_SELECTION_CORNERS;
14267
14402
  var init_Canvas2D = __esm({
14268
14403
  "components/game/molecules/Canvas2D.tsx"() {
14269
14404
  "use client";
@@ -14288,8 +14423,12 @@ var init_Canvas2D = __esm({
14288
14423
  init_DrawGroup();
14289
14424
  init_registry();
14290
14425
  init_hitTest();
14426
+ init_contract();
14291
14427
  init_isometric();
14292
14428
  canvas2DLog = createLogger("almadar:ui:game-canvas");
14429
+ EDIT_SELECTION_COLOR = "#3b82f6";
14430
+ EDIT_HANDLE_SIZE_PX = 8;
14431
+ EDIT_SELECTION_CORNERS = [[0, 0], [1, 0], [0, 1], [1, 1]];
14293
14432
  Canvas2D.displayName = "Canvas2D";
14294
14433
  }
14295
14434
  });
@@ -14331,6 +14470,12 @@ function Canvas({
14331
14470
  featureClickEvent,
14332
14471
  keyMap,
14333
14472
  keyUpMap,
14473
+ editable,
14474
+ selectedId,
14475
+ onSelect,
14476
+ onMove,
14477
+ selectEvent,
14478
+ moveEvent,
14334
14479
  children
14335
14480
  }) {
14336
14481
  canvasLog.debug("Canvas render", { mode, drawablesCount: drawables?.length, projection, camera: camera ? JSON.stringify(camera) : void 0 });
@@ -14408,6 +14553,12 @@ function Canvas({
14408
14553
  tileLeaveEvent,
14409
14554
  keyMap,
14410
14555
  keyUpMap,
14556
+ editable,
14557
+ selectedId,
14558
+ onSelect,
14559
+ onMove,
14560
+ selectEvent,
14561
+ moveEvent,
14411
14562
  ...children !== void 0 ? { children } : {}
14412
14563
  }
14413
14564
  );
@@ -14429,15 +14580,19 @@ function GameAudioToggle({
14429
14580
  size = "sm",
14430
14581
  className,
14431
14582
  onAsset,
14432
- offAsset
14583
+ offAsset,
14584
+ toggleEvent
14433
14585
  }) {
14434
14586
  const ctx = useGameAudioContextOptional();
14435
14587
  const [localMuted, setLocalMuted] = useState(false);
14436
14588
  const muted = ctx ? ctx.muted : localMuted;
14437
14589
  const setMuted = ctx ? ctx.setMuted : setLocalMuted;
14590
+ const eventBus = useEventBus();
14438
14591
  const handleToggle = useCallback(() => {
14439
- setMuted(!muted);
14440
- }, [muted, setMuted]);
14592
+ const next = !muted;
14593
+ setMuted(next);
14594
+ if (toggleEvent) eventBus.emit(`UI:${toggleEvent}`, { muted: next });
14595
+ }, [muted, setMuted, toggleEvent, eventBus]);
14441
14596
  const activeAsset = muted ? offAsset : onAsset;
14442
14597
  return /* @__PURE__ */ jsx(
14443
14598
  Button,
@@ -14456,10 +14611,319 @@ var init_GameAudioToggle = __esm({
14456
14611
  "use client";
14457
14612
  init_atoms();
14458
14613
  init_cn();
14614
+ init_useEventBus();
14459
14615
  init_GameIcon();
14460
14616
  GameAudioToggle.displayName = "GameAudioToggle";
14461
14617
  }
14462
14618
  });
14619
+ function pickPath(entry) {
14620
+ if (Array.isArray(entry.path)) {
14621
+ return entry.path[Math.floor(Math.random() * entry.path.length)];
14622
+ }
14623
+ return entry.path;
14624
+ }
14625
+ function useGameAudio({
14626
+ manifest,
14627
+ baseUrl = "",
14628
+ initialMuted = false,
14629
+ initialVolume = 1
14630
+ }) {
14631
+ const [muted, setMutedState] = useState(initialMuted);
14632
+ const [masterVolume, setMasterVolumeState] = useState(initialVolume);
14633
+ const mutedRef = useRef(muted);
14634
+ const volumeRef = useRef(masterVolume);
14635
+ const manifestRef = useRef(manifest);
14636
+ mutedRef.current = muted;
14637
+ volumeRef.current = masterVolume;
14638
+ manifestRef.current = manifest;
14639
+ const poolsRef = useRef(/* @__PURE__ */ new Map());
14640
+ const getOrCreateElement = useCallback((key) => {
14641
+ const entry = manifestRef.current[key];
14642
+ if (!entry) return null;
14643
+ let pool = poolsRef.current.get(key);
14644
+ if (!pool) {
14645
+ pool = [];
14646
+ poolsRef.current.set(key, pool);
14647
+ }
14648
+ const maxSize = entry.poolSize ?? 1;
14649
+ for (const audio of pool) {
14650
+ if (audio.paused && (audio.ended || audio.currentTime === 0)) {
14651
+ return audio;
14652
+ }
14653
+ }
14654
+ if (pool.length < maxSize) {
14655
+ const src = baseUrl + pickPath(entry);
14656
+ const audio = new Audio(src);
14657
+ audio.loop = entry.loop ?? false;
14658
+ pool.push(audio);
14659
+ return audio;
14660
+ }
14661
+ if (!entry.loop) {
14662
+ let oldest = pool[0];
14663
+ for (const audio of pool) {
14664
+ if (audio.currentTime > oldest.currentTime) {
14665
+ oldest = audio;
14666
+ }
14667
+ }
14668
+ oldest.pause();
14669
+ oldest.currentTime = 0;
14670
+ return oldest;
14671
+ }
14672
+ return null;
14673
+ }, [baseUrl]);
14674
+ const play = useCallback((key) => {
14675
+ if (mutedRef.current) return;
14676
+ const entry = manifestRef.current[key];
14677
+ if (!entry) return;
14678
+ const audio = getOrCreateElement(key);
14679
+ if (!audio) return;
14680
+ audio.volume = Math.min(1, (entry.volume ?? 1) * volumeRef.current);
14681
+ if (!entry.loop) {
14682
+ audio.currentTime = 0;
14683
+ }
14684
+ const promise = audio.play();
14685
+ if (promise) {
14686
+ promise.catch(() => {
14687
+ });
14688
+ }
14689
+ }, [getOrCreateElement]);
14690
+ const stop = useCallback((key) => {
14691
+ const pool = poolsRef.current.get(key);
14692
+ if (!pool) return;
14693
+ for (const audio of pool) {
14694
+ audio.pause();
14695
+ audio.currentTime = 0;
14696
+ }
14697
+ }, []);
14698
+ const currentMusicKeyRef = useRef(null);
14699
+ const currentMusicElRef = useRef(null);
14700
+ const musicFadeRef = useRef(null);
14701
+ const pendingMusicKeyRef = useRef(null);
14702
+ const clearMusicFade = useCallback(() => {
14703
+ if (musicFadeRef.current) {
14704
+ clearInterval(musicFadeRef.current);
14705
+ musicFadeRef.current = null;
14706
+ }
14707
+ }, []);
14708
+ const playMusic = useCallback((key) => {
14709
+ if (key === currentMusicKeyRef.current) return;
14710
+ pendingMusicKeyRef.current = key;
14711
+ const entry = manifestRef.current[key];
14712
+ if (!entry) return;
14713
+ const fadeDurationMs = entry.crossfadeDurationMs ?? 1500;
14714
+ const stepMs = 50;
14715
+ const totalSteps = Math.max(1, fadeDurationMs / stepMs);
14716
+ const targetVolume = Math.min(1, (entry.volume ?? 1) * volumeRef.current);
14717
+ clearMusicFade();
14718
+ const src = baseUrl + (Array.isArray(entry.path) ? entry.path[0] : entry.path);
14719
+ const incoming = new Audio(src);
14720
+ incoming.loop = true;
14721
+ incoming.volume = 0;
14722
+ const outgoing = currentMusicElRef.current;
14723
+ const outgoingStartVol = outgoing?.volume ?? 0;
14724
+ currentMusicKeyRef.current = key;
14725
+ currentMusicElRef.current = incoming;
14726
+ if (!mutedRef.current) {
14727
+ incoming.play().catch(() => {
14728
+ currentMusicKeyRef.current = null;
14729
+ currentMusicElRef.current = outgoing;
14730
+ });
14731
+ }
14732
+ let step = 0;
14733
+ musicFadeRef.current = setInterval(() => {
14734
+ step++;
14735
+ const progress = Math.min(step / totalSteps, 1);
14736
+ incoming.volume = Math.min(1, targetVolume * progress);
14737
+ if (outgoing) {
14738
+ outgoing.volume = Math.max(0, outgoingStartVol * (1 - progress));
14739
+ }
14740
+ if (progress >= 1) {
14741
+ clearMusicFade();
14742
+ if (outgoing) {
14743
+ outgoing.pause();
14744
+ outgoing.src = "";
14745
+ }
14746
+ }
14747
+ }, stepMs);
14748
+ }, [baseUrl, clearMusicFade]);
14749
+ const stopMusic = useCallback((fadeDurationMs = 1e3) => {
14750
+ const outgoing = currentMusicElRef.current;
14751
+ if (!outgoing) return;
14752
+ currentMusicKeyRef.current = null;
14753
+ currentMusicElRef.current = null;
14754
+ pendingMusicKeyRef.current = null;
14755
+ clearMusicFade();
14756
+ const startVolume = outgoing.volume;
14757
+ const stepMs = 50;
14758
+ const totalSteps = Math.max(1, fadeDurationMs / stepMs);
14759
+ let step = 0;
14760
+ musicFadeRef.current = setInterval(() => {
14761
+ step++;
14762
+ const progress = step / totalSteps;
14763
+ outgoing.volume = Math.max(0, startVolume * (1 - progress));
14764
+ if (progress >= 1) {
14765
+ clearMusicFade();
14766
+ outgoing.pause();
14767
+ outgoing.src = "";
14768
+ }
14769
+ }, stepMs);
14770
+ }, [clearMusicFade]);
14771
+ const stopAll = useCallback(() => {
14772
+ for (const pool of poolsRef.current.values()) {
14773
+ for (const audio of pool) {
14774
+ audio.pause();
14775
+ audio.currentTime = 0;
14776
+ }
14777
+ }
14778
+ stopMusic(0);
14779
+ }, [stopMusic]);
14780
+ const setMuted = useCallback((value) => {
14781
+ setMutedState(value);
14782
+ if (value) {
14783
+ for (const [key, pool] of poolsRef.current.entries()) {
14784
+ if (manifestRef.current[key]?.loop) {
14785
+ for (const audio of pool) {
14786
+ if (!audio.paused) audio.pause();
14787
+ }
14788
+ }
14789
+ }
14790
+ currentMusicElRef.current?.pause();
14791
+ } else {
14792
+ for (const [key, pool] of poolsRef.current.entries()) {
14793
+ const entry = manifestRef.current[key];
14794
+ if (entry?.loop && entry?.autostart) {
14795
+ for (const audio of pool) {
14796
+ if (audio.paused) audio.play().catch(() => {
14797
+ });
14798
+ }
14799
+ }
14800
+ }
14801
+ const musicEl = currentMusicElRef.current;
14802
+ if (musicEl) {
14803
+ musicEl.play().catch(() => {
14804
+ });
14805
+ }
14806
+ }
14807
+ }, []);
14808
+ const setMasterVolume = useCallback((volume) => {
14809
+ const clamped = Math.max(0, Math.min(1, volume));
14810
+ setMasterVolumeState(clamped);
14811
+ for (const [key, pool] of poolsRef.current.entries()) {
14812
+ const entryVol = manifestRef.current[key]?.volume ?? 1;
14813
+ for (const audio of pool) {
14814
+ audio.volume = Math.min(1, entryVol * clamped);
14815
+ }
14816
+ }
14817
+ if (!musicFadeRef.current && currentMusicElRef.current) {
14818
+ const key = currentMusicKeyRef.current;
14819
+ const entryVol = key ? manifestRef.current[key]?.volume ?? 1 : 1;
14820
+ currentMusicElRef.current.volume = Math.min(1, entryVol * clamped);
14821
+ }
14822
+ }, []);
14823
+ const unlockedRef = useRef(false);
14824
+ useEffect(() => {
14825
+ const autoKeys = Object.keys(manifest).filter((k) => manifest[k].autostart);
14826
+ const hasPendingMusic = () => pendingMusicKeyRef.current !== null;
14827
+ const hasAutoStart = autoKeys.length > 0;
14828
+ if (!hasAutoStart && !hasPendingMusic()) return;
14829
+ const unlock = () => {
14830
+ if (unlockedRef.current) return;
14831
+ unlockedRef.current = true;
14832
+ if (!mutedRef.current) {
14833
+ for (const key of autoKeys) {
14834
+ play(key);
14835
+ }
14836
+ const pending = pendingMusicKeyRef.current;
14837
+ if (pending && pending !== currentMusicKeyRef.current) {
14838
+ playMusic(pending);
14839
+ }
14840
+ }
14841
+ };
14842
+ document.addEventListener("click", unlock, { once: true });
14843
+ document.addEventListener("keydown", unlock, { once: true });
14844
+ document.addEventListener("touchstart", unlock, { once: true });
14845
+ return () => {
14846
+ document.removeEventListener("click", unlock);
14847
+ document.removeEventListener("keydown", unlock);
14848
+ document.removeEventListener("touchstart", unlock);
14849
+ };
14850
+ }, [manifest, play, playMusic]);
14851
+ useEffect(() => {
14852
+ return () => {
14853
+ clearMusicFade();
14854
+ for (const pool of poolsRef.current.values()) {
14855
+ for (const audio of pool) {
14856
+ audio.pause();
14857
+ audio.src = "";
14858
+ }
14859
+ }
14860
+ poolsRef.current.clear();
14861
+ if (currentMusicElRef.current) {
14862
+ currentMusicElRef.current.pause();
14863
+ currentMusicElRef.current.src = "";
14864
+ currentMusicElRef.current = null;
14865
+ }
14866
+ };
14867
+ }, [clearMusicFade]);
14868
+ return {
14869
+ play,
14870
+ stop,
14871
+ stopAll,
14872
+ playMusic,
14873
+ stopMusic,
14874
+ muted,
14875
+ setMuted,
14876
+ masterVolume,
14877
+ setMasterVolume
14878
+ };
14879
+ }
14880
+ var init_useGameAudio = __esm({
14881
+ "hooks/useGameAudio.ts"() {
14882
+ "use client";
14883
+ useGameAudio.displayName = "useGameAudio";
14884
+ }
14885
+ });
14886
+ function GameAudioCue({
14887
+ cue,
14888
+ cueSeq,
14889
+ music,
14890
+ muted,
14891
+ volume,
14892
+ manifest,
14893
+ baseUrl
14894
+ }) {
14895
+ const { play, playMusic, stopMusic, setMuted, setMasterVolume } = useGameAudio({
14896
+ manifest,
14897
+ baseUrl,
14898
+ initialMuted: muted,
14899
+ initialVolume: volume
14900
+ });
14901
+ const prevCueSeqRef = useRef(cueSeq);
14902
+ useEffect(() => {
14903
+ if (cue && cueSeq !== void 0 && cueSeq !== prevCueSeqRef.current) {
14904
+ play(cue);
14905
+ }
14906
+ prevCueSeqRef.current = cueSeq;
14907
+ }, [cue, cueSeq, play]);
14908
+ useEffect(() => {
14909
+ if (music) playMusic(music);
14910
+ else stopMusic();
14911
+ }, [music, playMusic, stopMusic]);
14912
+ useEffect(() => {
14913
+ if (muted !== void 0) setMuted(muted);
14914
+ }, [muted, setMuted]);
14915
+ useEffect(() => {
14916
+ if (volume !== void 0) setMasterVolume(volume);
14917
+ }, [volume, setMasterVolume]);
14918
+ return null;
14919
+ }
14920
+ var init_GameAudioCue = __esm({
14921
+ "components/game/atoms/GameAudioCue.tsx"() {
14922
+ "use client";
14923
+ init_useGameAudio();
14924
+ GameAudioCue.displayName = "GameAudioCue";
14925
+ }
14926
+ });
14463
14927
  function isKnownState(s) {
14464
14928
  return s in DEFAULT_STATE_STYLES;
14465
14929
  }
@@ -15158,15 +15622,18 @@ var init_StateJsonView = __esm({
15158
15622
  StateJsonView.displayName = "StateJsonView";
15159
15623
  }
15160
15624
  });
15161
- var GAME_FONTS, GameShell;
15162
- var init_GameShell = __esm({
15163
- "components/game/templates/GameShell.tsx"() {
15164
- init_cn();
15165
- init_Box();
15166
- init_Card();
15167
- init_Typography();
15168
- init_AtlasImage();
15169
- GAME_FONTS = {
15625
+
15626
+ // lib/gameFonts.ts
15627
+ function resolveGameFontFamily(input) {
15628
+ if (!input) return void 0;
15629
+ const resolved = GAME_FONT_KEYS[input];
15630
+ if (resolved) return `'${resolved}', ui-sans-serif, system-ui, sans-serif`;
15631
+ return input;
15632
+ }
15633
+ var GAME_FONT_KEYS;
15634
+ var init_gameFonts = __esm({
15635
+ "lib/gameFonts.ts"() {
15636
+ GAME_FONT_KEYS = {
15170
15637
  fredoka: "Fredoka",
15171
15638
  future: "Kenney Future",
15172
15639
  "future-narrow": "Kenney Future Narrow",
@@ -15174,6 +15641,17 @@ var init_GameShell = __esm({
15174
15641
  blocks: "Kenney Blocks",
15175
15642
  mini: "Kenney Mini"
15176
15643
  };
15644
+ }
15645
+ });
15646
+ var GameShell;
15647
+ var init_GameShell = __esm({
15648
+ "components/game/templates/GameShell.tsx"() {
15649
+ init_cn();
15650
+ init_gameFonts();
15651
+ init_Box();
15652
+ init_Card();
15653
+ init_Typography();
15654
+ init_AtlasImage();
15177
15655
  GameShell = ({
15178
15656
  appName = "Game",
15179
15657
  hud,
@@ -15187,7 +15665,7 @@ var init_GameShell = __esm({
15187
15665
  fontFamily,
15188
15666
  "data-theme": dataTheme
15189
15667
  }) => {
15190
- const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
15668
+ const displayFont = resolveGameFontFamily(fontFamily);
15191
15669
  return /* @__PURE__ */ jsxs(
15192
15670
  Box,
15193
15671
  {
@@ -15206,7 +15684,7 @@ var init_GameShell = __esm({
15206
15684
  // passed — an always-on inline stamp would shadow the orbital's
15207
15685
  // inline-theme font-family-display token (inline style beats the
15208
15686
  // theme provider's vars for the whole shell subtree).
15209
- ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
15687
+ ...displayFont ? { "--font-family-display": displayFont } : {}
15210
15688
  },
15211
15689
  children: [
15212
15690
  backgroundAsset && /* @__PURE__ */ jsx(
@@ -15417,14 +15895,15 @@ function drawArrowHead(ctx, x1, y1, x2, y2, size) {
15417
15895
  ctx.closePath();
15418
15896
  ctx.fill();
15419
15897
  }
15420
- function drawShape(ctx, shape, width, height, allShapes) {
15898
+ function drawShape(ctx, shape, width, height, allShapes, fontFamily) {
15421
15899
  ctx.save();
15422
15900
  const opacity = shape.opacity ?? 1;
15423
15901
  ctx.globalAlpha = opacity;
15424
15902
  const stroke = resolveColor2(shape.color, ctx, "#333333");
15425
15903
  const fill = shape.fill ? resolveColor2(shape.fill, ctx, "#cccccc") : void 0;
15426
15904
  ctx.lineWidth = shape.lineWidth ?? 2;
15427
- if (shape.dash) ctx.setLineDash([...DASH_PATTERNS[shape.dash]]);
15905
+ const dashPattern = shape.dash ? DASH_PATTERNS[shape.dash] : void 0;
15906
+ if (dashPattern) ctx.setLineDash([...dashPattern]);
15428
15907
  switch (shape.type) {
15429
15908
  case "grid": {
15430
15909
  const step = shape.step ?? 40;
@@ -15546,7 +16025,7 @@ function drawShape(ctx, shape, width, height, allShapes) {
15546
16025
  case "text": {
15547
16026
  if (shape.x == null || shape.y == null || !shape.text) break;
15548
16027
  ctx.fillStyle = stroke;
15549
- ctx.font = `${shape.fontSize ?? 14}px ${themeBodyFont(ctx.canvas)}`;
16028
+ ctx.font = `${shape.fontSize ?? 14}px ${shape.fontFamily ?? fontFamily ?? themeBodyFont(ctx.canvas)}`;
15550
16029
  ctx.textAlign = shape.align ?? "left";
15551
16030
  ctx.textBaseline = "middle";
15552
16031
  ctx.fillText(shape.text, shape.x, shape.y);
@@ -15588,7 +16067,7 @@ function drawShape(ctx, shape, width, height, allShapes) {
15588
16067
  }
15589
16068
  ctx.restore();
15590
16069
  }
15591
- function readoutShapes(readouts, width) {
16070
+ function readoutShapes(readouts, width, fontFamily) {
15592
16071
  const out = [];
15593
16072
  const chipH = 18;
15594
16073
  const gap = 6;
@@ -15612,13 +16091,14 @@ function readoutShapes(readouts, width) {
15612
16091
  text,
15613
16092
  color: "#ffffff",
15614
16093
  fontSize: 10,
15615
- align: "center"
16094
+ align: "center",
16095
+ fontFamily
15616
16096
  });
15617
16097
  rightEdge = chipX - gap;
15618
16098
  }
15619
16099
  return out;
15620
16100
  }
15621
- function traceShapes(panel, k, width, height) {
16101
+ function traceShapes(panel, k, width, height, fontFamily) {
15622
16102
  const w = panel.width ?? Math.round(width * 0.32);
15623
16103
  const h = panel.height ?? Math.round(height * 0.28);
15624
16104
  const x = panel.x ?? width - w - 8;
@@ -15672,14 +16152,14 @@ function traceShapes(panel, k, width, height) {
15672
16152
  out.push({ type: "circle", x: last.x, y: last.y, radius: 2, color, fill: color });
15673
16153
  }
15674
16154
  if (series.label) {
15675
- out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9 });
16155
+ out.push({ type: "text", x: x + 6, y: y + 10 + 11 * j, text: series.label, color, fontSize: 9, fontFamily });
15676
16156
  }
15677
16157
  });
15678
16158
  if (panel.yLabel) {
15679
- out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right" });
16159
+ out.push({ type: "text", x: x + w - 6, y: y + 10, text: panel.yLabel, color: "#6b7280", fontSize: 9, align: "right", fontFamily });
15680
16160
  }
15681
16161
  if (panel.xLabel) {
15682
- out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right" });
16162
+ out.push({ type: "text", x: x + w - 6, y: y + h - 6, text: panel.xLabel, color: "#6b7280", fontSize: 9, align: "right", fontFamily });
15683
16163
  }
15684
16164
  return out;
15685
16165
  }
@@ -15702,13 +16182,14 @@ var init_LearningCanvas = __esm({
15702
16182
  init_useEventBus();
15703
16183
  init_webPainter2d();
15704
16184
  init_paintDispatch();
15705
- DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
16185
+ DASH_PATTERNS = { solid: [], dashed: [6, 4], dotted: [2, 3] };
15706
16186
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15707
16187
  LearningCanvas = ({
15708
16188
  className,
15709
16189
  width = 600,
15710
16190
  height = 400,
15711
16191
  backgroundColor,
16192
+ fontFamily,
15712
16193
  shapes = [],
15713
16194
  drawables,
15714
16195
  projector,
@@ -15744,10 +16225,10 @@ var init_LearningCanvas = __esm({
15744
16225
  }, [shapes]);
15745
16226
  const derivedShapes = useMemo(() => {
15746
16227
  if (!traces?.length && !readouts?.length) return shapes;
15747
- const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height));
15748
- const readoutOut = readouts?.length ? readoutShapes(readouts, width) : [];
16228
+ const traceOut = (traces ?? []).flatMap((panel, k) => traceShapes(panel, k, width, height, fontFamily));
16229
+ const readoutOut = readouts?.length ? readoutShapes(readouts, width, fontFamily) : [];
15749
16230
  return [...shapes, ...traceOut, ...readoutOut];
15750
- }, [shapes, traces, readouts, width, height]);
16231
+ }, [shapes, traces, readouts, width, height, fontFamily]);
15751
16232
  const draw = useCallback(() => {
15752
16233
  const _perfT = perfStart("learningcanvas:paint");
15753
16234
  const canvas = canvasRef.current;
@@ -15766,15 +16247,15 @@ var init_LearningCanvas = __esm({
15766
16247
  ctx.fillRect(0, 0, width, height);
15767
16248
  }
15768
16249
  for (const shape of derivedShapes) {
15769
- if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes);
16250
+ if (shape.type !== "text") drawShape(ctx, shape, width, height, derivedShapes, fontFamily);
15770
16251
  }
15771
16252
  for (const shape of derivedShapes) {
15772
- if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
16253
+ if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes, fontFamily);
15773
16254
  }
15774
16255
  if (drawables?.length && projector) {
15775
16256
  const painter = createWebPainter(ctx, invalidateRef.current);
15776
16257
  const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
15777
- const dctx = { projector, time: timeMs, invalidate: invalidateRef.current, fontFamily: themeBodyFont(canvas) };
16258
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current, fontFamily: fontFamily || themeBodyFont(canvas) };
15778
16259
  for (const node of drawables) {
15779
16260
  paintDrawable(painter, node, dctx);
15780
16261
  }
@@ -19546,6 +20027,250 @@ var init_EmptyState = __esm({
19546
20027
  EmptyState.displayName = "EmptyState";
19547
20028
  }
19548
20029
  });
20030
+
20031
+ // lib/editorMotions.ts
20032
+ function clamp(value, min, max) {
20033
+ return Math.max(min, Math.min(max, value));
20034
+ }
20035
+ function computeLines(text) {
20036
+ const lines = [];
20037
+ let start = 0;
20038
+ for (let i = 0; i <= text.length; i++) {
20039
+ if (i === text.length || text[i] === "\n") {
20040
+ lines.push({ start, end: i });
20041
+ start = i + 1;
20042
+ }
20043
+ }
20044
+ return lines;
20045
+ }
20046
+ function lineIndexAt(lines, pos) {
20047
+ for (let i = 0; i < lines.length; i++) {
20048
+ if (pos <= lines[i].end) return i;
20049
+ }
20050
+ return lines.length - 1;
20051
+ }
20052
+ function findWords(text) {
20053
+ const words = [];
20054
+ const re = /\w+|\S+/g;
20055
+ let m;
20056
+ while ((m = re.exec(text)) !== null) {
20057
+ words.push({ start: m.index, end: m.index + m[0].length });
20058
+ }
20059
+ return words;
20060
+ }
20061
+ function nextWordStart(text, pos) {
20062
+ for (const w of findWords(text)) {
20063
+ if (w.start > pos) return w.start;
20064
+ }
20065
+ return text.length;
20066
+ }
20067
+ function prevWordStart(text, pos) {
20068
+ let result = 0;
20069
+ for (const w of findWords(text)) {
20070
+ if (w.start < pos) result = w.start;
20071
+ else break;
20072
+ }
20073
+ return result;
20074
+ }
20075
+ function nextWordEnd(text, pos) {
20076
+ for (const w of findWords(text)) {
20077
+ const lastChar = w.end - 1;
20078
+ if (lastChar > pos) return lastChar;
20079
+ }
20080
+ return text.length > 0 ? text.length - 1 : 0;
20081
+ }
20082
+ function firstNonBlank(text, line) {
20083
+ let i = line.start;
20084
+ while (i < line.end && (text[i] === " " || text[i] === " ")) i++;
20085
+ return i;
20086
+ }
20087
+ function nextParagraphBoundary(lines, fromLineIdx, textLength) {
20088
+ for (let i = fromLineIdx + 1; i < lines.length; i++) {
20089
+ if (lines[i].start === lines[i].end) return lines[i].start;
20090
+ }
20091
+ return textLength;
20092
+ }
20093
+ function prevParagraphBoundary(lines, fromLineIdx) {
20094
+ for (let i = fromLineIdx - 1; i >= 0; i--) {
20095
+ if (lines[i].start === lines[i].end) return lines[i].start;
20096
+ }
20097
+ return 0;
20098
+ }
20099
+ function applyMotion(text, caret, motion, count) {
20100
+ const n = Math.max(1, count);
20101
+ const lines = computeLines(text);
20102
+ const lineIdx = lineIndexAt(lines, caret);
20103
+ const line = lines[lineIdx];
20104
+ switch (motion) {
20105
+ case "left":
20106
+ return clamp(caret - n, line.start, line.end);
20107
+ case "right":
20108
+ return clamp(caret + n, line.start, line.end);
20109
+ case "up": {
20110
+ const col = caret - line.start;
20111
+ const targetIdx = clamp(lineIdx - n, 0, lines.length - 1);
20112
+ const target = lines[targetIdx];
20113
+ return clamp(target.start + col, target.start, target.end);
20114
+ }
20115
+ case "down": {
20116
+ const col = caret - line.start;
20117
+ const targetIdx = clamp(lineIdx + n, 0, lines.length - 1);
20118
+ const target = lines[targetIdx];
20119
+ return clamp(target.start + col, target.start, target.end);
20120
+ }
20121
+ case "word-forward": {
20122
+ let pos = caret;
20123
+ for (let i = 0; i < n; i++) pos = nextWordStart(text, pos);
20124
+ return pos;
20125
+ }
20126
+ case "word-back": {
20127
+ let pos = caret;
20128
+ for (let i = 0; i < n; i++) pos = prevWordStart(text, pos);
20129
+ return pos;
20130
+ }
20131
+ case "word-end": {
20132
+ let pos = caret;
20133
+ for (let i = 0; i < n; i++) pos = nextWordEnd(text, pos);
20134
+ return pos;
20135
+ }
20136
+ case "line-start":
20137
+ return line.start;
20138
+ case "line-end":
20139
+ return line.end > line.start ? line.end - 1 : line.start;
20140
+ case "first-nonblank":
20141
+ return firstNonBlank(text, line);
20142
+ case "doc-start":
20143
+ return 0;
20144
+ case "doc-end":
20145
+ return text.length;
20146
+ case "paragraph-forward": {
20147
+ let pos = caret;
20148
+ for (let i = 0; i < n; i++) {
20149
+ pos = nextParagraphBoundary(lines, lineIndexAt(lines, pos), text.length);
20150
+ }
20151
+ return pos;
20152
+ }
20153
+ case "paragraph-back": {
20154
+ let pos = caret;
20155
+ for (let i = 0; i < n; i++) {
20156
+ pos = prevParagraphBoundary(lines, lineIndexAt(lines, pos));
20157
+ }
20158
+ return pos;
20159
+ }
20160
+ case "line":
20161
+ case "selection":
20162
+ return caret;
20163
+ default: {
20164
+ const _exhaustive = motion;
20165
+ return _exhaustive;
20166
+ }
20167
+ }
20168
+ }
20169
+ function motionRange(text, caret, motion, count, selection) {
20170
+ if (motion === "selection") {
20171
+ if (!selection) return [caret, caret];
20172
+ return [Math.min(selection[0], selection[1]), Math.max(selection[0], selection[1])];
20173
+ }
20174
+ const lines = computeLines(text);
20175
+ if (motion === "line") {
20176
+ const n = Math.max(1, count);
20177
+ const startIdx = lineIndexAt(lines, caret);
20178
+ const endIdx = clamp(startIdx + n - 1, 0, lines.length - 1);
20179
+ const start2 = lines[startIdx].start;
20180
+ const rawEnd = lines[endIdx].end;
20181
+ const end2 = rawEnd < text.length ? rawEnd + 1 : rawEnd;
20182
+ return [start2, end2];
20183
+ }
20184
+ const newCaret = applyMotion(text, caret, motion, count);
20185
+ let start = Math.min(caret, newCaret);
20186
+ let end = Math.max(caret, newCaret);
20187
+ if (motion === "word-end" || motion === "line-end") {
20188
+ end = Math.min(Math.max(start, newCaret) + 1, text.length);
20189
+ } else if (motion === "word-forward" && newCaret > caret) {
20190
+ const startLine = lineIndexAt(lines, caret);
20191
+ const endLine = lineIndexAt(lines, newCaret);
20192
+ if (endLine !== startLine) {
20193
+ end = lines[startLine].end;
20194
+ }
20195
+ }
20196
+ return [start, end];
20197
+ }
20198
+ function applyOperator(text, range, operator, register) {
20199
+ const start = clamp(range[0], 0, text.length);
20200
+ const end = clamp(range[1], start, text.length);
20201
+ const removed = text.slice(start, end);
20202
+ if (operator === "yank") {
20203
+ return { text, caret: start, register: removed };
20204
+ }
20205
+ return { text: text.slice(0, start) + text.slice(end), caret: start, register: removed };
20206
+ }
20207
+ var init_editorMotions = __esm({
20208
+ "lib/editorMotions.ts"() {
20209
+ }
20210
+ });
20211
+ function isMotionPayload(payload) {
20212
+ return !!payload && typeof payload.editorId === "string" && typeof payload.motion === "string" && typeof payload.count === "number";
20213
+ }
20214
+ function isOperatePayload(payload) {
20215
+ return !!payload && typeof payload.editorId === "string" && typeof payload.operator === "string" && typeof payload.motion === "string" && typeof payload.count === "number";
20216
+ }
20217
+ function isInsertTextPayload(payload) {
20218
+ return !!payload && typeof payload.editorId === "string" && typeof payload.text === "string";
20219
+ }
20220
+ function isSetModePayload(payload) {
20221
+ return !!payload && typeof payload.editorId === "string" && typeof payload.mode === "string" && typeof payload.caret === "string";
20222
+ }
20223
+ function useEditorCapabilities(args) {
20224
+ const [caretMode, setCaretMode] = useState("bar");
20225
+ const registerRef = useRef("");
20226
+ useEventListener(`UI:${args.events.onMotion}`, (evt) => {
20227
+ if (!args.editorId || !isMotionPayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
20228
+ const ta = args.textareaRef.current;
20229
+ if (!ta) return;
20230
+ const { motion, count } = evt.payload;
20231
+ const newCaret = applyMotion(ta.value, ta.selectionStart, motion, count);
20232
+ if (ta.selectionStart !== ta.selectionEnd) {
20233
+ ta.setSelectionRange(ta.selectionStart, newCaret);
20234
+ } else {
20235
+ ta.setSelectionRange(newCaret, newCaret);
20236
+ }
20237
+ });
20238
+ useEventListener(`UI:${args.events.onOperate}`, (evt) => {
20239
+ if (!args.editorId || !isOperatePayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
20240
+ const ta = args.textareaRef.current;
20241
+ if (!ta) return;
20242
+ const { operator, motion, count } = evt.payload;
20243
+ const selection = motion === "selection" ? [ta.selectionStart, ta.selectionEnd] : void 0;
20244
+ const range = motionRange(ta.value, ta.selectionStart, motion, count, selection);
20245
+ const result = applyOperator(ta.value, range, operator, registerRef.current);
20246
+ registerRef.current = result.register;
20247
+ if (operator === "yank") {
20248
+ ta.setSelectionRange(range[0], range[0]);
20249
+ } else {
20250
+ ta.setRangeText("", range[0], range[1], "end");
20251
+ }
20252
+ args.applyChange(ta.value);
20253
+ });
20254
+ useEventListener(`UI:${args.events.onInsertText}`, (evt) => {
20255
+ if (!args.editorId || !isInsertTextPayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
20256
+ const ta = args.textareaRef.current;
20257
+ if (!ta) return;
20258
+ ta.setRangeText(evt.payload.text, ta.selectionStart, ta.selectionEnd, "end");
20259
+ args.applyChange(ta.value);
20260
+ });
20261
+ useEventListener(`UI:${args.events.onSetMode}`, (evt) => {
20262
+ if (!args.editorId || !isSetModePayload(evt.payload) || evt.payload.editorId !== args.editorId) return;
20263
+ setCaretMode(evt.payload.caret);
20264
+ });
20265
+ return { caretMode };
20266
+ }
20267
+ var init_useEditorCapabilities = __esm({
20268
+ "components/core/molecules/markdown/useEditorCapabilities.ts"() {
20269
+ "use client";
20270
+ init_useEventBus();
20271
+ init_editorMotions();
20272
+ }
20273
+ });
19549
20274
  function isLanguageRegistered(lang) {
19550
20275
  return CODE_LANGUAGE_SET.has(lang) || dynamicallyLoaded.has(lang);
19551
20276
  }
@@ -19631,7 +20356,36 @@ function useLanguageReady(language) {
19631
20356
  }, [language]);
19632
20357
  return ready;
19633
20358
  }
19634
- 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;
20359
+ function resolveHighlightStyle(lang) {
20360
+ if (lang === "orb") return orbStyle;
20361
+ if (lang === "lolo") return loloStyle;
20362
+ return dark;
20363
+ }
20364
+ function plainCodeColorOf(style) {
20365
+ return style['code[class*="language-"]']?.color ?? "#d4d4d4";
20366
+ }
20367
+ function buildLineProps(errorLines, extraClassName) {
20368
+ return (lineNumber) => {
20369
+ const base = {
20370
+ "data-line": String(lineNumber - 1),
20371
+ ...extraClassName ? { className: extraClassName } : {}
20372
+ };
20373
+ const severity = errorLines?.get(lineNumber);
20374
+ if (!severity) return base;
20375
+ return {
20376
+ ...base,
20377
+ style: {
20378
+ display: "block",
20379
+ backgroundColor: severity === "error" ? "rgba(248, 113, 113, 0.18)" : "rgba(251, 191, 36, 0.18)",
20380
+ // amber-400 @ 18%
20381
+ borderLeft: `3px solid ${severity === "error" ? "#ef4444" : "#f59e0b"}`,
20382
+ paddingLeft: "0.5rem",
20383
+ marginLeft: "-0.5rem"
20384
+ }
20385
+ };
20386
+ };
20387
+ }
20388
+ 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;
19635
20389
  var init_CodeBlock = __esm({
19636
20390
  "components/core/molecules/markdown/CodeBlock.tsx"() {
19637
20391
  init_cn();
@@ -19647,6 +20401,7 @@ var init_CodeBlock = __esm({
19647
20401
  init_Textarea();
19648
20402
  init_Icon();
19649
20403
  init_useEventBus();
20404
+ init_useEditorCapabilities();
19650
20405
  SyntaxHighlighter.registerLanguage("json", langJson);
19651
20406
  SyntaxHighlighter.registerLanguage("javascript", langJavascript);
19652
20407
  SyntaxHighlighter.registerLanguage("js", langJavascript);
@@ -19865,6 +20620,15 @@ var init_CodeBlock = __esm({
19865
20620
  LINE_PROPS_FN = (n) => ({ "data-line": String(n - 1) });
19866
20621
  HIDDEN_LINE_NUMBERS = { display: "none" };
19867
20622
  HIGHLIGHT_CAPACITY_BYTES = 512 * 1024;
20623
+ MONO_FONT_FAMILY = 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace';
20624
+ VIEWER_LINE_NUMBER_STYLE = {
20625
+ minWidth: "2.5em",
20626
+ paddingRight: "1rem",
20627
+ textAlign: "right",
20628
+ userSelect: "none",
20629
+ opacity: 0.5,
20630
+ fontVariantNumeric: "tabular-nums"
20631
+ };
19868
20632
  CodeBlock = React96__default.memo(
19869
20633
  ({
19870
20634
  code: rawCode,
@@ -19889,15 +20653,39 @@ var init_CodeBlock = __esm({
19889
20653
  actions,
19890
20654
  isLoading = false,
19891
20655
  error,
19892
- showCopy
20656
+ showCopy,
20657
+ // editor capability surface — P1 wires these
20658
+ editorId,
20659
+ onEditorFocus = "EDITOR_FOCUS",
20660
+ onEditorBlur = "EDITOR_BLUR",
20661
+ onMotion = "MOTION",
20662
+ onOperate = "OPERATE",
20663
+ onInsertText = "INSERT_TEXT",
20664
+ onSetMode = "SET_MODE",
20665
+ motions = [
20666
+ "left",
20667
+ "right",
20668
+ "up",
20669
+ "down",
20670
+ "word-forward",
20671
+ "word-back",
20672
+ "word-end",
20673
+ "line-start",
20674
+ "line-end",
20675
+ "first-nonblank",
20676
+ "doc-start",
20677
+ "doc-end",
20678
+ "paragraph-forward",
20679
+ "paragraph-back",
20680
+ "line",
20681
+ "selection"
20682
+ ],
20683
+ operators = ["delete", "yank", "change"]
19893
20684
  }) => {
19894
20685
  const code = typeof rawCode === "string" ? rawCode : String(rawCode ?? "");
19895
- const isOrb = language === "orb";
19896
- const isLolo = language === "lolo";
19897
- const activeStyle = isOrb ? orbStyle : isLolo ? loloStyle : dark;
20686
+ const activeStyle = resolveHighlightStyle(language);
19898
20687
  const overCapacity = code.length > HIGHLIGHT_CAPACITY_BYTES;
19899
- const plainCodeColor = activeStyle['code[class*="language-"]']?.color ?? "#d4d4d4";
19900
- const languageReady = useLanguageReady(language);
20688
+ const plainCodeColor = plainCodeColorOf(activeStyle);
19901
20689
  const eventBus = useEventBus();
19902
20690
  const { t } = useTranslate();
19903
20691
  const scrollRef = useRef(null);
@@ -19909,6 +20697,9 @@ var init_CodeBlock = __esm({
19909
20697
  const activeFile = files?.[activeFileIndex];
19910
20698
  const activeCode = activeFile?.code ?? code;
19911
20699
  const activeLanguage = activeFile?.language ?? language;
20700
+ const languageReady = useLanguageReady(activeLanguage);
20701
+ const viewerStyle = resolveHighlightStyle(activeLanguage);
20702
+ const viewerPlainCodeColor = plainCodeColorOf(viewerStyle);
19912
20703
  const diffLines = useMemo(() => {
19913
20704
  if (propDiff) return propDiff;
19914
20705
  if (mode === "diff" && oldValue !== void 0 && newValue !== void 0) {
@@ -19938,28 +20729,28 @@ var init_CodeBlock = __esm({
19938
20729
  ov.scrollLeft = ta.scrollLeft;
19939
20730
  }
19940
20731
  }, []);
19941
- const errorLineProps = useMemo(() => {
19942
- if (!errorLines || errorLines.size === 0) {
19943
- return LINE_PROPS_FN;
19944
- }
19945
- return (lineNumber) => {
19946
- const severity = errorLines.get(lineNumber);
19947
- if (!severity) {
19948
- return { "data-line": String(lineNumber - 1) };
19949
- }
19950
- return {
19951
- "data-line": String(lineNumber - 1),
19952
- style: {
19953
- display: "block",
19954
- backgroundColor: severity === "error" ? "rgba(248, 113, 113, 0.18)" : "rgba(251, 191, 36, 0.18)",
19955
- // amber-400 @ 18%
19956
- borderLeft: `3px solid ${severity === "error" ? "#ef4444" : "#f59e0b"}`,
19957
- paddingLeft: "0.5rem",
19958
- marginLeft: "-0.5rem"
19959
- }
19960
- };
19961
- };
19962
- }, [errorLines]);
20732
+ const handleEditableChange = useCallback((v) => {
20733
+ lastPropCodeRef.current = v;
20734
+ setEditableValue(v);
20735
+ onChange?.(v);
20736
+ }, [onChange]);
20737
+ const { caretMode } = useEditorCapabilities({
20738
+ editorId: editable ? editorId : void 0,
20739
+ textareaRef: editableTextareaRef,
20740
+ events: { onMotion, onOperate, onInsertText, onSetMode },
20741
+ applyChange: handleEditableChange
20742
+ });
20743
+ const [caretIndex, setCaretIndex] = useState(0);
20744
+ const caretRowCol = useMemo(() => {
20745
+ const before = editableValue.slice(0, Math.min(caretIndex, editableValue.length));
20746
+ const lines = before.split("\n");
20747
+ return { row: lines.length - 1, col: lines[lines.length - 1].length };
20748
+ }, [editableValue, caretIndex]);
20749
+ const errorLineProps = useMemo(() => buildLineProps(errorLines), [errorLines]);
20750
+ const viewerLineProps = useMemo(
20751
+ () => buildLineProps(errorLines, "px-4 py-0.5 hover:bg-muted/50"),
20752
+ [errorLines]
20753
+ );
19963
20754
  const isFoldable = foldableProp ?? true;
19964
20755
  const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
19965
20756
  const foldRegions = useMemo(
@@ -20082,6 +20873,110 @@ var init_CodeBlock = __esm({
20082
20873
  ),
20083
20874
  [code, overCapacity, plainCodeColor, language, activeStyle, languageReady]
20084
20875
  );
20876
+ const viewerOverCapacity = activeCode.length > HIGHLIGHT_CAPACITY_BYTES;
20877
+ const viewerHighlightedElement = useMemo(
20878
+ () => viewerOverCapacity ? /* @__PURE__ */ jsx(
20879
+ "div",
20880
+ {
20881
+ className: "px-4 py-0.5",
20882
+ style: {
20883
+ margin: 0,
20884
+ whiteSpace: wrap ? "pre-wrap" : "pre",
20885
+ wordBreak: wrap ? "break-all" : "normal",
20886
+ color: viewerPlainCodeColor,
20887
+ fontFamily: MONO_FONT_FAMILY,
20888
+ fontSize: "12px",
20889
+ lineHeight: "1.6"
20890
+ },
20891
+ children: activeCode
20892
+ }
20893
+ ) : /* @__PURE__ */ jsx(
20894
+ SyntaxHighlighter,
20895
+ {
20896
+ PreTag: "div",
20897
+ language: activeLanguage,
20898
+ style: viewerStyle,
20899
+ wrapLines: true,
20900
+ wrapLongLines: wrap,
20901
+ showLineNumbers,
20902
+ lineNumberStyle: VIEWER_LINE_NUMBER_STYLE,
20903
+ lineProps: viewerLineProps,
20904
+ customStyle: {
20905
+ backgroundColor: "transparent",
20906
+ borderRadius: 0,
20907
+ padding: "0.25rem 0",
20908
+ margin: 0,
20909
+ whiteSpace: wrap ? "pre-wrap" : "pre",
20910
+ wordBreak: wrap ? "break-all" : "normal",
20911
+ fontFamily: MONO_FONT_FAMILY,
20912
+ fontSize: "12px",
20913
+ lineHeight: "1.6"
20914
+ },
20915
+ codeTagProps: { style: { fontFamily: MONO_FONT_FAMILY, fontSize: "12px", lineHeight: "1.6" } },
20916
+ children: activeCode
20917
+ }
20918
+ ),
20919
+ [activeCode, viewerOverCapacity, viewerPlainCodeColor, activeLanguage, viewerStyle, wrap, showLineNumbers, viewerLineProps, languageReady]
20920
+ );
20921
+ const diffOverCapacity = useMemo(
20922
+ () => !!diffLines && diffLines.reduce((n, l) => n + l.content.length + 1, 0) > HIGHLIGHT_CAPACITY_BYTES,
20923
+ [diffLines]
20924
+ );
20925
+ const diffRowElements = useMemo(() => {
20926
+ if (!diffLines) return null;
20927
+ return diffLines.map((line, idx) => {
20928
+ const style = DIFF_STYLES[line.type] ?? DIFF_STYLE_FALLBACK;
20929
+ return /* @__PURE__ */ jsxs(HStack, { gap: "none", align: "start", className: cn(style.bg, "px-4 py-0.5"), children: [
20930
+ showLineNumbers && /* @__PURE__ */ jsx(
20931
+ Typography,
20932
+ {
20933
+ variant: "caption",
20934
+ color: "secondary",
20935
+ className: "w-8 text-right mr-3 select-none tabular-nums flex-shrink-0",
20936
+ children: line.lineNumber ?? ""
20937
+ }
20938
+ ),
20939
+ /* @__PURE__ */ jsxs(
20940
+ Typography,
20941
+ {
20942
+ variant: "caption",
20943
+ className: cn("font-mono flex-1 min-w-0", style.text, wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
20944
+ children: [
20945
+ /* @__PURE__ */ jsx(Box, { as: "span", className: "select-none opacity-50 mr-2", children: style.prefix }),
20946
+ diffOverCapacity ? line.content || " " : /* @__PURE__ */ jsx(
20947
+ SyntaxHighlighter,
20948
+ {
20949
+ PreTag: "span",
20950
+ CodeTag: "span",
20951
+ language: activeLanguage,
20952
+ style: viewerStyle,
20953
+ customStyle: {
20954
+ display: "inline",
20955
+ background: "transparent",
20956
+ padding: 0,
20957
+ margin: 0,
20958
+ whiteSpace: wrap ? "pre-wrap" : "pre",
20959
+ wordBreak: wrap ? "break-all" : "normal",
20960
+ fontFamily: "inherit",
20961
+ fontSize: "inherit",
20962
+ lineHeight: "inherit"
20963
+ },
20964
+ codeTagProps: {
20965
+ style: {
20966
+ whiteSpace: wrap ? "pre-wrap" : "pre",
20967
+ fontFamily: "inherit",
20968
+ fontSize: "inherit"
20969
+ }
20970
+ },
20971
+ children: line.content || " "
20972
+ }
20973
+ )
20974
+ ]
20975
+ }
20976
+ )
20977
+ ] }, idx);
20978
+ });
20979
+ }, [diffLines, showLineNumbers, wrap, diffOverCapacity, activeLanguage, viewerStyle, languageReady]);
20085
20980
  useLayoutEffect(() => {
20086
20981
  const container = codeRef.current;
20087
20982
  if (!container) return;
@@ -20225,7 +21120,6 @@ var init_CodeBlock = __esm({
20225
21120
  label: file.label,
20226
21121
  content: null
20227
21122
  }));
20228
- const lines = activeCode.split("\n");
20229
21123
  return /* @__PURE__ */ jsx(Card, { className: cn("overflow-hidden", className), children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: [
20230
21124
  tabItems && tabItems.length > 1 && /* @__PURE__ */ jsx(Box, { className: "border-b border-border", children: /* @__PURE__ */ jsx(
20231
21125
  Tabs,
@@ -20288,49 +21182,7 @@ var init_CodeBlock = __esm({
20288
21182
  ]
20289
21183
  }
20290
21184
  ),
20291
- /* @__PURE__ */ jsx(Box, { className: "overflow-auto bg-muted/20", style: { maxHeight }, children: diffLines ? /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column" }, className: "font-mono text-xs", children: diffLines.map((line, idx) => {
20292
- const style = DIFF_STYLES[line.type] ?? DIFF_STYLE_FALLBACK;
20293
- return /* @__PURE__ */ jsxs(HStack, { gap: "none", align: "start", className: cn(style.bg, "px-4 py-0.5"), children: [
20294
- showLineNumbers && /* @__PURE__ */ jsx(
20295
- Typography,
20296
- {
20297
- variant: "caption",
20298
- color: "secondary",
20299
- className: "w-8 text-right mr-3 select-none tabular-nums flex-shrink-0",
20300
- children: line.lineNumber ?? ""
20301
- }
20302
- ),
20303
- /* @__PURE__ */ jsxs(
20304
- Typography,
20305
- {
20306
- variant: "caption",
20307
- className: cn("font-mono flex-1 min-w-0", style.text, wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
20308
- children: [
20309
- /* @__PURE__ */ jsx(Box, { as: "span", className: "select-none opacity-50 mr-2", children: style.prefix }),
20310
- line.content
20311
- ]
20312
- }
20313
- )
20314
- ] }, idx);
20315
- }) }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column" }, className: "font-mono text-xs", children: lines.map((line, idx) => /* @__PURE__ */ jsxs(HStack, { gap: "none", align: "start", className: "px-4 py-0.5 hover:bg-muted/50", children: [
20316
- showLineNumbers && /* @__PURE__ */ jsx(
20317
- Typography,
20318
- {
20319
- variant: "caption",
20320
- color: "secondary",
20321
- className: "w-8 text-right mr-4 select-none tabular-nums flex-shrink-0",
20322
- children: idx + 1
20323
- }
20324
- ),
20325
- /* @__PURE__ */ jsx(
20326
- Typography,
20327
- {
20328
- variant: "caption",
20329
- className: cn("font-mono flex-1 min-w-0", wrap ? "whitespace-pre-wrap break-all" : "whitespace-pre"),
20330
- children: line || " "
20331
- }
20332
- )
20333
- ] }, idx)) }) })
21185
+ /* @__PURE__ */ jsx(Box, { className: "overflow-auto bg-muted/20", style: { maxHeight }, children: diffLines ? /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column" }, className: "font-mono text-xs", children: diffRowElements }) : /* @__PURE__ */ jsx("div", { className: "font-mono text-xs", children: viewerHighlightedElement }) })
20334
21186
  ] }) });
20335
21187
  }
20336
21188
  const hasHeader = showLanguageBadge || effectiveCopy;
@@ -20417,13 +21269,11 @@ var init_CodeBlock = __esm({
20417
21269
  {
20418
21270
  ref: editableTextareaRef,
20419
21271
  defaultValue: code,
20420
- onChange: (e) => {
20421
- const v = e.target.value;
20422
- lastPropCodeRef.current = v;
20423
- setEditableValue(v);
20424
- onChange?.(v);
20425
- },
21272
+ onChange: (e) => handleEditableChange(e.target.value),
20426
21273
  onScroll: handleEditableScroll,
21274
+ onSelect: (e) => setCaretIndex(e.currentTarget.selectionStart),
21275
+ onFocus: editorId ? () => eventBus.emit(`UI:${onEditorFocus}`, { editorId }) : void 0,
21276
+ onBlur: editorId ? () => eventBus.emit(`UI:${onEditorBlur}`, { editorId }) : void 0,
20427
21277
  spellCheck: false,
20428
21278
  style: {
20429
21279
  position: "absolute",
@@ -20438,7 +21288,7 @@ var init_CodeBlock = __esm({
20438
21288
  resize: "none",
20439
21289
  backgroundColor: "transparent",
20440
21290
  color: "transparent",
20441
- caretColor: "#e6e6e6",
21291
+ caretColor: caretMode === "block" ? "transparent" : "#e6e6e6",
20442
21292
  WebkitTextFillColor: "transparent",
20443
21293
  fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Mono", "Courier New", monospace',
20444
21294
  fontSize: "13px",
@@ -20449,6 +21299,22 @@ var init_CodeBlock = __esm({
20449
21299
  }
20450
21300
  },
20451
21301
  editableTextareaKey
21302
+ ),
21303
+ caretMode !== "bar" && /* @__PURE__ */ jsx(
21304
+ "span",
21305
+ {
21306
+ "aria-hidden": true,
21307
+ style: {
21308
+ position: "absolute",
21309
+ top: `calc(1rem + ${caretRowCol.row * 19.5}px)`,
21310
+ left: `calc(1rem + ${caretRowCol.col}ch)`,
21311
+ width: "1ch",
21312
+ height: caretMode === "block" ? "19.5px" : "2px",
21313
+ backgroundColor: caretMode === "block" ? "rgba(230, 230, 230, 0.5)" : void 0,
21314
+ borderBottom: caretMode === "underline" ? "2px solid #e6e6e6" : void 0,
21315
+ pointerEvents: "none"
21316
+ }
21317
+ }
20452
21318
  )
20453
21319
  ]
20454
21320
  }
@@ -20476,55 +21342,175 @@ var init_CodeBlock = __esm({
20476
21342
  )
20477
21343
  ] });
20478
21344
  },
20479
- (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
21345
+ (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
20480
21346
  );
20481
21347
  CodeBlock.displayName = "CodeBlock";
20482
21348
  }
20483
21349
  });
21350
+
21351
+ // components/core/molecules/markdown/mermaidSource.ts
21352
+ function isQuoted(label) {
21353
+ const trimmed = label.trim();
21354
+ return trimmed.length === 0 || trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length > 1;
21355
+ }
21356
+ function quote(label) {
21357
+ return `"${label.replace(/"/g, "#quot;")}"`;
21358
+ }
21359
+ function isIdentifierChar(ch) {
21360
+ return /[A-Za-z0-9_\-.]/.test(ch);
21361
+ }
21362
+ function declaredType(code) {
21363
+ for (const line of code.split("\n")) {
21364
+ const trimmed = line.trim();
21365
+ if (trimmed.length === 0 || trimmed.startsWith("%%")) continue;
21366
+ return trimmed;
21367
+ }
21368
+ return "";
21369
+ }
21370
+ function isFlowchart(code) {
21371
+ return FLOWCHART_DIRECTIVE.test(declaredType(code));
21372
+ }
21373
+ function quoteNodeLabels(code) {
21374
+ let out = "";
21375
+ let i = 0;
21376
+ while (i < code.length) {
21377
+ const shape = NODE_SHAPES.find(([open2]) => code.startsWith(open2, i));
21378
+ const precededByIdentifier = i > 0 && isIdentifierChar(code[i - 1] ?? "");
21379
+ if (shape === void 0 || !precededByIdentifier) {
21380
+ out += code[i];
21381
+ i += 1;
21382
+ continue;
21383
+ }
21384
+ const [open, close] = shape;
21385
+ const contentStart = i + open.length;
21386
+ const closeAt = code.indexOf(close, contentStart);
21387
+ const newlineAt = code.indexOf("\n", contentStart);
21388
+ if (closeAt === -1 || newlineAt !== -1 && newlineAt < closeAt) {
21389
+ out += code[i];
21390
+ i += 1;
21391
+ continue;
21392
+ }
21393
+ const label = code.slice(contentStart, closeAt);
21394
+ out += open + (isQuoted(label) ? label : quote(label)) + close;
21395
+ i = closeAt + close.length;
21396
+ }
21397
+ return out;
21398
+ }
21399
+ function quoteEdgeLabels(code) {
21400
+ return code.split("\n").map((line) => {
21401
+ let out = "";
21402
+ let rest = line;
21403
+ for (; ; ) {
21404
+ const open = rest.indexOf("|");
21405
+ if (open === -1) break;
21406
+ const close = rest.indexOf("|", open + 1);
21407
+ if (close === -1) break;
21408
+ const label = rest.slice(open + 1, close);
21409
+ out += rest.slice(0, open + 1) + (isQuoted(label) ? label : quote(label)) + "|";
21410
+ rest = rest.slice(close + 1);
21411
+ }
21412
+ return out + rest;
21413
+ }).join("\n");
21414
+ }
21415
+ function quoteSubgraphTitles(code) {
21416
+ return code.split("\n").map((line) => {
21417
+ const match = /^(\s*subgraph\s+)(.+?)(\s*)$/.exec(line);
21418
+ if (match === null) return line;
21419
+ const [, prefix, title, trailing] = match;
21420
+ if (title === void 0 || prefix === void 0) return line;
21421
+ if (isQuoted(title) || title.includes("[")) return line;
21422
+ return prefix + quote(title) + (trailing ?? "");
21423
+ }).join("\n");
21424
+ }
21425
+ function mermaidRepairCandidates(code) {
21426
+ if (!isFlowchart(code)) return [];
21427
+ const nodes = quoteNodeLabels(code);
21428
+ const nodesAndEdges = quoteEdgeLabels(nodes);
21429
+ const all = quoteSubgraphTitles(nodesAndEdges);
21430
+ const ordered2 = [nodes, nodesAndEdges, all];
21431
+ const seen = /* @__PURE__ */ new Set([code]);
21432
+ const candidates = [];
21433
+ for (const candidate of ordered2) {
21434
+ if (seen.has(candidate)) continue;
21435
+ seen.add(candidate);
21436
+ candidates.push(candidate);
21437
+ }
21438
+ return candidates;
21439
+ }
21440
+ var NODE_SHAPES, FLOWCHART_DIRECTIVE;
21441
+ var init_mermaidSource = __esm({
21442
+ "components/core/molecules/markdown/mermaidSource.ts"() {
21443
+ NODE_SHAPES = [
21444
+ ["[[", "]]"],
21445
+ ["[(", ")]"],
21446
+ ["([", "])"],
21447
+ ["((", "))"],
21448
+ ["{{", "}}"],
21449
+ ["[", "]"],
21450
+ ["(", ")"],
21451
+ ["{", "}"]
21452
+ ];
21453
+ FLOWCHART_DIRECTIVE = /^(?:graph|flowchart)\b/;
21454
+ }
21455
+ });
20484
21456
  function loadMermaid() {
20485
21457
  mermaidModule ?? (mermaidModule = import('mermaid').then((m) => m.default));
20486
21458
  return mermaidModule;
20487
21459
  }
20488
- var mermaidModule, MermaidDiagram;
21460
+ var log5, mermaidModule, MermaidDiagram;
20489
21461
  var init_MermaidDiagram = __esm({
20490
21462
  "components/core/molecules/markdown/MermaidDiagram.tsx"() {
20491
21463
  init_Box();
20492
21464
  init_Typography();
20493
21465
  init_CodeBlock();
21466
+ init_mermaidSource();
20494
21467
  init_cn();
21468
+ log5 = createLogger("almadar:ui:mermaid-diagram");
20495
21469
  mermaidModule = null;
20496
21470
  MermaidDiagram = React96__default.memo(
20497
21471
  ({ code, className }) => {
20498
21472
  const { resolvedMode } = useTheme();
21473
+ const { t } = useTranslate();
20499
21474
  const containerRef = useRef(null);
20500
- const [error, setError] = useState(null);
21475
+ const [unrenderable, setUnrenderable] = useState(false);
20501
21476
  const reactId = useId();
20502
21477
  useEffect(() => {
20503
21478
  let active = true;
20504
21479
  void (async () => {
20505
- try {
20506
- const mermaid = await loadMermaid();
20507
- mermaid.initialize({
20508
- startOnLoad: false,
20509
- securityLevel: "strict",
20510
- theme: resolvedMode === "dark" ? "dark" : "default"
20511
- });
20512
- const domId = `mermaid-${reactId.replace(/[^a-zA-Z0-9]/g, "")}`;
20513
- const { svg } = await mermaid.render(domId, code);
20514
- if (!active || !containerRef.current) return;
20515
- containerRef.current.innerHTML = svg;
20516
- setError(null);
20517
- } catch (err) {
20518
- if (active) setError(err instanceof Error ? err.message : String(err));
21480
+ const mermaid = await loadMermaid();
21481
+ mermaid.initialize({
21482
+ startOnLoad: false,
21483
+ securityLevel: "strict",
21484
+ theme: resolvedMode === "dark" ? "dark" : "default"
21485
+ });
21486
+ const domId = `mermaid-${reactId.replace(/[^a-zA-Z0-9]/g, "")}`;
21487
+ let firstError = null;
21488
+ for (const [index, source] of [code, ...mermaidRepairCandidates(code)].entries()) {
21489
+ try {
21490
+ const { svg } = await mermaid.render(domId, source);
21491
+ if (!active) return;
21492
+ const container = containerRef.current;
21493
+ if (container === null) return;
21494
+ container.innerHTML = svg;
21495
+ container.dataset.mermaidRepaired = String(index > 0);
21496
+ setUnrenderable(false);
21497
+ if (index > 0) log5.debug("mermaid:repaired", { candidate: index });
21498
+ return;
21499
+ } catch (err) {
21500
+ firstError ?? (firstError = err instanceof Error ? err : new Error(String(err)));
21501
+ }
20519
21502
  }
21503
+ if (!active) return;
21504
+ log5.warn("mermaid:unrenderable", { error: firstError?.message ?? "", code });
21505
+ setUnrenderable(true);
20520
21506
  })();
20521
21507
  return () => {
20522
21508
  active = false;
20523
21509
  };
20524
21510
  }, [code, resolvedMode, reactId]);
20525
21511
  return /* @__PURE__ */ jsxs(Box, { className: cn("not-prose my-4", className), children: [
20526
- error !== null && /* @__PURE__ */ jsxs(Box, { className: "space-y-2 mb-2", children: [
20527
- /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-error whitespace-pre-wrap", children: error }),
21512
+ unrenderable && /* @__PURE__ */ jsxs(Box, { className: "space-y-2 mb-2", "data-testid": "mermaid-unrenderable", children: [
21513
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "text-muted-foreground", children: t("mermaid.unrenderable") }),
20528
21514
  /* @__PURE__ */ jsx(CodeBlock, { code, language: "mermaid" })
20529
21515
  ] }),
20530
21516
  /* @__PURE__ */ jsx(
@@ -20533,7 +21519,7 @@ var init_MermaidDiagram = __esm({
20533
21519
  ref: containerRef,
20534
21520
  "data-testid": "mermaid-diagram",
20535
21521
  className: "overflow-x-auto",
20536
- style: error !== null ? { display: "none" } : void 0
21522
+ style: unrenderable ? { display: "none" } : void 0
20537
21523
  }
20538
21524
  )
20539
21525
  ] });
@@ -23938,14 +24924,14 @@ function useSafeEventBus2() {
23938
24924
  } };
23939
24925
  }
23940
24926
  }
23941
- var log5, lookStyles4, ButtonGroup;
24927
+ var log6, lookStyles4, ButtonGroup;
23942
24928
  var init_ButtonGroup = __esm({
23943
24929
  "components/core/molecules/ButtonGroup.tsx"() {
23944
24930
  "use client";
23945
24931
  init_cn();
23946
24932
  init_atoms();
23947
24933
  init_useEventBus();
23948
- log5 = createLogger("almadar:ui:button-group");
24934
+ log6 = createLogger("almadar:ui:button-group");
23949
24935
  lookStyles4 = {
23950
24936
  "right-aligned-buttons": "",
23951
24937
  "floating-bar": "fixed bottom-section left-1/2 -translate-x-1/2 shadow-elevation-toast bg-card p-card-sm rounded-container",
@@ -24026,7 +25012,7 @@ var init_ButtonGroup = __esm({
24026
25012
  {
24027
25013
  variant: "ghost",
24028
25014
  onClick: () => {
24029
- log5.debug("Filter clicked", { field: filter.field });
25015
+ log6.debug("Filter clicked", { field: filter.field });
24030
25016
  },
24031
25017
  children: filter.label
24032
25018
  },
@@ -26810,6 +27796,12 @@ function commandMatches(command, query) {
26810
27796
  if (matchesQuery(query, command.label)) return true;
26811
27797
  return (command.keywords ?? []).some((keyword) => matchesQuery(query, keyword));
26812
27798
  }
27799
+ function dispatchCommandPaletteCommand(command, deps) {
27800
+ if (command.disabled) return;
27801
+ if (command.event) deps.emit(`UI:${command.event}`, { commandId: command.id });
27802
+ if (command.action) deps.emit(`UI:${command.action}`, command.actionPayload ?? {});
27803
+ deps.onSelect?.(command);
27804
+ }
26813
27805
  var UNGROUPED, CommandPalette;
26814
27806
  var init_CommandPalette = __esm({
26815
27807
  "components/core/molecules/CommandPalette.tsx"() {
@@ -26864,9 +27856,7 @@ var init_CommandPalette = __esm({
26864
27856
  const handleSelect = useCallback(
26865
27857
  (command) => {
26866
27858
  if (command.disabled) return;
26867
- if (command.event) eventBus.emit(`UI:${command.event}`, { commandId: command.id });
26868
- if (command.action) eventBus.emit(`UI:${command.action}`, command.actionPayload ?? {});
26869
- onSelect?.(command);
27859
+ dispatchCommandPaletteCommand(command, { emit: eventBus.emit, onSelect });
26870
27860
  handleClose();
26871
27861
  },
26872
27862
  [eventBus, onSelect, handleClose]
@@ -31845,9 +32835,9 @@ function debug(...args) {
31845
32835
  const [first, ...rest] = args;
31846
32836
  const message = typeof first === "string" ? first : "<debug>";
31847
32837
  if (rest.length === 0 && typeof first === "string") {
31848
- log6.debug(message);
32838
+ log7.debug(message);
31849
32839
  } else {
31850
- log6.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
32840
+ log7.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
31851
32841
  }
31852
32842
  }
31853
32843
  function debugGroup(label) {
@@ -31875,11 +32865,11 @@ function toLogMetaValue(v) {
31875
32865
  }
31876
32866
  return String(v);
31877
32867
  }
31878
- var NAMESPACE, log6;
32868
+ var NAMESPACE, log7;
31879
32869
  var init_debug = __esm({
31880
32870
  "lib/debug.ts"() {
31881
32871
  NAMESPACE = "almadar:ui:debug";
31882
- log6 = createLogger(NAMESPACE);
32872
+ log7 = createLogger(NAMESPACE);
31883
32873
  createLogger("almadar:ui:debug:input");
31884
32874
  createLogger("almadar:ui:debug:collision");
31885
32875
  createLogger("almadar:ui:debug:physics");
@@ -33532,6 +34522,7 @@ var init_MathCanvas = __esm({
33532
34522
  init_perf();
33533
34523
  init_atoms();
33534
34524
  init_Stack();
34525
+ init_gameFonts();
33535
34526
  init_LearningCanvas();
33536
34527
  MathCanvas = ({
33537
34528
  className,
@@ -33551,6 +34542,7 @@ var init_MathCanvas = __esm({
33551
34542
  showTickLabels = false,
33552
34543
  tickLabelFontSize = 10,
33553
34544
  labelFontSize = 12,
34545
+ fontFamily: fontFamilyProp,
33554
34546
  showCurveLabels = false,
33555
34547
  curves = [],
33556
34548
  points = [],
@@ -33573,6 +34565,7 @@ var init_MathCanvas = __esm({
33573
34565
  error
33574
34566
  }) => {
33575
34567
  const eventBus = useEventBus();
34568
+ const fontFamily = resolveGameFontFamily(fontFamilyProp);
33576
34569
  const keyMapKey = keyMap ? JSON.stringify(keyMap) : null;
33577
34570
  const keyUpMapKey = keyUpMap ? JSON.stringify(keyUpMap) : null;
33578
34571
  const stableKeyMap = useMemo(() => keyMap, [keyMapKey]);
@@ -33622,18 +34615,18 @@ var init_MathCanvas = __esm({
33622
34615
  let kx = 0;
33623
34616
  for (let x = Math.ceil(xMin / gridStep) * gridStep; x <= xMax; x += gridStep, kx++) {
33624
34617
  if (kx % labelEveryX === 0 && x !== 0) {
33625
- out.push({ type: "text", x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: tickLabelFontSize, align: "center" });
34618
+ out.push({ type: "text", fontFamily, x: mapX(x), y: xAxisY + 12, text: formatTick(x), color: "#6b7280", fontSize: tickLabelFontSize, align: "center" });
33626
34619
  }
33627
34620
  }
33628
34621
  const labelEveryY = Math.max(1, Math.ceil((yMax - yMin) / gridStep / Math.floor(plotH / 28)));
33629
34622
  let ky = 0;
33630
34623
  for (let y = Math.ceil(yMin / gridStep) * gridStep; y <= yMax; y += gridStep, ky++) {
33631
34624
  if (ky % labelEveryY === 0 && y !== 0) {
33632
- out.push({ type: "text", x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
34625
+ out.push({ type: "text", fontFamily, x: yAxisX - 6, y: mapY(y), text: formatTick(y), color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
33633
34626
  }
33634
34627
  }
33635
34628
  if (xMin <= 0 && xMax >= 0 && yMin <= 0 && yMax >= 0) {
33636
- out.push({ type: "text", x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
34629
+ out.push({ type: "text", fontFamily, x: yAxisX - 6, y: xAxisY + 12, text: "0", color: "#6b7280", fontSize: tickLabelFontSize, align: "right" });
33637
34630
  }
33638
34631
  }
33639
34632
  for (const region of regions) {
@@ -33660,6 +34653,7 @@ var init_MathCanvas = __esm({
33660
34653
  const mid = Math.floor(region.samples.length / 2);
33661
34654
  out.push({
33662
34655
  type: "text",
34656
+ fontFamily,
33663
34657
  x: mapX((first.x + last.x) / 2),
33664
34658
  y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
33665
34659
  text: region.label,
@@ -33696,14 +34690,14 @@ var init_MathCanvas = __esm({
33696
34690
  const px = mapX(guide.at);
33697
34691
  out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
33698
34692
  if (guide.label) {
33699
- out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color, fontSize: 11 });
34693
+ out.push({ type: "text", fontFamily, x: px + 4, y: margin + 10, text: guide.label, color, fontSize: 11 });
33700
34694
  }
33701
34695
  } else {
33702
34696
  if (guide.at < yMin || guide.at > yMax) continue;
33703
34697
  const py = mapY(guide.at);
33704
34698
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
33705
34699
  if (guide.label) {
33706
- out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color, fontSize: labelFontSize, align: "right" });
34700
+ out.push({ type: "text", fontFamily, x: width - margin - 4, y: py - 8, text: guide.label, color, fontSize: labelFontSize, align: "right" });
33707
34701
  }
33708
34702
  }
33709
34703
  }
@@ -33742,6 +34736,7 @@ var init_MathCanvas = __esm({
33742
34736
  if (showCurveLabels && curve.label && lastInRange) {
33743
34737
  out.push({
33744
34738
  type: "text",
34739
+ fontFamily,
33745
34740
  x: mapX(lastInRange.x) + 6,
33746
34741
  y: mapY(lastInRange.y) - 6,
33747
34742
  text: curve.label,
@@ -33779,6 +34774,7 @@ var init_MathCanvas = __esm({
33779
34774
  if (hop.label) {
33780
34775
  out.push({
33781
34776
  type: "text",
34777
+ fontFamily,
33782
34778
  x: (x1 + x2) / 2,
33783
34779
  y: xAxisY - peak - 8,
33784
34780
  text: hop.label,
@@ -33806,6 +34802,7 @@ var init_MathCanvas = __esm({
33806
34802
  const rad = mid * Math.PI / 180;
33807
34803
  out.push({
33808
34804
  type: "text",
34805
+ fontFamily,
33809
34806
  x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
33810
34807
  y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
33811
34808
  text: angle.label,
@@ -33827,7 +34824,7 @@ var init_MathCanvas = __esm({
33827
34824
  fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
33828
34825
  });
33829
34826
  if (p.label) {
33830
- out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: p.color ?? "#111827", fontSize: labelFontSize });
34827
+ out.push({ type: "text", fontFamily, x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: p.color ?? "#111827", fontSize: labelFontSize });
33831
34828
  }
33832
34829
  }
33833
34830
  for (const v of vectors) {
@@ -33838,7 +34835,7 @@ var init_MathCanvas = __esm({
33838
34835
  const y2 = mapY(v.y + v.vy);
33839
34836
  out.push({ type: "arrow", x1, y1, x2, y2, color: v.color ?? "#7c3aed", lineWidth: 2 });
33840
34837
  if (v.label) {
33841
- out.push({ type: "text", x: x2 + 6, y: y2 - 6, text: v.label, color: v.color ?? "#7c3aed", fontSize: labelFontSize });
34838
+ out.push({ type: "text", fontFamily, x: x2 + 6, y: y2 - 6, text: v.label, color: v.color ?? "#7c3aed", fontSize: labelFontSize });
33842
34839
  }
33843
34840
  }
33844
34841
  out.push(...shapes);
@@ -33859,6 +34856,7 @@ var init_MathCanvas = __esm({
33859
34856
  showTickLabels,
33860
34857
  tickLabelFontSize,
33861
34858
  labelFontSize,
34859
+ fontFamily,
33862
34860
  showCurveLabels,
33863
34861
  curves,
33864
34862
  points,
@@ -33914,6 +34912,7 @@ var init_MathCanvas = __esm({
33914
34912
  width,
33915
34913
  height,
33916
34914
  backgroundColor,
34915
+ fontFamily,
33917
34916
  shapes: derivedShapes,
33918
34917
  drawables,
33919
34918
  projector,
@@ -35146,13 +36145,13 @@ var init_MapView = __esm({
35146
36145
  shadowSize: [41, 41]
35147
36146
  });
35148
36147
  L.Marker.prototype.options.icon = defaultIcon;
35149
- const { useEffect: useEffect68, useRef: useRef67, useCallback: useCallback99, useState: useState107 } = React96__default;
36148
+ const { useEffect: useEffect70, useRef: useRef70, useCallback: useCallback100, useState: useState109 } = React96__default;
35150
36149
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
35151
36150
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
35152
36151
  function MapUpdater({ centerLat, centerLng, zoom }) {
35153
36152
  const map = useMap();
35154
- const prevRef = useRef67({ centerLat, centerLng, zoom });
35155
- useEffect68(() => {
36153
+ const prevRef = useRef70({ centerLat, centerLng, zoom });
36154
+ useEffect70(() => {
35156
36155
  const prev = prevRef.current;
35157
36156
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
35158
36157
  map.setView([centerLat, centerLng], zoom);
@@ -35163,7 +36162,7 @@ var init_MapView = __esm({
35163
36162
  }
35164
36163
  function MapClickHandler({ onMapClick }) {
35165
36164
  const map = useMap();
35166
- useEffect68(() => {
36165
+ useEffect70(() => {
35167
36166
  if (!onMapClick) return;
35168
36167
  const handler = (e) => {
35169
36168
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -35191,8 +36190,8 @@ var init_MapView = __esm({
35191
36190
  showAttribution = true
35192
36191
  }) {
35193
36192
  const eventBus = useEventBus2();
35194
- const [clickedPosition, setClickedPosition] = useState107(null);
35195
- const handleMapClick = useCallback99((lat, lng) => {
36193
+ const [clickedPosition, setClickedPosition] = useState109(null);
36194
+ const handleMapClick = useCallback100((lat, lng) => {
35196
36195
  if (showClickedPin) {
35197
36196
  setClickedPosition({ lat, lng });
35198
36197
  }
@@ -35201,7 +36200,7 @@ var init_MapView = __esm({
35201
36200
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
35202
36201
  }
35203
36202
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
35204
- const handleMarkerClick = useCallback99((marker) => {
36203
+ const handleMarkerClick = useCallback100((marker) => {
35205
36204
  onMarkerClick?.(marker);
35206
36205
  if (markerClickEvent) {
35207
36206
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -49510,7 +50509,7 @@ function getAllEvents(traits2) {
49510
50509
  function EventDispatcherTab({ traits: traits2, schema }) {
49511
50510
  const eventBus = useEventBus();
49512
50511
  const { t } = useTranslate();
49513
- const [log11, setLog] = React96.useState([]);
50512
+ const [log12, setLog] = React96.useState([]);
49514
50513
  const prevStatesRef = React96.useRef(/* @__PURE__ */ new Map());
49515
50514
  React96.useEffect(() => {
49516
50515
  for (const trait of traits2) {
@@ -49574,9 +50573,9 @@ function EventDispatcherTab({ traits: traits2, schema }) {
49574
50573
  /* @__PURE__ */ jsx(Typography, { variant: "small", weight: "medium", className: "text-muted-foreground mb-1", children: t("debug.otherEvents") }),
49575
50574
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: unavailableEvents.map((event) => /* @__PURE__ */ jsx(Badge, { variant: "default", size: "sm", className: "opacity-50", children: event }, event)) })
49576
50575
  ] }),
49577
- log11.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
50576
+ log12.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
49578
50577
  /* @__PURE__ */ jsx(Typography, { variant: "small", weight: "medium", className: "text-muted-foreground mb-1", children: t("debug.recentTransitions") }),
49579
- /* @__PURE__ */ jsx(Stack, { gap: "xs", children: log11.map((entry, i) => /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
50578
+ /* @__PURE__ */ jsx(Stack, { gap: "xs", children: log12.map((entry, i) => /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
49580
50579
  /* @__PURE__ */ jsx("span", { className: "text-primary", children: entry.traitName }),
49581
50580
  " ",
49582
50581
  /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: entry.from }),
@@ -51767,6 +52766,7 @@ var init_component_registry_generated = __esm({
51767
52766
  init_FormSection();
51768
52767
  init_FormSectionHeader();
51769
52768
  init_FxOverlay();
52769
+ init_GameAudioCue();
51770
52770
  init_GameAudioToggle();
51771
52771
  init_GameHud();
51772
52772
  init_GameIcon();
@@ -52044,6 +53044,7 @@ var init_component_registry_generated = __esm({
52044
53044
  "FormLayout": FormLayout,
52045
53045
  "FormSectionHeader": FormSectionHeader,
52046
53046
  "FxOverlay": FxOverlay,
53047
+ "GameAudioCue": GameAudioCue,
52047
53048
  "GameAudioToggle": GameAudioToggle,
52048
53049
  "GameHud": GameHud,
52049
53050
  "GameIcon": GameIcon,
@@ -56840,7 +57841,7 @@ function convertFnFormLambdasInProps(props) {
56840
57841
 
56841
57842
  // hooks/index.ts
56842
57843
  init_useEventBus();
56843
- var log7 = createLogger("almadar:ui:shared-entity-store");
57844
+ var log8 = createLogger("almadar:ui:shared-entity-store");
56844
57845
  var EMPTY_ENTITY_STATE = {};
56845
57846
  function createSharedEntityStore() {
56846
57847
  const states = /* @__PURE__ */ new Map();
@@ -56870,7 +57871,7 @@ function createSharedEntityStore() {
56870
57871
  try {
56871
57872
  callback();
56872
57873
  } catch (error) {
56873
- log7.error("Shared entity subscriber error", {
57874
+ log8.error("Shared entity subscriber error", {
56874
57875
  entityId,
56875
57876
  error: error instanceof Error ? error : String(error)
56876
57877
  });
@@ -56901,7 +57902,7 @@ function runTickFrame(entityId, orderedWriters, store) {
56901
57902
  store.commit(entityId, scratch);
56902
57903
  return scratch;
56903
57904
  }
56904
- var log8 = createLogger("almadar:ui:effects:client-handlers");
57905
+ var log9 = createLogger("almadar:ui:effects:client-handlers");
56905
57906
  function createClientEffectHandlers(options) {
56906
57907
  const { eventBus, slotSetter, navigate, navigateBack, notify, callService, liveEntity } = options;
56907
57908
  return {
@@ -56910,12 +57911,12 @@ function createClientEffectHandlers(options) {
56910
57911
  eventBus.emit(prefixedEvent, payload, source);
56911
57912
  },
56912
57913
  persist: async () => {
56913
- log8.warn("persist is server-side only, ignored on client");
57914
+ log9.warn("persist is server-side only, ignored on client");
56914
57915
  },
56915
57916
  // @almadar/runtime EffectHandlers.set types value:unknown — should be FieldValue (upstream fix queued)
56916
57917
  set: ((_entityId, field, value) => {
56917
57918
  if (!liveEntity) {
56918
- log8.warn("set is server-side only, ignored on client (no live entity)");
57919
+ log9.warn("set is server-side only, ignored on client (no live entity)");
56919
57920
  return;
56920
57921
  }
56921
57922
  liveEntity[field] = value;
@@ -56951,13 +57952,13 @@ function createClientEffectHandlers(options) {
56951
57952
  window.location.href = path;
56952
57953
  return;
56953
57954
  }
56954
- log8.warn("No navigate handler, ignoring", { path });
57955
+ log9.warn("No navigate handler, ignoring", { path });
56955
57956
  }),
56956
57957
  navigateBack: navigateBack ?? (() => {
56957
- log8.warn("No navigate-back handler, ignoring");
57958
+ log9.warn("No navigate-back handler, ignoring");
56958
57959
  }),
56959
57960
  notify: notify ?? ((msg, type) => {
56960
- log8.debug("notify", { type, message: msg });
57961
+ log9.debug("notify", { type, message: msg });
56961
57962
  })
56962
57963
  };
56963
57964
  }
@@ -57392,7 +58393,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57392
58393
  };
57393
58394
  }, [traitBindings]);
57394
58395
  const executeTransitionEffects = useCallback(async (params) => {
57395
- const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log11 } = params;
58396
+ const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log12 } = params;
57396
58397
  const traitName = binding.trait.name;
57397
58398
  const linkedEntity = binding.linkedEntity || "";
57398
58399
  const entityId = payload?.entityId;
@@ -57521,7 +58522,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57521
58522
  if (sharedKey !== void 0) {
57522
58523
  sharedWrites.push({ field, value });
57523
58524
  }
57524
- log11.debug("set:write", {
58525
+ log12.debug("set:write", {
57525
58526
  traitName,
57526
58527
  field,
57527
58528
  value: JSON.stringify(value),
@@ -57589,7 +58590,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57589
58590
  mergeEntityFrame(sharedEntityStore.getSnapshot(sharedKey), sharedWrites)
57590
58591
  );
57591
58592
  }
57592
- log11.debug("effects:executed", () => ({
58593
+ log12.debug("effects:executed", () => ({
57593
58594
  traitName,
57594
58595
  transition: `${previousState}->${newState}`,
57595
58596
  event: flushEvent,
@@ -57599,7 +58600,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57599
58600
  slotsTouched: Array.from(pendingSlots.keys()).join(",")
57600
58601
  }));
57601
58602
  for (const [slot, patterns] of pendingSlots) {
57602
- log11.debug("flush:slot", {
58603
+ log12.debug("flush:slot", {
57603
58604
  traitName,
57604
58605
  slot,
57605
58606
  patternCount: patterns.length,
@@ -57617,7 +58618,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57617
58618
  publishBindingSnapshot(traitName, liveEntity);
57618
58619
  }
57619
58620
  } catch (error) {
57620
- log11.error("effects:error", {
58621
+ log12.error("effects:error", {
57621
58622
  traitName,
57622
58623
  transition: `${previousState}->${newState}`,
57623
58624
  event: flushEvent,
@@ -58696,11 +59697,20 @@ function BrowserPlayground({
58696
59697
  mode = "mock",
58697
59698
  initialPagePath,
58698
59699
  height,
58699
- className
59700
+ className,
59701
+ paused
58700
59702
  }) {
58701
59703
  const [runtime] = useState(
58702
59704
  () => new OrbitalServerRuntime({ mode, debug: false })
58703
59705
  );
59706
+ useEffect(() => {
59707
+ if (paused === void 0) return;
59708
+ if (paused) {
59709
+ runtime.pauseTicks();
59710
+ } else {
59711
+ runtime.resumeTicks();
59712
+ }
59713
+ }, [runtime, paused]);
58704
59714
  const registrationReady = useMemo(() => {
58705
59715
  const orbitalNames = schema.orbitals.map((o) => o.name);
58706
59716
  playgroundLog.debug("register:start", { schema: schema.name, orbitalNames });
@@ -58761,7 +59771,7 @@ init_useEventBus();
58761
59771
  // components/avl/hooks/useCanvasDnd.tsx
58762
59772
  init_useEventBus();
58763
59773
  init_useAlmadarDndCollision();
58764
- var log9 = createLogger("almadar:ui:canvas-dnd");
59774
+ var log10 = createLogger("almadar:ui:canvas-dnd");
58765
59775
  function useCanvasDraggable({
58766
59776
  id,
58767
59777
  payload,
@@ -58799,7 +59809,7 @@ function defaultEmit(eventBus, drop) {
58799
59809
  if (payload.kind === "pattern") {
58800
59810
  const patternType = payload.data["type"];
58801
59811
  if (typeof patternType !== "string") {
58802
- log9.warn("default-emit:pattern:missing-type");
59812
+ log10.warn("default-emit:pattern:missing-type");
58803
59813
  return;
58804
59814
  }
58805
59815
  const out = { patternType, containerNode: target.containerNode };
@@ -58808,26 +59818,26 @@ function defaultEmit(eventBus, drop) {
58808
59818
  out.index = resolved.index;
58809
59819
  }
58810
59820
  eventBus.emit("UI:PATTERN_DROP", out);
58811
- log9.info("default-emit:pattern", { patternType, level: target.level });
59821
+ log10.info("default-emit:pattern", { patternType, level: target.level });
58812
59822
  return;
58813
59823
  }
58814
59824
  if (payload.kind === "behavior") {
58815
59825
  const behaviorName = payload.data["name"];
58816
59826
  if (typeof behaviorName !== "string") {
58817
- log9.warn("default-emit:behavior:missing-name");
59827
+ log10.warn("default-emit:behavior:missing-name");
58818
59828
  return;
58819
59829
  }
58820
59830
  eventBus.emit("UI:BEHAVIOR_DROP", {
58821
59831
  behaviorName,
58822
59832
  containerNode: target.containerNode
58823
59833
  });
58824
- log9.info("default-emit:behavior", { behaviorName, level: target.level });
59834
+ log10.info("default-emit:behavior", { behaviorName, level: target.level });
58825
59835
  return;
58826
59836
  }
58827
59837
  if (payload.kind === "pattern-instance") {
58828
59838
  const fromPath = payload.data["fromPath"];
58829
59839
  if (typeof fromPath !== "string") {
58830
- log9.warn("default-emit:pattern-instance:missing-fromPath");
59840
+ log10.warn("default-emit:pattern-instance:missing-fromPath");
58831
59841
  return;
58832
59842
  }
58833
59843
  const out = { fromPath, loc: payload.data["loc"] };
@@ -58836,10 +59846,10 @@ function defaultEmit(eventBus, drop) {
58836
59846
  out.toIndex = resolved.index;
58837
59847
  }
58838
59848
  eventBus.emit("UI:PATTERN_MOVE", out);
58839
- log9.info("default-emit:pattern-instance", { fromPath, level: target.level });
59849
+ log10.info("default-emit:pattern-instance", { fromPath, level: target.level });
58840
59850
  return;
58841
59851
  }
58842
- log9.debug("default-emit:unhandled-kind", { kind: payload.kind });
59852
+ log10.debug("default-emit:unhandled-kind", { kind: payload.kind });
58843
59853
  }
58844
59854
  function CanvasDndProvider({
58845
59855
  children,
@@ -58862,9 +59872,9 @@ function CanvasDndProvider({
58862
59872
  if (payload) {
58863
59873
  setActivePayload(payload);
58864
59874
  eventBus.emit("UI:DRAG_START", { kind: payload.kind, data: payload.data });
58865
- log9.info("dragStart", { id: e.active.id, kind: payload.kind });
59875
+ log10.info("dragStart", { id: e.active.id, kind: payload.kind });
58866
59876
  } else {
58867
- log9.warn("dragStart:missing-payload", { id: e.active.id });
59877
+ log10.warn("dragStart:missing-payload", { id: e.active.id });
58868
59878
  }
58869
59879
  }, [eventBus, trackPointer]);
58870
59880
  const handleDragEnd = React96__default.useCallback((e) => {
@@ -58875,7 +59885,7 @@ function CanvasDndProvider({
58875
59885
  const overData = e.over?.data.current;
58876
59886
  const target = overData?.target;
58877
59887
  const accepts = overData?.accepts;
58878
- log9.info("dragEnd", {
59888
+ log10.info("dragEnd", {
58879
59889
  activeId: e.active.id,
58880
59890
  overId: e.over?.id,
58881
59891
  hasPayload: !!payload,
@@ -58887,12 +59897,12 @@ function CanvasDndProvider({
58887
59897
  }
58888
59898
  if (!payload || !target) return;
58889
59899
  if (accepts && !accepts.includes(payload.kind)) {
58890
- log9.debug("dragEnd:rejected:kind", { kind: payload.kind, accepts: [...accepts] });
59900
+ log10.debug("dragEnd:rejected:kind", { kind: payload.kind, accepts: [...accepts] });
58891
59901
  return;
58892
59902
  }
58893
59903
  const cursor = lastPointerRef.current;
58894
59904
  const resolved = target.resolvePath && cursor ? target.resolvePath(cursor) : null;
58895
- log9.debug("dragEnd:resolve", {
59905
+ log10.debug("dragEnd:resolve", {
58896
59906
  hasResolver: !!target.resolvePath,
58897
59907
  cursorX: cursor?.x,
58898
59908
  cursorY: cursor?.y,
@@ -58906,7 +59916,7 @@ function CanvasDndProvider({
58906
59916
  const handleDragCancel = React96__default.useCallback(() => {
58907
59917
  setActivePayload(null);
58908
59918
  document.removeEventListener("pointermove", trackPointer);
58909
- log9.info("dragCancel");
59919
+ log10.info("dragCancel");
58910
59920
  }, [trackPointer]);
58911
59921
  return /* @__PURE__ */ jsxs(
58912
59922
  DndContext,
@@ -60966,7 +61976,7 @@ init_AvlTransitionLane();
60966
61976
  init_AvlSwimLane();
60967
61977
  init_avl_atom_types();
60968
61978
  init_avl_elk_layout();
60969
- var log10 = createLogger("almadar:ui:avl:trait-scene");
61979
+ var log11 = createLogger("almadar:ui:avl:trait-scene");
60970
61980
  var SWIM_GUTTER2 = 120;
60971
61981
  var CENTER_W2 = 360;
60972
61982
  var AvlTraitScene = ({
@@ -60979,7 +61989,7 @@ var AvlTraitScene = ({
60979
61989
  const dataKey = useMemo(() => JSON.stringify(data), [data]);
60980
61990
  useEffect(() => {
60981
61991
  computeTraitLayout(data).then(setLayout).catch((error) => {
60982
- log10.error("computeTraitLayout failed", { error: error instanceof Error ? error : String(error) });
61992
+ log11.error("computeTraitLayout failed", { error: error instanceof Error ? error : String(error) });
60983
61993
  });
60984
61994
  }, [dataKey]);
60985
61995
  if (!layout) {