@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.
- package/LICENSE +72 -0
- package/README.md +335 -0
- package/dist/ThemeContext-lI5bo85E.d.ts +103 -0
- package/dist/components/index.d.ts +4789 -0
- package/dist/components/index.js +21566 -0
- package/dist/components/index.js.map +1 -0
- package/dist/context/index.d.ts +208 -0
- package/dist/context/index.js +443 -0
- package/dist/context/index.js.map +1 -0
- package/dist/event-bus-types-8-cjyMxw.d.ts +65 -0
- package/dist/hooks/index.d.ts +1006 -0
- package/dist/hooks/index.js +2262 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/lib/index.d.ts +291 -0
- package/dist/lib/index.js +431 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/offline-executor-CHr4uAhf.d.ts +401 -0
- package/dist/providers/index.d.ts +386 -0
- package/dist/providers/index.js +1111 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/renderer/index.d.ts +382 -0
- package/dist/renderer/index.js +808 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/stores/index.d.ts +151 -0
- package/dist/stores/index.js +196 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/useUISlots-mnggE9X9.d.ts +105 -0
- package/package.json +121 -0
- package/themes/almadar.css +196 -0
- package/themes/index.css +11 -0
- package/themes/minimalist.css +193 -0
- package/themes/wireframe.css +188 -0
|
@@ -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
|