@sweidos/eidos 1.0.25 → 1.0.31

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,42 @@
1
+ const i = "@eidos:queue";
2
+ class l {
3
+ constructor(t) {
4
+ this.storage = t;
5
+ }
6
+ async readAll() {
7
+ try {
8
+ const t = await this.storage.getItem(i);
9
+ return t ? JSON.parse(t) : [];
10
+ } catch {
11
+ return [];
12
+ }
13
+ }
14
+ async writeAll(t) {
15
+ await this.storage.setItem(i, JSON.stringify(t));
16
+ }
17
+ async add(t) {
18
+ const e = await this.readAll();
19
+ e.push(t), await this.writeAll(e);
20
+ }
21
+ async getAll() {
22
+ return this.readAll();
23
+ }
24
+ async getPending() {
25
+ return (await this.readAll()).filter((e) => e.status === "pending" || e.status === "failed");
26
+ }
27
+ async update(t, e) {
28
+ const a = await this.readAll(), s = a.findIndex((r) => r.id === t);
29
+ s !== -1 && (a[s] = { ...a[s], ...e }), await this.writeAll(a);
30
+ }
31
+ async remove(t) {
32
+ const e = await this.readAll();
33
+ await this.writeAll(e.filter((a) => a.id !== t));
34
+ }
35
+ async clear() {
36
+ await this.storage.removeItem(i);
37
+ }
38
+ }
39
+ export {
40
+ l as AsyncStorageQueueStorage
41
+ };
42
+ //# sourceMappingURL=async-storage-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"async-storage-adapter.js","sources":["../src/async-storage-adapter.ts"],"sourcesContent":["import type { ActionQueueItem } from './types'\nimport type { QueueStorage } from './queue-storage'\n\n/** Minimal subset of @react-native-async-storage/async-storage (or any compatible key-value store). */\nexport interface AsyncStorageLike {\n getItem(key: string): Promise<string | null>\n setItem(key: string, value: string): Promise<void>\n removeItem(key: string): Promise<void>\n}\n\nconst QUEUE_KEY = '@eidos:queue'\n\n/**\n * QueueStorage implementation backed by any AsyncStorage-compatible API.\n * Pass the AsyncStorage singleton from @react-native-async-storage/async-storage\n * (or MMKV, SQLite, or any store that satisfies AsyncStorageLike).\n */\nexport class AsyncStorageQueueStorage implements QueueStorage {\n constructor(private readonly storage: AsyncStorageLike) {}\n\n private async readAll(): Promise<ActionQueueItem[]> {\n try {\n const raw = await this.storage.getItem(QUEUE_KEY)\n if (!raw) return []\n return JSON.parse(raw) as ActionQueueItem[]\n } catch {\n return []\n }\n }\n\n private async writeAll(items: ActionQueueItem[]): Promise<void> {\n await this.storage.setItem(QUEUE_KEY, JSON.stringify(items))\n }\n\n async add(item: ActionQueueItem): Promise<void> {\n const items = await this.readAll()\n items.push(item)\n await this.writeAll(items)\n }\n\n async getAll(): Promise<ActionQueueItem[]> {\n return this.readAll()\n }\n\n async getPending(): Promise<ActionQueueItem[]> {\n const items = await this.readAll()\n return items.filter((i) => i.status === 'pending' || i.status === 'failed')\n }\n\n async update(id: string, patch: Partial<ActionQueueItem>): Promise<void> {\n const items = await this.readAll()\n const idx = items.findIndex((i) => i.id === id)\n if (idx !== -1) items[idx] = { ...items[idx], ...patch }\n await this.writeAll(items)\n }\n\n async remove(id: string): Promise<void> {\n const items = await this.readAll()\n await this.writeAll(items.filter((i) => i.id !== id))\n }\n\n async clear(): Promise<void> {\n await this.storage.removeItem(QUEUE_KEY)\n }\n}\n"],"names":["QUEUE_KEY","AsyncStorageQueueStorage","storage","raw","items","item","i","id","patch","idx"],"mappings":"AAUA,MAAMA,IAAY;AAOX,MAAMC,EAAiD;AAAA,EAC5D,YAA6BC,GAA2B;AAA3B,SAAA,UAAAA;AAAA,EAA4B;AAAA,EAEzD,MAAc,UAAsC;AAClD,QAAI;AACF,YAAMC,IAAM,MAAM,KAAK,QAAQ,QAAQH,CAAS;AAChD,aAAKG,IACE,KAAK,MAAMA,CAAG,IADJ,CAAA;AAAA,IAEnB,QAAQ;AACN,aAAO,CAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAc,SAASC,GAAyC;AAC9D,UAAM,KAAK,QAAQ,QAAQJ,GAAW,KAAK,UAAUI,CAAK,CAAC;AAAA,EAC7D;AAAA,EAEA,MAAM,IAAIC,GAAsC;AAC9C,UAAMD,IAAQ,MAAM,KAAK,QAAA;AACzB,IAAAA,EAAM,KAAKC,CAAI,GACf,MAAM,KAAK,SAASD,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,SAAqC;AACzC,WAAO,KAAK,QAAA;AAAA,EACd;AAAA,EAEA,MAAM,aAAyC;AAE7C,YADc,MAAM,KAAK,QAAA,GACZ,OAAO,CAACE,MAAMA,EAAE,WAAW,aAAaA,EAAE,WAAW,QAAQ;AAAA,EAC5E;AAAA,EAEA,MAAM,OAAOC,GAAYC,GAAgD;AACvE,UAAMJ,IAAQ,MAAM,KAAK,QAAA,GACnBK,IAAML,EAAM,UAAU,CAACE,MAAMA,EAAE,OAAOC,CAAE;AAC9C,IAAIE,MAAQ,OAAIL,EAAMK,CAAG,IAAI,EAAE,GAAGL,EAAMK,CAAG,GAAG,GAAGD,EAAA,IACjD,MAAM,KAAK,SAASJ,CAAK;AAAA,EAC3B;AAAA,EAEA,MAAM,OAAOG,GAA2B;AACtC,UAAMH,IAAQ,MAAM,KAAK,QAAA;AACzB,UAAM,KAAK,SAASA,EAAM,OAAO,CAACE,MAAMA,EAAE,OAAOC,CAAE,CAAC;AAAA,EACtD;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,KAAK,QAAQ,WAAWP,CAAS;AAAA,EACzC;AACF;"}
@@ -0,0 +1,2 @@
1
+ export { EidosDevtools } from './react/Devtools';
2
+ export type { EidosDevtoolsProps } from './react/Devtools';
@@ -0,0 +1,627 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useSyncExternalStore, useState, useCallback } from "react";
4
+ let _state;
5
+ const _listeners = /* @__PURE__ */ new Set();
6
+ function _notify() {
7
+ _listeners.forEach((fn) => fn());
8
+ }
9
+ function _set(updater) {
10
+ _state = { ..._state, ...updater(_state) };
11
+ _notify();
12
+ }
13
+ _state = {
14
+ // navigator.onLine is undefined in React Native — default to true unless explicitly false
15
+ isOnline: typeof navigator === "undefined" || navigator.onLine !== false,
16
+ swStatus: "idle",
17
+ swError: void 0,
18
+ resources: {},
19
+ queue: [],
20
+ setOnline: (isOnline) => _set(() => ({ isOnline })),
21
+ setSwStatus: (swStatus, swError) => _set(() => ({ swStatus, swError })),
22
+ registerResource: (url, entry) => _set((s) => ({ resources: { ...s.resources, [url]: entry } })),
23
+ updateResource: (url, update) => _set((s) => ({
24
+ resources: {
25
+ ...s.resources,
26
+ [url]: s.resources[url] ? { ...s.resources[url], ...update } : s.resources[url]
27
+ }
28
+ })),
29
+ unregisterResource: (url) => _set((s) => {
30
+ const { [url]: _removed, ...rest } = s.resources;
31
+ return { resources: rest };
32
+ }),
33
+ addQueueItem: (item) => _set((s) => ({ queue: [...s.queue, item] })),
34
+ updateQueueItem: (id, update) => _set((s) => ({
35
+ queue: s.queue.map((item) => item.id === id ? { ...item, ...update } : item)
36
+ })),
37
+ batchUpdateQueueItems: (updates) => _set((s) => {
38
+ const map = new Map(updates.map((u) => [u.id, u.update]));
39
+ return {
40
+ queue: s.queue.map((item) => {
41
+ const u = map.get(item.id);
42
+ return u ? { ...item, ...u } : item;
43
+ })
44
+ };
45
+ }),
46
+ removeQueueItem: (id) => _set((s) => ({ queue: s.queue.filter((item) => item.id !== id) })),
47
+ hydrateQueue: (items) => _set(() => ({ queue: items }))
48
+ };
49
+ function _getState() {
50
+ return _state;
51
+ }
52
+ function _subscribe(listener) {
53
+ _listeners.add(listener);
54
+ return () => {
55
+ _listeners.delete(listener);
56
+ };
57
+ }
58
+ const useEidosStore = {
59
+ getState: _getState,
60
+ subscribe: _subscribe,
61
+ // Test/devtools helper — merges partial state, preserves action methods.
62
+ setState: (partial) => {
63
+ const update = typeof partial === "function" ? partial(_state) : partial;
64
+ _state = { ..._state, ...update };
65
+ _notify();
66
+ }
67
+ };
68
+ function useStore(selector) {
69
+ const fn = selector ?? ((s) => s);
70
+ return useSyncExternalStore(useEidosStore.subscribe, () => fn(useEidosStore.getState()));
71
+ }
72
+ function useEidosResources() {
73
+ return useStore((s) => s.resources);
74
+ }
75
+ function useEidosQueue() {
76
+ return useStore((s) => s.queue);
77
+ }
78
+ function useEidosStatus() {
79
+ const isOnline = useStore((s) => s.isOnline);
80
+ const swStatus = useStore((s) => s.swStatus);
81
+ const swError = useStore((s) => s.swError);
82
+ return { isOnline, swStatus, swError };
83
+ }
84
+ function useEidosQueueStats() {
85
+ const encoded = useStore((s) => {
86
+ let pending = 0, failed = 0, replaying = 0;
87
+ for (const q of s.queue) {
88
+ if (q.status === "pending") pending++;
89
+ else if (q.status === "failed") failed++;
90
+ else if (q.status === "replaying") replaying++;
91
+ }
92
+ return `${pending},${failed},${replaying},${s.queue.length}`;
93
+ });
94
+ const [p, f, r, t] = encoded.split(",");
95
+ return { pending: +p, failed: +f, replaying: +r, total: +t };
96
+ }
97
+ function setOfflineSimulation(enabled) {
98
+ useEidosStore.getState().setOnline(!enabled);
99
+ }
100
+ const DB_NAME = "eidos";
101
+ const DB_VERSION = 1;
102
+ const QUEUE_STORE = "action-queue";
103
+ let _db = null;
104
+ function openDB() {
105
+ if (_db) return Promise.resolve(_db);
106
+ return new Promise((resolve, reject) => {
107
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
108
+ req.onupgradeneeded = (event) => {
109
+ const db = event.target.result;
110
+ if (!db.objectStoreNames.contains(QUEUE_STORE)) {
111
+ const store = db.createObjectStore(QUEUE_STORE, { keyPath: "id" });
112
+ store.createIndex("status", "status", { unique: false });
113
+ store.createIndex("actionId", "actionId", { unique: false });
114
+ }
115
+ };
116
+ req.onsuccess = () => {
117
+ _db = req.result;
118
+ resolve(req.result);
119
+ };
120
+ req.onerror = () => reject(req.error);
121
+ });
122
+ }
123
+ async function idbAddToQueue(item) {
124
+ const db = await openDB();
125
+ return new Promise((resolve, reject) => {
126
+ const tx = db.transaction(QUEUE_STORE, "readwrite");
127
+ tx.objectStore(QUEUE_STORE).add(item);
128
+ tx.oncomplete = () => resolve();
129
+ tx.onerror = () => reject(tx.error);
130
+ });
131
+ }
132
+ async function idbGetQueue() {
133
+ const db = await openDB();
134
+ return new Promise((resolve, reject) => {
135
+ const tx = db.transaction(QUEUE_STORE, "readonly");
136
+ const req = tx.objectStore(QUEUE_STORE).getAll();
137
+ req.onsuccess = () => resolve(req.result);
138
+ req.onerror = () => reject(req.error);
139
+ });
140
+ }
141
+ async function idbUpdateQueueItem(id, update) {
142
+ const db = await openDB();
143
+ return new Promise((resolve, reject) => {
144
+ const tx = db.transaction(QUEUE_STORE, "readwrite");
145
+ const store = tx.objectStore(QUEUE_STORE);
146
+ const get = store.get(id);
147
+ get.onsuccess = () => {
148
+ if (get.result) {
149
+ store.put({ ...get.result, ...update });
150
+ }
151
+ };
152
+ tx.oncomplete = () => resolve();
153
+ tx.onerror = () => reject(tx.error);
154
+ });
155
+ }
156
+ async function idbRemoveFromQueue(id) {
157
+ const db = await openDB();
158
+ return new Promise((resolve, reject) => {
159
+ const tx = db.transaction(QUEUE_STORE, "readwrite");
160
+ tx.objectStore(QUEUE_STORE).delete(id);
161
+ tx.oncomplete = () => resolve();
162
+ tx.onerror = () => reject(tx.error);
163
+ });
164
+ }
165
+ async function idbGetPendingItems() {
166
+ const db = await openDB();
167
+ return new Promise((resolve, reject) => {
168
+ const tx = db.transaction(QUEUE_STORE, "readonly");
169
+ const index = tx.objectStore(QUEUE_STORE).index("status");
170
+ const results = [];
171
+ let done = 0;
172
+ function finish(err) {
173
+ if (err) {
174
+ reject(err);
175
+ return;
176
+ }
177
+ if (++done === 2) resolve(results);
178
+ }
179
+ const pendingReq = index.openCursor(IDBKeyRange.only("pending"));
180
+ pendingReq.onsuccess = (e) => {
181
+ const cursor = e.target.result;
182
+ if (cursor) {
183
+ results.push(cursor.value);
184
+ cursor.continue();
185
+ } else finish();
186
+ };
187
+ pendingReq.onerror = () => finish(pendingReq.error);
188
+ const failedReq = index.openCursor(IDBKeyRange.only("failed"));
189
+ failedReq.onsuccess = (e) => {
190
+ const cursor = e.target.result;
191
+ if (cursor) {
192
+ results.push(cursor.value);
193
+ cursor.continue();
194
+ } else finish();
195
+ };
196
+ failedReq.onerror = () => finish(failedReq.error);
197
+ });
198
+ }
199
+ async function idbClearQueue() {
200
+ const db = await openDB();
201
+ return new Promise((resolve, reject) => {
202
+ const tx = db.transaction(QUEUE_STORE, "readwrite");
203
+ tx.objectStore(QUEUE_STORE).clear();
204
+ tx.oncomplete = () => resolve();
205
+ tx.onerror = () => reject(tx.error);
206
+ });
207
+ }
208
+ const _actionRegistry = /* @__PURE__ */ new Map();
209
+ const _rollbackRegistry = /* @__PURE__ */ new Map();
210
+ const _conflictRegistry = /* @__PURE__ */ new Map();
211
+ const _idbFallback = {
212
+ add: (item) => idbAddToQueue(item),
213
+ getAll: () => idbGetQueue(),
214
+ getPending: () => idbGetPendingItems(),
215
+ update: (id, patch) => idbUpdateQueueItem(id, patch),
216
+ remove: (id) => idbRemoveFromQueue(id),
217
+ clear: () => idbClearQueue()
218
+ };
219
+ function qs() {
220
+ return _idbFallback;
221
+ }
222
+ function isClientError(err) {
223
+ if (err instanceof Response) return err.status >= 400 && err.status < 500;
224
+ if (typeof err === "object" && err !== null) {
225
+ const s = err.status;
226
+ if (typeof s === "number") return s >= 400 && s < 500;
227
+ }
228
+ return false;
229
+ }
230
+ function backoffMs(retryCount) {
231
+ const base = Math.min(2e3 * 2 ** retryCount, 3e5);
232
+ return base * (0.8 + Math.random() * 0.4);
233
+ }
234
+ let _replaying = false;
235
+ async function replayQueue() {
236
+ const store = useEidosStore.getState();
237
+ if (!store.isOnline || _replaying) {
238
+ return { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
239
+ }
240
+ _replaying = true;
241
+ try {
242
+ return await _doReplayQueue(store);
243
+ } finally {
244
+ _replaying = false;
245
+ }
246
+ }
247
+ async function _replayItem(item, store) {
248
+ var _a;
249
+ const fn = _actionRegistry.get(item.actionId);
250
+ if (!fn) return "skipped";
251
+ try {
252
+ await fn(...item.args);
253
+ const completedAt = Date.now();
254
+ store.updateQueueItem(item.id, { status: "succeeded", completedAt });
255
+ await qs().update(item.id, { status: "succeeded", completedAt });
256
+ setTimeout(() => {
257
+ store.removeQueueItem(item.id);
258
+ qs().remove(item.id);
259
+ }, 3e3);
260
+ return "succeeded";
261
+ } catch (err) {
262
+ if (isClientError(err)) {
263
+ const onConflict = _conflictRegistry.get(item.actionId);
264
+ if (onConflict) {
265
+ const resolution = onConflict(err, item.args);
266
+ if (resolution === "skip") {
267
+ store.removeQueueItem(item.id);
268
+ await qs().remove(item.id);
269
+ return "conflicted";
270
+ }
271
+ }
272
+ }
273
+ const retryCount = item.retryCount + 1;
274
+ if (retryCount >= item.maxRetries) {
275
+ store.updateQueueItem(item.id, { status: "failed", error: String(err), retryCount });
276
+ await qs().update(item.id, { status: "failed", error: String(err), retryCount });
277
+ (_a = _rollbackRegistry.get(item.actionId)) == null ? void 0 : _a(...item.args);
278
+ return "failed";
279
+ } else {
280
+ const nextRetryAt = Date.now() + backoffMs(retryCount);
281
+ store.updateQueueItem(item.id, { status: "pending", retryCount, nextRetryAt });
282
+ await qs().update(item.id, { status: "pending", retryCount, nextRetryAt });
283
+ return "retrying";
284
+ }
285
+ }
286
+ }
287
+ async function _replayTier(items, store, result) {
288
+ if (items.length === 0) return;
289
+ const replayable = items.filter((item) => _actionRegistry.has(item.actionId));
290
+ result.skipped += items.length - replayable.length;
291
+ if (replayable.length > 0) {
292
+ store.batchUpdateQueueItems(replayable.map((item) => ({ id: item.id, update: { status: "replaying" } })));
293
+ for (const item of replayable) {
294
+ qs().update(item.id, { status: "replaying" });
295
+ }
296
+ }
297
+ const outcomes = await Promise.allSettled(replayable.map((item) => _replayItem(item, store)));
298
+ for (const o of outcomes) {
299
+ const outcome = o.status === "fulfilled" ? o.value : "failed";
300
+ if (outcome === "skipped") {
301
+ result.skipped++;
302
+ } else if (outcome === "conflicted") {
303
+ result.conflicted++;
304
+ } else {
305
+ result.attempted++;
306
+ result[outcome]++;
307
+ }
308
+ }
309
+ }
310
+ async function _doReplayQueue(store) {
311
+ const candidates = await qs().getPending();
312
+ const now = Date.now();
313
+ const pending = candidates.filter((item) => !item.nextRetryAt || item.nextRetryAt <= now);
314
+ const result = { attempted: 0, succeeded: 0, failed: 0, retrying: 0, skipped: 0, conflicted: 0 };
315
+ for (const tier of ["high", "normal", "low"]) {
316
+ const tierItems = pending.filter((item) => (item.priority ?? "normal") === tier);
317
+ await _replayTier(tierItems, store, result);
318
+ }
319
+ return result;
320
+ }
321
+ async function clearQueue() {
322
+ await qs().clear();
323
+ useEidosStore.getState().hydrateQueue([]);
324
+ }
325
+ const C = {
326
+ bg: "#0f1117",
327
+ surface: "#1a1d27",
328
+ border: "#2a2d3a",
329
+ text: "#e2e8f0",
330
+ muted: "#64748b",
331
+ green: "#22c55e",
332
+ red: "#ef4444",
333
+ yellow: "#f59e0b",
334
+ blue: "#3b82f6",
335
+ purple: "#a855f7",
336
+ cyan: "#06b6d4"
337
+ };
338
+ function pill(color) {
339
+ return {
340
+ display: "inline-flex",
341
+ alignItems: "center",
342
+ padding: "1px 7px",
343
+ borderRadius: 9999,
344
+ fontSize: 10,
345
+ fontWeight: 600,
346
+ background: `${color}22`,
347
+ color,
348
+ border: `1px solid ${color}44`,
349
+ fontFamily: "inherit"
350
+ };
351
+ }
352
+ function btn(variant = "ghost") {
353
+ const base = {
354
+ cursor: "pointer",
355
+ border: "none",
356
+ borderRadius: 6,
357
+ padding: "3px 10px",
358
+ fontSize: 11,
359
+ fontWeight: 500,
360
+ fontFamily: "inherit",
361
+ transition: "opacity 0.15s"
362
+ };
363
+ if (variant === "danger") return { ...base, background: `${C.red}22`, color: C.red };
364
+ if (variant === "primary") return { ...base, background: `${C.blue}22`, color: C.blue };
365
+ return { ...base, background: C.surface, color: C.muted };
366
+ }
367
+ function swStatusColor(s) {
368
+ if (s === "active") return C.green;
369
+ if (s === "registering") return C.yellow;
370
+ if (s === "error" || s === "unsupported") return C.red;
371
+ return C.muted;
372
+ }
373
+ function queueStatusColor(s) {
374
+ if (s === "succeeded") return C.green;
375
+ if (s === "failed") return C.red;
376
+ if (s === "replaying") return C.yellow;
377
+ return C.blue;
378
+ }
379
+ function resourceStatusColor(s) {
380
+ if (s === "fresh") return C.green;
381
+ if (s === "stale" || s === "offline") return C.yellow;
382
+ if (s === "error") return C.red;
383
+ if (s === "fetching") return C.cyan;
384
+ return C.muted;
385
+ }
386
+ function positionStyle(p) {
387
+ const base = { position: "fixed", zIndex: 99999 };
388
+ if (p === "bottom-left") return { ...base, bottom: 16, left: 16 };
389
+ if (p === "top-right") return { ...base, top: 16, right: 16 };
390
+ if (p === "top-left") return { ...base, top: 16, left: 16 };
391
+ return { ...base, bottom: 16, right: 16 };
392
+ }
393
+ function EidosDevtools({ position = "bottom-right", defaultOpen = false }) {
394
+ const [open, setOpen] = useState(defaultOpen);
395
+ const [tab, setTab] = useState("queue");
396
+ const [simOffline, setSimOffline] = useState(false);
397
+ const { isOnline, swStatus, swError } = useEidosStatus();
398
+ const queue = useEidosQueue();
399
+ const { pending, failed, replaying } = useEidosQueueStats();
400
+ const resources = useEidosResources();
401
+ const resourceList = Object.values(resources);
402
+ const badgeCount = pending + failed + replaying;
403
+ const toggleOffline = useCallback(() => {
404
+ const next = !simOffline;
405
+ setSimOffline(next);
406
+ setOfflineSimulation(next);
407
+ }, [simOffline]);
408
+ const handleReplay = useCallback(() => {
409
+ void replayQueue();
410
+ }, []);
411
+ const handleClear = useCallback(() => {
412
+ void clearQueue();
413
+ }, []);
414
+ const toggleBtn = /* @__PURE__ */ jsxs(
415
+ "button",
416
+ {
417
+ onClick: () => setOpen((v) => !v),
418
+ title: "Eidos Devtools",
419
+ style: {
420
+ display: "flex",
421
+ alignItems: "center",
422
+ gap: 6,
423
+ padding: "5px 10px",
424
+ background: C.bg,
425
+ border: `1px solid ${C.border}`,
426
+ borderRadius: 9999,
427
+ cursor: "pointer",
428
+ color: C.text,
429
+ fontSize: 11,
430
+ fontWeight: 600,
431
+ fontFamily: 'ui-monospace, "Cascadia Code", "Fira Mono", monospace',
432
+ boxShadow: "0 2px 12px rgba(0,0,0,0.5)",
433
+ userSelect: "none"
434
+ },
435
+ children: [
436
+ /* @__PURE__ */ jsx("span", { style: { color: isOnline ? C.green : C.red, fontSize: 8 }, children: "●" }),
437
+ /* @__PURE__ */ jsx("span", { style: { color: C.cyan }, children: "⚡" }),
438
+ /* @__PURE__ */ jsx("span", { children: "eidos" }),
439
+ badgeCount > 0 && /* @__PURE__ */ jsx("span", { style: {
440
+ background: failed > 0 ? C.red : C.yellow,
441
+ color: "#fff",
442
+ borderRadius: 9999,
443
+ minWidth: 16,
444
+ height: 16,
445
+ display: "inline-flex",
446
+ alignItems: "center",
447
+ justifyContent: "center",
448
+ fontSize: 9,
449
+ fontWeight: 700,
450
+ padding: "0 4px"
451
+ }, children: badgeCount })
452
+ ]
453
+ }
454
+ );
455
+ if (!open) {
456
+ return /* @__PURE__ */ jsx("div", { style: positionStyle(position), children: toggleBtn });
457
+ }
458
+ return /* @__PURE__ */ jsxs("div", { style: { ...positionStyle(position), display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 6 }, children: [
459
+ /* @__PURE__ */ jsxs("div", { style: {
460
+ width: 340,
461
+ maxHeight: 480,
462
+ display: "flex",
463
+ flexDirection: "column",
464
+ background: C.bg,
465
+ border: `1px solid ${C.border}`,
466
+ borderRadius: 10,
467
+ overflow: "hidden",
468
+ boxShadow: "0 8px 32px rgba(0,0,0,0.6)",
469
+ fontFamily: 'ui-monospace, "Cascadia Code", "Fira Mono", monospace',
470
+ fontSize: 11,
471
+ color: C.text
472
+ }, children: [
473
+ /* @__PURE__ */ jsxs("div", { style: {
474
+ display: "flex",
475
+ alignItems: "center",
476
+ gap: 8,
477
+ padding: "8px 12px",
478
+ background: C.surface,
479
+ borderBottom: `1px solid ${C.border}`,
480
+ flexShrink: 0
481
+ }, children: [
482
+ /* @__PURE__ */ jsx("span", { style: { color: C.cyan, fontSize: 13 }, children: "⚡" }),
483
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 700, fontSize: 12, color: C.text, flex: 1 }, children: "Eidos Devtools" }),
484
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
485
+ /* @__PURE__ */ jsx("span", { style: pill(isOnline ? C.green : C.red), children: isOnline ? "online" : "offline" }),
486
+ /* @__PURE__ */ jsx(
487
+ "button",
488
+ {
489
+ onClick: toggleOffline,
490
+ title: simOffline ? "Disable offline simulation" : "Enable offline simulation",
491
+ style: {
492
+ ...btn("ghost"),
493
+ background: simOffline ? `${C.yellow}22` : C.surface,
494
+ color: simOffline ? C.yellow : C.muted,
495
+ border: `1px solid ${simOffline ? C.yellow + "44" : C.border}`,
496
+ fontSize: 10
497
+ },
498
+ children: simOffline ? "📡 simulating" : "✈ sim offline"
499
+ }
500
+ )
501
+ ] })
502
+ ] }),
503
+ /* @__PURE__ */ jsxs("div", { style: {
504
+ display: "flex",
505
+ alignItems: "center",
506
+ gap: 8,
507
+ padding: "6px 12px",
508
+ borderBottom: `1px solid ${C.border}`,
509
+ flexShrink: 0,
510
+ background: C.bg
511
+ }, children: [
512
+ /* @__PURE__ */ jsx("span", { style: { color: C.muted, fontSize: 10 }, children: "SW" }),
513
+ /* @__PURE__ */ jsx("span", { style: pill(swStatusColor(swStatus)), children: swStatus }),
514
+ swError && /* @__PURE__ */ jsx("span", { style: { color: C.red, fontSize: 10, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: swError })
515
+ ] }),
516
+ /* @__PURE__ */ jsx("div", { style: {
517
+ display: "flex",
518
+ borderBottom: `1px solid ${C.border}`,
519
+ flexShrink: 0,
520
+ background: C.surface
521
+ }, children: ["queue", "cache"].map((t) => /* @__PURE__ */ jsx(
522
+ "button",
523
+ {
524
+ onClick: () => setTab(t),
525
+ style: {
526
+ flex: 1,
527
+ padding: "6px 0",
528
+ background: "none",
529
+ border: "none",
530
+ borderBottom: tab === t ? `2px solid ${C.cyan}` : "2px solid transparent",
531
+ cursor: "pointer",
532
+ color: tab === t ? C.cyan : C.muted,
533
+ fontSize: 11,
534
+ fontWeight: tab === t ? 600 : 400,
535
+ fontFamily: "inherit",
536
+ textTransform: "uppercase",
537
+ letterSpacing: "0.05em"
538
+ },
539
+ children: t === "queue" ? `Queue (${queue.length})` : `Cache (${resourceList.length})`
540
+ },
541
+ t
542
+ )) }),
543
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, overflowY: "auto", minHeight: 0 }, children: tab === "queue" ? /* @__PURE__ */ jsx(QueueTab, { queue, onReplay: handleReplay, onClear: handleClear }) : /* @__PURE__ */ jsx(CacheTab, { resources: resourceList }) })
544
+ ] }),
545
+ toggleBtn
546
+ ] });
547
+ }
548
+ function QueueTab({
549
+ queue,
550
+ onReplay,
551
+ onClear
552
+ }) {
553
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%" }, children: [
554
+ /* @__PURE__ */ jsxs("div", { style: {
555
+ display: "flex",
556
+ gap: 6,
557
+ padding: "8px 12px",
558
+ borderBottom: `1px solid ${C.border}`,
559
+ flexShrink: 0
560
+ }, children: [
561
+ /* @__PURE__ */ jsx("button", { onClick: onReplay, style: btn("primary"), children: "▶ Replay queue" }),
562
+ /* @__PURE__ */ jsx("button", { onClick: onClear, style: btn("danger"), children: "✕ Clear queue" }),
563
+ /* @__PURE__ */ jsxs("span", { style: { marginLeft: "auto", color: C.muted, fontSize: 10, alignSelf: "center" }, children: [
564
+ queue.length,
565
+ " item",
566
+ queue.length !== 1 ? "s" : ""
567
+ ] })
568
+ ] }),
569
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, overflowY: "auto" }, children: queue.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: "20px 12px", textAlign: "center", color: C.muted }, children: "Queue empty" }) : queue.map((item) => /* @__PURE__ */ jsxs("div", { style: {
570
+ display: "flex",
571
+ alignItems: "center",
572
+ gap: 8,
573
+ padding: "7px 12px",
574
+ borderBottom: `1px solid ${C.border}`
575
+ }, children: [
576
+ /* @__PURE__ */ jsx("span", { style: pill(queueStatusColor(item.status)), children: item.status }),
577
+ item.priority && item.priority !== "normal" && /* @__PURE__ */ jsx("span", { style: pill(item.priority === "high" ? C.purple : C.muted), children: item.priority }),
578
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: C.text }, children: item.actionName }),
579
+ item.retryCount > 0 && /* @__PURE__ */ jsxs("span", { style: { color: C.muted, fontSize: 10, flexShrink: 0 }, children: [
580
+ "×",
581
+ item.retryCount,
582
+ "/",
583
+ item.maxRetries
584
+ ] })
585
+ ] }, item.id)) })
586
+ ] });
587
+ }
588
+ function CacheTab({ resources }) {
589
+ return /* @__PURE__ */ jsx("div", { children: resources.length === 0 ? /* @__PURE__ */ jsx("div", { style: { padding: "20px 12px", textAlign: "center", color: C.muted }, children: "No resources registered" }) : resources.map((res) => /* @__PURE__ */ jsxs("div", { style: {
590
+ padding: "7px 12px",
591
+ borderBottom: `1px solid ${C.border}`
592
+ }, children: [
593
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }, children: [
594
+ /* @__PURE__ */ jsx("span", { style: pill(resourceStatusColor(res.status)), children: res.status }),
595
+ /* @__PURE__ */ jsx("span", { style: { color: C.muted, fontSize: 10 }, children: res.strategy.name })
596
+ ] }),
597
+ /* @__PURE__ */ jsx("div", { style: {
598
+ color: C.text,
599
+ overflow: "hidden",
600
+ textOverflow: "ellipsis",
601
+ whiteSpace: "nowrap",
602
+ fontSize: 10,
603
+ marginBottom: 2
604
+ }, children: res.url }),
605
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 10, color: C.muted, fontSize: 10 }, children: [
606
+ /* @__PURE__ */ jsxs("span", { title: "Cache hits", children: [
607
+ "↑",
608
+ res.cacheHits,
609
+ " hit",
610
+ res.cacheHits !== 1 ? "s" : ""
611
+ ] }),
612
+ /* @__PURE__ */ jsxs("span", { title: "Cache misses", children: [
613
+ "↓",
614
+ res.cacheMisses,
615
+ " miss",
616
+ res.cacheMisses !== 1 ? "es" : ""
617
+ ] }),
618
+ res.cachedAt && /* @__PURE__ */ jsxs("span", { title: "Cached at", children: [
619
+ "⏱ ",
620
+ new Date(res.cachedAt).toLocaleTimeString()
621
+ ] })
622
+ ] })
623
+ ] }, res.url)) });
624
+ }
625
+ export {
626
+ EidosDevtools
627
+ };