@almadar/ui 2.8.1 → 2.9.1

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.
@@ -0,0 +1,471 @@
1
+ import '../chunk-GTIAVPI5.js';
2
+ import '../chunk-N7MVUW4R.js';
3
+ import '../chunk-3HJHHULT.js';
4
+ import { useFetchedDataContext } from '../chunk-7AKZ7SRV.js';
5
+ import '../chunk-P6NZVIE5.js';
6
+ import '../chunk-DKQN5FVU.js';
7
+ import '../chunk-WGJIL4YR.js';
8
+ import { useEventBus } from '../chunk-YXZM3WCF.js';
9
+ import '../chunk-3JGAROCW.js';
10
+ import '../chunk-6D5QMEUS.js';
11
+ import '../chunk-TSETXL2E.js';
12
+ import '../chunk-K2D5D3WK.js';
13
+ import '../chunk-PKBMQBKP.js';
14
+ import { createContext, useMemo, useContext, useState, useRef, useEffect, useCallback } from 'react';
15
+ import { isCircuitEvent, schemaToIR, getPage, clearSchemaCache as clearSchemaCache$1 } from '@almadar/core';
16
+ import { StateMachineManager, EffectExecutor } from '@almadar/runtime';
17
+ import { isEntityAwarePattern } from '@almadar/patterns';
18
+ import { jsx } from 'react/jsx-runtime';
19
+
20
+ // runtime/createClientEffectHandlers.ts
21
+ function createClientEffectHandlers(options) {
22
+ const { eventBus, slotSetter, navigate, notify, enrichPattern } = options;
23
+ return {
24
+ emit: (event, payload) => {
25
+ const prefixedEvent = event.startsWith("UI:") ? event : `UI:${event}`;
26
+ eventBus.emit(prefixedEvent, { payload });
27
+ },
28
+ persist: async () => {
29
+ console.warn("[ClientEffectHandlers] persist is server-side only, ignored on client");
30
+ },
31
+ set: () => {
32
+ console.warn("[ClientEffectHandlers] set is server-side only, ignored on client");
33
+ },
34
+ callService: async () => {
35
+ console.warn("[ClientEffectHandlers] callService is server-side only, ignored on client");
36
+ return {};
37
+ },
38
+ renderUI: (slot, pattern, props) => {
39
+ if (pattern === null) {
40
+ slotSetter.clearSlot(slot);
41
+ return;
42
+ }
43
+ const enriched = enrichPattern ? enrichPattern(pattern) : pattern;
44
+ slotSetter.addPattern(slot, enriched, props);
45
+ },
46
+ navigate: navigate ?? ((path) => {
47
+ console.warn("[ClientEffectHandlers] No navigate handler, ignoring:", path);
48
+ }),
49
+ notify: notify ?? ((msg, type) => {
50
+ console.log(`[ClientEffectHandlers] notify (${type}):`, msg);
51
+ })
52
+ };
53
+ }
54
+ var EntitySchemaContext = createContext(null);
55
+ function EntitySchemaProvider({
56
+ entities,
57
+ children
58
+ }) {
59
+ const entitiesMap = useMemo(() => {
60
+ const map = /* @__PURE__ */ new Map();
61
+ for (const entity of entities) {
62
+ map.set(entity.name, entity);
63
+ }
64
+ return map;
65
+ }, [entities]);
66
+ const contextValue = useMemo(
67
+ () => ({
68
+ entities: entitiesMap
69
+ }),
70
+ [entitiesMap]
71
+ );
72
+ return /* @__PURE__ */ jsx(EntitySchemaContext.Provider, { value: contextValue, children });
73
+ }
74
+ function useEntitySchema() {
75
+ const context = useContext(EntitySchemaContext);
76
+ if (!context) {
77
+ throw new Error("useEntitySchema must be used within an EntitySchemaProvider");
78
+ }
79
+ return context;
80
+ }
81
+ function useEntityDefinition(entityName) {
82
+ const { entities } = useEntitySchema();
83
+ return entities.get(entityName);
84
+ }
85
+
86
+ // runtime/useTraitStateMachine.ts
87
+ function toTraitDefinition(binding) {
88
+ return {
89
+ name: binding.trait.name,
90
+ states: binding.trait.states,
91
+ transitions: binding.trait.transitions,
92
+ listens: binding.trait.listens
93
+ };
94
+ }
95
+ function normalizeEventKey(eventKey) {
96
+ return eventKey.startsWith("UI:") ? eventKey.slice(3) : eventKey;
97
+ }
98
+ function useTraitStateMachine(traitBindings, slotsActions, options) {
99
+ const eventBus = useEventBus();
100
+ const { entities } = useEntitySchema();
101
+ const fetchedDataContext = useFetchedDataContext();
102
+ const manager = useMemo(() => {
103
+ const traitDefs = traitBindings.map(toTraitDefinition);
104
+ return new StateMachineManager(traitDefs);
105
+ }, [traitBindings]);
106
+ const [traitStates, setTraitStates] = useState(() => {
107
+ return manager.getAllStates();
108
+ });
109
+ const traitBindingsRef = useRef(traitBindings);
110
+ const managerRef = useRef(manager);
111
+ const slotsActionsRef = useRef(slotsActions);
112
+ const optionsRef = useRef(options);
113
+ useEffect(() => {
114
+ traitBindingsRef.current = traitBindings;
115
+ }, [traitBindings]);
116
+ useEffect(() => {
117
+ managerRef.current = manager;
118
+ setTraitStates(manager.getAllStates());
119
+ }, [manager]);
120
+ useEffect(() => {
121
+ slotsActionsRef.current = slotsActions;
122
+ }, [slotsActions]);
123
+ useEffect(() => {
124
+ optionsRef.current = options;
125
+ }, [options]);
126
+ useEffect(() => {
127
+ const newManager = managerRef.current;
128
+ newManager.resetAll();
129
+ setTraitStates(newManager.getAllStates());
130
+ console.log(
131
+ "[TraitStateMachine] Reset states for page navigation:",
132
+ Array.from(newManager.getAllStates().keys()).join(", ")
133
+ );
134
+ }, [traitBindings]);
135
+ const processEvent = useCallback((eventKey, payload) => {
136
+ const normalizedEvent = normalizeEventKey(eventKey);
137
+ const bindings = traitBindingsRef.current;
138
+ const currentManager = managerRef.current;
139
+ const actions = slotsActionsRef.current;
140
+ console.log("[TraitStateMachine] Processing event:", normalizedEvent, "payload:", payload);
141
+ const bindingMap = new Map(bindings.map((b) => [b.trait.name, b]));
142
+ const results = currentManager.sendEvent(normalizedEvent, payload);
143
+ for (const { traitName, result } of results) {
144
+ const binding = bindingMap.get(traitName);
145
+ const traitState = currentManager.getState(traitName);
146
+ if (!binding || !traitState) continue;
147
+ if (result.executed && result.effects.length > 0) {
148
+ console.log(
149
+ "[TraitStateMachine] Executing",
150
+ result.effects.length,
151
+ "effects for",
152
+ traitName,
153
+ "| linkedEntity:",
154
+ binding.linkedEntity,
155
+ "| transition:",
156
+ `${result.previousState} -> ${result.newState}`,
157
+ "| effects:",
158
+ JSON.stringify(result.effects)
159
+ );
160
+ const linkedEntity = binding.linkedEntity || "";
161
+ const entityId = payload?.entityId;
162
+ const entityData = linkedEntity && entityId && fetchedDataContext ? fetchedDataContext.getById(linkedEntity, entityId) || {} : payload || {};
163
+ const pendingSlots = /* @__PURE__ */ new Map();
164
+ const slotSource = {
165
+ trait: binding.trait.name,
166
+ state: result.previousState,
167
+ transition: `${result.previousState}->${result.newState}`,
168
+ effects: result.effects,
169
+ traitDefinition: binding.trait
170
+ };
171
+ const handlers = createClientEffectHandlers({
172
+ eventBus,
173
+ slotSetter: {
174
+ addPattern: (slot, pattern, props) => {
175
+ const existing = pendingSlots.get(slot) || [];
176
+ existing.push({ pattern, props: props || {} });
177
+ pendingSlots.set(slot, existing);
178
+ },
179
+ clearSlot: (slot) => {
180
+ pendingSlots.set(slot, []);
181
+ }
182
+ },
183
+ navigate: optionsRef.current?.navigate,
184
+ notify: optionsRef.current?.notify,
185
+ enrichPattern: (pattern) => {
186
+ const patternType = pattern?.type;
187
+ if (linkedEntity && patternType && isEntityAwarePattern(patternType)) {
188
+ const patternRecord = pattern;
189
+ if (!patternRecord.entity && fetchedDataContext) {
190
+ const records = fetchedDataContext.getData(linkedEntity);
191
+ if (records.length > 0) {
192
+ return { type: patternType, ...patternRecord, entity: records };
193
+ }
194
+ }
195
+ }
196
+ return pattern;
197
+ }
198
+ });
199
+ const bindingCtx = {
200
+ entity: entityData,
201
+ payload: payload || {},
202
+ state: result.previousState
203
+ };
204
+ const effectContext = {
205
+ traitName: binding.trait.name,
206
+ state: result.previousState,
207
+ transition: `${result.previousState}->${result.newState}`,
208
+ linkedEntity,
209
+ entityId
210
+ };
211
+ const executor = new EffectExecutor({ handlers, bindings: bindingCtx, context: effectContext });
212
+ executor.executeAll(result.effects).then(() => {
213
+ console.log(
214
+ "[TraitStateMachine] After executeAll, pendingSlots:",
215
+ Object.fromEntries(pendingSlots.entries())
216
+ );
217
+ for (const [slot, patterns] of pendingSlots) {
218
+ if (patterns.length === 0) {
219
+ actions.clearSlot(slot);
220
+ } else {
221
+ actions.setSlotPatterns(slot, patterns, slotSource);
222
+ }
223
+ }
224
+ }).catch((error) => {
225
+ console.error(
226
+ "[TraitStateMachine] Effect execution error:",
227
+ error,
228
+ "| effects:",
229
+ JSON.stringify(result.effects)
230
+ );
231
+ });
232
+ } else if (!result.executed) {
233
+ if (result.guardResult === false) {
234
+ console.log(
235
+ "[TraitStateMachine] Guard blocked transition:",
236
+ traitName,
237
+ result.previousState,
238
+ "->",
239
+ result.transition?.to
240
+ );
241
+ } else if (!result.transition) {
242
+ if (isCircuitEvent(normalizedEvent)) {
243
+ console.warn(
244
+ `[CLOSED CIRCUIT VIOLATION] Trait "${traitName}" in state "${traitState.currentState}" received event "${normalizedEvent}" but has no transition to handle it.
245
+ This is likely a schema issue. To fix, add a transition:
246
+ { from: "${traitState.currentState}", to: "<target_state>", event: "${normalizedEvent}", effects: [...] }
247
+ Or ensure the previous action (that opened this UI) properly transitions back before emitting this event.`
248
+ );
249
+ } else {
250
+ console.log(
251
+ "[TraitStateMachine] No transition for",
252
+ traitName,
253
+ "from state:",
254
+ traitState.currentState,
255
+ "on event:",
256
+ normalizedEvent
257
+ );
258
+ }
259
+ }
260
+ }
261
+ }
262
+ if (results.length > 0) {
263
+ setTraitStates(currentManager.getAllStates());
264
+ }
265
+ const onEventProcessed = optionsRef.current?.onEventProcessed;
266
+ if (onEventProcessed) {
267
+ onEventProcessed(normalizedEvent, payload);
268
+ }
269
+ }, [entities, fetchedDataContext, eventBus]);
270
+ const sendEvent = useCallback((eventKey, payload) => {
271
+ processEvent(eventKey, payload);
272
+ }, [processEvent]);
273
+ const getTraitState = useCallback((traitName) => {
274
+ return managerRef.current.getState(traitName);
275
+ }, []);
276
+ const canHandleEvent = useCallback((traitName, eventKey) => {
277
+ const normalizedEvent = normalizeEventKey(eventKey);
278
+ return managerRef.current.canHandleEvent(traitName, normalizedEvent);
279
+ }, []);
280
+ useEffect(() => {
281
+ const allEvents = /* @__PURE__ */ new Set();
282
+ for (const binding of traitBindings) {
283
+ for (const event of binding.trait.events) {
284
+ allEvents.add(event.key);
285
+ }
286
+ for (const transition of binding.trait.transitions) {
287
+ allEvents.add(transition.event);
288
+ }
289
+ }
290
+ console.log("[TraitStateMachine] Subscribing to events:", Array.from(allEvents));
291
+ const unsubscribes = [];
292
+ for (const eventKey of allEvents) {
293
+ if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
294
+ continue;
295
+ }
296
+ const unsub = eventBus.on(`UI:${eventKey}`, (event) => {
297
+ console.log("[TraitStateMachine] Received event:", `UI:${eventKey}`, event);
298
+ processEvent(eventKey, event.payload);
299
+ });
300
+ unsubscribes.push(unsub);
301
+ }
302
+ return () => {
303
+ for (const unsub of unsubscribes) {
304
+ unsub();
305
+ }
306
+ };
307
+ }, [traitBindings, eventBus, processEvent]);
308
+ return {
309
+ traitStates,
310
+ sendEvent,
311
+ getTraitState,
312
+ canHandleEvent
313
+ };
314
+ }
315
+ function useResolvedSchema(schema, pageName) {
316
+ const [loading, setLoading] = useState(true);
317
+ const [error, setError] = useState(null);
318
+ const ir = useMemo(() => {
319
+ if (!schema) return null;
320
+ try {
321
+ return schemaToIR(schema);
322
+ } catch (err) {
323
+ setError(err instanceof Error ? err.message : "Schema resolution failed");
324
+ return null;
325
+ }
326
+ }, [schema]);
327
+ useEffect(() => {
328
+ setLoading(false);
329
+ }, [ir]);
330
+ const result = useMemo(() => {
331
+ if (!ir) {
332
+ return {
333
+ page: void 0,
334
+ traits: [],
335
+ entities: /* @__PURE__ */ new Map(),
336
+ allEntities: /* @__PURE__ */ new Map(),
337
+ allTraits: /* @__PURE__ */ new Map(),
338
+ loading,
339
+ error: error || (schema ? null : "No schema provided"),
340
+ ir: null
341
+ };
342
+ }
343
+ const page = getPage(ir, pageName);
344
+ console.log("[useResolvedSchema] Resolved page:", page?.name, "| path:", page?.path, "| traits:", page?.traits.length);
345
+ const traits = page?.traits || [];
346
+ const entities = /* @__PURE__ */ new Map();
347
+ if (page) {
348
+ for (const binding of page.entityBindings) {
349
+ entities.set(binding.entity.name, binding.entity);
350
+ }
351
+ }
352
+ return {
353
+ page,
354
+ traits,
355
+ entities,
356
+ allEntities: ir.entities,
357
+ allTraits: ir.traits,
358
+ loading,
359
+ error,
360
+ ir
361
+ };
362
+ }, [ir, pageName, loading, error, schema]);
363
+ return result;
364
+ }
365
+ function clearSchemaCache() {
366
+ clearSchemaCache$1();
367
+ }
368
+ var TraitContext = createContext(null);
369
+ function TraitProvider({
370
+ traits: traitBindings,
371
+ children
372
+ }) {
373
+ const traitInstances = useMemo(() => {
374
+ const map = /* @__PURE__ */ new Map();
375
+ for (const binding of traitBindings) {
376
+ const trait = binding.trait;
377
+ const initialState = trait.states.find((s) => s.isInitial);
378
+ const stateName = initialState?.name || trait.states[0]?.name || "idle";
379
+ const instance = {
380
+ name: trait.name,
381
+ currentState: stateName,
382
+ availableEvents: trait.transitions.filter((t) => t.from === stateName).map((t) => t.event),
383
+ dispatch: (eventKey, payload) => {
384
+ console.log(`[TraitProvider] Dispatch to ${trait.name}: ${eventKey}`, payload);
385
+ },
386
+ canDispatch: (eventKey) => {
387
+ return trait.transitions.some(
388
+ (t) => t.from === stateName && t.event === eventKey
389
+ );
390
+ },
391
+ trait
392
+ };
393
+ map.set(trait.name, instance);
394
+ }
395
+ return map;
396
+ }, [traitBindings]);
397
+ const contextValue = useMemo(() => {
398
+ return {
399
+ traits: traitInstances,
400
+ getTrait: (name) => traitInstances.get(name),
401
+ dispatchToTrait: (traitName, eventKey, payload) => {
402
+ const instance = traitInstances.get(traitName);
403
+ if (instance) {
404
+ instance.dispatch(eventKey, payload);
405
+ }
406
+ },
407
+ canDispatch: (traitName, eventKey) => {
408
+ const instance = traitInstances.get(traitName);
409
+ return instance?.canDispatch(eventKey) || false;
410
+ }
411
+ };
412
+ }, [traitInstances]);
413
+ return /* @__PURE__ */ jsx(TraitContext.Provider, { value: contextValue, children });
414
+ }
415
+ function useTraitContext() {
416
+ const context = useContext(TraitContext);
417
+ if (!context) {
418
+ throw new Error("useTraitContext must be used within a TraitProvider");
419
+ }
420
+ return context;
421
+ }
422
+ function useTrait(traitName) {
423
+ const context = useTraitContext();
424
+ return context.getTrait(traitName);
425
+ }
426
+ var SlotsStateContext = createContext({});
427
+ var SlotsActionsContext = createContext(null);
428
+ function SlotsProvider({ children }) {
429
+ const [slots, setSlots] = useState({});
430
+ const setSlotPatterns = useCallback((slot, patterns, source) => {
431
+ setSlots((prev) => ({
432
+ ...prev,
433
+ [slot]: { patterns, source }
434
+ }));
435
+ }, []);
436
+ const clearSlot = useCallback((slot) => {
437
+ setSlots((prev) => {
438
+ if (!(slot in prev)) return prev;
439
+ const next = { ...prev };
440
+ delete next[slot];
441
+ return next;
442
+ });
443
+ }, []);
444
+ const clearAllSlots = useCallback(() => {
445
+ setSlots({});
446
+ }, []);
447
+ const actionsRef = useRef({ setSlotPatterns, clearSlot, clearAllSlots });
448
+ actionsRef.current = { setSlotPatterns, clearSlot, clearAllSlots };
449
+ const [stableActions] = useState(() => ({
450
+ setSlotPatterns: (...args) => actionsRef.current.setSlotPatterns(...args),
451
+ clearSlot: (...args) => actionsRef.current.clearSlot(...args),
452
+ clearAllSlots: () => actionsRef.current.clearAllSlots()
453
+ }));
454
+ return /* @__PURE__ */ jsx(SlotsActionsContext.Provider, { value: stableActions, children: /* @__PURE__ */ jsx(SlotsStateContext.Provider, { value: slots, children }) });
455
+ }
456
+ function useSlots() {
457
+ return useContext(SlotsStateContext);
458
+ }
459
+ function useSlotContent(slotName) {
460
+ const slots = useContext(SlotsStateContext);
461
+ return slots[slotName] || null;
462
+ }
463
+ function useSlotsActions() {
464
+ const actions = useContext(SlotsActionsContext);
465
+ if (!actions) {
466
+ throw new Error("useSlotsActions must be used within a SlotsProvider");
467
+ }
468
+ return actions;
469
+ }
470
+
471
+ export { EntitySchemaProvider, SlotsProvider, TraitContext, TraitProvider, clearSchemaCache, createClientEffectHandlers, useEntityDefinition, useEntitySchema, useResolvedSchema, useSlotContent, useSlots, useSlotsActions, useTrait, useTraitContext, useTraitStateMachine };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "2.8.1",
3
+ "version": "2.9.1",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "main": "./dist/components/index.js",
@@ -31,6 +31,10 @@
31
31
  "import": "./dist/renderer/index.js",
