@affectively/aeon-pages 1.3.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +625 -0
  3. package/examples/basic/aeon.config.ts +39 -0
  4. package/examples/basic/components/Cursor.tsx +86 -0
  5. package/examples/basic/components/OfflineIndicator.tsx +103 -0
  6. package/examples/basic/components/PresenceBar.tsx +77 -0
  7. package/examples/basic/package.json +20 -0
  8. package/examples/basic/pages/index.tsx +80 -0
  9. package/package.json +101 -0
  10. package/packages/analytics/README.md +309 -0
  11. package/packages/analytics/build.ts +35 -0
  12. package/packages/analytics/package.json +50 -0
  13. package/packages/analytics/src/click-tracker.ts +368 -0
  14. package/packages/analytics/src/context-bridge.ts +319 -0
  15. package/packages/analytics/src/data-layer.ts +302 -0
  16. package/packages/analytics/src/gtm-loader.ts +239 -0
  17. package/packages/analytics/src/index.ts +230 -0
  18. package/packages/analytics/src/merkle-tree.ts +489 -0
  19. package/packages/analytics/src/provider.tsx +300 -0
  20. package/packages/analytics/src/types.ts +320 -0
  21. package/packages/analytics/src/use-analytics.ts +296 -0
  22. package/packages/analytics/tsconfig.json +19 -0
  23. package/packages/benchmarks/src/benchmark.test.ts +691 -0
  24. package/packages/cli/dist/index.js +61899 -0
  25. package/packages/cli/package.json +43 -0
  26. package/packages/cli/src/commands/build.test.ts +682 -0
  27. package/packages/cli/src/commands/build.ts +890 -0
  28. package/packages/cli/src/commands/dev.ts +473 -0
  29. package/packages/cli/src/commands/init.ts +409 -0
  30. package/packages/cli/src/commands/start.ts +297 -0
  31. package/packages/cli/src/index.ts +105 -0
  32. package/packages/directives/src/use-aeon.ts +272 -0
  33. package/packages/mcp-server/package.json +51 -0
  34. package/packages/mcp-server/src/index.ts +178 -0
  35. package/packages/mcp-server/src/resources.ts +346 -0
  36. package/packages/mcp-server/src/tools/index.ts +36 -0
  37. package/packages/mcp-server/src/tools/navigation.ts +545 -0
  38. package/packages/mcp-server/tsconfig.json +21 -0
  39. package/packages/react/package.json +40 -0
  40. package/packages/react/src/Link.tsx +388 -0
  41. package/packages/react/src/components/InstallPrompt.tsx +286 -0
  42. package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
  43. package/packages/react/src/components/PushNotifications.tsx +453 -0
  44. package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
  45. package/packages/react/src/hooks/useConflicts.ts +277 -0
  46. package/packages/react/src/hooks/useNetworkState.ts +209 -0
  47. package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
  48. package/packages/react/src/hooks/useServiceWorker.ts +278 -0
  49. package/packages/react/src/hooks.ts +195 -0
  50. package/packages/react/src/index.ts +151 -0
  51. package/packages/react/src/provider.tsx +467 -0
  52. package/packages/react/tsconfig.json +19 -0
  53. package/packages/runtime/README.md +399 -0
  54. package/packages/runtime/build.ts +48 -0
  55. package/packages/runtime/package.json +71 -0
  56. package/packages/runtime/schema.sql +40 -0
  57. package/packages/runtime/src/api-routes.ts +465 -0
  58. package/packages/runtime/src/benchmark.ts +171 -0
  59. package/packages/runtime/src/cache.ts +479 -0
  60. package/packages/runtime/src/durable-object.ts +1341 -0
  61. package/packages/runtime/src/index.ts +360 -0
  62. package/packages/runtime/src/navigation.test.ts +421 -0
  63. package/packages/runtime/src/navigation.ts +422 -0
  64. package/packages/runtime/src/nextjs-adapter.ts +272 -0
  65. package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
  66. package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
  67. package/packages/runtime/src/offline/encryption.test.ts +412 -0
  68. package/packages/runtime/src/offline/encryption.ts +397 -0
  69. package/packages/runtime/src/offline/types.ts +465 -0
  70. package/packages/runtime/src/predictor.ts +371 -0
  71. package/packages/runtime/src/registry.ts +351 -0
  72. package/packages/runtime/src/router/context-extractor.ts +661 -0
  73. package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
  74. package/packages/runtime/src/router/esi-control.ts +541 -0
  75. package/packages/runtime/src/router/esi-cyrano.ts +779 -0
  76. package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
  77. package/packages/runtime/src/router/esi-react.tsx +1065 -0
  78. package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
  79. package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
  80. package/packages/runtime/src/router/esi-translate.ts +503 -0
  81. package/packages/runtime/src/router/esi.ts +666 -0
  82. package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
  83. package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
  84. package/packages/runtime/src/router/index.ts +298 -0
  85. package/packages/runtime/src/router/merkle-capability.ts +473 -0
  86. package/packages/runtime/src/router/speculation.ts +451 -0
  87. package/packages/runtime/src/router/types.ts +630 -0
  88. package/packages/runtime/src/router.test.ts +470 -0
  89. package/packages/runtime/src/router.ts +302 -0
  90. package/packages/runtime/src/server.ts +481 -0
  91. package/packages/runtime/src/service-worker-push.ts +319 -0
  92. package/packages/runtime/src/service-worker.ts +553 -0
  93. package/packages/runtime/src/skeleton-hydrate.ts +237 -0
  94. package/packages/runtime/src/speculation.test.ts +389 -0
  95. package/packages/runtime/src/speculation.ts +486 -0
  96. package/packages/runtime/src/storage.test.ts +1297 -0
  97. package/packages/runtime/src/storage.ts +1048 -0
  98. package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
  99. package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
  100. package/packages/runtime/src/sync/coordinator.test.ts +608 -0
  101. package/packages/runtime/src/sync/coordinator.ts +596 -0
  102. package/packages/runtime/src/tree-compiler.ts +295 -0
  103. package/packages/runtime/src/types.ts +728 -0
  104. package/packages/runtime/src/worker.ts +327 -0
  105. package/packages/runtime/tsconfig.json +20 -0
  106. package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
  107. package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
  108. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
  109. package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
  110. package/packages/runtime/wasm/package.json +21 -0
  111. package/packages/runtime/wrangler.toml +41 -0
  112. package/packages/runtime-wasm/Cargo.lock +436 -0
  113. package/packages/runtime-wasm/Cargo.toml +29 -0
  114. package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
  115. package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
  116. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
  117. package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
  118. package/packages/runtime-wasm/pkg/package.json +21 -0
  119. package/packages/runtime-wasm/src/hydrate.rs +352 -0
  120. package/packages/runtime-wasm/src/lib.rs +191 -0
  121. package/packages/runtime-wasm/src/render.rs +629 -0
  122. package/packages/runtime-wasm/src/router.rs +298 -0
  123. package/packages/runtime-wasm/src/skeleton.rs +430 -0
  124. package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
