@almadar/ui 1.0.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.
@@ -0,0 +1,1111 @@
1
+ import { createContext, useRef, useCallback, useMemo, useEffect, useState, useContext } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ var __defProp = Object.defineProperty;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
7
+ var BUILT_IN_THEMES = [
8
+ {
9
+ name: "wireframe",
10
+ displayName: "Wireframe",
11
+ hasLightMode: true,
12
+ hasDarkMode: true
13
+ },
14
+ {
15
+ name: "minimalist",
16
+ displayName: "Minimalist",
17
+ hasLightMode: true,
18
+ hasDarkMode: true
19
+ },
20
+ {
21
+ name: "almadar",
22
+ displayName: "Almadar",
23
+ hasLightMode: true,
24
+ hasDarkMode: true
25
+ }
26
+ ];
27
+ var ThemeContext = createContext(void 0);
28
+ var THEME_STORAGE_KEY = "theme";
29
+ var MODE_STORAGE_KEY = "theme-mode";
30
+ function getSystemMode() {
31
+ if (typeof window === "undefined") return "light";
32
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
33
+ }
34
+ function resolveMode(mode) {
35
+ if (mode === "system") {
36
+ return getSystemMode();
37
+ }
38
+ return mode;
39
+ }
40
+ var ThemeProvider = ({
41
+ children,
42
+ themes = [],
43
+ defaultTheme = "wireframe",
44
+ defaultMode = "system"
45
+ }) => {
46
+ const availableThemes = useMemo(() => {
47
+ const themeMap = /* @__PURE__ */ new Map();
48
+ BUILT_IN_THEMES.forEach((t) => themeMap.set(t.name, t));
49
+ themes.forEach((t) => themeMap.set(t.name, t));
50
+ return Array.from(themeMap.values());
51
+ }, [themes]);
52
+ const [theme, setThemeState] = useState(() => {
53
+ if (typeof window === "undefined") return defaultTheme;
54
+ const stored = localStorage.getItem(THEME_STORAGE_KEY);
55
+ const validThemes = [
56
+ ...BUILT_IN_THEMES.map((t) => t.name),
57
+ ...themes.map((t) => t.name)
58
+ ];
59
+ if (stored && validThemes.includes(stored)) {
60
+ return stored;
61
+ }
62
+ return defaultTheme;
63
+ });
64
+ const [mode, setModeState] = useState(() => {
65
+ if (typeof window === "undefined") return defaultMode;
66
+ const stored = localStorage.getItem(MODE_STORAGE_KEY);
67
+ if (stored === "light" || stored === "dark" || stored === "system") {
68
+ return stored;
69
+ }
70
+ return defaultMode;
71
+ });
72
+ const [resolvedMode, setResolvedMode] = useState(
73
+ () => resolveMode(mode)
74
+ );
75
+ const appliedTheme = useMemo(
76
+ () => `${theme}-${resolvedMode}`,
77
+ [theme, resolvedMode]
78
+ );
79
+ useEffect(() => {
80
+ const updateResolved = () => {
81
+ setResolvedMode(resolveMode(mode));
82
+ };
83
+ updateResolved();
84
+ if (mode === "system") {
85
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
86
+ const handleChange = () => updateResolved();
87
+ mediaQuery.addEventListener("change", handleChange);
88
+ return () => mediaQuery.removeEventListener("change", handleChange);
89
+ }
90
+ return void 0;
91
+ }, [mode]);
92
+ useEffect(() => {
93
+ const root = document.documentElement;
94
+ root.setAttribute("data-theme", appliedTheme);
95
+ root.classList.remove("light", "dark");
96
+ root.classList.add(resolvedMode);
97
+ }, [appliedTheme, resolvedMode]);
98
+ const setTheme = useCallback(
99
+ (newTheme) => {
100
+ const validTheme = availableThemes.find((t) => t.name === newTheme);
101
+ if (validTheme) {
102
+ setThemeState(newTheme);
103
+ if (typeof window !== "undefined") {
104
+ localStorage.setItem(THEME_STORAGE_KEY, newTheme);
105
+ }
106
+ } else {
107
+ console.warn(
108
+ `Theme "${newTheme}" not found. Available: ${availableThemes.map((t) => t.name).join(", ")}`
109
+ );
110
+ }
111
+ },
112
+ [availableThemes]
113
+ );
114
+ const setMode = useCallback((newMode) => {
115
+ setModeState(newMode);
116
+ if (typeof window !== "undefined") {
117
+ localStorage.setItem(MODE_STORAGE_KEY, newMode);
118
+ }
119
+ }, []);
120
+ const toggleMode = useCallback(() => {
121
+ const newMode = resolvedMode === "dark" ? "light" : "dark";
122
+ setMode(newMode);
123
+ }, [resolvedMode, setMode]);
124
+ const contextValue = useMemo(
125
+ () => ({
126
+ theme,
127
+ mode,
128
+ resolvedMode,
129
+ setTheme,
130
+ setMode,
131
+ toggleMode,
132
+ availableThemes,
133
+ appliedTheme
134
+ }),
135
+ [
136
+ theme,
137
+ mode,
138
+ resolvedMode,
139
+ setTheme,
140
+ setMode,
141
+ toggleMode,
142
+ availableThemes,
143
+ appliedTheme
144
+ ]
145
+ );
146
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, { value: contextValue, children });
147
+ };
148
+ var DEFAULT_SLOTS = {
149
+ main: null,
150
+ sidebar: null,
151
+ modal: null,
152
+ drawer: null,
153
+ overlay: null,
154
+ center: null,
155
+ toast: null,
156
+ "hud-top": null,
157
+ "hud-bottom": null,
158
+ floating: null
159
+ };
160
+ var idCounter = 0;
161
+ function generateId() {
162
+ return `slot-content-${++idCounter}-${Date.now()}`;
163
+ }
164
+ function useUISlotManager() {
165
+ const [slots, setSlots] = useState(DEFAULT_SLOTS);
166
+ const subscribersRef = useRef(/* @__PURE__ */ new Set());
167
+ const timersRef = useRef(/* @__PURE__ */ new Map());
168
+ useEffect(() => {
169
+ return () => {
170
+ timersRef.current.forEach((timer) => clearTimeout(timer));
171
+ timersRef.current.clear();
172
+ };
173
+ }, []);
174
+ const notifySubscribers = useCallback((slot, content) => {
175
+ subscribersRef.current.forEach((callback) => {
176
+ try {
177
+ callback(slot, content);
178
+ } catch (error) {
179
+ console.error("[UISlots] Subscriber error:", error);
180
+ }
181
+ });
182
+ }, []);
183
+ const render = useCallback((config) => {
184
+ const id = generateId();
185
+ const content = {
186
+ id,
187
+ pattern: config.pattern,
188
+ props: config.props ?? {},
189
+ priority: config.priority ?? 0,
190
+ animation: config.animation ?? "fade",
191
+ onDismiss: config.onDismiss,
192
+ sourceTrait: config.sourceTrait
193
+ };
194
+ if (config.autoDismissMs && config.autoDismissMs > 0) {
195
+ content.autoDismissAt = Date.now() + config.autoDismissMs;
196
+ const timer = setTimeout(() => {
197
+ setSlots((prev) => {
198
+ if (prev[config.target]?.id === id) {
199
+ content.onDismiss?.();
200
+ notifySubscribers(config.target, null);
201
+ return { ...prev, [config.target]: null };
202
+ }
203
+ return prev;
204
+ });
205
+ timersRef.current.delete(id);
206
+ }, config.autoDismissMs);
207
+ timersRef.current.set(id, timer);
208
+ }
209
+ setSlots((prev) => {
210
+ const existing = prev[config.target];
211
+ if (existing && existing.priority > content.priority) {
212
+ console.warn(
213
+ `[UISlots] Slot "${config.target}" already has higher priority content (${existing.priority} > ${content.priority})`
214
+ );
215
+ return prev;
216
+ }
217
+ notifySubscribers(config.target, content);
218
+ return { ...prev, [config.target]: content };
219
+ });
220
+ return id;
221
+ }, [notifySubscribers]);
222
+ const clear = useCallback((slot) => {
223
+ setSlots((prev) => {
224
+ const content = prev[slot];
225
+ if (content) {
226
+ const timer = timersRef.current.get(content.id);
227
+ if (timer) {
228
+ clearTimeout(timer);
229
+ timersRef.current.delete(content.id);
230
+ }
231
+ content.onDismiss?.();
232
+ notifySubscribers(slot, null);
233
+ }
234
+ return { ...prev, [slot]: null };
235
+ });
236
+ }, [notifySubscribers]);
237
+ const clearById = useCallback((id) => {
238
+ setSlots((prev) => {
239
+ const entry = Object.entries(prev).find(([, content]) => content?.id === id);
240
+ if (entry) {
241
+ const [slot, content] = entry;
242
+ const timer = timersRef.current.get(id);
243
+ if (timer) {
244
+ clearTimeout(timer);
245
+ timersRef.current.delete(id);
246
+ }
247
+ content.onDismiss?.();
248
+ notifySubscribers(slot, null);
249
+ return { ...prev, [slot]: null };
250
+ }
251
+ return prev;
252
+ });
253
+ }, [notifySubscribers]);
254
+ const clearAll = useCallback(() => {
255
+ timersRef.current.forEach((timer) => clearTimeout(timer));
256
+ timersRef.current.clear();
257
+ setSlots((prev) => {
258
+ Object.entries(prev).forEach(([slot, content]) => {
259
+ if (content) {
260
+ content.onDismiss?.();
261
+ notifySubscribers(slot, null);
262
+ }
263
+ });
264
+ return DEFAULT_SLOTS;
265
+ });
266
+ }, [notifySubscribers]);
267
+ const subscribe = useCallback((callback) => {
268
+ subscribersRef.current.add(callback);
269
+ return () => {
270
+ subscribersRef.current.delete(callback);
271
+ };
272
+ }, []);
273
+ const hasContent = useCallback((slot) => {
274
+ return slots[slot] !== null;
275
+ }, [slots]);
276
+ const getContent = useCallback((slot) => {
277
+ return slots[slot];
278
+ }, [slots]);
279
+ return {
280
+ slots,
281
+ render,
282
+ clear,
283
+ clearById,
284
+ clearAll,
285
+ subscribe,
286
+ hasContent,
287
+ getContent
288
+ };
289
+ }
290
+ var UISlotContext = createContext(null);
291
+ function UISlotProvider({ children }) {
292
+ const slotManager = useUISlotManager();
293
+ const contextValue = useMemo(() => slotManager, [slotManager]);
294
+ return /* @__PURE__ */ jsx(UISlotContext.Provider, { value: contextValue, children });
295
+ }
296
+ var globalEventBus = null;
297
+ function setGlobalEventBus(bus) {
298
+ globalEventBus = bus;
299
+ }
300
+ var fallbackListeners = /* @__PURE__ */ new Map();
301
+ var fallbackEventBus = {
302
+ emit: (type, payload) => {
303
+ const event = {
304
+ type,
305
+ payload,
306
+ timestamp: Date.now()
307
+ };
308
+ const handlers = fallbackListeners.get(type);
309
+ if (handlers) {
310
+ handlers.forEach((handler) => {
311
+ try {
312
+ handler(event);
313
+ } catch (error) {
314
+ console.error(`[EventBus] Error in listener for '${type}':`, error);
315
+ }
316
+ });
317
+ }
318
+ },
319
+ on: (type, listener) => {
320
+ if (!fallbackListeners.has(type)) {
321
+ fallbackListeners.set(type, /* @__PURE__ */ new Set());
322
+ }
323
+ fallbackListeners.get(type).add(listener);
324
+ return () => {
325
+ const handlers = fallbackListeners.get(type);
326
+ if (handlers) {
327
+ handlers.delete(listener);
328
+ if (handlers.size === 0) {
329
+ fallbackListeners.delete(type);
330
+ }
331
+ }
332
+ };
333
+ },
334
+ once: (type, listener) => {
335
+ const wrappedListener = (event) => {
336
+ fallbackListeners.get(type)?.delete(wrappedListener);
337
+ listener(event);
338
+ };
339
+ return fallbackEventBus.on(type, wrappedListener);
340
+ },
341
+ hasListeners: (type) => {
342
+ const handlers = fallbackListeners.get(type);
343
+ return handlers !== void 0 && handlers.size > 0;
344
+ }
345
+ };
346
+ function useEventBus() {
347
+ const context = useContext(EventBusContext);
348
+ return context ?? globalEventBus ?? fallbackEventBus;
349
+ }
350
+ var EventBusContext = createContext(null);
351
+ function EventBusProvider({ children, debug = false }) {
352
+ const listenersRef = useRef(/* @__PURE__ */ new Map());
353
+ const deprecationWarningShown = useRef(false);
354
+ const getSelectedEntity = useCallback(() => {
355
+ if (!deprecationWarningShown.current) {
356
+ console.warn(
357
+ "[EventBus] getSelectedEntity is deprecated. Use SelectionProvider and useSelection hook instead. See SelectionProvider.tsx for migration guide."
358
+ );
359
+ deprecationWarningShown.current = true;
360
+ }
361
+ return null;
362
+ }, []);
363
+ const clearSelectedEntity = useCallback(() => {
364
+ if (!deprecationWarningShown.current) {
365
+ console.warn(
366
+ "[EventBus] clearSelectedEntity is deprecated. Use SelectionProvider and useSelection hook instead. See SelectionProvider.tsx for migration guide."
367
+ );
368
+ deprecationWarningShown.current = true;
369
+ }
370
+ }, []);
371
+ const emit = useCallback((type, payload) => {
372
+ const event = {
373
+ type,
374
+ payload,
375
+ timestamp: Date.now()
376
+ };
377
+ const listeners = listenersRef.current.get(type);
378
+ const listenerCount = listeners?.size ?? 0;
379
+ if (debug) {
380
+ if (listenerCount > 0) {
381
+ console.log(`[EventBus] Emit: ${type} \u2192 ${listenerCount} listener(s)`, payload);
382
+ } else {
383
+ console.warn(`[EventBus] Emit: ${type} (NO LISTENERS - event may be lost!)`, payload);
384
+ }
385
+ }
386
+ if (listeners) {
387
+ const listenersCopy = Array.from(listeners);
388
+ for (const listener of listenersCopy) {
389
+ try {
390
+ listener(event);
391
+ } catch (error) {
392
+ console.error(`[EventBus] Error in listener for '${type}':`, error);
393
+ }
394
+ }
395
+ }
396
+ }, [debug]);
397
+ const on = useCallback((type, listener) => {
398
+ if (!listenersRef.current.has(type)) {
399
+ listenersRef.current.set(type, /* @__PURE__ */ new Set());
400
+ }
401
+ const listeners = listenersRef.current.get(type);
402
+ listeners.add(listener);
403
+ if (debug) {
404
+ console.log(`[EventBus] Subscribed to '${type}', total: ${listeners.size}`);
405
+ }
406
+ return () => {
407
+ listeners.delete(listener);
408
+ if (debug) {
409
+ console.log(`[EventBus] Unsubscribed from '${type}', remaining: ${listeners.size}`);
410
+ }
411
+ if (listeners.size === 0) {
412
+ listenersRef.current.delete(type);
413
+ }
414
+ };
415
+ }, [debug]);
416
+ const once = useCallback((type, listener) => {
417
+ const wrappedListener = (event) => {
418
+ listenersRef.current.get(type)?.delete(wrappedListener);
419
+ listener(event);
420
+ };
421
+ return on(type, wrappedListener);
422
+ }, [on]);
423
+ const hasListeners = useCallback((type) => {
424
+ const listeners = listenersRef.current.get(type);
425
+ return listeners !== void 0 && listeners.size > 0;
426
+ }, []);
427
+ const contextValue = useMemo(
428
+ () => ({
429
+ emit,
430
+ on,
431
+ once,
432
+ hasListeners,
433
+ getSelectedEntity,
434
+ clearSelectedEntity
435
+ }),
436
+ [emit, on, once, hasListeners, getSelectedEntity, clearSelectedEntity]
437
+ );
438
+ useEffect(() => {
439
+ setGlobalEventBus(contextValue);
440
+ return () => {
441
+ setGlobalEventBus(null);
442
+ };
443
+ }, [contextValue]);
444
+ return /* @__PURE__ */ jsx(EventBusContext.Provider, { value: contextValue, children });
445
+ }
446
+ var SelectionContext = createContext(null);
447
+ var defaultCompareEntities = (a, b) => {
448
+ if (a === b) return true;
449
+ if (!a || !b) return false;
450
+ if (typeof a === "object" && typeof b === "object") {
451
+ const aId = a.id;
452
+ const bId = b.id;
453
+ return aId !== void 0 && aId === bId;
454
+ }
455
+ return false;
456
+ };
457
+ function SelectionProvider({
458
+ children,
459
+ debug = false,
460
+ compareEntities = defaultCompareEntities
461
+ }) {
462
+ const eventBus = useEventBus();
463
+ const [selected, setSelectedState] = useState(null);
464
+ const setSelected = useCallback(
465
+ (entity) => {
466
+ setSelectedState(entity);
467
+ if (debug) {
468
+ console.log("[SelectionProvider] Selection set:", entity);
469
+ }
470
+ },
471
+ [debug]
472
+ );
473
+ const clearSelection = useCallback(() => {
474
+ setSelectedState(null);
475
+ if (debug) {
476
+ console.log("[SelectionProvider] Selection cleared");
477
+ }
478
+ }, [debug]);
479
+ const isSelected = useCallback(
480
+ (entity) => {
481
+ return compareEntities(selected, entity);
482
+ },
483
+ [selected, compareEntities]
484
+ );
485
+ useEffect(() => {
486
+ const handleSelect = (event) => {
487
+ const row = event.payload?.row;
488
+ if (row) {
489
+ setSelected(row);
490
+ if (debug) {
491
+ console.log(`[SelectionProvider] ${event.type} received:`, row);
492
+ }
493
+ }
494
+ };
495
+ const handleDeselect = (event) => {
496
+ clearSelection();
497
+ if (debug) {
498
+ console.log(`[SelectionProvider] ${event.type} received - clearing selection`);
499
+ }
500
+ };
501
+ const unsubView = eventBus.on("UI:VIEW", handleSelect);
502
+ const unsubSelect = eventBus.on("UI:SELECT", handleSelect);
503
+ const unsubClose = eventBus.on("UI:CLOSE", handleDeselect);
504
+ const unsubDeselect = eventBus.on("UI:DESELECT", handleDeselect);
505
+ const unsubCancel = eventBus.on("UI:CANCEL", handleDeselect);
506
+ return () => {
507
+ unsubView();
508
+ unsubSelect();
509
+ unsubClose();
510
+ unsubDeselect();
511
+ unsubCancel();
512
+ };
513
+ }, [eventBus, setSelected, clearSelection, debug]);
514
+ const contextValue = {
515
+ selected,
516
+ setSelected,
517
+ clearSelection,
518
+ isSelected
519
+ };
520
+ return /* @__PURE__ */ jsx(SelectionContext.Provider, { value: contextValue, children });
521
+ }
522
+ function useSelection() {
523
+ const context = useContext(SelectionContext);
524
+ if (!context) {
525
+ throw new Error("useSelection must be used within a SelectionProvider");
526
+ }
527
+ return context;
528
+ }
529
+ function useSelectionOptional() {
530
+ const context = useContext(SelectionContext);
531
+ return context;
532
+ }
533
+ var FetchedDataContext = createContext(null);
534
+ function FetchedDataProvider({
535
+ initialData,
536
+ children
537
+ }) {
538
+ const [state, setState] = useState(() => ({
539
+ data: initialData || {},
540
+ fetchedAt: {},
541
+ loading: false,
542
+ error: null
543
+ }));
544
+ const getData = useCallback(
545
+ (entityName) => {
546
+ return state.data[entityName] || [];
547
+ },
548
+ [state.data]
549
+ );
550
+ const getById = useCallback(
551
+ (entityName, id) => {
552
+ const records = state.data[entityName];
553
+ return records?.find((r) => r.id === id);
554
+ },
555
+ [state.data]
556
+ );
557
+ const hasData = useCallback(
558
+ (entityName) => {
559
+ return entityName in state.data && state.data[entityName].length > 0;
560
+ },
561
+ [state.data]
562
+ );
563
+ const getFetchedAt = useCallback(
564
+ (entityName) => {
565
+ return state.fetchedAt[entityName];
566
+ },
567
+ [state.fetchedAt]
568
+ );
569
+ const setData = useCallback((data) => {
570
+ const now = Date.now();
571
+ setState((prev) => ({
572
+ ...prev,
573
+ data: {
574
+ ...prev.data,
575
+ ...data
576
+ },
577
+ fetchedAt: {
578
+ ...prev.fetchedAt,
579
+ ...Object.keys(data).reduce(
580
+ (acc, key) => ({ ...acc, [key]: now }),
581
+ {}
582
+ )
583
+ },
584
+ loading: false,
585
+ error: null
586
+ }));
587
+ }, []);
588
+ const clearData = useCallback(() => {
589
+ setState((prev) => ({
590
+ ...prev,
591
+ data: {},
592
+ fetchedAt: {}
593
+ }));
594
+ }, []);
595
+ const clearEntity = useCallback((entityName) => {
596
+ setState((prev) => {
597
+ const newData = { ...prev.data };
598
+ const newFetchedAt = { ...prev.fetchedAt };
599
+ delete newData[entityName];
600
+ delete newFetchedAt[entityName];
601
+ return {
602
+ ...prev,
603
+ data: newData,
604
+ fetchedAt: newFetchedAt
605
+ };
606
+ });
607
+ }, []);
608
+ const setLoading = useCallback((loading) => {
609
+ setState((prev) => ({ ...prev, loading }));
610
+ }, []);
611
+ const setError = useCallback((error) => {
612
+ setState((prev) => ({ ...prev, error, loading: false }));
613
+ }, []);
614
+ const contextValue = useMemo(
615
+ () => ({
616
+ getData,
617
+ getById,
618
+ hasData,
619
+ getFetchedAt,
620
+ setData,
621
+ clearData,
622
+ clearEntity,
623
+ loading: state.loading,
624
+ setLoading,
625
+ error: state.error,
626
+ setError
627
+ }),
628
+ [
629
+ getData,
630
+ getById,
631
+ hasData,
632
+ getFetchedAt,
633
+ setData,
634
+ clearData,
635
+ clearEntity,
636
+ state.loading,
637
+ setLoading,
638
+ state.error,
639
+ setError
640
+ ]
641
+ );
642
+ return /* @__PURE__ */ jsx(FetchedDataContext.Provider, { value: contextValue, children });
643
+ }
644
+ function useFetchedDataContext() {
645
+ return useContext(FetchedDataContext);
646
+ }
647
+ function useFetchedData() {
648
+ const context = useContext(FetchedDataContext);
649
+ if (!context) {
650
+ return {
651
+ getData: () => [],
652
+ getById: () => void 0,
653
+ hasData: () => false,
654
+ getFetchedAt: () => void 0,
655
+ setData: () => {
656
+ },
657
+ clearData: () => {
658
+ },
659
+ clearEntity: () => {
660
+ },
661
+ loading: false,
662
+ setLoading: () => {
663
+ },
664
+ error: null,
665
+ setError: () => {
666
+ }
667
+ };
668
+ }
669
+ return context;
670
+ }
671
+ function useFetchedEntity(entityName) {
672
+ const context = useFetchedData();
673
+ return {
674
+ /** All fetched records for this entity */
675
+ records: context.getData(entityName),
676
+ /** Get a record by ID */
677
+ getById: (id) => context.getById(entityName, id),
678
+ /** Whether data has been fetched for this entity */
679
+ hasData: context.hasData(entityName),
680
+ /** When data was last fetched */
681
+ fetchedAt: context.getFetchedAt(entityName),
682
+ /** Whether data is loading */
683
+ loading: context.loading,
684
+ /** Current error */
685
+ error: context.error
686
+ };
687
+ }
688
+ function OrbitalProvider({
689
+ children,
690
+ themes,
691
+ defaultTheme = "wireframe",
692
+ defaultMode = "system",
693
+ debug = false,
694
+ initialData
695
+ }) {
696
+ return /* @__PURE__ */ jsx(
697
+ ThemeProvider,
698
+ {
699
+ themes,
700
+ defaultTheme,
701
+ defaultMode,
702
+ children: /* @__PURE__ */ jsx(FetchedDataProvider, { initialData, children: /* @__PURE__ */ jsx(EventBusProvider, { debug, children: /* @__PURE__ */ jsx(UISlotProvider, { children: /* @__PURE__ */ jsx(SelectionProvider, { debug, children }) }) }) })
703
+ }
704
+ );
705
+ }
706
+ OrbitalProvider.displayName = "OrbitalProvider";
707
+
708
+ // renderer/client-effect-executor.ts
709
+ function executeClientEffects(effects, config) {
710
+ if (!effects || effects.length === 0) {
711
+ return;
712
+ }
713
+ for (const effect of effects) {
714
+ try {
715
+ executeEffect(effect, config);
716
+ } catch (error) {
717
+ console.error(
718
+ `[ClientEffectExecutor] Error executing effect:`,
719
+ effect,
720
+ error
721
+ );
722
+ }
723
+ }
724
+ config.onComplete?.();
725
+ }
726
+ function executeEffect(effect, config) {
727
+ const [effectType, ...args] = effect;
728
+ switch (effectType) {
729
+ case "render-ui": {
730
+ const [slot, patternConfig] = args;
731
+ executeRenderUI(slot, patternConfig, config);
732
+ break;
733
+ }
734
+ case "navigate": {
735
+ const [path, params] = args;
736
+ executeNavigate(path, params, config);
737
+ break;
738
+ }
739
+ case "notify": {
740
+ const [message, options] = args;
741
+ executeNotify(message, options, config);
742
+ break;
743
+ }
744
+ case "emit": {
745
+ const [event, payload] = args;
746
+ executeEmit(event, payload, config);
747
+ break;
748
+ }
749
+ default:
750
+ console.warn(`[ClientEffectExecutor] Unknown effect type: ${effectType}`);
751
+ }
752
+ }
753
+ function executeRenderUI(slot, patternConfig, config) {
754
+ config.renderToSlot(slot, patternConfig);
755
+ }
756
+ function executeNavigate(path, params, config) {
757
+ config.navigate(path, params);
758
+ }
759
+ function executeNotify(message, options, config) {
760
+ config.notify(message, options);
761
+ }
762
+ function executeEmit(event, payload, config) {
763
+ config.eventBus.emit(event, payload);
764
+ }
765
+ var effectIdCounter = 0;
766
+ function generateEffectId() {
767
+ return `offline-effect-${++effectIdCounter}-${Date.now()}`;
768
+ }
769
+ var OfflineExecutor = class {
770
+ constructor(config) {
771
+ __publicField(this, "config");
772
+ __publicField(this, "state");
773
+ __publicField(this, "storage");
774
+ /**
775
+ * Handle going online
776
+ */
777
+ __publicField(this, "handleOnline", () => {
778
+ this.state.isOffline = false;
779
+ });
780
+ /**
781
+ * Handle going offline
782
+ */
783
+ __publicField(this, "handleOffline", () => {
784
+ this.state.isOffline = true;
785
+ });
786
+ this.config = {
787
+ enableSyncQueue: true,
788
+ maxQueueSize: 100,
789
+ ...config
790
+ };
791
+ this.state = {
792
+ isOffline: !this.checkOnline(),
793
+ syncQueue: [],
794
+ localEffectsProcessed: 0,
795
+ effectsSynced: 0
796
+ };
797
+ this.storage = typeof localStorage !== "undefined" ? localStorage : null;
798
+ this.loadSyncQueue();
799
+ if (typeof window !== "undefined") {
800
+ window.addEventListener("online", this.handleOnline);
801
+ window.addEventListener("offline", this.handleOffline);
802
+ }
803
+ }
804
+ /**
805
+ * Check if we're online (browser API)
806
+ */
807
+ checkOnline() {
808
+ return typeof navigator !== "undefined" ? navigator.onLine : true;
809
+ }
810
+ /**
811
+ * Load sync queue from localStorage
812
+ */
813
+ loadSyncQueue() {
814
+ if (!this.storage) return;
815
+ try {
816
+ const stored = this.storage.getItem("orbital-offline-queue");
817
+ if (stored) {
818
+ this.state.syncQueue = JSON.parse(stored);
819
+ }
820
+ } catch (error) {
821
+ console.warn("[OfflineExecutor] Failed to load sync queue:", error);
822
+ }
823
+ }
824
+ /**
825
+ * Save sync queue to localStorage
826
+ */
827
+ saveSyncQueue() {
828
+ if (!this.storage) return;
829
+ try {
830
+ this.storage.setItem("orbital-offline-queue", JSON.stringify(this.state.syncQueue));
831
+ } catch (error) {
832
+ console.warn("[OfflineExecutor] Failed to save sync queue:", error);
833
+ }
834
+ }
835
+ /**
836
+ * Add an effect to the sync queue
837
+ */
838
+ queueForSync(type, payload) {
839
+ if (!this.config.enableSyncQueue) return;
840
+ const effect = {
841
+ id: generateEffectId(),
842
+ timestamp: Date.now(),
843
+ type,
844
+ payload,
845
+ retries: 0,
846
+ maxRetries: 3
847
+ };
848
+ this.state.syncQueue.push(effect);
849
+ if (this.state.syncQueue.length > (this.config.maxQueueSize ?? 100)) {
850
+ this.state.syncQueue.shift();
851
+ }
852
+ this.saveSyncQueue();
853
+ this.config.onEffectQueued?.(effect);
854
+ this.config.onQueueChange?.(this.state.syncQueue);
855
+ }
856
+ /**
857
+ * Execute client effects immediately.
858
+ */
859
+ executeClientEffects(effects) {
860
+ if (effects.length === 0) return;
861
+ executeClientEffects(effects, this.config);
862
+ this.state.localEffectsProcessed += effects.length;
863
+ }
864
+ /**
865
+ * Process an event in offline mode.
866
+ *
867
+ * Returns a simulated EventResponse with mock data.
868
+ * Client effects are executed immediately.
869
+ * Server effects are queued for sync.
870
+ */
871
+ processEventOffline(event, payload, effects) {
872
+ const clientEffects = [];
873
+ const fetchedData = {};
874
+ if (effects) {
875
+ for (const effect of effects) {
876
+ if (!Array.isArray(effect) || effect.length < 1) continue;
877
+ const [type, ...args] = effect;
878
+ switch (type) {
879
+ // Client effects - execute immediately
880
+ case "render-ui":
881
+ case "navigate":
882
+ case "notify":
883
+ case "emit":
884
+ clientEffects.push(effect);
885
+ break;
886
+ // Fetch effect - use mock data
887
+ case "fetch": {
888
+ const [entityName, _query] = args;
889
+ if (typeof entityName === "string" && this.config.mockDataProvider) {
890
+ fetchedData[entityName] = this.config.mockDataProvider(entityName);
891
+ }
892
+ break;
893
+ }
894
+ // Server effects - queue for sync
895
+ case "persist":
896
+ case "call-service":
897
+ case "spawn":
898
+ case "despawn":
899
+ this.queueForSync(type, { args, event, payload });
900
+ break;
901
+ default:
902
+ console.warn(`[OfflineExecutor] Unknown effect type: ${type}`);
903
+ }
904
+ }
905
+ }
906
+ if (clientEffects.length > 0) {
907
+ this.executeClientEffects(clientEffects);
908
+ }
909
+ return {
910
+ success: true,
911
+ data: Object.keys(fetchedData).length > 0 ? fetchedData : void 0,
912
+ clientEffects: clientEffects.length > 0 ? clientEffects : void 0
913
+ };
914
+ }
915
+ /**
916
+ * Sync pending effects to server.
917
+ *
918
+ * @param serverUrl - Base URL for the orbital server
919
+ * @param authToken - Optional auth token for requests
920
+ * @returns Number of successfully synced effects
921
+ */
922
+ async syncPendingEffects(serverUrl, authToken) {
923
+ if (this.state.syncQueue.length === 0) {
924
+ return 0;
925
+ }
926
+ this.state.lastSyncAttempt = Date.now();
927
+ let syncedCount = 0;
928
+ const failedEffects = [];
929
+ const headers = {
930
+ "Content-Type": "application/json"
931
+ };
932
+ if (authToken) {
933
+ headers["Authorization"] = `Bearer ${authToken}`;
934
+ }
935
+ for (const effect of this.state.syncQueue) {
936
+ try {
937
+ const response = await fetch(`${serverUrl}/sync-effect`, {
938
+ method: "POST",
939
+ headers,
940
+ body: JSON.stringify({
941
+ type: effect.type,
942
+ payload: effect.payload,
943
+ offlineId: effect.id,
944
+ offlineTimestamp: effect.timestamp
945
+ })
946
+ });
947
+ if (response.ok) {
948
+ syncedCount++;
949
+ } else {
950
+ effect.retries++;
951
+ if (effect.retries < effect.maxRetries) {
952
+ failedEffects.push(effect);
953
+ }
954
+ }
955
+ } catch (error) {
956
+ effect.retries++;
957
+ if (effect.retries < effect.maxRetries) {
958
+ failedEffects.push(effect);
959
+ }
960
+ }
961
+ }
962
+ this.state.syncQueue = failedEffects;
963
+ this.state.effectsSynced += syncedCount;
964
+ if (syncedCount > 0) {
965
+ this.state.lastSuccessfulSync = Date.now();
966
+ }
967
+ this.saveSyncQueue();
968
+ this.config.onQueueChange?.(this.state.syncQueue);
969
+ return syncedCount;
970
+ }
971
+ /**
972
+ * Get current executor state
973
+ */
974
+ getState() {
975
+ return { ...this.state };
976
+ }
977
+ /**
978
+ * Get number of pending effects
979
+ */
980
+ getPendingCount() {
981
+ return this.state.syncQueue.length;
982
+ }
983
+ /**
984
+ * Clear the sync queue
985
+ */
986
+ clearQueue() {
987
+ this.state.syncQueue = [];
988
+ this.saveSyncQueue();
989
+ this.config.onQueueChange?.(this.state.syncQueue);
990
+ }
991
+ /**
992
+ * Dispose the executor and clean up listeners
993
+ */
994
+ dispose() {
995
+ if (typeof window !== "undefined") {
996
+ window.removeEventListener("online", this.handleOnline);
997
+ window.removeEventListener("offline", this.handleOffline);
998
+ }
999
+ }
1000
+ };
1001
+ function useOfflineExecutor(options) {
1002
+ const executorRef = useRef(null);
1003
+ const [state, setState] = useState({
1004
+ isOffline: false,
1005
+ syncQueue: [],
1006
+ localEffectsProcessed: 0,
1007
+ effectsSynced: 0
1008
+ });
1009
+ useEffect(() => {
1010
+ const executor = new OfflineExecutor({
1011
+ ...options,
1012
+ onQueueChange: (queue) => {
1013
+ setState(executor.getState());
1014
+ options.onQueueChange?.(queue);
1015
+ }
1016
+ });
1017
+ executorRef.current = executor;
1018
+ setState(executor.getState());
1019
+ return () => {
1020
+ executor.dispose();
1021
+ executorRef.current = null;
1022
+ };
1023
+ }, []);
1024
+ useEffect(() => {
1025
+ if (!options.autoSync || !options.serverUrl) return;
1026
+ const handleOnline = async () => {
1027
+ if (executorRef.current) {
1028
+ await executorRef.current.syncPendingEffects(
1029
+ options.serverUrl,
1030
+ options.authToken
1031
+ );
1032
+ setState(executorRef.current.getState());
1033
+ }
1034
+ };
1035
+ window.addEventListener("online", handleOnline);
1036
+ return () => window.removeEventListener("online", handleOnline);
1037
+ }, [options.autoSync, options.serverUrl, options.authToken]);
1038
+ const executeEffects = useCallback((effects) => {
1039
+ executorRef.current?.executeClientEffects(effects);
1040
+ if (executorRef.current) {
1041
+ setState(executorRef.current.getState());
1042
+ }
1043
+ }, []);
1044
+ const processEventOffline = useCallback(
1045
+ (event, payload, effects) => {
1046
+ const result = executorRef.current?.processEventOffline(event, payload, effects);
1047
+ if (executorRef.current) {
1048
+ setState(executorRef.current.getState());
1049
+ }
1050
+ return result ?? { success: false, error: "Executor not initialized" };
1051
+ },
1052
+ []
1053
+ );
1054
+ const sync = useCallback(async () => {
1055
+ if (!executorRef.current || !options.serverUrl) return 0;
1056
+ const count = await executorRef.current.syncPendingEffects(
1057
+ options.serverUrl,
1058
+ options.authToken
1059
+ );
1060
+ setState(executorRef.current.getState());
1061
+ return count;
1062
+ }, [options.serverUrl, options.authToken]);
1063
+ const clearQueue = useCallback(() => {
1064
+ executorRef.current?.clearQueue();
1065
+ if (executorRef.current) {
1066
+ setState(executorRef.current.getState());
1067
+ }
1068
+ }, []);
1069
+ return {
1070
+ state,
1071
+ isOffline: state.isOffline,
1072
+ pendingCount: state.syncQueue.length,
1073
+ executeClientEffects: executeEffects,
1074
+ processEventOffline,
1075
+ sync,
1076
+ clearQueue
1077
+ };
1078
+ }
1079
+ var OfflineModeContext = createContext(null);
1080
+ function OfflineModeProvider({
1081
+ children,
1082
+ ...executorOptions
1083
+ }) {
1084
+ const [forceOffline, setForceOffline] = useState(false);
1085
+ const executor = useOfflineExecutor(executorOptions);
1086
+ const effectivelyOffline = executor.isOffline || forceOffline;
1087
+ const contextValue = useMemo(
1088
+ () => ({
1089
+ ...executor,
1090
+ forceOffline,
1091
+ setForceOffline,
1092
+ effectivelyOffline
1093
+ }),
1094
+ [executor, forceOffline, effectivelyOffline]
1095
+ );
1096
+ return /* @__PURE__ */ jsx(OfflineModeContext.Provider, { value: contextValue, children });
1097
+ }
1098
+ function useOfflineMode() {
1099
+ const context = useContext(OfflineModeContext);
1100
+ if (!context) {
1101
+ throw new Error("useOfflineMode must be used within OfflineModeProvider");
1102
+ }
1103
+ return context;
1104
+ }
1105
+ function useOptionalOfflineMode() {
1106
+ return useContext(OfflineModeContext);
1107
+ }
1108
+
1109
+ export { EventBusContext, EventBusProvider, FetchedDataContext, FetchedDataProvider, OfflineModeProvider, OrbitalProvider, SelectionContext, SelectionProvider, useFetchedData, useFetchedDataContext, useFetchedEntity, useOfflineMode, useOptionalOfflineMode, useSelection, useSelectionOptional };
1110
+ //# sourceMappingURL=index.js.map
1111
+ //# sourceMappingURL=index.js.map