@almadar/ui 5.152.0 → 5.154.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{EntityBindingContext-CD9ZoXb4.d.cts → EntityBindingContext-BfZGeDfX.d.cts} +4 -2
  2. package/dist/{EntityBindingContext-CD9ZoXb4.d.ts → EntityBindingContext-BfZGeDfX.d.ts} +4 -2
  3. package/dist/NavStackContext-BoVV9nWb.d.cts +83 -0
  4. package/dist/NavStackContext-BoVV9nWb.d.ts +83 -0
  5. package/dist/avl/index.cjs +571 -76
  6. package/dist/avl/index.js +572 -77
  7. package/dist/{avl-schema-parser-Cx_4SVg9.d.ts → avl-schema-parser-CLIm28Wa.d.ts} +1 -1
  8. package/dist/{avl-schema-parser-CVzkNzg7.d.cts → avl-schema-parser-Do_LPV1o.d.cts} +1 -1
  9. package/dist/{cn-DJTUjk1M.d.ts → cn-CFurBb2q.d.ts} +1 -1
  10. package/dist/{cn-BnAJZcNb.d.cts → cn-TH-RHihd.d.cts} +1 -1
  11. package/dist/components/index.cjs +502 -52
  12. package/dist/components/index.d.cts +58 -9
  13. package/dist/components/index.d.ts +58 -9
  14. package/dist/components/index.js +501 -55
  15. package/dist/hooks/index.cjs +4 -0
  16. package/dist/hooks/index.d.cts +1 -0
  17. package/dist/hooks/index.d.ts +1 -0
  18. package/dist/hooks/index.js +1 -0
  19. package/dist/lib/drawable/three/index.cjs +212 -0
  20. package/dist/lib/drawable/three/index.d.cts +3 -3
  21. package/dist/lib/drawable/three/index.d.ts +3 -3
  22. package/dist/lib/drawable/three/index.js +212 -0
  23. package/dist/lib/index.d.cts +2 -2
  24. package/dist/lib/index.d.ts +2 -2
  25. package/dist/{paintDispatch-CxdLjHQq.d.ts → paintDispatch-B5n2DTB-.d.ts} +178 -2
  26. package/dist/{paintDispatch-BjZjUbcb.d.cts → paintDispatch-DgBctxIq.d.cts} +178 -2
  27. package/dist/providers/index.cjs +677 -59
  28. package/dist/providers/index.d.cts +2 -1
  29. package/dist/providers/index.d.ts +2 -1
  30. package/dist/providers/index.js +677 -62
  31. package/dist/runtime/index.cjs +570 -75
  32. package/dist/runtime/index.d.cts +12 -5
  33. package/dist/runtime/index.d.ts +12 -5
  34. package/dist/runtime/index.js +571 -76
  35. package/package.json +4 -4
@@ -5803,27 +5803,61 @@ var init_InfiniteScrollSentinel = __esm({
5803
5803
  InfiniteScrollSentinel.displayName = "InfiniteScrollSentinel";
5804
5804
  }
5805
5805
  });
5806
- function createParticles(count) {
5807
- return Array.from({ length: count }, () => {
5808
- particleIdCounter += 1;
5809
- return {
5810
- id: particleIdCounter,
5811
- color: CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)],
5812
- left: 30 + Math.random() * 40,
5813
- delay: Math.random() * 300,
5814
- angle: Math.random() * 360,
5815
- distance: 40 + Math.random() * 80,
5816
- rotation: Math.random() * 720 - 360,
5817
- size: 4 + Math.random() * 6
5818
- };
5819
- });
5806
+
5807
+ // components/core/atoms/fx.ts
5808
+ function resolveFxView(item, presets) {
5809
+ const row = presets?.find((p) => p.type === item.type);
5810
+ if (!row) return item;
5811
+ return {
5812
+ ...item,
5813
+ space: item.space ?? row.space,
5814
+ effect: item.effect ?? row.effect,
5815
+ color: item.color ?? row.color,
5816
+ size: item.size ?? row.size,
5817
+ vx: item.vx ?? row.vx,
5818
+ vy: item.vy ?? row.vy,
5819
+ vz: item.vz ?? row.vz,
5820
+ particleCount: item.particleCount ?? row.particleCount,
5821
+ shape: row.shape,
5822
+ count: row.count,
5823
+ glow: row.glow,
5824
+ gravity: row.gravity,
5825
+ color2: row.color2
5826
+ };
5820
5827
  }