@@ -0,0 +1,677 @@
1
+ /**
2
+ * OfflineDiagnostics Component
3
+ *
4
+ * Comprehensive diagnostics panel for offline-first applications.
5
+ * Shows network status, service worker state, cache management, and queue stats.
6
+ *
7
+ * Features:
8
+ * - Network status monitoring
9
+ * - Service worker status display
10
+ * - Cache management UI
11
+ * - Queue statistics display
12
+ * - Conflict resolution UI
13
+ * - Composable panels
14
+ */
15
+
16
+ 'use client';
17
+
18
+ import { useState, useEffect, useCallback, type ReactNode } from 'react';
19
+ import {
20
+ useNetworkState,
21
+ type NetworkState,
22
+ type BandwidthProfile,
23
+ } from '../hooks/useNetworkState';
24
+ import {
25
+ useConflicts,
26
+ type Conflict,
27
+ type ConflictStats,
28
+ } from '../hooks/useConflicts';
29
+
30
+ // ============================================================================
31
+ // Types
32
+ // ============================================================================
33
+
34
+ export interface ServiceWorkerState {
35
+ isSupported: boolean;
36
+ registration: 'none' | 'installing' | 'waiting' | 'active';
37
+ updateAvailable: boolean;
38
+ controller: boolean;
39
+ }
40
+
41
+ export interface CacheInfo {
42
+ name: string;
43
+ itemCount: number;
44
+ sampleUrls: string[];
45
+ }
46
+
47
+ export interface QueueStats {
48
+ pending: number;
49
+ syncing: number;
50
+ synced: number;
51
+ failed: number;
52
+ totalBytes: number;
53
+ }
54
+
55
+ export interface OfflineDiagnosticsProps {
56
+ /** Show network status panel */
57
+ showNetworkStatus?: boolean;
58
+ /** Show service worker panel */
59
+ showServiceWorker?: boolean;
60
+ /** Show cache management panel */
61
+ showCacheManagement?: boolean;
62
+ /** Show queue statistics panel */
63
+ showQueueStats?: boolean;
64
+ /** Show conflict resolution panel */
65
+ showConflicts?: boolean;
66
+ /** Callback when cache is cleared */
67
+ onClearCache?: (cacheName?: string) => Promise<void>;
68
+ /** CSS class for container */
69
+ className?: string;
70
+ }
71
+
72
+ // ============================================================================
73
+ // Panel Components
74
+ // ============================================================================
75
+
76
+ /**
77
+ * Network Status Panel
78
+ */
79
+ export function NetworkStatusPanel(): ReactNode {
80
+ const { state, isOnline, isPoor, bandwidth, timeSinceChange, refresh } =
81
+ useNetworkState();
82
+
83
+ const stateColor = {
84
+ online: '#10b981',
85
+ offline: '#ef4444',
86
+ poor: '#f59e0b',
87
+ unknown: '#6b7280',
88
+ }[state];
89
+
90
+ const formatTime = (ms: number) => {
91
+ if (ms < 1000) return `${ms}ms`;
92
+ if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
93
+ return `${Math.floor(ms / 60000)}m`;
94
+ };
95
+
96
+ return (
97
+ <div style={{ marginBottom: '1.5rem' }}>
98
+ <h4
99
+ style={{
100
+ fontSize: '1rem',
101
+ fontWeight: 600,
102
+ marginBottom: '0.75rem',
103
+ display: 'flex',
104
+ alignItems: 'center',
105
+ gap: '0.5rem',
106
+ }}
107
+ >
108
+ <span
109
+ style={{
110
+ width: '0.75rem',
111
+ height: '0.75rem',
112
+ borderRadius: '50%',
113
+ backgroundColor: stateColor,
114
+ display: 'inline-block',
115
+ }}
116
+ />
117
+ Network Status
118
+ </h4>
119
+ <div style={{ display: 'grid', gap: '0.5rem', fontSize: '0.875rem' }}>
120
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
121
+ <span style={{ color: '#6b7280' }}>Status:</span>
122
+ <span style={{ color: stateColor, fontWeight: 500 }}>
123
+ {state.charAt(0).toUpperCase() + state.slice(1)}
124
+ </span>
125
+ </div>
126
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
127
+ <span style={{ color: '#6b7280' }}>Connection Type:</span>
128
+ <span>{bandwidth.effectiveType || 'Unknown'}</span>
129
+ </div>
130
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
131
+ <span style={{ color: '#6b7280' }}>Speed:</span>
132
+ <span>
133
+ {bandwidth.speedKbps >= 1024
134
+ ? `${(bandwidth.speedKbps / 1024).toFixed(1)} Mbps`
135
+ : `${bandwidth.speedKbps} Kbps`}
136
+ </span>
137
+ </div>
138
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
139
+ <span style={{ color: '#6b7280' }}>Latency:</span>
140
+ <span>{bandwidth.latencyMs}ms</span>
141
+ </div>
142
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
143
+ <span style={{ color: '#6b7280' }}>Last Change:</span>
144
+ <span>{formatTime(timeSinceChange)} ago</span>
145
+ </div>
146
+ </div>
147
+ <button
148
+ onClick={refresh}
149
+ style={{
150
+ marginTop: '0.75rem',
151
+ padding: '0.375rem 0.75rem',
152
+ backgroundColor: '#e5e7eb',
153
+ border: 'none',
154
+ borderRadius: '0.375rem',
155
+ cursor: 'pointer',
156
+ fontSize: '0.75rem',
157
+ }}
158
+ >
159
+ Refresh
160
+ </button>
161
+ </div>
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Service Worker Panel
167
+ */
168
+ export function ServiceWorkerPanel(): ReactNode {
169
+ const [swState, setSwState] = useState<ServiceWorkerState>({
170
+ isSupported: false,
171
+ registration: 'none',
172
+ updateAvailable: false,
173
+ controller: false,
174
+ });
175
+ const [isChecking, setIsChecking] = useState(false);
176
+
177
+ const checkServiceWorker = useCallback(async () => {
178
+ if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
179
+ return;
180
+ }
181
+
182
+ setSwState((prev) => ({ ...prev, isSupported: true }));
183
+
184
+ try {
185
+ const registration = await navigator.serviceWorker.getRegistration();
186
+
187
+ if (!registration) {
188
+ setSwState((prev) => ({ ...prev, registration: 'none' }));
189
+ return;
190
+ }
191
+
192
+ let regState: ServiceWorkerState['registration'] = 'none';
193
+ if (registration.active) regState = 'active';
194
+ else if (registration.waiting) regState = 'waiting';
195
+ else if (registration.installing) regState = 'installing';
196
+
197
+ setSwState({
198
+ isSupported: true,
199
+ registration: regState,
200
+ updateAvailable: !!registration.waiting,
201
+ controller: !!navigator.serviceWorker.controller,
202
+ });
203
+ } catch (error) {
204
+ console.error('Error checking service worker:', error);
205
+ }
206
+ }, []);
207
+
208
+ useEffect(() => {
209
+ checkServiceWorker();
210
+ }, [checkServiceWorker]);
211
+
212
+ const handleCheckUpdate = async () => {
213
+ setIsChecking(true);
214
+ try {
215
+ const registration = await navigator.serviceWorker.getRegistration();
216
+ if (registration) {
217
+ await registration.update();
218
+ await checkServiceWorker();
219
+ }
220
+ } catch (error) {
221
+ console.error('Error checking for updates:', error);
222
+ } finally {
223
+ setIsChecking(false);
224
+ }
225
+ };
226
+
227
+ const handleUnregister = async () => {
228
+ try {
229
+ const registration = await navigator.serviceWorker.getRegistration();
230
+ if (registration) {
231
+ await registration.unregister();
232
+ await checkServiceWorker();
233
+ }
234
+ } catch (error) {
235
+ console.error('Error unregistering service worker:', error);
236
+ }
237
+ };
238
+
239
+ const regColor = {
240
+ none: '#6b7280',
241
+ installing: '#f59e0b',
242
+ waiting: '#f59e0b',
243
+ active: '#10b981',
244
+ }[swState.registration];
245
+
246
+ return (
247
+ <div style={{ marginBottom: '1.5rem' }}>
248
+ <h4
249
+ style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
250
+ >
251
+ Service Worker
252
+ </h4>
253
+ {!swState.isSupported ? (
254
+ <p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
255
+ Service workers are not supported in this browser.
256
+ </p>
257
+ ) : (
258
+ <>
259
+ <div style={{ display: 'grid', gap: '0.5rem', fontSize: '0.875rem' }}>
260
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
261
+ <span style={{ color: '#6b7280' }}>Status:</span>
262
+ <span style={{ color: regColor, fontWeight: 500 }}>
263
+ {swState.registration.charAt(0).toUpperCase() +
264
+ swState.registration.slice(1)}
265
+ </span>
266
+ </div>
267
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
268
+ <span style={{ color: '#6b7280' }}>Controller:</span>
269
+ <span>{swState.controller ? 'Yes' : 'No'}</span>
270
+ </div>
271
+ {swState.updateAvailable && (
272
+ <div style={{ color: '#f59e0b', fontWeight: 500 }}>
273
+ ⚠ Update available
274
+ </div>
275
+ )}
276
+ </div>
277
+ <div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem' }}>
278
+ <button
279
+ onClick={handleCheckUpdate}
280
+ disabled={isChecking}
281
+ style={{
282
+ padding: '0.375rem 0.75rem',
283
+ backgroundColor: '#e5e7eb',
284
+ border: 'none',
285
+ borderRadius: '0.375rem',
286
+ cursor: isChecking ? 'not-allowed' : 'pointer',
287
+ opacity: isChecking ? 0.5 : 1,
288
+ fontSize: '0.75rem',
289
+ }}
290
+ >
291
+ {isChecking ? 'Checking...' : 'Check for Updates'}
292
+ </button>
293
+ <button
294
+ onClick={handleUnregister}
295
+ style={{
296
+ padding: '0.375rem 0.75rem',
297
+ backgroundColor: '#fef2f2',
298
+ color: '#ef4444',
299
+ border: 'none',
300
+ borderRadius: '0.375rem',
301
+ cursor: 'pointer',
302
+ fontSize: '0.75rem',
303
+ }}
304
+ >
305
+ Unregister
306
+ </button>
307
+ </div>
308
+ </>
309
+ )}
310
+ </div>
311
+ );
312
+ }
313
+
314
+ /**
315
+ * Cache Management Panel
316
+ */
317
+ export function CacheManagementPanel({
318
+ onClearCache,
319
+ }: {
320
+ onClearCache?: (cacheName?: string) => Promise<void>;
321
+ }): ReactNode {
322
+ const [caches, setCaches] = useState<CacheInfo[]>([]);
323
+ const [isLoading, setIsLoading] = useState(true);
324
+ const [isClearing, setIsClearing] = useState<string | null>(null);
325
+
326
+ const loadCaches = useCallback(async () => {
327
+ if (typeof window === 'undefined' || !('caches' in window)) {
328
+ setIsLoading(false);
329
+ return;
330
+ }
331
+
332
+ try {
333
+ const cacheNames = await window.caches.keys();
334
+ const cacheInfos: CacheInfo[] = [];
335
+
336
+ for (const name of cacheNames) {
337
+ const cache = await window.caches.open(name);
338
+ const keys = await cache.keys();
339
+ cacheInfos.push({
340
+ name,
341
+ itemCount: keys.length,
342
+ sampleUrls: keys.slice(0, 5).map((k) => k.url),
343
+ });
344
+ }
345
+
346
+ setCaches(cacheInfos);
347
+ } catch (error) {
348
+ console.error('Error loading caches:', error);
349
+ } finally {
350
+ setIsLoading(false);
351
+ }
352
+ }, []);
353
+
354
+ useEffect(() => {
355
+ loadCaches();
356
+ }, [loadCaches]);
357
+
358
+ const handleClearCache = async (cacheName?: string) => {
359
+ setIsClearing(cacheName || 'all');
360
+ try {
361
+ if (onClearCache) {
362
+ await onClearCache(cacheName);
363
+ } else if (cacheName) {
364
+ await window.caches.delete(cacheName);
365
+ } else {
366
+ const names = await window.caches.keys();
367
+ await Promise.all(names.map((name) => window.caches.delete(name)));
368
+ }
369
+ await loadCaches();
370
+ } catch (error) {
371
+ console.error('Error clearing cache:', error);
372
+ } finally {
373
+ setIsClearing(null);
374
+ }
375
+ };
376
+
377
+ if (!('caches' in window)) {
378
+ return (
379
+ <div style={{ marginBottom: '1.5rem' }}>
380
+ <h4
381
+ style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
382
+ >
383
+ Cache Storage
384
+ </h4>
385
+ <p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
386
+ Cache API is not supported in this browser.
387
+ </p>
388
+ </div>
389
+ );
390
+ }
391
+
392
+ return (
393
+ <div style={{ marginBottom: '1.5rem' }}>
394
+ <h4
395
+ style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
396
+ >
397
+ Cache Storage
398
+ </h4>
399
+ {isLoading ? (
400
+ <p style={{ color: '#6b7280', fontSize: '0.875rem' }}>Loading...</p>
401
+ ) : caches.length === 0 ? (
402
+ <p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
403
+ No caches found.
404
+ </p>
405
+ ) : (
406
+ <>
407
+ <div style={{ display: 'grid', gap: '0.75rem' }}>
408
+ {caches.map((cache) => (
409
+ <div
410
+ key={cache.name}
411
+ style={{
412
+ padding: '0.75rem',
413
+ backgroundColor: '#f3f4f6',
414
+ borderRadius: '0.375rem',
415
+ fontSize: '0.875rem',
416
+ }}
417
+ >
418
+ <div
419
+ style={{
420
+ display: 'flex',
421
+ justifyContent: 'space-between',
422
+ alignItems: 'center',
423
+ }}
424
+ >
425
+ <span style={{ fontWeight: 500 }}>{cache.name}</span>
426
+ <span style={{ color: '#6b7280' }}>
427
+ {cache.itemCount} items
428
+ </span>
429
+ </div>
430
+ <button
431
+ onClick={() => handleClearCache(cache.name)}
432
+ disabled={isClearing === cache.name}
433
+ style={{
434
+ marginTop: '0.5rem',
435
+ padding: '0.25rem 0.5rem',
436
+ backgroundColor: '#fef2f2',
437
+ color: '#ef4444',
438
+ border: 'none',
439
+ borderRadius: '0.25rem',
440
+ cursor:
441
+ isClearing === cache.name ? 'not-allowed' : 'pointer',
442
+ opacity: isClearing === cache.name ? 0.5 : 1,
443
+ fontSize: '0.75rem',
444
+ }}
445
+ >
446
+ {isClearing === cache.name ? 'Clearing...' : 'Clear'}
447
+ </button>
448
+ </div>
449
+ ))}
450
+ </div>
451
+ <button
452
+ onClick={() => handleClearCache()}
453
+ disabled={isClearing === 'all'}
454
+ style={{
455
+ marginTop: '0.75rem',
456
+ padding: '0.375rem 0.75rem',
457
+ backgroundColor: '#fef2f2',
458
+ color: '#ef4444',
459
+ border: 'none',
460
+ borderRadius: '0.375rem',
461
+ cursor: isClearing === 'all' ? 'not-allowed' : 'pointer',
462
+ opacity: isClearing === 'all' ? 0.5 : 1,
463
+ fontSize: '0.75rem',
464
+ }}
465
+ >
466
+ {isClearing === 'all' ? 'Clearing...' : 'Clear All Caches'}
467
+ </button>
468
+ </>
469
+ )}
470
+ </div>
471
+ );
472
+ }
473
+
474
+ /**
475
+ * Queue Stats Panel
476
+ */
477
+ export function QueueStatsPanel({ stats }: { stats?: QueueStats }): ReactNode {
478
+ const defaultStats: QueueStats = stats || {
479
+ pending: 0,
480
+ syncing: 0,
481
+ synced: 0,
482
+ failed: 0,
483
+ totalBytes: 0,
484
+ };
485
+
486
+ const formatBytes = (bytes: number) => {
487
+ if (bytes < 1024) return `${bytes} B`;
488
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
489
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
490
+ };
491
+
492
+ return (
493
+ <div style={{ marginBottom: '1.5rem' }}>
494
+ <h4
495
+ style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
496
+ >
497
+ Offline Queue
498
+ </h4>
499
+ <div style={{ display: 'grid', gap: '0.5rem', fontSize: '0.875rem' }}>
500
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
501
+ <span style={{ color: '#6b7280' }}>Pending:</span>
502
+ <span
503
+ style={{ color: defaultStats.pending > 0 ? '#f59e0b' : '#10b981' }}
504
+ >
505
+ {defaultStats.pending}
506
+ </span>
507
+ </div>
508
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
509
+ <span style={{ color: '#6b7280' }}>Syncing:</span>
510
+ <span>{defaultStats.syncing}</span>
511
+ </div>
512
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
513
+ <span style={{ color: '#6b7280' }}>Synced:</span>
514
+ <span style={{ color: '#10b981' }}>{defaultStats.synced}</span>
515
+ </div>
516
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
517
+ <span style={{ color: '#6b7280' }}>Failed:</span>
518
+ <span
519
+ style={{ color: defaultStats.failed > 0 ? '#ef4444' : '#6b7280' }}
520
+ >
521
+ {defaultStats.failed}
522
+ </span>
523
+ </div>
524
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
525
+ <span style={{ color: '#6b7280' }}>Total Size:</span>
526
+ <span>{formatBytes(defaultStats.totalBytes)}</span>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ );
531
+ }
532
+
533
+ /**
534
+ * Conflicts Panel
535
+ */
536
+ export function ConflictsPanel(): ReactNode {
537
+ const { unresolvedConflicts, stats, resolveConflict } = useConflicts();
538
+
539
+ return (
540
+ <div style={{ marginBottom: '1.5rem' }}>
541
+ <h4
542
+ style={{ fontSize: '1rem', fontWeight: 600, marginBottom: '0.75rem' }}
543
+ >
544
+ Conflicts
545
+ </h4>
546
+ <div style={{ display: 'grid', gap: '0.5rem', fontSize: '0.875rem' }}>
547
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
548
+ <span style={{ color: '#6b7280' }}>Total:</span>
549
+ <span>{stats.total}</span>
550
+ </div>
551
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
552
+ <span style={{ color: '#6b7280' }}>Unresolved:</span>
553
+ <span style={{ color: stats.unresolved > 0 ? '#f59e0b' : '#10b981' }}>
554
+ {stats.unresolved}
555
+ </span>
556
+ </div>
557
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
558
+ <span style={{ color: '#6b7280' }}>High Severity:</span>
559
+ <span
560
+ style={{ color: stats.highSeverity > 0 ? '#ef4444' : '#6b7280' }}
561
+ >
562
+ {stats.highSeverity}
563
+ </span>
564
+ </div>
565
+ </div>
566
+ {unresolvedConflicts.length > 0 && (
567
+ <div style={{ marginTop: '0.75rem' }}>
568
+ <p
569
+ style={{
570
+ fontSize: '0.75rem',
571
+ color: '#6b7280',
572
+ marginBottom: '0.5rem',
573
+ }}
574
+ >
575
+ Unresolved conflicts:
576
+ </p>
577
+ {unresolvedConflicts.slice(0, 3).map((conflict) => (
578
+ <div
579
+ key={conflict.id}
580
+ style={{
581
+ padding: '0.5rem',
582
+ backgroundColor: '#fef3c7',
583
+ borderRadius: '0.25rem',
584
+ marginBottom: '0.5rem',
585
+ fontSize: '0.75rem',
586
+ }}
587
+ >
588
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
589
+ <span>{conflict.type}</span>
590
+ <span
591
+ style={{
592
+ color: conflict.severity === 'high' ? '#ef4444' : '#f59e0b',
593
+ }}
594
+ >
595
+ {conflict.severity}
596
+ </span>
597
+ </div>
598
+ <div
599
+ style={{
600
+ display: 'flex',
601
+ gap: '0.25rem',
602
+ marginTop: '0.25rem',
603
+ }}
604
+ >
605
+ <button
606
+ onClick={() => resolveConflict(conflict.id, 'local-wins')}
607
+ style={{
608
+ padding: '0.125rem 0.375rem',
609
+ backgroundColor: '#dbeafe',
610
+ color: '#1d4ed8',
611
+ border: 'none',
612
+ borderRadius: '0.125rem',
613
+ cursor: 'pointer',
614
+ fontSize: '0.625rem',
615
+ }}
616
+ >
617
+ Keep Local
618
+ </button>
619
+ <button
620
+ onClick={() => resolveConflict(conflict.id, 'remote-wins')}
621
+ style={{
622
+ padding: '0.125rem 0.375rem',
623
+ backgroundColor: '#dcfce7',
624
+ color: '#15803d',
625
+ border: 'none',
626
+ borderRadius: '0.125rem',
627
+ cursor: 'pointer',
628
+ fontSize: '0.625rem',
629
+ }}
630
+ >
631
+ Use Remote
632
+ </button>
633
+ </div>
634
+ </div>
635
+ ))}
636
+ </div>
637
+ )}
638
+ </div>
639
+ );
640
+ }
641
+
642
+ // ============================================================================
643
+ // Main Component
644
+ // ============================================================================
645
+
646
+ /**
647
+ * Comprehensive offline diagnostics panel
648
+ */
649
+ export function OfflineDiagnostics({
650
+ showNetworkStatus = true,
651
+ showServiceWorker = true,
652
+ showCacheManagement = true,
653
+ showQueueStats = true,
654
+ showConflicts = true,
655
+ onClearCache,
656
+ className,
657
+ }: OfflineDiagnosticsProps): ReactNode {
658
+ return (
659
+ <div className={className} role="region" aria-label="Offline diagnostics">
660
+ <h3
661
+ style={{ fontSize: '1.25rem', fontWeight: 600, marginBottom: '1rem' }}
662
+ >
663
+ Offline Diagnostics
664
+ </h3>
665
+
666
+ {showNetworkStatus && <NetworkStatusPanel />}
667
+ {showServiceWorker && <ServiceWorkerPanel />}
668
+ {showCacheManagement && (
669
+ <CacheManagementPanel onClearCache={onClearCache} />
670
+ )}
671
+ {showQueueStats && <QueueStatsPanel />}
672
+ {showConflicts && <ConflictsPanel />}
673
+ </div>
674
+ );
675
+ }
676
+
677
+ export default OfflineDiagnostics;