@almadar/ui 5.101.0 → 5.103.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.
@@ -11,7 +11,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
11
  import { createPortal } from 'react-dom';
12
12
  import { useTranslate } from '@almadar/ui/hooks';
13
13
  import { useUISlots, UISlotProvider, useTheme } from '@almadar/ui/context';
14
- import { evaluateGuard, evaluate, createMinimalContext } from '@almadar/evaluator';
14
+ import { evaluateGuard, createMinimalContext, executeEffects, evaluate } from '@almadar/evaluator';
15
15
  import { Link, Outlet, useLocation } from 'react-router-dom';
16
16
  import ELK from 'elkjs/lib/elk.bundled.js';
17
17
  import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light.js';
@@ -37,7 +37,7 @@ import ReactMarkdown from 'react-markdown';
37
37
  import remarkGfm from 'remark-gfm';
38
38
  import remarkMath from 'remark-math';
39
39
  import rehypeKatex from 'rehype-katex';
40
- import { isCircuitEvent, schemaToIR, getPage, clearSchemaCache as clearSchemaCache$1, isEntityCall, isInlineTrait } from '@almadar/core';
40
+ import { isCircuitEvent, schemaToIR, getPage, clearSchemaCache as clearSchemaCache$1, isEntityCall, walkSExpr, mergeEntityFrame, isInlineTrait } from '@almadar/core';
41
41
  import { DndContext, pointerWithin, rectIntersection, closestCorners, useSensors, useSensor, PointerSensor, KeyboardSensor, useDroppable } from '@dnd-kit/core';
42
42
  import { useSortable, arrayMove, sortableKeyboardCoordinates, SortableContext, rectSortingStrategy, verticalListSortingStrategy } from '@dnd-kit/sortable';
43
43
  import { CSS } from '@dnd-kit/utilities';
