@almadar/ui 5.143.0 → 5.145.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.
@@ -1,7 +1,7 @@
1
- export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-CvImTgKH.cjs';
1
+ export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-BdGFhYe3.cjs';
2
2
  import { FieldValue, EntityRow, EventPayload } from '@almadar/core';
3
3
  export { AssetLoadStatus, BridgeHealth, CheckStatus, EffectTrace, EventLogEntry, OrbitalVerificationAPI, ServerResponseTrace, TraitStateSnapshot, TransitionTrace, VerificationCheck, VerificationSnapshot, VerificationSummary } from '@almadar/core';
4
- import '../paintDispatch-D3-5_cOb.cjs';
4
+ import '../paintDispatch-Cb_hQj4Y.cjs';
5
5
  import 'clsx';
6
6
 
7
7
  /**
@@ -1,7 +1,7 @@
1
- export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-DryTiahe.js';
1
+ export { C as ContentSegment, D as DEFAULT_CONFIG, a as DomEntityBox, b as DomLayoutData, c as DomOutputsBox, d as DomStateNode, e as DomTransitionLabel, f as DomTransitionPath, E as EntityDefinition, R as RenderOptions, S as StateDefinition, g as StateMachineDefinition, T as TraitSnapshotGetter, h as TransitionDefinition, V as VisualizerConfig, i as bindCanvasCapture, j as bindEventBus, k as bindLastDrawables, l as bindTraitStateGetter, m as clearVerification, n as cn, o as extractOutputsFromTransitions, p as extractStateMachine, q as formatGuard, r as getAllChecks, s as getBridgeHealth, t as getEffectSummary, u as getSnapshot, v as getSummary, w as getTraitSnapshots, x as getTransitions, y as getTransitionsForTrait, z as parseContentSegments, A as parseMarkdownWithCodeBlocks, B as recordServerResponse, F as recordTransition, G as registerCheck, H as registerTraitSnapshot, I as renderStateMachineToDomData, J as renderStateMachineToSvg, K as subscribeToVerification, L as updateAssetStatus, M as updateBridgeHealth, N as updateCheck, O as waitForTransition } from '../cn-DNkqAWZK.js';
2
2
  import { FieldValue, EntityRow, EventPayload } from '@almadar/core';
3
3
  export { AssetLoadStatus, BridgeHealth, CheckStatus, EffectTrace, EventLogEntry, OrbitalVerificationAPI, ServerResponseTrace, TraitStateSnapshot, TransitionTrace, VerificationCheck, VerificationSnapshot, VerificationSummary } from '@almadar/core';
4
- import '../paintDispatch-D3-5_cOb.js';
4
+ import '../paintDispatch-Cb_hQj4Y.js';
5
5
  import 'clsx';
6
6
 
7
7
  /**
@@ -3343,6 +3343,10 @@ var atlasCache = /* @__PURE__ */ new Map();
3343
3343
  function isTilesheet(a) {
3344
3344
  return typeof a.tileWidth === "number";
3345
3345
  }
3346
+ function isSpriteSheetAtlas(a) {
3347
+ const s = a;
3348
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
3349
+ }
3346
3350
  function getAtlas(url, onReady) {
3347
3351
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
3348
3352
  atlasCache.set(url, void 0);
@@ -3379,6 +3383,7 @@ function subRectFor(atlas, sprite) {
3379
3383
  sh: atlas.tileHeight
3380
3384
  };
3381
3385
  }
3386
+ if (isSpriteSheetAtlas(atlas)) return null;
3382
3387
  const st = atlas.subTextures[sprite];
3383
3388
  if (!st) return null;
3384
3389
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -3612,7 +3617,7 @@ var Button = React12__namespace.default.forwardRef(
3612
3617
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
3613
3618
  children: [
3614
3619
  isLoading ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons2.Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
3615
- children || label,
3620
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
3616
3621
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
3617
3622
  ]
3618
3623
  }
@@ -3322,6 +3322,10 @@ var atlasCache = /* @__PURE__ */ new Map();
3322
3322
  function isTilesheet(a) {
3323
3323
  return typeof a.tileWidth === "number";
3324
3324
  }
