@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.
- package/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PushNotifications Component
|
|
3
|
+
*
|
|
4
|
+
* Push notification management component for PWA applications.
|
|
5
|
+
* Handles subscription, permission, and notification sending.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - VAPID-based push subscription
|
|
9
|
+
* - Permission handling
|
|
10
|
+
* - Subscription serialization
|
|
11
|
+
* - Customizable UI via render props
|
|
12
|
+
* - Headless hook export
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use client';
|
|
16
|
+
|
|
17
|
+
import { useState, useEffect, useCallback, type ReactNode } from 'react';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export interface PushSubscriptionData {
|
|
24
|
+
endpoint: string;
|
|
25
|
+
keys: {
|
|
26
|
+
p256dh: string;
|
|
27
|
+
auth: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PushNotificationState {
|
|
32
|
+
/** Whether push is supported */
|
|
33
|
+
isSupported: boolean;
|
|
34
|
+
/** Current permission state */
|
|
35
|
+
permission: NotificationPermission | 'unsupported';
|
|
36
|
+
/** Current subscription (if subscribed) */
|
|
37
|
+
subscription: PushSubscriptionData | null;
|
|
38
|
+
/** Whether currently loading */
|
|
39
|
+
isLoading: boolean;
|
|
40
|
+
/** Last error message */
|
|
41
|
+
error: string | null;
|
|
42
|
+
/** Subscribe to push notifications */
|
|
43
|
+
subscribe: () => Promise<PushSubscriptionData | null>;
|
|
44
|
+
/** Unsubscribe from push notifications */
|
|
45
|
+
unsubscribe: () => Promise<boolean>;
|
|
46
|
+
/** Request permission */
|
|
47
|
+
requestPermission: () => Promise<NotificationPermission>;
|
|
48
|
+
/** Clear error */
|
|
49
|
+
clearError: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PushNotificationsProps {
|
|
53
|
+
/** VAPID public key */
|
|
54
|
+
vapidPublicKey?: string;
|
|
55
|
+
/** Called when subscription changes */
|
|
56
|
+
onSubscribe?: (subscription: PushSubscriptionData) => Promise<void> | void;
|
|
57
|
+
/** Called when unsubscribing */
|
|
58
|
+
onUnsubscribe?: (endpoint: string) => Promise<void> | void;
|
|
59
|
+
/** Custom render function */
|
|
60
|
+
render?: (state: PushNotificationState) => ReactNode;
|
|
61
|
+
/** Show default UI */
|
|
62
|
+
showUI?: boolean;
|
|
63
|
+
/** CSS class for container */
|
|
64
|
+
className?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Utility Functions
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert VAPID key from base64url to Uint8Array
|
|
73
|
+
*/
|
|
74
|
+
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
75
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
76
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
77
|
+
|
|
78
|
+
const rawData = atob(base64);
|
|
79
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
82
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
83
|
+
}
|
|
84
|
+
return outputArray;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Serialize PushSubscription for server
|
|
89
|
+
*/
|
|
90
|
+
function serializeSubscription(sub: PushSubscription): PushSubscriptionData {
|
|
91
|
+
const p256dh = sub.getKey('p256dh');
|
|
92
|
+
const auth = sub.getKey('auth');
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
endpoint: sub.endpoint,
|
|
96
|
+
keys: {
|
|
97
|
+
p256dh: p256dh
|
|
98
|
+
? btoa(String.fromCharCode(...new Uint8Array(p256dh)))
|
|
99
|
+
: '',
|
|
100
|
+
auth: auth ? btoa(String.fromCharCode(...new Uint8Array(auth))) : '',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// usePushNotifications Hook
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
export interface UsePushNotificationsConfig {
|
|
110
|
+
/** VAPID public key */
|
|
111
|
+
vapidPublicKey?: string;
|
|
112
|
+
/** Service worker URL */
|
|
113
|
+
serviceWorkerUrl?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Hook for managing push notifications
|
|
118
|
+
*/
|
|
119
|
+
export function usePushNotifications(
|
|
120
|
+
config: UsePushNotificationsConfig = {},
|
|
121
|
+
): PushNotificationState {
|
|
122
|
+
const [isSupported, setIsSupported] = useState(false);
|
|
123
|
+
const [permission, setPermission] = useState<
|
|
124
|
+
NotificationPermission | 'unsupported'
|
|
125
|
+
>('unsupported');
|
|
126
|
+
const [subscription, setSubscription] = useState<PushSubscriptionData | null>(
|
|
127
|
+
null,
|
|
128
|
+
);
|
|
129
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
130
|
+
const [error, setError] = useState<string | null>(null);
|
|
131
|
+
|
|
132
|
+
const { vapidPublicKey, serviceWorkerUrl = '/sw.js' } = config;
|
|
133
|
+
|
|
134
|
+
// Check support and load existing subscription
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (typeof window === 'undefined') return;
|
|
137
|
+
|
|
138
|
+
const supported = 'serviceWorker' in navigator && 'PushManager' in window;
|
|
139
|
+
setIsSupported(supported);
|
|
140
|
+
|
|
141
|
+
if (!supported) {
|
|
142
|
+
setPermission('unsupported');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check current permission
|
|
147
|
+
setPermission(Notification.permission);
|
|
148
|
+
|
|
149
|
+
// Load existing subscription
|
|
150
|
+
navigator.serviceWorker.ready.then(async (registration) => {
|
|
151
|
+
try {
|
|
152
|
+
const existingSub = await registration.pushManager.getSubscription();
|
|
153
|
+
if (existingSub) {
|
|
154
|
+
setSubscription(serializeSubscription(existingSub));
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error('Error loading push subscription:', err);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
const requestPermission =
|
|
163
|
+
useCallback(async (): Promise<NotificationPermission> => {
|
|
164
|
+
if (!isSupported) {
|
|
165
|
+
return 'denied';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = await Notification.requestPermission();
|
|
169
|
+
setPermission(result);
|
|
170
|
+
return result;
|
|
171
|
+
}, [isSupported]);
|
|
172
|
+
|
|
173
|
+
const subscribe =
|
|
174
|
+
useCallback(async (): Promise<PushSubscriptionData | null> => {
|
|
175
|
+
if (!isSupported) {
|
|
176
|
+
setError('Push notifications are not supported');
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!vapidPublicKey) {
|
|
181
|
+
setError('VAPID public key is required');
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setIsLoading(true);
|
|
186
|
+
setError(null);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Ensure service worker is registered
|
|
190
|
+
let registration: ServiceWorkerRegistration;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
registration = await navigator.serviceWorker.ready;
|
|
194
|
+
} catch {
|
|
195
|
+
// Try to register if not ready
|
|
196
|
+
registration = await navigator.serviceWorker.register(
|
|
197
|
+
serviceWorkerUrl,
|
|
198
|
+
{
|
|
199
|
+
scope: '/',
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Request permission if needed
|
|
205
|
+
if (Notification.permission === 'default') {
|
|
206
|
+
const perm = await Notification.requestPermission();
|
|
207
|
+
setPermission(perm);
|
|
208
|
+
|
|
209
|
+
if (perm !== 'granted') {
|
|
210
|
+
throw new Error('Notification permission denied');
|
|
211
|
+
}
|
|
212
|
+
} else if (Notification.permission !== 'granted') {
|
|
213
|
+
throw new Error('Notification permission not granted');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Subscribe to push
|
|
217
|
+
const sub = await registration.pushManager.subscribe({
|
|
218
|
+
userVisibleOnly: true,
|
|
219
|
+
applicationServerKey: urlBase64ToUint8Array(
|
|
220
|
+
vapidPublicKey,
|
|
221
|
+
) as BufferSource,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const serialized = serializeSubscription(sub);
|
|
225
|
+
setSubscription(serialized);
|
|
226
|
+
|
|
227
|
+
return serialized;
|
|
228
|
+
} catch (err) {
|
|
229
|
+
const message =
|
|
230
|
+
err instanceof Error ? err.message : 'Failed to subscribe';
|
|
231
|
+
setError(message);
|
|
232
|
+
return null;
|
|
233
|
+
} finally {
|
|
234
|
+
setIsLoading(false);
|
|
235
|
+
}
|
|
236
|
+
}, [isSupported, vapidPublicKey, serviceWorkerUrl]);
|
|
237
|
+
|
|
238
|
+
const unsubscribe = useCallback(async (): Promise<boolean> => {
|
|
239
|
+
if (!isSupported) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
setIsLoading(true);
|
|
244
|
+
setError(null);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const registration = await navigator.serviceWorker.ready;
|
|
248
|
+
const sub = await registration.pushManager.getSubscription();
|
|
249
|
+
|
|
250
|
+
if (sub) {
|
|
251
|
+
await sub.unsubscribe();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setSubscription(null);
|
|
255
|
+
return true;
|
|
256
|
+
} catch (err) {
|
|
257
|
+
const message =
|
|
258
|
+
err instanceof Error ? err.message : 'Failed to unsubscribe';
|
|
259
|
+
setError(message);
|
|
260
|
+
return false;
|
|
261
|
+
} finally {
|
|
262
|
+
setIsLoading(false);
|
|
263
|
+
}
|
|
264
|
+
}, [isSupported]);
|
|
265
|
+
|
|
266
|
+
const clearError = useCallback(() => {
|
|
267
|
+
setError(null);
|
|
268
|
+
}, []);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
isSupported,
|
|
272
|
+
permission,
|
|
273
|
+
subscription,
|
|
274
|
+
isLoading,
|
|
275
|
+
error,
|
|
276
|
+
subscribe,
|
|
277
|
+
unsubscribe,
|
|
278
|
+
requestPermission,
|
|
279
|
+
clearError,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// PushNotifications Component
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Push notifications management component
|
|
289
|
+
*/
|
|
290
|
+
export function PushNotifications({
|
|
291
|
+
vapidPublicKey,
|
|
292
|
+
onSubscribe,
|
|
293
|
+
onUnsubscribe,
|
|
294
|
+
render,
|
|
295
|
+
showUI = true,
|
|
296
|
+
className,
|
|
297
|
+
}: PushNotificationsProps): ReactNode {
|
|
298
|
+
const state = usePushNotifications({ vapidPublicKey });
|
|
299
|
+
|
|
300
|
+
const handleSubscribe = async () => {
|
|
301
|
+
const sub = await state.subscribe();
|
|
302
|
+
if (sub && onSubscribe) {
|
|
303
|
+
await onSubscribe(sub);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const handleUnsubscribe = async () => {
|
|
308
|
+
const endpoint = state.subscription?.endpoint;
|
|
309
|
+
const success = await state.unsubscribe();
|
|
310
|
+
if (success && endpoint && onUnsubscribe) {
|
|
311
|
+
await onUnsubscribe(endpoint);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Custom render
|
|
316
|
+
if (render) {
|
|
317
|
+
return render(state);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Don't show UI if not requested
|
|
321
|
+
if (!showUI) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Not supported message
|
|
326
|
+
if (!state.isSupported) {
|
|
327
|
+
return (
|
|
328
|
+
<div className={className} role="region" aria-label="Push notifications">
|
|
329
|
+
<p style={{ color: '#6b7280', fontSize: '0.875rem' }}>
|
|
330
|
+
Push notifications are not supported in this browser.
|
|
331
|
+
</p>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div className={className} role="region" aria-label="Push notifications">
|
|
338
|
+
<h3
|
|
339
|
+
style={{
|
|
340
|
+
fontSize: '1.125rem',
|
|
341
|
+
fontWeight: 600,
|
|
342
|
+
marginBottom: '0.5rem',
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
Push Notifications
|
|
346
|
+
</h3>
|
|
347
|
+
|
|
348
|
+
{state.error && (
|
|
349
|
+
<div
|
|
350
|
+
style={{
|
|
351
|
+
padding: '0.75rem',
|
|
352
|
+
backgroundColor: '#fef2f2',
|
|
353
|
+
border: '1px solid #fecaca',
|
|
354
|
+
borderRadius: '0.375rem',
|
|
355
|
+
color: '#dc2626',
|
|
356
|
+
fontSize: '0.875rem',
|
|
357
|
+
marginBottom: '1rem',
|
|
358
|
+
}}
|
|
359
|
+
role="alert"
|
|
360
|
+
>
|
|
361
|
+
{state.error}
|
|
362
|
+
<button
|
|
363
|
+
onClick={state.clearError}
|
|
364
|
+
style={{
|
|
365
|
+
marginLeft: '0.5rem',
|
|
366
|
+
color: '#dc2626',
|
|
367
|
+
background: 'none',
|
|
368
|
+
border: 'none',
|
|
369
|
+
cursor: 'pointer',
|
|
370
|
+
}}
|
|
371
|
+
aria-label="Dismiss error"
|
|
372
|
+
>
|
|
373
|
+
✕
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
{state.subscription ? (
|
|
379
|
+
<div>
|
|
380
|
+
<p
|
|
381
|
+
style={{
|
|
382
|
+
color: '#10b981',
|
|
383
|
+
fontSize: '0.875rem',
|
|
384
|
+
marginBottom: '1rem',
|
|
385
|
+
}}
|
|
386
|
+
>
|
|
387
|
+
✓ You are subscribed to push notifications.
|
|
388
|
+
</p>
|
|
389
|
+
<button
|
|
390
|
+
onClick={handleUnsubscribe}
|
|
391
|
+
disabled={state.isLoading}
|
|
392
|
+
style={{
|
|
393
|
+
padding: '0.5rem 1rem',
|
|
394
|
+
backgroundColor: '#ef4444',
|
|
395
|
+
color: 'white',
|
|
396
|
+
border: 'none',
|
|
397
|
+
borderRadius: '0.375rem',
|
|
398
|
+
cursor: state.isLoading ? 'not-allowed' : 'pointer',
|
|
399
|
+
opacity: state.isLoading ? 0.5 : 1,
|
|
400
|
+
fontSize: '0.875rem',
|
|
401
|
+
}}
|
|
402
|
+
aria-label="Unsubscribe from push notifications"
|
|
403
|
+
>
|
|
404
|
+
{state.isLoading ? 'Unsubscribing...' : 'Unsubscribe'}
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
) : (
|
|
408
|
+
<div>
|
|
409
|
+
<p
|
|
410
|
+
style={{
|
|
411
|
+
color: '#6b7280',
|
|
412
|
+
fontSize: '0.875rem',
|
|
413
|
+
marginBottom: '1rem',
|
|
414
|
+
}}
|
|
415
|
+
>
|
|
416
|
+
You are not subscribed to push notifications.
|
|
417
|
+
</p>
|
|
418
|
+
<button
|
|
419
|
+
onClick={handleSubscribe}
|
|
420
|
+
disabled={state.isLoading || !vapidPublicKey}
|
|
421
|
+
style={{
|
|
422
|
+
padding: '0.5rem 1rem',
|
|
423
|
+
backgroundColor: '#0d9488',
|
|
424
|
+
color: 'white',
|
|
425
|
+
border: 'none',
|
|
426
|
+
borderRadius: '0.375rem',
|
|
427
|
+
cursor:
|
|
428
|
+
state.isLoading || !vapidPublicKey ? 'not-allowed' : 'pointer',
|
|
429
|
+
opacity: state.isLoading || !vapidPublicKey ? 0.5 : 1,
|
|
430
|
+
fontSize: '0.875rem',
|
|
431
|
+
}}
|
|
432
|
+
aria-label="Subscribe to push notifications"
|
|
433
|
+
>
|
|
434
|
+
{state.isLoading ? 'Subscribing...' : 'Subscribe'}
|
|
435
|
+
</button>
|
|
436
|
+
{!vapidPublicKey && (
|
|
437
|
+
<p
|
|
438
|
+
style={{
|
|
439
|
+
color: '#f59e0b',
|
|
440
|
+
fontSize: '0.75rem',
|
|
441
|
+
marginTop: '0.5rem',
|
|
442
|
+
}}
|
|
443
|
+
>
|
|
444
|
+
VAPID public key is required for push notifications.
|
|
445
|
+
</p>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export default PushNotifications;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aeon Navigation Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for the cutting-edge navigation system.
|
|
5
|
+
* The navigation state itself is an Aeon - the site is a session.
|
|
6
|
+
*
|
|
7
|
+
* Recursive Aeon Architecture:
|
|
8
|
+
* - Component = Aeon entity
|
|
9
|
+
* - Page = Aeon session
|
|
10
|
+
* - Site = Aeon of sessions (routes are collaborative)
|
|
11
|
+
* - Federation = Aeon of Aeons (cross-site sync)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
useContext,
|
|
16
|
+
useCallback,
|
|
17
|
+
useSyncExternalStore,
|
|
18
|
+
createContext,
|
|
19
|
+
} from 'react';
|
|
20
|
+
import type {
|
|
21
|
+
AeonNavigationEngine,
|
|
22
|
+
NavigationOptions,
|
|
23
|
+
PrefetchOptions,
|
|
24
|
+
NavigationState,
|
|
25
|
+
} from '@affectively/aeon-pages-runtime/navigation';
|
|
26
|
+
import { getNavigator } from '@affectively/aeon-pages-runtime/navigation';
|
|
27
|
+
import type { PresenceInfo as RoutePresenceInfo } from '@affectively/aeon-pages-runtime/navigation';
|
|
28
|
+
|
|
29
|
+
// Navigation-level predicted route (simpler than ML predictor's version)
|
|
30
|
+
export interface NavigationPrediction {
|
|
31
|
+
route: string;
|
|
32
|
+
probability: number;
|
|
33
|
+
reason: 'history' | 'hover' | 'visibility' | 'community';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Context for providing custom navigation engine
|
|
37
|
+
export interface AeonNavigationContextValue {
|
|
38
|
+
navigator: AeonNavigationEngine;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const AeonNavigationContext =
|
|
42
|
+
createContext<AeonNavigationContextValue | null>(null);
|
|
43
|
+
|
|
44
|
+
// Get navigator from context or use global singleton
|
|
45
|
+
function useNavigator(): AeonNavigationEngine {
|
|
46
|
+
const context = useContext(AeonNavigationContext);
|
|
47
|
+
return context?.navigator ?? getNavigator();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Main navigation hook - provides navigation, prefetch, and state
|
|
52
|
+
*/
|
|
53
|
+
export function useAeonNavigation() {
|
|
54
|
+
const navigator = useNavigator();
|
|
55
|
+
|
|
56
|
+
// Subscribe to navigation state changes with useSyncExternalStore
|
|
57
|
+
const state = useSyncExternalStore(
|
|
58
|
+
useCallback((callback) => navigator.subscribe(callback), [navigator]),
|
|
59
|
+
() => navigator.getState(),
|
|
60
|
+
() => navigator.getState(),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Navigation function with view transitions
|
|
64
|
+
const navigate = useCallback(
|
|
65
|
+
async (href: string, options?: NavigationOptions) => {
|
|
66
|
+
await navigator.navigate(href, options);
|
|
67
|
+
},
|
|
68
|
+
[navigator],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Prefetch a route (session + presence)
|
|
72
|
+
const prefetch = useCallback(
|
|
73
|
+
async (href: string, options?: PrefetchOptions) => {
|
|
74
|
+
await navigator.prefetch(href, options);
|
|
75
|
+
},
|
|
76
|
+
[navigator],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Go back in history
|
|
80
|
+
const back = useCallback(async () => {
|
|
81
|
+
await navigator.back();
|
|
82
|
+
}, [navigator]);
|
|
83
|
+
|
|
84
|
+
// Check if route is preloaded
|
|
85
|
+
const isPreloaded = useCallback(
|
|
86
|
+
(href: string): boolean => {
|
|
87
|
+
return navigator.isPreloaded(href);
|
|
88
|
+
},
|
|
89
|
+
[navigator],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Preload ALL routes (total preload strategy)
|
|
93
|
+
const preloadAll = useCallback(
|
|
94
|
+
async (onProgress?: (loaded: number, total: number) => void) => {
|
|
95
|
+
await navigator.preloadAll(onProgress);
|
|
96
|
+
},
|
|
97
|
+
[navigator],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Get cache statistics
|
|
101
|
+
const getCacheStats = useCallback(() => {
|
|
102
|
+
return navigator.getCacheStats();
|
|
103
|
+
}, [navigator]);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
// State
|
|
107
|
+
current: state.current,
|
|
108
|
+
previous: state.previous,
|
|
109
|
+
history: state.history,
|
|
110
|
+
isNavigating: state.isNavigating,
|
|
111
|
+
|
|
112
|
+
// Actions
|
|
113
|
+
navigate,
|
|
114
|
+
prefetch,
|
|
115
|
+
back,
|
|
116
|
+
preloadAll,
|
|
117
|
+
|
|
118
|
+
// Utilities
|
|
119
|
+
isPreloaded,
|
|
120
|
+
getCacheStats,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Route Presence hook - subscribe to who's viewing/editing routes
|
|
126
|
+
*
|
|
127
|
+
* Presence flows upward through the Aeon hierarchy:
|
|
128
|
+
* - Page presence = users on this page
|
|
129
|
+
* - Site presence = aggregate of all page presence
|
|
130
|
+
* - Federation presence = aggregate across sites
|
|
131
|
+
*
|
|
132
|
+
* Note: This is different from usePresence in provider.tsx which is for
|
|
133
|
+
* page-level editing presence. This hook is for navigation-level presence
|
|
134
|
+
* (who's viewing what routes before you navigate there).
|
|
135
|
+
*/
|
|
136
|
+
export function useRoutePresence() {
|
|
137
|
+
const navigator = useNavigator();
|
|
138
|
+
|
|
139
|
+
// Get cached presence for a route
|
|
140
|
+
const getPresence = useCallback(
|
|
141
|
+
(route: string): RoutePresenceInfo | null => {
|
|
142
|
+
return navigator.getPresence(route);
|
|
143
|
+
},
|
|
144
|
+
[navigator],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Subscribe to presence updates
|
|
148
|
+
const subscribePresence = useCallback(
|
|
149
|
+
(
|
|
150
|
+
callback: (route: string, presence: RoutePresenceInfo) => void,
|
|
151
|
+
): (() => void) => {
|
|
152
|
+
return navigator.subscribePresence(callback);
|
|
153
|
+
},
|
|
154
|
+
[navigator],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
getPresence,
|
|
159
|
+
subscribePresence,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Navigation prediction hook
|
|
165
|
+
*/
|
|
166
|
+
export function useNavigationPrediction(): {
|
|
167
|
+
predict: (fromRoute?: string) => NavigationPrediction[];
|
|
168
|
+
} {
|
|
169
|
+
const navigator = useNavigator();
|
|
170
|
+
|
|
171
|
+
// Get predictions for current route
|
|
172
|
+
const predict = useCallback(
|
|
173
|
+
(fromRoute?: string): NavigationPrediction[] => {
|
|
174
|
+
const state = navigator.getState();
|
|
175
|
+
return navigator.predict(fromRoute ?? state.current);
|
|
176
|
+
},
|
|
177
|
+
[navigator],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
predict,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Hook for observing links and auto-prefetching
|
|
187
|
+
*/
|
|
188
|
+
export function useLinkObserver(containerRef: React.RefObject<Element>) {
|
|
189
|
+
const navigator = useNavigator();
|
|
190
|
+
|
|
191
|
+
// Set up observation on mount
|
|
192
|
+
const observe = useCallback(() => {
|
|
193
|
+
if (!containerRef.current) return () => {};
|
|
194
|
+
return navigator.observeLinks(containerRef.current);
|
|
195
|
+
}, [navigator, containerRef]);
|
|
196
|
+
|
|
197
|
+
return { observe };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Hook for total preload progress
|
|
202
|
+
*/
|
|
203
|
+
export function useTotalPreload() {
|
|
204
|
+
const { preloadAll, getCacheStats } = useAeonNavigation();
|
|
205
|
+
|
|
206
|
+
// Preload with progress tracking
|
|
207
|
+
const startPreload = useCallback(
|
|
208
|
+
async (onProgress?: (loaded: number, total: number) => void) => {
|
|
209
|
+
await preloadAll(onProgress);
|
|
210
|
+
},
|
|
211
|
+
[preloadAll],
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
startPreload,
|
|
216
|
+
getStats: getCacheStats,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|