@@ -7685,7 +7685,7 @@ function recordTransition(trace) {
7685
7685
  ...trace,
7686
7686
  id: `t-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
7687
7687
  };
7688
- log3.debug("transition:recorded", { trait: trace.traitName, from: trace.from, to: trace.to, event: trace.event, effectCount: trace.effects.length });
7688
+ log4.debug("transition:recorded", { trait: trace.traitName, from: trace.from, to: trace.to, event: trace.event, effectCount: trace.effects.length });
7689
7689
  getState().transitions.push(entry);
7690
7690
  if (getState().transitions.length > MAX_TRANSITIONS) {
7691
7691
  getState().transitions.shift();
@@ -7787,7 +7787,7 @@ function getTraitSnapshots() {
7787
7787
  try {
7788
7788
  snapshots.push(getter());
7789
7789
  } catch (err) {
7790
- log3.error("traitSnapshot getter failed", { trait: traitName, err: String(err) });
7790
+ log4.error("traitSnapshot getter failed", { trait: traitName, err: String(err) });
7791
7791
  }
7792
7792
  }
7793
7793
  return snapshots;
@@ -7886,10 +7886,10 @@ function updateAssetStatus(url, status) {
7886
7886
  window.__orbitalVerification.assetStatus[url] = status;
7887
7887
  }
7888
7888
  }
7889
- var log3, MAX_TRANSITIONS;
7889
+ var log4, MAX_TRANSITIONS;
7890
7890
  var init_verificationRegistry = __esm({
7891
7891
  "lib/verificationRegistry.ts"() {
7892
- log3 = createLogger("almadar:bridge");
7892
+ log4 = createLogger("almadar:bridge");
7893
7893
  MAX_TRANSITIONS = 500;
7894
7894
  exposeOnWindow();
7895
7895
  }
@@ -17497,7 +17497,7 @@ var init_avl_elk_layout = __esm({
17497
17497
  elk = new ELK();
17498
17498
  }
17499
17499
  });
17500
- var log4, SWIM_GUTTER, CENTER_W, BehaviorView;
17500
+ var log5, SWIM_GUTTER, CENTER_W, BehaviorView;
17501
17501
  var init_BehaviorView = __esm({
17502
17502
  "components/avl/molecules/BehaviorView.tsx"() {
17503
17503
  "use client";
@@ -17506,7 +17506,7 @@ var init_BehaviorView = __esm({
17506
17506
  init_AvlSwimLane();
17507
17507
  init_avl_atom_types();
17508
17508
  init_avl_elk_layout();
17509
- log4 = createLogger("almadar:ui:avl:behavior-view");
17509
+ log5 = createLogger("almadar:ui:avl:behavior-view");
17510
17510
  SWIM_GUTTER = 120;
17511
17511
  CENTER_W = 360;
17512
17512
  BehaviorView = ({ data }) => {
@@ -17517,7 +17517,7 @@ var init_BehaviorView = __esm({
17517
17517
  const dataKey = useMemo(() => JSON.stringify(traitData), [traitData]);
17518
17518
  useEffect(() => {
17519
17519
  if (!traitData) return;
17520
- computeTraitLayout(traitData).then(setLayout).catch((err) => log4.error("compute-trait-layout-failed", { error: err instanceof Error ? err : String(err) }));
17520
+ computeTraitLayout(traitData).then(setLayout).catch((err) => log5.error("compute-trait-layout-failed", { error: err instanceof Error ? err : String(err) }));
17521
17521
  }, [dataKey]);
17522
17522
  if (!traitData) {
17523
17523
  return /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4 text-center text-[var(--color-muted-foreground)] text-sm", children: t("avl.noTraitData") });
@@ -18069,7 +18069,7 @@ function generateDiff(oldVal, newVal) {
18069
18069
  }
18070
18070
  return diff;
18071
18071
  }
18072
- var orbStyleOverrides, orbStyle, loloStyleOverrides, loloStyle, log5, CODE_LANGUAGES, CODE_LANGUAGE_SET, DIFF_STYLES, DIFF_STYLE_FALLBACK, LINE_PROPS_FN, HIDDEN_LINE_NUMBERS, CodeBlock;
18072
+ var orbStyleOverrides, orbStyle, loloStyleOverrides, loloStyle, log6, CODE_LANGUAGES, CODE_LANGUAGE_SET, DIFF_STYLES, DIFF_STYLE_FALLBACK, LINE_PROPS_FN, HIDDEN_LINE_NUMBERS, CodeBlock;
18073
18073
  var init_CodeBlock = __esm({
18074
18074
  "components/core/molecules/markdown/CodeBlock.tsx"() {
18075
18075
  init_cn();
@@ -18152,7 +18152,7 @@ var init_CodeBlock = __esm({
18152
18152
  "lolo-op-async": { color: ORB_COLORS.dark.async }
18153
18153
  };
18154
18154
  loloStyle = { ...dark, ...loloStyleOverrides };
18155
- log5 = createLogger("almadar:ui:markdown-code");
18155
+ log6 = createLogger("almadar:ui:markdown-code");
18156
18156
  CODE_LANGUAGES = [
18157
18157
  "text",
18158
18158
  "json",
@@ -18420,7 +18420,7 @@ var init_CodeBlock = __esm({
18420
18420
  eventBus.emit("UI:COPY_CODE", { language: activeLanguage, success: true });
18421
18421
  setTimeout(() => setCopied(false), 2e3);
18422
18422
  } catch (err) {
18423
- log5.error("Failed to copy code", { error: err instanceof Error ? err : String(err) });
18423
+ log6.error("Failed to copy code", { error: err instanceof Error ? err : String(err) });
18424
18424
  eventBus.emit("UI:COPY_CODE", { language: activeLanguage, success: false });
18425
18425
  }
18426
18426
  };
@@ -27285,14 +27285,14 @@ function useSafeEventBus3() {
27285
27285
  } };
27286
27286
  }
27287
27287
  }
27288
- var log6, lookStyles5, ButtonGroup;
27288
+ var log7, lookStyles5, ButtonGroup;
27289
27289
  var init_ButtonGroup = __esm({
27290
27290
  "components/core/molecules/ButtonGroup.tsx"() {
27291
27291
  "use client";
27292
27292
  init_cn();
27293
27293
  init_atoms();
27294
27294
  init_useEventBus();
27295
- log6 = createLogger("almadar:ui:button-group");
27295
+ log7 = createLogger("almadar:ui:button-group");
27296
27296
  lookStyles5 = {
27297
27297
  "right-aligned-buttons": "",
27298
27298
  "floating-bar": "fixed bottom-section left-1/2 -translate-x-1/2 shadow-elevation-toast bg-card p-card-sm rounded-container",
@@ -27373,7 +27373,7 @@ var init_ButtonGroup = __esm({
27373
27373
  {
27374
27374
  variant: "ghost",
27375
27375
  onClick: () => {
27376
- log6.debug("Filter clicked", { field: filter.field });
27376
+ log7.debug("Filter clicked", { field: filter.field });
27377
27377
  },
27378
27378
  children: filter.label
27379
27379
  },
@@ -28282,9 +28282,9 @@ function debug(...args) {
28282
28282
  const [first, ...rest] = args;
28283
28283
  const message = typeof first === "string" ? first : "<debug>";
28284
28284
  if (rest.length === 0 && typeof first === "string") {
28285
- log7.debug(message);
28285
+ log8.debug(message);
28286
28286
  } else {
28287
- log7.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
28287
+ log8.debug(message, { args: rest.length > 0 ? formatArgs(rest) : formatArgs([first]) });
28288
28288
  }
28289
28289
  }
28290
28290
  function debugGroup(label) {
@@ -28312,11 +28312,11 @@ function toLogMetaValue(v) {
28312
28312
  }
28313
28313
  return String(v);
28314
28314
  }
28315
- var NAMESPACE, log7;
28315
+ var NAMESPACE, log8;
28316
28316
  var init_debug = __esm({
28317
28317
  "lib/debug.ts"() {
28318
28318
  NAMESPACE = "almadar:ui:debug";
28319
- log7 = createLogger(NAMESPACE);
28319
+ log8 = createLogger(NAMESPACE);
28320
28320
  createLogger("almadar:ui:debug:input");
28321
28321
  createLogger("almadar:ui:debug:collision");
28322
28322
  createLogger("almadar:ui:debug:physics");
@@ -30474,12 +30474,12 @@ var init_MapView = __esm({
30474
30474
  shadowSize: [41, 41]
30475
30475
  });
30476
30476
  L.Marker.prototype.options.icon = defaultIcon;
30477
- const { useEffect: useEffect67, useRef: useRef65, useCallback: useCallback98, useState: useState97 } = React96__default;
30477
+ const { useEffect: useEffect67, useRef: useRef66, useCallback: useCallback98, useState: useState97 } = React96__default;
30478
30478
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
30479
30479
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
30480
30480
  function MapUpdater({ centerLat, centerLng, zoom }) {
30481
30481
  const map = useMap();
30482
- const prevRef = useRef65({ centerLat, centerLng, zoom });
30482
+ const prevRef = useRef66({ centerLat, centerLng, zoom });
30483
30483
  useEffect67(() => {
30484
30484
  const prev = prevRef.current;
30485
30485
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
@@ -43741,7 +43741,7 @@ function getAllEvents(traits2) {
43741
43741
  function EventDispatcherTab({ traits: traits2, schema }) {
43742
43742
  const eventBus = useEventBus();
43743
43743
  const { t } = useTranslate();
43744
- const [log9, setLog] = React96.useState([]);
43744
+ const [log10, setLog] = React96.useState([]);
43745
43745
  const prevStatesRef = React96.useRef(/* @__PURE__ */ new Map());
43746
43746
  React96.useEffect(() => {
43747
43747
  for (const trait of traits2) {
@@ -43805,9 +43805,9 @@ function EventDispatcherTab({ traits: traits2, schema }) {
43805
43805
  /* @__PURE__ */ jsx(Typography, { variant: "small", weight: "medium", className: "text-gray-500 mb-1", children: t("debug.otherEvents") }),
43806
43806
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: unavailableEvents.map((event) => /* @__PURE__ */ jsx(Badge, { variant: "default", size: "sm", className: "opacity-50", children: event }, event)) })
43807
43807
  ] }),
43808
- log9.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
43808
+ log10.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
43809
43809
  /* @__PURE__ */ jsx(Typography, { variant: "small", weight: "medium", className: "text-gray-500 mb-1", children: t("debug.recentTransitions") }),
43810
- /* @__PURE__ */ jsx(Stack, { gap: "xs", children: log9.map((entry, i) => /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
43810
+ /* @__PURE__ */ jsx(Stack, { gap: "xs", children: log10.map((entry, i) => /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "font-mono text-xs", children: [
43811
43811
  /* @__PURE__ */ jsx("span", { className: "text-purple-400", children: entry.traitName }),
43812
43812
  " ",
43813
43813
  /* @__PURE__ */ jsx("span", { className: "text-gray-500", children: entry.from }),
@@ -47517,7 +47517,67 @@ var init_UISlotRenderer = __esm({
47517
47517
 
47518
47518
  // hooks/index.ts
47519
47519
  init_useEventBus();
47520
- var log2 = createLogger("almadar:ui:effects:client-handlers");
47520
+ var log2 = createLogger("almadar:ui:shared-entity-store");
47521
+ var EMPTY_ENTITY_STATE = {};
47522
+ function createSharedEntityStore() {
47523
+ const states = /* @__PURE__ */ new Map();
47524
+ const subscribers2 = /* @__PURE__ */ new Map();
47525
+ const getSnapshot3 = (entityId) => states.get(entityId) ?? EMPTY_ENTITY_STATE;
47526
+ const subscribe2 = (entityId, callback) => {
47527
+ let set = subscribers2.get(entityId);
47528
+ if (!set) {
47529
+ set = /* @__PURE__ */ new Set();
47530
+ subscribers2.set(entityId, set);
47531
+ }
47532
+ set.add(callback);
47533
+ return () => {
47534
+ const current = subscribers2.get(entityId);
47535
+ if (!current) return;
47536
+ current.delete(callback);
47537
+ if (current.size === 0) {
47538
+ subscribers2.delete(entityId);
47539
+ }
47540
+ };
47541
+ };
47542
+ const commit = (entityId, nextState) => {
47543
+ states.set(entityId, nextState);
47544
+ const set = subscribers2.get(entityId);
47545
+ if (!set) return;
47546
+ set.forEach((callback) => {
47547
+ try {
47548
+ callback();
47549
+ } catch (error) {
47550
+ log2.error("Shared entity subscriber error", {
47551
+ entityId,
47552
+ error: error instanceof Error ? error : String(error)
47553
+ });
47554
+ }
47555
+ });
47556
+ };
47557
+ const seed = (entityId, initialState) => {
47558
+ if (!states.has(entityId)) {
47559
+ states.set(entityId, initialState);
47560
+ }
47561
+ };
47562
+ return { getSnapshot: getSnapshot3, subscribe: subscribe2, commit, seed };
47563
+ }
47564
+ function useSharedEntityStore() {
47565
+ const storeRef = useRef(null);
47566
+ if (storeRef.current === null) {
47567
+ storeRef.current = createSharedEntityStore();
47568
+ }
47569
+ return storeRef.current;
47570
+ }
47571
+ function runTickFrame(entityId, orderedWriters, store) {
47572
+ let scratch = store.getSnapshot(entityId);
47573
+ for (const writer of orderedWriters) {
47574
+ const writes = writer(scratch);
47575
+ scratch = mergeEntityFrame(scratch, writes);
47576
+ }
47577
+ store.commit(entityId, scratch);
47578
+ return scratch;
47579
+ }
47580
+ var log3 = createLogger("almadar:ui:effects:client-handlers");
47521
47581
  function createClientEffectHandlers(options) {
47522
47582
  const { eventBus, slotSetter, navigate, notify, callService, liveEntity } = options;
47523
47583
  return {
@@ -47526,12 +47586,12 @@ function createClientEffectHandlers(options) {
47526
47586
  eventBus.emit(prefixedEvent, payload);
47527
47587
  },
47528
47588
  persist: async () => {
47529
- log2.warn("persist is server-side only, ignored on client");
47589
+ log3.warn("persist is server-side only, ignored on client");
47530
47590
  },
47531
47591
  // @almadar/runtime EffectHandlers.set types value:unknown — should be FieldValue (upstream fix queued)
47532
47592
  set: ((_entityId, field, value) => {
47533
47593
  if (!liveEntity) {
47534
- log2.warn("set is server-side only, ignored on client (no live entity)");
47594
+ log3.warn("set is server-side only, ignored on client (no live entity)");
47535
47595
  return;
47536
47596
  }
47537
47597
  liveEntity[field] = value;
@@ -47563,10 +47623,10 @@ function createClientEffectHandlers(options) {
47563
47623
  slotSetter.addPattern(slot, pattern, props);
47564
47624
  },
47565
47625
  navigate: navigate ?? ((path) => {
47566
- log2.warn("No navigate handler, ignoring", { path });
47626
+ log3.warn("No navigate handler, ignoring", { path });
47567
47627
  }),
47568
47628
  notify: notify ?? ((msg, type) => {
47569
- log2.debug("notify", { type, message: msg });
47629
+ log3.debug("notify", { type, message: msg });
47570
47630
  })
47571
47631
  };
47572
47632
  }
@@ -47737,11 +47797,101 @@ function toTraitDefinition(binding) {
47737
47797
  function normalizeEventKey(eventKey) {
47738
47798
  return eventKey.startsWith("UI:") ? eventKey.slice(3) : eventKey;
47739
47799
  }
47800
+ var SHARED_ENTITY_WRITE_OPS = /* @__PURE__ */ new Set(["set"]);
47801
+ var SHARED_ENTITY_RENDER_OPS = /* @__PURE__ */ new Set(["render-ui", "render"]);
47802
+ function collectAllTraitEffects(trait) {
47803
+ return [
47804
+ ...trait.ticks.flatMap((t) => t.effects),
47805
+ ...trait.transitions.flatMap((t) => t.effects)
47806
+ ];
47807
+ }
47808
+ function effectsCallOp(effects, ops) {
47809
+ for (const effect of effects) {
47810
+ let found = false;
47811
+ walkSExpr(effect, (node) => {
47812
+ if (!found && Array.isArray(node) && typeof node[0] === "string" && ops.has(node[0])) {
47813
+ found = true;
47814
+ }
47815
+ });
47816
+ if (found) return true;
47817
+ }
47818
+ return false;
47819
+ }
47820
+ function createSharedEntityWriter(binding, tick, traitStatesRef, emit) {
47821
+ return (scratch) => {
47822
+ const traitName = binding.trait.name;
47823
+ const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
47824
+ if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return [];
47825
+ const scratchEntity = { ...scratch };
47826
+ const writes = [];
47827
+ const ctx = createMinimalContext(scratchEntity, {}, currentState);
47828
+ const declaredDefaults = collectDeclaredConfigDefaults(binding.trait);
47829
+ const callSiteConfig = binding.config;
47830
+ if (declaredDefaults || callSiteConfig) {
47831
+ ctx.config = { ...declaredDefaults ?? {}, ...callSiteConfig ?? {} };
47832
+ }
47833
+ ctx.mutateEntity = (changes) => {
47834
+ for (const [field, value] of Object.entries(changes)) {
47835
+ const fieldValue = value;
47836
+ scratchEntity[field] = fieldValue;
47837
+ writes.push({ field, value: fieldValue });
47838
+ }
47839
+ };
47840
+ ctx.emit = (event, payload) => {
47841
+ emit(event, payload);
47842
+ };
47843
+ if (tick.guard !== void 0 && !evaluateGuard(tick.guard, ctx)) {
47844
+ tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
47845
+ return [];
47846
+ }
47847
+ executeEffects(tick.effects, ctx);
47848
+ return writes;
47849
+ };
47850
+ }
47740
47851
  function useTraitStateMachine(traitBindings, uiSlots, options) {
47741
47852
  const eventBus = useEventBus();
47742
47853
  const { entities } = useEntitySchema();
47743
47854
  const traitConfigsByName = options?.traitConfigsByName;
47744
47855
  const orbitalsByTrait = options?.orbitalsByTrait;
47856
+ const sharedEntityStore = useSharedEntityStore();
47857
+ const sharedGroups = useMemo(() => {
47858
+ const groups = /* @__PURE__ */ new Map();
47859
+ for (const binding of traitBindings) {
47860
+ const linkedEntityName = binding.linkedEntity ?? binding.trait.linkedEntity;
47861
+ if (!linkedEntityName) continue;
47862
+ const entityDef = entities.get(linkedEntityName);
47863
+ if (!entityDef?.shared) continue;
47864
+ const orbitalName = orbitalsByTrait?.[binding.trait.name] ?? "";
47865
+ const storeKey = `${orbitalName}::${linkedEntityName}`;
47866
+ let group = groups.get(storeKey);
47867
+ if (!group) {
47868
+ let defaults;
47869
+ for (const field of entityDef.fields) {
47870
+ if (field.default !== void 0) {
47871
+ (defaults ?? (defaults = {}))[field.name] = field.default;
47872
+ }
47873
+ }
47874
+ group = { storeKey, entityName: linkedEntityName, writerBindings: [], renderBindings: [], defaults };
47875
+ groups.set(storeKey, group);
47876
+ }
47877
+ const allEffects = collectAllTraitEffects(binding.trait);
47878
+ if (effectsCallOp(allEffects, SHARED_ENTITY_WRITE_OPS)) group.writerBindings.push(binding);
47879
+ if (effectsCallOp(allEffects, SHARED_ENTITY_RENDER_OPS)) group.renderBindings.push(binding);
47880
+ }
47881
+ return groups;
47882
+ }, [traitBindings, entities, orbitalsByTrait]);
47883
+ for (const group of sharedGroups.values()) {
47884
+ if (group.defaults) sharedEntityStore.seed(group.storeKey, group.defaults);
47885
+ }
47886
+ const sharedKeyByTraitNameRef = useRef(/* @__PURE__ */ new Map());
47887
+ useEffect(() => {
47888
+ const map = /* @__PURE__ */ new Map();
47889
+ for (const group of sharedGroups.values()) {
47890
+ for (const binding of group.writerBindings) map.set(binding.trait.name, group.storeKey);
47891
+ for (const binding of group.renderBindings) map.set(binding.trait.name, group.storeKey);
47892
+ }
47893
+ sharedKeyByTraitNameRef.current = map;
47894
+ }, [sharedGroups]);
47745
47895
  const manager = useMemo(() => {
47746
47896
  const traitDefs = traitBindings.map(toTraitDefinition);
47747
47897
  const m = new StateMachineManager(traitDefs);
@@ -47924,14 +48074,21 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
47924
48074
  };
47925
48075
  }, [traitBindings]);
47926
48076
  const executeTransitionEffects = useCallback(async (params) => {
47927
- const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log9 } = params;
48077
+ const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log10 } = params;
47928
48078
  const traitName = binding.trait.name;
47929
48079
  const linkedEntity = binding.linkedEntity || "";
47930
48080
  const entityId = payload?.entityId;
47931
- let liveEntity = traitFieldStatesRef.current.get(traitName);
47932
- if (!liveEntity) {
47933
- liveEntity = {};
47934
- traitFieldStatesRef.current.set(traitName, liveEntity);
48081
+ const sharedKey = sharedKeyByTraitNameRef.current.get(traitName);
48082
+ let liveEntity;
48083
+ if (sharedKey !== void 0) {
48084
+ liveEntity = { ...sharedEntityStore.getSnapshot(sharedKey) };
48085
+ } else {
48086
+ let existing = traitFieldStatesRef.current.get(traitName);
48087
+ if (!existing) {
48088
+ existing = {};
48089
+ traitFieldStatesRef.current.set(traitName, existing);
48090
+ }
48091
+ liveEntity = existing;
47935
48092
  }
47936
48093
  const effects = syncOnly ? params.effects.filter(
47937
48094
  (e) => Array.isArray(e) && SYNC_TICK_OPERATORS.has(String(e[0]))
@@ -48013,7 +48170,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48013
48170
  ...handlers,
48014
48171
  set: async (targetId, field, value) => {
48015
48172
  if (baseSet) await baseSet(targetId, field, value);
48016
- log9.debug("set:write", {
48173
+ log10.debug("set:write", {
48017
48174
  traitName,
48018
48175
  field,
48019
48176
  value: JSON.stringify(value),
@@ -48053,17 +48210,20 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48053
48210
  const executor = new EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
48054
48211
  try {
48055
48212
  await executor.executeAll(effects);
48056
- log9.debug("effects:executed", () => ({
48213
+ if (sharedKey !== void 0) {
48214
+ sharedEntityStore.commit(sharedKey, liveEntity);
48215
+ }
48216
+ log10.debug("effects:executed", () => ({
48057
48217
  traitName,
48058
48218
  transition: `${previousState}->${newState}`,
48059
48219
  event: flushEvent,
48060
48220
  effectCount: effects.length,
48061
48221
  emitted: emittedDuringExec.join(","),
48062
- entityAfter: JSON.stringify(traitFieldStatesRef.current.get(traitName) ?? {}),
48222
+ entityAfter: JSON.stringify(liveEntity ?? {}),
48063
48223
  slotsTouched: Array.from(pendingSlots.keys()).join(",")
48064
48224
  }));
48065
48225
  for (const [slot, patterns] of pendingSlots) {
48066
- log9.debug("flush:slot", {
48226
+ log10.debug("flush:slot", {
48067
48227
  traitName,
48068
48228
  slot,
48069
48229
  patternCount: patterns.length,
@@ -48078,7 +48238,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48078
48238
  });
48079
48239
  }
48080
48240
  } catch (error) {
48081
- log9.error("effects:error", {
48241
+ log10.error("effects:error", {
48082
48242
  traitName,
48083
48243
  transition: `${previousState}->${newState}`,
48084
48244
  event: flushEvent,
@@ -48087,14 +48247,16 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48087
48247
  });
48088
48248
  }
48089
48249
  return emittedDuringExec;
48090
- }, [eventBus, flushSlot]);
48250
+ }, [eventBus, flushSlot, sharedEntityStore]);
48091
48251
  const runTickEffects = useCallback((tick, binding) => {
48092
48252
  const traitName = binding.trait.name;
48093
48253
  const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
48094
48254
  if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return;
48095
48255
  if (tick.guard !== void 0) {
48256
+ const sharedKey = sharedKeyByTraitNameRef.current.get(traitName);
48257
+ const entity = sharedKey !== void 0 ? sharedEntityStore.getSnapshot(sharedKey) : traitFieldStatesRef.current.get(traitName) ?? {};
48096
48258
  const guardCtx = {
48097
- entity: traitFieldStatesRef.current.get(traitName) ?? {},
48259
+ entity,
48098
48260
  payload: {},
48099
48261
  state: currentState
48100
48262
  };
@@ -48119,10 +48281,46 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48119
48281
  syncOnly: true,
48120
48282
  log: tickLog
48121
48283
  });
48122
- }, [executeTransitionEffects]);
48284
+ }, [executeTransitionEffects, sharedEntityStore]);
48285
+ const emitFromSharedWriter = useCallback((event, payload) => {
48286
+ const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
48287
+ eventBus.emit(prefixedEvent, payload);
48288
+ }, [eventBus]);
48123
48289
  useEffect(() => {
48124
48290
  const scheduler = createTickScheduler();
48291
+ const writerTraitNames = /* @__PURE__ */ new Set();
48292
+ for (const group of sharedGroups.values()) {
48293
+ const ticksByInterval = /* @__PURE__ */ new Map();
48294
+ for (const binding of group.writerBindings) {
48295
+ writerTraitNames.add(binding.trait.name);
48296
+ for (const tick of binding.trait.ticks ?? []) {
48297
+ const intervalKey = String(tick.interval);
48298
+ const entries = ticksByInterval.get(intervalKey) ?? [];
48299
+ entries.push({ binding, tick });
48300
+ ticksByInterval.set(intervalKey, entries);
48301
+ }
48302
+ }
48303
+ for (const entries of ticksByInterval.values()) {
48304
+ const interval = entries[0].tick.interval;
48305
+ const onDue = () => {
48306
+ const writers = entries.map(
48307
+ ({ binding, tick }) => createSharedEntityWriter(binding, tick, traitStatesRef, emitFromSharedWriter)
48308
+ );
48309
+ runTickFrame(group.storeKey, writers, sharedEntityStore);
48310
+ };
48311
+ if (interval === "frame") {
48312
+ scheduler.add(0, onDue);
48313
+ } else if (typeof interval === "number") {
48314
+ scheduler.add(interval, onDue);
48315
+ } else if (isValidCronExpression(interval)) {
48316
+ scheduler.addCron(interval, onDue);
48317
+ } else {
48318
+ scheduler.add(parseDurationString(interval), onDue);
48319
+ }
48320
+ }
48321
+ }
48125
48322
  for (const binding of traitBindings) {
48323
+ if (writerTraitNames.has(binding.trait.name)) continue;
48126
48324
  for (const tick of binding.trait.ticks ?? []) {
48127
48325
  if (tick.interval === "frame") {
48128
48326
  scheduler.add(0, () => runTickEffects(tick, binding));
@@ -48136,7 +48334,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
48136
48334
  }
48137
48335
  }
48138
48336
  return () => scheduler.stopAll();
48139
- }, [traitBindings, runTickEffects]);
48337
+ }, [traitBindings, runTickEffects, sharedGroups, sharedEntityStore, emitFromSharedWriter]);
48140
48338
  const processEventQueued = useCallback(async (eventKey, payload) => {
48141
48339
  const normalizedEvent = normalizeEventKey(eventKey);
48142
48340
  const bindings = traitBindingsRef.current;
@@ -48656,7 +48854,7 @@ function buildOrbitalsByTrait(schema, resolvedPages = []) {
48656
48854
  // runtime/OrbPreview.tsx
48657
48855
  init_verificationRegistry();
48658
48856
  var PERF_NAMESPACE = "almadar:perf:canvas";
48659
- var log8 = createLogger(PERF_NAMESPACE);
48857
+ var log9 = createLogger(PERF_NAMESPACE);
48660
48858
  var RING_SIZE = 50;
48661
48859
  var ring = [];
48662
48860
  var writeIdx = 0;
@@ -48711,7 +48909,7 @@ function perfEnd(name, startToken, detail) {
48711
48909
  }
48712
48910
  }
48713
48911
  push({ name, durationMs, ts: endTs, detail });
48714
- log8.debug(name, () => ({ durationMs, ...detail ?? {} }));
48912
+ log9.debug(name, () => ({ durationMs, ...detail ?? {} }));
48715
48913
  }
48716
48914
  function perfTime(name, fn, detail) {
48717
48915
  const t = perfStart(name);
@@ -48729,7 +48927,7 @@ var profilerOnRender = (id, phase, actualDuration, baseDuration, _startTime, com
48729
48927
  ts: commitTime,
48730
48928
  detail: { baseDuration }
48731
48929
  });
48732
- log8.debug(`profiler:${id}:${phase}`, () => ({ actualDuration, baseDuration }));
48930
+ log9.debug(`profiler:${id}:${phase}`, () => ({ actualDuration, baseDuration }));
48733
48931
  };
48734
48932
  function getPerfSnapshot() {
48735
48933
  if (ring.length < RING_SIZE) return ring.slice();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "5.101.0",
3
+ "version": "5.103.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -123,7 +123,7 @@
123
123
  "lint": "eslint --no-warn-ignored --max-warnings 0 ."
124
124
  },
125
125
  "dependencies": {
126
- "@almadar/core": "^10.15.0",
126
+ "@almadar/core": "^10.16.0",
127
127
  "@almadar/evaluator": ">=2.9.2",
128
128
  "@almadar/logger": "^1.3.0",
129
129
  "@almadar/patterns": ">=2.35.0",