@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,254 @@
1
+ /**
2
+ * Pilot Navigation Hook
3
+ *
4
+ * AI-driven navigation with user consent.
5
+ * Cyrano acts as the "pilot" - suggesting navigation destinations,
6
+ * but always with user consent before actually navigating.
7
+ *
8
+ * The pilot metaphor:
9
+ * - User is the captain
10
+ * - AI (Cyrano) is the pilot suggesting routes
11
+ * - Navigation only happens with captain's approval
12
+ *
13
+ * Features:
14
+ * - Pending navigation queue
15
+ * - Consent confirmation UI
16
+ * - History API integration (smooth client-side navigation)
17
+ * - Navigation analytics/tracking
18
+ */
19
+
20
+ import { useState, useCallback, useEffect, useMemo } from 'react';
21
+ import { useAeonNavigation } from './useAeonNavigation';
22
+ import type { NavigationOptions } from '@affectively/aeon-pages-runtime/navigation';
23
+
24
+ export interface PilotNavigationIntent {
25
+ id: string;
26
+ destination: string;
27
+ reason?: string;
28
+ source: 'cyrano' | 'esi' | 'user' | 'system';
29
+ confidence?: number;
30
+ timestamp: number;
31
+ metadata?: Record<string, unknown>;
32
+ }
33
+
34
+ export interface PilotNavigationOptions extends NavigationOptions {
35
+ /** Whether to require explicit consent (default: true for AI sources) */
36
+ requireConsent?: boolean;
37
+ /** Reason for navigation (shown to user) */
38
+ reason?: string;
39
+ /** Source of navigation intent */
40
+ source?: PilotNavigationIntent['source'];
41
+ /** Confidence level (0-1) for AI-driven navigation */
42
+ confidence?: number;
43
+ /** Additional metadata */
44
+ metadata?: Record<string, unknown>;
45
+ /** Auto-navigate after delay (ms) if consent not required */
46
+ autoNavigateDelay?: number;
47
+ }
48
+
49
+ export interface PilotNavigationState {
50
+ /** Current pending navigation intent awaiting consent */
51
+ pendingIntent: PilotNavigationIntent | null;
52
+ /** History of navigation intents */
53
+ intentHistory: PilotNavigationIntent[];
54
+ /** Whether navigation is in progress */
55
+ isNavigating: boolean;
56
+ }
57
+
58
+ type NavigationConsentCallback = (
59
+ intent: PilotNavigationIntent,
60
+ ) => Promise<boolean> | boolean;
61
+
62
+ /**
63
+ * Hook for AI-piloted navigation with user consent
64
+ */
65
+ export function usePilotNavigation(options?: {
66
+ /** Custom consent handler (if not provided, uses built-in pending state) */
67
+ onConsentRequired?: NavigationConsentCallback;
68
+ /** Maximum intent history to keep */
69
+ maxHistory?: number;
70
+ }) {
71
+ const { onConsentRequired, maxHistory = 50 } = options ?? {};
72
+
73
+ const navigation = useAeonNavigation();
74
+ const [pendingIntent, setPendingIntent] =
75
+ useState<PilotNavigationIntent | null>(null);
76
+ const [intentHistory, setIntentHistory] = useState<PilotNavigationIntent[]>(
77
+ [],
78
+ );
79
+
80
+ /**
81
+ * Request navigation with optional consent
82
+ */
83
+ const pilot = useCallback(
84
+ async (
85
+ destination: string,
86
+ pilotOptions?: PilotNavigationOptions,
87
+ ): Promise<boolean> => {
88
+ const {
89
+ requireConsent = true,
90
+ reason,
91
+ source = 'user',
92
+ confidence,
93
+ metadata,
94
+ autoNavigateDelay,
95
+ ...navOptions
96
+ } = pilotOptions ?? {};
97
+
98
+ // Create intent
99
+ const intent: PilotNavigationIntent = {
100
+ id: `nav-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
101
+ destination,
102
+ reason,
103
+ source,
104
+ confidence,
105
+ timestamp: Date.now(),
106
+ metadata,
107
+ };
108
+
109
+ // Add to history
110
+ setIntentHistory((prev) => [...prev.slice(-maxHistory + 1), intent]);
111
+
112
+ // Check if consent is required
113
+ const needsConsent =
114
+ requireConsent && (source === 'cyrano' || source === 'esi');
115
+
116
+ if (!needsConsent) {
117
+ // Navigate immediately
118
+ await navigation.navigate(destination, navOptions);
119
+ return true;
120
+ }
121
+
122
+ // If custom consent handler provided, use it
123
+ if (onConsentRequired) {
124
+ const consented = await onConsentRequired(intent);
125
+ if (consented) {
126
+ await navigation.navigate(destination, navOptions);
127
+ return true;
128
+ }
129
+ return false;
130
+ }
131
+
132
+ // Otherwise, set pending intent for UI to handle
133
+ setPendingIntent(intent);
134
+
135
+ // Auto-navigate after delay if specified
136
+ if (autoNavigateDelay && autoNavigateDelay > 0) {
137
+ setTimeout(async () => {
138
+ // Only navigate if this intent is still pending
139
+ setPendingIntent((current) => {
140
+ if (current?.id === intent.id) {
141
+ navigation.navigate(destination, navOptions);
142
+ return null;
143
+ }
144
+ return current;
145
+ });
146
+ }, autoNavigateDelay);
147
+ }
148
+
149
+ return false; // Pending consent
150
+ },
151
+ [navigation, onConsentRequired, maxHistory],
152
+ );
153
+
154
+ /**
155
+ * Approve pending navigation
156
+ */
157
+ const approve = useCallback(async () => {
158
+ if (!pendingIntent) return false;
159
+
160
+ const destination = pendingIntent.destination;
161
+ setPendingIntent(null);
162
+ await navigation.navigate(destination);
163
+ return true;
164
+ }, [pendingIntent, navigation]);
165
+
166
+ /**
167
+ * Reject pending navigation
168
+ */
169
+ const reject = useCallback(() => {
170
+ if (!pendingIntent) return;
171
+ setPendingIntent(null);
172
+ }, [pendingIntent]);
173
+
174
+ /**
175
+ * Clear all pending intents
176
+ */
177
+ const clearPending = useCallback(() => {
178
+ setPendingIntent(null);
179
+ }, []);
180
+
181
+ /**
182
+ * Navigate without consent (for user-initiated navigation)
183
+ */
184
+ const navigateDirect = useCallback(
185
+ async (destination: string, navOptions?: NavigationOptions) => {
186
+ await navigation.navigate(destination, navOptions);
187
+ },
188
+ [navigation],
189
+ );
190
+
191
+ return useMemo(
192
+ () => ({
193
+ // State
194
+ pendingIntent,
195
+ intentHistory,
196
+ isNavigating: navigation.isNavigating,
197
+ current: navigation.current,
198
+
199
+ // Actions
200
+ pilot, // AI-driven navigation with consent
201
+ approve, // Approve pending navigation
202
+ reject, // Reject pending navigation
203
+ clearPending, // Clear pending intent
204
+ navigateDirect, // Navigate without consent (user-initiated)
205
+
206
+ // Pass through navigation utilities
207
+ prefetch: navigation.prefetch,
208
+ back: navigation.back,
209
+ isPreloaded: navigation.isPreloaded,
210
+ }),
211
+ [
212
+ pendingIntent,
213
+ intentHistory,
214
+ navigation.isNavigating,
215
+ navigation.current,
216
+ pilot,
217
+ approve,
218
+ reject,
219
+ clearPending,
220
+ navigateDirect,
221
+ navigation.prefetch,
222
+ navigation.back,
223
+ navigation.isPreloaded,
224
+ ],
225
+ );
226
+ }
227
+
228
+ /**
229
+ * Parse navigation tags from AI response
230
+ * Returns array of destinations extracted from [navigate:/path] tags
231
+ */
232
+ export function parseNavigationTags(
233
+ text: string,
234
+ ): { destination: string; fullMatch: string }[] {
235
+ const regex = /\[navigate:([^\]]+)\]/g;
236
+ const matches: { destination: string; fullMatch: string }[] = [];
237
+
238
+ let match;
239
+ while ((match = regex.exec(text)) !== null) {
240
+ matches.push({
241
+ destination: match[1],
242
+ fullMatch: match[0],
243
+ });
244
+ }
245
+
246
+ return matches;
247
+ }
248
+
249
+ /**
250
+ * Remove navigation tags from text for display
251
+ */
252
+ export function stripNavigationTags(text: string): string {
253
+ return text.replace(/\[navigate:[^\]]+\]/g, '').trim();
254
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Service Worker Hooks - Client communication with Aeon SW
3
+ *
4
+ * Provides React hooks for:
5
+ * - Total preload progress tracking
6
+ * - Cache status monitoring
7
+ * - Manual preload triggers
8
+ */
9
+
10
+ import { useEffect, useState, useCallback, useRef } from 'react';
11
+
12
+ export interface PreloadProgress {
13
+ loaded: number;
14
+ total: number;
15
+ percentage: number;
16
+ isComplete: boolean;
17
+ cachedRoutes: string[];
18
+ }
19
+
20
+ export interface CacheStatus {
21
+ cached: number;
22
+ total: number;
23
+ routes: string[];
24
+ isReady: boolean;
25
+ }
26
+
27
+ /**
28
+ * Hook to register and track the Aeon service worker
29
+ */
30
+ export function useAeonServiceWorker() {
31
+ const [isRegistered, setIsRegistered] = useState(false);
32
+ const [isActive, setIsActive] = useState(false);
33
+ const [error, setError] = useState<Error | null>(null);
34
+ const registrationRef = useRef<ServiceWorkerRegistration | null>(null);
35
+
36
+ useEffect(() => {
37
+ if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
38
+ return;
39
+ }
40
+
41
+ const registerSW = async () => {
42
+ try {
43
+ const registration = await navigator.serviceWorker.register(
44
+ '/.aeon/sw.js',
45
+ { scope: '/' },
46
+ );
47
+
48
+ registrationRef.current = registration;
49
+ setIsRegistered(true);
50
+
51
+ // Check if active
52
+ if (registration.active) {
53
+ setIsActive(true);
54
+ }
55
+
56
+ // Listen for state changes
57
+ registration.addEventListener('updatefound', () => {
58
+ const newWorker = registration.installing;
59
+ if (newWorker) {
60
+ newWorker.addEventListener('statechange', () => {
61
+ if (newWorker.state === 'activated') {
62
+ setIsActive(true);
63
+ }
64
+ });
65
+ }
66
+ });
67
+ } catch (err) {
68
+ setError(
69
+ err instanceof Error ? err : new Error('Failed to register SW'),
70
+ );
71
+ }
72
+ };
73
+
74
+ registerSW();
75
+ }, []);
76
+
77
+ const update = useCallback(async () => {
78
+ if (registrationRef.current) {
79
+ await registrationRef.current.update();
80
+ }
81
+ }, []);
82
+
83
+ const unregister = useCallback(async () => {
84
+ if (registrationRef.current) {
85
+ await registrationRef.current.unregister();
86
+ setIsRegistered(false);
87
+ setIsActive(false);
88
+ }
89
+ }, []);
90
+
91
+ return {
92
+ isRegistered,
93
+ isActive,
94
+ error,
95
+ update,
96
+ unregister,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Hook to track total preload progress
102
+ */
103
+ export function usePreloadProgress(): PreloadProgress {
104
+ const [progress, setProgress] = useState<PreloadProgress>({
105
+ loaded: 0,
106
+ total: 0,
107
+ percentage: 0,
108
+ isComplete: false,
109
+ cachedRoutes: [],
110
+ });
111
+
112
+ useEffect(() => {
113
+ if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
114
+ return;
115
+ }
116
+
117
+ const handleMessage = (event: MessageEvent) => {
118
+ const data = event.data;
119
+
120
+ if (data.type === 'PRELOAD_PROGRESS') {
121
+ setProgress({
122
+ loaded: data.loaded,
123
+ total: data.total,
124
+ percentage: data.percentage,
125
+ isComplete: false,
126
+ cachedRoutes: [],
127
+ });
128
+ } else if (data.type === 'PRELOAD_COMPLETE') {
129
+ setProgress({
130
+ loaded: data.loaded,
131
+ total: data.total,
132
+ percentage: 100,
133
+ isComplete: true,
134
+ cachedRoutes: data.cachedRoutes || [],
135
+ });
136
+ }
137
+ };
138
+
139
+ navigator.serviceWorker.addEventListener('message', handleMessage);
140
+
141
+ return () => {
142
+ navigator.serviceWorker.removeEventListener('message', handleMessage);
143
+ };
144
+ }, []);
145
+
146
+ return progress;
147
+ }
148
+
149
+ /**
150
+ * Hook to get current cache status
151
+ */
152
+ export function useCacheStatus(): CacheStatus & { refresh: () => void } {
153
+ const [status, setStatus] = useState<CacheStatus>({
154
+ cached: 0,
155
+ total: 0,
156
+ routes: [],
157
+ isReady: false,
158
+ });
159
+
160
+ const refresh = useCallback(async () => {
161
+ if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
162
+ return;
163
+ }
164
+
165
+ const controller = navigator.serviceWorker.controller;
166
+ if (!controller) {
167
+ return;
168
+ }
169
+
170
+ // Use MessageChannel for request/response
171
+ const channel = new MessageChannel();
172
+
173
+ channel.port1.onmessage = (event: MessageEvent) => {
174
+ const data = event.data;
175
+ setStatus({
176
+ cached: data.cached,
177
+ total: data.total,
178
+ routes: data.routes,
179
+ isReady: data.cached === data.total && data.total > 0,
180
+ });
181
+ };
182
+
183
+ controller.postMessage({ type: 'GET_CACHE_STATUS' }, [channel.port2]);
184
+ }, []);
185
+
186
+ useEffect(() => {
187
+ // Initial fetch
188
+ refresh();
189
+
190
+ // Refresh periodically
191
+ const interval = setInterval(refresh, 5000);
192
+ return () => clearInterval(interval);
193
+ }, [refresh]);
194
+
195
+ return { ...status, refresh };
196
+ }
197
+
198
+ /**
199
+ * Hook to trigger manual preload
200
+ */
201
+ export function useManualPreload() {
202
+ const [isPreloading, setIsPreloading] = useState(false);
203
+ const progress = usePreloadProgress();
204
+
205
+ const triggerPreload = useCallback(() => {
206
+ if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
207
+ return;
208
+ }
209
+
210
+ const controller = navigator.serviceWorker.controller;
211
+ if (!controller) {
212
+ return;
213
+ }
214
+
215
+ setIsPreloading(true);
216
+ controller.postMessage({ type: 'TRIGGER_PRELOAD' });
217
+ }, []);
218
+
219
+ // Reset isPreloading when complete
220
+ useEffect(() => {
221
+ if (progress.isComplete) {
222
+ setIsPreloading(false);
223
+ }
224
+ }, [progress.isComplete]);
225
+
226
+ return {
227
+ triggerPreload,
228
+ isPreloading,
229
+ progress,
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Hook to prefetch a specific route
235
+ */
236
+ export function usePrefetchRoute() {
237
+ const prefetch = useCallback((route: string) => {
238
+ if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
239
+ return;
240
+ }
241
+
242
+ const controller = navigator.serviceWorker.controller;
243
+ if (!controller) {
244
+ return;
245
+ }
246
+
247
+ controller.postMessage({ type: 'PREFETCH_ROUTE', route });
248
+ }, []);
249
+
250
+ return prefetch;
251
+ }
252
+
253
+ /**
254
+ * Hook to clear the cache
255
+ */
256
+ export function useClearCache() {
257
+ const [isClearing, setIsClearing] = useState(false);
258
+
259
+ const clearCache = useCallback(async () => {
260
+ if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
261
+ return;
262
+ }
263
+
264
+ const controller = navigator.serviceWorker.controller;
265
+ if (!controller) {
266
+ return;
267
+ }
268
+
269
+ setIsClearing(true);
270
+ controller.postMessage({ type: 'CLEAR_CACHE' });
271
+
272
+ // Wait a bit for cache to clear
273
+ await new Promise((r) => setTimeout(r, 100));
274
+ setIsClearing(false);
275
+ }, []);
276
+
277
+ return { clearCache, isClearing };
278
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Additional React hooks for Aeon Pages
3
+ */
4
+
5
+ import { useEffect, useRef, useCallback, useState } from 'react';
6
+ import { useAeonPage, type PresenceUser } from './provider';
7
+
8
+ /**
9
+ * useAeonVersion - Access version and migration tools
10
+ */
11
+ export function useAeonVersion() {
12
+ const { version, migrate } = useAeonPage();
13
+ return { ...version, migrate };
14
+ }
15
+
16
+ /**
17
+ * useAeonTree - Access the component tree for advanced manipulation
18
+ */
19
+ export function useAeonTree() {
20
+ const { tree, updateTree } = useAeonPage();
21
+ return { tree, updateTree };
22
+ }
23
+
24
+ /**
25
+ * useCursorTracking - Automatically track cursor movement
26
+ */
27
+ export function useCursorTracking(enabled = true) {
28
+ const { updateCursor } = useAeonPage();
29
+ const throttleRef = useRef<number | null>(null);
30
+
31
+ useEffect(() => {
32
+ if (!enabled) return;
33
+
34
+ const handleMouseMove = (e: MouseEvent) => {
35
+ // Throttle to ~60fps
36
+ if (throttleRef.current) return;
37
+
38
+ throttleRef.current = window.requestAnimationFrame(() => {
39
+ updateCursor({ x: e.clientX, y: e.clientY });
40
+ throttleRef.current = null;
41
+ });
42
+ };
43
+
44
+ window.addEventListener('mousemove', handleMouseMove);
45
+ return () => {
46
+ window.removeEventListener('mousemove', handleMouseMove);
47
+ if (throttleRef.current) {
48
+ window.cancelAnimationFrame(throttleRef.current);
49
+ }
50
+ };
51
+ }, [enabled, updateCursor]);
52
+ }
53
+
54
+ /**
55
+ * useEditableElement - Make an element collaboratively editable
56
+ */
57
+ export function useEditableElement(elementPath: string) {
58
+ const { updateEditing, updateTree, localUser } = useAeonPage();
59
+ const [isFocused, setIsFocused] = useState(false);
60
+
61
+ const onFocus = useCallback(() => {
62
+ setIsFocused(true);
63
+ updateEditing(elementPath);
64
+ }, [elementPath, updateEditing]);
65
+
66
+ const onBlur = useCallback(() => {
67
+ setIsFocused(false);
68
+ updateEditing(null);
69
+ }, [updateEditing]);
70
+
71
+ const onChange = useCallback(
72
+ (value: unknown) => {
73
+ updateTree(elementPath, value);
74
+ },
75
+ [elementPath, updateTree],
76
+ );
77
+
78
+ return {
79
+ isFocused,
80
+ isBeingEditedByOther: false, // Would check presence
81
+ onFocus,
82
+ onBlur,
83
+ onChange,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * useOtherCursors - Get cursors of other users (excluding self)
89
+ */
90
+ export function useOtherCursors(): PresenceUser[] {
91
+ const { presence, localUser } = useAeonPage();
92
+ return presence.filter((user) => user.userId !== localUser?.userId);
93
+ }
94
+
95
+ /**
96
+ * useOfflineStatus - Track offline status and pending operations
97
+ */
98
+ export function useOfflineStatus() {
99
+ const { sync } = useAeonPage();
100
+ return {
101
+ isOffline: !sync.isOnline,
102
+ isSyncing: sync.isSyncing,
103
+ pendingOperations: sync.pendingOperations,
104
+ lastSyncAt: sync.lastSyncAt,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * useCollaborativeInput - Hook for collaborative text input
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * function EditableTitle() {
114
+ * const { value, onChange, onFocus, onBlur, isEditing, editingBy } = useCollaborativeInput('title');
115
+ *
116
+ * return (
117
+ * <input
118
+ * value={value}
119
+ * onChange={(e) => onChange(e.target.value)}
120
+ * onFocus={onFocus}
121
+ * onBlur={onBlur}
122
+ * style={{ borderColor: editingBy ? 'blue' : undefined }}
123
+ * />
124
+ * );
125
+ * }
126
+ * ```
127
+ */
128
+ export function useCollaborativeInput(key: string) {
129
+ const { data, setData, presence, localUser, updateEditing } = useAeonPage();
130
+ const [isEditing, setIsEditing] = useState(false);
131
+
132
+ const value = (data[key] as string) ?? '';
133
+
134
+ // Find if someone else is editing this field
135
+ const editingBy = presence.find(
136
+ (user) => user.editing === key && user.userId !== localUser?.userId,
137
+ );
138
+
139
+ const onChange = useCallback(
140
+ (newValue: string) => {
141
+ setData(key, newValue);
142
+ },
143
+ [key, setData],
144
+ );
145
+
146
+ const onFocus = useCallback(() => {
147
+ setIsEditing(true);
148
+ updateEditing(key);
149
+ }, [key, updateEditing]);
150
+
151
+ const onBlur = useCallback(() => {
152
+ setIsEditing(false);
153
+ updateEditing(null);
154
+ }, [updateEditing]);
155
+
156
+ return {
157
+ value,
158
+ onChange,
159
+ onFocus,
160
+ onBlur,
161
+ isEditing,
162
+ editingBy,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * useAeonEffect - Run effect when Aeon data changes
168
+ */
169
+ export function useAeonEffect(
170
+ key: string,
171
+ effect: (value: unknown) => void | (() => void),
172
+ ) {
173
+ const { data } = useAeonPage();
174
+ const value = data[key];
175
+
176
+ useEffect(() => {
177
+ return effect(value);
178
+ }, [value, effect]);
179
+ }
180
+
181
+ /**
182
+ * useSessionId - Get the current session ID
183
+ */
184
+ export function useSessionId(): string {
185
+ const { sessionId } = useAeonPage();
186
+ return sessionId;
187
+ }
188
+
189
+ /**
190
+ * useRoute - Get the current route
191
+ */
192
+ export function useRoute(): string {
193
+ const { route } = useAeonPage();
194
+ return route;
195
+ }