@almadar/ui 1.0.0 → 1.0.10

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,14 @@
1
+ import { ThemeProvider, useTheme } from './chunk-I5RSZIOE.js';
2
+
3
+ // context/DesignThemeContext.tsx
4
+ var DesignThemeProvider = ThemeProvider;
5
+ function useDesignTheme() {
6
+ const { theme, setTheme, availableThemes } = useTheme();
7
+ return {
8
+ designTheme: theme,
9
+ setDesignTheme: setTheme,
10
+ availableThemes: availableThemes.map((t) => t.name)
11
+ };
12
+ }
13
+
14
+ export { DesignThemeProvider, useDesignTheme };
@@ -0,0 +1,147 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+
3
+ // hooks/useUISlots.ts
4
+ var DEFAULT_SLOTS = {
5
+ main: null,
6
+ sidebar: null,
7
+ modal: null,
8
+ drawer: null,
9
+ overlay: null,
10
+ center: null,
11
+ toast: null,
12
+ "hud-top": null,
13
+ "hud-bottom": null,
14
+ floating: null
15
+ };
16
+ var idCounter = 0;
17
+ function generateId() {
18
+ return `slot-content-${++idCounter}-${Date.now()}`;
19
+ }
20
+ function useUISlotManager() {
21
+ const [slots, setSlots] = useState(DEFAULT_SLOTS);
22
+ const subscribersRef = useRef(/* @__PURE__ */ new Set());
23
+ const timersRef = useRef(/* @__PURE__ */ new Map());
24
+ useEffect(() => {
25
+ return () => {
26
+ timersRef.current.forEach((timer) => clearTimeout(timer));
27
+ timersRef.current.clear();
28
+ };
29
+ }, []);
30
+ const notifySubscribers = useCallback((slot, content) => {
31
+ subscribersRef.current.forEach((callback) => {
32
+ try {
33
+ callback(slot, content);
34
+ } catch (error) {
35
+ console.error("[UISlots] Subscriber error:", error);
36
+ }
37
+ });
38
+ }, []);
39
+ const render = useCallback((config) => {
40
+ const id = generateId();
41
+ const content = {
42
+ id,
43
+ pattern: config.pattern,
44
+ props: config.props ?? {},
45
+ priority: config.priority ?? 0,
46
+ animation: config.animation ?? "fade",
47
+ onDismiss: config.onDismiss,
48
+ sourceTrait: config.sourceTrait
49
+ };
50
+ if (config.autoDismissMs && config.autoDismissMs > 0) {
51
+ content.autoDismissAt = Date.now() + config.autoDismissMs;
52
+ const timer = setTimeout(() => {
53
+ setSlots((prev) => {
54
+ if (prev[config.target]?.id === id) {
55
+ content.onDismiss?.();
56
+ notifySubscribers(config.target, null);
57
+ return { ...prev, [config.target]: null };
58
+ }
59
+ return prev;
60
+ });
61
+ timersRef.current.delete(id);
62
+ }, config.autoDismissMs);
63
+ timersRef.current.set(id, timer);
64
+ }
65
+ setSlots((prev) => {
66
+ const existing = prev[config.target];
67
+ if (existing && existing.priority > content.priority) {
68
+ console.warn(
69
+ `[UISlots] Slot "${config.target}" already has higher priority content (${existing.priority} > ${content.priority})`
70
+ );
71
+ return prev;
72
+ }
73
+ notifySubscribers(config.target, content);
74
+ return { ...prev, [config.target]: content };
75
+ });
76
+ return id;
77
+ }, [notifySubscribers]);
78
+ const clear = useCallback((slot) => {
79
+ setSlots((prev) => {
80
+ const content = prev[slot];
81
+ if (content) {
82
+ const timer = timersRef.current.get(content.id);
83
+ if (timer) {
84
+ clearTimeout(timer);
85
+ timersRef.current.delete(content.id);
86
+ }
87
+ content.onDismiss?.();
88
+ notifySubscribers(slot, null);
89
+ }
90
+ return { ...prev, [slot]: null };
91
+ });
92
+ }, [notifySubscribers]);
93
+ const clearById = useCallback((id) => {
94
+ setSlots((prev) => {
95
+ const entry = Object.entries(prev).find(([, content]) => content?.id === id);
96
+ if (entry) {
97
+ const [slot, content] = entry;
98
+ const timer = timersRef.current.get(id);
99
+ if (timer) {
100
+ clearTimeout(timer);
101
+ timersRef.current.delete(id);
102
+ }
103
+ content.onDismiss?.();
104
+ notifySubscribers(slot, null);
105
+ return { ...prev, [slot]: null };
106
+ }
107
+ return prev;
108
+ });
109
+ }, [notifySubscribers]);
110
+ const clearAll = useCallback(() => {
111
+ timersRef.current.forEach((timer) => clearTimeout(timer));
112
+ timersRef.current.clear();
113
+ setSlots((prev) => {
114
+ Object.entries(prev).forEach(([slot, content]) => {
115
+ if (content) {
116
+ content.onDismiss?.();
117
+ notifySubscribers(slot, null);
118
+ }
119
+ });
120
+ return DEFAULT_SLOTS;
121
+ });
122
+ }, [notifySubscribers]);
123
+ const subscribe = useCallback((callback) => {
124
+ subscribersRef.current.add(callback);
125
+ return () => {
126
+ subscribersRef.current.delete(callback);
127
+ };
128
+ }, []);
129
+ const hasContent = useCallback((slot) => {
130
+ return slots[slot] !== null;
131
+ }, [slots]);
132
+ const getContent = useCallback((slot) => {
133
+ return slots[slot];
134
+ }, [slots]);
135
+ return {
136
+ slots,
137
+ render,
138
+ clear,
139
+ clearById,
140
+ clearAll,
141
+ subscribe,
142
+ hasContent,
143
+ getContent
144
+ };
145
+ }
146
+
147
+ export { DEFAULT_SLOTS, useUISlotManager };
@@ -0,0 +1,426 @@
1
+ import { __publicField } from './chunk-S7EYY36U.js';
2
+ import { useRef, useState, useEffect, useCallback } from 'react';
3
+
4
+ // renderer/client-effect-executor.ts
5
+ function executeClientEffects(effects, config) {
6
+ if (!effects || effects.length === 0) {
7
+ return;
8
+ }
9
+ for (const effect of effects) {
10
+ try {
11
+ executeEffect(effect, config);
12
+ } catch (error) {
13
+ console.error(
14
+ `[ClientEffectExecutor] Error executing effect:`,
15
+ effect,
16
+ error
17
+ );
18
+ }
19
+ }
20
+ config.onComplete?.();
21
+ }
22
+ function executeEffect(effect, config) {
23
+ const [effectType, ...args] = effect;
24
+ switch (effectType) {
25
+ case "render-ui": {
26
+ const [slot, patternConfig] = args;
27
+ executeRenderUI(slot, patternConfig, config);
28
+ break;
29
+ }
30
+ case "navigate": {
31
+ const [path, params] = args;
32
+ executeNavigate(path, params, config);
33
+ break;
34
+ }
35
+ case "notify": {
36
+ const [message, options] = args;
37
+ executeNotify(message, options, config);
38
+ break;
39
+ }
40
+ case "emit": {
41
+ const [event, payload] = args;
42
+ executeEmit(event, payload, config);
43
+ break;
44
+ }
45
+ default:
46
+ console.warn(`[ClientEffectExecutor] Unknown effect type: ${effectType}`);
47
+ }
48
+ }
49
+ function executeRenderUI(slot, patternConfig, config) {
50
+ config.renderToSlot(slot, patternConfig);
51
+ }
52
+ function executeNavigate(path, params, config) {
53
+ config.navigate(path, params);
54
+ }
55
+ function executeNotify(message, options, config) {
56
+ config.notify(message, options);
57
+ }
58
+ function executeEmit(event, payload, config) {
59
+ config.eventBus.emit(event, payload);
60
+ }
61
+ function parseClientEffect(raw) {
62
+ if (!Array.isArray(raw) || raw.length < 1) {
63
+ console.warn("[ClientEffectExecutor] Invalid effect format:", raw);
64
+ return null;
65
+ }
66
+ const [type, ...args] = raw;
67
+ if (typeof type !== "string") {
68
+ console.warn("[ClientEffectExecutor] Effect type must be string:", raw);
69
+ return null;
70
+ }
71
+ switch (type) {
72
+ case "render-ui":
73
+ return ["render-ui", args[0], args[1]];
74
+ case "navigate":
75
+ return ["navigate", args[0], args[1]];
76
+ case "notify":
77
+ return ["notify", args[0], args[1]];
78
+ case "emit":
79
+ return ["emit", args[0], args[1]];
80
+ default:
81
+ console.warn(`[ClientEffectExecutor] Unknown effect type: ${type}`);
82
+ return null;
83
+ }
84
+ }
85
+ function parseClientEffects(raw) {
86
+ if (!raw || !Array.isArray(raw)) {
87
+ return [];
88
+ }
89
+ return raw.map((effect) => parseClientEffect(effect)).filter((effect) => effect !== null);
90
+ }
91
+ function filterEffectsByType(effects, type) {
92
+ return effects.filter(
93
+ (effect) => effect[0] === type
94
+ );
95
+ }
96
+ function getRenderUIEffects(effects) {
97
+ return filterEffectsByType(effects, "render-ui");
98
+ }
99
+ function getNavigateEffects(effects) {
100
+ return filterEffectsByType(effects, "navigate");
101
+ }
102
+ function getNotifyEffects(effects) {
103
+ return filterEffectsByType(effects, "notify");
104
+ }
105
+ function getEmitEffects(effects) {
106
+ return filterEffectsByType(effects, "emit");
107
+ }
108
+ var effectIdCounter = 0;
109
+ function generateEffectId() {
110
+ return `offline-effect-${++effectIdCounter}-${Date.now()}`;
111
+ }
112
+ var OfflineExecutor = class {
113
+ constructor(config) {
114
+ __publicField(this, "config");
115
+ __publicField(this, "state");
116
+ __publicField(this, "storage");
117
+ /**
118
+ * Handle going online
119
+ */
120
+ __publicField(this, "handleOnline", () => {
121
+ this.state.isOffline = false;
122
+ });
123
+ /**
124
+ * Handle going offline
125
+ */
126
+ __publicField(this, "handleOffline", () => {
127
+ this.state.isOffline = true;
128
+ });
129
+ this.config = {
130
+ enableSyncQueue: true,
131
+ maxQueueSize: 100,
132
+ ...config
133
+ };
134
+ this.state = {
135
+ isOffline: !this.checkOnline(),
136
+ syncQueue: [],
137
+ localEffectsProcessed: 0,
138
+ effectsSynced: 0
139
+ };
140
+ this.storage = typeof localStorage !== "undefined" ? localStorage : null;
141
+ this.loadSyncQueue();
142
+ if (typeof window !== "undefined") {
143
+ window.addEventListener("online", this.handleOnline);
144
+ window.addEventListener("offline", this.handleOffline);
145
+ }
146
+ }
147
+ /**
148
+ * Check if we're online (browser API)
149
+ */
150
+ checkOnline() {
151
+ return typeof navigator !== "undefined" ? navigator.onLine : true;
152
+ }
153
+ /**
154
+ * Load sync queue from localStorage
155
+ */
156
+ loadSyncQueue() {
157
+ if (!this.storage) return;
158
+ try {
159
+ const stored = this.storage.getItem("orbital-offline-queue");
160
+ if (stored) {
161
+ this.state.syncQueue = JSON.parse(stored);
162
+ }
163
+ } catch (error) {
164
+ console.warn("[OfflineExecutor] Failed to load sync queue:", error);
165
+ }
166
+ }
167
+ /**
168
+ * Save sync queue to localStorage
169
+ */
170
+ saveSyncQueue() {
171
+ if (!this.storage) return;
172
+ try {
173
+ this.storage.setItem("orbital-offline-queue", JSON.stringify(this.state.syncQueue));
174
+ } catch (error) {
175
+ console.warn("[OfflineExecutor] Failed to save sync queue:", error);
176
+ }
177
+ }
178
+ /**
179
+ * Add an effect to the sync queue
180
+ */
181
+ queueForSync(type, payload) {
182
+ if (!this.config.enableSyncQueue) return;
183
+ const effect = {
184
+ id: generateEffectId(),
185
+ timestamp: Date.now(),
186
+ type,
187
+ payload,
188
+ retries: 0,
189
+ maxRetries: 3
190
+ };
191
+ this.state.syncQueue.push(effect);
192
+ if (this.state.syncQueue.length > (this.config.maxQueueSize ?? 100)) {
193
+ this.state.syncQueue.shift();
194
+ }
195
+ this.saveSyncQueue();
196
+ this.config.onEffectQueued?.(effect);
197
+ this.config.onQueueChange?.(this.state.syncQueue);
198
+ }
199
+ /**
200
+ * Execute client effects immediately.
201
+ */
202
+ executeClientEffects(effects) {
203
+ if (effects.length === 0) return;
204
+ executeClientEffects(effects, this.config);
205
+ this.state.localEffectsProcessed += effects.length;
206
+ }
207
+ /**
208
+ * Process an event in offline mode.
209
+ *
210
+ * Returns a simulated EventResponse with mock data.
211
+ * Client effects are executed immediately.
212
+ * Server effects are queued for sync.
213
+ */
214
+ processEventOffline(event, payload, effects) {
215
+ const clientEffects = [];
216
+ const fetchedData = {};
217
+ if (effects) {
218
+ for (const effect of effects) {
219
+ if (!Array.isArray(effect) || effect.length < 1) continue;
220
+ const [type, ...args] = effect;
221
+ switch (type) {
222
+ // Client effects - execute immediately
223
+ case "render-ui":
224
+ case "navigate":
225
+ case "notify":
226
+ case "emit":
227
+ clientEffects.push(effect);
228
+ break;
229
+ // Fetch effect - use mock data
230
+ case "fetch": {
231
+ const [entityName, _query] = args;
232
+ if (typeof entityName === "string" && this.config.mockDataProvider) {
233
+ fetchedData[entityName] = this.config.mockDataProvider(entityName);
234
+ }
235
+ break;
236
+ }
237
+ // Server effects - queue for sync
238
+ case "persist":
239
+ case "call-service":
240
+ case "spawn":
241
+ case "despawn":
242
+ this.queueForSync(type, { args, event, payload });
243
+ break;
244
+ default:
245
+ console.warn(`[OfflineExecutor] Unknown effect type: ${type}`);
246
+ }
247
+ }
248
+ }
249
+ if (clientEffects.length > 0) {
250
+ this.executeClientEffects(clientEffects);
251
+ }
252
+ return {
253
+ success: true,
254
+ data: Object.keys(fetchedData).length > 0 ? fetchedData : void 0,
255
+ clientEffects: clientEffects.length > 0 ? clientEffects : void 0
256
+ };
257
+ }
258
+ /**
259
+ * Sync pending effects to server.
260
+ *
261
+ * @param serverUrl - Base URL for the orbital server
262
+ * @param authToken - Optional auth token for requests
263
+ * @returns Number of successfully synced effects
264
+ */
265
+ async syncPendingEffects(serverUrl, authToken) {
266
+ if (this.state.syncQueue.length === 0) {
267
+ return 0;
268
+ }
269
+ this.state.lastSyncAttempt = Date.now();
270
+ let syncedCount = 0;
271
+ const failedEffects = [];
272
+ const headers = {
273
+ "Content-Type": "application/json"
274
+ };
275
+ if (authToken) {
276
+ headers["Authorization"] = `Bearer ${authToken}`;
277
+ }
278
+ for (const effect of this.state.syncQueue) {
279
+ try {
280
+ const response = await fetch(`${serverUrl}/sync-effect`, {
281
+ method: "POST",
282
+ headers,
283
+ body: JSON.stringify({
284
+ type: effect.type,
285
+ payload: effect.payload,
286
+ offlineId: effect.id,
287
+ offlineTimestamp: effect.timestamp
288
+ })
289
+ });
290
+ if (response.ok) {
291
+ syncedCount++;
292
+ } else {
293
+ effect.retries++;
294
+ if (effect.retries < effect.maxRetries) {
295
+ failedEffects.push(effect);
296
+ }
297
+ }
298
+ } catch (error) {
299
+ effect.retries++;
300
+ if (effect.retries < effect.maxRetries) {
301
+ failedEffects.push(effect);
302
+ }
303
+ }
304
+ }
305
+ this.state.syncQueue = failedEffects;
306
+ this.state.effectsSynced += syncedCount;
307
+ if (syncedCount > 0) {
308
+ this.state.lastSuccessfulSync = Date.now();
309
+ }
310
+ this.saveSyncQueue();
311
+ this.config.onQueueChange?.(this.state.syncQueue);
312
+ return syncedCount;
313
+ }
314
+ /**
315
+ * Get current executor state
316
+ */
317
+ getState() {
318
+ return { ...this.state };
319
+ }
320
+ /**
321
+ * Get number of pending effects
322
+ */
323
+ getPendingCount() {
324
+ return this.state.syncQueue.length;
325
+ }
326
+ /**
327
+ * Clear the sync queue
328
+ */
329
+ clearQueue() {
330
+ this.state.syncQueue = [];
331
+ this.saveSyncQueue();
332
+ this.config.onQueueChange?.(this.state.syncQueue);
333
+ }
334
+ /**
335
+ * Dispose the executor and clean up listeners
336
+ */
337
+ dispose() {
338
+ if (typeof window !== "undefined") {
339
+ window.removeEventListener("online", this.handleOnline);
340
+ window.removeEventListener("offline", this.handleOffline);
341
+ }
342
+ }
343
+ };
344
+ function createOfflineExecutor(config) {
345
+ return new OfflineExecutor(config);
346
+ }
347
+ function useOfflineExecutor(options) {
348
+ const executorRef = useRef(null);
349
+ const [state, setState] = useState({
350
+ isOffline: false,
351
+ syncQueue: [],
352
+ localEffectsProcessed: 0,
353
+ effectsSynced: 0
354
+ });
355
+ useEffect(() => {
356
+ const executor = new OfflineExecutor({
357
+ ...options,
358
+ onQueueChange: (queue) => {
359
+ setState(executor.getState());
360
+ options.onQueueChange?.(queue);
361
+ }
362
+ });
363
+ executorRef.current = executor;
364
+ setState(executor.getState());
365
+ return () => {
366
+ executor.dispose();
367
+ executorRef.current = null;
368
+ };
369
+ }, []);
370
+ useEffect(() => {
371
+ if (!options.autoSync || !options.serverUrl) return;
372
+ const handleOnline = async () => {
373
+ if (executorRef.current) {
374
+ await executorRef.current.syncPendingEffects(
375
+ options.serverUrl,
376
+ options.authToken
377
+ );
378
+ setState(executorRef.current.getState());
379
+ }
380
+ };
381
+ window.addEventListener("online", handleOnline);
382
+ return () => window.removeEventListener("online", handleOnline);
383
+ }, [options.autoSync, options.serverUrl, options.authToken]);
384
+ const executeEffects = useCallback((effects) => {
385
+ executorRef.current?.executeClientEffects(effects);
386
+ if (executorRef.current) {
387
+ setState(executorRef.current.getState());
388
+ }
389
+ }, []);
390
+ const processEventOffline = useCallback(
391
+ (event, payload, effects) => {
392
+ const result = executorRef.current?.processEventOffline(event, payload, effects);
393
+ if (executorRef.current) {
394
+ setState(executorRef.current.getState());
395
+ }
396
+ return result ?? { success: false, error: "Executor not initialized" };
397
+ },
398
+ []
399
+ );
400
+ const sync = useCallback(async () => {
401
+ if (!executorRef.current || !options.serverUrl) return 0;
402
+ const count = await executorRef.current.syncPendingEffects(
403
+ options.serverUrl,
404
+ options.authToken
405
+ );
406
+ setState(executorRef.current.getState());
407
+ return count;
408
+ }, [options.serverUrl, options.authToken]);
409
+ const clearQueue = useCallback(() => {
410
+ executorRef.current?.clearQueue();
411
+ if (executorRef.current) {
412
+ setState(executorRef.current.getState());
413
+ }
414
+ }, []);
415
+ return {
416
+ state,
417
+ isOffline: state.isOffline,
418
+ pendingCount: state.syncQueue.length,
419
+ executeClientEffects: executeEffects,
420
+ processEventOffline,
421
+ sync,
422
+ clearQueue
423
+ };
424
+ }
425
+
426
+ export { OfflineExecutor, createOfflineExecutor, executeClientEffects, filterEffectsByType, getEmitEffects, getNavigateEffects, getNotifyEffects, getRenderUIEffects, parseClientEffect, parseClientEffects, useOfflineExecutor };