5821
- var CONFETTI_COLORS, particleIdCounter, ConfettiEffect;
5822
- var init_ConfettiEffect = __esm({
5823
- "components/core/atoms/ConfettiEffect.tsx"() {
5824
- "use client";
5825
- init_cn();
5826
- init_Box();
5828
+ function fxLifecycle(item, epochNowMs, tickMs) {
5829
+ const maxTtl = Math.max(item.maxTtl ?? item.ttl, item.ttl, 1);
5830
+ const lifeMs = maxTtl * tickMs;
5831
+ const ageMs = item.bornAt !== void 0 && epochNowMs > 0 ? Math.max(0, epochNowMs - item.bornAt) : (1 - item.ttl / maxTtl) * lifeMs;
5832
+ const progress = Math.min(1, Math.max(0, ageMs / lifeMs));
5833
+ return { lifeMs, ageMs, progress, fade: 1 - progress };
5834
+ }
5835
+ function fxHash01(seed, salt) {
5836
+ let h = 2166136261 ^ salt;
5837
+ for (let i = 0; i < seed.length; i++) {
5838
+ h ^= seed.charCodeAt(i);
5839
+ h = Math.imul(h, 16777619);
5840
+ }
5841
+ h ^= h >>> 13;
5842
+ h = Math.imul(h, 1274126177);
5843
+ h ^= h >>> 16;
5844
+ return (h >>> 0) / 4294967296;
5845
+ }
5846
+ function createConfettiParticles(count, seed) {
5847
+ return Array.from({ length: count }, (_, i) => ({
5848
+ id: i,
5849
+ color: CONFETTI_COLORS[Math.floor(fxHash01(seed, i * 7 + 1) * CONFETTI_COLORS.length)],
5850
+ left: 30 + fxHash01(seed, i * 7 + 2) * 40,
5851
+ delay: fxHash01(seed, i * 7 + 3) * 300,
5852
+ angle: fxHash01(seed, i * 7 + 4) * 360,
5853
+ distance: 40 + fxHash01(seed, i * 7 + 5) * 80,
5854
+ rotation: fxHash01(seed, i * 7 + 6) * 720 - 360,
5855
+ size: 4 + fxHash01(seed, i * 7 + 7) * 6
5856
+ }));
5857
+ }
5858
+ var CONFETTI_COLORS, CONFETTI_BURST_KEYFRAMES;
5859
+ var init_fx = __esm({
5860
+ "components/core/atoms/fx.ts"() {
5827
5861
  CONFETTI_COLORS = [
5828
5862
  "var(--color-primary)",
5829
5863
  "var(--color-success)",
@@ -5832,7 +5866,30 @@ var init_ConfettiEffect = __esm({
5832
5866
  "gold",
5833
5867
  "dodgerblue"
5834
5868
  ];
5835
- particleIdCounter = 0;
5869
+ CONFETTI_BURST_KEYFRAMES = `
5870
+ @keyframes confetti-burst {
5871
+ 0% {
5872
+ opacity: 1;
5873
+ transform: translate(0, 0) rotate(0deg) scale(1);
5874
+ }
5875
+ 70% {
5876
+ opacity: 1;
5877
+ }
5878
+ 100% {
5879
+ opacity: 0;
5880
+ transform: translate(var(--confetti-tx), var(--confetti-ty)) rotate(var(--confetti-rotate)) scale(0.5);
5881
+ }
5882
+ }
5883
+ `;
5884
+ }
5885
+ });
5886
+ var ConfettiEffect;
5887
+ var init_ConfettiEffect = __esm({
5888
+ "components/core/atoms/ConfettiEffect.tsx"() {
5889
+ "use client";
5890
+ init_cn();
5891
+ init_Box();
5892
+ init_fx();
5836
5893
  ConfettiEffect = ({
5837
5894
  trigger,
5838
5895
  duration = 2e3,
@@ -5841,11 +5898,13 @@ var init_ConfettiEffect = __esm({
5841
5898
  }) => {
5842
5899
  const [particles, setParticles] = React87.useState([]);
5843
5900
  const previousTriggerRef = React87.useRef(false);
5901
+ const burstRef = React87.useRef(0);
5844
5902
  React87.useEffect(() => {
5845
5903
  const wasFalse = !previousTriggerRef.current;
5846
5904
  previousTriggerRef.current = trigger;
5847
5905
  if (trigger && wasFalse) {
5848
- const newParticles = createParticles(particleCount);
5906
+ burstRef.current += 1;
5907
+ const newParticles = createConfettiParticles(particleCount, `confetti-${burstRef.current}`);
5849
5908
  setParticles(newParticles);
5850
5909
  const timer = window.setTimeout(() => {
5851
5910
  setParticles([]);
@@ -5893,21 +5952,7 @@ var init_ConfettiEffect = __esm({
5893
5952
  p.id
5894
5953
  );
5895
5954
  }),
5896
- /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
5897
- @keyframes confetti-burst {
5898
- 0% {
5899
- opacity: 1;
5900
- transform: translate(0, 0) rotate(0deg) scale(1);
5901
- }
5902
- 70% {
5903
- opacity: 1;
5904
- }
5905
- 100% {
5906
- opacity: 0;
5907
- transform: translate(var(--confetti-tx), var(--confetti-ty)) rotate(var(--confetti-rotate)) scale(0.5);
5908
- }
5909
- }
5910
- ` })
5955
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: CONFETTI_BURST_KEYFRAMES })
5911
5956
  ]
5912
5957
  }
5913
5958
  );
@@ -9464,7 +9509,7 @@ var init_DrawShape = __esm({
9464
9509
  const cx = p.x + (node.offsetX ?? 0) * tw;
9465
9510
  const cy = p.y + (node.offsetY ?? 0) * tw;
9466
9511
  const rx = (node.radiusX ?? 0) * tw;
9467
- const ry = (node.radiusY ?? rx) * tw;
9512
+ const ry = (node.radiusY ?? node.radiusX ?? 0) * tw;
9468
9513
  if (pxFill) painter.fillEllipse(cx, cy, rx, ry, pxFill);
9469
9514
  if (patFill) painter.fillEllipse(cx, cy, rx, ry, patFill);
9470
9515
  if (pxStroke) painter.strokeEllipse(cx, cy, rx, ry, pxStroke, strokePx);
@@ -9654,6 +9699,172 @@ var init_DrawTextLayer = __esm({
9654
9699
  };
9655
9700
  }
9656
9701
  });
9702
+
9703
+ // components/game/molecules/DrawFxLayer.tsx
9704
+ function fxPosition(view, dim, ageSec) {
9705
+ const g = view.gravity ?? 0;
9706
+ const fall = 0.5 * g * ageSec * ageSec;
9707
+ {
9708
+ const base2 = view.position ?? { x: view.x, y: view.z ?? view.y ?? 0 };
9709
+ if (!Number.isFinite(base2.x) || !Number.isFinite(base2.y)) return void 0;
9710
+ return {
9711
+ x: base2.x + (view.vx ?? 0) * ageSec,
9712
+ y: base2.y + (view.vz ?? 0) * ageSec + fall
9713
+ };
9714
+ }
9715
+ }
9716
+ function sparkShape(view, pos, i, fade, progress) {
9717
+ const size = view.size ?? 0.5;
9718
+ const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
9719
+ const dist = size * (0.4 + 0.6 * fxHash01(view.id, i * 3 + 1)) * easeOut2(progress);
9720
+ const r = size * (0.06 + 0.06 * fxHash01(view.id, i * 3 + 2));
9721
+ const color = view.color ?? DEFAULT_SPARK_COLOR;
9722
+ return {
9723
+ type: "draw-shape",
9724
+ shape: "ellipse",
9725
+ position: { ...pos, x: pos.x + Math.cos(angle) * dist, y: pos.y + Math.sin(angle) * dist },
9726
+ anchor: "center",
9727
+ radiusX: r,
9728
+ fill: color,
9729
+ blendMode: "lighter",
9730
+ opacity: fade,
9731
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
9732
+ };
9733
+ }
9734
+ function proceduralShapes(view, pos, fade, progress) {
9735
+ const size = view.size ?? 0.5;
9736
+ const color = view.color ?? DEFAULT_SPARK_COLOR;
9737
+ switch (view.shape ?? "spark") {
9738
+ case "ring": {
9739
+ return [
9740
+ {
9741
+ type: "draw-shape",
9742
+ shape: "ellipse",
9743
+ position: pos,
9744
+ anchor: "center",
9745
+ radiusX: size * easeOut2(progress),
9746
+ stroke: view.color2 ?? color,
9747
+ strokeWidth: 2,
9748
+ blendMode: "lighter",
9749
+ opacity: fade,
9750
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
9751
+ }
9752
+ ];
9753
+ }
9754
+ case "puff": {
9755
+ const count = view.count ?? 3;
9756
+ return Array.from({ length: count }, (_, i) => {
9757
+ const angle = fxHash01(view.id, i * 5) * Math.PI * 2;
9758
+ const drift = size * 0.3 * fxHash01(view.id, i * 5 + 1) * easeOut2(progress);
9759
+ return {
9760
+ type: "draw-shape",
9761
+ shape: "ellipse",
9762
+ position: { ...pos, x: pos.x + Math.cos(angle) * drift, y: pos.y + Math.sin(angle) * drift },
9763
+ anchor: "center",
9764
+ radiusX: size * (0.15 + 0.35 * progress) * (0.7 + 0.6 * fxHash01(view.id, i * 5 + 2)),
9765
+ fill: i === 0 && view.color2 ? view.color2 : color,
9766
+ opacity: fade * 0.6,
9767
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
9768
+ };
9769
+ });
9770
+ }
9771
+ case "streak": {
9772
+ const count = view.count ?? 5;
9773
+ return Array.from({ length: count }, (_, i) => {
9774
+ const angle = fxHash01(view.id, i * 3) * Math.PI * 2;
9775
+ const inner = size * (0.2 + 0.8 * easeOut2(progress)) * (0.6 + 0.4 * fxHash01(view.id, i * 3 + 1));
9776
+ const len = size * 0.35;
9777
+ return {
9778
+ type: "draw-shape",
9779
+ shape: "ellipse",
9780
+ position: pos,
9781
+ anchor: "center",
9782
+ offsetX: inner + len / 2,
9783
+ offsetY: 0,
9784
+ radiusX: len / 2,
9785
+ radiusY: size * 0.04,
9786
+ rotate: angle,
9787
+ fill: color,
9788
+ blendMode: "lighter",
9789
+ opacity: fade,
9790
+ ...view.glow ? { shadow: { color, blur: view.glow } } : {}
9791
+ };
9792
+ });
9793
+ }
9794
+ default: {
9795
+ const count = view.count ?? 6;
9796
+ return Array.from({ length: count }, (_, i) => sparkShape(view, pos, i, fade, progress));
9797
+ }
9798
+ }
9799
+ }
9800
+ function expandFxItem(view, node, epochNowMs, dim) {
9801
+ if (view.space === "screen") return [];
9802
+ const tickMs = node.tickMs ?? DEFAULT_TICK_MS;
9803
+ const { ageMs, progress, fade } = fxLifecycle(view, epochNowMs, tickMs);
9804
+ if (progress >= 1) return [];
9805
+ const pos = fxPosition(view, dim, ageMs / 1e3);
9806
+ if (!pos) return [];
9807
+ const out = [];
9808
+ const artItems = node.art?.[view.type]?.["idle"];
9809
+ const sprite = node.sprites?.[view.type];
9810
+ if (Array.isArray(artItems)) {
9811
+ out.push({
9812
+ type: "draw-group",
9813
+ position: pos,
9814
+ opacity: fade,
9815
+ ...view.size !== void 0 ? { scale: view.size } : {},
9816
+ items: artItems
9817
+ });
9818
+ } else if (sprite?.url) {
9819
+ const spriteSize = view.size ?? 0.6;
9820
+ out.push({
9821
+ type: "draw-sprite",
9822
+ position: pos,
9823
+ asset: sprite,
9824
+ anchor: "center",
9825
+ width: spriteSize,
9826
+ height: spriteSize,
9827
+ opacity: fade
9828
+ });
9829
+ } else {
9830
+ out.push(...proceduralShapes(view, pos, fade, progress));
9831
+ }
9832
+ if (view.message) {
9833
+ const text = {
9834
+ type: "draw-text",
9835
+ text: view.message,
9836
+ position: pos,
9837
+ offsetY: -(0.15 + 0.55 * progress),
9838
+ color: view.color ?? node.textColor ?? DEFAULT_TEXT_COLOR,
9839
+ opacity: fade
9840
+ };
9841
+ out.push(text);
9842
+ }
9843
+ return out;
9844
+ }
9845
+ function expandFxLayer(node, epochNowMs, dim) {
9846
+ if (!Array.isArray(node.items)) return [];
9847
+ const out = [];
9848
+ for (const item of node.items) {
9849
+ if (!item || typeof item.id !== "string") continue;
9850
+ out.push(...expandFxItem(resolveFxView(item, node.presets), node, epochNowMs, dim));
9851
+ }
9852
+ return out;
9853
+ }
9854
+ function DrawFxLayer(_props) {
9855
+ return null;
9856
+ }
9857
+ var DEFAULT_TICK_MS, DEFAULT_TEXT_COLOR, DEFAULT_SPARK_COLOR, easeOut2;
9858
+ var init_DrawFxLayer = __esm({
9859
+ "components/game/molecules/DrawFxLayer.tsx"() {
9860
+ "use client";
9861
+ init_fx();
9862
+ DEFAULT_TICK_MS = 500;
9863
+ DEFAULT_TEXT_COLOR = "#ffe066";
9864
+ DEFAULT_SPARK_COLOR = "#ffffff";
9865
+ easeOut2 = (t) => 1 - (1 - t) * (1 - t);
9866
+ }
9867
+ });
9657
9868
  function paintDrawable(painter, node, dctx) {
9658
9869
  switch (node.type) {
9659
9870
  case "draw-sprite":
@@ -9701,6 +9912,11 @@ function paintDrawable(painter, node, dctx) {
9701
9912
  case "draw-text-layer":
9702
9913
  paintTextLayer(painter, node, dctx);
9703
9914
  break;
9915
+ case "draw-fx-layer": {
9916
+ const epochNow = dctx.time > 0 && typeof performance !== "undefined" ? performance.timeOrigin + dctx.time : 0;
9917
+ for (const child of expandFxLayer(node, epochNow, "2d")) paintDrawable(painter, child, dctx);
9918
+ break;
9919
+ }
9704
9920
  }
9705
9921
  }
9706
9922
  var paint2dLog, warnedUnsupported2d, warnUnsupported2d;
@@ -9715,6 +9931,7 @@ var init_paintDispatch = __esm({
9715
9931
  init_DrawSpriteLayer();
9716
9932
  init_DrawShapeLayer();
9717
9933
  init_DrawTextLayer();
9934
+ init_DrawFxLayer();
9718
9935
  paint2dLog = logger.createLogger("almadar:ui:drawable-2d");
9719
9936
  warnedUnsupported2d = /* @__PURE__ */ new Set();
9720
9937
  warnUnsupported2d = (kind) => {
@@ -9963,6 +10180,7 @@ function Canvas2D({
9963
10180
  return isAnimatedGroup(node) || Array.isArray(node.items) && node.items.some(drawableIsAnimated);
9964
10181
  if (node.type === "draw-shape-layer") return Array.isArray(node.items) && node.items.some(isAnimatedShape);
9965
10182
  if (node.type === "draw-sprite-layer") return Array.isArray(node.items) && node.items.some(isAnimatedSprite);
10183
+ if (node.type === "draw-fx-layer") return Array.isArray(node.items) && node.items.length > 0;
9966
10184
  return false;
9967
10185
  };
9968
10186
  const animRafRef = React87.useRef(0);
@@ -21182,17 +21400,27 @@ var init_Breadcrumb = __esm({
21182
21400
  init_useEventBus();
21183
21401
  Breadcrumb = ({
21184
21402
  items,
21403
+ fromNavStack = false,
21404
+ itemEvent,
21185
21405
  separator = "chevron-right",
21186
21406
  maxItems,
21187
21407
  className
21188
21408
  }) => {
21189
21409
  const eventBus = useEventBus();
21190
21410
  const { t } = hooks.useTranslate();
21191
- const displayItems = maxItems && items.length > maxItems ? [
21192
- ...items.slice(0, 1),
21411
+ const navStack = providers.useNavStack();
21412
+ const sourceItems = fromNavStack ? navStack.entries.map((entry, i) => ({
21413
+ label: entry.label,
21414
+ path: entry.href,
21415
+ isCurrent: i === navStack.entries.length - 1,
21416
+ event: itemEvent
21417
+ })) : items ?? [];
21418
+ if (fromNavStack && sourceItems.length === 0) return null;
21419
+ const displayItems = maxItems && sourceItems.length > maxItems ? [
21420
+ ...sourceItems.slice(0, 1),
21193
21421
  { label: "...", isCurrent: false },
21194
- ...items.slice(-maxItems + 1)
21195
- ] : items;
21422
+ ...sourceItems.slice(-maxItems + 1)
21423
+ ] : sourceItems;
21196
21424
  return /* @__PURE__ */ jsxRuntime.jsx(
21197
21425
  "nav",
21198
21426
  {
@@ -21202,7 +21430,7 @@ var init_Breadcrumb = __esm({
21202
21430
  const isLast = index === displayItems.length - 1;
21203
21431
  const isEllipsis = item.label === "...";
21204
21432
  return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "flex items-center gap-2", children: [
21205
- isEllipsis ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", children: item.label }) : item.href || item.path ? /* @__PURE__ */ jsxRuntime.jsxs(
21433
+ isEllipsis ? /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", children: item.label }) : (item.href || item.path) && !item.event && !fromNavStack ? /* @__PURE__ */ jsxRuntime.jsxs(
21206
21434
  "a",
21207
21435
  {
21208
21436
  href: item.href || item.path,
@@ -21228,7 +21456,12 @@ var init_Breadcrumb = __esm({
21228
21456
  {
21229
21457
  type: "button",
21230
21458
  onClick: () => {
21231
- if (item.event) eventBus.emit(`UI:${item.event}`, { label: item.label });
21459
+ const href = item.path ?? item.href;
21460
+ if (item.event) {
21461
+ eventBus.emit(`UI:${item.event}`, { label: item.label, href, index });
21462
+ } else if (fromNavStack && href) {
21463
+ navStack.goTo(href);
21464
+ }
21232
21465
  item.onClick?.();
21233
21466
  },
21234
21467
  className: cn(
@@ -28202,6 +28435,196 @@ var init_PageTransition = __esm({
28202
28435
  PageTransition.displayName = "PageTransition";
28203
28436
  }
28204
28437
  });
28438
+ function ConfettiBurst({ item, lifeMs }) {
28439
+ const particles = createConfettiParticles(item.particleCount ?? 30, item.id);
28440
+ const left = (item.x ?? 0.5) * 100;
28441
+ const top = (item.z ?? item.y ?? 0.4) * 100;
28442
+ return /* @__PURE__ */ jsxRuntime.jsx(Box, { position: "absolute", style: { left: `${left}%`, top: `${top}%` }, children: particles.map((p) => {
28443
+ const rad = p.angle * Math.PI / 180;
28444
+ const tx = Math.cos(rad) * p.distance * (item.size ?? 1);
28445
+ const ty = Math.sin(rad) * p.distance * (item.size ?? 1) - 20;
28446
+ return /* @__PURE__ */ jsxRuntime.jsx(
28447
+ Box,
28448
+ {
28449
+ className: "absolute rounded-sm",
28450
+ style: {
28451
+ left: (p.left - 50) * 2,
28452
+ top: -10,
28453
+ width: p.size,
28454
+ height: p.size,
28455
+ backgroundColor: item.color ?? p.color,
28456
+ animation: `confetti-burst ${Math.max(lifeMs - p.delay, 200)}ms ease-out ${p.delay}ms forwards`,
28457
+ opacity: 0,
28458
+ "--confetti-tx": `${tx}px`,
28459
+ "--confetti-ty": `${ty}px`,
28460
+ "--confetti-rotate": `${p.rotation}deg`
28461
+ }
28462
+ },
28463
+ p.id
28464
+ );
28465
+ }) });
28466
+ }
28467
+ function SparkleBurst({ item, lifeMs }) {
28468
+ const count = item.particleCount ?? 8;
28469
+ const scale = item.size ?? 1;
28470
+ const left = (item.x ?? 0.5) * 100;
28471
+ const top = (item.z ?? item.y ?? 0.4) * 100;
28472
+ return /* @__PURE__ */ jsxRuntime.jsx(Box, { position: "absolute", style: { left: `${left}%`, top: `${top}%` }, children: Array.from({ length: count }, (_, i) => {
28473
+ const dx = (fxHash01(item.id, i * 4) - 0.5) * 160 * scale;
28474
+ const dy = (fxHash01(item.id, i * 4 + 1) - 0.5) * 120 * scale;
28475
+ const dot = (4 + fxHash01(item.id, i * 4 + 2) * 5) * scale;
28476
+ const delay = fxHash01(item.id, i * 4 + 3) * lifeMs * 0.4;
28477
+ return /* @__PURE__ */ jsxRuntime.jsx(
28478
+ Box,
28479
+ {
28480
+ className: "absolute rounded-full",
28481
+ style: {
28482
+ left: dx,
28483
+ top: dy,
28484
+ width: dot,
28485
+ height: dot,
28486
+ backgroundColor: item.color ?? "gold",
28487
+ opacity: 0,
28488
+ animation: `fx-overlay-sparkle ${Math.max(lifeMs - delay, 200)}ms ease-in-out ${delay}ms forwards`
28489
+ }
28490
+ },
28491
+ i
28492
+ );
28493
+ }) });
28494
+ }
28495
+ function OverlayFxNode({ item, tickMs }) {
28496
+ const lifeMs = lifeMsOf(item, tickMs);
28497
+ switch (item.effect ?? "sparkle") {
28498
+ case "confetti":
28499
+ return /* @__PURE__ */ jsxRuntime.jsx(ConfettiBurst, { item, lifeMs });
28500
+ case "flash":
28501
+ return /* @__PURE__ */ jsxRuntime.jsx(
28502
+ Box,
28503
+ {
28504
+ position: "absolute",
28505
+ className: "inset-0",
28506
+ style: {
28507
+ backgroundColor: item.color ?? "#ffffff",
28508
+ opacity: 0,
28509
+ animation: `fx-overlay-flash ${lifeMs}ms ease-out forwards`
28510
+ }
28511
+ }
28512
+ );
28513
+ case "streak-glow":
28514
+ return /* @__PURE__ */ jsxRuntime.jsx(
28515
+ Box,
28516
+ {
28517
+ position: "absolute",
28518
+ className: "inset-0",
28519
+ style: {
28520
+ boxShadow: `inset 0 0 ${60 * (item.size ?? 1)}px ${item.color ?? "var(--color-warning)"}`,
28521
+ opacity: 0,
28522
+ animation: `fx-overlay-glow ${lifeMs}ms ease-in-out forwards`
28523
+ }
28524
+ }
28525
+ );
28526
+ case "float-text": {
28527
+ if (!item.message) return null;
28528
+ return /* @__PURE__ */ jsxRuntime.jsx(
28529
+ Box,
28530
+ {
28531
+ position: "absolute",
28532
+ style: {
28533
+ left: `${(item.x ?? 0.5) * 100}%`,
28534
+ top: `${(item.z ?? item.y ?? 0.4) * 100}%`,
28535
+ color: item.color ?? "var(--color-success)",
28536
+ fontWeight: 700,
28537
+ fontSize: 16 * (item.size ?? 1),
28538
+ textShadow: "0 1px 2px rgba(0,0,0,0.4)",
28539
+ opacity: 0,
28540
+ animation: `fx-overlay-float ${lifeMs}ms ease-out forwards`,
28541
+ whiteSpace: "nowrap"
28542
+ },
28543
+ children: item.message
28544
+ }
28545
+ );
28546
+ }
28547
+ case "shake":
28548
+ return null;
28549
+ default:
28550
+ return /* @__PURE__ */ jsxRuntime.jsx(SparkleBurst, { item, lifeMs });
28551
+ }
28552
+ }
28553
+ var DEFAULT_TICK_MS2, FX_OVERLAY_KEYFRAMES, lifeMsOf, FxOverlay;
28554
+ var init_FxOverlay = __esm({
28555
+ "components/core/molecules/FxOverlay.tsx"() {
28556
+ "use client";
28557
+ init_cn();
28558
+ init_Box();
28559
+ init_fx();
28560
+ DEFAULT_TICK_MS2 = 500;
28561
+ FX_OVERLAY_KEYFRAMES = `
28562
+ ${CONFETTI_BURST_KEYFRAMES}
28563
+ @keyframes fx-overlay-flash {
28564
+ 0% { opacity: 0.75; }
28565
+ 100% { opacity: 0; }
28566
+ }
28567
+ @keyframes fx-overlay-glow {
28568
+ 0% { opacity: 0.9; }
28569
+ 60% { opacity: 0.6; }
28570
+ 100% { opacity: 0; }
28571
+ }
28572
+ @keyframes fx-overlay-sparkle {
28573
+ 0% { opacity: 0; transform: scale(0); }
28574
+ 30% { opacity: 1; transform: scale(1); }
28575
+ 100% { opacity: 0; transform: scale(0.3); }
28576
+ }
28577
+ @keyframes fx-overlay-float {
28578
+ 0% { opacity: 0; transform: translate(-50%, 8px); }
28579
+ 15% { opacity: 1; }
28580
+ 100% { opacity: 0; transform: translate(-50%, -40px); }
28581
+ }
28582
+ @keyframes fx-overlay-shake {
28583
+ 0% { transform: translateX(0); }
28584
+ 15% { transform: translateX(-6px); }
28585
+ 30% { transform: translateX(5px); }
28586
+ 45% { transform: translateX(-4px); }
28587
+ 60% { transform: translateX(3px); }
28588
+ 75% { transform: translateX(-2px); }
28589
+ 100% { transform: translateX(0); }
28590
+ }
28591
+ `;
28592
+ lifeMsOf = (it, tickMs) => Math.max(it.maxTtl ?? it.ttl, it.ttl, 1) * tickMs;
28593
+ FxOverlay = ({ items, tickMs = DEFAULT_TICK_MS2, children, className }) => {
28594
+ const screenItems = (Array.isArray(items) ? items : []).filter(
28595
+ (it) => Boolean(it) && typeof it.id === "string" && it.space !== "world"
28596
+ );
28597
+ const shakeItem = [...screenItems].reverse().find((it) => it.effect === "shake");
28598
+ const overlay = /* @__PURE__ */ jsxRuntime.jsxs(
28599
+ Box,
28600
+ {
28601
+ position: "absolute",
28602
+ className: cn("inset-0 pointer-events-none overflow-hidden z-50", !children && className),
28603
+ "aria-hidden": "true",
28604
+ children: [
28605
+ screenItems.map((it) => /* @__PURE__ */ jsxRuntime.jsx(OverlayFxNode, { item: it, tickMs }, it.id)),
28606
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: FX_OVERLAY_KEYFRAMES })
28607
+ ]
28608
+ }
28609
+ );
28610
+ if (children !== void 0 && children !== null) {
28611
+ return /* @__PURE__ */ jsxRuntime.jsxs(Box, { position: "relative", className, children: [
28612
+ /* @__PURE__ */ jsxRuntime.jsx(
28613
+ Box,
28614
+ {
28615
+ style: shakeItem ? { animation: `fx-overlay-shake ${Math.min(lifeMsOf(shakeItem, tickMs), 600)}ms ease-in-out` } : void 0,
28616
+ children
28617
+ },
28618
+ shakeItem?.id ?? "steady"
28619
+ ),
28620
+ overlay
28621
+ ] });
28622
+ }
28623
+ return overlay;
28624
+ };
28625
+ FxOverlay.displayName = "FxOverlay";
28626
+ }
28627
+ });
28205
28628
  function computePopoverStyle(position, triggerRect, popoverWidth) {
28206
28629
  if (position === "left" || position === "right") {
28207
28630
  return {
@@ -30207,7 +30630,7 @@ var init_MathCanvas = __esm({
30207
30630
  x: mapX((first.x + last.x) / 2),
30208
30631
  y: (mapY(region.samples[mid].y) + mapY(baseline)) / 2,
30209
30632
  text: region.label,
30210
- color: "#111827",
30633
+ color,
30211
30634
  fontSize: 11
30212
30635
  });
30213
30636
  }
@@ -30240,14 +30663,14 @@ var init_MathCanvas = __esm({
30240
30663
  const px = mapX(guide.at);
30241
30664
  out.push({ type: "line", x1: px, y1: margin, x2: px, y2: height - margin, color, dash });
30242
30665
  if (guide.label) {
30243
- out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color: "#111827", fontSize: 11 });
30666
+ out.push({ type: "text", x: px + 4, y: margin + 10, text: guide.label, color, fontSize: 11 });
30244
30667
  }
30245
30668
  } else {
30246
30669
  if (guide.at < yMin || guide.at > yMax) continue;
30247
30670
  const py = mapY(guide.at);
30248
30671
  out.push({ type: "line", x1: margin, y1: py, x2: width - margin, y2: py, color, dash });
30249
30672
  if (guide.label) {
30250
- out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color: "#111827", fontSize: 11, align: "right" });
30673
+ out.push({ type: "text", x: width - margin - 4, y: py - 8, text: guide.label, color, fontSize: 11, align: "right" });
30251
30674
  }
30252
30675
  }
30253
30676
  }
@@ -30313,7 +30736,7 @@ var init_MathCanvas = __esm({
30313
30736
  x: (x1 + x2) / 2,
30314
30737
  y: xAxisY - peak - 8,
30315
30738
  text: hop.label,
30316
- color: "#111827",
30739
+ color,
30317
30740
  fontSize: 10,
30318
30741
  align: "center"
30319
30742
  });
@@ -30340,7 +30763,7 @@ var init_MathCanvas = __esm({
30340
30763
  x: mapX(angle.x + 1.35 * radius * Math.cos(rad)),
30341
30764
  y: mapY(angle.y + 1.35 * radius * Math.sin(rad)),
30342
30765
  text: angle.label,
30343
- color: "#111827",
30766
+ color,
30344
30767
  fontSize: 11,
30345
30768
  align: "center"
30346
30769
  });
@@ -30358,7 +30781,7 @@ var init_MathCanvas = __esm({
30358
30781
  fill: isOpen ? "#ffffff" : p.color ?? "#dc2626"
30359
30782
  });
30360
30783
  if (p.label) {
30361
- out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: "#111827", fontSize: 12 });
30784
+ out.push({ type: "text", x: mapX(p.x) + 8, y: mapY(p.y) - 8, text: p.label, color: p.color ?? "#111827", fontSize: 12 });
30362
30785
  }
30363
30786
  }
30364
30787
  for (const v of vectors) {
@@ -30369,7 +30792,7 @@ var init_MathCanvas = __esm({
30369
30792
  const y2 = mapY(v.y + v.vy);
30370
30793
  out.push({ type: "arrow", x1, y1, x2, y2, color: v.color ?? "#7c3aed", lineWidth: 2 });
30371
30794
  if (v.label) {
30372
- out.push({ type: "text", x: x2 + 6, y: y2 - 6, text: v.label, color: "#111827", fontSize: 12 });
30795
+ out.push({ type: "text", x: x2 + 6, y: y2 - 6, text: v.label, color: v.color ?? "#7c3aed", fontSize: 12 });
30373
30796
  }
30374
30797
  }
30375
30798
  out.push(...shapes);
@@ -31633,13 +32056,13 @@ var init_MapView = __esm({
31633
32056
  shadowSize: [41, 41]
31634
32057
  });
31635
32058
  L.Marker.prototype.options.icon = defaultIcon;
31636
- const { useEffect: useEffect64, useRef: useRef63, useCallback: useCallback96, useState: useState94 } = React87__namespace.default;
32059
+ const { useEffect: useEffect65, useRef: useRef64, useCallback: useCallback97, useState: useState95 } = React87__namespace.default;
31637
32060
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31638
32061
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31639
32062
  function MapUpdater({ centerLat, centerLng, zoom }) {
31640
32063
  const map = useMap();
31641
- const prevRef = useRef63({ centerLat, centerLng, zoom });
31642
- useEffect64(() => {
32064
+ const prevRef = useRef64({ centerLat, centerLng, zoom });
32065
+ useEffect65(() => {
31643
32066
  const prev = prevRef.current;
31644
32067
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
31645
32068
  map.setView([centerLat, centerLng], zoom);
@@ -31650,7 +32073,7 @@ var init_MapView = __esm({
31650
32073
  }
31651
32074
  function MapClickHandler({ onMapClick }) {
31652
32075
  const map = useMap();
31653
- useEffect64(() => {
32076
+ useEffect65(() => {
31654
32077
  if (!onMapClick) return;
31655
32078
  const handler = (e) => {
31656
32079
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -31678,8 +32101,8 @@ var init_MapView = __esm({
31678
32101
  showAttribution = true
31679
32102
  }) {
31680
32103
  const eventBus = useEventBus2();
31681
- const [clickedPosition, setClickedPosition] = useState94(null);
31682
- const handleMapClick = useCallback96((lat, lng) => {
32104
+ const [clickedPosition, setClickedPosition] = useState95(null);
32105
+ const handleMapClick = useCallback97((lat, lng) => {
31683
32106
  if (showClickedPin) {
31684
32107
  setClickedPosition({ lat, lng });
31685
32108
  }
@@ -31688,7 +32111,7 @@ var init_MapView = __esm({
31688
32111
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
31689
32112
  }
31690
32113
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
31691
- const handleMarkerClick = useCallback96((marker) => {
32114
+ const handleMarkerClick = useCallback97((marker) => {
31692
32115
  onMarkerClick?.(marker);
31693
32116
  if (markerClickEvent) {
31694
32117
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -41920,6 +42343,7 @@ var init_DetailPanel = __esm({
41920
42343
  init_Box();
41921
42344
  init_Stack();
41922
42345
  init_SimpleGrid();
42346
+ init_Menu();
41923
42347
  init_LoadingState();
41924
42348
  init_ErrorState();
41925
42349
  init_EmptyState();
@@ -41937,6 +42361,7 @@ var init_DetailPanel = __esm({
41937
42361
  avatar,
41938
42362
  sections: propSections,
41939
42363
  actions,
42364
+ maxInlineActions,
41940
42365
  backAction,
41941
42366
  footer,
41942
42367
  slideOver = false,
@@ -42171,7 +42596,7 @@ var init_DetailPanel = __esm({
42171
42596
  }
42172
42597
  ) }),
42173
42598
  /* @__PURE__ */ jsxRuntime.jsxs(HStack, { justify: "end", align: "center", gap: "xs", children: [
42174
- otherActions.map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
42599
+ (maxInlineActions != null ? otherActions.slice(0, maxInlineActions) : otherActions).map((action, idx) => /* @__PURE__ */ jsxRuntime.jsx(
42175
42600
  Button,
42176
42601
  {
42177
42602
  variant: action.variant || "secondary",
@@ -42186,6 +42611,22 @@ var init_DetailPanel = __esm({
42186
42611
  },
42187
42612
  idx
42188
42613
  )),
42614
+ maxInlineActions != null && otherActions.length > maxInlineActions && /* @__PURE__ */ jsxRuntime.jsx(
42615
+ Menu,
42616
+ {
42617
+ position: "bottom-end",
42618
+ trigger: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "ghost", size: "sm", "aria-label": t("common.actions"), "data-testid": "action-overflow", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "more-horizontal", size: "xs" }) }),
42619
+ items: otherActions.slice(maxInlineActions).map((action) => ({
42620
+ // ONE firing path: onClick → handleActionClick (emits with
42621
+ // the {id, row} payload). Passing `event` too would make
42622
+ // Menu emit a second, payload-less copy of the same event.
42623
+ label: action.label,
42624
+ icon: action.icon,
42625
+ variant: action.variant === "danger" ? "danger" : "default",
42626
+ onClick: () => handleActionClick(action, normalizedData)
42627
+ }))
42628
+ }
42629
+ ),
42189
42630
  /* @__PURE__ */ jsxRuntime.jsx(
42190
42631
  Button,
42191
42632
  {
@@ -48042,6 +48483,7 @@ var init_component_registry_generated = __esm({
48042
48483
  init_DocSidebar();
48043
48484
  init_DocTOC();
48044
48485
  init_DocumentViewer();
48486
+ init_DrawFxLayer();
48045
48487
  init_DrawGroup();
48046
48488
  init_DrawMesh();
48047
48489
  init_DrawShape();
@@ -48072,6 +48514,7 @@ var init_component_registry_generated = __esm({
48072
48514
  init_FormField();
48073
48515
  init_FormSection();
48074
48516
  init_FormSectionHeader();
48517
+ init_FxOverlay();
48075
48518
  init_GameAudioToggle();
48076
48519
  init_GameHud();
48077
48520
  init_GameIcon();
@@ -48312,6 +48755,7 @@ var init_component_registry_generated = __esm({
48312
48755
  "DocSidebar": DocSidebar,
48313
48756
  "DocTOC": DocTOC,
48314
48757
  "DocumentViewer": DocumentViewer,
48758
+ "DrawFxLayer": DrawFxLayer,
48315
48759
  "DrawGroup": DrawGroup,
48316
48760
  "DrawMesh": DrawMesh,
48317
48761
  "DrawShape": DrawShape,
@@ -48342,6 +48786,7 @@ var init_component_registry_generated = __esm({
48342
48786
  "FormField": FormField,
48343
48787
  "FormLayout": FormLayout,
48344
48788
  "FormSectionHeader": FormSectionHeader,
48789
+ "FxOverlay": FxOverlay,
48345
48790
  "GameAudioToggle": GameAudioToggle,
48346
48791
  "GameHud": GameHud,
48347
48792
  "GameIcon": GameIcon,
@@ -50806,12 +51251,18 @@ function ServerBridgeProvider({
50806
51251
  } else if (effectType === "navigate") {
50807
51252
  const route = effect[1];
50808
51253
  const rawParams = effect[2];
51254
+ const rawOptions = effect[3];
51255
+ const optionsObj = rawOptions !== null && rawOptions !== void 0 && typeof rawOptions === "object" && !Array.isArray(rawOptions) ? rawOptions : void 0;
51256
+ const crumb = typeof optionsObj?.crumb === "string" ? optionsObj.crumb : void 0;
50809
51257
  effects.push({
50810
51258
  type: "navigate",
50811
51259
  route,
50812
51260
  params: rawParams !== null && rawParams !== void 0 && typeof rawParams === "object" && !Array.isArray(rawParams) ? rawParams : void 0,
51261
+ crumb,
50813
51262
  traitName
50814
51263
  });
51264
+ } else if (effectType === "navigate-back") {
51265
+ effects.push({ type: "navigate-back", traitName });
50815
51266
  } else if (effectType === "notify") {
50816
51267
  const message = effect[1];
50817
51268
  effects.push({ type: "notify", message: typeof message === "string" ? message : void 0, traitName });
@@ -51076,6 +51527,170 @@ function GameAudioProvider2({
51076
51527
  return /* @__PURE__ */ jsxRuntime.jsx(GameAudioContext2.Provider, { value, children });
51077
51528
  }
51078
51529
  GameAudioProvider2.displayName = "GameAudioProvider";
51530
+ function derivePageLabel(name) {
51531
+ const stripped = name.endsWith("Page") && name.length > 4 ? name.slice(0, -4) : name;
51532
+ return stripped.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
51533
+ }
51534
+ function normalizePath(p) {
51535
+ let normalized = p.trim();
51536
+ if (!normalized.startsWith("/")) normalized = "/" + normalized;
51537
+ if (normalized.length > 1 && normalized.endsWith("/")) {
51538
+ normalized = normalized.slice(0, -1);
51539
+ }
51540
+ return normalized;
51541
+ }
51542
+ function matchNavPage(pages, path) {
51543
+ const hit = providers.matchPathAmong(pages, path, (p) => p.path);
51544
+ return hit ? hit.candidate : null;
51545
+ }
51546
+ function seedAncestors(pages, path) {
51547
+ const segments = normalizePath(path).split("/").filter(Boolean);
51548
+ const ancestors = [];
51549
+ for (let depth = 1; depth < segments.length; depth++) {
51550
+ const prefix = "/" + segments.slice(0, depth).join("/");
51551
+ const page = matchNavPage(pages, prefix);
51552
+ if (page) {
51553
+ ancestors.push({ href: prefix, label: derivePageLabel(page.name) });
51554
+ }
51555
+ }
51556
+ return ancestors;
51557
+ }
51558
+ function syncNavStack(state, pages, path, pending) {
51559
+ const normalized = normalizePath(path);
51560
+ const page = matchNavPage(pages, normalized);
51561
+ if (!page) return { state, orbital: null };
51562
+ const label = pending && normalizePath(pending.href) === normalized ? pending.crumb : derivePageLabel(page.name);
51563
+ const prior = state[page.orbital];
51564
+ const stack = prior && prior.length > 0 ? [...prior] : seedAncestors(pages, normalized);
51565
+ const existing = stack.findIndex((e) => e.href === normalized);
51566
+ if (existing >= 0) {
51567
+ stack.length = existing + 1;
51568
+ if (pending && normalizePath(pending.href) === normalized) {
51569
+ stack[existing] = { href: normalized, label };
51570
+ }
51571
+ } else {
51572
+ stack.push({ href: normalized, label });
51573
+ }
51574
+ return { state: { ...state, [page.orbital]: stack }, orbital: page.orbital };
51575
+ }
51576
+ function previousEntry(state, pages, path) {
51577
+ const page = matchNavPage(pages, normalizePath(path));
51578
+ if (!page) return null;
51579
+ const stack = state[page.orbital] ?? [];
51580
+ return stack.length >= 2 ? stack[stack.length - 2] : null;
51581
+ }
51582
+ function entriesFor(state, pages, path) {
51583
+ const page = matchNavPage(pages, normalizePath(path));
51584
+ if (!page) return [];
51585
+ return state[page.orbital] ?? [];
51586
+ }
51587
+ var INERT_API = {
51588
+ entries: [],
51589
+ canGoBack: false,
51590
+ beginNavigate: () => void 0,
51591
+ back: () => void 0,
51592
+ goTo: () => void 0
51593
+ };
51594
+ var NavStackContext = React87.createContext(INERT_API);
51595
+ function useNavStack2() {
51596
+ return React87.useContext(NavStackContext);
51597
+ }
51598
+ function loadStored(storageKey) {
51599
+ if (!storageKey) return {};
51600
+ try {
51601
+ const raw = window.sessionStorage.getItem(storageKey);
51602
+ if (!raw) return {};
51603
+ const parsed = JSON.parse(raw);
51604
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
51605
+ return parsed;
51606
+ }
51607
+ return {};
51608
+ } catch {
51609
+ return {};
51610
+ }
51611
+ }
51612
+ function persistStored(storageKey, state) {
51613
+ if (!storageKey) return;
51614
+ try {
51615
+ window.sessionStorage.setItem(storageKey, JSON.stringify(state));
51616
+ } catch {
51617
+ }
51618
+ }
51619
+ var NavStackProvider = ({
51620
+ pages,
51621
+ currentPath,
51622
+ navigate,
51623
+ storageKey,
51624
+ children
51625
+ }) => {
51626
+ const [state, setState] = React87.useState(() => loadStored(storageKey));
51627
+ const pendingCrumbRef = React87.useRef(null);
51628
+ const stateRef = React87.useRef(state);
51629
+ stateRef.current = state;
51630
+ React87.useEffect(() => {
51631
+ const pending = pendingCrumbRef.current;
51632
+ pendingCrumbRef.current = null;
51633
+ setState((prev) => {
51634
+ const next = syncNavStack(prev, pages, currentPath, pending);
51635
+ if (next.state !== prev) persistStored(storageKey, next.state);
51636
+ return next.state;
51637
+ });
51638
+ }, [currentPath, pages, storageKey]);
51639
+ const beginNavigate = React87.useCallback((href, crumb) => {
51640
+ pendingCrumbRef.current = crumb !== void 0 && crumb !== "" ? { href, crumb } : null;
51641
+ }, []);
51642
+ const back = React87.useCallback(() => {
51643
+ const prev = previousEntry(stateRef.current, pages, currentPath);
51644
+ if (prev) navigate(prev.href);
51645
+ }, [pages, currentPath, navigate]);
51646
+ const goTo = React87.useCallback(
51647
+ (href) => {
51648
+ navigate(href);
51649
+ },
51650
+ [navigate]
51651
+ );
51652
+ const entries = React87.useMemo(
51653
+ () => entriesFor(state, pages, currentPath),
51654
+ [state, pages, currentPath]
51655
+ );
51656
+ const api = React87.useMemo(
51657
+ () => ({
51658
+ entries,
51659
+ canGoBack: previousEntry(state, pages, currentPath) !== null,
51660
+ beginNavigate,
51661
+ back,
51662
+ goTo
51663
+ }),
51664
+ [entries, state, pages, currentPath, beginNavigate, back, goTo]
51665
+ );
51666
+ return /* @__PURE__ */ jsxRuntime.jsx(NavStackContext.Provider, { value: api, children });
51667
+ };
51668
+ NavStackProvider.displayName = "NavStackProvider";
51669
+ var NavStackRouterBridge = ({
51670
+ pages,
51671
+ storageKey = "almadar:navstack",
51672
+ children
51673
+ }) => {
51674
+ const location = reactRouterDom.useLocation();
51675
+ const routerNavigate = reactRouterDom.useNavigate();
51676
+ const navigate = React87.useCallback(
51677
+ (path) => {
51678
+ routerNavigate(path);
51679
+ },
51680
+ [routerNavigate]
51681
+ );
51682
+ return /* @__PURE__ */ jsxRuntime.jsx(
51683
+ NavStackProvider,
51684
+ {
51685
+ pages,
51686
+ currentPath: location.pathname,
51687
+ navigate,
51688
+ storageKey,
51689
+ children
51690
+ }
51691
+ );
51692
+ };
51693
+ NavStackRouterBridge.displayName = "NavStackRouterBridge";
51079
51694
 
51080
51695
  Object.defineProperty(exports, "ANONYMOUS_USER", {
51081
51696
  enumerable: true,
@@ -51097,6 +51712,8 @@ exports.EventBusContext = EventBusContext2;
51097
51712
  exports.EventBusProvider = EventBusProvider;
51098
51713
  exports.GameAudioContext = GameAudioContext2;
51099
51714
  exports.GameAudioProvider = GameAudioProvider2;
51715
+ exports.NavStackProvider = NavStackProvider;
51716
+ exports.NavStackRouterBridge = NavStackRouterBridge;
51100
51717
  exports.NavigationProvider = NavigationProvider2;
51101
51718
  exports.OfflineModeProvider = OfflineModeProvider;
51102
51719
  exports.OrbitalProvider = OrbitalProvider;
@@ -51129,6 +51746,7 @@ exports.useGameAudioContextOptional = useGameAudioContextOptional2;
51129
51746
  exports.useHasPermission = useHasPermission;
51130
51747
  exports.useHasRole = useHasRole;
51131
51748
  exports.useInitPayload = useInitPayload2;
51749
+ exports.useNavStack = useNavStack2;
51132
51750
  exports.useNavigateTo = useNavigateTo2;
51133
51751
  exports.useNavigation = useNavigation2;
51134
51752
  exports.useNavigationId = useNavigationId2;