@almadar/ui 5.134.0 → 5.136.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 (31) hide show
  1. package/dist/{TraitProvider-Ch79cUcb.d.cts → EntityBindingContext-CD9ZoXb4.d.cts} +30 -2
  2. package/dist/{TraitProvider-Ch79cUcb.d.ts → EntityBindingContext-CD9ZoXb4.d.ts} +30 -2
  3. package/dist/avl/index.cjs +819 -429
  4. package/dist/avl/index.js +820 -430
  5. package/dist/{cn-Dm0VrLRG.d.ts → cn-CCaph5o9.d.ts} +1 -1
  6. package/dist/{cn-D3H9UzCW.d.cts → cn-CL0kdshO.d.cts} +1 -1
  7. package/dist/components/index.cjs +1649 -1482
  8. package/dist/components/index.d.cts +105 -202
  9. package/dist/components/index.d.ts +105 -202
  10. package/dist/components/index.js +705 -533
  11. package/dist/lib/drawable/three/index.cjs +16 -0
  12. package/dist/lib/drawable/three/index.d.cts +1 -1
  13. package/dist/lib/drawable/three/index.d.ts +1 -1
  14. package/dist/lib/drawable/three/index.js +16 -0
  15. package/dist/lib/index.cjs +28 -0
  16. package/dist/lib/index.d.cts +21 -3
  17. package/dist/lib/index.d.ts +21 -3
  18. package/dist/lib/index.js +27 -1
  19. package/dist/marketing/index.cjs +1 -0
  20. package/dist/marketing/index.js +1 -0
  21. package/dist/{paintDispatch-BXJgISot.d.cts → paintDispatch-DXygiK7M.d.cts} +39 -10
  22. package/dist/{paintDispatch-BXJgISot.d.ts → paintDispatch-DXygiK7M.d.ts} +39 -10
  23. package/dist/providers/index.cjs +701 -323
  24. package/dist/providers/index.d.cts +3 -3
  25. package/dist/providers/index.d.ts +3 -3
  26. package/dist/providers/index.js +698 -322
  27. package/dist/runtime/index.cjs +819 -429
  28. package/dist/runtime/index.d.cts +5 -2
  29. package/dist/runtime/index.d.ts +5 -2
  30. package/dist/runtime/index.js +820 -430
  31. package/package.json +6 -5
@@ -1,8 +1,10 @@
1
1
  import * as React83 from 'react';
2
2
  import React83__default, { createContext, useContext, useMemo, useRef, useEffect, useCallback, Suspense, useState, useSyncExternalStore, useLayoutEffect, lazy, useId } from 'react';
3
- import { EventBusContext, useTraitScopeChain, useEntitySchemaOptional, useEntitySchema, getAllPages, matchPathAmong, OrbitalProvider, TraitScopeProvider, ServerBridgeProvider, useCurrentPagePath, useGameAudioContextOptional, VerificationProvider, EntitySchemaProvider, OrbitalThemeProvider, useServerBridge } from '@almadar/ui/providers';
3
+ import { EventBusContext, useTraitScopeChain, useEntitySchemaOptional, useEntityBindingSnapshot, useTraitScope, useEntitySchema, getAllPages, matchPathAmong, OrbitalProvider, TraitScopeProvider, ServerBridgeProvider, useCurrentPagePath, useGameAudioContextOptional, VerificationProvider, EntitySchemaProvider, OrbitalThemeProvider, useServerBridge, EntityBindingContext } from '@almadar/ui/providers';
4
4
  export { EntitySchemaProvider, ServerBridgeProvider, TraitContext, TraitProvider, useEntitySchema, useEntitySchemaOptional, useServerBridge, useTrait, useTraitContext } from '@almadar/ui/providers';
5
5
  import { createLogger, setNamespaceLevel, isLogLevelEnabled } from '@almadar/logger';
6
+ import { StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, InMemoryPersistence, normalizeCallSiteConfigToValues, interpolateValue } from '@almadar/runtime';
7
+ import { mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, schemaToIR, getPage, clearSchemaCache as clearSchemaCache$1, walkSExpr, buildResolvedTraitConfigs, isRenderBindingMarker, isInlineTrait, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER } from '@almadar/core';
6
8
  import { clsx } from 'clsx';
7
9
  import { twMerge } from 'tailwind-merge';
8
10
  import * as LucideIcons2 from 'lucide-react';
@@ -69,14 +71,13 @@ import ReactMarkdown from 'react-markdown';
69
71
  import remarkGfm from 'remark-gfm';
70
72
  import remarkMath from 'remark-math';
71
73
  import rehypeKatex from 'rehype-katex';
72
- import { mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, schemaToIR, getPage, clearSchemaCache as clearSchemaCache$1, walkSExpr, buildResolvedTraitConfigs, isInlineTrait, isSExpr, isEventPayloadValue } from '@almadar/core';
73
74
  import { DndContext, pointerWithin, rectIntersection, closestCorners, useSensors, useSensor, PointerSensor, KeyboardSensor, useDroppable } from '@dnd-kit/core';
74
75
  import { useSortable, arrayMove, sortableKeyboardCoordinates, SortableContext, rectSortingStrategy, verticalListSortingStrategy } from '@dnd-kit/sortable';
75
76
  import { CSS } from '@dnd-kit/utilities';
77
+ import { ordered, lib } from 'emojilib';
76
78
  import { useNodeId, ReactFlowProvider, Handle, Position } from '@xyflow/react';
77
79
  import { forceSimulation, forceLink, forceManyBody, forceCollide, forceX, forceY } from 'd3-force';
78
80
  import { isDrawHostPattern, getPatternDefinition, getComponentForPattern as getComponentForPattern$1 } from '@almadar/core/patterns';
79
- import { StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, InMemoryPersistence, normalizeCallSiteConfigToValues } from '@almadar/runtime';
80
81
  import { isKnownStdOperator } from '@almadar/std/registry';
81
82
  import { OrbitalServerRuntime } from '@almadar/runtime/OrbitalServerRuntime';
82
83
 
@@ -812,6 +813,75 @@ var init_useTapReveal = __esm({
812
813
  "hooks/useTapReveal.ts"() {
813
814
  }
814
815
  });
816
+ function resolveMarkerExpression(expression, entity, config, state) {
817
+ const ctx = createContextFromBindings({
818
+ entity,
819
+ payload: {},
820
+ state,
821
+ ...config !== void 0 ? { config } : {}
822
+ });
823
+ return interpolateValue(expression, ctx);
824
+ }
825
+ function isPlainObject(value) {
826
+ if (value === null || value === void 0 || typeof value !== "object") return false;
827
+ if (Array.isArray(value)) return false;
828
+ if (React83__default.isValidElement(value)) return false;
829
+ if (value instanceof Date) return false;
830
+ if (typeof value === "function") return false;
831
+ return true;
832
+ }
833
+ function walkValue(value, scopeTrait, entity, config, state) {
834
+ if (isRenderBindingMarker(value)) {
835
+ return { resolved: resolveMarkerExpression(value.expression, entity, config, state), changed: true };
836
+ }
837
+ if (Array.isArray(value)) {
838
+ const out = [];
839
+ let changed = false;
840
+ for (const item of value) {
841
+ const element = item;
842
+ const wasMarker = isRenderBindingMarker(element);
843
+ const { resolved, changed: itemChanged } = walkValue(element, scopeTrait, entity, config, state);
844
+ if (wasMarker && Array.isArray(resolved)) {
845
+ out.push(...resolved);
846
+ changed = true;
847
+ continue;
848
+ }
849
+ out.push(resolved);
850
+ if (itemChanged) changed = true;
851
+ }
852
+ return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
853
+ }
854
+ if (isPlainObject(value)) {
855
+ const sourceTrait = value._sourceTrait;
856
+ if (typeof sourceTrait === "string" && sourceTrait !== scopeTrait) {
857
+ return { resolved: value, changed: false };
858
+ }
859
+ const out = {};
860
+ let changed = false;
861
+ for (const [key, item] of Object.entries(value)) {
862
+ const { resolved, changed: itemChanged } = walkValue(item, scopeTrait, entity, config, state);
863
+ out[key] = resolved;
864
+ if (itemChanged) changed = true;
865
+ }
866
+ return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
867
+ }
868
+ return { resolved: value, changed: false };
869
+ }
870
+ function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
871
+ const out = {};
872
+ let changed = false;
873
+ for (const [key, value] of Object.entries(props)) {
874
+ const { resolved, changed: propChanged } = walkValue(value, scopeTrait, entity, config, state);
875
+ out[key] = resolved;
876
+ if (propChanged) changed = true;
877
+ }
878
+ return changed ? out : props;
879
+ }
880
+ var init_resolve_render_bindings = __esm({
881
+ "lib/resolve-render-bindings.ts"() {
882
+ "use client";
883
+ }
884
+ });
815
885
  function cn(...inputs) {
816
886
  return twMerge(clsx(inputs));
817
887
  }
@@ -1272,9 +1342,13 @@ function getAtlas(url, onReady) {
1272
1342
  onReady();
1273
1343
  }).catch(() => {
1274
1344
  atlasCache.set(url, null);
1345
+ onReady();
1275
1346
  });
1276
1347
  return void 0;
1277
1348
  }