32
32
  "types": "./dist/renderer/index.d.ts"
33
33
  },
34
+ "./runtime": {
35
+ "import": "./dist/runtime/index.js",
36
+ "types": "./dist/runtime/index.d.ts"
37
+ },
34
38
  "./stores": {
35
39
  "import": "./dist/stores/index.js",
36
40
  "types": "./dist/stores/index.d.ts"
@@ -73,6 +77,7 @@
73
77
  "@almadar/core": ">=2.1.0",
74
78
  "@almadar/evaluator": ">=2.0.0",
75
79
  "@almadar/patterns": ">=2.0.0",
80
+ "@almadar/runtime": ">=2.0.0",
76
81
  "clsx": "^2.1.0",
77
82
  "leaflet": "1.9.4",
78
83
  "lucide-react": "^0.344.0",
@@ -1,7 +1,7 @@
1
+ import { subscribe, getSnapshot, clearEntities, removeEntity, updateSingleton, updateEntity, spawnEntity, getSingleton, getAllEntities, getByType, getEntity } from './chunk-N7MVUW4R.js';
1
2
  import { apiClient } from './chunk-3HJHHULT.js';
2
3
  import { SelectionContext, entityDataKeys, useEntityList } from './chunk-WGJIL4YR.js';
3
4
  import { useEventBus } from './chunk-YXZM3WCF.js';
4
- import { subscribe, getSnapshot, clearEntities, removeEntity, updateSingleton, updateEntity, spawnEntity, getSingleton, getAllEntities, getByType, getEntity } from './chunk-N7MVUW4R.js';
5
5
  import { useCallback, useState, useEffect, useMemo, useContext, useSyncExternalStore, useRef } from 'react';
6
6
  import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query';
7
7