@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
package/dist/avl/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
2
  import * as React92 from 'react';
3
3
  import React92__default, { createContext, useState, useMemo, useRef, useEffect, useContext, useCallback, Suspense, useLayoutEffect, Profiler, useReducer, useSyncExternalStore, lazy, useId } from 'react';
4
- import { getAllPages, matchPathAmong, OrbitalProvider, EventBusContext, useTraitScopeChain, ServerBridgeProvider, VerificationProvider, EntitySchemaProvider, OrbitalThemeProvider, useServerBridge, useEntitySchema, useEntitySchemaOptional, TraitScopeProvider, useCurrentPagePath, useGameAudioContextOptional } from '@almadar/ui/providers';
4
+ import { getAllPages, matchPathAmong, OrbitalProvider, EventBusContext, useTraitScopeChain, ServerBridgeProvider, VerificationProvider, EntitySchemaProvider, OrbitalThemeProvider, useServerBridge, EntityBindingContext, useEntitySchema, useEntitySchemaOptional, useEntityBindingSnapshot, TraitScopeProvider, useTraitScope, useCurrentPagePath, useGameAudioContextOptional } from '@almadar/ui/providers';
5
5
  import { createLogger, setNamespaceLevel, isLogLevelEnabled } from '@almadar/logger';
6
6
  import ELK from 'elkjs/lib/elk.bundled.js';
7
7
  import { MarkerType, useReactFlow, Handle, Position, getBezierPath, EdgeLabelRenderer, useNodeId, ReactFlowProvider, BaseEdge, useNodesState, useEdgesState, ReactFlow, Controls, Background, BackgroundVariant } from '@xyflow/react';
8
8
  import { useTranslate } from '@almadar/ui/hooks';
9
+ import { InMemoryPersistence, StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, normalizeCallSiteConfigToValues, interpolateValue } from '@almadar/runtime';
10
+ import { FieldTypeSchema, isInlineTrait, isPageReference, buildResolvedTraitConfigs, schemaToIR, getPage, mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, walkSExpr, isRenderBindingMarker, containsEntityBinding, isSExpr, isEventPayloadValue, RENDER_BINDING_MARKER } from '@almadar/core';
9
11
  import * as LucideIcons2 from 'lucide-react';
10
12
  import { Loader2, X, Code, FileText, WrapText, Check, Copy, Lightbulb, CheckCircle, List, Printer, ChevronRight, ChevronLeft, GitBranch, Pencil, Eye, Plus, ArrowRight, Trash, RotateCcw, Play, Terminal, XCircle, AlertTriangle, Trash2, Link2, ZoomOut, ZoomIn, Download, ChevronDown, Menu as Menu$1, Package, Calendar, MoreHorizontal, Image as Image$1, Upload, ArrowLeft, HelpCircle, PauseCircle, Search, Type, Heading1, Heading2, Heading3, ListOrdered, Quote, Minus, Eraser, TrendingUp, TrendingDown, AlertCircle, Circle, Clock, CheckCircle2, ChevronUp, Tag, User, DollarSign } from 'lucide-react';
11
13
  import { createPortal } from 'react-dom';
@@ -66,15 +68,14 @@ import ReactMarkdown from 'react-markdown';
66
68
  import remarkGfm from 'remark-gfm';
67
69
  import remarkMath from 'remark-math';
68
70
  import rehypeKatex from 'rehype-katex';
69
- import { FieldTypeSchema, isInlineTrait, isPageReference, buildResolvedTraitConfigs, schemaToIR, getPage, mergeEntityFrame, isCircuitEvent, applyListenPayloadMapping, walkSExpr, isSExpr, isEventPayloadValue } from '@almadar/core';
70
71
  import { useDroppable, useDraggable, DndContext, DragOverlay, useSensors, useSensor, PointerSensor, KeyboardSensor, pointerWithin, rectIntersection, closestCorners } from '@dnd-kit/core';
71
72
  import { sortableKeyboardCoordinates, useSortable, arrayMove, SortableContext, rectSortingStrategy, verticalListSortingStrategy } from '@dnd-kit/sortable';
72
73
  import { CSS } from '@dnd-kit/utilities';
74
+ import { ordered, lib } from 'emojilib';
73
75
  import { forceSimulation, forceLink, forceManyBody, forceCollide, forceX, forceY } from 'd3-force';
74
76
  import { isDrawHostPattern, getPatternDefinition, getComponentForPattern as getComponentForPattern$1, isEntityAwarePattern } from '@almadar/core/patterns';
75
77
  import { OrbitalServerRuntime } from '@almadar/runtime/OrbitalServerRuntime';
76
78
  import { isKnownStdOperator } from '@almadar/std/registry';
77
- import { InMemoryPersistence, StateMachineManager, collectDeclaredConfigDefaults, resolveCallSitePayloadCaptures, createServerEffectHandlers, EffectExecutor, createContextFromBindings, createTickScheduler, isValidCronExpression, parseDurationString, normalizeCallSiteConfigToValues } from '@almadar/runtime';
78
79
 
79
80
  var __defProp = Object.defineProperty;
80
81
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -3162,6 +3163,32 @@ function formatValue(value, format) {
3162
3163
  return String(value);
3163
3164
  }
3164
3165
  }
