@almadar/ui 6.0.0 → 6.2.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.
@@ -3,7 +3,7 @@ import React87__default, { createContext, useContext, useMemo, useRef, useEffect
3
3
  import { EventBusContext, useTraitScopeChain, RenderSlotProvider, useEntitySchemaOptional, useEntityBindingSnapshot, useTraitScope, TraitScopeProvider, useNavStack, useCurrentPagePath, useRenderSlot, useGameAudioContextOptional, matchPathAmong } from '@almadar/ui/providers';
4
4
  import { createLogger, isLogLevelEnabled } from '@almadar/logger';
5
5
  import { createContextFromBindings, interpolateValue } from '@almadar/runtime';
6
- import { ANONYMOUS_USER, isRenderBindingMarker, isFileValue, isInlineTrait, ANIMATION_NAMES } from '@almadar/core';
6
+ import { ANONYMOUS_USER, isRenderBindingMarker, isFileValue, isInlineTrait } from '@almadar/core';
7
7
  export { ANONYMOUS_USER } from '@almadar/core';
8
8
  import { clsx } from 'clsx';
9
9
  import { twMerge } from 'tailwind-merge';
@@ -3240,9 +3240,16 @@ var init_Input = __esm({
3240
3240
  }
3241
3241
  };
3242
3242
  const handleKeyDown = (e) => {
3243
- if (action && e.key === "Enter") {
3244
- eventBus.emit(`UI:${action}`, { value: e.currentTarget.value });
3243
+ if (!action || e.key !== "Enter") return;
3244
+ const raw = e.currentTarget.value;
3245
+ if (type === "number") {
3246
+ if (raw.trim() === "") return;
3247
+ const numeric = Number(raw);
3248
+ if (Number.isNaN(numeric)) return;
3249
+ eventBus.emit(`UI:${action}`, { value: numeric });
3250
+ return;
3245
3251
  }
3252
+ eventBus.emit(`UI:${action}`, { value: raw });
3246
3253
  };
3247
3254
  const wrapField = (field, fullWidth = true) => /* @__PURE__ */ jsxs("div", { className: fullWidth ? "w-full" : "w-fit", children: [
3248
3255
  label && /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-foreground mb-1", children: label }),
@@ -5800,6 +5807,7 @@ function resolveFxView(item, presets) {
5800
5807
  vy: item.vy ?? row.vy,
5801
5808
  vz: item.vz ?? row.vz,
5802
5809
  particleCount: item.particleCount ?? row.particleCount,
5810
+ animation: item.animation ?? row.animation,
5803
5811
  shape: row.shape,
5804
5812
  count: row.count,
5805
5813
  glow: row.glow,
@@ -7332,9 +7340,6 @@ var init_isometric = __esm({
7332
7340
  };
7333
7341
  }
7334
7342
  });
7335
- function isAnimationName(name) {
7336
- return ANIMATION_NAMES.includes(name);
7337
- }
7338
7343
  function frameRect(frame, row, columns, frameWidth, frameHeight) {
7339
7344
  return {
7340
7345
  sx: frame % columns * frameWidth,
@@ -9268,13 +9273,17 @@ var init_DrawSprite = __esm({
9268
9273
  return;
9269
9274
  }
9270
9275
  if (isSpriteSheetAtlas(atlas)) {
9271
- const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9276
+ const def = atlas.animations[node.animation];
9272
9277
  if (!def) {
9273
9278
  paintFallbackSquare(painter, node, dctx, "sprite-missing");
9274
9279
  return;
9275
9280
  }
9276
- const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9281
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, node.clockMs ?? dctx.time);
9277
9282
  const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9283
+ if (![r.sx, r.sy, r.sw, r.sh].every(Number.isFinite)) {
9284
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9285
+ return;
9286
+ }
9278
9287
  src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9279
9288
  }
9280
9289
  }
@@ -9822,7 +9831,10 @@ function expandFxItem(view, node, epochNowMs, dim) {
9822
9831
  anchor: "center",
9823
9832
  width: spriteSize,
9824
9833
  height: spriteSize,
9825
- opacity: fade
9834
+ opacity: fade,
9835
+ animation: view.animation ?? sprite.animations?.[0],
9836
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
9837
+ clockMs: ageMs
9826
9838
  });
9827
9839
  } else {
9828
9840
  out.push(...proceduralShapes(view, pos, fade, progress));
@@ -11669,6 +11681,7 @@ var init_GameShell = __esm({
11669
11681
  init_Typography();
11670
11682
  init_AtlasImage();
11671
11683
  GAME_FONTS = {
11684
+ fredoka: "Fredoka",
11672
11685
  future: "Kenney Future",
11673
11686
  "future-narrow": "Kenney Future Narrow",
11674
11687
  pixel: "Kenney Pixel",
@@ -11685,10 +11698,10 @@ var init_GameShell = __esm({
11685
11698
  showTopBar = true,
11686
11699
  children,
11687
11700
  backgroundAsset,
11688
- fontFamily = "future",
11701
+ fontFamily,
11689
11702
  "data-theme": dataTheme
11690
11703
  }) => {
11691
- const font = GAME_FONTS[fontFamily] ?? fontFamily;
11704
+ const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
11692
11705
  return /* @__PURE__ */ jsxs(
11693
11706
  Box,
11694
11707
  {
@@ -11703,8 +11716,11 @@ var init_GameShell = __esm({
11703
11716
  color: "var(--color-foreground, #e0e0e0)",
11704
11717
  // The fontFamily knob is a scoped override of the theme contract's
11705
11718
  // display slot: titles/numerics take the game face, body text keeps
11706
- // the active theme's --font-family-body.
11707
- "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif`
11719
+ // the active theme's --font-family-body. Set ONLY when explicitly
11720
+ // passed an always-on inline stamp would shadow the orbital's
11721
+ // inline-theme font-family-display token (inline style beats the
11722
+ // theme provider's vars for the whole shell subtree).
11723
+ ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
11708
11724
  },
11709
11725
  children: [
11710
11726
  backgroundAsset && /* @__PURE__ */ jsx(
@@ -12172,6 +12188,16 @@ function traceShapes(panel, k, width, height) {
12172
12188
  }
12173
12189
  return out;
12174
12190
  }
12191
+ function drawableNeedsAnimation(nodes) {
12192
+ if (!nodes) return false;
12193
+ return nodes.some((node) => {
12194
+ if (node.type === "draw-sprite") return node.animation !== void 0;
12195
+ if (node.type === "draw-fx-layer") return Array.isArray(node.items) && node.items.length > 0;
12196
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some((it) => it.animation !== void 0);
12197
+ if (node.type === "draw-group") return Array.isArray(node.items) && drawableNeedsAnimation(node.items);
12198
+ return false;
12199
+ });
12200
+ }
12175
12201
  var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
12176
12202
  var init_LearningCanvas = __esm({
12177
12203
  "components/learning/atoms/LearningCanvas.tsx"() {
@@ -12179,6 +12205,8 @@ var init_LearningCanvas = __esm({
12179
12205
  init_cn();
12180
12206
  init_perf();
12181
12207
  init_useEventBus();
12208
+ init_webPainter2d();
12209
+ init_paintDispatch();
12182
12210
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12183
12211
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
12184
12212
  LearningCanvas = ({
@@ -12187,6 +12215,8 @@ var init_LearningCanvas = __esm({
12187
12215
  height = 400,
12188
12216
  backgroundColor,
12189
12217
  shapes = [],
12218
+ drawables,
12219
+ projector,
12190
12220
  readouts,
12191
12221
  traces,
12192
12222
  interactive = false,
@@ -12200,6 +12230,9 @@ var init_LearningCanvas = __esm({
12200
12230
  const eventBus = useEventBus();
12201
12231
  const animRef = useRef(0);
12202
12232
  const hoverIndexRef = useRef(-1);
12233
+ const [drawVersion, setDrawVersion] = useState(0);
12234
+ const invalidateRef = useRef(() => setDrawVersion((v) => v + 1));
12235
+ const needsAnim = useMemo(() => drawableNeedsAnimation(drawables), [drawables]);
12203
12236
  const findShapeAt = useCallback((clientX, clientY) => {
12204
12237
  const canvas = canvasRef.current;
12205
12238
  if (!canvas) return -1;
@@ -12243,20 +12276,29 @@ var init_LearningCanvas = __esm({
12243
12276
  for (const shape of derivedShapes) {
12244
12277
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
12245
12278
  }
12279
+ if (drawables?.length && projector) {
12280
+ const painter = createWebPainter(ctx, invalidateRef.current);
12281
+ const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
12282
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current };
12283
+ for (const node of drawables) {
12284
+ paintDrawable(painter, node, dctx);
12285
+ }
12286
+ }
12246
12287
  perfEnd("learningcanvas:paint", _perfT);
12247
- }, [width, height, backgroundColor, derivedShapes]);
12288
+ }, [width, height, backgroundColor, derivedShapes, drawables, projector, needsAnim]);
12248
12289
  useEffect(() => {
12249
12290
  draw();
12250
- }, [draw]);
12291
+ }, [draw, drawVersion]);
12251
12292
  useEffect(() => {
12252
- if (!animate) return;
12293
+ const shouldAnimate = animate || needsAnim;
12294
+ if (!shouldAnimate) return;
12253
12295
  const loop = () => {
12254
12296
  draw();
12255
12297
  animRef.current = requestAnimationFrame(loop);
12256
12298
  };
12257
12299
  animRef.current = requestAnimationFrame(loop);
12258
12300
  return () => cancelAnimationFrame(animRef.current);
12259
- }, [animate, draw]);
12301
+ }, [animate, needsAnim, draw]);
12260
12302
  const handlePointerMove = useCallback(
12261
12303
  (e) => {
12262
12304
  if (!interactive) return;
@@ -24898,8 +24940,9 @@ var init_DashboardLayout = __esm({
24898
24940
  if (notificationClickEvent) eventBus.emit(`UI:${notificationClickEvent}`, {});
24899
24941
  if (onNotificationClick) onNotificationClick();
24900
24942
  };
24901
- const handleTopBarActionClick = (event) => {
24902
- eventBus.emit(`UI:${event}`, {});
24943
+ const handleTopBarActionClick = (action) => {
24944
+ if (action.event) eventBus.emit(`UI:${action.event}`, {});
24945
+ if (action.navigatesTo) eventBus.emit("UI:NAVIGATE", { url: action.navigatesTo });
24903
24946
  };
24904
24947
  const [sidebarOpen, setSidebarOpen] = useState(false);
24905
24948
  const [userMenuOpen, setUserMenuOpen] = useState(false);
@@ -25114,7 +25157,7 @@ var init_DashboardLayout = __esm({
25114
25157
  {
25115
25158
  variant: "ghost",
25116
25159
  className: "relative p-2 rounded-full hover:bg-muted dark:hover:bg-muted",
25117
- onClick: () => handleTopBarActionClick(action.event),
25160
+ onClick: () => handleTopBarActionClick(action),
25118
25161
  "aria-label": action.label ?? (typeof action.icon === "string" ? action.icon : void 0),
25119
25162
  children: [
25120
25163
  /* @__PURE__ */ jsx(Icon, { icon: action.icon, className: "h-5 w-5 text-muted-foreground dark:text-muted-foreground" }),
@@ -25131,7 +25174,7 @@ var init_DashboardLayout = __esm({
25131
25174
  )
25132
25175
  ]
25133
25176
  },
25134
- `${action.event}-${idx}`
25177
+ `${action.event ?? action.navigatesTo ?? "action"}-${idx}`
25135
25178
  )),
25136
25179
  notificationsEnabled && /* @__PURE__ */ jsxs(
25137
25180
  Button,
@@ -30793,6 +30836,7 @@ var init_MathCanvas = __esm({
30793
30836
  angles = [],
30794
30837
  hops = [],
30795
30838
  shapes = [],
30839
+ drawables,
30796
30840
  readouts,
30797
30841
  traces,
30798
30842
  interactive = false,
@@ -31099,6 +31143,42 @@ var init_MathCanvas = __esm({
31099
31143
  hops,
31100
31144
  shapes
31101
31145
  ]);
31146
+ const projector = useMemo(() => {
31147
+ if (!drawables?.length) return void 0;
31148
+ const margin = 24;
31149
+ const plotW = width - margin * 2;
31150
+ const plotH = height - margin * 2;
31151
+ const xScale = plotW / (xMax - xMin);
31152
+ const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
31153
+ const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
31154
+ const project = (pos) => ({ x: mapX(pos.x), y: mapY(pos.y) });
31155
+ const anchorPoint = (pos, anchor) => {
31156
+ const base = project(pos);
31157
+ if (anchor === "top-left") return base;
31158
+ const cx = base.x + xScale / 2;
31159
+ if (anchor === "ground") return { x: cx, y: base.y + xScale * 0.92 };
31160
+ return { x: cx, y: base.y + xScale / 2 };
31161
+ };
31162
+ const cellPath = (pos) => {
31163
+ const base = project(pos);
31164
+ return [
31165
+ { x: base.x, y: base.y },
31166
+ { x: base.x + xScale, y: base.y },
31167
+ { x: base.x + xScale, y: base.y + xScale },
31168
+ { x: base.x, y: base.y + xScale }
31169
+ ];
31170
+ };
31171
+ return {
31172
+ project,
31173
+ anchorPoint,
31174
+ cellPath,
31175
+ tileWidth: xScale,
31176
+ floorHeight: xScale / 2,
31177
+ diamondTopY: 0,
31178
+ squareGrid: true,
31179
+ worldPixelDirect: false
31180
+ };
31181
+ }, [drawables?.length, width, height, xMin, xMax, yMin, yMax]);
31102
31182
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
31103
31183
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
31104
31184
  /* @__PURE__ */ jsx(
@@ -31108,6 +31188,8 @@ var init_MathCanvas = __esm({
31108
31188
  height,
31109
31189
  backgroundColor,
31110
31190
  shapes: derivedShapes,
31191
+ drawables,
31192
+ projector,
31111
31193
  readouts,
31112
31194
  traces,
31113
31195
  interactive,
@@ -32337,7 +32419,7 @@ var init_MapView = __esm({
32337
32419
  shadowSize: [41, 41]
32338
32420
  });
32339
32421
  L.Marker.prototype.options.icon = defaultIcon;
32340
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState97 } = React87__default;
32422
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback97, useState: useState98 } = React87__default;
32341
32423
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
32342
32424
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
32343
32425
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -32382,7 +32464,7 @@ var init_MapView = __esm({
32382
32464
  showAttribution = true
32383
32465
  }) {
32384
32466
  const eventBus = useEventBus2();
32385
- const [clickedPosition, setClickedPosition] = useState97(null);
32467
+ const [clickedPosition, setClickedPosition] = useState98(null);
32386
32468
  const handleMapClick = useCallback97((lat, lng) => {
32387
32469
  if (showClickedPin) {
32388
32470
  setClickedPosition({ lat, lng });
@@ -3807,9 +3807,16 @@ var init_Input = __esm({
3807
3807
  }
3808
3808
  };
3809
3809
  const handleKeyDown = (e) => {
3810
- if (action && e.key === "Enter") {
3811
- eventBus.emit(`UI:${action}`, { value: e.currentTarget.value });
3810
+ if (!action || e.key !== "Enter") return;
3811
+ const raw = e.currentTarget.value;
3812
+ if (type === "number") {
3813
+ if (raw.trim() === "") return;
3814
+ const numeric = Number(raw);
3815
+ if (Number.isNaN(numeric)) return;
3816
+ eventBus.emit(`UI:${action}`, { value: numeric });
3817
+ return;
3812
3818
  }
3819
+ eventBus.emit(`UI:${action}`, { value: raw });
3813
3820
  };
3814
3821
  const wrapField = (field, fullWidth = true) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: fullWidth ? "w-full" : "w-fit", children: [
3815
3822
  label && /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-sm font-medium text-foreground mb-1", children: label }),
@@ -6332,6 +6339,7 @@ function resolveFxView(item, presets) {
6332
6339
  vy: item.vy ?? row.vy,
6333
6340
  vz: item.vz ?? row.vz,
6334
6341
  particleCount: item.particleCount ?? row.particleCount,
6342
+ animation: item.animation ?? row.animation,
6335
6343
  shape: row.shape,
6336
6344
  count: row.count,
6337
6345
  glow: row.glow,
@@ -7863,9 +7871,6 @@ var init_isometric = __esm({
7863
7871
  };
7864
7872
  }
7865
7873
  });
7866
- function isAnimationName(name) {
7867
- return core.ANIMATION_NAMES.includes(name);
7868
- }
7869
7874
  function frameRect(frame, row, columns, frameWidth, frameHeight) {
7870
7875
  return {
7871
7876
  sx: frame % columns * frameWidth,
@@ -9692,13 +9697,17 @@ var init_DrawSprite = __esm({
9692
9697
  return;
9693
9698
  }
9694
9699
  if (isSpriteSheetAtlas(atlas)) {
9695
- const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9700
+ const def = atlas.animations[node.animation];
9696
9701
  if (!def) {
9697
9702
  paintFallbackSquare(painter, node, dctx, "sprite-missing");
9698
9703
  return;
9699
9704
  }
9700
- const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9705
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, node.clockMs ?? dctx.time);
9701
9706
  const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9707
+ if (![r.sx, r.sy, r.sw, r.sh].every(Number.isFinite)) {
9708
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9709
+ return;
9710
+ }
9702
9711
  src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9703
9712
  }
9704
9713
  }
@@ -10246,7 +10255,10 @@ function expandFxItem(view, node, epochNowMs, dim) {
10246
10255
  anchor: "center",
10247
10256
  width: spriteSize,
10248
10257
  height: spriteSize,
10249
- opacity: fade
10258
+ opacity: fade,
10259
+ animation: view.animation ?? sprite.animations?.[0],
10260
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
10261
+ clockMs: ageMs
10250
10262
  });
10251
10263
  } else {
10252
10264
  out.push(...proceduralShapes(view, pos, fade, progress));
@@ -11826,6 +11838,7 @@ var init_GameShell = __esm({
11826
11838
  init_Typography();
11827
11839
  init_AtlasImage();
11828
11840
  GAME_FONTS = {
11841
+ fredoka: "Fredoka",
11829
11842
  future: "Kenney Future",
11830
11843
  "future-narrow": "Kenney Future Narrow",
11831
11844
  pixel: "Kenney Pixel",
@@ -11842,10 +11855,10 @@ var init_GameShell = __esm({
11842
11855
  showTopBar = true,
11843
11856
  children,
11844
11857
  backgroundAsset,
11845
- fontFamily = "future",
11858
+ fontFamily,
11846
11859
  "data-theme": dataTheme
11847
11860
  }) => {
11848
- const font = GAME_FONTS[fontFamily] ?? fontFamily;
11861
+ const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
11849
11862
  return /* @__PURE__ */ jsxRuntime.jsxs(
11850
11863
  Box,
11851
11864
  {
@@ -11860,8 +11873,11 @@ var init_GameShell = __esm({
11860
11873
  color: "var(--color-foreground, #e0e0e0)",
11861
11874
  // The fontFamily knob is a scoped override of the theme contract's
11862
11875
  // display slot: titles/numerics take the game face, body text keeps
11863
- // the active theme's --font-family-body.
11864
- "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif`
11876
+ // the active theme's --font-family-body. Set ONLY when explicitly
11877
+ // passed an always-on inline stamp would shadow the orbital's
11878
+ // inline-theme font-family-display token (inline style beats the
11879
+ // theme provider's vars for the whole shell subtree).
11880
+ ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
11865
11881
  },
11866
11882
  children: [
11867
11883
  backgroundAsset && /* @__PURE__ */ jsxRuntime.jsx(
@@ -12325,6 +12341,16 @@ function traceShapes(panel, k, width, height) {
12325
12341
  }
12326
12342
  return out;
12327
12343
  }
12344
+ function drawableNeedsAnimation(nodes) {
12345
+ if (!nodes) return false;
12346
+ return nodes.some((node) => {
12347
+ if (node.type === "draw-sprite") return node.animation !== void 0;
12348
+ if (node.type === "draw-fx-layer") return Array.isArray(node.items) && node.items.length > 0;
12349
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some((it) => it.animation !== void 0);
12350
+ if (node.type === "draw-group") return Array.isArray(node.items) && drawableNeedsAnimation(node.items);
12351
+ return false;
12352
+ });
12353
+ }
12328
12354
  var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
12329
12355
  var init_LearningCanvas = __esm({
12330
12356
  "components/learning/atoms/LearningCanvas.tsx"() {
@@ -12332,6 +12358,8 @@ var init_LearningCanvas = __esm({
12332
12358
  init_cn();
12333
12359
  init_perf();
12334
12360
  init_useEventBus();
12361
+ init_webPainter2d();
12362
+ init_paintDispatch();
12335
12363
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
12336
12364
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
12337
12365
  LearningCanvas = ({
@@ -12340,6 +12368,8 @@ var init_LearningCanvas = __esm({
12340
12368
  height = 400,
12341
12369
  backgroundColor,
12342
12370
  shapes = [],
12371
+ drawables,
12372
+ projector,
12343
12373
  readouts,
12344
12374
  traces,
12345
12375
  interactive = false,
@@ -12353,6 +12383,9 @@ var init_LearningCanvas = __esm({
12353
12383
  const eventBus = useEventBus();
12354
12384
  const animRef = React85.useRef(0);
12355
12385
  const hoverIndexRef = React85.useRef(-1);
12386
+ const [drawVersion, setDrawVersion] = React85.useState(0);
12387
+ const invalidateRef = React85.useRef(() => setDrawVersion((v) => v + 1));
12388
+ const needsAnim = React85.useMemo(() => drawableNeedsAnimation(drawables), [drawables]);
12356
12389
  const findShapeAt = React85.useCallback((clientX, clientY) => {
12357
12390
  const canvas = canvasRef.current;
12358
12391
  if (!canvas) return -1;
@@ -12396,20 +12429,29 @@ var init_LearningCanvas = __esm({
12396
12429
  for (const shape of derivedShapes) {
12397
12430
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
12398
12431
  }
12432
+ if (drawables?.length && projector) {
12433
+ const painter = createWebPainter(ctx, invalidateRef.current);
12434
+ const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
12435
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current };
12436
+ for (const node of drawables) {
12437
+ paintDrawable(painter, node, dctx);
12438
+ }
12439
+ }
12399
12440
  ui.perfEnd("learningcanvas:paint", _perfT);
12400
- }, [width, height, backgroundColor, derivedShapes]);
12441
+ }, [width, height, backgroundColor, derivedShapes, drawables, projector, needsAnim]);
12401
12442
  React85.useEffect(() => {
12402
12443
  draw();
12403
- }, [draw]);
12444
+ }, [draw, drawVersion]);
12404
12445
  React85.useEffect(() => {
12405
- if (!animate) return;
12446
+ const shouldAnimate = animate || needsAnim;
12447
+ if (!shouldAnimate) return;
12406
12448
  const loop = () => {
12407
12449
  draw();
12408
12450
  animRef.current = requestAnimationFrame(loop);
12409
12451
  };
12410
12452
  animRef.current = requestAnimationFrame(loop);
12411
12453
  return () => cancelAnimationFrame(animRef.current);
12412
- }, [animate, draw]);
12454
+ }, [animate, needsAnim, draw]);
12413
12455
  const handlePointerMove = React85.useCallback(
12414
12456
  (e) => {
12415
12457
  if (!interactive) return;
@@ -24537,8 +24579,9 @@ var init_DashboardLayout = __esm({
24537
24579
  if (notificationClickEvent) eventBus.emit(`UI:${notificationClickEvent}`, {});
24538
24580
  if (onNotificationClick) onNotificationClick();
24539
24581
  };
24540
- const handleTopBarActionClick = (event) => {
24541
- eventBus.emit(`UI:${event}`, {});
24582
+ const handleTopBarActionClick = (action) => {
24583
+ if (action.event) eventBus.emit(`UI:${action.event}`, {});
24584
+ if (action.navigatesTo) eventBus.emit("UI:NAVIGATE", { url: action.navigatesTo });
24542
24585
  };
24543
24586
  const [sidebarOpen, setSidebarOpen] = React85.useState(false);
24544
24587
  const [userMenuOpen, setUserMenuOpen] = React85.useState(false);
@@ -24753,7 +24796,7 @@ var init_DashboardLayout = __esm({
24753
24796
  {
24754
24797
  variant: "ghost",
24755
24798
  className: "relative p-2 rounded-full hover:bg-muted dark:hover:bg-muted",
24756
- onClick: () => handleTopBarActionClick(action.event),
24799
+ onClick: () => handleTopBarActionClick(action),
24757
24800
  "aria-label": action.label ?? (typeof action.icon === "string" ? action.icon : void 0),
24758
24801
  children: [
24759
24802
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: action.icon, className: "h-5 w-5 text-muted-foreground dark:text-muted-foreground" }),
@@ -24770,7 +24813,7 @@ var init_DashboardLayout = __esm({
24770
24813
  )
24771
24814
  ]
24772
24815
  },
24773
- `${action.event}-${idx}`
24816
+ `${action.event ?? action.navigatesTo ?? "action"}-${idx}`
24774
24817
  )),
24775
24818
  notificationsEnabled && /* @__PURE__ */ jsxRuntime.jsxs(
24776
24819
  Button,
@@ -30368,6 +30411,7 @@ var init_MathCanvas = __esm({
30368
30411
  angles = [],
30369
30412
  hops = [],
30370
30413
  shapes = [],
30414
+ drawables,
30371
30415
  readouts,
30372
30416
  traces,
30373
30417
  interactive = false,
@@ -30674,6 +30718,42 @@ var init_MathCanvas = __esm({
30674
30718
  hops,
30675
30719
  shapes
30676
30720
  ]);
30721
+ const projector = React85.useMemo(() => {
30722
+ if (!drawables?.length) return void 0;
30723
+ const margin = 24;
30724
+ const plotW = width - margin * 2;
30725
+ const plotH = height - margin * 2;
30726
+ const xScale = plotW / (xMax - xMin);
30727
+ const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
30728
+ const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
30729
+ const project = (pos) => ({ x: mapX(pos.x), y: mapY(pos.y) });
30730
+ const anchorPoint = (pos, anchor) => {
30731
+ const base = project(pos);
30732
+ if (anchor === "top-left") return base;
30733
+ const cx = base.x + xScale / 2;
30734
+ if (anchor === "ground") return { x: cx, y: base.y + xScale * 0.92 };
30735
+ return { x: cx, y: base.y + xScale / 2 };
30736
+ };
30737
+ const cellPath = (pos) => {
30738
+ const base = project(pos);
30739
+ return [
30740
+ { x: base.x, y: base.y },
30741
+ { x: base.x + xScale, y: base.y },
30742
+ { x: base.x + xScale, y: base.y + xScale },
30743
+ { x: base.x, y: base.y + xScale }
30744
+ ];
30745
+ };
30746
+ return {
30747
+ project,
30748
+ anchorPoint,
30749
+ cellPath,
30750
+ tileWidth: xScale,
30751
+ floorHeight: xScale / 2,
30752
+ diamondTopY: 0,
30753
+ squareGrid: true,
30754
+ worldPixelDirect: false
30755
+ };
30756
+ }, [drawables?.length, width, height, xMin, xMax, yMin, yMax]);
30677
30757
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
30678
30758
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
30679
30759
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -30683,6 +30763,8 @@ var init_MathCanvas = __esm({
30683
30763
  height,
30684
30764
  backgroundColor,
30685
30765
  shapes: derivedShapes,
30766
+ drawables,
30767
+ projector,
30686
30768
  readouts,
30687
30769
  traces,
30688
30770
  interactive,
@@ -31912,7 +31994,7 @@ var init_MapView = __esm({
31912
31994
  shadowSize: [41, 41]
31913
31995
  });
31914
31996
  L.Marker.prototype.options.icon = defaultIcon;
31915
- const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback90, useState: useState94 } = React85__namespace.default;
31997
+ const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback90, useState: useState95 } = React85__namespace.default;
31916
31998
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31917
31999
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31918
32000
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -31957,7 +32039,7 @@ var init_MapView = __esm({
31957
32039
  showAttribution = true
31958
32040
  }) {
31959
32041
  const eventBus = useEventBus2();
31960
- const [clickedPosition, setClickedPosition] = useState94(null);
32042
+ const [clickedPosition, setClickedPosition] = useState95(null);
31961
32043
  const handleMapClick = useCallback90((lat, lng) => {
31962
32044
  if (showClickedPin) {
31963
32045
  setClickedPosition({ lat, lng });