@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.
@@ -7142,9 +7142,16 @@ var init_Input = __esm({
7142
7142
  }
7143
7143
  };
7144
7144
  const handleKeyDown = (e) => {
7145
- if (action && e.key === "Enter") {
7146
- eventBus.emit(`UI:${action}`, { value: e.currentTarget.value });
7145
+ if (!action || e.key !== "Enter") return;
7146
+ const raw = e.currentTarget.value;
7147
+ if (type === "number") {
7148
+ if (raw.trim() === "") return;
7149
+ const numeric = Number(raw);
7150
+ if (Number.isNaN(numeric)) return;
7151
+ eventBus.emit(`UI:${action}`, { value: numeric });
7152
+ return;
7147
7153
  }
7154
+ eventBus.emit(`UI:${action}`, { value: raw });
7148
7155
  };
7149
7156
  const wrapField = (field, fullWidth = true) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: fullWidth ? "w-full" : "w-fit", children: [
7150
7157
  label && /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-sm font-medium text-foreground mb-1", children: label }),
@@ -9617,6 +9624,7 @@ function resolveFxView(item, presets) {
9617
9624
  vy: item.vy ?? row.vy,
9618
9625
  vz: item.vz ?? row.vz,
9619
9626
  particleCount: item.particleCount ?? row.particleCount,
9627
+ animation: item.animation ?? row.animation,
9620
9628
  shape: row.shape,
9621
9629
  count: row.count,
9622
9630
  glow: row.glow,
@@ -11148,9 +11156,6 @@ var init_isometric = __esm({
11148
11156
  };
11149
11157
  }
11150
11158
  });
11151
- function isAnimationName(name) {
11152
- return core.ANIMATION_NAMES.includes(name);
11153
- }
11154
11159
  function frameRect(frame, row, columns, frameWidth, frameHeight) {
11155
11160
  return {
11156
11161
  sx: frame % columns * frameWidth,
@@ -13084,13 +13089,17 @@ var init_DrawSprite = __esm({
13084
13089
  return;
13085
13090
  }
13086
13091
  if (isSpriteSheetAtlas(atlas)) {
13087
- const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
13092
+ const def = atlas.animations[node.animation];
13088
13093
  if (!def) {
13089
13094
  paintFallbackSquare(painter, node, dctx, "sprite-missing");
13090
13095
  return;
13091
13096
  }
13092
- const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
13097
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, node.clockMs ?? dctx.time);
13093
13098
  const r2 = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
13099
+ if (![r2.sx, r2.sy, r2.sw, r2.sh].every(Number.isFinite)) {
13100
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
13101
+ return;
13102
+ }
13094
13103
  src = { x: r2.sx, y: r2.sy, w: r2.sw, h: r2.sh };
13095
13104
  }
13096
13105
  }
@@ -13638,7 +13647,10 @@ function expandFxItem(view, node, epochNowMs, dim) {
13638
13647
  anchor: "center",
13639
13648
  width: spriteSize,
13640
13649
  height: spriteSize,
13641
- opacity: fade
13650
+ opacity: fade,
13651
+ animation: view.animation ?? sprite.animations?.[0],
13652
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
13653
+ clockMs: ageMs
13642
13654
  });
13643
13655
  } else {
13644
13656
  out.push(...proceduralShapes(view, pos, fade, progress));
@@ -15218,6 +15230,7 @@ var init_GameShell = __esm({
15218
15230
  init_Typography();
15219
15231
  init_AtlasImage();
15220
15232
  GAME_FONTS = {
15233
+ fredoka: "Fredoka",
15221
15234
  future: "Kenney Future",
15222
15235
  "future-narrow": "Kenney Future Narrow",
15223
15236
  pixel: "Kenney Pixel",
@@ -15234,10 +15247,10 @@ var init_GameShell = __esm({
15234
15247
  showTopBar = true,
15235
15248
  children,
15236
15249
  backgroundAsset,
15237
- fontFamily = "future",
15250
+ fontFamily,
15238
15251
  "data-theme": dataTheme
15239
15252
  }) => {
15240
- const font = GAME_FONTS[fontFamily] ?? fontFamily;
15253
+ const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
15241
15254
  return /* @__PURE__ */ jsxRuntime.jsxs(
15242
15255
  Box,
15243
15256
  {
@@ -15252,8 +15265,11 @@ var init_GameShell = __esm({
15252
15265
  color: "var(--color-foreground, #e0e0e0)",
15253
15266
  // The fontFamily knob is a scoped override of the theme contract's
15254
15267
  // display slot: titles/numerics take the game face, body text keeps
15255
- // the active theme's --font-family-body.
15256
- "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif`
15268
+ // the active theme's --font-family-body. Set ONLY when explicitly
15269
+ // passed an always-on inline stamp would shadow the orbital's
15270
+ // inline-theme font-family-display token (inline style beats the
15271
+ // theme provider's vars for the whole shell subtree).
15272
+ ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
15257
15273
  },
15258
15274
  children: [
15259
15275
  backgroundAsset && /* @__PURE__ */ jsxRuntime.jsx(
@@ -15730,6 +15746,16 @@ function traceShapes(panel, k, width, height) {
15730
15746
  }
15731
15747
  return out;
15732
15748
  }
15749
+ function drawableNeedsAnimation(nodes) {
15750
+ if (!nodes) return false;
15751
+ return nodes.some((node) => {
15752
+ if (node.type === "draw-sprite") return node.animation !== void 0;
15753
+ if (node.type === "draw-fx-layer") return Array.isArray(node.items) && node.items.length > 0;
15754
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some((it) => it.animation !== void 0);
15755
+ if (node.type === "draw-group") return Array.isArray(node.items) && drawableNeedsAnimation(node.items);
15756
+ return false;
15757
+ });
15758
+ }
15733
15759
  var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
15734
15760
  var init_LearningCanvas = __esm({
15735
15761
  "components/learning/atoms/LearningCanvas.tsx"() {
@@ -15737,6 +15763,8 @@ var init_LearningCanvas = __esm({
15737
15763
  init_cn();
15738
15764
  init_perf();
15739
15765
  init_useEventBus();
15766
+ init_webPainter2d();
15767
+ init_paintDispatch();
15740
15768
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15741
15769
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15742
15770
  LearningCanvas = ({
@@ -15745,6 +15773,8 @@ var init_LearningCanvas = __esm({
15745
15773
  height = 400,
15746
15774
  backgroundColor,
15747
15775
  shapes = [],
15776
+ drawables,
15777
+ projector,
15748
15778
  readouts,
15749
15779
  traces,
15750
15780
  interactive = false,
@@ -15758,6 +15788,9 @@ var init_LearningCanvas = __esm({
15758
15788
  const eventBus = useEventBus();
15759
15789
  const animRef = React94.useRef(0);
15760
15790
  const hoverIndexRef = React94.useRef(-1);
15791
+ const [drawVersion, setDrawVersion] = React94.useState(0);
15792
+ const invalidateRef = React94.useRef(() => setDrawVersion((v) => v + 1));
15793
+ const needsAnim = React94.useMemo(() => drawableNeedsAnimation(drawables), [drawables]);
15761
15794
  const findShapeAt = React94.useCallback((clientX, clientY) => {
15762
15795
  const canvas = canvasRef.current;
15763
15796
  if (!canvas) return -1;
@@ -15801,20 +15834,29 @@ var init_LearningCanvas = __esm({
15801
15834
  for (const shape of derivedShapes) {
15802
15835
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
15803
15836
  }
15837
+ if (drawables?.length && projector) {
15838
+ const painter = createWebPainter(ctx, invalidateRef.current);
15839
+ const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
15840
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current };
15841
+ for (const node of drawables) {
15842
+ paintDrawable(painter, node, dctx);
15843
+ }
15844
+ }
15804
15845
  ui.perfEnd("learningcanvas:paint", _perfT);
15805
- }, [width, height, backgroundColor, derivedShapes]);
15846
+ }, [width, height, backgroundColor, derivedShapes, drawables, projector, needsAnim]);
15806
15847
  React94.useEffect(() => {
15807
15848
  draw();
15808
- }, [draw]);
15849
+ }, [draw, drawVersion]);
15809
15850
  React94.useEffect(() => {
15810
- if (!animate) return;
15851
+ const shouldAnimate = animate || needsAnim;
15852
+ if (!shouldAnimate) return;
15811
15853
  const loop = () => {
15812
15854
  draw();
15813
15855
  animRef.current = requestAnimationFrame(loop);
15814
15856
  };
15815
15857
  animRef.current = requestAnimationFrame(loop);
15816
15858
  return () => cancelAnimationFrame(animRef.current);
15817
- }, [animate, draw]);
15859
+ }, [animate, needsAnim, draw]);
15818
15860
  const handlePointerMove = React94.useCallback(
15819
15861
  (e) => {
15820
15862
  if (!interactive) return;
@@ -27246,8 +27288,9 @@ var init_DashboardLayout = __esm({
27246
27288
  if (notificationClickEvent) eventBus.emit(`UI:${notificationClickEvent}`, {});
27247
27289
  if (onNotificationClick) onNotificationClick();
27248
27290
  };
27249
- const handleTopBarActionClick = (event) => {
27250
- eventBus.emit(`UI:${event}`, {});
27291
+ const handleTopBarActionClick = (action) => {
27292
+ if (action.event) eventBus.emit(`UI:${action.event}`, {});
27293
+ if (action.navigatesTo) eventBus.emit("UI:NAVIGATE", { url: action.navigatesTo });
27251
27294
  };
27252
27295
  const [sidebarOpen, setSidebarOpen] = React94.useState(false);
27253
27296
  const [userMenuOpen, setUserMenuOpen] = React94.useState(false);
@@ -27462,7 +27505,7 @@ var init_DashboardLayout = __esm({
27462
27505
  {
27463
27506
  variant: "ghost",
27464
27507
  className: "relative p-2 rounded-full hover:bg-muted dark:hover:bg-muted",
27465
- onClick: () => handleTopBarActionClick(action.event),
27508
+ onClick: () => handleTopBarActionClick(action),
27466
27509
  "aria-label": action.label ?? (typeof action.icon === "string" ? action.icon : void 0),
27467
27510
  children: [
27468
27511
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { icon: action.icon, className: "h-5 w-5 text-muted-foreground dark:text-muted-foreground" }),
@@ -27479,7 +27522,7 @@ var init_DashboardLayout = __esm({
27479
27522
  )
27480
27523
  ]
27481
27524
  },
27482
- `${action.event}-${idx}`
27525
+ `${action.event ?? action.navigatesTo ?? "action"}-${idx}`
27483
27526
  )),
27484
27527
  notificationsEnabled && /* @__PURE__ */ jsxRuntime.jsxs(
27485
27528
  Button,
@@ -33141,6 +33184,7 @@ var init_MathCanvas = __esm({
33141
33184
  angles = [],
33142
33185
  hops = [],
33143
33186
  shapes = [],
33187
+ drawables,
33144
33188
  readouts,
33145
33189
  traces,
33146
33190
  interactive = false,
@@ -33447,6 +33491,42 @@ var init_MathCanvas = __esm({
33447
33491
  hops,
33448
33492
  shapes
33449
33493
  ]);
33494
+ const projector = React94.useMemo(() => {
33495
+ if (!drawables?.length) return void 0;
33496
+ const margin = 24;
33497
+ const plotW = width - margin * 2;
33498
+ const plotH = height - margin * 2;
33499
+ const xScale = plotW / (xMax - xMin);
33500
+ const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
33501
+ const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
33502
+ const project = (pos) => ({ x: mapX(pos.x), y: mapY(pos.y) });
33503
+ const anchorPoint = (pos, anchor) => {
33504
+ const base = project(pos);
33505
+ if (anchor === "top-left") return base;
33506
+ const cx = base.x + xScale / 2;
33507
+ if (anchor === "ground") return { x: cx, y: base.y + xScale * 0.92 };
33508
+ return { x: cx, y: base.y + xScale / 2 };
33509
+ };
33510
+ const cellPath = (pos) => {
33511
+ const base = project(pos);
33512
+ return [
33513
+ { x: base.x, y: base.y },
33514
+ { x: base.x + xScale, y: base.y },
33515
+ { x: base.x + xScale, y: base.y + xScale },
33516
+ { x: base.x, y: base.y + xScale }
33517
+ ];
33518
+ };
33519
+ return {
33520
+ project,
33521
+ anchorPoint,
33522
+ cellPath,
33523
+ tileWidth: xScale,
33524
+ floorHeight: xScale / 2,
33525
+ diamondTopY: 0,
33526
+ squareGrid: true,
33527
+ worldPixelDirect: false
33528
+ };
33529
+ }, [drawables?.length, width, height, xMin, xMax, yMin, yMax]);
33450
33530
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { className, children: /* @__PURE__ */ jsxRuntime.jsxs(VStack, { gap: "sm", children: [
33451
33531
  title ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "h4", children: title }) : null,
33452
33532
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -33456,6 +33536,8 @@ var init_MathCanvas = __esm({
33456
33536
  height,
33457
33537
  backgroundColor,
33458
33538
  shapes: derivedShapes,
33539
+ drawables,
33540
+ projector,
33459
33541
  readouts,
33460
33542
  traces,
33461
33543
  interactive,
@@ -34685,7 +34767,7 @@ var init_MapView = __esm({
34685
34767
  shadowSize: [41, 41]
34686
34768
  });
34687
34769
  L.Marker.prototype.options.icon = defaultIcon;
34688
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState102 } = React94__namespace.default;
34770
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState103 } = React94__namespace.default;
34689
34771
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
34690
34772
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
34691
34773
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -34730,7 +34812,7 @@ var init_MapView = __esm({
34730
34812
  showAttribution = true
34731
34813
  }) {
34732
34814
  const eventBus = useEventBus2();
34733
- const [clickedPosition, setClickedPosition] = useState102(null);
34815
+ const [clickedPosition, setClickedPosition] = useState103(null);
34734
34816
  const handleMapClick = useCallback96((lat, lng) => {
34735
34817
  if (showClickedPin) {
34736
34818
  setClickedPosition({ lat, lng });
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import React__default from 'react';
3
- import { JsonObject, JsonValue, OrbitalSchema, Expression, UISlot, EntityData, EditFocus, EventPayload, ThemeDefinition } from '@almadar/core';
3
+ import { JsonObject, SExpr, OrbitalSchema, Expression, UISlot, EntityData, EditFocus, EventPayload, ThemeDefinition } from '@almadar/core';
4
4
  import { Node, Edge, NodeProps, EdgeProps } from '@xyflow/react';
5
5
  import { AnyPatternConfig } from '@almadar/core/patterns';
6
6
  import { useDraggable } from '@dnd-kit/core';
@@ -457,7 +457,7 @@ declare function curveControlPoint(x1: number, y1: number, x2: number, y2: numbe
457
457
 
458
458
  interface SerializedEffect extends JsonObject {
459
459
  type: string;
460
- args: JsonValue[];
460
+ args: SExpr[];
461
461
  }
462
462
  interface ApplicationOrbitalData {
463
463
  name: string;
@@ -526,7 +526,7 @@ interface TraitTransitionInfo extends JsonObject {
526
526
  from: string;
527
527
  to: string;
528
528
  event: string;
529
- guard: JsonValue | null;
529
+ guard: SExpr | null;
530
530
  effects: SerializedEffect[];
531
531
  index: number;
532
532
  }
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import React__default from 'react';
3
- import { JsonObject, JsonValue, OrbitalSchema, Expression, UISlot, EntityData, EditFocus, EventPayload, ThemeDefinition } from '@almadar/core';
3
+ import { JsonObject, SExpr, OrbitalSchema, Expression, UISlot, EntityData, EditFocus, EventPayload, ThemeDefinition } from '@almadar/core';
4
4
  import { Node, Edge, NodeProps, EdgeProps } from '@xyflow/react';
5
5
  import { AnyPatternConfig } from '@almadar/core/patterns';
6
6
  import { useDraggable } from '@dnd-kit/core';
@@ -457,7 +457,7 @@ declare function curveControlPoint(x1: number, y1: number, x2: number, y2: numbe
457
457
 
458
458
  interface SerializedEffect extends JsonObject {
459
459
  type: string;
460
- args: JsonValue[];
460
+ args: SExpr[];
461
461
  }
462
462
  interface ApplicationOrbitalData {
463
463
  name: string;
@@ -526,7 +526,7 @@ interface TraitTransitionInfo extends JsonObject {
526
526
  from: string;
527
527
  to: string;
528
528
  event: string;
529
- guard: JsonValue | null;
529
+ guard: SExpr | null;
530
530
  effects: SerializedEffect[];
531
531
  index: number;
532
532
  }
package/dist/avl/index.js CHANGED
@@ -7,7 +7,7 @@ import ELK from 'elkjs/lib/elk.bundled.js';
7
7
  import { MarkerType, useReactFlow, Handle, Position, getBezierPath, EdgeLabelRenderer, useNodeId, ReactFlowProvider, BaseEdge, useNodesState, useEdgesState, ReactFlow, Controls, Background, BackgroundVariant } from '@xyflow/react';
8
8
  import { useTranslate } from '@almadar/ui/hooks';
9
9
  import { InMemoryPersistence, StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, normalizeCallSiteConfigToValues, interpolateValue } from '@almadar/runtime';
10
- import { FieldTypeSchema, isInlineTrait, isPageReference, buildResolvedTraitConfigs, schemaToIR, getPage, mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, walkSExpr, isRenderBindingMarker, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER, isFileValue, ANIMATION_NAMES } from '@almadar/core';
10
+ import { FieldTypeSchema, isInlineTrait, isPageReference, buildResolvedTraitConfigs, schemaToIR, getPage, mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, walkSExpr, isRenderBindingMarker, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER, isFileValue } from '@almadar/core';
11
11
  import * as LucideIcons2 from 'lucide-react';
12
12
  import { Loader2, X, Code, FileText, WrapText, Check, Copy, Lightbulb, CheckCircle, List, Printer, ChevronRight, ChevronLeft, GitBranch, Pencil, Eye, Plus, ArrowRight, Trash, RotateCcw, Play, Terminal, XCircle, AlertTriangle, Trash2, Link2, ArrowLeft, ZoomOut, ZoomIn, Download, DollarSign, ChevronDown, Menu as Menu$1, Package, Calendar, MoreHorizontal, Image as Image$1, Upload, HelpCircle, PauseCircle, Search, Bold, Italic, Underline, Strikethrough, Heading1, Heading2, Heading3, ListOrdered, Quote, Link as Link$1, Minus, Eraser, TrendingUp, TrendingDown, AlertCircle, Circle, Clock, CheckCircle2, ChevronUp } from 'lucide-react';
13
13
  import { createPortal } from 'react-dom';
@@ -7066,9 +7066,16 @@ var init_Input = __esm({
7066
7066
  }
7067
7067
  };
7068
7068
  const handleKeyDown = (e) => {
7069
- if (action && e.key === "Enter") {
7070
- eventBus.emit(`UI:${action}`, { value: e.currentTarget.value });
7069
+ if (!action || e.key !== "Enter") return;
7070
+ const raw = e.currentTarget.value;
7071
+ if (type === "number") {
7072
+ if (raw.trim() === "") return;
7073
+ const numeric = Number(raw);
7074
+ if (Number.isNaN(numeric)) return;
7075
+ eventBus.emit(`UI:${action}`, { value: numeric });
7076
+ return;
7071
7077
  }
7078
+ eventBus.emit(`UI:${action}`, { value: raw });
7072
7079
  };
7073
7080
  const wrapField = (field, fullWidth = true) => /* @__PURE__ */ jsxs("div", { className: fullWidth ? "w-full" : "w-fit", children: [
7074
7081
  label && /* @__PURE__ */ jsx("label", { className: "block text-sm font-medium text-foreground mb-1", children: label }),
@@ -9541,6 +9548,7 @@ function resolveFxView(item, presets) {
9541
9548
  vy: item.vy ?? row.vy,
9542
9549
  vz: item.vz ?? row.vz,
9543
9550
  particleCount: item.particleCount ?? row.particleCount,
9551
+ animation: item.animation ?? row.animation,
9544
9552
  shape: row.shape,
9545
9553
  count: row.count,
9546
9554
  glow: row.glow,
@@ -11072,9 +11080,6 @@ var init_isometric = __esm({
11072
11080
  };
11073
11081
  }
11074
11082
  });
11075
- function isAnimationName(name) {
11076
- return ANIMATION_NAMES.includes(name);
11077
- }
11078
11083
  function frameRect(frame, row, columns, frameWidth, frameHeight) {
11079
11084
  return {
11080
11085
  sx: frame % columns * frameWidth,
@@ -13008,13 +13013,17 @@ var init_DrawSprite = __esm({
13008
13013
  return;
13009
13014
  }
13010
13015
  if (isSpriteSheetAtlas(atlas)) {
13011
- const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
13016
+ const def = atlas.animations[node.animation];
13012
13017
  if (!def) {
13013
13018
  paintFallbackSquare(painter, node, dctx, "sprite-missing");
13014
13019
  return;
13015
13020
  }
13016
- const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
13021
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, node.clockMs ?? dctx.time);
13017
13022
  const r2 = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
13023
+ if (![r2.sx, r2.sy, r2.sw, r2.sh].every(Number.isFinite)) {
13024
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
13025
+ return;
13026
+ }
13018
13027
  src = { x: r2.sx, y: r2.sy, w: r2.sw, h: r2.sh };
13019
13028
  }
13020
13029
  }
@@ -13562,7 +13571,10 @@ function expandFxItem(view, node, epochNowMs, dim) {
13562
13571
  anchor: "center",
13563
13572
  width: spriteSize,
13564
13573
  height: spriteSize,
13565
- opacity: fade
13574
+ opacity: fade,
13575
+ animation: view.animation ?? sprite.animations?.[0],
13576
+ // One-shot fx rows (burst) play from the item's birth, not wall-time.
13577
+ clockMs: ageMs
13566
13578
  });
13567
13579
  } else {
13568
13580
  out.push(...proceduralShapes(view, pos, fade, progress));
@@ -15142,6 +15154,7 @@ var init_GameShell = __esm({
15142
15154
  init_Typography();
15143
15155
  init_AtlasImage();
15144
15156
  GAME_FONTS = {
15157
+ fredoka: "Fredoka",
15145
15158
  future: "Kenney Future",
15146
15159
  "future-narrow": "Kenney Future Narrow",
15147
15160
  pixel: "Kenney Pixel",
@@ -15158,10 +15171,10 @@ var init_GameShell = __esm({
15158
15171
  showTopBar = true,
15159
15172
  children,
15160
15173
  backgroundAsset,
15161
- fontFamily = "future",
15174
+ fontFamily,
15162
15175
  "data-theme": dataTheme
15163
15176
  }) => {
15164
- const font = GAME_FONTS[fontFamily] ?? fontFamily;
15177
+ const font = fontFamily ? GAME_FONTS[fontFamily] ?? fontFamily : void 0;
15165
15178
  return /* @__PURE__ */ jsxs(
15166
15179
  Box,
15167
15180
  {
@@ -15176,8 +15189,11 @@ var init_GameShell = __esm({
15176
15189
  color: "var(--color-foreground, #e0e0e0)",
15177
15190
  // The fontFamily knob is a scoped override of the theme contract's
15178
15191
  // display slot: titles/numerics take the game face, body text keeps
15179
- // the active theme's --font-family-body.
15180
- "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif`
15192
+ // the active theme's --font-family-body. Set ONLY when explicitly
15193
+ // passed an always-on inline stamp would shadow the orbital's
15194
+ // inline-theme font-family-display token (inline style beats the
15195
+ // theme provider's vars for the whole shell subtree).
15196
+ ...font ? { "--font-family-display": `'${font}', ui-sans-serif, system-ui, sans-serif` } : {}
15181
15197
  },
15182
15198
  children: [
15183
15199
  backgroundAsset && /* @__PURE__ */ jsx(
@@ -15654,6 +15670,16 @@ function traceShapes(panel, k, width, height) {
15654
15670
  }
15655
15671
  return out;
15656
15672
  }
15673
+ function drawableNeedsAnimation(nodes) {
15674
+ if (!nodes) return false;
15675
+ return nodes.some((node) => {
15676
+ if (node.type === "draw-sprite") return node.animation !== void 0;
15677
+ if (node.type === "draw-fx-layer") return Array.isArray(node.items) && node.items.length > 0;
15678
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some((it) => it.animation !== void 0);
15679
+ if (node.type === "draw-group") return Array.isArray(node.items) && drawableNeedsAnimation(node.items);
15680
+ return false;
15681
+ });
15682
+ }
15657
15683
  var DASH_PATTERNS, TRACE_SERIES_COLORS, LearningCanvas;
15658
15684
  var init_LearningCanvas = __esm({
15659
15685
  "components/learning/atoms/LearningCanvas.tsx"() {
@@ -15661,6 +15687,8 @@ var init_LearningCanvas = __esm({
15661
15687
  init_cn();
15662
15688
  init_perf();
15663
15689
  init_useEventBus();
15690
+ init_webPainter2d();
15691
+ init_paintDispatch();
15664
15692
  DASH_PATTERNS = { dashed: [6, 4], dotted: [2, 3] };
15665
15693
  TRACE_SERIES_COLORS = ["#2563eb", "#dc2626", "#16a34a", "#f59e0b"];
15666
15694
  LearningCanvas = ({
@@ -15669,6 +15697,8 @@ var init_LearningCanvas = __esm({
15669
15697
  height = 400,
15670
15698
  backgroundColor,
15671
15699
  shapes = [],
15700
+ drawables,
15701
+ projector,
15672
15702
  readouts,
15673
15703
  traces,
15674
15704
  interactive = false,
@@ -15682,6 +15712,9 @@ var init_LearningCanvas = __esm({
15682
15712
  const eventBus = useEventBus();
15683
15713
  const animRef = useRef(0);
15684
15714
  const hoverIndexRef = useRef(-1);
15715
+ const [drawVersion, setDrawVersion] = useState(0);
15716
+ const invalidateRef = useRef(() => setDrawVersion((v) => v + 1));
15717
+ const needsAnim = useMemo(() => drawableNeedsAnimation(drawables), [drawables]);
15685
15718
  const findShapeAt = useCallback((clientX, clientY) => {
15686
15719
  const canvas = canvasRef.current;
15687
15720
  if (!canvas) return -1;
@@ -15725,20 +15758,29 @@ var init_LearningCanvas = __esm({
15725
15758
  for (const shape of derivedShapes) {
15726
15759
  if (shape.type === "text") drawShape(ctx, shape, width, height, derivedShapes);
15727
15760
  }
15761
+ if (drawables?.length && projector) {
15762
+ const painter = createWebPainter(ctx, invalidateRef.current);
15763
+ const timeMs = needsAnim && typeof performance !== "undefined" ? performance.now() : 0;
15764
+ const dctx = { projector, time: timeMs, invalidate: invalidateRef.current };
15765
+ for (const node of drawables) {
15766
+ paintDrawable(painter, node, dctx);
15767
+ }
15768
+ }
15728
15769
  perfEnd("learningcanvas:paint", _perfT);
15729
- }, [width, height, backgroundColor, derivedShapes]);
15770
+ }, [width, height, backgroundColor, derivedShapes, drawables, projector, needsAnim]);
15730
15771
  useEffect(() => {
15731
15772
  draw();
15732
- }, [draw]);
15773
+ }, [draw, drawVersion]);
15733
15774
  useEffect(() => {
15734
- if (!animate) return;
15775
+ const shouldAnimate = animate || needsAnim;
15776
+ if (!shouldAnimate) return;
15735
15777
  const loop = () => {
15736
15778
  draw();
15737
15779
  animRef.current = requestAnimationFrame(loop);
15738
15780
  };
15739
15781
  animRef.current = requestAnimationFrame(loop);
15740
15782
  return () => cancelAnimationFrame(animRef.current);
15741
- }, [animate, draw]);
15783
+ }, [animate, needsAnim, draw]);
15742
15784
  const handlePointerMove = useCallback(
15743
15785
  (e) => {
15744
15786
  if (!interactive) return;
@@ -27170,8 +27212,9 @@ var init_DashboardLayout = __esm({
27170
27212
  if (notificationClickEvent) eventBus.emit(`UI:${notificationClickEvent}`, {});
27171
27213
  if (onNotificationClick) onNotificationClick();
27172
27214
  };
27173
- const handleTopBarActionClick = (event) => {
27174
- eventBus.emit(`UI:${event}`, {});
27215
+ const handleTopBarActionClick = (action) => {
27216
+ if (action.event) eventBus.emit(`UI:${action.event}`, {});
27217
+ if (action.navigatesTo) eventBus.emit("UI:NAVIGATE", { url: action.navigatesTo });
27175
27218
  };
27176
27219
  const [sidebarOpen, setSidebarOpen] = useState(false);
27177
27220
  const [userMenuOpen, setUserMenuOpen] = useState(false);
@@ -27386,7 +27429,7 @@ var init_DashboardLayout = __esm({
27386
27429
  {
27387
27430
  variant: "ghost",
27388
27431
  className: "relative p-2 rounded-full hover:bg-muted dark:hover:bg-muted",
27389
- onClick: () => handleTopBarActionClick(action.event),
27432
+ onClick: () => handleTopBarActionClick(action),
27390
27433
  "aria-label": action.label ?? (typeof action.icon === "string" ? action.icon : void 0),
27391
27434
  children: [
27392
27435
  /* @__PURE__ */ jsx(Icon, { icon: action.icon, className: "h-5 w-5 text-muted-foreground dark:text-muted-foreground" }),
@@ -27403,7 +27446,7 @@ var init_DashboardLayout = __esm({
27403
27446
  )
27404
27447
  ]
27405
27448
  },
27406
- `${action.event}-${idx}`
27449
+ `${action.event ?? action.navigatesTo ?? "action"}-${idx}`
27407
27450
  )),
27408
27451
  notificationsEnabled && /* @__PURE__ */ jsxs(
27409
27452
  Button,
@@ -33065,6 +33108,7 @@ var init_MathCanvas = __esm({
33065
33108
  angles = [],
33066
33109
  hops = [],
33067
33110
  shapes = [],
33111
+ drawables,
33068
33112
  readouts,
33069
33113
  traces,
33070
33114
  interactive = false,
@@ -33371,6 +33415,42 @@ var init_MathCanvas = __esm({
33371
33415
  hops,
33372
33416
  shapes
33373
33417
  ]);
33418
+ const projector = useMemo(() => {
33419
+ if (!drawables?.length) return void 0;
33420
+ const margin = 24;
33421
+ const plotW = width - margin * 2;
33422
+ const plotH = height - margin * 2;
33423
+ const xScale = plotW / (xMax - xMin);
33424
+ const mapX = (x) => margin + (x - xMin) / (xMax - xMin) * plotW;
33425
+ const mapY = (y) => height - (margin + (y - yMin) / (yMax - yMin) * plotH);
33426
+ const project = (pos) => ({ x: mapX(pos.x), y: mapY(pos.y) });
33427
+ const anchorPoint = (pos, anchor) => {
33428
+ const base = project(pos);
33429
+ if (anchor === "top-left") return base;
33430
+ const cx = base.x + xScale / 2;
33431
+ if (anchor === "ground") return { x: cx, y: base.y + xScale * 0.92 };
33432
+ return { x: cx, y: base.y + xScale / 2 };
33433
+ };
33434
+ const cellPath = (pos) => {
33435
+ const base = project(pos);
33436
+ return [
33437
+ { x: base.x, y: base.y },
33438
+ { x: base.x + xScale, y: base.y },
33439
+ { x: base.x + xScale, y: base.y + xScale },
33440
+ { x: base.x, y: base.y + xScale }
33441
+ ];
33442
+ };
33443
+ return {
33444
+ project,
33445
+ anchorPoint,
33446
+ cellPath,
33447
+ tileWidth: xScale,
33448
+ floorHeight: xScale / 2,
33449
+ diamondTopY: 0,
33450
+ squareGrid: true,
33451
+ worldPixelDirect: false
33452
+ };
33453
+ }, [drawables?.length, width, height, xMin, xMax, yMin, yMax]);
33374
33454
  return /* @__PURE__ */ jsx(Card, { className, children: /* @__PURE__ */ jsxs(VStack, { gap: "sm", children: [
33375
33455
  title ? /* @__PURE__ */ jsx(Typography, { variant: "h4", children: title }) : null,
33376
33456
  /* @__PURE__ */ jsx(
@@ -33380,6 +33460,8 @@ var init_MathCanvas = __esm({
33380
33460
  height,
33381
33461
  backgroundColor,
33382
33462
  shapes: derivedShapes,
33463
+ drawables,
33464
+ projector,
33383
33465
  readouts,
33384
33466
  traces,
33385
33467
  interactive,
@@ -34609,7 +34691,7 @@ var init_MapView = __esm({
34609
34691
  shadowSize: [41, 41]
34610
34692
  });
34611
34693
  L.Marker.prototype.options.icon = defaultIcon;
34612
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState102 } = React94__default;
34694
+ const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback96, useState: useState103 } = React94__default;
34613
34695
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
34614
34696
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
34615
34697
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -34654,7 +34736,7 @@ var init_MapView = __esm({
34654
34736
  showAttribution = true
34655
34737
  }) {
34656
34738
  const eventBus = useEventBus2();
34657
- const [clickedPosition, setClickedPosition] = useState102(null);
34739
+ const [clickedPosition, setClickedPosition] = useState103(null);
34658
34740
  const handleMapClick = useCallback96((lat, lng) => {
34659
34741
  if (showClickedPin) {
34660
34742
  setClickedPosition({ lat, lng });
@@ -1,7 +1,7 @@
1
- import { AnimationName, Asset, ScenePos, EventEmit, JsonObject, JsonValue } from '@almadar/core';
1
+ import { AnimationName, Asset, ScenePos, EventEmit, JsonObject, SExpr } from '@almadar/core';
2
2
  import React__default from 'react';
3
3
  import * as THREE from 'three';
4
- import { D as DrawableNode } from './paintDispatch-DgBctxIq.cjs';
4
+ import { D as DrawableNode } from './paintDispatch-D3R8NNmV.cjs';
5
5
 
6
6
  /**
7
7
  * Sprite Sheet Animation Types
@@ -541,7 +541,7 @@ declare const Canvas3DHost: React__default.ForwardRefExoticComponent<Canvas3DHos
541
541
 
542
542
  interface SerializedEffect extends JsonObject {
543
543
  type: string;
544
- args: JsonValue[];
544
+ args: SExpr[];
545
545
  }
546
546
  interface ApplicationOrbitalData {
547
547
  name: string;
@@ -610,7 +610,7 @@ interface TraitTransitionInfo extends JsonObject {
610
610
  from: string;
611
611
  to: string;
612
612
  event: string;
613
- guard: JsonValue | null;
613
+ guard: SExpr | null;
614
614
  effects: SerializedEffect[];
615
615
  index: number;
616
616
  }