3325
+ function isSpriteSheetAtlas(a) {
3326
+ const s = a;
3327
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
3328
+ }
3325
3329
  function getAtlas(url, onReady) {
3326
3330
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
3327
3331
  atlasCache.set(url, void 0);
@@ -3358,6 +3362,7 @@ function subRectFor(atlas, sprite) {
3358
3362
  sh: atlas.tileHeight
3359
3363
  };
3360
3364
  }
3365
+ if (isSpriteSheetAtlas(atlas)) return null;
3361
3366
  const st = atlas.subTextures[sprite];
3362
3367
  if (!st) return null;
3363
3368
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -3591,7 +3596,7 @@ var Button = React12__default.forwardRef(
3591
3596
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
3592
3597
  children: [
3593
3598
  isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
3594
- children || label,
3599
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
3595
3600
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
3596
3601
  ]
3597
3602
  }
@@ -87,8 +87,10 @@ interface DrawSpriteProps extends DrawableBase {
87
87
  height?: number;
88
88
  /** Explicit atlas sub-rect override (px); omitted → resolved from `asset.atlas`/`asset.sprite`. */
89
89
  frame?: BlitSrc;
90
- /** Named GLB animation clip to play (3D backend only; the 2D painter animates via `frame`). Matched case-insensitively against the model's clips. */
90
+ /** Animation to play. 3D backend: a named GLB clip (matched case-insensitively). 2D painter: when `asset.atlas` resolves to a `SpriteSheetAtlas` manifest, the canonical row of that name (idle/walk/attack/hit/death) plays off the host paint clock; an explicit `frame` rect overrides. */
91
91
  animation?: string;
92
+ /** Override the sheet row's loop flag. The paint clock is wall-time, so one-shot rows hold their final frame — preview pages set `true` to cycle attack/hit/death continuously. */
93
+ loop?: boolean;
92
94
  /** Mirror horizontally (facing). */
93
95
  flipX?: boolean;
94
96
  /** Rotation in radians about the sprite's center. */
@@ -319,6 +321,8 @@ interface MeshMaterial {
319
321
  flatShading?: boolean;
320
322
  /** Which faces render (default `front`). */
321
323
  side?: MeshMaterialSide;
324
+ /** Albedo texture: an image URL or `data:` URI, sampled through the polyhedron's `uvs`. Multiplied by `color` — keep `color` white (or unset) to show the texture unmodified. */
325
+ map?: string;
322
326
  }
323
327
  /** Inverted-hull toon outline: a back-face shell of the same geometry, scaled out by `width`. */
324
328
  interface MeshOutline {
@@ -400,6 +404,8 @@ interface DrawMeshProps extends DrawableBase {
400
404
  vertices?: number[][];
401
405
  /** `polyhedron` only: triangle index triples into `vertices`, wound counter-clockwise seen from OUTSIDE in the scene frame. */
402
406
  faces?: number[][];
407
+ /** `polyhedron` only: per-vertex `[u, v]` texture coordinates parallel to `vertices`, glTF convention (`v` grows DOWN from the image's top-left). Pairs with `material.map`. */
408
+ uvs?: number[][];
403
409
  /** `polyhedron` only: smooth-skinning weights binding the vertices to named bone groups. Vertices must then be authored in BIND POSE model space; the bones' animated transforms deform them. */
404
410
  skin?: DrawMeshSkin;
405
411
  /** Euler rotation `[x, y, z]` in radians. */
@@ -87,8 +87,10 @@ interface DrawSpriteProps extends DrawableBase {
87
87
  height?: number;
88
88
  /** Explicit atlas sub-rect override (px); omitted → resolved from `asset.atlas`/`asset.sprite`. */
89
89
  frame?: BlitSrc;
90
- /** Named GLB animation clip to play (3D backend only; the 2D painter animates via `frame`). Matched case-insensitively against the model's clips. */
90
+ /** Animation to play. 3D backend: a named GLB clip (matched case-insensitively). 2D painter: when `asset.atlas` resolves to a `SpriteSheetAtlas` manifest, the canonical row of that name (idle/walk/attack/hit/death) plays off the host paint clock; an explicit `frame` rect overrides. */
91
91
  animation?: string;
92
+ /** Override the sheet row's loop flag. The paint clock is wall-time, so one-shot rows hold their final frame — preview pages set `true` to cycle attack/hit/death continuously. */
93
+ loop?: boolean;
92
94
  /** Mirror horizontally (facing). */
93
95
  flipX?: boolean;
94
96
  /** Rotation in radians about the sprite's center. */
@@ -319,6 +321,8 @@ interface MeshMaterial {
319
321
  flatShading?: boolean;
320
322
  /** Which faces render (default `front`). */
321
323
  side?: MeshMaterialSide;
324
+ /** Albedo texture: an image URL or `data:` URI, sampled through the polyhedron's `uvs`. Multiplied by `color` — keep `color` white (or unset) to show the texture unmodified. */
325
+ map?: string;
322
326
  }
323
327
  /** Inverted-hull toon outline: a back-face shell of the same geometry, scaled out by `width`. */
324
328
  interface MeshOutline {
@@ -400,6 +404,8 @@ interface DrawMeshProps extends DrawableBase {
400
404
  vertices?: number[][];
401
405
  /** `polyhedron` only: triangle index triples into `vertices`, wound counter-clockwise seen from OUTSIDE in the scene frame. */
402
406
  faces?: number[][];
407
+ /** `polyhedron` only: per-vertex `[u, v]` texture coordinates parallel to `vertices`, glTF convention (`v` grows DOWN from the image's top-left). Pairs with `material.map`. */
408
+ uvs?: number[][];
403
409
  /** `polyhedron` only: smooth-skinning weights binding the vertices to named bone groups. Vertices must then be authored in BIND POSE model space; the bones' animated transforms deform them. */
404
410
  skin?: DrawMeshSkin;
405
411
  /** Euler rotation `[x, y, z]` in radians. */
@@ -911,6 +911,10 @@ var init_Icon = __esm({
911
911
  function isTilesheet(a) {
912
912
  return typeof a.tileWidth === "number";
913
913
  }
914
+ function isSpriteSheetAtlas(a) {
915
+ const s = a;
916
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
917
+ }
914
918
  function getAtlas(url, onReady) {
915
919
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
916
920
  atlasCache.set(url, void 0);
@@ -950,6 +954,7 @@ function subRectFor(atlas, sprite) {
950
954
  sh: atlas.tileHeight
951
955
  };
952
956
  }
957
+ if (isSpriteSheetAtlas(atlas)) return null;
953
958
  const st = atlas.subTextures[sprite];
954
959
  if (!st) return null;
955
960
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -1259,7 +1264,7 @@ var init_Button = __esm({
1259
1264
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
1260
1265
  children: [
1261
1266
  isLoading ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons2.Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
1262
- children || label,
1267
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
1263
1268
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
1264
1269
  ]
1265
1270
  }
@@ -7300,6 +7305,34 @@ var init_isometric = __esm({
7300
7305
  };
7301
7306
  }
7302
7307
  });
7308
+ function isAnimationName(name) {
7309
+ return core.ANIMATION_NAMES.includes(name);
7310
+ }
7311
+ function frameRect(frame, row, columns, frameWidth, frameHeight) {
7312
+ return {
7313
+ sx: frame % columns * frameWidth,
7314
+ sy: row * frameHeight,
7315
+ sw: frameWidth,
7316
+ sh: frameHeight
7317
+ };
7318
+ }
7319
+ function getCurrentFrameFromDef(def, elapsed) {
7320
+ const frameDuration = 1e3 / def.frameRate;
7321
+ const totalDuration = def.frames * frameDuration;
7322
+ if (def.loop) {
7323
+ const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
7324
+ return { frame: frame2, finished: false };
7325
+ }
7326
+ if (elapsed >= totalDuration) {
7327
+ return { frame: def.frames - 1, finished: true };
7328
+ }
7329
+ const frame = Math.floor(elapsed / frameDuration);
7330
+ return { frame, finished: false };
7331
+ }
7332
+ var init_spriteAnimation = __esm({
7333
+ "lib/spriteAnimation.ts"() {
7334
+ }
7335
+ });
7303
7336
 
7304
7337
  // lib/gameShared.ts
7305
7338
  var init_gameShared = __esm({
@@ -9160,6 +9193,9 @@ function paintFallbackSquare(painter, node, dctx, reason) {
9160
9193
  painter.strokeRect(rect.x, rect.y, rect.w, rect.h, "#5e564b", Math.max(1, tw / 32));
9161
9194
  painter.restore();
9162
9195
  }
9196
+ function isAnimatedSprite(node) {
9197
+ return node.animation !== void 0 && typeof node.asset?.atlas === "string";
9198
+ }
9163
9199
  function DrawSprite(_props) {
9164
9200
  return null;
9165
9201
  }
@@ -9168,6 +9204,7 @@ var init_DrawSprite = __esm({
9168
9204
  "components/game/atoms/DrawSprite.tsx"() {
9169
9205
  "use client";
9170
9206
  init_atlasSlice();
9207
+ init_spriteAnimation();
9171
9208
  init_imageCache();
9172
9209
  init_contract();
9173
9210
  spriteLog = logger.createLogger("almadar:ui:draw-sprite");
@@ -9181,6 +9218,23 @@ var init_DrawSprite = __esm({
9181
9218
  return;
9182
9219
  }
9183
9220
  let src = typeof node.frame === "object" ? node.frame : void 0;
9221
+ if (!src && node.animation !== void 0 && typeof node.asset.atlas === "string") {
9222
+ const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9223
+ if (!atlas) {
9224
+ if (atlasFailed(node.asset.atlas)) paintFallbackSquare(painter, node, dctx, "atlas-failed");
9225
+ return;
9226
+ }
9227
+ if (isSpriteSheetAtlas(atlas)) {
9228
+ const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9229
+ if (!def) {
9230
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9231
+ return;
9232
+ }
9233
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9234
+ const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9235
+ src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9236
+ }
9237
+ }
9184
9238
  if (!src && isAtlasAsset(node.asset)) {
9185
9239
  const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9186
9240
  if (!atlas) {
@@ -9904,9 +9958,11 @@ function Canvas2D({
9904
9958
  const miniMapHeight = gridExtent.height || 10;
9905
9959
  const drawableIsAnimated = (node) => {
9906
9960
  if (node.type === "draw-shape") return isAnimatedShape(node);
9961
+ if (node.type === "draw-sprite") return isAnimatedSprite(node);
9907
9962
  if (node.type === "draw-group")
9908
9963
  return isAnimatedGroup(node) || Array.isArray(node.items) && node.items.some(drawableIsAnimated);
9909
9964
  if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
9965
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some(isAnimatedSprite);
9910
9966
  return false;
9911
9967
  };
9912
9968
  const animRafRef = React87.useRef(0);
@@ -10247,6 +10303,7 @@ var init_Canvas2D = __esm({
10247
10303
  init_projector();
10248
10304
  init_paintDispatch();
10249
10305
  init_DrawShape();
10306
+ init_DrawSprite();
10250
10307
  init_DrawGroup();
10251
10308
  init_registry();
10252
10309
  init_hitTest();
@@ -10320,6 +10377,7 @@ function Canvas({
10320
10377
  cameraMode: to3DCameraMode(camera?.mode),
10321
10378
  ...zoom !== void 0 ? { scale: zoom } : {},
10322
10379
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10380
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10323
10381
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10324
10382
  unitScale,
10325
10383
  backgroundColor,
@@ -22603,7 +22661,7 @@ var init_DashboardLayout = __esm({
22603
22661
  }, [isMobile, sidebarOpen]);
22604
22662
  const location = reactRouterDom.useLocation();
22605
22663
  const ctxPagePath = providers.useCurrentPagePath();
22606
- const activePath = currentPath ?? ctxPagePath ?? location.pathname;
22664
+ const activePath = (currentPath || void 0) ?? ctxPagePath ?? location.pathname;
22607
22665
  const { signOut: authSignOut } = useAuthContext();
22608
22666
  const user = userProp || (null);
22609
22667
  const { t } = hooks.useTranslate();
@@ -23066,6 +23124,14 @@ var init_DashboardLayout = __esm({
23066
23124
  NavLinkBottom.displayName = "NavLinkBottom";
23067
23125
  }
23068
23126
  });
23127
+ function downloadItemUrl(url, label) {
23128
+ const a = document.createElement("a");
23129
+ a.href = url;
23130
+ a.download = url.startsWith("data:") ? label : url.split("/").pop() ?? "download";
23131
+ document.body.appendChild(a);
23132
+ a.click();
23133
+ a.remove();
23134
+ }
23069
23135
  function computeMenuStyle(position, triggerRect) {
23070
23136
  const isTop = position.startsWith("top");
23071
23137
  const isRight = position.endsWith("right") || position.endsWith("end");
@@ -23118,6 +23184,7 @@ function SubMenu({
23118
23184
  onClick: () => {
23119
23185
  if (item.disabled) return;
23120
23186
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
23187
+ if (item.url) downloadItemUrl(item.url, item.label);
23121
23188
  item.onClick?.();
23122
23189
  },
23123
23190
  "aria-disabled": item.disabled || void 0,
@@ -23271,6 +23338,7 @@ var init_Menu = __esm({
23271
23338
  setActiveSubMenu(itemId);
23272
23339
  } else {
23273
23340
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
23341
+ if (item.url) downloadItemUrl(item.url, item.label);
23274
23342
  item.onClick?.();
23275
23343
  setIsOpen(false);
23276
23344
  }
@@ -23306,12 +23374,19 @@ var init_Menu = __esm({
23306
23374
  "bottom-end": "bottom-start"
23307
23375
  };
23308
23376
  const effectivePosition = direction === "rtl" ? rtlMirror[position] ?? position : position;
23309
- const triggerChild = React87__namespace.default.isValidElement(trigger) ? trigger : /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", as: "span", children: trigger });
23310
- const triggerElement = React87__namespace.default.cloneElement(
23311
- triggerChild,
23377
+ const triggerElement = React87__namespace.default.isValidElement(trigger) ? React87__namespace.default.cloneElement(trigger, {
23378
+ ref: triggerRef,
23379
+ onClick: handleToggle
23380
+ }) : /* @__PURE__ */ jsxRuntime.jsx(
23381
+ Box,
23312
23382
  {
23313
- ref: triggerRef,
23314
- onClick: handleToggle
23383
+ as: "span",
23384
+ ref: (el) => {
23385
+ triggerRef.current = el;
23386
+ },
23387
+ onClick: handleToggle,
23388
+ className: "inline-flex",
23389
+ children: typeof trigger === "string" || typeof trigger === "number" ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", as: "span", children: trigger }) : trigger
23315
23390
  }
23316
23391
  );
23317
23392
  const renderMenuItems = (menuItems) => menuItems.map((item, index) => {
@@ -3,7 +3,7 @@ import React87__default, { createContext, useContext, useMemo, useRef, useEffect
3
3
  import { EventBusContext, useTraitScopeChain, useEntitySchemaOptional, useEntityBindingSnapshot, useTraitScope, TraitScopeProvider, useCurrentPagePath, useGameAudioContextOptional } 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, isInlineTrait } from '@almadar/core';
6
+ import { ANONYMOUS_USER, isRenderBindingMarker, isInlineTrait, ANIMATION_NAMES } from '@almadar/core';
7
7
  export { ANONYMOUS_USER } from '@almadar/core';
8
8
  import { clsx } from 'clsx';
9
9
  import { twMerge } from 'tailwind-merge';
@@ -837,6 +837,10 @@ var init_Icon = __esm({
837
837
  function isTilesheet(a) {
838
838
  return typeof a.tileWidth === "number";
839
839
  }
840
+ function isSpriteSheetAtlas(a) {
841
+ const s = a;
842
+ return typeof s.frameWidth === "number" && typeof s.frameHeight === "number" && typeof s.animations === "object";
843
+ }
840
844
  function getAtlas(url, onReady) {
841
845
  if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
842
846
  atlasCache.set(url, void 0);
@@ -876,6 +880,7 @@ function subRectFor(atlas, sprite) {
876
880
  sh: atlas.tileHeight
877
881
  };
878
882
  }
883
+ if (isSpriteSheetAtlas(atlas)) return null;
879
884
  const st = atlas.subTextures[sprite];
880
885
  if (!st) return null;
881
886
  return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
@@ -1185,7 +1190,7 @@ var init_Button = __esm({
1185
1190
  "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
1186
1191
  children: [
1187
1192
  isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
1188
- children || label,
1193
+ (Array.isArray(children) ? children.length > 0 : Boolean(children)) ? children : label,
1189
1194
  resolvedRightIcon && !isLoading && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
1190
1195
  ]
1191
1196
  }
@@ -7226,6 +7231,34 @@ var init_isometric = __esm({
7226
7231
  };
7227
7232
  }
7228
7233
  });
7234
+ function isAnimationName(name) {
7235
+ return ANIMATION_NAMES.includes(name);
7236
+ }
7237
+ function frameRect(frame, row, columns, frameWidth, frameHeight) {
7238
+ return {
7239
+ sx: frame % columns * frameWidth,
7240
+ sy: row * frameHeight,
7241
+ sw: frameWidth,
7242
+ sh: frameHeight
7243
+ };
7244
+ }
7245
+ function getCurrentFrameFromDef(def, elapsed) {
7246
+ const frameDuration = 1e3 / def.frameRate;
7247
+ const totalDuration = def.frames * frameDuration;
7248
+ if (def.loop) {
7249
+ const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
7250
+ return { frame: frame2, finished: false };
7251
+ }
7252
+ if (elapsed >= totalDuration) {
7253
+ return { frame: def.frames - 1, finished: true };
7254
+ }
7255
+ const frame = Math.floor(elapsed / frameDuration);
7256
+ return { frame, finished: false };
7257
+ }
7258
+ var init_spriteAnimation = __esm({
7259
+ "lib/spriteAnimation.ts"() {
7260
+ }
7261
+ });
7229
7262
 
7230
7263
  // lib/gameShared.ts
7231
7264
  var init_gameShared = __esm({
@@ -9086,6 +9119,9 @@ function paintFallbackSquare(painter, node, dctx, reason) {
9086
9119
  painter.strokeRect(rect.x, rect.y, rect.w, rect.h, "#5e564b", Math.max(1, tw / 32));
9087
9120
  painter.restore();
9088
9121
  }
9122
+ function isAnimatedSprite(node) {
9123
+ return node.animation !== void 0 && typeof node.asset?.atlas === "string";
9124
+ }
9089
9125
  function DrawSprite(_props) {
9090
9126
  return null;
9091
9127
  }
@@ -9094,6 +9130,7 @@ var init_DrawSprite = __esm({
9094
9130
  "components/game/atoms/DrawSprite.tsx"() {
9095
9131
  "use client";
9096
9132
  init_atlasSlice();
9133
+ init_spriteAnimation();
9097
9134
  init_imageCache();
9098
9135
  init_contract();
9099
9136
  spriteLog = createLogger("almadar:ui:draw-sprite");
@@ -9107,6 +9144,23 @@ var init_DrawSprite = __esm({
9107
9144
  return;
9108
9145
  }
9109
9146
  let src = typeof node.frame === "object" ? node.frame : void 0;
9147
+ if (!src && node.animation !== void 0 && typeof node.asset.atlas === "string") {
9148
+ const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9149
+ if (!atlas) {
9150
+ if (atlasFailed(node.asset.atlas)) paintFallbackSquare(painter, node, dctx, "atlas-failed");
9151
+ return;
9152
+ }
9153
+ if (isSpriteSheetAtlas(atlas)) {
9154
+ const def = isAnimationName(node.animation) ? atlas.animations[node.animation] : void 0;
9155
+ if (!def) {
9156
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9157
+ return;
9158
+ }
9159
+ const { frame } = getCurrentFrameFromDef(node.loop === void 0 ? def : { ...def, loop: node.loop }, dctx.time);
9160
+ const r = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
9161
+ src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9162
+ }
9163
+ }
9110
9164
  if (!src && isAtlasAsset(node.asset)) {
9111
9165
  const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9112
9166
  if (!atlas) {
@@ -9830,9 +9884,11 @@ function Canvas2D({
9830
9884
  const miniMapHeight = gridExtent.height || 10;
9831
9885
  const drawableIsAnimated = (node) => {
9832
9886
  if (node.type === "draw-shape") return isAnimatedShape(node);
9887
+ if (node.type === "draw-sprite") return isAnimatedSprite(node);
9833
9888
  if (node.type === "draw-group")
9834
9889
  return isAnimatedGroup(node) || Array.isArray(node.items) && node.items.some(drawableIsAnimated);
9835
9890
  if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
9891
+ if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some(isAnimatedSprite);
9836
9892
  return false;
9837
9893
  };
9838
9894
  const animRafRef = useRef(0);
@@ -10173,6 +10229,7 @@ var init_Canvas2D = __esm({
10173
10229
  init_projector();
10174
10230
  init_paintDispatch();
10175
10231
  init_DrawShape();
10232
+ init_DrawSprite();
10176
10233
  init_DrawGroup();
10177
10234
  init_registry();
10178
10235
  init_hitTest();
@@ -10246,6 +10303,7 @@ function Canvas({
10246
10303
  cameraMode: to3DCameraMode(camera?.mode),
10247
10304
  ...zoom !== void 0 ? { scale: zoom } : {},
10248
10305
  ...camera?.fov !== void 0 ? { fov: camera.fov } : {},
10306
+ ...camera?.azimuth !== void 0 ? { azimuth: camera.azimuth } : {},
10249
10307
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10250
10308
  unitScale,
10251
10309
  backgroundColor,
@@ -22529,7 +22587,7 @@ var init_DashboardLayout = __esm({
22529
22587
  }, [isMobile, sidebarOpen]);
22530
22588
  const location = useLocation();
22531
22589
  const ctxPagePath = useCurrentPagePath();
22532
- const activePath = currentPath ?? ctxPagePath ?? location.pathname;
22590
+ const activePath = (currentPath || void 0) ?? ctxPagePath ?? location.pathname;
22533
22591
  const { signOut: authSignOut } = useAuthContext();
22534
22592
  const user = userProp || (null);
22535
22593
  const { t } = useTranslate();
@@ -22992,6 +23050,14 @@ var init_DashboardLayout = __esm({
22992
23050
  NavLinkBottom.displayName = "NavLinkBottom";
22993
23051
  }
22994
23052
  });
23053
+ function downloadItemUrl(url, label) {
23054
+ const a = document.createElement("a");
23055
+ a.href = url;
23056
+ a.download = url.startsWith("data:") ? label : url.split("/").pop() ?? "download";
23057
+ document.body.appendChild(a);
23058
+ a.click();
23059
+ a.remove();
23060
+ }
22995
23061
  function computeMenuStyle(position, triggerRect) {
22996
23062
  const isTop = position.startsWith("top");
22997
23063
  const isRight = position.endsWith("right") || position.endsWith("end");
@@ -23044,6 +23110,7 @@ function SubMenu({
23044
23110
  onClick: () => {
23045
23111
  if (item.disabled) return;
23046
23112
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
23113
+ if (item.url) downloadItemUrl(item.url, item.label);
23047
23114
  item.onClick?.();
23048
23115
  },
23049
23116
  "aria-disabled": item.disabled || void 0,
@@ -23197,6 +23264,7 @@ var init_Menu = __esm({
23197
23264
  setActiveSubMenu(itemId);
23198
23265
  } else {
23199
23266
  if (item.event) eventBus.emit(`UI:${item.event}`, { itemId, label: item.label });
23267
+ if (item.url) downloadItemUrl(item.url, item.label);
23200
23268
  item.onClick?.();
23201
23269
  setIsOpen(false);
23202
23270
  }
@@ -23232,12 +23300,19 @@ var init_Menu = __esm({
23232
23300
  "bottom-end": "bottom-start"
23233
23301
  };
23234
23302
  const effectivePosition = direction === "rtl" ? rtlMirror[position] ?? position : position;
23235
- const triggerChild = React87__default.isValidElement(trigger) ? trigger : /* @__PURE__ */ jsx(Typography, { variant: "small", as: "span", children: trigger });
23236
- const triggerElement = React87__default.cloneElement(
23237
- triggerChild,
23303
+ const triggerElement = React87__default.isValidElement(trigger) ? React87__default.cloneElement(trigger, {
23304
+ ref: triggerRef,
23305
+ onClick: handleToggle
23306
+ }) : /* @__PURE__ */ jsx(
23307
+ Box,
23238
23308
  {
23239
- ref: triggerRef,
23240
- onClick: handleToggle
23309
+ as: "span",
23310
+ ref: (el) => {
23311
+ triggerRef.current = el;
23312
+ },
23313
+ onClick: handleToggle,
23314
+ className: "inline-flex",
23315
+ children: typeof trigger === "string" || typeof trigger === "number" ? /* @__PURE__ */ jsx(Typography, { variant: "small", as: "span", children: trigger }) : trigger
23241
23316
  }
23242
23317
  );
23243
23318
  const renderMenuItems = (menuItems) => menuItems.map((item, index) => {