1349
+ function atlasFailed(url) {
1350
+ return atlasCache.get(url) === null;
1351
+ }
1278
1352
  function subRectFor(atlas, sprite) {
1279
1353
  if (isTilesheet(atlas)) {
1280
1354
  let col;
@@ -1695,6 +1769,32 @@ function formatValue(value, format) {
1695
1769
  return String(value);
1696
1770
  }
1697
1771
  }
1772
+ function compareCellValues(a, b) {
1773
+ const aEmpty = a === null || a === void 0 || a === "";
1774
+ const bEmpty = b === null || b === void 0 || b === "";
1775
+ if (aEmpty || bEmpty) return aEmpty && bEmpty ? 0 : aEmpty ? 1 : -1;
1776
+ if (typeof a === "number" && typeof b === "number") return a - b;
1777
+ if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
1778
+ const aNum = Number(a);
1779
+ const bNum = Number(b);
1780
+ if (Number.isFinite(aNum) && Number.isFinite(bNum)) return aNum - bNum;
1781
+ const aTime = dateLikeTime(a);
1782
+ const bTime = dateLikeTime(b);
1783
+ if (aTime !== null && bTime !== null) return aTime - bTime;
1784
+ return String(a).localeCompare(String(b));
1785
+ }
1786
+ function dateLikeTime(value) {
1787
+ if (value instanceof Date) return value.getTime();
1788
+ const text = String(value);
1789
+ if (!/\d/.test(text) || !/[-/:T]/.test(text)) return null;
1790
+ const time = Date.parse(text);
1791
+ return Number.isNaN(time) ? null : time;
1792
+ }
1793
+ function sortRows(rows, field, direction = "asc") {
1794
+ if (!field) return rows;
1795
+ const dir = direction === "desc" ? -1 : 1;
1796
+ return [...rows].sort((a, b) => dir * compareCellValues(a?.[field], b?.[field]));
1797
+ }
1698
1798
  var init_format = __esm({
1699
1799
  "lib/format.ts"() {
1700
1800
  }
@@ -3517,6 +3617,20 @@ var init_Input = __esm({
3517
3617
  const { t } = useTranslate();
3518
3618
  const eventBus = useEventBus();
3519
3619
  const type = inputType || htmlType || "text";
3620
+ const isDeclarative = typeof onChange === "string";
3621
+ const [localValue, setLocalValue] = React83__default.useState(value);
3622
+ const pendingEchoRef = React83__default.useRef(/* @__PURE__ */ new Set());
3623
+ React83__default.useEffect(() => {
3624
+ if (!isDeclarative) return;
3625
+ const incoming = value == null ? "" : String(value);
3626
+ if (pendingEchoRef.current.has(incoming)) {
3627
+ pendingEchoRef.current.delete(incoming);
3628
+ return;
3629
+ }
3630
+ pendingEchoRef.current.clear();
3631
+ setLocalValue(value);
3632
+ }, [value, isDeclarative]);
3633
+ const displayValue = isDeclarative ? localValue : value;
3520
3634
  const resolveIconNode = (i, cls) => {
3521
3635
  if (!i) return null;
3522
3636
  if (typeof i === "string") return /* @__PURE__ */ jsx(Icon, { name: i, className: cls });
@@ -3526,7 +3640,7 @@ var init_Input = __esm({
3526
3640
  const iconCls = "h-icon-default w-icon-default";
3527
3641
  const IconComponent = typeof iconProp === "string" ? resolveIcon(iconProp) : iconProp;
3528
3642
  const resolvedLeftIcon = (leftIcon ? resolveIconNode(leftIcon, iconCls) : null) || IconComponent && /* @__PURE__ */ jsx(IconComponent, { className: iconCls });
3529
- const showClearButton = clearable && value && String(value).length > 0;
3643
+ const showClearButton = clearable && displayValue && String(displayValue).length > 0;
3530
3644
  const isMultiline = type === "textarea";
3531
3645
  const baseClassName = cn(
3532
3646
  "block w-full rounded-sm transition-all duration-fast",
@@ -3545,6 +3659,10 @@ var init_Input = __esm({
3545
3659
  if (typeof onChange === "string") {
3546
3660
  const target = e.target;
3547
3661
  const payload = type === "checkbox" ? { checked: target.checked } : { value: target.value };
3662
+ if (type !== "checkbox") {
3663
+ pendingEchoRef.current.add(target.value);
3664
+ setLocalValue(target.value);
3665
+ }
3548
3666
  eventBus.emit(`UI:${onChange}`, payload);
3549
3667
  } else {
3550
3668
  onChange?.(e);
@@ -3575,7 +3693,7 @@ var init_Input = __esm({
3575
3693
  "select",
3576
3694
  {
3577
3695
  ref,
3578
- value,
3696
+ value: displayValue,
3579
3697
  onChange: handleChange,
3580
3698
  className: cn(baseClassName, "appearance-none pr-10", className),
3581
3699
  ...props,
@@ -3595,7 +3713,7 @@ var init_Input = __esm({
3595
3713
  "textarea",
3596
3714
  {
3597
3715
  ref,
3598
- value,
3716
+ value: displayValue,
3599
3717
  onChange: handleChange,
3600
3718
  rows,
3601
3719
  className: baseClassName,
@@ -3634,7 +3752,7 @@ var init_Input = __esm({
3634
3752
  {
3635
3753
  ref,
3636
3754
  type,
3637
- value,
3755
+ value: displayValue,
3638
3756
  onChange: handleChange,
3639
3757
  onKeyDown: handleKeyDown,
3640
3758
  className: baseClassName,
@@ -7521,46 +7639,45 @@ var init_useImageCache = __esm({
7521
7639
  });
7522
7640
 
7523
7641
  // lib/isometric.ts
7524
- function isoToScreen(tileX, tileY, scale, baseOffsetX, layout = "isometric") {
7525
- const scaledTileWidth = TILE_WIDTH * scale;
7526
- const scaledFloorHeight = FLOOR_HEIGHT * scale;
7642
+ function isoToScreen(tileX, tileY, cellWidth, baseOffsetX, layout = "isometric") {
7643
+ const w = cellWidth;
7644
+ const fh = cellWidth / 2;
7527
7645
  if (layout === "hex") {
7528
- const screenX2 = tileX * scaledTileWidth + (tileY & 1) * (scaledTileWidth / 2) + baseOffsetX;
7529
- const screenY2 = tileY * (scaledFloorHeight * 0.75);
7646
+ const screenX2 = tileX * w + (tileY & 1) * (w / 2) + baseOffsetX;
7647
+ const screenY2 = tileY * (fh * 0.75);
7530
7648
  return { x: screenX2, y: screenY2 };
7531
7649
  }
7532
7650
  if (layout === "flat") {
7533
- const screenX2 = tileX * scaledTileWidth + baseOffsetX;
7534
- const screenY2 = tileY * scaledTileWidth;
7651
+ const screenX2 = tileX * w + baseOffsetX;
7652
+ const screenY2 = tileY * w;
7535
7653
  return { x: screenX2, y: screenY2 };
7536
7654
  }
7537
- const screenX = (tileX - tileY) * (scaledTileWidth / 2) + baseOffsetX;
7538
- const screenY = (tileX + tileY) * (scaledFloorHeight / 2);
7655
+ const screenX = (tileX - tileY) * (w / 2) + baseOffsetX;
7656
+ const screenY = (tileX + tileY) * (fh / 2);
7539
7657
  return { x: screenX, y: screenY };
7540
7658
  }
7541
- function screenToIso(screenX, screenY, scale, baseOffsetX, layout = "isometric") {
7542
- const scaledTileWidth = TILE_WIDTH * scale;
7543
- const scaledFloorHeight = FLOOR_HEIGHT * scale;
7659
+ function screenToIso(screenX, screenY, cellWidth, baseOffsetX, layout = "isometric") {
7660
+ const w = cellWidth;
7661
+ const fh = cellWidth / 2;
7544
7662
  if (layout === "hex") {
7545
- const row = Math.round(screenY / (scaledFloorHeight * 0.75));
7546
- const col = Math.round((screenX - (row & 1) * (scaledTileWidth / 2) - baseOffsetX) / scaledTileWidth);
7663
+ const row = Math.round(screenY / (fh * 0.75));
7664
+ const col = Math.round((screenX - (row & 1) * (w / 2) - baseOffsetX) / w);
7547
7665
  return { x: col, y: row };
7548
7666
  }
7549
7667
  if (layout === "flat") {
7550
- const col = Math.round((screenX - baseOffsetX) / scaledTileWidth);
7551
- const row = Math.round(screenY / scaledTileWidth);
7668
+ const col = Math.round((screenX - baseOffsetX) / w);
7669
+ const row = Math.round(screenY / w);
7552
7670
  return { x: col, y: row };
7553
7671
  }
7554
7672
  const adjustedX = screenX - baseOffsetX;
7555
- const tileX = (adjustedX / (scaledTileWidth / 2) + screenY / (scaledFloorHeight / 2)) / 2;
7556
- const tileY = (screenY / (scaledFloorHeight / 2) - adjustedX / (scaledTileWidth / 2)) / 2;
7673
+ const tileX = (adjustedX / (w / 2) + screenY / (fh / 2)) / 2;
7674
+ const tileY = (screenY / (fh / 2) - adjustedX / (w / 2)) / 2;
7557
7675
  return { x: Math.round(tileX), y: Math.round(tileY) };
7558
7676
  }
7559
- var TILE_WIDTH, FLOOR_HEIGHT, DIAMOND_TOP_Y, BACKGROUND_FALLBACK_COLOR, MINIMAP_TERRAIN_COLORS;
7677
+ var TILE_WIDTH, DIAMOND_TOP_Y, BACKGROUND_FALLBACK_COLOR, MINIMAP_TERRAIN_COLORS;
7560
7678
  var init_isometric = __esm({
7561
7679
  "lib/isometric.ts"() {
7562
7680
  TILE_WIDTH = 256;
7563
- FLOOR_HEIGHT = 128;
7564
7681
  DIAMOND_TOP_Y = 374;
7565
7682
  BACKGROUND_FALLBACK_COLOR = "#1a1a2e";
7566
7683
  MINIMAP_TERRAIN_COLORS = {
@@ -8045,210 +8162,6 @@ var init_ChoiceButton = __esm({
8045
8162
  ChoiceButton.displayName = "ChoiceButton";
8046
8163
  }
8047
8164
  });
8048
- function SvgStage({
8049
- cols,
8050
- rows,
8051
- tileSize = 32,
8052
- background = "var(--color-background)",
8053
- tileClickEvent,
8054
- tileHoverEvent,
8055
- tileLeaveEvent,
8056
- keyMap,
8057
- keyUpMap,
8058
- className,
8059
- children
8060
- }) {
8061
- const eventBus = useEventBus();
8062
- const svgRef = useRef(null);
8063
- const pointerDownRef = useRef(null);
8064
- const cellFromClient = useCallback((clientX, clientY) => {
8065
- const svg = svgRef.current;
8066
- if (!svg) return null;
8067
- const rect = svg.getBoundingClientRect();
8068
- if (rect.width === 0 || rect.height === 0) return null;
8069
- const vbW = cols * tileSize;
8070
- const vbH = rows * tileSize;
8071
- const meet = Math.min(rect.width / vbW, rect.height / vbH);
8072
- const offsetX = (rect.width - vbW * meet) / 2;
8073
- const offsetY = (rect.height - vbH * meet) / 2;
8074
- const svgX = (clientX - rect.left - offsetX) / meet;
8075
- const svgY = (clientY - rect.top - offsetY) / meet;
8076
- return {
8077
- x: Math.min(Math.max(Math.floor(svgX / tileSize), 0), cols - 1),
8078
- y: Math.min(Math.max(Math.floor(svgY / tileSize), 0), rows - 1)
8079
- };
8080
- }, [cols, rows, tileSize]);
8081
- const handlePointerDown = useCallback((e) => {
8082
- pointerDownRef.current = { clientX: e.clientX, clientY: e.clientY };
8083
- }, []);
8084
- const handlePointerUp = useCallback((e) => {
8085
- const down = pointerDownRef.current;
8086
- pointerDownRef.current = null;
8087
- if (!tileClickEvent) return;
8088
- if (down && Math.hypot(e.clientX - down.clientX, e.clientY - down.clientY) > 5) return;
8089
- const cell = cellFromClient(e.clientX, e.clientY);
8090
- if (cell) eventBus.emit(`UI:${tileClickEvent}`, cell);
8091
- }, [cellFromClient, tileClickEvent, eventBus]);
8092
- const handlePointerMove = useCallback((e) => {
8093
- if (!tileHoverEvent) return;
8094
- const cell = cellFromClient(e.clientX, e.clientY);
8095
- if (cell) eventBus.emit(`UI:${tileHoverEvent}`, cell);
8096
- }, [cellFromClient, tileHoverEvent, eventBus]);
8097
- const handlePointerLeave = useCallback(() => {
8098
- pointerDownRef.current = null;
8099
- if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
8100
- }, [tileLeaveEvent, eventBus]);
8101
- useEffect(() => {
8102
- if (!keyMap && !keyUpMap) return;
8103
- const onDown = (e) => {
8104
- const ev = keyMap?.[e.code];
8105
- if (ev) {
8106
- eventBus.emit(`UI:${ev}`, {});
8107
- e.preventDefault();
8108
- }
8109
- };
8110
- const onUp = (e) => {
8111
- const ev = keyUpMap?.[e.code];
8112
- if (ev) eventBus.emit(`UI:${ev}`, {});
8113
- };
8114
- window.addEventListener("keydown", onDown);
8115
- window.addEventListener("keyup", onUp);
8116
- return () => {
8117
- window.removeEventListener("keydown", onDown);
8118
- window.removeEventListener("keyup", onUp);
8119
- };
8120
- }, [keyMap, keyUpMap, eventBus]);
8121
- useEffect(() => {
8122
- if (!keyMap && !keyUpMap) return;
8123
- svgRef.current?.focus();
8124
- }, [keyMap, keyUpMap]);
8125
- const stageContext = useMemo(() => ({ tileSize }), [tileSize]);
8126
- return /* @__PURE__ */ jsxs(
8127
- "svg",
8128
- {
8129
- ref: svgRef,
8130
- "data-testid": "svg-stage",
8131
- viewBox: `0 0 ${cols * tileSize} ${rows * tileSize}`,
8132
- preserveAspectRatio: "xMidYMid meet",
8133
- className: cn("block h-full w-full", className),
8134
- tabIndex: keyMap || keyUpMap ? 0 : void 0,
8135
- onPointerDown: handlePointerDown,
8136
- onPointerMove: handlePointerMove,
8137
- onPointerUp: handlePointerUp,
8138
- onPointerLeave: handlePointerLeave,
8139
- children: [
8140
- /* @__PURE__ */ jsx("rect", { width: cols * tileSize, height: rows * tileSize, fill: background }),
8141
- /* @__PURE__ */ jsx(SvgStageContext.Provider, { value: stageContext, children })
8142
- ]
8143
- }
8144
- );
8145
- }
8146
- var SvgStageContext;
8147
- var init_SvgStage = __esm({
8148
- "components/game/molecules/SvgStage.tsx"() {
8149
- "use client";
8150
- init_cn();
8151
- init_useEventBus();
8152
- SvgStageContext = React83.createContext({ tileSize: 1 });
8153
- SvgStage.displayName = "SvgStage";
8154
- }
8155
- });
8156
- function SvgDrawShape({
8157
- shape,
8158
- x,
8159
- y,
8160
- width,
8161
- height,
8162
- radius,
8163
- radiusY,
8164
- points,
8165
- d,
8166
- x2,
8167
- y2,
8168
- fill,
8169
- stroke,
8170
- strokeWidth,
8171
- opacity,
8172
- className
8173
- }) {
8174
- const { tileSize } = useContext(SvgStageContext);
8175
- const cell = (v) => v === void 0 ? void 0 : v * tileSize;
8176
- const paint = {
8177
- fill: fill ?? (stroke === void 0 ? "var(--color-primary)" : "none"),
8178
- stroke,
8179
- strokeWidth,
8180
- opacity,
8181
- className
8182
- };
8183
- return /* @__PURE__ */ jsxs("g", { transform: `translate(${x * tileSize} ${y * tileSize})`, children: [
8184
- shape === "rect" && /* @__PURE__ */ jsx("rect", { width: cell(width), height: cell(height), ...paint }),
8185
- shape === "circle" && /* @__PURE__ */ jsx("circle", { r: cell(radius), ...paint }),
8186
- shape === "ellipse" && /* @__PURE__ */ jsx("ellipse", { rx: cell(radius), ry: cell(radiusY ?? radius), ...paint }),
8187
- shape === "polygon" && /* @__PURE__ */ jsx("polygon", { points, ...paint }),
8188
- shape === "polyline" && /* @__PURE__ */ jsx("polyline", { points, ...paint }),
8189
- shape === "path" && /* @__PURE__ */ jsx("path", { d, ...paint }),
8190
- shape === "line" && /* @__PURE__ */ jsx("line", { x2: cell(x2), y2: cell(y2), ...paint })
8191
- ] });
8192
- }
8193
- var init_SvgDrawShape = __esm({
8194
- "components/game/atoms/SvgDrawShape.tsx"() {
8195
- "use client";
8196
- init_SvgStage();
8197
- SvgDrawShape.displayName = "SvgDrawShape";
8198
- }
8199
- });
8200
- function SvgDrawGroup({
8201
- x = 0,
8202
- y = 0,
8203
- scale,
8204
- rotate,
8205
- opacity,
8206
- className,
8207
- children
8208
- }) {
8209
- const { tileSize } = useContext(SvgStageContext);
8210
- const transforms = [`translate(${x * tileSize} ${y * tileSize})`];
8211
- if (rotate !== void 0) transforms.push(`rotate(${rotate})`);
8212
- if (scale !== void 0) transforms.push(`scale(${scale})`);
8213
- return /* @__PURE__ */ jsx("g", { transform: transforms.join(" "), opacity, className, children });
8214
- }
8215
- var init_SvgDrawGroup = __esm({
8216
- "components/game/atoms/SvgDrawGroup.tsx"() {
8217
- "use client";
8218
- init_SvgStage();
8219
- SvgDrawGroup.displayName = "SvgDrawGroup";
8220
- }
8221
- });
8222
- function SvgDrawText({
8223
- x,
8224
- y,
8225
- text,
8226
- size = 12,
8227
- fill = "var(--color-foreground)",
8228
- anchor = "middle",
8229
- className
8230
- }) {
8231
- const { tileSize } = useContext(SvgStageContext);
8232
- return /* @__PURE__ */ jsx(
8233
- "text",
8234
- {
8235
- x: x * tileSize,
8236
- y: y * tileSize,
8237
- fontSize: size,
8238
- fill,
8239
- textAnchor: anchor,
8240
- className,
8241
- children: text
8242
- }
8243
- );
8244
- }
8245
- var init_SvgDrawText = __esm({
8246
- "components/game/atoms/SvgDrawText.tsx"() {
8247
- "use client";
8248
- init_SvgStage();
8249
- SvgDrawText.displayName = "SvgDrawText";
8250
- }
8251
- });
8252
8165
  function ControlGrid({
8253
8166
  kind,
8254
8167
  buttons = DEFAULT_BUTTONS,
@@ -8952,6 +8865,7 @@ function MiniMap({
8952
8865
  const cached = imgCacheRef.current.get(url);
8953
8866
  if (cached) return cached.complete ? cached : null;
8954
8867
  const img = new Image();
8868
+ img.crossOrigin = "anonymous";
8955
8869
  img.src = url;
8956
8870
  img.onload = () => {
8957
8871
  const canvas = canvasRef.current;
@@ -9119,7 +9033,7 @@ function useCamera(initial) {
9119
9033
  const handleWheel = useCallback((e, drawFn) => {
9120
9034
  e.preventDefault();
9121
9035
  const zoomDelta = e.deltaY > 0 ? 0.9 : 1.1;
9122
- cameraRef.current.zoom = Math.max(0.5, Math.min(3, cameraRef.current.zoom * zoomDelta));
9036
+ cameraRef.current.zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, cameraRef.current.zoom * zoomDelta));
9123
9037
  drawFn?.();
9124
9038
  }, []);
9125
9039
  const handlePointerDown = useCallback((e) => {
@@ -9140,7 +9054,7 @@ function useCamera(initial) {
9140
9054
  const zoomAtPoint = useCallback((factor, centerX, centerY, viewportSize, drawFn) => {
9141
9055
  const cam = cameraRef.current;
9142
9056
  const oldZoom = cam.zoom;
9143
- const newZoom = Math.max(0.5, Math.min(3, oldZoom * factor));
9057
+ const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, oldZoom * factor));
9144
9058
  if (newZoom === oldZoom) {
9145
9059
  drawFn?.();
9146
9060
  return;
@@ -9192,9 +9106,12 @@ function useCamera(initial) {
9192
9106
  lerpToTarget
9193
9107
  };
9194
9108
  }
9109
+ var MIN_ZOOM, MAX_ZOOM;
9195
9110
  var init_useCamera = __esm({
9196
9111
  "hooks/useCamera.ts"() {
9197
9112
  "use client";
9113
+ MIN_ZOOM = 0.05;
9114
+ MAX_ZOOM = 10;
9198
9115
  }
9199
9116
  });
9200
9117
 
@@ -9208,6 +9125,7 @@ function getOrLoadImage(url, onReady) {
9208
9125
  return null;
9209
9126
  }
9210
9127
  const img = new Image();
9128
+ img.crossOrigin = "anonymous";
9211
9129
  const entry = { img, status: "pending", onReady };
9212
9130
  cache.set(url, entry);
9213
9131
  updateAssetStatus(url, "pending");
@@ -9219,10 +9137,14 @@ function getOrLoadImage(url, onReady) {
9219
9137
  img.onerror = () => {
9220
9138
  entry.status = "failed";
9221
9139
  updateAssetStatus(url, "failed");
9140
+ entry.onReady?.();
9222
9141
  };
9223
9142
  img.src = url;
9224
9143
  return null;
9225
9144
  }
9145
+ function getImageStatus(url) {
9146
+ return cache.get(url)?.status;
9147
+ }
9226
9148
  var cache;
9227
9149
  var init_imageCache = __esm({
9228
9150
  "lib/imageCache.ts"() {
@@ -9331,6 +9253,15 @@ function createWebPainter(ctx, onAssetLoad) {
9331
9253
  ctx.lineWidth = lineWidth;
9332
9254
  ctx.stroke();
9333
9255
  },
9256
+ fillPath(d, color) {
9257
+ ctx.fillStyle = color;
9258
+ ctx.fill(new Path2D(d));
9259
+ },
9260
+ strokePath(d, color, lineWidth = 1) {
9261
+ ctx.strokeStyle = color;
9262
+ ctx.lineWidth = lineWidth;
9263
+ ctx.stroke(new Path2D(d));
9264
+ },
9334
9265
  text(str, x, y, style) {
9335
9266
  if (style.font) ctx.font = style.font;
9336
9267
  ctx.fillStyle = style.color;
@@ -9352,12 +9283,13 @@ var init_webPainter2d = __esm({
9352
9283
 
9353
9284
  // lib/drawable/projector.ts
9354
9285
  function create2DProjector(opts) {
9355
- const { scale, baseOffsetX, layout } = opts;
9356
- const tileWidth = layout === "free" ? 1 : TILE_WIDTH * scale;
9357
- const floorHeight = layout === "free" ? 1 : FLOOR_HEIGHT * scale;
9358
- const diamondTopY = layout === "free" ? 0 : (opts.diamondTopY ?? DIAMOND_TOP_Y) * scale;
9286
+ const { baseOffsetX, layout } = opts;
9287
+ const tw = opts.tileWidth ?? TILE_WIDTH;
9288
+ const tileWidth = layout === "free" ? 1 : tw;
9289
+ const floorHeight = layout === "free" ? 1 : tw / 2;
9290
+ const diamondTopY = layout === "free" ? 0 : opts.diamondTopY ?? tw * (DIAMOND_TOP_Y / TILE_WIDTH);
9359
9291
  const squareGrid = layout === "flat" || layout === "free";
9360
- const project = (pos) => layout === "free" ? { x: pos.x, y: pos.y } : isoToScreen(pos.x, pos.y, scale, baseOffsetX, layout);
9292
+ const project = (pos) => layout === "free" ? { x: pos.x, y: pos.y } : isoToScreen(pos.x, pos.y, tw, baseOffsetX, layout);
9361
9293
  const anchorPoint = (pos, anchor) => {
9362
9294
  const base = project(pos);
9363
9295
  if (anchor === "top-left") return base;
@@ -9388,7 +9320,7 @@ function create2DProjector(opts) {
9388
9320
  { x: base.x, y: topY + floorHeight / 2 }
9389
9321
  ];
9390
9322
  };
9391
- return { project, anchorPoint, cellPath, tileWidth, floorHeight, diamondTopY, scale, squareGrid };
9323
+ return { project, anchorPoint, cellPath, tileWidth, floorHeight, diamondTopY, squareGrid, worldPixelDirect: layout === "free" };
9392
9324
  }
9393
9325
  var init_projector = __esm({
9394
9326
  "lib/drawable/projector.ts"() {
@@ -9404,32 +9336,67 @@ var init_contract = __esm({
9404
9336
  "lib/drawable/contract.ts"() {
9405
9337
  }
9406
9338
  });
9407
-
9408
- // components/game/atoms/DrawSprite.tsx
9339
+ function warnMissingOnce(reason, node) {
9340
+ const key = `${reason}:${node.asset.url}:${String(node.asset.atlas)}:${String(node.asset.sprite)}`;
9341
+ if (loggedMissing.has(key)) return;
9342
+ loggedMissing.add(key);
9343
+ spriteLog.warn("draw-sprite asset unresolvable \u2014 painting fallback square", { reason, url: node.asset.url, atlas: node.asset.atlas, sprite: node.asset.sprite });
9344
+ }
9345
+ function paintFallbackSquare(painter, node, dctx, reason) {
9346
+ warnMissingOnce(reason, node);
9347
+ const tw = dctx.projector.tileWidth;
9348
+ const natural = dctx.projector.worldPixelDirect ? FALLBACK_WORLD_PX : tw;
9349
+ const w = node.width !== void 0 ? node.width * tw : natural;
9350
+ const h = node.height !== void 0 ? node.height * tw : natural;
9351
+ const anchor = node.anchor ?? "top-left";
9352
+ const p = dctx.projector.anchorPoint(node.position, anchor);
9353
+ const dx = anchor === "top-left" ? p.x : p.x - w / 2;
9354
+ const dy = anchor === "ground" ? p.y - h : anchor === "center" ? p.y - h / 2 : p.y;
9355
+ painter.save();
9356
+ if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
9357
+ painter.fillRect(dx, dy, w, h, "#9b8f7f");
9358
+ painter.strokeRect(dx, dy, w, h, "#5e564b", Math.max(1, tw / 32));
9359
+ painter.restore();
9360
+ }
9409
9361
  function DrawSprite(_props) {
9410
9362
  return null;
9411
9363
  }
9412
- var paintSprite;
9364
+ var spriteLog, loggedMissing, FALLBACK_WORLD_PX, paintSprite;
9413
9365
  var init_DrawSprite = __esm({
9414
9366
  "components/game/atoms/DrawSprite.tsx"() {
9415
9367
  "use client";
9416
9368
  init_atlasSlice();
9369
+ init_imageCache();
9417
9370
  init_contract();
9371
+ spriteLog = createLogger("almadar:ui:draw-sprite");
9372
+ loggedMissing = /* @__PURE__ */ new Set();
9373
+ FALLBACK_WORLD_PX = 32;
9418
9374
  paintSprite = (painter, node, dctx) => {
9419
9375
  if (!node.asset?.url || !isValidScenePos(node.position)) return;
9420
9376
  const tex = painter.resolveTexture(node.asset.url);
9421
- if (!tex) return;
9377
+ if (!tex) {
9378
+ if (getImageStatus(node.asset.url) === "failed") paintFallbackSquare(painter, node, dctx, "texture-failed");
9379
+ return;
9380
+ }
9422
9381
  let src = typeof node.frame === "object" ? node.frame : void 0;
9423
9382
  if (!src && isAtlasAsset(node.asset)) {
9424
9383
  const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
9425
- if (!atlas) return;
9384
+ if (!atlas) {
9385
+ if (atlasFailed(node.asset.atlas)) paintFallbackSquare(painter, node, dctx, "atlas-failed");
9386
+ return;
9387
+ }
9426
9388
  const r = subRectFor(atlas, node.asset.sprite);
9427
- if (!r) return;
9389
+ if (!r) {
9390
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
9391
+ return;
9392
+ }
9428
9393
  src = { x: r.sx, y: r.sy, w: r.sw, h: r.sh };
9429
9394
  }
9430
9395
  const tw = dctx.projector.tileWidth;
9431
- const w = node.width !== void 0 ? node.width * tw : src ? src.w : tex.width;
9432
- const h = node.height !== void 0 ? node.height * tw : src ? src.h : tex.height;
9396
+ const fallbackW = dctx.projector.worldPixelDirect ? src ? src.w : tex.width : tw;
9397
+ const fallbackH = dctx.projector.worldPixelDirect ? src ? src.h : tex.height : tw;
9398
+ const w = node.width !== void 0 ? node.width * tw : fallbackW;
9399
+ const h = node.height !== void 0 ? node.height * tw : fallbackH;
9433
9400
  const anchor = node.anchor ?? "top-left";
9434
9401
  const p = dctx.projector.anchorPoint(node.position, anchor);
9435
9402
  const dx = anchor === "top-left" ? p.x : p.x - w / 2;
@@ -9506,6 +9473,16 @@ var init_DrawShape = __esm({
9506
9473
  if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
9507
9474
  break;
9508
9475
  }
9476
+ case "path": {
9477
+ if (!node.d) break;
9478
+ const base = dctx.projector.project(node.position);
9479
+ const tw = dctx.projector.tileWidth;
9480
+ painter.translate(base.x, base.y);
9481
+ painter.scale(tw, tw);
9482
+ if (node.fill) painter.fillPath(node.d, node.fill);
9483
+ if (node.stroke) painter.strokePath(node.d, node.stroke, (node.strokeWidth ?? 1) / tw);
9484
+ break;
9485
+ }
9509
9486
  }
9510
9487
  painter.restore();
9511
9488
  };
@@ -9597,6 +9574,19 @@ function paintDrawable(painter, node, dctx) {
9597
9574
  case "draw-text":
9598
9575
  paintText(painter, node, dctx);
9599
9576
  break;
9577
+ case "draw-group": {
9578
+ if (!isValidScenePos(node.position)) break;
9579
+ if (!Array.isArray(node.items)) break;
9580
+ const p = dctx.projector.project(node.position);
9581
+ painter.save();
9582
+ painter.translate(p.x, p.y);
9583
+ if (node.scale !== void 0) painter.scale(node.scale, node.scale);
9584
+ if (node.rotate !== void 0) painter.rotate(node.rotate);
9585
+ if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
9586
+ for (const item of node.items) paintDrawable(painter, item, dctx);
9587
+ painter.restore();
9588
+ break;
9589
+ }
9600
9590
  case "draw-sprite-layer":
9601
9591
  paintSpriteLayer(painter, node, dctx);
9602
9592
  break;
@@ -9610,6 +9600,7 @@ function paintDrawable(painter, node, dctx) {
9610
9600
  }
9611
9601
  var init_paintDispatch = __esm({
9612
9602
  "lib/drawable/paintDispatch.ts"() {
9603
+ init_contract();
9613
9604
  init_DrawSprite();
9614
9605
  init_DrawShape();
9615
9606
  init_DrawText();
@@ -9627,6 +9618,7 @@ function collectDrawnItems(nodes) {
9627
9618
  case "draw-sprite":
9628
9619
  case "draw-shape":
9629
9620
  case "draw-text":
9621
+ case "draw-group":
9630
9622
  if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id });
9631
9623
  break;
9632
9624
  case "draw-sprite-layer":
@@ -9670,6 +9662,8 @@ function Canvas2D({
9670
9662
  keyUpMap,
9671
9663
  camera = "pan-zoom",
9672
9664
  scale = 0.4,
9665
+ tileWidth,
9666
+ fit = false,
9673
9667
  showMinimap = true,
9674
9668
  followTarget,
9675
9669
  cameraPos,
@@ -9712,9 +9706,32 @@ function Canvas2D({
9712
9706
  observer2.observe(el);
9713
9707
  return () => observer2.disconnect();
9714
9708
  }, []);
9715
- const scaledTileWidth = TILE_WIDTH * scale;
9716
- const scaledFloorHeight = FLOOR_HEIGHT * scale;
9717
- const scaledDiamondTopY = DIAMOND_TOP_Y * scale;
9709
+ const [atlasVersion, setAtlasVersion] = useState(0);
9710
+ const bumpAtlas = useCallback(() => setAtlasVersion((v) => v + 1), []);
9711
+ const detectedTileWidth = useMemo(() => {
9712
+ for (const n of drawables ?? []) {
9713
+ const refs = [];
9714
+ if (n.type === "draw-sprite") refs.push(n.asset);
9715
+ else if (n.type === "draw-sprite-layer") for (const it of n.items) refs.push(it.asset);
9716
+ for (const a of refs) {
9717
+ if (a && isAtlasAsset(a) && a.atlas) {
9718
+ const atlas = getAtlas(a.atlas, bumpAtlas);
9719
+ if (atlas) {
9720
+ if ("tileWidth" in atlas) return atlas.tileWidth;
9721
+ if ("subTextures" in atlas) {
9722
+ const first = Object.values(atlas.subTextures)[0];
9723
+ if (first && typeof first.width === "number") return first.width;
9724
+ }
9725
+ }
9726
+ }
9727
+ }
9728
+ }
9729
+ return void 0;
9730
+ }, [drawables, atlasVersion]);
9731
+ const nativeTileW = tileWidth ?? detectedTileWidth ?? TILE_WIDTH;
9732
+ const scaledTileWidth = nativeTileW;
9733
+ const scaledFloorHeight = nativeTileW / 2;
9734
+ const scaledDiamondTopY = nativeTileW * (DIAMOND_TOP_Y / TILE_WIDTH);
9718
9735
  const drawnItems = useMemo(() => collectDrawnItems(drawables ?? []), [drawables]);
9719
9736
  const scenePositions = useMemo(() => drawnItems.map((i) => i.pos), [drawnItems]);
9720
9737
  const hitIndex = useMemo(() => buildHitIndex(drawnItems), [drawnItems]);
@@ -9732,14 +9749,36 @@ function Canvas2D({
9732
9749
  if (isFree || projection === "flat" || projection === "side") return 0;
9733
9750
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
9734
9751
  }, [isFree, projection, gridExtent.height, scaledTileWidth]);
9752
+ const effectiveZoom = useMemo(() => {
9753
+ if (isFree || projection === "side") return scale;
9754
+ if (!fit) {
9755
+ const z2 = TILE_WIDTH * scale * scale / nativeTileW;
9756
+ return Number.isFinite(z2) && z2 > 0 ? z2 : scale;
9757
+ }
9758
+ if (!viewportSize.width || gridExtent.width < 2 || gridExtent.height < 2) return scale;
9759
+ let boardW;
9760
+ let boardH;
9761
+ if (projection === "flat") {
9762
+ boardW = gridExtent.width * nativeTileW;
9763
+ boardH = gridExtent.height * nativeTileW;
9764
+ } else if (projection === "hex") {
9765
+ boardW = (gridExtent.width + 0.5) * nativeTileW;
9766
+ boardH = gridExtent.height * (nativeTileW / 2) * 0.75 + nativeTileW / 2;
9767
+ } else {
9768
+ boardW = (gridExtent.width + gridExtent.height) * (nativeTileW / 2);
9769
+ boardH = (gridExtent.width + gridExtent.height) * (nativeTileW / 4);
9770
+ }
9771
+ const z = Math.min(viewportSize.width * 0.85 / boardW, viewportSize.height * 0.85 / boardH);
9772
+ return Number.isFinite(z) && z > 0 ? z : scale;
9773
+ }, [isFree, projection, fit, viewportSize, gridExtent, nativeTileW, scale]);
9735
9774
  const projector = useMemo(
9736
- () => create2DProjector({ scale, baseOffsetX, layout }),
9737
- [scale, baseOffsetX, layout]
9775
+ () => create2DProjector({ tileWidth: nativeTileW, baseOffsetX, layout }),
9776
+ [nativeTileW, baseOffsetX, layout]
9738
9777
  );
9739
9778
  const unproject = useCallback((screenX, screenY) => {
9740
9779
  if (projection === "free" || projection === "side") return { x: Math.round(screenX), y: Math.round(screenY) };
9741
- return screenToIso(screenX, screenY, scale, baseOffsetX, projection);
9742
- }, [projection, scale, baseOffsetX]);
9780
+ return screenToIso(screenX, screenY, nativeTileW, baseOffsetX, projection);
9781
+ }, [projection, nativeTileW, baseOffsetX]);
9743
9782
  const bgUrls = useMemo(() => backgroundImage ? [backgroundImage.url] : [], [backgroundImage]);
9744
9783
  const { getImage, pendingCount: _imagePendingCount } = useImageCache(bgUrls);
9745
9784
  useEffect(() => {
@@ -9766,9 +9805,7 @@ function Canvas2D({
9766
9805
  zoomAtPoint,
9767
9806
  screenToWorld,
9768
9807
  lerpToTarget
9769
- } = useCamera({ zoom: scale });
9770
- const [atlasVersion, setAtlasVersion] = useState(0);
9771
- const bumpAtlas = useCallback(() => setAtlasVersion((v) => v + 1), []);
9808
+ } = useCamera({ zoom: effectiveZoom });
9772
9809
  const miniMapTiles = useMemo(() => {
9773
9810
  if (!showMinimap) return [];
9774
9811
  const color = MINIMAP_TERRAIN_COLORS.default;
@@ -9843,6 +9880,13 @@ function Canvas2D({
9843
9880
  useEffect(() => {
9844
9881
  draw();
9845
9882
  }, [_imagePendingCount, draw]);
9883
+ const userZoomedRef = useRef(false);
9884
+ useEffect(() => {
9885
+ if (userZoomedRef.current) return;
9886
+ if (cameraRef.current.zoom === effectiveZoom) return;
9887
+ cameraRef.current.zoom = effectiveZoom;
9888
+ draw();
9889
+ }, [effectiveZoom, cameraRef, draw]);
9846
9890
  useEffect(() => {
9847
9891
  draw();
9848
9892
  }, [atlasVersion, draw]);
@@ -9899,7 +9943,9 @@ function Canvas2D({
9899
9943
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
9900
9944
  }, [handleMouseLeave, tileLeaveEvent, eventBus]);
9901
9945
  const applyZoom = useCallback((factor, centerX, centerY) => {
9902
- if (enableCamera) zoomAtPoint(factor, centerX, centerY, viewportSize, () => draw());
9946
+ if (!enableCamera) return;
9947
+ userZoomedRef.current = true;
9948
+ zoomAtPoint(factor, centerX, centerY, viewportSize, () => draw());
9903
9949
  }, [enableCamera, zoomAtPoint, viewportSize, draw]);
9904
9950
  const applyPanDelta = useCallback((dx, dy) => {
9905
9951
  if (enableCamera) panBy(dx, dy, () => draw());
@@ -10084,6 +10130,8 @@ function Canvas({
10084
10130
  isLoading,
10085
10131
  unitScale,
10086
10132
  showMinimap,
10133
+ fit,
10134
+ tileWidth,
10087
10135
  backgroundImage,
10088
10136
  backgroundColor,
10089
10137
  worldWidth,
@@ -10141,6 +10189,8 @@ function Canvas({
10141
10189
  projection,
10142
10190
  camera: to2DCamera(camera?.mode),
10143
10191
  ...zoom !== void 0 ? { scale: zoom } : {},
10192
+ ...fit !== void 0 ? { fit } : {},
10193
+ ...tileWidth !== void 0 ? { tileWidth } : {},
10144
10194
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
10145
10195
  ...camera?.pos !== void 0 ? { cameraPos: camera.pos } : {},
10146
10196
  showMinimap,
@@ -10167,32 +10217,6 @@ var init_Canvas = __esm({
10167
10217
  Canvas.displayName = "Canvas";
10168
10218
  }
10169
10219
  });
10170
- function SvgDrawShapeLayer({
10171
- items,
10172
- fill,
10173
- stroke,
10174
- strokeWidth,
10175
- opacity
10176
- }) {
10177
- return /* @__PURE__ */ jsx("g", { children: items.map(({ id, ...shape }) => /* @__PURE__ */ jsx(
10178
- SvgDrawShape,
10179
- {
10180
- ...shape,
10181
- fill: shape.fill ?? fill,
10182
- stroke: shape.stroke ?? stroke,
10183
- strokeWidth: shape.strokeWidth ?? strokeWidth,
10184
- opacity: shape.opacity ?? opacity
10185
- },
10186
- id
10187
- )) });
10188
- }
10189
- var init_SvgDrawShapeLayer = __esm({
10190
- "components/game/molecules/SvgDrawShapeLayer.tsx"() {
10191
- "use client";
10192
- init_SvgDrawShape();
10193
- SvgDrawShapeLayer.displayName = "SvgDrawShapeLayer";
10194
- }
10195
- });
10196
10220
  function GameAudioToggle({
10197
10221
  size = "sm",
10198
10222
  className,
@@ -23378,6 +23402,8 @@ function DataList({
23378
23402
  hasMore,
23379
23403
  children,
23380
23404
  pageSize = 5,
23405
+ sortBy,
23406
+ sortDirection,
23381
23407
  renderItem: schemaRenderItem,
23382
23408
  dragGroup,
23383
23409
  accepts,
@@ -23406,7 +23432,11 @@ function DataList({
23406
23432
  dndItemIdField,
23407
23433
  dndRoot
23408
23434
  });
23409
- const allData = dnd.orderedItems;
23435
+ const orderedData = dnd.orderedItems;
23436
+ const allData = React83__default.useMemo(
23437
+ () => sortRows(orderedData, sortBy, sortDirection),
23438
+ [orderedData, sortBy, sortDirection]
23439
+ );
23410
23440
  const data = pageSize > 0 ? allData.slice(0, visibleCount) : allData;
23411
23441
  const hasMoreLocal = pageSize > 0 && visibleCount < allData.length;
23412
23442
  const hasRenderProp = typeof children === "function";
@@ -23443,7 +23473,7 @@ function DataList({
23443
23473
  };
23444
23474
  eventBus.emit(`UI:${action.event}`, payload);
23445
23475
  };
23446
- const renderItemActions = (itemData) => {
23476
+ const renderItemActions = (itemData, onPrimary = false) => {
23447
23477
  if (!itemActions || itemActions.length === 0) return null;
23448
23478
  const inline = maxInlineActions != null ? itemActions.slice(0, maxInlineActions) : itemActions;
23449
23479
  const overflow = maxInlineActions != null ? itemActions.slice(maxInlineActions) : [];
@@ -23456,7 +23486,12 @@ function DataList({
23456
23486
  onClick: handleActionClick(action, itemData),
23457
23487
  "data-testid": `action-${action.event}`,
23458
23488
  "data-row-id": String(itemData.id),
23459
- className: cn(action.variant === "danger" && "text-error hover:bg-error/10"),
23489
+ className: cn(
23490
+ action.variant === "danger" && "text-error hover:bg-error/10",
23491
+ // Must sit on the Button itself: the variant's own text colour
23492
+ // beats an inherited one from the row wrapper.
23493
+ onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
23494
+ ),
23460
23495
  children: [
23461
23496
  action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
23462
23497
  action.label
@@ -23556,7 +23591,11 @@ function DataList({
23556
23591
  metaFields.length > 0 && /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "mt-1 flex-wrap", children: metaFields.map((f3) => {
23557
23592
  const v = getNestedValue(itemData, f3.name);
23558
23593
  if (v === void 0 || v === null || v === "") return null;
23559
- return f3.variant === "badge" ? /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(String(v)), children: String(v) }, f3.name) : /* @__PURE__ */ jsx(
23594
+ return f3.variant === "badge" ? (
23595
+ // `format` applies here too — a boolean field badged
23596
+ // without it renders the raw "false" instead of "No".
23597
+ /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(String(v)), children: formatValue2(v, f3.format) }, f3.name)
23598
+ ) : /* @__PURE__ */ jsx(
23560
23599
  Typography,
23561
23600
  {
23562
23601
  variant: "caption",
@@ -23575,7 +23614,7 @@ function DataList({
23575
23614
  children: formatDate(timestamp)
23576
23615
  }
23577
23616
  ) : /* @__PURE__ */ jsx("span", {}),
23578
- renderItemActions(itemData)
23617
+ renderItemActions(itemData, isSent)
23579
23618
  ] })
23580
23619
  ]
23581
23620
  }
@@ -23668,7 +23707,7 @@ function DataList({
23668
23707
  if (val === void 0 || val === null) return null;
23669
23708
  return /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "items-center flex-shrink-0", children: [
23670
23709
  field.icon && renderIconInput2(field.icon, { size: "xs" }),
23671
- /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(String(val)), children: String(val) })
23710
+ /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(String(val)), children: formatValue2(val, field.format) })
23672
23711
  ] }, field.name);
23673
23712
  })
23674
23713
  ] }),
@@ -23907,6 +23946,182 @@ var init_FormSection = __esm({
23907
23946
  FormActions.displayName = "FormActions";
23908
23947
  }
23909
23948
  });
23949
+ var ALL_CATEGORY, MAX_RENDERED, GridPicker;
23950
+ var init_GridPicker = __esm({
23951
+ "components/core/molecules/GridPicker.tsx"() {
23952
+ "use client";
23953
+ init_cn();
23954
+ init_Input();
23955
+ init_Badge();
23956
+ init_Stack();
23957
+ ALL_CATEGORY = "__all__";
23958
+ MAX_RENDERED = 300;
23959
+ GridPicker = ({
23960
+ items,
23961
+ value,
23962
+ onChange,
23963
+ categories,
23964
+ searchPlaceholder,
23965
+ renderThumbnail,
23966
+ cellSize = 32,
23967
+ className
23968
+ }) => {
23969
+ const [search, setSearch] = useState("");
23970
+ const [activeCategory, setActiveCategory] = useState(ALL_CATEGORY);
23971
+ const gridRef = useRef(null);
23972
+ const categoryChips = useMemo(() => {
23973
+ if (categories !== void 0) return categories;
23974
+ const seen = [];
23975
+ for (const item of items) {
23976
+ if (!seen.includes(item.category)) seen.push(item.category);
23977
+ }
23978
+ return seen;
23979
+ }, [categories, items]);
23980
+ const filtered = useMemo(() => {
23981
+ const needle = search.trim().toLowerCase();
23982
+ return items.filter((item) => {
23983
+ const matchesCategory = activeCategory === ALL_CATEGORY || item.category === activeCategory;
23984
+ const matchesSearch = needle === "" || item.label.toLowerCase().includes(needle) || item.keywords !== void 0 && item.keywords.some((k) => k.toLowerCase().includes(needle));
23985
+ return matchesCategory && matchesSearch;
23986
+ });
23987
+ }, [items, search, activeCategory]);
23988
+ const visible = useMemo(() => filtered.slice(0, MAX_RENDERED), [filtered]);
23989
+ const truncated = filtered.length - visible.length;
23990
+ const select = useCallback(
23991
+ (item) => {
23992
+ onChange(item.id);
23993
+ },
23994
+ [onChange]
23995
+ );
23996
+ const handleKeyDown = useCallback(
23997
+ (e, index) => {
23998
+ const cells = gridRef.current?.querySelectorAll(
23999
+ "[data-gridpicker-cell]"
24000
+ );
24001
+ if (cells === void 0 || cells.length === 0) return;
24002
+ const columns = (() => {
24003
+ const grid = gridRef.current;
24004
+ if (grid === null) return 1;
24005
+ const style = window.getComputedStyle(grid);
24006
+ const cols = style.gridTemplateColumns.split(" ").filter(Boolean).length;
24007
+ return cols > 0 ? cols : 1;
24008
+ })();
24009
+ let next = -1;
24010
+ if (e.key === "ArrowRight") next = index + 1;
24011
+ else if (e.key === "ArrowLeft") next = index - 1;
24012
+ else if (e.key === "ArrowDown") next = index + columns;
24013
+ else if (e.key === "ArrowUp") next = index - columns;
24014
+ else if (e.key === "Enter" || e.key === " ") {
24015
+ e.preventDefault();
24016
+ select(filtered[index]);
24017
+ return;
24018
+ } else {
24019
+ return;
24020
+ }
24021
+ e.preventDefault();
24022
+ if (next >= 0 && next < cells.length) {
24023
+ cells[next].focus();
24024
+ }
24025
+ },
24026
+ [filtered, select]
24027
+ );
24028
+ return /* @__PURE__ */ jsxs(VStack, { gap: "sm", className: cn("w-full", className), children: [
24029
+ /* @__PURE__ */ jsx(
24030
+ Input,
24031
+ {
24032
+ type: "search",
24033
+ icon: "search",
24034
+ value: search,
24035
+ placeholder: searchPlaceholder,
24036
+ clearable: true,
24037
+ onClear: () => setSearch(""),
24038
+ onChange: (e) => setSearch(e.target.value)
24039
+ }
24040
+ ),
24041
+ categoryChips.length > 0 && /* @__PURE__ */ jsxs(HStack, { gap: "xs", wrap: true, children: [
24042
+ /* @__PURE__ */ jsx(
24043
+ Badge,
24044
+ {
24045
+ variant: activeCategory === ALL_CATEGORY ? "primary" : "neutral",
24046
+ size: "sm",
24047
+ role: "button",
24048
+ tabIndex: 0,
24049
+ "aria-pressed": activeCategory === ALL_CATEGORY,
24050
+ className: "cursor-pointer",
24051
+ onClick: () => setActiveCategory(ALL_CATEGORY),
24052
+ onKeyDown: (e) => {
24053
+ if (e.key === "Enter" || e.key === " ") {
24054
+ e.preventDefault();
24055
+ setActiveCategory(ALL_CATEGORY);
24056
+ }
24057
+ },
24058
+ children: "All"
24059
+ }
24060
+ ),
24061
+ categoryChips.map((category) => /* @__PURE__ */ jsx(
24062
+ Badge,
24063
+ {
24064
+ variant: activeCategory === category ? "primary" : "neutral",
24065
+ size: "sm",
24066
+ role: "button",
24067
+ tabIndex: 0,
24068
+ "aria-pressed": activeCategory === category,
24069
+ className: "cursor-pointer",
24070
+ onClick: () => setActiveCategory(category),
24071
+ onKeyDown: (e) => {
24072
+ if (e.key === "Enter" || e.key === " ") {
24073
+ e.preventDefault();
24074
+ setActiveCategory(category);
24075
+ }
24076
+ },
24077
+ children: category
24078
+ },
24079
+ category
24080
+ ))
24081
+ ] }),
24082
+ /* @__PURE__ */ jsx(
24083
+ "div",
24084
+ {
24085
+ ref: gridRef,
24086
+ role: "listbox",
24087
+ className: "grid gap-1 overflow-y-auto max-h-64 p-1",
24088
+ style: {
24089
+ gridTemplateColumns: `repeat(auto-fill, minmax(${cellSize}px, 1fr))`
24090
+ },
24091
+ children: visible.map((item, index) => {
24092
+ const selected = item.id === value;
24093
+ return /* @__PURE__ */ jsx(
24094
+ "button",
24095
+ {
24096
+ type: "button",
24097
+ role: "option",
24098
+ "aria-selected": selected,
24099
+ "aria-label": item.label,
24100
+ title: item.label,
24101
+ "data-gridpicker-cell": true,
24102
+ tabIndex: selected || value === void 0 && index === 0 ? 0 : -1,
24103
+ onClick: () => select(item),
24104
+ onKeyDown: (e) => handleKeyDown(e, index),
24105
+ className: cn(
24106
+ "flex items-center justify-center rounded-sm",
24107
+ "transition-colors hover:bg-muted",
24108
+ "focus:outline-none focus:ring-1 focus:ring-ring",
24109
+ selected && "bg-primary/10 ring-1 ring-primary"
24110
+ ),
24111
+ style: { width: cellSize, height: cellSize },
24112
+ children: renderThumbnail(item)
24113
+ },
24114
+ item.id
24115
+ );
24116
+ })
24117
+ }
24118
+ ),
24119
+ truncated > 0 && /* @__PURE__ */ jsx("div", { className: "px-1 text-xs text-muted-foreground", children: `+${truncated} more \u2014 refine your search` })
24120
+ ] });
24121
+ };
24122
+ GridPicker.displayName = "GridPicker";
24123
+ }
24124
+ });
23910
24125
  function fileIcon(name) {
23911
24126
  const ext = name.split(".").pop()?.toLowerCase() ?? "";
23912
24127
  switch (ext) {
@@ -24871,9 +25086,16 @@ var init_Popover = __esm({
24871
25086
  position = "bottom",
24872
25087
  trigger = "click",
24873
25088
  showArrow = true,
25089
+ open,
25090
+ onOpenChange,
24874
25091
  className
24875
25092
  }) => {
24876
- const [isOpen, setIsOpen] = useState(false);
25093
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
25094
+ const isOpen = open !== void 0 ? open : uncontrolledOpen;
25095
+ const setIsOpen = (next) => {
25096
+ if (open === void 0) setUncontrolledOpen(next);
25097
+ onOpenChange?.(next);
25098
+ };
24877
25099
  const [triggerRect, setTriggerRect] = useState(null);
24878
25100
  const [popoverWidth, setPopoverWidth] = useState(0);
24879
25101
  const triggerRef = useRef(null);
@@ -24906,6 +25128,23 @@ var init_Popover = __esm({
24906
25128
  updatePosition();
24907
25129
  }
24908
25130
  }, [isOpen]);
25131
+ useEffect(() => {
25132
+ if (!isOpen) return;
25133
+ let raf = 0;
25134
+ let lastTop = Number.NaN;
25135
+ let lastLeft = Number.NaN;
25136
+ const track = () => {
25137
+ const rect = triggerRef.current?.getBoundingClientRect();
25138
+ if (rect && (rect.top !== lastTop || rect.left !== lastLeft)) {
25139
+ lastTop = rect.top;
25140
+ lastLeft = rect.left;
25141
+ updatePosition();
25142
+ }
25143
+ raf = requestAnimationFrame(track);
25144
+ };
25145
+ raf = requestAnimationFrame(track);
25146
+ return () => cancelAnimationFrame(raf);
25147
+ }, [isOpen]);
24909
25148
  useEffect(() => {
24910
25149
  if (!mounted) setPopoverWidth(0);
24911
25150
  }, [mounted]);
@@ -26254,6 +26493,80 @@ var init_FlipCard = __esm({
26254
26493
  FlipCard.displayName = "FlipCard";
26255
26494
  }
26256
26495
  });
26496
+ var EMOJI_ITEMS, EmojiPicker;
26497
+ var init_EmojiPicker = __esm({
26498
+ "components/core/molecules/EmojiPicker.tsx"() {
26499
+ "use client";
26500
+ init_useEventBus();
26501
+ init_Button();
26502
+ init_GridPicker();
26503
+ init_Popover();
26504
+ EMOJI_ITEMS = (() => {
26505
+ const items = [];
26506
+ for (const name of ordered) {
26507
+ const entry = lib[name];
26508
+ if (entry === void 0 || entry.char === null || entry.char === "") continue;
26509
+ items.push({
26510
+ id: entry.char,
26511
+ label: name.replace(/_/g, " "),
26512
+ category: entry.category.replace(/_/g, " "),
26513
+ keywords: entry.keywords
26514
+ });
26515
+ }
26516
+ return items;
26517
+ })();
26518
+ EmojiPicker = ({
26519
+ pickEvent,
26520
+ position = "top",
26521
+ triggerIcon = "smile",
26522
+ triggerLabel = "Add emoji",
26523
+ className
26524
+ }) => {
26525
+ const eventBus = useEventBus();
26526
+ const [open, setOpen] = useState(false);
26527
+ const handlePick = (glyph) => {
26528
+ if (pickEvent !== void 0) {
26529
+ const payload = { emoji: glyph };
26530
+ eventBus.emit(`UI:${pickEvent}`, payload);
26531
+ }
26532
+ setOpen(false);
26533
+ };
26534
+ return /* @__PURE__ */ jsx(
26535
+ Popover,
26536
+ {
26537
+ position,
26538
+ trigger: "click",
26539
+ showArrow: false,
26540
+ open,
26541
+ onOpenChange: setOpen,
26542
+ content: /* @__PURE__ */ jsx(
26543
+ GridPicker,
26544
+ {
26545
+ items: EMOJI_ITEMS,
26546
+ onChange: handlePick,
26547
+ searchPlaceholder: "Search emoji\u2026",
26548
+ renderThumbnail: (item) => /* @__PURE__ */ jsx("span", { className: "text-xl leading-none", "aria-hidden": "true", children: item.id }),
26549
+ cellSize: 32,
26550
+ className: "w-80"
26551
+ }
26552
+ ),
26553
+ children: /* @__PURE__ */ jsx(
26554
+ Button,
26555
+ {
26556
+ variant: "ghost",
26557
+ icon: triggerIcon,
26558
+ "aria-label": triggerLabel,
26559
+ title: triggerLabel,
26560
+ className,
26561
+ "data-testid": "emoji-picker-trigger"
26562
+ }
26563
+ )
26564
+ }
26565
+ );
26566
+ };
26567
+ EmojiPicker.displayName = "EmojiPicker";
26568
+ }
26569
+ });
26257
26570
  function toISODate(d) {
26258
26571
  return d.toISOString().slice(0, 10);
26259
26572
  }
@@ -27206,13 +27519,13 @@ var init_MapView = __esm({
27206
27519
  shadowSize: [41, 41]
27207
27520
  });
27208
27521
  L.Marker.prototype.options.icon = defaultIcon;
27209
- const { useEffect: useEffect60, useRef: useRef61, useCallback: useCallback89, useState: useState89 } = React83__default;
27522
+ const { useEffect: useEffect59, useRef: useRef61, useCallback: useCallback89, useState: useState91 } = React83__default;
27210
27523
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
27211
27524
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
27212
27525
  function MapUpdater({ centerLat, centerLng, zoom }) {
27213
27526
  const map = useMap();
27214
27527
  const prevRef = useRef61({ centerLat, centerLng, zoom });
27215
- useEffect60(() => {
27528
+ useEffect59(() => {
27216
27529
  const prev = prevRef.current;
27217
27530
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
27218
27531
  map.setView([centerLat, centerLng], zoom);
@@ -27223,7 +27536,7 @@ var init_MapView = __esm({
27223
27536
  }
27224
27537
  function MapClickHandler({ onMapClick }) {
27225
27538
  const map = useMap();
27226
- useEffect60(() => {
27539
+ useEffect59(() => {
27227
27540
  if (!onMapClick) return;
27228
27541
  const handler = (e) => {
27229
27542
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -27251,7 +27564,7 @@ var init_MapView = __esm({
27251
27564
  showAttribution = true
27252
27565
  }) {
27253
27566
  const eventBus = useEventBus2();
27254
- const [clickedPosition, setClickedPosition] = useState89(null);
27567
+ const [clickedPosition, setClickedPosition] = useState91(null);
27255
27568
  const handleMapClick = useCallback89((lat, lng) => {
27256
27569
  if (showClickedPin) {
27257
27570
  setClickedPosition({ lat, lng });
@@ -28076,6 +28389,7 @@ function TableView({
28076
28389
  fields,
28077
28390
  itemActions,
28078
28391
  maxInlineActions,
28392
+ itemClickEvent,
28079
28393
  selectable = false,
28080
28394
  selectEvent,
28081
28395
  selectedIds,
@@ -28120,9 +28434,9 @@ function TableView({
28120
28434
  dndItemIdField,
28121
28435
  dndRoot
28122
28436
  });
28123
- const ordered = dnd.orderedItems;
28124
- const data = pageSize > 0 ? ordered.slice(0, visibleCount) : ordered;
28125
- const hasMore = pageSize > 0 && visibleCount < ordered.length;
28437
+ const ordered2 = dnd.orderedItems;
28438
+ const data = pageSize > 0 ? ordered2.slice(0, visibleCount) : ordered2;
28439
+ const hasMore = pageSize > 0 && visibleCount < ordered2.length;
28126
28440
  const hasRenderProp = typeof children === "function";
28127
28441
  const idField = dndItemIdField ?? "id";
28128
28442
  const isCoarsePointer = useMediaQuery("(pointer: coarse)");
@@ -28175,6 +28489,14 @@ function TableView({
28175
28489
  };
28176
28490
  eventBus.emit(`UI:${action.event}`, payload);
28177
28491
  };
28492
+ const handleRowClick = (row) => () => {
28493
+ if (!itemClickEvent) return;
28494
+ const payload = {
28495
+ id: row.id,
28496
+ row
28497
+ };
28498
+ eventBus.emit(`UI:${itemClickEvent}`, payload);
28499
+ };
28178
28500
  const colFloors = React83__default.useMemo(
28179
28501
  () => colDefs.map((col) => {
28180
28502
  const longest = data.reduce((widest, row) => {
@@ -28251,10 +28573,12 @@ function TableView({
28251
28573
  role: "row",
28252
28574
  "data-entity-row": true,
28253
28575
  "data-entity-id": id,
28576
+ onClick: itemClickEvent ? handleRowClick(row) : void 0,
28254
28577
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
28255
28578
  className: cn(
28256
28579
  "group items-center gap-3 transition-colors duration-fast",
28257
28580
  hasRenderProp ? "flex" : "grid",
28581
+ itemClickEvent && "cursor-pointer",
28258
28582
  lk.rowPad,
28259
28583
  lk.divider && "border-b border-[var(--color-border)]",
28260
28584
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -28262,7 +28586,7 @@ function TableView({
28262
28586
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
28263
28587
  ),
28264
28588
  children: [
28265
- selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", children: /* @__PURE__ */ jsx(
28589
+ selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
28266
28590
  Checkbox,
28267
28591
  {
28268
28592
  checked: selected.has(id),
@@ -28287,6 +28611,7 @@ function TableView({
28287
28611
  HStack,
28288
28612
  {
28289
28613
  gap: "xs",
28614
+ onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
28290
28615
  className: cn(
28291
28616
  // Pinned: the fixed column tracks routinely overflow the caller's
28292
28617
  // scroll container, which used to leave the actions off-screen.
@@ -28356,7 +28681,7 @@ function TableView({
28356
28681
  /* @__PURE__ */ jsx(Icon, { name: "chevron-down", size: "xs", className: "mr-1" }),
28357
28682
  t("common.showMore"),
28358
28683
  " (",
28359
- t("common.remaining", { count: ordered.length - visibleCount }),
28684
+ t("common.remaining", { count: ordered2.length - visibleCount }),
28360
28685
  ")"
28361
28686
  ] }) })
28362
28687
  ]
@@ -37657,6 +37982,16 @@ var init_DetailPanel = __esm({
37657
37982
  DetailPanel.displayName = "DetailPanel";
37658
37983
  }
37659
37984
  });
37985
+
37986
+ // components/game/atoms/DrawGroup.tsx
37987
+ function DrawGroup(_props) {
37988
+ return null;
37989
+ }
37990
+ var init_DrawGroup = __esm({
37991
+ "components/game/atoms/DrawGroup.tsx"() {
37992
+ "use client";
37993
+ }
37994
+ });
37660
37995
  function extractTitle(children) {
37661
37996
  if (!React83__default.isValidElement(children)) return void 0;
37662
37997
  const props = children.props;
@@ -43379,6 +43714,7 @@ var init_component_registry_generated = __esm({
43379
43714
  init_DocSidebar();
43380
43715
  init_DocTOC();
43381
43716
  init_DocumentViewer();
43717
+ init_DrawGroup();
43382
43718
  init_DrawShape();
43383
43719
  init_DrawShapeLayer();
43384
43720
  init_DrawSprite();
@@ -43388,6 +43724,7 @@ var init_component_registry_generated = __esm({
43388
43724
  init_Drawer();
43389
43725
  init_DrawerSlot();
43390
43726
  init_EdgeDecoration();
43727
+ init_EmojiPicker();
43391
43728
  init_EmptyState();
43392
43729
  init_ErrorBoundary();
43393
43730
  init_ErrorState();
@@ -43522,10 +43859,6 @@ var init_component_registry_generated = __esm({
43522
43859
  init_SubagentTracePanel();
43523
43860
  init_SvgBranch();
43524
43861
  init_SvgConnection();
43525
- init_SvgDrawGroup();
43526
- init_SvgDrawShape();
43527
- init_SvgDrawShapeLayer();
43528
- init_SvgDrawText();
43529
43862
  init_SvgFlow();
43530
43863
  init_SvgGrid();
43531
43864
  init_SvgLobe();
@@ -43536,7 +43869,6 @@ var init_component_registry_generated = __esm({
43536
43869
  init_SvgRing();
43537
43870
  init_SvgShield();
43538
43871
  init_SvgStack();
43539
- init_SvgStage();
43540
43872
  init_SwipeableRow();
43541
43873
  init_Switch();
43542
43874
  init_TabbedContainer();
@@ -43650,6 +43982,7 @@ var init_component_registry_generated = __esm({
43650
43982
  "DocSidebar": DocSidebar,
43651
43983
  "DocTOC": DocTOC,
43652
43984
  "DocumentViewer": DocumentViewer,
43985
+ "DrawGroup": DrawGroup,
43653
43986
  "DrawShape": DrawShape,
43654
43987
  "DrawShapeLayer": DrawShapeLayer,
43655
43988
  "DrawSprite": DrawSprite,
@@ -43659,6 +43992,7 @@ var init_component_registry_generated = __esm({
43659
43992
  "Drawer": Drawer,
43660
43993
  "DrawerSlot": DrawerSlot,
43661
43994
  "EdgeDecoration": EdgeDecoration,
43995
+ "EmojiPicker": EmojiPicker,
43662
43996
  "EmptyState": EmptyState,
43663
43997
  "ErrorBoundary": ErrorBoundary,
43664
43998
  "ErrorState": ErrorState,
@@ -43798,10 +44132,6 @@ var init_component_registry_generated = __esm({
43798
44132
  "SubagentTracePanel": SubagentTracePanel,
43799
44133
  "SvgBranch": SvgBranch,
43800
44134
  "SvgConnection": SvgConnection,
43801
- "SvgDrawGroup": SvgDrawGroup,
43802
- "SvgDrawShape": SvgDrawShape,
43803
- "SvgDrawShapeLayer": SvgDrawShapeLayer,
43804
- "SvgDrawText": SvgDrawText,
43805
44135
  "SvgFlow": SvgFlow,
43806
44136
  "SvgGrid": SvgGrid,
43807
44137
  "SvgLobe": SvgLobe,
@@ -43812,7 +44142,6 @@ var init_component_registry_generated = __esm({
43812
44142
  "SvgRing": SvgRing,
43813
44143
  "SvgShield": SvgShield,
43814
44144
  "SvgStack": SvgStack,
43815
- "SvgStage": SvgStage,
43816
44145
  "SwipeableRow": SwipeableRow,
43817
44146
  "Switch": Switch,
43818
44147
  "TabbedContainer": TabbedContainer,
@@ -44091,7 +44420,19 @@ function UISlotComponent({
44091
44420
  const suspenseConfig = useContext(SuspenseConfigContext);
44092
44421
  const contained = useContext(SlotContainedContext);
44093
44422
  const schemaCtx = useEntitySchemaOptional();
44094
- const content = slots[slot];
44423
+ const rawContent = slots[slot];
44424
+ const binding = useEntityBindingSnapshot(rawContent?.sourceTrait);
44425
+ const content = useMemo(() => {
44426
+ if (!rawContent) return rawContent;
44427
+ const resolvedProps = resolveRenderBindingMarkers(
44428
+ rawContent.props,
44429
+ rawContent.sourceTrait,
44430
+ binding.entity,
44431
+ binding.config,
44432
+ binding.state
44433
+ );
44434
+ return resolvedProps === rawContent.props ? rawContent : { ...rawContent, props: resolvedProps };
44435
+ }, [rawContent, binding.entity, binding.config, binding.state]);
44095
44436
  if (children !== void 0) {
44096
44437
  if (pattern === "clear") {
44097
44438
  return null;
@@ -44435,6 +44776,7 @@ function isPlainConfigObject(value) {
44435
44776
  return proto === Object.prototype || proto === null;
44436
44777
  }
44437
44778
  function substituteTraitRefsDeep(value, pathKey) {
44779
+ if (isRenderBindingMarker(value)) return value;
44438
44780
  if (typeof value === "string") {
44439
44781
  const match = TRAIT_BINDING_RE.exec(value);
44440
44782
  if (match) {
@@ -44507,7 +44849,14 @@ function SlotContentRenderer({
44507
44849
  onDismiss,
44508
44850
  patternPath
44509
44851
  }) {
44510
- const entityProp = content.props.entity;
44852
+ const ambientScope = useTraitScope();
44853
+ const bindingTrait = content.sourceTrait ?? ambientScope?.trait;
44854
+ const binding = useEntityBindingSnapshot(bindingTrait);
44855
+ const liveProps = useMemo(
44856
+ () => resolveRenderBindingMarkers(content.props, bindingTrait, binding.entity, binding.config, binding.state),
44857
+ [content.props, bindingTrait, binding.entity, binding.config, binding.state]
44858
+ );
44859
+ const entityProp = liveProps.entity;
44511
44860
  if (content.pattern === "form-section") {
44512
44861
  slotLog.debug("SlotContentRenderer:form-section-render", {
44513
44862
  contentId: content.id,
@@ -44538,7 +44887,7 @@ function SlotContentRenderer({
44538
44887
  const orbitalName = schemaCtx && content.sourceTrait !== void 0 ? schemaCtx.orbitalsByTrait.get(content.sourceTrait) : void 0;
44539
44888
  const PatternComponent = getComponentForPattern(content.pattern);
44540
44889
  if (PatternComponent) {
44541
- const childrenConfig = content.props.children;
44890
+ const childrenConfig = liveProps.children;
44542
44891
  const isSingleChild = typeof childrenConfig === "string" || typeof childrenConfig === "object" && childrenConfig !== null && !Array.isArray(childrenConfig) && "type" in childrenConfig;
44543
44892
  const hasChildren = PATTERNS_WITH_CHILDREN.has(content.pattern) || Array.isArray(childrenConfig) && childrenConfig.length > 0 || isSingleChild;
44544
44893
  const isDrawHost = isDrawHostPattern(content.pattern);
@@ -44549,9 +44898,9 @@ function SlotContentRenderer({
44549
44898
  fromState: content.fromState,
44550
44899
  entity: content.entity
44551
44900
  }) : void 0;
44552
- const incomingChildren = content.props.children;
44901
+ const incomingChildren = liveProps.children;
44553
44902
  const childrenIsRenderFn = typeof incomingChildren === "function";
44554
- const { children: _childrenConfig, ...restPropsNoChildren } = content.props;
44903
+ const { children: _childrenConfig, ...restPropsNoChildren } = liveProps;
44555
44904
  const restProps = childrenIsRenderFn ? { ...restPropsNoChildren, children: incomingChildren } : restPropsNoChildren;
44556
44905
  const nodeSlotOverrides = {};
44557
44906
  for (const slotKey of CONTENT_NODE_SLOTS) {
@@ -44660,7 +45009,7 @@ function SlotContentRenderer({
44660
45009
  "data-orb-path": patternPath ?? "root",
44661
45010
  "data-orb-pattern": content.pattern,
44662
45011
  "data-orb-orbital": orbitalName,
44663
- children: content.props.children ?? /* @__PURE__ */ jsxs(Box, { className: "p-4 text-sm text-muted-foreground border border-dashed border-border rounded", children: [
45012
+ children: liveProps.children ?? /* @__PURE__ */ jsxs(Box, { className: "p-4 text-sm text-muted-foreground border border-dashed border-border rounded", children: [
44664
45013
  "Unknown pattern: ",
44665
45014
  content.pattern,
44666
45015
  content.sourceTrait && /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "ml-2", children: [
@@ -44732,6 +45081,7 @@ var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext,
44732
45081
  var init_UISlotRenderer = __esm({
44733
45082
  "components/core/organisms/UISlotRenderer.tsx"() {
44734
45083
  "use client";
45084
+ init_resolve_render_bindings();
44735
45085
  init_Modal();
44736
45086
  init_Drawer();
44737
45087
  init_Toast();
@@ -44978,6 +45328,9 @@ function resolveLambdaBindings(body, params, item, index) {
44978
45328
  const arr = body;
44979
45329
  const substituted = arr.map((b) => recur(b));
44980
45330
  if (isOperatorCall(substituted) && isSExpr(substituted)) {
45331
+ if (containsEntityBinding(substituted)) {
45332
+ return substituted;
45333
+ }
44981
45334
  const evaluated = evaluate(substituted, createMinimalContext());
44982
45335
  return isEventPayloadValue(evaluated) ? evaluated : String(evaluated);
44983
45336
  }
@@ -44999,9 +45352,37 @@ function getSlotContentRenderer2() {
44999
45352
  _slotContentRenderer2 = mod.SlotContentRenderer;
45000
45353
  return _slotContentRenderer2;
45001
45354
  }
45355
+ function deferLambdaEntityExprs(value) {
45356
+ if (typeof value === "string") {
45357
+ if (containsEntityBinding(value)) {
45358
+ const marker = { [RENDER_BINDING_MARKER]: true, expression: value };
45359
+ return marker;
45360
+ }
45361
+ return value;
45362
+ }
45363
+ if (Array.isArray(value)) {
45364
+ if (isFnFormLambda(value)) return value;
45365
+ const arr = value;
45366
+ if (isOperatorCall(arr) && isSExpr(arr) && containsEntityBinding(arr)) {
45367
+ const marker = { [RENDER_BINDING_MARKER]: true, expression: arr };
45368
+ return marker;
45369
+ }
45370
+ return arr.map((v) => deferLambdaEntityExprs(v));
45371
+ }
45372
+ if (value !== null && typeof value === "object" && !React83__default.isValidElement(value) && !(value instanceof Date) && typeof value !== "function" && !isRenderBindingMarker(value)) {
45373
+ const out = {};
45374
+ for (const [k, v] of Object.entries(value)) {
45375
+ out[k] = deferLambdaEntityExprs(v);
45376
+ }
45377
+ return out;
45378
+ }
45379
+ return value;
45380
+ }
45002
45381
  function makeLambdaFn(params, lambdaBody, callerKey) {
45003
45382
  return (item, index) => {
45004
- const resolvedBody = resolveLambdaBindings(lambdaBody, params, item, index);
45383
+ const resolvedBody = deferLambdaEntityExprs(
45384
+ resolveLambdaBindings(lambdaBody, params, item, index)
45385
+ );
45005
45386
  if (resolvedBody === null || typeof resolvedBody !== "object" || Array.isArray(resolvedBody) || typeof resolvedBody === "function" || React83__default.isValidElement(resolvedBody) || resolvedBody instanceof Date) {
45006
45387
  return null;
45007
45388
  }
@@ -45026,6 +45407,7 @@ function makeLambdaFn(params, lambdaBody, callerKey) {
45026
45407
  }
45027
45408
  function convertNode(node, callerKey) {
45028
45409
  if (node === null || node === void 0) return node;
45410
+ if (isRenderBindingMarker(node)) return node;
45029
45411
  if (Array.isArray(node)) {
45030
45412
  if (isFnFormLambda(node)) {
45031
45413
  const arr2 = node;
@@ -45078,6 +45460,7 @@ var tickLog = createLogger("almadar:ui:tick-effects");
45078
45460
  setNamespaceLevel("almadar:ui:tick-effects", "WARN");
45079
45461
  var sharedEntityLog = createLogger("almadar:ui:shared-entity");
45080
45462
  setNamespaceLevel("almadar:ui:shared-entity", "WARN");
45463
+ var EMPTY_BINDING_SNAPSHOT = {};
45081
45464
  var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
45082
45465
  "set",
45083
45466
  "emit",
@@ -45098,11 +45481,6 @@ var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
45098
45481
  "do",
45099
45482
  "when"
45100
45483
  ]);
45101
- var REACTIVE_REPAINT_PATTERN_TYPES = /* @__PURE__ */ new Set([
45102
- "canvas",
45103
- "game-hud",
45104
- "game-shell"
45105
- ]);
45106
45484
  var LIFECYCLE_EVENTS = ["INIT", "LOAD", "$MOUNT"];
45107
45485
  function toTraitDefinition(binding) {
45108
45486
  return {
@@ -45143,6 +45521,27 @@ function classifySharedTick(tick) {
45143
45521
  if (renders) return "renderer";
45144
45522
  return "neither";
45145
45523
  }
45524
+ function buildTraitRenderConfig(binding, traitConfigsByName, payload) {
45525
+ const declaredDefaults = collectDeclaredConfigDefaults(binding.trait);
45526
+ const resolvedDefaults = traitConfigsByName?.[binding.trait.name];
45527
+ const callSiteConfig = getBindingConfig(binding);
45528
+ const callSiteOverrides = callSiteConfig ? resolveCallSitePayloadCaptures(
45529
+ Object.fromEntries(
45530
+ Object.entries(callSiteConfig).filter(
45531
+ ([, v]) => !containsConfigForward(v)
45532
+ )
45533
+ ),
45534
+ payload
45535
+ ) : void 0;
45536
+ if (declaredDefaults || resolvedDefaults || callSiteOverrides) {
45537
+ return {
45538
+ ...declaredDefaults ?? {},
45539
+ ...resolvedDefaults ?? {},
45540
+ ...callSiteOverrides ?? {}
45541
+ };
45542
+ }
45543
+ return void 0;
45544
+ }
45146
45545
  function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
45147
45546
  return (scratch) => {
45148
45547
  const traitName = binding.trait.name;
@@ -45363,8 +45762,24 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45363
45762
  }, [traitStates]);
45364
45763
  const traitSnapshotDataRef = useRef(/* @__PURE__ */ new Map());
45365
45764
  const traitFieldStatesRef = useRef(/* @__PURE__ */ new Map());
45366
- const lastCanvasRenderUiRef = useRef(/* @__PURE__ */ new Map());
45367
- const executeTransitionEffectsRef = useRef(null);
45765
+ const bridgeEchoPendingRef = useRef(/* @__PURE__ */ new Map());
45766
+ const bindingSnapshotsRef = useRef(/* @__PURE__ */ new Map());
45767
+ const bindingListenersRef = useRef(/* @__PURE__ */ new Map());
45768
+ const publishBindingSnapshot = useCallback((traitName, row) => {
45769
+ bindingSnapshotsRef.current.set(traitName, { ...row });
45770
+ const listeners7 = bindingListenersRef.current.get(traitName);
45771
+ if (!listeners7) return;
45772
+ listeners7.forEach((callback) => {
45773
+ try {
45774
+ callback();
45775
+ } catch (error) {
45776
+ stateLog.error("binding-snapshot listener error", {
45777
+ traitName,
45778
+ error: error instanceof Error ? error.message : String(error)
45779
+ });
45780
+ }
45781
+ });
45782
+ }, []);
45368
45783
  useEffect(() => {
45369
45784
  const mgr = managerRef.current;
45370
45785
  const bindings = traitBindingsRef.current;
@@ -45492,15 +45907,6 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45492
45907
  const existing = pendingSlots.get(slot) || [];
45493
45908
  existing.push({ pattern, props: props || {} });
45494
45909
  pendingSlots.set(slot, existing);
45495
- const patternType = pattern?.type;
45496
- if (patternType !== void 0 && REACTIVE_REPAINT_PATTERN_TYPES.has(patternType)) {
45497
- const rawForSlot = effects.filter(
45498
- (e) => Array.isArray(e) && (e[0] === "render-ui" || e[0] === "render") && e[1] === slot
45499
- );
45500
- if (rawForSlot.length > 0) {
45501
- lastCanvasRenderUiRef.current.set(traitName, rawForSlot);
45502
- }
45503
- }
45504
45910
  },
45505
45911
  clearSlot: (slot) => {
45506
45912
  pendingSlots.set(slot, []);
@@ -45572,11 +45978,13 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45572
45978
  };
45573
45979
  }
45574
45980
  const sharedWrites = [];
45981
+ let didWrite = false;
45575
45982
  const baseSet = handlers.set;
45576
45983
  handlers = {
45577
45984
  ...handlers,
45578
45985
  set: async (targetId, field, value) => {
45579
45986
  if (baseSet) await baseSet(targetId, field, value);
45987
+ didWrite = true;
45580
45988
  if (sharedKey !== void 0) {
45581
45989
  sharedWrites.push({ field, value });
45582
45990
  }
@@ -45593,23 +46001,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45593
46001
  payload: payload || {},
45594
46002
  state: previousState
45595
46003
  };
45596
- const declaredDefaults = collectDeclaredConfigDefaults(binding.trait);
45597
- const resolvedDefaults = traitConfigsByName?.[traitName];
45598
- const callSiteConfig = getBindingConfig(binding);
45599
- const callSiteOverrides = callSiteConfig ? resolveCallSitePayloadCaptures(
45600
- Object.fromEntries(
45601
- Object.entries(callSiteConfig).filter(
45602
- ([, v]) => !containsConfigForward(v)
45603
- )
45604
- ),
45605
- payload || {}
45606
- ) : void 0;
45607
- if (declaredDefaults || resolvedDefaults || callSiteOverrides) {
45608
- bindingCtx.config = {
45609
- ...declaredDefaults ?? {},
45610
- ...resolvedDefaults ?? {},
45611
- ...callSiteOverrides ?? {}
45612
- };
46004
+ const renderConfig = buildTraitRenderConfig(binding, traitConfigsByName, payload || {});
46005
+ if (renderConfig !== void 0) {
46006
+ bindingCtx.config = renderConfig;
45613
46007
  }
45614
46008
  if (traitName === "Authority" || traitName === "Hero") {
45615
46009
  sharedEntityLog.debug("executeTransitionEffects config tiles", {
@@ -45643,7 +46037,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45643
46037
  effectHeads: effects.map((e) => Array.isArray(e) ? String(e[0]) : "??")
45644
46038
  });
45645
46039
  }
45646
- const executor = new EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
46040
+ const executor = new EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext, deferRenderBindings: true });
45647
46041
  try {
45648
46042
  await executor.executeAll(effects);
45649
46043
  if (traitName === "Authority" || traitName === "Hero") {
@@ -45684,37 +46078,8 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45684
46078
  entity: binding.linkedEntity
45685
46079
  });
45686
46080
  }
45687
- if (pendingSlots.size === 0 && effectsCallOp(effects, SHARED_ENTITY_WRITE_OPS)) {
45688
- const stashed = lastCanvasRenderUiRef.current.get(traitName);
45689
- if (stashed !== void 0 && stashed.length > 0) {
45690
- await executor.executeAll(stashed);
45691
- for (const [slot, patterns] of pendingSlots) {
45692
- flushSlot(traitName, slot, patterns, {
45693
- event: flushEvent,
45694
- state: previousState,
45695
- entity: binding.linkedEntity
45696
- });
45697
- }
45698
- }
45699
- if (sharedKey !== void 0 && executeTransitionEffectsRef.current !== null) {
45700
- for (const sibling of traitBindingsRef.current) {
45701
- const siblingName = sibling.trait.name;
45702
- if (siblingName === traitName) continue;
45703
- if (sharedKeyByTraitNameRef.current.get(siblingName) !== sharedKey) continue;
45704
- const siblingStash = lastCanvasRenderUiRef.current.get(siblingName);
45705
- if (siblingStash === void 0 || siblingStash.length === 0) continue;
45706
- const siblingState = traitStatesRef.current.get(siblingName)?.currentState ?? "";
45707
- await executeTransitionEffectsRef.current({
45708
- binding: sibling,
45709
- effects: siblingStash,
45710
- previousState: siblingState,
45711
- newState: siblingState,
45712
- flushEvent: `${flushEvent}:repaint`,
45713
- syncOnly,
45714
- log: log9
45715
- });
45716
- }
45717
- }
46081
+ if (didWrite && sharedKey === void 0) {
46082
+ publishBindingSnapshot(traitName, liveEntity);
45718
46083
  }
45719
46084
  } catch (error) {
45720
46085
  log9.error("effects:error", {
@@ -45726,10 +46091,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45726
46091
  });
45727
46092
  }
45728
46093
  return emittedDuringExec;
45729
- }, [eventBus, flushSlot, sharedEntityStore]);
45730
- useEffect(() => {
45731
- executeTransitionEffectsRef.current = executeTransitionEffects;
45732
- }, [executeTransitionEffects]);
46094
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
45733
46095
  const runTickEffects = useCallback((tick, binding) => {
45734
46096
  const traitName = binding.trait.name;
45735
46097
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -45792,24 +46154,6 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45792
46154
  ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
45793
46155
  );
45794
46156
  runTickFrame(group.storeKey, writers, sharedEntityStore);
45795
- const repaint = executeTransitionEffectsRef.current;
45796
- if (repaint !== null) {
45797
- for (const renderBinding of group.renderBindings) {
45798
- const renderTraitName = renderBinding.trait.name;
45799
- const stash = lastCanvasRenderUiRef.current.get(renderTraitName);
45800
- if (stash === void 0 || stash.length === 0) continue;
45801
- const renderState = traitStatesRef.current.get(renderTraitName)?.currentState ?? "";
45802
- void repaint({
45803
- binding: renderBinding,
45804
- effects: stash,
45805
- previousState: renderState,
45806
- newState: renderState,
45807
- flushEvent: "tick:repaint",
45808
- syncOnly: true,
45809
- log: sharedEntityLog
45810
- });
45811
- }
45812
- }
45813
46157
  };
45814
46158
  if (interval === "frame") {
45815
46159
  scheduler.add(0, onDue);
@@ -45880,6 +46224,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45880
46224
  executedTraits: results.map((r) => r.traitName).join(",")
45881
46225
  });
45882
46226
  const emittedByTrait = /* @__PURE__ */ new Map();
46227
+ bridgeEchoPendingRef.current.clear();
45883
46228
  for (const { traitName, result } of results) {
45884
46229
  const binding = bindingMap.get(traitName);
45885
46230
  const traitState = currentManager.getState(traitName);
@@ -45934,6 +46279,12 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
45934
46279
  log: stateLog
45935
46280
  });
45936
46281
  emittedByTrait.set(traitName, emittedDuringExec);
46282
+ for (const emittedKey of emittedDuringExec) {
46283
+ bridgeEchoPendingRef.current.set(
46284
+ emittedKey,
46285
+ (bridgeEchoPendingRef.current.get(emittedKey) ?? 0) + 1
46286
+ );
46287
+ }
45937
46288
  } else if (!result.executed) {
45938
46289
  if (result.guardResult === false) {
45939
46290
  stateLog.debug("guard-blocked-transition", {
@@ -46083,8 +46434,13 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
46083
46434
  crossTraitLog.debug("self:subscribe", { traitName, busKey: selfBusKey, eventKey });
46084
46435
  const unsub = eventBus.on(selfBusKey, (event) => {
46085
46436
  if (event.source && event.source.dispatched) {
46086
- crossTraitLog.debug("self:fire-skipped-bridge-echo", { traitName, busKey: selfBusKey, eventKey });
46087
- return;
46437
+ const pendingEchoes = bridgeEchoPendingRef.current.get(eventKey) ?? 0;
46438
+ if (pendingEchoes > 0) {
46439
+ bridgeEchoPendingRef.current.set(eventKey, pendingEchoes - 1);
46440
+ crossTraitLog.debug("self:fire-skipped-bridge-echo", { traitName, busKey: selfBusKey, eventKey });
46441
+ return;
46442
+ }
46443
+ crossTraitLog.debug("self:fire-server-cascade", { traitName, busKey: selfBusKey, eventKey });
46088
46444
  }
46089
46445
  crossTraitLog.debug("self:fire", { traitName, busKey: selfBusKey, eventKey });
46090
46446
  enqueueAndDrain(eventKey, event.payload, traitName);
@@ -46176,11 +46532,47 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
46176
46532
  initedTraitsRef.current = /* @__PURE__ */ new Set();
46177
46533
  };
46178
46534
  }, [traitBindings, enqueueAndDrain]);
46535
+ const entityBindingSource = useMemo(() => ({
46536
+ getEntitySnapshot: (traitName) => {
46537
+ const sharedKey = sharedKeyByTraitNameRef.current.get(traitName);
46538
+ if (sharedKey !== void 0) {
46539
+ return sharedEntityStore.getSnapshot(sharedKey);
46540
+ }
46541
+ return bindingSnapshotsRef.current.get(traitName) ?? EMPTY_BINDING_SNAPSHOT;
46542
+ },
46543
+ getConfig: (traitName) => {
46544
+ const binding = traitBindingsRef.current.find((b) => b.trait.name === traitName);
46545
+ if (binding === void 0) return void 0;
46546
+ return buildTraitRenderConfig(binding, traitConfigsByName, {});
46547
+ },
46548
+ getState: (traitName) => traitStatesRef.current.get(traitName)?.currentState ?? "",
46549
+ subscribe: (traitName, callback) => {
46550
+ const sharedKey = sharedKeyByTraitNameRef.current.get(traitName);
46551
+ if (sharedKey !== void 0) {
46552
+ return sharedEntityStore.subscribe(sharedKey, callback);
46553
+ }
46554
+ let listeners7 = bindingListenersRef.current.get(traitName);
46555
+ if (!listeners7) {
46556
+ listeners7 = /* @__PURE__ */ new Set();
46557
+ bindingListenersRef.current.set(traitName, listeners7);
46558
+ }
46559
+ listeners7.add(callback);
46560
+ return () => {
46561
+ const current = bindingListenersRef.current.get(traitName);
46562
+ if (!current) return;
46563
+ current.delete(callback);
46564
+ if (current.size === 0) {
46565
+ bindingListenersRef.current.delete(traitName);
46566
+ }
46567
+ };
46568
+ }
46569
+ }), [sharedEntityStore, traitConfigsByName]);
46179
46570
  return {
46180
46571
  traitStates,
46181
46572
  sendEvent,
46182
46573
  getTraitState,
46183
- canHandleEvent
46574
+ canHandleEvent,
46575
+ entityBindingSource
46184
46576
  };
46185
46577
  }
46186
46578
  var resolvedSchemaLog = createLogger("almadar:ui:resolved-schema");
@@ -46372,7 +46764,7 @@ function collectServerActiveTraits(ir, allTraits, mountedTraitNames) {
46372
46764
  }
46373
46765
  return active;
46374
46766
  }
46375
- function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNavigate, onLocalFallback, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, serverActiveTraits }) {
46767
+ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNavigate, onLocalFallback, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, serverActiveTraits, children }) {
46376
46768
  const bridge = useServerBridge();
46377
46769
  const activeTraitNames = useMemo(
46378
46770
  () => new Set(traits2.map((b) => b.trait.name).filter((n) => !!n)),
@@ -46402,7 +46794,7 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
46402
46794
  }
46403
46795
  }, [bridge.connected, bridge.sendEvent, orbitalNames, uiSlots, onNavigate, embeddedTraits, activeTraitNames, withActiveTraits]);
46404
46796
  const opts = orbitalNames ? { onEventProcessed, navigate: onNavigate, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams } : { navigate: onNavigate, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams };
46405
- const { sendEvent } = useTraitStateMachine(traits2, uiSlots, opts);
46797
+ const { sendEvent, entityBindingSource } = useTraitStateMachine(traits2, uiSlots, opts);
46406
46798
  const initSentRef = useRef(false);
46407
46799
  const prevTraitsRef = useRef(void 0);
46408
46800
  const paramsKey = JSON.stringify(routeParams ?? {});
@@ -46448,7 +46840,7 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
46448
46840
  { type: "fetch", args: [], status: "executed" },
46449
46841
  ...effects.map((eff) => ({
46450
46842
  type: eff.type,
46451
- args: eff.type === "render-ui" ? [eff.slot] : [],
46843
+ args: eff.type === "render-ui" && eff.slot !== void 0 ? [eff.slot] : [],
46452
46844
  status: "executed"
46453
46845
  }))
46454
46846
  ];
@@ -46464,7 +46856,7 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
46464
46856
  }
46465
46857
  })();
46466
46858
  }, [bridge.connected, orbitalNames, bridge.sendEvent, uiSlots, onNavigate, embeddedTraits, activeTraitNames, withActiveTraits, routeParams]);
46467
- return null;
46859
+ return /* @__PURE__ */ jsx(EntityBindingContext.Provider, { value: entityBindingSource, children });
46468
46860
  }
46469
46861
  function SchemaRunner({ schema, serverUrl, transport, mockData, pageName, routeParams, onNavigate, onLocalFallback, persistence }) {
46470
46862
  const { traits: traits2, allEntities, allTraits, ir } = useResolvedSchema(schema, pageName);
@@ -46597,30 +46989,28 @@ function SchemaRunner({ schema, serverUrl, transport, mockData, pageName, routeP
46597
46989
  }
46598
46990
  return schema.orbitals[0]?.theme;
46599
46991
  }, [schema, pageName]);
46600
- const inner = /* @__PURE__ */ jsx(VerificationProvider, { enabled: true, children: /* @__PURE__ */ jsxs(
46992
+ const inner = /* @__PURE__ */ jsx(VerificationProvider, { enabled: true, children: /* @__PURE__ */ jsx(
46601
46993
  EntitySchemaProvider,
46602
46994
  {
46603
46995
  entities: entitiesArray,
46604
46996
  traitLinkedEntities: traitLinkedEntitiesMap,
46605
46997
  orbitalsByTrait: orbitalsByTraitMap,
46606
- children: [
46607
- /* @__PURE__ */ jsx(
46608
- TraitInitializer,
46609
- {
46610
- traits: allPageTraits,
46611
- routeParams,
46612
- orbitalNames: serverUrl || transport ? pageOrbitalNames : void 0,
46613
- traitConfigsByName,
46614
- orbitalsByTrait,
46615
- embeddedTraits,
46616
- serverActiveTraits,
46617
- onNavigate,
46618
- onLocalFallback,
46619
- persistence
46620
- }
46621
- ),
46622
- /* @__PURE__ */ jsx(OrbitalThemeProvider, { theme: activeOrbitalTheme, children: /* @__PURE__ */ jsx(Box, { className: "h-full min-h-full overflow-auto p-4", children: /* @__PURE__ */ jsx(UISlotRenderer, { includeHud: true, hudMode: "inline", includeFloating: true }) }) })
46623
- ]
46998
+ children: /* @__PURE__ */ jsx(
46999
+ TraitInitializer,
47000
+ {
47001
+ traits: allPageTraits,
47002
+ routeParams,
47003
+ orbitalNames: serverUrl || transport ? pageOrbitalNames : void 0,
47004
+ traitConfigsByName,
47005
+ orbitalsByTrait,
47006
+ embeddedTraits,
47007
+ serverActiveTraits,
47008
+ onNavigate,
47009
+ onLocalFallback,
47010
+ persistence,
47011
+ children: /* @__PURE__ */ jsx(OrbitalThemeProvider, { theme: activeOrbitalTheme, children: /* @__PURE__ */ jsx(Box, { className: "h-full min-h-full overflow-auto p-4", children: /* @__PURE__ */ jsx(UISlotRenderer, { includeHud: true, hudMode: "inline", includeFloating: true }) }) })
47012
+ }
47013
+ )
46624
47014
  }
46625
47015
  ) });
46626
47016
  if (serverUrl || transport) {