@almadar/ui 4.6.13 → 4.7.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.
@@ -1071,13 +1071,23 @@ function SlotsProvider({ children }) {
1071
1071
  const setSlotPatterns = useCallback((slot, patterns, source) => {
1072
1072
  const sourceKey = source?.trait ?? DEFAULT_SOURCE_KEY;
1073
1073
  const entityProp = patterns[0]?.pattern && typeof patterns[0].pattern === "object" ? patterns[0].pattern.entity : void 0;
1074
+ const firstPatternType = patterns[0]?.pattern && typeof patterns[0].pattern === "object" ? patterns[0].pattern.type : void 0;
1074
1075
  slotLog.debug("setSlotPatterns", {
1075
1076
  slot,
1076
1077
  sourceKey,
1077
1078
  patternCount: patterns.length,
1078
- firstPatternType: patterns[0]?.pattern && typeof patterns[0].pattern === "object" ? patterns[0].pattern.type : void 0,
1079
+ firstPatternType,
1079
1080
  entityRefId: refId(entityProp)
1080
1081
  });
1082
+ if (source?.trait) {
1083
+ xOrbitalLog.info("slot-set", {
1084
+ slot,
1085
+ sourceTrait: source.trait,
1086
+ patternCount: patterns.length,
1087
+ firstPatternType,
1088
+ state: source.state
1089
+ });
1090
+ }
1081
1091
  setSlots((prev) => {
1082
1092
  const prevSlot = prev[slot] ?? {};
1083
1093
  return {
@@ -1099,6 +1109,7 @@ function SlotsProvider({ children }) {
1099
1109
  });
1100
1110
  }, []);
1101
1111
  const clearSlotForSource = useCallback((slot, sourceTrait) => {
1112
+ xOrbitalLog.info("slot-clear-source", { slot, sourceTrait });
1102
1113
  setSlots((prev) => {
1103
1114
  const existing = prev[slot];
1104
1115
  if (!existing || !(sourceTrait in existing)) return prev;
@@ -1139,11 +1150,12 @@ function useSlotsActions() {
1139
1150
  }
1140
1151
  return actions;
1141
1152
  }
1142
- var slotLog, refIds, nextRefId, DEFAULT_SOURCE_KEY, SlotsStateContext, SlotsActionsContext;
1153
+ var slotLog, xOrbitalLog, refIds, nextRefId, DEFAULT_SOURCE_KEY, SlotsStateContext, SlotsActionsContext;
1143
1154
  var init_SlotsContext = __esm({
1144
1155
  "runtime/ui/SlotsContext.tsx"() {
1145
1156
  init_logger();
1146
1157
  slotLog = createLogger("almadar:ui:slot-render");
1158
+ xOrbitalLog = createLogger("almadar:runtime:cross-orbital");
1147
1159
  refIds = /* @__PURE__ */ new WeakMap();
1148
1160
  nextRefId = 1;
1149
1161
  DEFAULT_SOURCE_KEY = "__default__";
@@ -8137,7 +8149,7 @@ var init_MapView = __esm({
8137
8149
  shadowSize: [41, 41]
8138
8150
  });
8139
8151
  L.Marker.prototype.options.icon = defaultIcon;
8140
- const { useEffect: useEffect68, useRef: useRef65, useCallback: useCallback110, useState: useState103 } = React115__default;
8152
+ const { useEffect: useEffect68, useRef: useRef65, useCallback: useCallback110, useState: useState102 } = React115__default;
8141
8153
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
8142
8154
  const { useEventBus: useEventBus2 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
8143
8155
  function MapUpdater({ centerLat, centerLng, zoom }) {
@@ -8181,7 +8193,7 @@ var init_MapView = __esm({
8181
8193
  showAttribution = true
8182
8194
  }) {
8183
8195
  const eventBus = useEventBus2();
8184
- const [clickedPosition, setClickedPosition] = useState103(null);
8196
+ const [clickedPosition, setClickedPosition] = useState102(null);
8185
8197
  const handleMapClick = useCallback110((lat, lng) => {
8186
8198
  if (showClickedPin) {
8187
8199
  setClickedPosition({ lat, lng });
@@ -38134,6 +38146,7 @@ function useTraitStateMachine(traitBindings, slotsActions, options) {
38134
38146
  const eventBus = useEventBus();
38135
38147
  const { entities } = useEntitySchema();
38136
38148
  const traitConfigsByName = options?.traitConfigsByName;
38149
+ const orbitalsByTrait = options?.orbitalsByTrait;
38137
38150
  const manager = useMemo(() => {
38138
38151
  const traitDefs = traitBindings.map(toTraitDefinition);
38139
38152
  const m = new StateMachineManager(traitDefs);
@@ -38346,24 +38359,7 @@ function useTraitStateMachine(traitBindings, slotsActions, options) {
38346
38359
  const actions = slotsActionsRef.current;
38347
38360
  console.log("[TraitStateMachine] Processing event:", normalizedEvent, "payload:", payload);
38348
38361
  const bindingMap = new Map(bindings.map((b) => [b.trait.name, b]));
38349
- for (const traitName of bindingMap.keys()) {
38350
- const traitState = currentManager.getState(traitName);
38351
- eventBus.emit(`${traitName}:DISPATCH`, {
38352
- event: normalizedEvent,
38353
- payload,
38354
- currentState: traitState?.currentState
38355
- });
38356
- }
38357
38362
  const results = currentManager.sendEvent(normalizedEvent, payload);
38358
- for (const { traitName, result } of results) {
38359
- const suffix = result.executed ? "SUCCESS" : "ERROR";
38360
- eventBus.emit(`${traitName}:${normalizedEvent}:${suffix}`, {
38361
- event: normalizedEvent,
38362
- payload,
38363
- newState: result.newState,
38364
- currentState: result.previousState
38365
- });
38366
- }
38367
38363
  const emittedByTrait = /* @__PURE__ */ new Map();
38368
38364
  for (const { traitName, result } of results) {
38369
38365
  const binding = bindingMap.get(traitName);
@@ -38599,7 +38595,10 @@ function useTraitStateMachine(traitBindings, slotsActions, options) {
38599
38595
  if (!LIFECYCLE_EVENTS.has(normalizedEvent)) {
38600
38596
  for (const { traitName, result } of results) {
38601
38597
  if (!result.executed) continue;
38602
- eventBus.emit(normalizedEvent, payload, {
38598
+ const orbitalName = orbitalsByTrait?.[traitName];
38599
+ if (!orbitalName) continue;
38600
+ eventBus.emit(`UI:${orbitalName}.${traitName}.${normalizedEvent}`, payload, {
38601
+ orbital: orbitalName,
38603
38602
  trait: traitName,
38604
38603
  fromBridge: true
38605
38604
  });
@@ -38610,7 +38609,13 @@ function useTraitStateMachine(traitBindings, slotsActions, options) {
38610
38609
  }
38611
38610
  const onEventProcessed = optionsRef.current?.onEventProcessed;
38612
38611
  if (onEventProcessed) {
38613
- await onEventProcessed(normalizedEvent, payload);
38612
+ const dispatchedOrbitals = /* @__PURE__ */ new Set();
38613
+ for (const { traitName, result } of results) {
38614
+ if (!result.executed) continue;
38615
+ const orbital = orbitalsByTrait?.[traitName];
38616
+ if (orbital) dispatchedOrbitals.add(orbital);
38617
+ }
38618
+ await onEventProcessed(normalizedEvent, payload, dispatchedOrbitals);
38614
38619
  }
38615
38620
  }, [entities, eventBus]);
38616
38621
  const drainEventQueue = useCallback(async () => {
@@ -38654,26 +38659,34 @@ function useTraitStateMachine(traitBindings, slotsActions, options) {
38654
38659
  }
38655
38660
  console.log("[TraitStateMachine] Subscribing to events:", Array.from(allEvents));
38656
38661
  const unsubscribes = [];
38657
- for (const eventKey of allEvents) {
38658
- if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
38659
- continue;
38662
+ for (const binding of traitBindings) {
38663
+ const traitName = binding.trait.name;
38664
+ const orbitalName = orbitalsByTrait?.[traitName];
38665
+ if (!orbitalName) continue;
38666
+ for (const transition of binding.trait.transitions) {
38667
+ const eventKey = transition.event;
38668
+ if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
38669
+ continue;
38670
+ }
38671
+ const unsub = eventBus.on(`UI:${orbitalName}.${traitName}.${eventKey}`, (event) => {
38672
+ if (event.source && event.source.fromBridge) {
38673
+ return;
38674
+ }
38675
+ enqueueAndDrain(eventKey, event.payload);
38676
+ });
38677
+ unsubscribes.push(unsub);
38660
38678
  }
38661
- const unsub = eventBus.on(`UI:${eventKey}`, (event) => {
38662
- console.log("[TraitStateMachine] Received event:", `UI:${eventKey}`, event);
38663
- enqueueAndDrain(eventKey, event.payload);
38664
- });
38665
- unsubscribes.push(unsub);
38666
38679
  }
38667
38680
  for (const binding of traitBindings) {
38681
+ const ownOrbital = orbitalsByTrait?.[binding.trait.name];
38668
38682
  const listens = binding.trait.listens ?? [];
38669
38683
  for (const listen of listens) {
38670
- const expectedTrait = listen.source?.trait;
38671
- const unsub = eventBus.on(listen.event, (event) => {
38672
- if (expectedTrait) {
38673
- const emitTrait = event.source?.trait;
38674
- if (emitTrait !== expectedTrait) return;
38675
- }
38676
- console.log("[TraitStateMachine] listens", binding.trait.name, listen.event, "\u2192", listen.triggers, "from", event.source?.trait);
38684
+ const sourceTrait = listen.source?.trait;
38685
+ if (!sourceTrait) continue;
38686
+ const sourceOrbital = listen.source?.orbital ?? ownOrbital;
38687
+ if (!sourceOrbital) continue;
38688
+ const busKey = `UI:${sourceOrbital}.${sourceTrait}.${listen.event}`;
38689
+ const unsub = eventBus.on(busKey, (event) => {
38677
38690
  enqueueAndDrain(listen.triggers, event.payload);
38678
38691
  });
38679
38692
  unsubscribes.push(unsub);
@@ -38820,6 +38833,8 @@ init_EntitySchemaContext();
38820
38833
 
38821
38834
  // runtime/ServerBridge.tsx
38822
38835
  init_useEventBus();
38836
+ init_logger();
38837
+ var xOrbitalLog2 = createLogger("almadar:runtime:cross-orbital");
38823
38838
  var ServerBridgeContext = createContext(null);
38824
38839
  function useServerBridge() {
38825
38840
  const ctx = useContext(ServerBridgeContext);
@@ -38897,8 +38912,22 @@ function ServerBridgeProvider({
38897
38912
  }
38898
38913
  if (result.emittedEvents) {
38899
38914
  for (const emitted of result.emittedEvents) {
38900
- eventBus.emit(`UI:${emitted.event}`, emitted.payload);
38901
- eventBus.emit(emitted.event, emitted.payload);
38915
+ const evTrait = emitted.source?.trait;
38916
+ if (!evTrait) {
38917
+ xOrbitalLog2.warn("emit:dropped-no-source", {
38918
+ event: emitted.event,
38919
+ dispatchOrbital: orbitalName
38920
+ });
38921
+ continue;
38922
+ }
38923
+ const key = emitted.source?.orbital ? `UI:${emitted.source.orbital}.${evTrait}.${emitted.event}` : `UI:${evTrait}.${emitted.event}`;
38924
+ xOrbitalLog2.info("emit:rebroadcast", {
38925
+ busKey: key,
38926
+ sourceOrbital: emitted.source?.orbital,
38927
+ sourceTrait: evTrait,
38928
+ dispatchOrbital: orbitalName
38929
+ });
38930
+ eventBus.emit(key, emitted.payload);
38902
38931
  }
38903
38932
  }
38904
38933
  } else if (result.error) {
@@ -39019,6 +39048,10 @@ function prepareSchemaForPreview(input) {
39019
39048
  const schema = adjustSchemaForMockData(parsed, mockData);
39020
39049
  return { schema, mockData };
39021
39050
  }
39051
+
39052
+ // runtime/OrbPreview.tsx
39053
+ init_logger();
39054
+ var xOrbitalLog3 = createLogger("almadar:runtime:cross-orbital");
39022
39055
  function normalizeChild(child) {
39023
39056
  if (typeof child === "string") return child;
39024
39057
  if (child === null || typeof child !== "object" || Array.isArray(child)) {
@@ -39098,6 +39131,11 @@ function applyServerEffects(effects, uiSlots, onNavigate) {
39098
39131
  const patternRecord = eff.pattern;
39099
39132
  const { type: patternType, children, ...inlineProps } = patternRecord;
39100
39133
  const normalizedChildren = Array.isArray(children) ? children.map((c) => normalizeChild(c)) : children;
39134
+ xOrbitalLog3.info("slot-write", {
39135
+ slot: eff.slot,
39136
+ sourceTrait: eff.traitName ?? "server",
39137
+ patternType: typeof patternType === "string" ? patternType : void 0
39138
+ });
39101
39139
  uiSlots.render({
39102
39140
  target: eff.slot,
39103
39141
  pattern: patternType,
@@ -39112,19 +39150,26 @@ function applyServerEffects(effects, uiSlots, onNavigate) {
39112
39150
  }
39113
39151
  }
39114
39152
  }
39115
- function TraitInitializer({ traits: traits2, orbitalNames, onNavigate, onLocalFallback, persistence, traitConfigsByName }) {
39153
+ function TraitInitializer({ traits: traits2, orbitalNames, onNavigate, onLocalFallback, persistence, traitConfigsByName, orbitalsByTrait }) {
39116
39154
  const slotsActions = useSlotsActions();
39117
39155
  const bridge = useServerBridge();
39118
39156
  const uiSlots = useUISlots();
39119
- const onEventProcessed = useCallback(async (event, payload) => {
39157
+ const onEventProcessed = useCallback(async (event, payload, dispatchedOrbitals) => {
39120
39158
  if (!bridge.connected || !orbitalNames?.length) return;
39121
- for (const name of orbitalNames) {
39159
+ const targets = dispatchedOrbitals && dispatchedOrbitals.size > 0 ? orbitalNames.filter((n) => dispatchedOrbitals.has(n)) : orbitalNames;
39160
+ xOrbitalLog3.info("TraitInitializer:fanout", {
39161
+ event,
39162
+ sentTo: targets,
39163
+ skipped: orbitalNames.filter((n) => !targets.includes(n)),
39164
+ dispatchedOrbitalsSize: dispatchedOrbitals?.size ?? 0
39165
+ });
39166
+ for (const name of targets) {
39122
39167
  const { effects, meta } = await bridge.sendEvent(name, event, payload);
39123
39168
  recordServerResponse(name, event, meta);
39124
39169
  applyServerEffects(effects, uiSlots, onNavigate);
39125
39170
  }
39126
39171
  }, [bridge.connected, bridge.sendEvent, orbitalNames, uiSlots, onNavigate]);
39127
- const opts = orbitalNames ? { onEventProcessed, navigate: onNavigate, traitConfigsByName } : { navigate: onNavigate, persistence, traitConfigsByName };
39172
+ const opts = orbitalNames ? { onEventProcessed, navigate: onNavigate, traitConfigsByName, orbitalsByTrait } : { navigate: onNavigate, persistence, traitConfigsByName, orbitalsByTrait };
39128
39173
  const { sendEvent } = useTraitStateMachine(traits2, slotsActions, opts);
39129
39174
  const initSentRef = useRef(false);
39130
39175
  useEffect(() => {
@@ -39174,27 +39219,68 @@ function SchemaRunner({ schema, serverUrl, mockData, pageName, onNavigate, onLoc
39174
39219
  const allPageTraits = useMemo(() => {
39175
39220
  if (pageName && traits2.length > 0) return traits2;
39176
39221
  if (!ir?.pages || ir.pages.size <= 1) return traits2;
39177
- const combined = [];
39222
+ const firstPage = ir.pages.values().next().value;
39223
+ if (!firstPage) return traits2;
39224
+ const firstPageTraits = [];
39178
39225
  const seen = /* @__PURE__ */ new Set();
39179
- for (const page of ir.pages.values()) {
39180
- for (const t of page.traits) {
39181
- const binding = t;
39182
- const traitObj = binding.trait;
39183
- const name = traitObj?.name ?? binding.name ?? "";
39184
- if (name && !seen.has(name)) {
39185
- seen.add(name);
39186
- combined.push(t);
39187
- }
39226
+ for (const binding of firstPage.traits) {
39227
+ const name = binding.trait.name;
39228
+ if (name && !seen.has(name)) {
39229
+ seen.add(name);
39230
+ firstPageTraits.push(binding);
39188
39231
  }
39189
39232
  }
39190
- return combined.length > 0 ? combined : traits2;
39233
+ return firstPageTraits.length > 0 ? firstPageTraits : traits2;
39191
39234
  }, [ir, traits2, pageName]);
39192
- const orbitalNames = useMemo(() => {
39235
+ useMemo(() => {
39193
39236
  const parsed = schema;
39194
39237
  const orbitals = parsed?.orbitals;
39195
39238
  if (!orbitals) return [];
39196
39239
  return orbitals.filter((o) => typeof o.name === "string").map((o) => o.name);
39197
39240
  }, [schema]);
39241
+ const orbitalsByTrait = useMemo(() => {
39242
+ const map = {};
39243
+ const parsed = schema;
39244
+ if (!parsed?.orbitals) return map;
39245
+ for (const orb of parsed.orbitals) {
39246
+ for (const traitRef of orb.traits) {
39247
+ let traitName;
39248
+ if (typeof traitRef === "string") {
39249
+ const parts = traitRef.split(".");
39250
+ traitName = parts[parts.length - 1];
39251
+ } else if ("ref" in traitRef && typeof traitRef.ref === "string") {
39252
+ const parts = traitRef.ref.split(".");
39253
+ traitName = traitRef.name ?? parts[parts.length - 1];
39254
+ } else if ("name" in traitRef && typeof traitRef.name === "string") {
39255
+ traitName = traitRef.name;
39256
+ }
39257
+ if (traitName) map[traitName] = orb.name;
39258
+ }
39259
+ }
39260
+ return map;
39261
+ }, [schema]);
39262
+ const pageOrbitalNames = useMemo(() => {
39263
+ const set = /* @__PURE__ */ new Set();
39264
+ for (const binding of allPageTraits) {
39265
+ const orb = orbitalsByTrait[binding.trait.name];
39266
+ if (orb) set.add(orb);
39267
+ }
39268
+ return Array.from(set);
39269
+ }, [allPageTraits, orbitalsByTrait]);
39270
+ useEffect(() => {
39271
+ const traitNames = allPageTraits.map((b) => b.trait.name);
39272
+ const orbitalsByTraitForPage = {};
39273
+ for (const name of traitNames) {
39274
+ const orb = orbitalsByTrait[name];
39275
+ if (orb) orbitalsByTraitForPage[name] = orb;
39276
+ }
39277
+ xOrbitalLog3.info("SchemaRunner:mount", {
39278
+ pageName,
39279
+ traitNames,
39280
+ orbitalsByTraitForPage,
39281
+ pageOrbitalNames: pageOrbitalNames.join(",")
39282
+ });
39283
+ }, [pageName, allPageTraits, orbitalsByTrait, pageOrbitalNames]);
39198
39284
  const traitConfigsByName = useMemo(() => {
39199
39285
  const map = {};
39200
39286
  const parsed = schema;
@@ -39235,8 +39321,9 @@ function SchemaRunner({ schema, serverUrl, mockData, pageName, onNavigate, onLoc
39235
39321
  TraitInitializer,
39236
39322
  {
39237
39323
  traits: allPageTraits,
39238
- orbitalNames: serverUrl ? orbitalNames : void 0,
39324
+ orbitalNames: serverUrl ? pageOrbitalNames : void 0,
39239
39325
  traitConfigsByName,
39326
+ orbitalsByTrait,
39240
39327
  onNavigate,
39241
39328
  onLocalFallback,
39242
39329
  persistence
@@ -39258,7 +39345,8 @@ function OrbPreview({
39258
39345
  autoMock = false,
39259
39346
  height = "400px",
39260
39347
  className,
39261
- serverUrl
39348
+ serverUrl,
39349
+ initialPagePath
39262
39350
  }) {
39263
39351
  const [localFallback, setLocalFallback] = useState(false);
39264
39352
  const eventBus = useEventBus();
@@ -39304,7 +39392,17 @@ function OrbPreview({
39304
39392
  return [];
39305
39393
  }
39306
39394
  }, [parsedSchema]);
39307
- const [currentPage, setCurrentPage] = useState(void 0);
39395
+ const initialPageName = useMemo(() => {
39396
+ if (!initialPagePath) return void 0;
39397
+ const match = pages.find(({ page }) => page.path === initialPagePath);
39398
+ return match?.page.name;
39399
+ }, [pages, initialPagePath]);
39400
+ const [currentPage, setCurrentPage] = useState(initialPageName);
39401
+ useEffect(() => {
39402
+ if (initialPageName && initialPageName !== currentPage) {
39403
+ setCurrentPage(initialPageName);
39404
+ }
39405
+ }, [initialPageName, currentPage]);
39308
39406
  const handleNavigate = useCallback((path) => {
39309
39407
  const match = pages.find(({ page }) => page.path === path);
39310
39408
  if (match) {
@@ -32,8 +32,18 @@ export interface TraitStateMachineResult {
32
32
  canHandleEvent: (traitName: string, eventKey: string) => boolean;
33
33
  }
34
34
  export interface UseTraitStateMachineOptions {
35
- /** Callback invoked after each event is processed (for server forwarding) */
36
- onEventProcessed?: (eventKey: string, payload?: EventPayload) => void | Promise<void>;
35
+ /**
36
+ * Callback invoked after each event is processed (for server forwarding).
37
+ *
38
+ * `dispatchedOrbitals` is the set of orbital names whose traits actually
39
+ * executed a transition for this event. The server-bridge fan-out should
40
+ * be scoped to this set; sending the event to other orbitals fires
41
+ * same-named transitions in unrelated orbitals and stacks their UI into
42
+ * the wrong slot (gap #11 in `docs/Almadar_Std_Verification.md`). Empty
43
+ * set means no local transition matched — leave the legacy fallback to
44
+ * the consumer.
45
+ */
46
+ onEventProcessed?: (eventKey: string, payload?: EventPayload, dispatchedOrbitals?: Set<string>) => void | Promise<void>;
37
47
  /** Router navigate function for navigate effects */
38
48
  navigate?: (path: string, params?: Record<string, unknown>) => void;
39
49
  /** Notification function for notify effects */
@@ -57,6 +67,15 @@ export interface UseTraitStateMachineOptions {
57
67
  * caller assembles this map from the orbital schema directly.
58
68
  */
59
69
  traitConfigsByName?: Record<string, import('@almadar/core').TraitConfig>;
70
+ /**
71
+ * Trait → orbital map built from `schema.orbitals[].traits[]` by the
72
+ * caller. Threaded down so bus emits (`UI:Orbital.Trait.Event`) and
73
+ * subscriptions can carry the qualified `Orbital.Trait` scope —
74
+ * gap #13 unification. Same mechanism as `traitConfigsByName`: the
75
+ * orbital boundary doesn't propagate through `@almadar/core`'s
76
+ * `ResolvedTraitBinding`, so the caller assembles the map directly.
77
+ */
78
+ orbitalsByTrait?: Record<string, string>;
60
79
  }
61
80
  /**
62
81
  * useTraitStateMachine - Manages state machines for multiple traits
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "4.6.13",
3
+ "version": "4.7.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "main": "./dist/components/index.js",
@@ -121,7 +121,7 @@
121
121
  "@almadar/core": "^7.0.0",
122
122
  "@almadar/evaluator": ">=2.9.2",
123
123
  "@almadar/patterns": ">=2.17.1",
124
- "@almadar/runtime": "^5.3.0",
124
+ "@almadar/runtime": "^5.5.0",
125
125
  "@almadar/std": ">=6.4.1",
126
126
  "@almadar/syntax": ">=1.3.1",
127
127
  "@xyflow/react": "12.10.1",