3166
+ function compareCellValues(a, b) {
3167
+ const aEmpty = a === null || a === void 0 || a === "";
3168
+ const bEmpty = b === null || b === void 0 || b === "";
3169
+ if (aEmpty || bEmpty) return aEmpty && bEmpty ? 0 : aEmpty ? 1 : -1;
3170
+ if (typeof a === "number" && typeof b === "number") return a - b;
3171
+ if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
3172
+ const aNum = Number(a);
3173
+ const bNum = Number(b);
3174
+ if (Number.isFinite(aNum) && Number.isFinite(bNum)) return aNum - bNum;
3175
+ const aTime = dateLikeTime(a);
3176
+ const bTime = dateLikeTime(b);
3177
+ if (aTime !== null && bTime !== null) return aTime - bTime;
3178
+ return String(a).localeCompare(String(b));
3179
+ }
3180
+ function dateLikeTime(value) {
3181
+ if (value instanceof Date) return value.getTime();
3182
+ const text = String(value);
3183
+ if (!/\d/.test(text) || !/[-/:T]/.test(text)) return null;
3184
+ const time = Date.parse(text);
3185
+ return Number.isNaN(time) ? null : time;
3186
+ }
3187
+ function sortRows(rows, field, direction = "asc") {
3188
+ if (!field) return rows;
3189
+ const dir = direction === "desc" ? -1 : 1;
3190
+ return [...rows].sort((a, b) => dir * compareCellValues(a?.[field], b?.[field]));
3191
+ }
3165
3192
  var init_format = __esm({
3166
3193
  "lib/format.ts"() {
3167
3194
  }
@@ -4544,6 +4571,75 @@ var init_BehaviorView = __esm({
4544
4571
  BehaviorView.displayName = "BehaviorView";
4545
4572
  }
4546
4573
  });
4574
+ function resolveMarkerExpression(expression, entity, config, state) {
4575
+ const ctx = createContextFromBindings({
4576
+ entity,
4577
+ payload: {},
4578
+ state,
4579
+ ...config !== void 0 ? { config } : {}
4580
+ });
4581
+ return interpolateValue(expression, ctx);
4582
+ }
4583
+ function isPlainObject(value) {
4584
+ if (value === null || value === void 0 || typeof value !== "object") return false;
4585
+ if (Array.isArray(value)) return false;
4586
+ if (React92__default.isValidElement(value)) return false;
4587
+ if (value instanceof Date) return false;
4588
+ if (typeof value === "function") return false;
4589
+ return true;
4590
+ }
4591
+ function walkValue(value, scopeTrait, entity, config, state) {
4592
+ if (isRenderBindingMarker(value)) {
4593
+ return { resolved: resolveMarkerExpression(value.expression, entity, config, state), changed: true };
4594
+ }
4595
+ if (Array.isArray(value)) {
4596
+ const out = [];
4597
+ let changed = false;
4598
+ for (const item of value) {
4599
+ const element = item;
4600
+ const wasMarker = isRenderBindingMarker(element);
4601
+ const { resolved, changed: itemChanged } = walkValue(element, scopeTrait, entity, config, state);
4602
+ if (wasMarker && Array.isArray(resolved)) {
4603
+ out.push(...resolved);
4604
+ changed = true;
4605
+ continue;
4606
+ }
4607
+ out.push(resolved);
4608
+ if (itemChanged) changed = true;
4609
+ }
4610
+ return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
4611
+ }
4612
+ if (isPlainObject(value)) {
4613
+ const sourceTrait = value._sourceTrait;
4614
+ if (typeof sourceTrait === "string" && sourceTrait !== scopeTrait) {
4615
+ return { resolved: value, changed: false };
4616
+ }
4617
+ const out = {};
4618
+ let changed = false;
4619
+ for (const [key, item] of Object.entries(value)) {
4620
+ const { resolved, changed: itemChanged } = walkValue(item, scopeTrait, entity, config, state);
4621
+ out[key] = resolved;
4622
+ if (itemChanged) changed = true;
4623
+ }
4624
+ return changed ? { resolved: out, changed: true } : { resolved: value, changed: false };
4625
+ }
4626
+ return { resolved: value, changed: false };
4627
+ }
4628
+ function resolveRenderBindingMarkers(props, scopeTrait, entity, config, state) {
4629
+ const out = {};
4630
+ let changed = false;
4631
+ for (const [key, value] of Object.entries(props)) {
4632
+ const { resolved, changed: propChanged } = walkValue(value, scopeTrait, entity, config, state);
4633
+ out[key] = resolved;
4634
+ if (propChanged) changed = true;
4635
+ }
4636
+ return changed ? out : props;
4637
+ }
4638
+ var init_resolve_render_bindings = __esm({
4639
+ "lib/resolve-render-bindings.ts"() {
4640
+ "use client";
4641
+ }
4642
+ });
4547
4643
  function getCurrentIconFamily() {
4548
4644
  if (typeof window === "undefined" || typeof document === "undefined") {
4549
4645
  return DEFAULT_FAMILY;
@@ -4786,9 +4882,13 @@ function getAtlas(url, onReady) {
4786
4882
  onReady();
4787
4883
  }).catch(() => {
4788
4884
  atlasCache.set(url, null);
4885
+ onReady();
4789
4886
  });
4790
4887
  return void 0;
4791
4888
  }
4889
+ function atlasFailed(url) {
4890
+ return atlasCache.get(url) === null;
4891
+ }
4792
4892
  function subRectFor(atlas, sprite) {
4793
4893
  if (isTilesheet(atlas)) {
4794
4894
  let col;
@@ -6846,6 +6946,20 @@ var init_Input = __esm({
6846
6946
  const { t } = useTranslate();
6847
6947
  const eventBus = useEventBus();
6848
6948
  const type = inputType || htmlType || "text";
6949
+ const isDeclarative = typeof onChange === "string";
6950
+ const [localValue, setLocalValue] = React92__default.useState(value);
6951
+ const pendingEchoRef = React92__default.useRef(/* @__PURE__ */ new Set());
6952
+ React92__default.useEffect(() => {
6953
+ if (!isDeclarative) return;
6954
+ const incoming = value == null ? "" : String(value);
6955
+ if (pendingEchoRef.current.has(incoming)) {
6956
+ pendingEchoRef.current.delete(incoming);
6957
+ return;
6958
+ }
6959
+ pendingEchoRef.current.clear();
6960
+ setLocalValue(value);
6961
+ }, [value, isDeclarative]);
6962
+ const displayValue = isDeclarative ? localValue : value;
6849
6963
  const resolveIconNode = (i, cls) => {
6850
6964
  if (!i) return null;
6851
6965
  if (typeof i === "string") return /* @__PURE__ */ jsx(Icon, { name: i, className: cls });
@@ -6855,7 +6969,7 @@ var init_Input = __esm({
6855
6969
  const iconCls = "h-icon-default w-icon-default";
6856
6970
  const IconComponent = typeof iconProp === "string" ? resolveIcon(iconProp) : iconProp;
6857
6971
  const resolvedLeftIcon = (leftIcon ? resolveIconNode(leftIcon, iconCls) : null) || IconComponent && /* @__PURE__ */ jsx(IconComponent, { className: iconCls });
6858
- const showClearButton = clearable && value && String(value).length > 0;
6972
+ const showClearButton = clearable && displayValue && String(displayValue).length > 0;
6859
6973
  const isMultiline = type === "textarea";
6860
6974
  const baseClassName = cn(
6861
6975
  "block w-full rounded-sm transition-all duration-fast",
@@ -6874,6 +6988,10 @@ var init_Input = __esm({
6874
6988
  if (typeof onChange === "string") {
6875
6989
  const target = e.target;
6876
6990
  const payload = type === "checkbox" ? { checked: target.checked } : { value: target.value };
6991
+ if (type !== "checkbox") {
6992
+ pendingEchoRef.current.add(target.value);
6993
+ setLocalValue(target.value);
6994
+ }
6877
6995
  eventBus.emit(`UI:${onChange}`, payload);
6878
6996
  } else {
6879
6997
  onChange?.(e);
@@ -6904,7 +7022,7 @@ var init_Input = __esm({
6904
7022
  "select",
6905
7023
  {
6906
7024
  ref,
6907
- value,
7025
+ value: displayValue,
6908
7026
  onChange: handleChange,
6909
7027
  className: cn(baseClassName, "appearance-none pr-10", className),
6910
7028
  ...props,
@@ -6924,7 +7042,7 @@ var init_Input = __esm({
6924
7042
  "textarea",
6925
7043
  {
6926
7044
  ref,
6927
- value,
7045
+ value: displayValue,
6928
7046
  onChange: handleChange,
6929
7047
  rows,
6930
7048
  className: baseClassName,
@@ -6963,7 +7081,7 @@ var init_Input = __esm({
6963
7081
  {
6964
7082
  ref,
6965
7083
  type,
6966
- value,
7084
+ value: displayValue,
6967
7085
  onChange: handleChange,
6968
7086
  onKeyDown: handleKeyDown,
6969
7087
  className: baseClassName,
@@ -10800,46 +10918,45 @@ var init_useImageCache = __esm({
10800
10918
  });
10801
10919
 
10802
10920
  // lib/isometric.ts
10803
- function isoToScreen(tileX, tileY, scale, baseOffsetX, layout = "isometric") {
10804
- const scaledTileWidth = TILE_WIDTH * scale;
10805
- const scaledFloorHeight = FLOOR_HEIGHT * scale;
10921
+ function isoToScreen(tileX, tileY, cellWidth, baseOffsetX, layout = "isometric") {
10922
+ const w = cellWidth;
10923
+ const fh = cellWidth / 2;
10806
10924
  if (layout === "hex") {
10807
- const screenX2 = tileX * scaledTileWidth + (tileY & 1) * (scaledTileWidth / 2) + baseOffsetX;
10808
- const screenY2 = tileY * (scaledFloorHeight * 0.75);
10925
+ const screenX2 = tileX * w + (tileY & 1) * (w / 2) + baseOffsetX;
10926
+ const screenY2 = tileY * (fh * 0.75);
10809
10927
  return { x: screenX2, y: screenY2 };
10810
10928
  }
10811
10929
  if (layout === "flat") {
10812
- const screenX2 = tileX * scaledTileWidth + baseOffsetX;
10813
- const screenY2 = tileY * scaledTileWidth;
10930
+ const screenX2 = tileX * w + baseOffsetX;
10931
+ const screenY2 = tileY * w;
10814
10932
  return { x: screenX2, y: screenY2 };
10815
10933
  }
10816
- const screenX = (tileX - tileY) * (scaledTileWidth / 2) + baseOffsetX;
10817
- const screenY = (tileX + tileY) * (scaledFloorHeight / 2);
10934
+ const screenX = (tileX - tileY) * (w / 2) + baseOffsetX;
10935
+ const screenY = (tileX + tileY) * (fh / 2);
10818
10936
  return { x: screenX, y: screenY };
10819
10937
  }
10820
- function screenToIso(screenX, screenY, scale, baseOffsetX, layout = "isometric") {
10821
- const scaledTileWidth = TILE_WIDTH * scale;
10822
- const scaledFloorHeight = FLOOR_HEIGHT * scale;
10938
+ function screenToIso(screenX, screenY, cellWidth, baseOffsetX, layout = "isometric") {
10939
+ const w = cellWidth;
10940
+ const fh = cellWidth / 2;
10823
10941
  if (layout === "hex") {
10824
- const row = Math.round(screenY / (scaledFloorHeight * 0.75));
10825
- const col = Math.round((screenX - (row & 1) * (scaledTileWidth / 2) - baseOffsetX) / scaledTileWidth);
10942
+ const row = Math.round(screenY / (fh * 0.75));
10943
+ const col = Math.round((screenX - (row & 1) * (w / 2) - baseOffsetX) / w);
10826
10944
  return { x: col, y: row };
10827
10945
  }
10828
10946
  if (layout === "flat") {
10829
- const col = Math.round((screenX - baseOffsetX) / scaledTileWidth);
10830
- const row = Math.round(screenY / scaledTileWidth);
10947
+ const col = Math.round((screenX - baseOffsetX) / w);
10948
+ const row = Math.round(screenY / w);
10831
10949
  return { x: col, y: row };
10832
10950
  }
10833
10951
  const adjustedX = screenX - baseOffsetX;
10834
- const tileX = (adjustedX / (scaledTileWidth / 2) + screenY / (scaledFloorHeight / 2)) / 2;
10835
- const tileY = (screenY / (scaledFloorHeight / 2) - adjustedX / (scaledTileWidth / 2)) / 2;
10952
+ const tileX = (adjustedX / (w / 2) + screenY / (fh / 2)) / 2;
10953
+ const tileY = (screenY / (fh / 2) - adjustedX / (w / 2)) / 2;
10836
10954
  return { x: Math.round(tileX), y: Math.round(tileY) };
10837
10955
  }
10838
- var TILE_WIDTH, FLOOR_HEIGHT, DIAMOND_TOP_Y, BACKGROUND_FALLBACK_COLOR, MINIMAP_TERRAIN_COLORS;
10956
+ var TILE_WIDTH, DIAMOND_TOP_Y, BACKGROUND_FALLBACK_COLOR, MINIMAP_TERRAIN_COLORS;
10839
10957
  var init_isometric = __esm({
10840
10958
  "lib/isometric.ts"() {
10841
10959
  TILE_WIDTH = 256;
10842
- FLOOR_HEIGHT = 128;
10843
10960
  DIAMOND_TOP_Y = 374;
10844
10961
  BACKGROUND_FALLBACK_COLOR = "#1a1a2e";
10845
10962
  MINIMAP_TERRAIN_COLORS = {
@@ -11324,210 +11441,6 @@ var init_ChoiceButton = __esm({
11324
11441
  ChoiceButton.displayName = "ChoiceButton";
11325
11442
  }
11326
11443
  });
11327
- function SvgStage({
11328
- cols,
11329
- rows,
11330
- tileSize = 32,
11331
- background = "var(--color-background)",
11332
- tileClickEvent,
11333
- tileHoverEvent,
11334
- tileLeaveEvent,
11335
- keyMap,
11336
- keyUpMap,
11337
- className,
11338
- children
11339
- }) {
11340
- const eventBus = useEventBus();
11341
- const svgRef = useRef(null);
11342
- const pointerDownRef = useRef(null);
11343
- const cellFromClient = useCallback((clientX, clientY) => {
11344
- const svg = svgRef.current;
11345
- if (!svg) return null;
11346
- const rect = svg.getBoundingClientRect();
11347
- if (rect.width === 0 || rect.height === 0) return null;
11348
- const vbW = cols * tileSize;
11349
- const vbH = rows * tileSize;
11350
- const meet = Math.min(rect.width / vbW, rect.height / vbH);
11351
- const offsetX = (rect.width - vbW * meet) / 2;
11352
- const offsetY = (rect.height - vbH * meet) / 2;
11353
- const svgX = (clientX - rect.left - offsetX) / meet;
11354
- const svgY = (clientY - rect.top - offsetY) / meet;
11355
- return {
11356
- x: Math.min(Math.max(Math.floor(svgX / tileSize), 0), cols - 1),
11357
- y: Math.min(Math.max(Math.floor(svgY / tileSize), 0), rows - 1)
11358
- };
11359
- }, [cols, rows, tileSize]);
11360
- const handlePointerDown = useCallback((e) => {
11361
- pointerDownRef.current = { clientX: e.clientX, clientY: e.clientY };
11362
- }, []);
11363
- const handlePointerUp = useCallback((e) => {
11364
- const down = pointerDownRef.current;
11365
- pointerDownRef.current = null;
11366
- if (!tileClickEvent) return;
11367
- if (down && Math.hypot(e.clientX - down.clientX, e.clientY - down.clientY) > 5) return;
11368
- const cell = cellFromClient(e.clientX, e.clientY);
11369
- if (cell) eventBus.emit(`UI:${tileClickEvent}`, cell);
11370
- }, [cellFromClient, tileClickEvent, eventBus]);
11371
- const handlePointerMove = useCallback((e) => {
11372
- if (!tileHoverEvent) return;
11373
- const cell = cellFromClient(e.clientX, e.clientY);
11374
- if (cell) eventBus.emit(`UI:${tileHoverEvent}`, cell);
11375
- }, [cellFromClient, tileHoverEvent, eventBus]);
11376
- const handlePointerLeave = useCallback(() => {
11377
- pointerDownRef.current = null;
11378
- if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
11379
- }, [tileLeaveEvent, eventBus]);
11380
- useEffect(() => {
11381
- if (!keyMap && !keyUpMap) return;
11382
- const onDown = (e) => {
11383
- const ev = keyMap?.[e.code];
11384
- if (ev) {
11385
- eventBus.emit(`UI:${ev}`, {});
11386
- e.preventDefault();
11387
- }
11388
- };
11389
- const onUp = (e) => {
11390
- const ev = keyUpMap?.[e.code];
11391
- if (ev) eventBus.emit(`UI:${ev}`, {});
11392
- };
11393
- window.addEventListener("keydown", onDown);
11394
- window.addEventListener("keyup", onUp);
11395
- return () => {
11396
- window.removeEventListener("keydown", onDown);
11397
- window.removeEventListener("keyup", onUp);
11398
- };
11399
- }, [keyMap, keyUpMap, eventBus]);
11400
- useEffect(() => {
11401
- if (!keyMap && !keyUpMap) return;
11402
- svgRef.current?.focus();
11403
- }, [keyMap, keyUpMap]);
11404
- const stageContext = useMemo(() => ({ tileSize }), [tileSize]);
11405
- return /* @__PURE__ */ jsxs(
11406
- "svg",
11407
- {
11408
- ref: svgRef,
11409
- "data-testid": "svg-stage",
11410
- viewBox: `0 0 ${cols * tileSize} ${rows * tileSize}`,
11411
- preserveAspectRatio: "xMidYMid meet",
11412
- className: cn("block h-full w-full", className),
11413
- tabIndex: keyMap || keyUpMap ? 0 : void 0,
11414
- onPointerDown: handlePointerDown,
11415
- onPointerMove: handlePointerMove,
11416
- onPointerUp: handlePointerUp,
11417
- onPointerLeave: handlePointerLeave,
11418
- children: [
11419
- /* @__PURE__ */ jsx("rect", { width: cols * tileSize, height: rows * tileSize, fill: background }),
11420
- /* @__PURE__ */ jsx(SvgStageContext.Provider, { value: stageContext, children })
11421
- ]
11422
- }
11423
- );
11424
- }
11425
- var SvgStageContext;
11426
- var init_SvgStage = __esm({
11427
- "components/game/molecules/SvgStage.tsx"() {
11428
- "use client";
11429
- init_cn();
11430
- init_useEventBus();
11431
- SvgStageContext = React92.createContext({ tileSize: 1 });
11432
- SvgStage.displayName = "SvgStage";
11433
- }
11434
- });
11435
- function SvgDrawShape({
11436
- shape,
11437
- x,
11438
- y,
11439
- width,
11440
- height,
11441
- radius,
11442
- radiusY,
11443
- points,
11444
- d,
11445
- x2,
11446
- y2,
11447
- fill,
11448
- stroke,
11449
- strokeWidth,
11450
- opacity,
11451
- className
11452
- }) {
11453
- const { tileSize } = useContext(SvgStageContext);
11454
- const cell = (v) => v === void 0 ? void 0 : v * tileSize;
11455
- const paint = {
11456
- fill: fill ?? (stroke === void 0 ? "var(--color-primary)" : "none"),
11457
- stroke,
11458
- strokeWidth,
11459
- opacity,
11460
- className
11461
- };
11462
- return /* @__PURE__ */ jsxs("g", { transform: `translate(${x * tileSize} ${y * tileSize})`, children: [
11463
- shape === "rect" && /* @__PURE__ */ jsx("rect", { width: cell(width), height: cell(height), ...paint }),
11464
- shape === "circle" && /* @__PURE__ */ jsx("circle", { r: cell(radius), ...paint }),
11465
- shape === "ellipse" && /* @__PURE__ */ jsx("ellipse", { rx: cell(radius), ry: cell(radiusY ?? radius), ...paint }),
11466
- shape === "polygon" && /* @__PURE__ */ jsx("polygon", { points, ...paint }),
11467
- shape === "polyline" && /* @__PURE__ */ jsx("polyline", { points, ...paint }),
11468
- shape === "path" && /* @__PURE__ */ jsx("path", { d, ...paint }),
11469
- shape === "line" && /* @__PURE__ */ jsx("line", { x2: cell(x2), y2: cell(y2), ...paint })
11470
- ] });
11471
- }
11472
- var init_SvgDrawShape = __esm({
11473
- "components/game/atoms/SvgDrawShape.tsx"() {
11474
- "use client";
11475
- init_SvgStage();
11476
- SvgDrawShape.displayName = "SvgDrawShape";
11477
- }
11478
- });
11479
- function SvgDrawGroup({
11480
- x = 0,
11481
- y = 0,
11482
- scale,
11483
- rotate,
11484
- opacity,
11485
- className,
11486
- children
11487
- }) {
11488
- const { tileSize } = useContext(SvgStageContext);
11489
- const transforms = [`translate(${x * tileSize} ${y * tileSize})`];
11490
- if (rotate !== void 0) transforms.push(`rotate(${rotate})`);
11491
- if (scale !== void 0) transforms.push(`scale(${scale})`);
11492
- return /* @__PURE__ */ jsx("g", { transform: transforms.join(" "), opacity, className, children });
11493
- }
11494
- var init_SvgDrawGroup = __esm({
11495
- "components/game/atoms/SvgDrawGroup.tsx"() {
11496
- "use client";
11497
- init_SvgStage();
11498
- SvgDrawGroup.displayName = "SvgDrawGroup";
11499
- }
11500
- });
11501
- function SvgDrawText({
11502
- x,
11503
- y,
11504
- text,
11505
- size = 12,
11506
- fill = "var(--color-foreground)",
11507
- anchor = "middle",
11508
- className
11509
- }) {
11510
- const { tileSize } = useContext(SvgStageContext);
11511
- return /* @__PURE__ */ jsx(
11512
- "text",
11513
- {
11514
- x: x * tileSize,
11515
- y: y * tileSize,
11516
- fontSize: size,
11517
- fill,
11518
- textAnchor: anchor,
11519
- className,
11520
- children: text
11521
- }
11522
- );
11523
- }
11524
- var init_SvgDrawText = __esm({
11525
- "components/game/atoms/SvgDrawText.tsx"() {
11526
- "use client";
11527
- init_SvgStage();
11528
- SvgDrawText.displayName = "SvgDrawText";
11529
- }
11530
- });
11531
11444
  function ControlGrid({
11532
11445
  kind,
11533
11446
  buttons = DEFAULT_BUTTONS,
@@ -12231,6 +12144,7 @@ function MiniMap({
12231
12144
  const cached = imgCacheRef.current.get(url);
12232
12145
  if (cached) return cached.complete ? cached : null;
12233
12146
  const img = new Image();
12147
+ img.crossOrigin = "anonymous";
12234
12148
  img.src = url;
12235
12149
  img.onload = () => {
12236
12150
  const canvas = canvasRef.current;
@@ -12398,7 +12312,7 @@ function useCamera(initial) {
12398
12312
  const handleWheel = useCallback((e, drawFn) => {
12399
12313
  e.preventDefault();
12400
12314
  const zoomDelta = e.deltaY > 0 ? 0.9 : 1.1;
12401
- cameraRef.current.zoom = Math.max(0.5, Math.min(3, cameraRef.current.zoom * zoomDelta));
12315
+ cameraRef.current.zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, cameraRef.current.zoom * zoomDelta));
12402
12316
  drawFn?.();
12403
12317
  }, []);
12404
12318
  const handlePointerDown = useCallback((e) => {
@@ -12419,7 +12333,7 @@ function useCamera(initial) {
12419
12333
  const zoomAtPoint = useCallback((factor, centerX, centerY, viewportSize, drawFn) => {
12420
12334
  const cam = cameraRef.current;
12421
12335
  const oldZoom = cam.zoom;
12422
- const newZoom = Math.max(0.5, Math.min(3, oldZoom * factor));
12336
+ const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, oldZoom * factor));
12423
12337
  if (newZoom === oldZoom) {
12424
12338
  drawFn?.();
12425
12339
  return;
@@ -12471,9 +12385,12 @@ function useCamera(initial) {
12471
12385
  lerpToTarget
12472
12386
  };
12473
12387
  }
12388
+ var MIN_ZOOM, MAX_ZOOM;
12474
12389
  var init_useCamera = __esm({
12475
12390
  "hooks/useCamera.ts"() {
12476
12391
  "use client";
12392
+ MIN_ZOOM = 0.05;
12393
+ MAX_ZOOM = 10;
12477
12394
  }
12478
12395
  });
12479
12396
  function localPoint(canvas, clientX, clientY) {
@@ -12594,6 +12511,7 @@ function getOrLoadImage(url, onReady) {
12594
12511
  return null;
12595
12512
  }
12596
12513
  const img = new Image();
12514
+ img.crossOrigin = "anonymous";
12597
12515
  const entry = { img, status: "pending", onReady };
12598
12516
  cache.set(url, entry);
12599
12517
  updateAssetStatus(url, "pending");
@@ -12605,10 +12523,14 @@ function getOrLoadImage(url, onReady) {
12605
12523
  img.onerror = () => {
12606
12524
  entry.status = "failed";
12607
12525
  updateAssetStatus(url, "failed");
12526
+ entry.onReady?.();
12608
12527
  };
12609
12528
  img.src = url;
12610
12529
  return null;
12611
12530
  }
12531
+ function getImageStatus(url) {
12532
+ return cache.get(url)?.status;
12533
+ }
12612
12534
  var cache;
12613
12535
  var init_imageCache = __esm({
12614
12536
  "lib/imageCache.ts"() {
@@ -12717,6 +12639,15 @@ function createWebPainter(ctx, onAssetLoad) {
12717
12639
  ctx.lineWidth = lineWidth;
12718
12640
  ctx.stroke();
12719
12641
  },
12642
+ fillPath(d, color) {
12643
+ ctx.fillStyle = color;
12644
+ ctx.fill(new Path2D(d));
12645
+ },
12646
+ strokePath(d, color, lineWidth = 1) {
12647
+ ctx.strokeStyle = color;
12648
+ ctx.lineWidth = lineWidth;
12649
+ ctx.stroke(new Path2D(d));
12650
+ },
12720
12651
  text(str, x, y, style) {
12721
12652
  if (style.font) ctx.font = style.font;
12722
12653
  ctx.fillStyle = style.color;
@@ -12738,12 +12669,13 @@ var init_webPainter2d = __esm({
12738
12669
 
12739
12670
  // lib/drawable/projector.ts
12740
12671
  function create2DProjector(opts) {
12741
- const { scale, baseOffsetX, layout } = opts;
12742
- const tileWidth = layout === "free" ? 1 : TILE_WIDTH * scale;
12743
- const floorHeight = layout === "free" ? 1 : FLOOR_HEIGHT * scale;
12744
- const diamondTopY = layout === "free" ? 0 : (opts.diamondTopY ?? DIAMOND_TOP_Y) * scale;
12672
+ const { baseOffsetX, layout } = opts;
12673
+ const tw = opts.tileWidth ?? TILE_WIDTH;
12674
+ const tileWidth = layout === "free" ? 1 : tw;
12675
+ const floorHeight = layout === "free" ? 1 : tw / 2;
12676
+ const diamondTopY = layout === "free" ? 0 : opts.diamondTopY ?? tw * (DIAMOND_TOP_Y / TILE_WIDTH);
12745
12677
  const squareGrid = layout === "flat" || layout === "free";
12746
- const project = (pos) => layout === "free" ? { x: pos.x, y: pos.y } : isoToScreen(pos.x, pos.y, scale, baseOffsetX, layout);
12678
+ const project = (pos) => layout === "free" ? { x: pos.x, y: pos.y } : isoToScreen(pos.x, pos.y, tw, baseOffsetX, layout);
12747
12679
  const anchorPoint = (pos, anchor) => {
12748
12680
  const base = project(pos);
12749
12681
  if (anchor === "top-left") return base;
@@ -12774,7 +12706,7 @@ function create2DProjector(opts) {
12774
12706
  { x: base.x, y: topY + floorHeight / 2 }
12775
12707
  ];
12776
12708
  };
12777
- return { project, anchorPoint, cellPath, tileWidth, floorHeight, diamondTopY, scale, squareGrid };
12709
+ return { project, anchorPoint, cellPath, tileWidth, floorHeight, diamondTopY, squareGrid, worldPixelDirect: layout === "free" };
12778
12710
  }
12779
12711
  var init_projector = __esm({
12780
12712
  "lib/drawable/projector.ts"() {
@@ -12790,32 +12722,67 @@ var init_contract = __esm({
12790
12722
  "lib/drawable/contract.ts"() {
12791
12723
  }
12792
12724
  });
12793
-
12794
- // components/game/atoms/DrawSprite.tsx
12725
+ function warnMissingOnce(reason, node) {
12726
+ const key = `${reason}:${node.asset.url}:${String(node.asset.atlas)}:${String(node.asset.sprite)}`;
12727
+ if (loggedMissing.has(key)) return;
12728
+ loggedMissing.add(key);
12729
+ spriteLog.warn("draw-sprite asset unresolvable \u2014 painting fallback square", { reason, url: node.asset.url, atlas: node.asset.atlas, sprite: node.asset.sprite });
12730
+ }
12731
+ function paintFallbackSquare(painter, node, dctx, reason) {
12732
+ warnMissingOnce(reason, node);
12733
+ const tw = dctx.projector.tileWidth;
12734
+ const natural = dctx.projector.worldPixelDirect ? FALLBACK_WORLD_PX : tw;
12735
+ const w = node.width !== void 0 ? node.width * tw : natural;
12736
+ const h = node.height !== void 0 ? node.height * tw : natural;
12737
+ const anchor = node.anchor ?? "top-left";
12738
+ const p = dctx.projector.anchorPoint(node.position, anchor);
12739
+ const dx = anchor === "top-left" ? p.x : p.x - w / 2;
12740
+ const dy = anchor === "ground" ? p.y - h : anchor === "center" ? p.y - h / 2 : p.y;
12741
+ painter.save();
12742
+ if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
12743
+ painter.fillRect(dx, dy, w, h, "#9b8f7f");
12744
+ painter.strokeRect(dx, dy, w, h, "#5e564b", Math.max(1, tw / 32));
12745
+ painter.restore();
12746
+ }
12795
12747
  function DrawSprite(_props) {
12796
12748
  return null;
12797
12749
  }
12798
- var paintSprite;
12750
+ var spriteLog, loggedMissing, FALLBACK_WORLD_PX, paintSprite;
12799
12751
  var init_DrawSprite = __esm({
12800
12752
  "components/game/atoms/DrawSprite.tsx"() {
12801
12753
  "use client";
12802
12754
  init_atlasSlice();
12755
+ init_imageCache();
12803
12756
  init_contract();
12757
+ spriteLog = createLogger("almadar:ui:draw-sprite");
12758
+ loggedMissing = /* @__PURE__ */ new Set();
12759
+ FALLBACK_WORLD_PX = 32;
12804
12760
  paintSprite = (painter, node, dctx) => {
12805
12761
  if (!node.asset?.url || !isValidScenePos(node.position)) return;
12806
12762
  const tex = painter.resolveTexture(node.asset.url);
12807
- if (!tex) return;
12763
+ if (!tex) {
12764
+ if (getImageStatus(node.asset.url) === "failed") paintFallbackSquare(painter, node, dctx, "texture-failed");
12765
+ return;
12766
+ }
12808
12767
  let src = typeof node.frame === "object" ? node.frame : void 0;
12809
12768
  if (!src && isAtlasAsset(node.asset)) {
12810
12769
  const atlas = getAtlas(node.asset.atlas, dctx.invalidate);
12811
- if (!atlas) return;
12770
+ if (!atlas) {
12771
+ if (atlasFailed(node.asset.atlas)) paintFallbackSquare(painter, node, dctx, "atlas-failed");
12772
+ return;
12773
+ }
12812
12774
  const r2 = subRectFor(atlas, node.asset.sprite);
12813
- if (!r2) return;
12775
+ if (!r2) {
12776
+ paintFallbackSquare(painter, node, dctx, "sprite-missing");
12777
+ return;
12778
+ }
12814
12779
  src = { x: r2.sx, y: r2.sy, w: r2.sw, h: r2.sh };
12815
12780
  }
12816
12781
  const tw = dctx.projector.tileWidth;
12817
- const w = node.width !== void 0 ? node.width * tw : src ? src.w : tex.width;
12818
- const h = node.height !== void 0 ? node.height * tw : src ? src.h : tex.height;
12782
+ const fallbackW = dctx.projector.worldPixelDirect ? src ? src.w : tex.width : tw;
12783
+ const fallbackH = dctx.projector.worldPixelDirect ? src ? src.h : tex.height : tw;
12784
+ const w = node.width !== void 0 ? node.width * tw : fallbackW;
12785
+ const h = node.height !== void 0 ? node.height * tw : fallbackH;
12819
12786
  const anchor = node.anchor ?? "top-left";
12820
12787
  const p = dctx.projector.anchorPoint(node.position, anchor);
12821
12788
  const dx = anchor === "top-left" ? p.x : p.x - w / 2;
@@ -12892,6 +12859,16 @@ var init_DrawShape = __esm({
12892
12859
  if (node.stroke) painter.strokePoly(pts, node.stroke, node.strokeWidth ?? 1, true);
12893
12860
  break;
12894
12861
  }
12862
+ case "path": {
12863
+ if (!node.d) break;
12864
+ const base = dctx.projector.project(node.position);
12865
+ const tw = dctx.projector.tileWidth;
12866
+ painter.translate(base.x, base.y);
12867
+ painter.scale(tw, tw);
12868
+ if (node.fill) painter.fillPath(node.d, node.fill);
12869
+ if (node.stroke) painter.strokePath(node.d, node.stroke, (node.strokeWidth ?? 1) / tw);
12870
+ break;
12871
+ }
12895
12872
  }
12896
12873
  painter.restore();
12897
12874
  };
@@ -12983,6 +12960,19 @@ function paintDrawable(painter, node, dctx) {
12983
12960
  case "draw-text":
12984
12961
  paintText(painter, node, dctx);
12985
12962
  break;
12963
+ case "draw-group": {
12964
+ if (!isValidScenePos(node.position)) break;
12965
+ if (!Array.isArray(node.items)) break;
12966
+ const p = dctx.projector.project(node.position);
12967
+ painter.save();
12968
+ painter.translate(p.x, p.y);
12969
+ if (node.scale !== void 0) painter.scale(node.scale, node.scale);
12970
+ if (node.rotate !== void 0) painter.rotate(node.rotate);
12971
+ if (node.opacity !== void 0 && node.opacity !== 1) painter.setAlpha(node.opacity);
12972
+ for (const item of node.items) paintDrawable(painter, item, dctx);
12973
+ painter.restore();
12974
+ break;
12975
+ }
12986
12976
  case "draw-sprite-layer":
12987
12977
  paintSpriteLayer(painter, node, dctx);
12988
12978
  break;
@@ -12996,6 +12986,7 @@ function paintDrawable(painter, node, dctx) {
12996
12986
  }
12997
12987
  var init_paintDispatch = __esm({
12998
12988
  "lib/drawable/paintDispatch.ts"() {
12989
+ init_contract();
12999
12990
  init_DrawSprite();
13000
12991
  init_DrawShape();
13001
12992
  init_DrawText();
@@ -13013,6 +13004,7 @@ function collectDrawnItems(nodes) {
13013
13004
  case "draw-sprite":
13014
13005
  case "draw-shape":
13015
13006
  case "draw-text":
13007
+ case "draw-group":
13016
13008
  if (isValidScenePos(n.position)) out.push({ pos: n.position, id: n.id });
13017
13009
  break;
13018
13010
  case "draw-sprite-layer":
@@ -13056,6 +13048,8 @@ function Canvas2D({
13056
13048
  keyUpMap,
13057
13049
  camera = "pan-zoom",
13058
13050
  scale = 0.4,
13051
+ tileWidth,
13052
+ fit = false,
13059
13053
  showMinimap = true,
13060
13054
  followTarget,
13061
13055
  cameraPos,
@@ -13098,9 +13092,32 @@ function Canvas2D({
13098
13092
  observer2.observe(el);
13099
13093
  return () => observer2.disconnect();
13100
13094
  }, []);
13101
- const scaledTileWidth = TILE_WIDTH * scale;
13102
- const scaledFloorHeight = FLOOR_HEIGHT * scale;
13103
- const scaledDiamondTopY = DIAMOND_TOP_Y * scale;
13095
+ const [atlasVersion, setAtlasVersion] = useState(0);
13096
+ const bumpAtlas = useCallback(() => setAtlasVersion((v) => v + 1), []);
13097
+ const detectedTileWidth = useMemo(() => {
13098
+ for (const n of drawables ?? []) {
13099
+ const refs = [];
13100
+ if (n.type === "draw-sprite") refs.push(n.asset);
13101
+ else if (n.type === "draw-sprite-layer") for (const it of n.items) refs.push(it.asset);
13102
+ for (const a of refs) {
13103
+ if (a && isAtlasAsset(a) && a.atlas) {
13104
+ const atlas = getAtlas(a.atlas, bumpAtlas);
13105
+ if (atlas) {
13106
+ if ("tileWidth" in atlas) return atlas.tileWidth;
13107
+ if ("subTextures" in atlas) {
13108
+ const first = Object.values(atlas.subTextures)[0];
13109
+ if (first && typeof first.width === "number") return first.width;
13110
+ }
13111
+ }
13112
+ }
13113
+ }
13114
+ }
13115
+ return void 0;
13116
+ }, [drawables, atlasVersion]);
13117
+ const nativeTileW = tileWidth ?? detectedTileWidth ?? TILE_WIDTH;
13118
+ const scaledTileWidth = nativeTileW;
13119
+ const scaledFloorHeight = nativeTileW / 2;
13120
+ const scaledDiamondTopY = nativeTileW * (DIAMOND_TOP_Y / TILE_WIDTH);
13104
13121
  const drawnItems = useMemo(() => collectDrawnItems(drawables ?? []), [drawables]);
13105
13122
  const scenePositions = useMemo(() => drawnItems.map((i) => i.pos), [drawnItems]);
13106
13123
  const hitIndex = useMemo(() => buildHitIndex(drawnItems), [drawnItems]);
@@ -13118,14 +13135,36 @@ function Canvas2D({
13118
13135
  if (isFree || projection === "flat" || projection === "side") return 0;
13119
13136
  return (gridExtent.height - 1) * (scaledTileWidth / 2);
13120
13137
  }, [isFree, projection, gridExtent.height, scaledTileWidth]);
13138
+ const effectiveZoom = useMemo(() => {
13139
+ if (isFree || projection === "side") return scale;
13140
+ if (!fit) {
13141
+ const z2 = TILE_WIDTH * scale * scale / nativeTileW;
13142
+ return Number.isFinite(z2) && z2 > 0 ? z2 : scale;
13143
+ }
13144
+ if (!viewportSize.width || gridExtent.width < 2 || gridExtent.height < 2) return scale;
13145
+ let boardW;
13146
+ let boardH;
13147
+ if (projection === "flat") {
13148
+ boardW = gridExtent.width * nativeTileW;
13149
+ boardH = gridExtent.height * nativeTileW;
13150
+ } else if (projection === "hex") {
13151
+ boardW = (gridExtent.width + 0.5) * nativeTileW;
13152
+ boardH = gridExtent.height * (nativeTileW / 2) * 0.75 + nativeTileW / 2;
13153
+ } else {
13154
+ boardW = (gridExtent.width + gridExtent.height) * (nativeTileW / 2);
13155
+ boardH = (gridExtent.width + gridExtent.height) * (nativeTileW / 4);
13156
+ }
13157
+ const z = Math.min(viewportSize.width * 0.85 / boardW, viewportSize.height * 0.85 / boardH);
13158
+ return Number.isFinite(z) && z > 0 ? z : scale;
13159
+ }, [isFree, projection, fit, viewportSize, gridExtent, nativeTileW, scale]);
13121
13160
  const projector = useMemo(
13122
- () => create2DProjector({ scale, baseOffsetX, layout }),
13123
- [scale, baseOffsetX, layout]
13161
+ () => create2DProjector({ tileWidth: nativeTileW, baseOffsetX, layout }),
13162
+ [nativeTileW, baseOffsetX, layout]
13124
13163
  );
13125
13164
  const unproject = useCallback((screenX, screenY) => {
13126
13165
  if (projection === "free" || projection === "side") return { x: Math.round(screenX), y: Math.round(screenY) };
13127
- return screenToIso(screenX, screenY, scale, baseOffsetX, projection);
13128
- }, [projection, scale, baseOffsetX]);
13166
+ return screenToIso(screenX, screenY, nativeTileW, baseOffsetX, projection);
13167
+ }, [projection, nativeTileW, baseOffsetX]);
13129
13168
  const bgUrls = useMemo(() => backgroundImage ? [backgroundImage.url] : [], [backgroundImage]);
13130
13169
  const { getImage, pendingCount: _imagePendingCount } = useImageCache(bgUrls);
13131
13170
  useEffect(() => {
@@ -13152,9 +13191,7 @@ function Canvas2D({
13152
13191
  zoomAtPoint,
13153
13192
  screenToWorld,
13154
13193
  lerpToTarget
13155
- } = useCamera({ zoom: scale });
13156
- const [atlasVersion, setAtlasVersion] = useState(0);
13157
- const bumpAtlas = useCallback(() => setAtlasVersion((v) => v + 1), []);
13194
+ } = useCamera({ zoom: effectiveZoom });
13158
13195
  const miniMapTiles = useMemo(() => {
13159
13196
  if (!showMinimap) return [];
13160
13197
  const color = MINIMAP_TERRAIN_COLORS.default;
@@ -13229,6 +13266,13 @@ function Canvas2D({
13229
13266
  useEffect(() => {
13230
13267
  draw();
13231
13268
  }, [_imagePendingCount, draw]);
13269
+ const userZoomedRef = useRef(false);
13270
+ useEffect(() => {
13271
+ if (userZoomedRef.current) return;
13272
+ if (cameraRef.current.zoom === effectiveZoom) return;
13273
+ cameraRef.current.zoom = effectiveZoom;
13274
+ draw();
13275
+ }, [effectiveZoom, cameraRef, draw]);
13232
13276
  useEffect(() => {
13233
13277
  draw();
13234
13278
  }, [atlasVersion, draw]);
@@ -13285,7 +13329,9 @@ function Canvas2D({
13285
13329
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
13286
13330
  }, [handleMouseLeave, tileLeaveEvent, eventBus]);
13287
13331
  const applyZoom = useCallback((factor, centerX, centerY) => {
13288
- if (enableCamera) zoomAtPoint(factor, centerX, centerY, viewportSize, () => draw());
13332
+ if (!enableCamera) return;
13333
+ userZoomedRef.current = true;
13334
+ zoomAtPoint(factor, centerX, centerY, viewportSize, () => draw());
13289
13335
  }, [enableCamera, zoomAtPoint, viewportSize, draw]);
13290
13336
  const applyPanDelta = useCallback((dx, dy) => {
13291
13337
  if (enableCamera) panBy(dx, dy, () => draw());
@@ -13470,6 +13516,8 @@ function Canvas({
13470
13516
  isLoading,
13471
13517
  unitScale,
13472
13518
  showMinimap,
13519
+ fit,
13520
+ tileWidth,
13473
13521
  backgroundImage,
13474
13522
  backgroundColor,
13475
13523
  worldWidth,
@@ -13527,6 +13575,8 @@ function Canvas({
13527
13575
  projection,
13528
13576
  camera: to2DCamera(camera?.mode),
13529
13577
  ...zoom !== void 0 ? { scale: zoom } : {},
13578
+ ...fit !== void 0 ? { fit } : {},
13579
+ ...tileWidth !== void 0 ? { tileWidth } : {},
13530
13580
  ...camera?.target !== void 0 ? { followTarget: camera.target } : {},
13531
13581
  ...camera?.pos !== void 0 ? { cameraPos: camera.pos } : {},
13532
13582
  showMinimap,
@@ -13553,32 +13603,6 @@ var init_Canvas = __esm({
13553
13603
  Canvas.displayName = "Canvas";
13554
13604
  }
13555
13605
  });
13556
- function SvgDrawShapeLayer({
13557
- items,
13558
- fill,
13559
- stroke,
13560
- strokeWidth,
13561
- opacity
13562
- }) {
13563
- return /* @__PURE__ */ jsx("g", { children: items.map(({ id, ...shape }) => /* @__PURE__ */ jsx(
13564
- SvgDrawShape,
13565
- {
13566
- ...shape,
13567
- fill: shape.fill ?? fill,
13568
- stroke: shape.stroke ?? stroke,
13569
- strokeWidth: shape.strokeWidth ?? strokeWidth,
13570
- opacity: shape.opacity ?? opacity
13571
- },
13572
- id
13573
- )) });
13574
- }
13575
- var init_SvgDrawShapeLayer = __esm({
13576
- "components/game/molecules/SvgDrawShapeLayer.tsx"() {
13577
- "use client";
13578
- init_SvgDrawShape();
13579
- SvgDrawShapeLayer.displayName = "SvgDrawShapeLayer";
13580
- }
13581
- });
13582
13606
  function GameAudioToggle({
13583
13607
  size = "sm",
13584
13608
  className,
@@ -26068,6 +26092,8 @@ function DataList({
26068
26092
  hasMore,
26069
26093
  children,
26070
26094
  pageSize = 5,
26095
+ sortBy,
26096
+ sortDirection,
26071
26097
  renderItem: schemaRenderItem,
26072
26098
  dragGroup,
26073
26099
  accepts,
@@ -26096,7 +26122,11 @@ function DataList({
26096
26122
  dndItemIdField,
26097
26123
  dndRoot
26098
26124
  });
26099
- const allData = dnd.orderedItems;
26125
+ const orderedData = dnd.orderedItems;
26126
+ const allData = React92__default.useMemo(
26127
+ () => sortRows(orderedData, sortBy, sortDirection),
26128
+ [orderedData, sortBy, sortDirection]
26129
+ );
26100
26130
  const data = pageSize > 0 ? allData.slice(0, visibleCount) : allData;
26101
26131
  const hasMoreLocal = pageSize > 0 && visibleCount < allData.length;
26102
26132
  const hasRenderProp = typeof children === "function";
@@ -26133,7 +26163,7 @@ function DataList({
26133
26163
  };
26134
26164
  eventBus.emit(`UI:${action.event}`, payload);
26135
26165
  };
26136
- const renderItemActions = (itemData) => {
26166
+ const renderItemActions = (itemData, onPrimary = false) => {
26137
26167
  if (!itemActions || itemActions.length === 0) return null;
26138
26168
  const inline = maxInlineActions != null ? itemActions.slice(0, maxInlineActions) : itemActions;
26139
26169
  const overflow = maxInlineActions != null ? itemActions.slice(maxInlineActions) : [];
@@ -26146,7 +26176,12 @@ function DataList({
26146
26176
  onClick: handleActionClick(action, itemData),
26147
26177
  "data-testid": `action-${action.event}`,
26148
26178
  "data-row-id": String(itemData.id),
26149
- className: cn(action.variant === "danger" && "text-error hover:bg-error/10"),
26179
+ className: cn(
26180
+ action.variant === "danger" && "text-error hover:bg-error/10",
26181
+ // Must sit on the Button itself: the variant's own text colour
26182
+ // beats an inherited one from the row wrapper.
26183
+ onPrimary && "!text-primary-foreground hover:bg-primary-foreground/15"
26184
+ ),
26150
26185
  children: [
26151
26186
  action.icon && renderIconInput2(action.icon, { size: "xs", className: "mr-1" }),
26152
26187
  action.label
@@ -26246,7 +26281,11 @@ function DataList({
26246
26281
  metaFields.length > 0 && /* @__PURE__ */ jsx(HStack, { gap: "xs", className: "mt-1 flex-wrap", children: metaFields.map((f3) => {
26247
26282
  const v = getNestedValue(itemData, f3.name);
26248
26283
  if (v === void 0 || v === null || v === "") return null;
26249
- return f3.variant === "badge" ? /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(String(v)), children: String(v) }, f3.name) : /* @__PURE__ */ jsx(
26284
+ return f3.variant === "badge" ? (
26285
+ // `format` applies here too — a boolean field badged
26286
+ // without it renders the raw "false" instead of "No".
26287
+ /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(String(v)), children: formatValue2(v, f3.format) }, f3.name)
26288
+ ) : /* @__PURE__ */ jsx(
26250
26289
  Typography,
26251
26290
  {
26252
26291
  variant: "caption",
@@ -26265,7 +26304,7 @@ function DataList({
26265
26304
  children: formatDate(timestamp)
26266
26305
  }
26267
26306
  ) : /* @__PURE__ */ jsx("span", {}),
26268
- renderItemActions(itemData)
26307
+ renderItemActions(itemData, isSent)
26269
26308
  ] })
26270
26309
  ]
26271
26310
  }
@@ -26358,7 +26397,7 @@ function DataList({
26358
26397
  if (val === void 0 || val === null) return null;
26359
26398
  return /* @__PURE__ */ jsxs(HStack, { gap: "xs", className: "items-center flex-shrink-0", children: [
26360
26399
  field.icon && renderIconInput2(field.icon, { size: "xs" }),
26361
- /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(String(val)), children: String(val) })
26400
+ /* @__PURE__ */ jsx(Badge, { variant: statusVariant3(String(val)), children: formatValue2(val, field.format) })
26362
26401
  ] }, field.name);
26363
26402
  })
26364
26403
  ] }),
@@ -26597,6 +26636,182 @@ var init_FormSection = __esm({
26597
26636
  FormActions.displayName = "FormActions";
26598
26637
  }
26599
26638
  });
26639
+ var ALL_CATEGORY, MAX_RENDERED, GridPicker;
26640
+ var init_GridPicker = __esm({
26641
+ "components/core/molecules/GridPicker.tsx"() {
26642
+ "use client";
26643
+ init_cn();
26644
+ init_Input();
26645
+ init_Badge();
26646
+ init_Stack();
26647
+ ALL_CATEGORY = "__all__";
26648
+ MAX_RENDERED = 300;
26649
+ GridPicker = ({
26650
+ items,
26651
+ value,
26652
+ onChange,
26653
+ categories,
26654
+ searchPlaceholder,
26655
+ renderThumbnail,
26656
+ cellSize = 32,
26657
+ className
26658
+ }) => {
26659
+ const [search, setSearch] = useState("");
26660
+ const [activeCategory, setActiveCategory] = useState(ALL_CATEGORY);
26661
+ const gridRef = useRef(null);
26662
+ const categoryChips = useMemo(() => {
26663
+ if (categories !== void 0) return categories;
26664
+ const seen = [];
26665
+ for (const item of items) {
26666
+ if (!seen.includes(item.category)) seen.push(item.category);
26667
+ }
26668
+ return seen;
26669
+ }, [categories, items]);
26670
+ const filtered = useMemo(() => {
26671
+ const needle = search.trim().toLowerCase();
26672
+ return items.filter((item) => {
26673
+ const matchesCategory = activeCategory === ALL_CATEGORY || item.category === activeCategory;
26674
+ const matchesSearch = needle === "" || item.label.toLowerCase().includes(needle) || item.keywords !== void 0 && item.keywords.some((k) => k.toLowerCase().includes(needle));
26675
+ return matchesCategory && matchesSearch;
26676
+ });
26677
+ }, [items, search, activeCategory]);
26678
+ const visible = useMemo(() => filtered.slice(0, MAX_RENDERED), [filtered]);
26679
+ const truncated = filtered.length - visible.length;
26680
+ const select = useCallback(
26681
+ (item) => {
26682
+ onChange(item.id);
26683
+ },
26684
+ [onChange]
26685
+ );
26686
+ const handleKeyDown = useCallback(
26687
+ (e, index) => {
26688
+ const cells = gridRef.current?.querySelectorAll(
26689
+ "[data-gridpicker-cell]"
26690
+ );
26691
+ if (cells === void 0 || cells.length === 0) return;
26692
+ const columns = (() => {
26693
+ const grid = gridRef.current;
26694
+ if (grid === null) return 1;
26695
+ const style = window.getComputedStyle(grid);
26696
+ const cols = style.gridTemplateColumns.split(" ").filter(Boolean).length;
26697
+ return cols > 0 ? cols : 1;
26698
+ })();
26699
+ let next = -1;
26700
+ if (e.key === "ArrowRight") next = index + 1;
26701
+ else if (e.key === "ArrowLeft") next = index - 1;
26702
+ else if (e.key === "ArrowDown") next = index + columns;
26703
+ else if (e.key === "ArrowUp") next = index - columns;
26704
+ else if (e.key === "Enter" || e.key === " ") {
26705
+ e.preventDefault();
26706
+ select(filtered[index]);
26707
+ return;
26708
+ } else {
26709
+ return;
26710
+ }
26711
+ e.preventDefault();
26712
+ if (next >= 0 && next < cells.length) {
26713
+ cells[next].focus();
26714
+ }
26715
+ },
26716
+ [filtered, select]
26717
+ );
26718
+ return /* @__PURE__ */ jsxs(VStack, { gap: "sm", className: cn("w-full", className), children: [
26719
+ /* @__PURE__ */ jsx(
26720
+ Input,
26721
+ {
26722
+ type: "search",
26723
+ icon: "search",
26724
+ value: search,
26725
+ placeholder: searchPlaceholder,
26726
+ clearable: true,
26727
+ onClear: () => setSearch(""),
26728
+ onChange: (e) => setSearch(e.target.value)
26729
+ }
26730
+ ),
26731
+ categoryChips.length > 0 && /* @__PURE__ */ jsxs(HStack, { gap: "xs", wrap: true, children: [
26732
+ /* @__PURE__ */ jsx(
26733
+ Badge,
26734
+ {
26735
+ variant: activeCategory === ALL_CATEGORY ? "primary" : "neutral",
26736
+ size: "sm",
26737
+ role: "button",
26738
+ tabIndex: 0,
26739
+ "aria-pressed": activeCategory === ALL_CATEGORY,
26740
+ className: "cursor-pointer",
26741
+ onClick: () => setActiveCategory(ALL_CATEGORY),
26742
+ onKeyDown: (e) => {
26743
+ if (e.key === "Enter" || e.key === " ") {
26744
+ e.preventDefault();
26745
+ setActiveCategory(ALL_CATEGORY);
26746
+ }
26747
+ },
26748
+ children: "All"
26749
+ }
26750
+ ),
26751
+ categoryChips.map((category) => /* @__PURE__ */ jsx(
26752
+ Badge,
26753
+ {
26754
+ variant: activeCategory === category ? "primary" : "neutral",
26755
+ size: "sm",
26756
+ role: "button",
26757
+ tabIndex: 0,
26758
+ "aria-pressed": activeCategory === category,
26759
+ className: "cursor-pointer",
26760
+ onClick: () => setActiveCategory(category),
26761
+ onKeyDown: (e) => {
26762
+ if (e.key === "Enter" || e.key === " ") {
26763
+ e.preventDefault();
26764
+ setActiveCategory(category);
26765
+ }
26766
+ },
26767
+ children: category
26768
+ },
26769
+ category
26770
+ ))
26771
+ ] }),
26772
+ /* @__PURE__ */ jsx(
26773
+ "div",
26774
+ {
26775
+ ref: gridRef,
26776
+ role: "listbox",
26777
+ className: "grid gap-1 overflow-y-auto max-h-64 p-1",
26778
+ style: {
26779
+ gridTemplateColumns: `repeat(auto-fill, minmax(${cellSize}px, 1fr))`
26780
+ },
26781
+ children: visible.map((item, index) => {
26782
+ const selected = item.id === value;
26783
+ return /* @__PURE__ */ jsx(
26784
+ "button",
26785
+ {
26786
+ type: "button",
26787
+ role: "option",
26788
+ "aria-selected": selected,
26789
+ "aria-label": item.label,
26790
+ title: item.label,
26791
+ "data-gridpicker-cell": true,
26792
+ tabIndex: selected || value === void 0 && index === 0 ? 0 : -1,
26793
+ onClick: () => select(item),
26794
+ onKeyDown: (e) => handleKeyDown(e, index),
26795
+ className: cn(
26796
+ "flex items-center justify-center rounded-sm",
26797
+ "transition-colors hover:bg-muted",
26798
+ "focus:outline-none focus:ring-1 focus:ring-ring",
26799
+ selected && "bg-primary/10 ring-1 ring-primary"
26800
+ ),
26801
+ style: { width: cellSize, height: cellSize },
26802
+ children: renderThumbnail(item)
26803
+ },
26804
+ item.id
26805
+ );
26806
+ })
26807
+ }
26808
+ ),
26809
+ truncated > 0 && /* @__PURE__ */ jsx("div", { className: "px-1 text-xs text-muted-foreground", children: `+${truncated} more \u2014 refine your search` })
26810
+ ] });
26811
+ };
26812
+ GridPicker.displayName = "GridPicker";
26813
+ }
26814
+ });
26600
26815
  function fileIcon(name) {
26601
26816
  const ext = name.split(".").pop()?.toLowerCase() ?? "";
26602
26817
  switch (ext) {
@@ -27625,9 +27840,16 @@ var init_Popover = __esm({
27625
27840
  position = "bottom",
27626
27841
  trigger = "click",
27627
27842
  showArrow = true,
27843
+ open,
27844
+ onOpenChange,
27628
27845
  className
27629
27846
  }) => {
27630
- const [isOpen, setIsOpen] = useState(false);
27847
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
27848
+ const isOpen = open !== void 0 ? open : uncontrolledOpen;
27849
+ const setIsOpen = (next) => {
27850
+ if (open === void 0) setUncontrolledOpen(next);
27851
+ onOpenChange?.(next);
27852
+ };
27631
27853
  const [triggerRect, setTriggerRect] = useState(null);
27632
27854
  const [popoverWidth, setPopoverWidth] = useState(0);
27633
27855
  const triggerRef = useRef(null);
@@ -27660,6 +27882,23 @@ var init_Popover = __esm({
27660
27882
  updatePosition();
27661
27883
  }
27662
27884
  }, [isOpen]);
27885
+ useEffect(() => {
27886
+ if (!isOpen) return;
27887
+ let raf = 0;
27888
+ let lastTop = Number.NaN;
27889
+ let lastLeft = Number.NaN;
27890
+ const track = () => {
27891
+ const rect = triggerRef.current?.getBoundingClientRect();
27892
+ if (rect && (rect.top !== lastTop || rect.left !== lastLeft)) {
27893
+ lastTop = rect.top;
27894
+ lastLeft = rect.left;
27895
+ updatePosition();
27896
+ }
27897
+ raf = requestAnimationFrame(track);
27898
+ };
27899
+ raf = requestAnimationFrame(track);
27900
+ return () => cancelAnimationFrame(raf);
27901
+ }, [isOpen]);
27663
27902
  useEffect(() => {
27664
27903
  if (!mounted) setPopoverWidth(0);
27665
27904
  }, [mounted]);
@@ -29008,6 +29247,80 @@ var init_FlipCard = __esm({
29008
29247
  FlipCard.displayName = "FlipCard";
29009
29248
  }
29010
29249
  });
29250
+ var EMOJI_ITEMS, EmojiPicker;
29251
+ var init_EmojiPicker = __esm({
29252
+ "components/core/molecules/EmojiPicker.tsx"() {
29253
+ "use client";
29254
+ init_useEventBus();
29255
+ init_Button();
29256
+ init_GridPicker();
29257
+ init_Popover();
29258
+ EMOJI_ITEMS = (() => {
29259
+ const items = [];
29260
+ for (const name of ordered) {
29261
+ const entry = lib[name];
29262
+ if (entry === void 0 || entry.char === null || entry.char === "") continue;
29263
+ items.push({
29264
+ id: entry.char,
29265
+ label: name.replace(/_/g, " "),
29266
+ category: entry.category.replace(/_/g, " "),
29267
+ keywords: entry.keywords
29268
+ });
29269
+ }
29270
+ return items;
29271
+ })();
29272
+ EmojiPicker = ({
29273
+ pickEvent,
29274
+ position = "top",
29275
+ triggerIcon = "smile",
29276
+ triggerLabel = "Add emoji",
29277
+ className
29278
+ }) => {
29279
+ const eventBus = useEventBus();
29280
+ const [open, setOpen] = useState(false);
29281
+ const handlePick = (glyph) => {
29282
+ if (pickEvent !== void 0) {
29283
+ const payload = { emoji: glyph };
29284
+ eventBus.emit(`UI:${pickEvent}`, payload);
29285
+ }
29286
+ setOpen(false);
29287
+ };
29288
+ return /* @__PURE__ */ jsx(
29289
+ Popover,
29290
+ {
29291
+ position,
29292
+ trigger: "click",
29293
+ showArrow: false,
29294
+ open,
29295
+ onOpenChange: setOpen,
29296
+ content: /* @__PURE__ */ jsx(
29297
+ GridPicker,
29298
+ {
29299
+ items: EMOJI_ITEMS,
29300
+ onChange: handlePick,
29301
+ searchPlaceholder: "Search emoji\u2026",
29302
+ renderThumbnail: (item) => /* @__PURE__ */ jsx("span", { className: "text-xl leading-none", "aria-hidden": "true", children: item.id }),
29303
+ cellSize: 32,
29304
+ className: "w-80"
29305
+ }
29306
+ ),
29307
+ children: /* @__PURE__ */ jsx(
29308
+ Button,
29309
+ {
29310
+ variant: "ghost",
29311
+ icon: triggerIcon,
29312
+ "aria-label": triggerLabel,
29313
+ title: triggerLabel,
29314
+ className,
29315
+ "data-testid": "emoji-picker-trigger"
29316
+ }
29317
+ )
29318
+ }
29319
+ );
29320
+ };
29321
+ EmojiPicker.displayName = "EmojiPicker";
29322
+ }
29323
+ });
29011
29324
  function toISODate(d) {
29012
29325
  return d.toISOString().slice(0, 10);
29013
29326
  }
@@ -29960,13 +30273,13 @@ var init_MapView = __esm({
29960
30273
  shadowSize: [41, 41]
29961
30274
  });
29962
30275
  L.Marker.prototype.options.icon = defaultIcon;
29963
- const { useEffect: useEffect64, useRef: useRef63, useCallback: useCallback95, useState: useState97 } = React92__default;
30276
+ const { useEffect: useEffect63, useRef: useRef63, useCallback: useCallback95, useState: useState99 } = React92__default;
29964
30277
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
29965
30278
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
29966
30279
  function MapUpdater({ centerLat, centerLng, zoom }) {
29967
30280
  const map = useMap();
29968
30281
  const prevRef = useRef63({ centerLat, centerLng, zoom });
29969
- useEffect64(() => {
30282
+ useEffect63(() => {
29970
30283
  const prev = prevRef.current;
29971
30284
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
29972
30285
  map.setView([centerLat, centerLng], zoom);
@@ -29977,7 +30290,7 @@ var init_MapView = __esm({
29977
30290
  }
29978
30291
  function MapClickHandler({ onMapClick }) {
29979
30292
  const map = useMap();
29980
- useEffect64(() => {
30293
+ useEffect63(() => {
29981
30294
  if (!onMapClick) return;
29982
30295
  const handler = (e) => {
29983
30296
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -30005,7 +30318,7 @@ var init_MapView = __esm({
30005
30318
  showAttribution = true
30006
30319
  }) {
30007
30320
  const eventBus = useEventBus2();
30008
- const [clickedPosition, setClickedPosition] = useState97(null);
30321
+ const [clickedPosition, setClickedPosition] = useState99(null);
30009
30322
  const handleMapClick = useCallback95((lat, lng) => {
30010
30323
  if (showClickedPin) {
30011
30324
  setClickedPosition({ lat, lng });
@@ -30850,6 +31163,7 @@ function TableView({
30850
31163
  fields,
30851
31164
  itemActions,
30852
31165
  maxInlineActions,
31166
+ itemClickEvent,
30853
31167
  selectable = false,
30854
31168
  selectEvent,
30855
31169
  selectedIds,
@@ -30894,9 +31208,9 @@ function TableView({
30894
31208
  dndItemIdField,
30895
31209
  dndRoot
30896
31210
  });
30897
- const ordered = dnd.orderedItems;
30898
- const data = pageSize > 0 ? ordered.slice(0, visibleCount) : ordered;
30899
- const hasMore = pageSize > 0 && visibleCount < ordered.length;
31211
+ const ordered2 = dnd.orderedItems;
31212
+ const data = pageSize > 0 ? ordered2.slice(0, visibleCount) : ordered2;
31213
+ const hasMore = pageSize > 0 && visibleCount < ordered2.length;
30900
31214
  const hasRenderProp = typeof children === "function";
30901
31215
  const idField = dndItemIdField ?? "id";
30902
31216
  const isCoarsePointer = useMediaQuery("(pointer: coarse)");
@@ -30949,6 +31263,14 @@ function TableView({
30949
31263
  };
30950
31264
  eventBus.emit(`UI:${action.event}`, payload);
30951
31265
  };
31266
+ const handleRowClick = (row) => () => {
31267
+ if (!itemClickEvent) return;
31268
+ const payload = {
31269
+ id: row.id,
31270
+ row
31271
+ };
31272
+ eventBus.emit(`UI:${itemClickEvent}`, payload);
31273
+ };
30952
31274
  const colFloors = React92__default.useMemo(
30953
31275
  () => colDefs.map((col) => {
30954
31276
  const longest = data.reduce((widest, row) => {
@@ -31025,10 +31347,12 @@ function TableView({
31025
31347
  role: "row",
31026
31348
  "data-entity-row": true,
31027
31349
  "data-entity-id": id,
31350
+ onClick: itemClickEvent ? handleRowClick(row) : void 0,
31028
31351
  style: !hasRenderProp ? { gridTemplateColumns } : void 0,
31029
31352
  className: cn(
31030
31353
  "group items-center gap-3 transition-colors duration-fast",
31031
31354
  hasRenderProp ? "flex" : "grid",
31355
+ itemClickEvent && "cursor-pointer",
31032
31356
  lk.rowPad,
31033
31357
  lk.divider && "border-b border-[var(--color-border)]",
31034
31358
  lk.striped && index % 2 === 1 && "bg-[var(--color-surface-subtle)]",
@@ -31036,7 +31360,7 @@ function TableView({
31036
31360
  look === "bordered" && "[&>*]:border-r [&>*]:border-[var(--color-border)] [&>*:last-child]:border-r-0"
31037
31361
  ),
31038
31362
  children: [
31039
- selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", children: /* @__PURE__ */ jsx(
31363
+ selectable && /* @__PURE__ */ jsx(Box, { className: "flex items-center", onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0, children: /* @__PURE__ */ jsx(
31040
31364
  Checkbox,
31041
31365
  {
31042
31366
  checked: selected.has(id),
@@ -31061,6 +31385,7 @@ function TableView({
31061
31385
  HStack,
31062
31386
  {
31063
31387
  gap: "xs",
31388
+ onClick: itemClickEvent ? (e) => e.stopPropagation() : void 0,
31064
31389
  className: cn(
31065
31390
  // Pinned: the fixed column tracks routinely overflow the caller's
31066
31391
  // scroll container, which used to leave the actions off-screen.
@@ -31130,7 +31455,7 @@ function TableView({
31130
31455
  /* @__PURE__ */ jsx(Icon, { name: "chevron-down", size: "xs", className: "mr-1" }),
31131
31456
  t("common.showMore"),
31132
31457
  " (",
31133
- t("common.remaining", { count: ordered.length - visibleCount }),
31458
+ t("common.remaining", { count: ordered2.length - visibleCount }),
31134
31459
  ")"
31135
31460
  ] }) })
31136
31461
  ]
@@ -40160,6 +40485,16 @@ var init_DetailPanel = __esm({
40160
40485
  DetailPanel.displayName = "DetailPanel";
40161
40486
  }
40162
40487
  });
40488
+
40489
+ // components/game/atoms/DrawGroup.tsx
40490
+ function DrawGroup(_props) {
40491
+ return null;
40492
+ }
40493
+ var init_DrawGroup = __esm({
40494
+ "components/game/atoms/DrawGroup.tsx"() {
40495
+ "use client";
40496
+ }
40497
+ });
40163
40498
  function extractTitle(children) {
40164
40499
  if (!React92__default.isValidElement(children)) return void 0;
40165
40500
  const props = children.props;
@@ -45882,6 +46217,7 @@ var init_component_registry_generated = __esm({
45882
46217
  init_DocSidebar();
45883
46218
  init_DocTOC();
45884
46219
  init_DocumentViewer();
46220
+ init_DrawGroup();
45885
46221
  init_DrawShape();
45886
46222
  init_DrawShapeLayer();
45887
46223
  init_DrawSprite();
@@ -45891,6 +46227,7 @@ var init_component_registry_generated = __esm({
45891
46227
  init_Drawer();
45892
46228
  init_DrawerSlot();
45893
46229
  init_EdgeDecoration();
46230
+ init_EmojiPicker();
45894
46231
  init_EmptyState();
45895
46232
  init_ErrorBoundary();
45896
46233
  init_ErrorState();
@@ -46025,10 +46362,6 @@ var init_component_registry_generated = __esm({
46025
46362
  init_SubagentTracePanel();
46026
46363
  init_SvgBranch();
46027
46364
  init_SvgConnection();
46028
- init_SvgDrawGroup();
46029
- init_SvgDrawShape();
46030
- init_SvgDrawShapeLayer();
46031
- init_SvgDrawText();
46032
46365
  init_SvgFlow();
46033
46366
  init_SvgGrid();
46034
46367
  init_SvgLobe();
@@ -46039,7 +46372,6 @@ var init_component_registry_generated = __esm({
46039
46372
  init_SvgRing();
46040
46373
  init_SvgShield();
46041
46374
  init_SvgStack();
46042
- init_SvgStage();
46043
46375
  init_SwipeableRow();
46044
46376
  init_Switch();
46045
46377
  init_TabbedContainer();
@@ -46153,6 +46485,7 @@ var init_component_registry_generated = __esm({
46153
46485
  "DocSidebar": DocSidebar,
46154
46486
  "DocTOC": DocTOC,
46155
46487
  "DocumentViewer": DocumentViewer,
46488
+ "DrawGroup": DrawGroup,
46156
46489
  "DrawShape": DrawShape,
46157
46490
  "DrawShapeLayer": DrawShapeLayer,
46158
46491
  "DrawSprite": DrawSprite,
@@ -46162,6 +46495,7 @@ var init_component_registry_generated = __esm({
46162
46495
  "Drawer": Drawer,
46163
46496
  "DrawerSlot": DrawerSlot,
46164
46497
  "EdgeDecoration": EdgeDecoration,
46498
+ "EmojiPicker": EmojiPicker,
46165
46499
  "EmptyState": EmptyState,
46166
46500
  "ErrorBoundary": ErrorBoundary,
46167
46501
  "ErrorState": ErrorState,
@@ -46301,10 +46635,6 @@ var init_component_registry_generated = __esm({
46301
46635
  "SubagentTracePanel": SubagentTracePanel,
46302
46636
  "SvgBranch": SvgBranch,
46303
46637
  "SvgConnection": SvgConnection,
46304
- "SvgDrawGroup": SvgDrawGroup,
46305
- "SvgDrawShape": SvgDrawShape,
46306
- "SvgDrawShapeLayer": SvgDrawShapeLayer,
46307
- "SvgDrawText": SvgDrawText,
46308
46638
  "SvgFlow": SvgFlow,
46309
46639
  "SvgGrid": SvgGrid,
46310
46640
  "SvgLobe": SvgLobe,
@@ -46315,7 +46645,6 @@ var init_component_registry_generated = __esm({
46315
46645
  "SvgRing": SvgRing,
46316
46646
  "SvgShield": SvgShield,
46317
46647
  "SvgStack": SvgStack,
46318
- "SvgStage": SvgStage,
46319
46648
  "SwipeableRow": SwipeableRow,
46320
46649
  "Switch": Switch,
46321
46650
  "TabbedContainer": TabbedContainer,
@@ -46594,7 +46923,19 @@ function UISlotComponent({
46594
46923
  const suspenseConfig = useContext(SuspenseConfigContext);
46595
46924
  const contained = useContext(SlotContainedContext);
46596
46925
  const schemaCtx = useEntitySchemaOptional();
46597
- const content = slots[slot];
46926
+ const rawContent = slots[slot];
46927
+ const binding = useEntityBindingSnapshot(rawContent?.sourceTrait);
46928
+ const content = useMemo(() => {
46929
+ if (!rawContent) return rawContent;
46930
+ const resolvedProps = resolveRenderBindingMarkers(
46931
+ rawContent.props,
46932
+ rawContent.sourceTrait,
46933
+ binding.entity,
46934
+ binding.config,
46935
+ binding.state
46936
+ );
46937
+ return resolvedProps === rawContent.props ? rawContent : { ...rawContent, props: resolvedProps };
46938
+ }, [rawContent, binding.entity, binding.config, binding.state]);
46598
46939
  if (children !== void 0) {
46599
46940
  if (pattern === "clear") {
46600
46941
  return null;
@@ -46938,6 +47279,7 @@ function isPlainConfigObject(value) {
46938
47279
  return proto === Object.prototype || proto === null;
46939
47280
  }
46940
47281
  function substituteTraitRefsDeep(value, pathKey) {
47282
+ if (isRenderBindingMarker(value)) return value;
46941
47283
  if (typeof value === "string") {
46942
47284
  const match = TRAIT_BINDING_RE.exec(value);
46943
47285
  if (match) {
@@ -47010,7 +47352,14 @@ function SlotContentRenderer({
47010
47352
  onDismiss,
47011
47353
  patternPath
47012
47354
  }) {
47013
- const entityProp = content.props.entity;
47355
+ const ambientScope = useTraitScope();
47356
+ const bindingTrait = content.sourceTrait ?? ambientScope?.trait;
47357
+ const binding = useEntityBindingSnapshot(bindingTrait);
47358
+ const liveProps = useMemo(
47359
+ () => resolveRenderBindingMarkers(content.props, bindingTrait, binding.entity, binding.config, binding.state),
47360
+ [content.props, bindingTrait, binding.entity, binding.config, binding.state]
47361
+ );
47362
+ const entityProp = liveProps.entity;
47014
47363
  if (content.pattern === "form-section") {
47015
47364
  slotLog.debug("SlotContentRenderer:form-section-render", {
47016
47365
  contentId: content.id,
@@ -47041,7 +47390,7 @@ function SlotContentRenderer({
47041
47390
  const orbitalName = schemaCtx && content.sourceTrait !== void 0 ? schemaCtx.orbitalsByTrait.get(content.sourceTrait) : void 0;
47042
47391
  const PatternComponent = getComponentForPattern(content.pattern);
47043
47392
  if (PatternComponent) {
47044
- const childrenConfig = content.props.children;
47393
+ const childrenConfig = liveProps.children;
47045
47394
  const isSingleChild = typeof childrenConfig === "string" || typeof childrenConfig === "object" && childrenConfig !== null && !Array.isArray(childrenConfig) && "type" in childrenConfig;
47046
47395
  const hasChildren = PATTERNS_WITH_CHILDREN.has(content.pattern) || Array.isArray(childrenConfig) && childrenConfig.length > 0 || isSingleChild;
47047
47396
  const isDrawHost = isDrawHostPattern(content.pattern);
@@ -47052,9 +47401,9 @@ function SlotContentRenderer({
47052
47401
  fromState: content.fromState,
47053
47402
  entity: content.entity
47054
47403
  }) : void 0;
47055
- const incomingChildren = content.props.children;
47404
+ const incomingChildren = liveProps.children;
47056
47405
  const childrenIsRenderFn = typeof incomingChildren === "function";
47057
- const { children: _childrenConfig, ...restPropsNoChildren } = content.props;
47406
+ const { children: _childrenConfig, ...restPropsNoChildren } = liveProps;
47058
47407
  const restProps = childrenIsRenderFn ? { ...restPropsNoChildren, children: incomingChildren } : restPropsNoChildren;
47059
47408
  const nodeSlotOverrides = {};
47060
47409
  for (const slotKey of CONTENT_NODE_SLOTS) {
@@ -47163,7 +47512,7 @@ function SlotContentRenderer({
47163
47512
  "data-orb-path": patternPath ?? "root",
47164
47513
  "data-orb-pattern": content.pattern,
47165
47514
  "data-orb-orbital": orbitalName,
47166
- children: content.props.children ?? /* @__PURE__ */ jsxs(Box, { className: "p-4 text-sm text-muted-foreground border border-dashed border-border rounded", children: [
47515
+ children: liveProps.children ?? /* @__PURE__ */ jsxs(Box, { className: "p-4 text-sm text-muted-foreground border border-dashed border-border rounded", children: [
47167
47516
  "Unknown pattern: ",
47168
47517
  content.pattern,
47169
47518
  content.sourceTrait && /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "ml-2", children: [
@@ -47235,6 +47584,7 @@ var scopeWrapLog, TRAIT_BINDING_RE, SuspenseConfigContext, SlotContainedContext,
47235
47584
  var init_UISlotRenderer = __esm({
47236
47585
  "components/core/organisms/UISlotRenderer.tsx"() {
47237
47586
  "use client";
47587
+ init_resolve_render_bindings();
47238
47588
  init_Modal();
47239
47589
  init_Drawer();
47240
47590
  init_Toast();
@@ -50736,6 +51086,9 @@ function resolveLambdaBindings(body, params, item, index) {
50736
51086
  const arr = body;
50737
51087
  const substituted = arr.map((b) => recur(b));
50738
51088
  if (isOperatorCall(substituted) && isSExpr(substituted)) {
51089
+ if (containsEntityBinding(substituted)) {
51090
+ return substituted;
51091
+ }
50739
51092
  const evaluated = evaluate(substituted, createMinimalContext());
50740
51093
  return isEventPayloadValue(evaluated) ? evaluated : String(evaluated);
50741
51094
  }
@@ -50757,9 +51110,37 @@ function getSlotContentRenderer2() {
50757
51110
  _slotContentRenderer2 = mod.SlotContentRenderer;
50758
51111
  return _slotContentRenderer2;
50759
51112
  }
51113
+ function deferLambdaEntityExprs(value) {
51114
+ if (typeof value === "string") {
51115
+ if (containsEntityBinding(value)) {
51116
+ const marker = { [RENDER_BINDING_MARKER]: true, expression: value };
51117
+ return marker;
51118
+ }
51119
+ return value;
51120
+ }
51121
+ if (Array.isArray(value)) {
51122
+ if (isFnFormLambda(value)) return value;
51123
+ const arr = value;
51124
+ if (isOperatorCall(arr) && isSExpr(arr) && containsEntityBinding(arr)) {
51125
+ const marker = { [RENDER_BINDING_MARKER]: true, expression: arr };
51126
+ return marker;
51127
+ }
51128
+ return arr.map((v) => deferLambdaEntityExprs(v));
51129
+ }
51130
+ if (value !== null && typeof value === "object" && !React92__default.isValidElement(value) && !(value instanceof Date) && typeof value !== "function" && !isRenderBindingMarker(value)) {
51131
+ const out = {};
51132
+ for (const [k, v] of Object.entries(value)) {
51133
+ out[k] = deferLambdaEntityExprs(v);
51134
+ }
51135
+ return out;
51136
+ }
51137
+ return value;
51138
+ }
50760
51139
  function makeLambdaFn(params, lambdaBody, callerKey) {
50761
51140
  return (item, index) => {
50762
- const resolvedBody = resolveLambdaBindings(lambdaBody, params, item, index);
51141
+ const resolvedBody = deferLambdaEntityExprs(
51142
+ resolveLambdaBindings(lambdaBody, params, item, index)
51143
+ );
50763
51144
  if (resolvedBody === null || typeof resolvedBody !== "object" || Array.isArray(resolvedBody) || typeof resolvedBody === "function" || React92__default.isValidElement(resolvedBody) || resolvedBody instanceof Date) {
50764
51145
  return null;
50765
51146
  }
@@ -50784,6 +51165,7 @@ function makeLambdaFn(params, lambdaBody, callerKey) {
50784
51165
  }
50785
51166
  function convertNode(node, callerKey) {
50786
51167
  if (node === null || node === void 0) return node;
51168
+ if (isRenderBindingMarker(node)) return node;
50787
51169
  if (Array.isArray(node)) {
50788
51170
  if (isFnFormLambda(node)) {
50789
51171
  const arr2 = node;
@@ -50957,6 +51339,7 @@ var tickLog = createLogger("almadar:ui:tick-effects");
50957
51339
  setNamespaceLevel("almadar:ui:tick-effects", "WARN");
50958
51340
  var sharedEntityLog = createLogger("almadar:ui:shared-entity");
50959
51341
  setNamespaceLevel("almadar:ui:shared-entity", "WARN");
51342
+ var EMPTY_BINDING_SNAPSHOT = {};
50960
51343
  var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
50961
51344
  "set",
50962
51345
  "emit",
@@ -50977,11 +51360,6 @@ var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
50977
51360
  "do",
50978
51361
  "when"
50979
51362
  ]);
50980
- var REACTIVE_REPAINT_PATTERN_TYPES = /* @__PURE__ */ new Set([
50981
- "canvas",
50982
- "game-hud",
50983
- "game-shell"
50984
- ]);
50985
51363
  var LIFECYCLE_EVENTS = ["INIT", "LOAD", "$MOUNT"];
50986
51364
  function toTraitDefinition(binding) {
50987
51365
  return {
@@ -51022,6 +51400,27 @@ function classifySharedTick(tick) {
51022
51400
  if (renders) return "renderer";
51023
51401
  return "neither";
51024
51402
  }
51403
+ function buildTraitRenderConfig(binding, traitConfigsByName, payload) {
51404
+ const declaredDefaults = collectDeclaredConfigDefaults(binding.trait);
51405
+ const resolvedDefaults = traitConfigsByName?.[binding.trait.name];
51406
+ const callSiteConfig = getBindingConfig(binding);
51407
+ const callSiteOverrides = callSiteConfig ? resolveCallSitePayloadCaptures(
51408
+ Object.fromEntries(
51409
+ Object.entries(callSiteConfig).filter(
51410
+ ([, v]) => !containsConfigForward(v)
51411
+ )
51412
+ ),
51413
+ payload
51414
+ ) : void 0;
51415
+ if (declaredDefaults || resolvedDefaults || callSiteOverrides) {
51416
+ return {
51417
+ ...declaredDefaults ?? {},
51418
+ ...resolvedDefaults ?? {},
51419
+ ...callSiteOverrides ?? {}
51420
+ };
51421
+ }
51422
+ return void 0;
51423
+ }
51025
51424
  function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
51026
51425
  return (scratch) => {
51027
51426
  const traitName = binding.trait.name;
@@ -51242,8 +51641,24 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51242
51641
  }, [traitStates]);
51243
51642
  const traitSnapshotDataRef = useRef(/* @__PURE__ */ new Map());
51244
51643
  const traitFieldStatesRef = useRef(/* @__PURE__ */ new Map());
51245
- const lastCanvasRenderUiRef = useRef(/* @__PURE__ */ new Map());
51246
- const executeTransitionEffectsRef = useRef(null);
51644
+ const bridgeEchoPendingRef = useRef(/* @__PURE__ */ new Map());
51645
+ const bindingSnapshotsRef = useRef(/* @__PURE__ */ new Map());
51646
+ const bindingListenersRef = useRef(/* @__PURE__ */ new Map());
51647
+ const publishBindingSnapshot = useCallback((traitName, row) => {
51648
+ bindingSnapshotsRef.current.set(traitName, { ...row });
51649
+ const listeners7 = bindingListenersRef.current.get(traitName);
51650
+ if (!listeners7) return;
51651
+ listeners7.forEach((callback) => {
51652
+ try {
51653
+ callback();
51654
+ } catch (error) {
51655
+ stateLog.error("binding-snapshot listener error", {
51656
+ traitName,
51657
+ error: error instanceof Error ? error.message : String(error)
51658
+ });
51659
+ }
51660
+ });
51661
+ }, []);
51247
51662
  useEffect(() => {
51248
51663
  const mgr = managerRef.current;
51249
51664
  const bindings = traitBindingsRef.current;
@@ -51371,15 +51786,6 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51371
51786
  const existing = pendingSlots.get(slot) || [];
51372
51787
  existing.push({ pattern, props: props || {} });
51373
51788
  pendingSlots.set(slot, existing);
51374
- const patternType = pattern?.type;
51375
- if (patternType !== void 0 && REACTIVE_REPAINT_PATTERN_TYPES.has(patternType)) {
51376
- const rawForSlot = effects.filter(
51377
- (e) => Array.isArray(e) && (e[0] === "render-ui" || e[0] === "render") && e[1] === slot
51378
- );
51379
- if (rawForSlot.length > 0) {
51380
- lastCanvasRenderUiRef.current.set(traitName, rawForSlot);
51381
- }
51382
- }
51383
51789
  },
51384
51790
  clearSlot: (slot) => {
51385
51791
  pendingSlots.set(slot, []);
@@ -51451,11 +51857,13 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51451
51857
  };
51452
51858
  }
51453
51859
  const sharedWrites = [];
51860
+ let didWrite = false;
51454
51861
  const baseSet = handlers.set;
51455
51862
  handlers = {
51456
51863
  ...handlers,
51457
51864
  set: async (targetId, field, value) => {
51458
51865
  if (baseSet) await baseSet(targetId, field, value);
51866
+ didWrite = true;
51459
51867
  if (sharedKey !== void 0) {
51460
51868
  sharedWrites.push({ field, value });
51461
51869
  }
@@ -51472,23 +51880,9 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51472
51880
  payload: payload || {},
51473
51881
  state: previousState
51474
51882
  };
51475
- const declaredDefaults = collectDeclaredConfigDefaults(binding.trait);
51476
- const resolvedDefaults = traitConfigsByName?.[traitName];
51477
- const callSiteConfig = getBindingConfig(binding);
51478
- const callSiteOverrides = callSiteConfig ? resolveCallSitePayloadCaptures(
51479
- Object.fromEntries(
51480
- Object.entries(callSiteConfig).filter(
51481
- ([, v]) => !containsConfigForward(v)
51482
- )
51483
- ),
51484
- payload || {}
51485
- ) : void 0;
51486
- if (declaredDefaults || resolvedDefaults || callSiteOverrides) {
51487
- bindingCtx.config = {
51488
- ...declaredDefaults ?? {},
51489
- ...resolvedDefaults ?? {},
51490
- ...callSiteOverrides ?? {}
51491
- };
51883
+ const renderConfig = buildTraitRenderConfig(binding, traitConfigsByName, payload || {});
51884
+ if (renderConfig !== void 0) {
51885
+ bindingCtx.config = renderConfig;
51492
51886
  }
51493
51887
  if (traitName === "Authority" || traitName === "Hero") {
51494
51888
  sharedEntityLog.debug("executeTransitionEffects config tiles", {
@@ -51522,7 +51916,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51522
51916
  effectHeads: effects.map((e) => Array.isArray(e) ? String(e[0]) : "??")
51523
51917
  });
51524
51918
  }
51525
- const executor = new EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
51919
+ const executor = new EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext, deferRenderBindings: true });
51526
51920
  try {
51527
51921
  await executor.executeAll(effects);
51528
51922
  if (traitName === "Authority" || traitName === "Hero") {
@@ -51563,37 +51957,8 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51563
51957
  entity: binding.linkedEntity
51564
51958
  });
51565
51959
  }
51566
- if (pendingSlots.size === 0 && effectsCallOp(effects, SHARED_ENTITY_WRITE_OPS)) {
51567
- const stashed = lastCanvasRenderUiRef.current.get(traitName);
51568
- if (stashed !== void 0 && stashed.length > 0) {
51569
- await executor.executeAll(stashed);
51570
- for (const [slot, patterns] of pendingSlots) {
51571
- flushSlot(traitName, slot, patterns, {
51572
- event: flushEvent,
51573
- state: previousState,
51574
- entity: binding.linkedEntity
51575
- });
51576
- }
51577
- }
51578
- if (sharedKey !== void 0 && executeTransitionEffectsRef.current !== null) {
51579
- for (const sibling of traitBindingsRef.current) {
51580
- const siblingName = sibling.trait.name;
51581
- if (siblingName === traitName) continue;
51582
- if (sharedKeyByTraitNameRef.current.get(siblingName) !== sharedKey) continue;
51583
- const siblingStash = lastCanvasRenderUiRef.current.get(siblingName);
51584
- if (siblingStash === void 0 || siblingStash.length === 0) continue;
51585
- const siblingState = traitStatesRef.current.get(siblingName)?.currentState ?? "";
51586
- await executeTransitionEffectsRef.current({
51587
- binding: sibling,
51588
- effects: siblingStash,
51589
- previousState: siblingState,
51590
- newState: siblingState,
51591
- flushEvent: `${flushEvent}:repaint`,
51592
- syncOnly,
51593
- log: log11
51594
- });
51595
- }
51596
- }
51960
+ if (didWrite && sharedKey === void 0) {
51961
+ publishBindingSnapshot(traitName, liveEntity);
51597
51962
  }
51598
51963
  } catch (error) {
51599
51964
  log11.error("effects:error", {
@@ -51605,10 +51970,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51605
51970
  });
51606
51971
  }
51607
51972
  return emittedDuringExec;
51608
- }, [eventBus, flushSlot, sharedEntityStore]);
51609
- useEffect(() => {
51610
- executeTransitionEffectsRef.current = executeTransitionEffects;
51611
- }, [executeTransitionEffects]);
51973
+ }, [eventBus, flushSlot, sharedEntityStore, publishBindingSnapshot]);
51612
51974
  const runTickEffects = useCallback((tick, binding) => {
51613
51975
  const traitName = binding.trait.name;
51614
51976
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
@@ -51671,24 +52033,6 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51671
52033
  ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
51672
52034
  );
51673
52035
  runTickFrame(group.storeKey, writers, sharedEntityStore);
51674
- const repaint = executeTransitionEffectsRef.current;
51675
- if (repaint !== null) {
51676
- for (const renderBinding of group.renderBindings) {
51677
- const renderTraitName = renderBinding.trait.name;
51678
- const stash = lastCanvasRenderUiRef.current.get(renderTraitName);
51679
- if (stash === void 0 || stash.length === 0) continue;
51680
- const renderState = traitStatesRef.current.get(renderTraitName)?.currentState ?? "";
51681
- void repaint({
51682
- binding: renderBinding,
51683
- effects: stash,
51684
- previousState: renderState,
51685
- newState: renderState,
51686
- flushEvent: "tick:repaint",
51687
- syncOnly: true,
51688
- log: sharedEntityLog
51689
- });
51690
- }
51691
- }
51692
52036
  };
51693
52037
  if (interval === "frame") {
51694
52038
  scheduler.add(0, onDue);
@@ -51759,6 +52103,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51759
52103
  executedTraits: results.map((r2) => r2.traitName).join(",")
51760
52104
  });
51761
52105
  const emittedByTrait = /* @__PURE__ */ new Map();
52106
+ bridgeEchoPendingRef.current.clear();
51762
52107
  for (const { traitName, result } of results) {
51763
52108
  const binding = bindingMap.get(traitName);
51764
52109
  const traitState = currentManager.getState(traitName);
@@ -51813,6 +52158,12 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51813
52158
  log: stateLog
51814
52159
  });
51815
52160
  emittedByTrait.set(traitName, emittedDuringExec);
52161
+ for (const emittedKey of emittedDuringExec) {
52162
+ bridgeEchoPendingRef.current.set(
52163
+ emittedKey,
52164
+ (bridgeEchoPendingRef.current.get(emittedKey) ?? 0) + 1
52165
+ );
52166
+ }
51816
52167
  } else if (!result.executed) {
51817
52168
  if (result.guardResult === false) {
51818
52169
  stateLog.debug("guard-blocked-transition", {
@@ -51962,8 +52313,13 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51962
52313
  crossTraitLog.debug("self:subscribe", { traitName, busKey: selfBusKey, eventKey });
51963
52314
  const unsub = eventBus.on(selfBusKey, (event) => {
51964
52315
  if (event.source && event.source.dispatched) {
51965
- crossTraitLog.debug("self:fire-skipped-bridge-echo", { traitName, busKey: selfBusKey, eventKey });
51966
- return;
52316
+ const pendingEchoes = bridgeEchoPendingRef.current.get(eventKey) ?? 0;
52317
+ if (pendingEchoes > 0) {
52318
+ bridgeEchoPendingRef.current.set(eventKey, pendingEchoes - 1);
52319
+ crossTraitLog.debug("self:fire-skipped-bridge-echo", { traitName, busKey: selfBusKey, eventKey });
52320
+ return;
52321
+ }
52322
+ crossTraitLog.debug("self:fire-server-cascade", { traitName, busKey: selfBusKey, eventKey });
51967
52323
  }
51968
52324
  crossTraitLog.debug("self:fire", { traitName, busKey: selfBusKey, eventKey });
51969
52325
  enqueueAndDrain(eventKey, event.payload, traitName);
@@ -52055,11 +52411,47 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
52055
52411
  initedTraitsRef.current = /* @__PURE__ */ new Set();
52056
52412
  };
52057
52413
  }, [traitBindings, enqueueAndDrain]);
52414
+ const entityBindingSource = useMemo(() => ({
52415
+ getEntitySnapshot: (traitName) => {
52416
+ const sharedKey = sharedKeyByTraitNameRef.current.get(traitName);
52417
+ if (sharedKey !== void 0) {
52418
+ return sharedEntityStore.getSnapshot(sharedKey);
52419
+ }
52420
+ return bindingSnapshotsRef.current.get(traitName) ?? EMPTY_BINDING_SNAPSHOT;
52421
+ },
52422
+ getConfig: (traitName) => {
52423
+ const binding = traitBindingsRef.current.find((b) => b.trait.name === traitName);
52424
+ if (binding === void 0) return void 0;
52425
+ return buildTraitRenderConfig(binding, traitConfigsByName, {});
52426
+ },
52427
+ getState: (traitName) => traitStatesRef.current.get(traitName)?.currentState ?? "",
52428
+ subscribe: (traitName, callback) => {
52429
+ const sharedKey = sharedKeyByTraitNameRef.current.get(traitName);
52430
+ if (sharedKey !== void 0) {
52431
+ return sharedEntityStore.subscribe(sharedKey, callback);
52432
+ }
52433
+ let listeners7 = bindingListenersRef.current.get(traitName);
52434
+ if (!listeners7) {
52435
+ listeners7 = /* @__PURE__ */ new Set();
52436
+ bindingListenersRef.current.set(traitName, listeners7);
52437
+ }
52438
+ listeners7.add(callback);
52439
+ return () => {
52440
+ const current = bindingListenersRef.current.get(traitName);
52441
+ if (!current) return;
52442
+ current.delete(callback);
52443
+ if (current.size === 0) {
52444
+ bindingListenersRef.current.delete(traitName);
52445
+ }
52446
+ };
52447
+ }
52448
+ }), [sharedEntityStore, traitConfigsByName]);
52058
52449
  return {
52059
52450
  traitStates,
52060
52451
  sendEvent,
52061
52452
  getTraitState,
52062
- canHandleEvent
52453
+ canHandleEvent,
52454
+ entityBindingSource
52063
52455
  };
52064
52456
  }
52065
52457
 
@@ -52148,7 +52540,7 @@ function collectServerActiveTraits(ir, allTraits, mountedTraitNames) {
52148
52540
  }
52149
52541
  return active;
52150
52542
  }
52151
- function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNavigate, onLocalFallback, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, serverActiveTraits }) {
52543
+ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNavigate, onLocalFallback, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, serverActiveTraits, children }) {
52152
52544
  const bridge = useServerBridge();
52153
52545
  const activeTraitNames = useMemo(
52154
52546
  () => new Set(traits2.map((b) => b.trait.name).filter((n) => !!n)),
@@ -52178,7 +52570,7 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
52178
52570
  }
52179
52571
  }, [bridge.connected, bridge.sendEvent, orbitalNames, uiSlots, onNavigate, embeddedTraits, activeTraitNames, withActiveTraits]);
52180
52572
  const opts = orbitalNames ? { onEventProcessed, navigate: onNavigate, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams } : { navigate: onNavigate, persistence, traitConfigsByName, orbitalsByTrait, embeddedTraits, initPayload: routeParams };
52181
- const { sendEvent } = useTraitStateMachine(traits2, uiSlots, opts);
52573
+ const { sendEvent, entityBindingSource } = useTraitStateMachine(traits2, uiSlots, opts);
52182
52574
  const initSentRef = useRef(false);
52183
52575
  const prevTraitsRef = useRef(void 0);
52184
52576
  const paramsKey = JSON.stringify(routeParams ?? {});
@@ -52224,7 +52616,7 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
52224
52616
  { type: "fetch", args: [], status: "executed" },
52225
52617
  ...effects.map((eff) => ({
52226
52618
  type: eff.type,
52227
- args: eff.type === "render-ui" ? [eff.slot] : [],
52619
+ args: eff.type === "render-ui" && eff.slot !== void 0 ? [eff.slot] : [],
52228
52620
  status: "executed"
52229
52621
  }))
52230
52622
  ];
@@ -52240,7 +52632,7 @@ function TraitInitializer({ traits: traits2, routeParams, orbitalNames, onNaviga
52240
52632
  }
52241
52633
  })();
52242
52634
  }, [bridge.connected, orbitalNames, bridge.sendEvent, uiSlots, onNavigate, embeddedTraits, activeTraitNames, withActiveTraits, routeParams]);
52243
- return null;
52635
+ return /* @__PURE__ */ jsx(EntityBindingContext.Provider, { value: entityBindingSource, children });
52244
52636
  }
52245
52637
  function SchemaRunner({ schema, serverUrl, transport, mockData, pageName, routeParams, onNavigate, onLocalFallback, persistence }) {
52246
52638
  const { traits: traits2, allEntities, allTraits, ir } = useResolvedSchema(schema, pageName);
@@ -52373,30 +52765,28 @@ function SchemaRunner({ schema, serverUrl, transport, mockData, pageName, routeP
52373
52765
  }
52374
52766
  return schema.orbitals[0]?.theme;
52375
52767
  }, [schema, pageName]);
52376
- const inner = /* @__PURE__ */ jsx(VerificationProvider, { enabled: true, children: /* @__PURE__ */ jsxs(
52768
+ const inner = /* @__PURE__ */ jsx(VerificationProvider, { enabled: true, children: /* @__PURE__ */ jsx(
52377
52769
  EntitySchemaProvider,
52378
52770
  {
52379
52771
  entities: entitiesArray,
52380
52772
  traitLinkedEntities: traitLinkedEntitiesMap,
52381
52773
  orbitalsByTrait: orbitalsByTraitMap,
52382
- children: [
52383
- /* @__PURE__ */ jsx(
52384
- TraitInitializer,
52385
- {
52386
- traits: allPageTraits,
52387
- routeParams,
52388
- orbitalNames: serverUrl || transport ? pageOrbitalNames : void 0,
52389
- traitConfigsByName,
52390
- orbitalsByTrait,
52391
- embeddedTraits,
52392
- serverActiveTraits,
52393
- onNavigate,
52394
- onLocalFallback,
52395
- persistence
52396
- }
52397
- ),
52398
- /* @__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 }) }) })
52399
- ]
52774
+ children: /* @__PURE__ */ jsx(
52775
+ TraitInitializer,
52776
+ {
52777
+ traits: allPageTraits,
52778
+ routeParams,
52779
+ orbitalNames: serverUrl || transport ? pageOrbitalNames : void 0,
52780
+ traitConfigsByName,
52781
+ orbitalsByTrait,
52782
+ embeddedTraits,
52783
+ serverActiveTraits,
52784
+ onNavigate,
52785
+ onLocalFallback,
52786
+ persistence,
52787
+ 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 }) }) })
52788
+ }
52789
+ )
52400
52790
  }
52401
52791
  ) });
52402
52792
  if (serverUrl || transport) {