@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,86 @@
|
|
|
1
|
+
import type { PresenceUser } from '@affectively/aeon-pages/react';
|
|
2
|
+
|
|
3
|
+
interface CursorProps {
|
|
4
|
+
user: PresenceUser;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Generate a color from user ID
|
|
8
|
+
function userColor(userId: string): string {
|
|
9
|
+
const colors = [
|
|
10
|
+
'#ef4444', // red
|
|
11
|
+
'#f97316', // orange
|
|
12
|
+
'#eab308', // yellow
|
|
13
|
+
'#22c55e', // green
|
|
14
|
+
'#06b6d4', // cyan
|
|
15
|
+
'#3b82f6', // blue
|
|
16
|
+
'#336699', // steel blue
|
|
17
|
+
'#ec4899', // pink
|
|
18
|
+
];
|
|
19
|
+
let hash = 0;
|
|
20
|
+
for (let i = 0; i < userId.length; i++) {
|
|
21
|
+
hash = (hash << 5) - hash + userId.charCodeAt(i);
|
|
22
|
+
hash = hash & hash;
|
|
23
|
+
}
|
|
24
|
+
return colors[Math.abs(hash) % colors.length];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Cursor({ user }: CursorProps) {
|
|
28
|
+
if (!user.cursor || user.status === 'offline') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const color = userColor(user.userId);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
style={{
|
|
37
|
+
position: 'fixed',
|
|
38
|
+
left: user.cursor.x,
|
|
39
|
+
top: user.cursor.y,
|
|
40
|
+
pointerEvents: 'none',
|
|
41
|
+
zIndex: 9999,
|
|
42
|
+
transition: 'left 0.1s ease-out, top 0.1s ease-out',
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{/* Cursor arrow */}
|
|
46
|
+
<svg
|
|
47
|
+
width="24"
|
|
48
|
+
height="24"
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
fill="none"
|
|
51
|
+
style={{ filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.3))' }}
|
|
52
|
+
>
|
|
53
|
+
<path
|
|
54
|
+
d="M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a.5.5 0 0 1 .35-.15h6.87c.48 0 .72-.58.38-.92L5.92 2.53a.5.5 0 0 0-.42.68z"
|
|
55
|
+
fill={color}
|
|
56
|
+
stroke="white"
|
|
57
|
+
strokeWidth="1.5"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
|
|
61
|
+
{/* User label */}
|
|
62
|
+
<div
|
|
63
|
+
style={{
|
|
64
|
+
marginLeft: '16px',
|
|
65
|
+
marginTop: '-4px',
|
|
66
|
+
padding: '2px 8px',
|
|
67
|
+
backgroundColor: color,
|
|
68
|
+
color: 'white',
|
|
69
|
+
fontSize: '12px',
|
|
70
|
+
fontWeight: 500,
|
|
71
|
+
borderRadius: '4px',
|
|
72
|
+
whiteSpace: 'nowrap',
|
|
73
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{user.role === 'assistant' ? '🤖 ' : ''}
|
|
77
|
+
{user.userId.slice(0, 8)}
|
|
78
|
+
{user.editing && (
|
|
79
|
+
<span style={{ marginLeft: '4px', opacity: 0.8 }}>editing</span>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default Cursor;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useOfflineStatus } from '@affectively/aeon-pages/react';
|
|
2
|
+
|
|
3
|
+
export function OfflineIndicator() {
|
|
4
|
+
const { isOffline, isSyncing, pendingOperations, lastSyncAt } =
|
|
5
|
+
useOfflineStatus();
|
|
6
|
+
|
|
7
|
+
if (!isOffline && !isSyncing && pendingOperations === 0) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="fixed bottom-4 right-4 flex flex-col gap-2">
|
|
13
|
+
{/* Offline banner */}
|
|
14
|
+
{isOffline && (
|
|
15
|
+
<div className="flex items-center gap-2 bg-yellow-50 border border-yellow-200 rounded-lg px-4 py-2 shadow-lg">
|
|
16
|
+
<svg
|
|
17
|
+
className="w-5 h-5 text-yellow-500"
|
|
18
|
+
fill="none"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
>
|
|
22
|
+
<path
|
|
23
|
+
strokeLinecap="round"
|
|
24
|
+
strokeLinejoin="round"
|
|
25
|
+
strokeWidth={2}
|
|
26
|
+
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3"
|
|
27
|
+
/>
|
|
28
|
+
</svg>
|
|
29
|
+
<div>
|
|
30
|
+
<p className="text-sm font-medium text-yellow-800">
|
|
31
|
+
You're offline
|
|
32
|
+
</p>
|
|
33
|
+
<p className="text-xs text-yellow-600">
|
|
34
|
+
Changes will sync when back online
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
{/* Pending operations */}
|
|
41
|
+
{pendingOperations > 0 && (
|
|
42
|
+
<div className="flex items-center gap-2 bg-blue-50 border border-blue-200 rounded-lg px-4 py-2 shadow-lg">
|
|
43
|
+
<div className="w-5 h-5">
|
|
44
|
+
<svg
|
|
45
|
+
className="animate-spin text-blue-500"
|
|
46
|
+
fill="none"
|
|
47
|
+
viewBox="0 0 24 24"
|
|
48
|
+
>
|
|
49
|
+
<circle
|
|
50
|
+
className="opacity-25"
|
|
51
|
+
cx="12"
|
|
52
|
+
cy="12"
|
|
53
|
+
r="10"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
strokeWidth="4"
|
|
56
|
+
/>
|
|
57
|
+
<path
|
|
58
|
+
className="opacity-75"
|
|
59
|
+
fill="currentColor"
|
|
60
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
61
|
+
/>
|
|
62
|
+
</svg>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<p className="text-sm font-medium text-blue-800">
|
|
66
|
+
{isSyncing ? 'Syncing...' : `${pendingOperations} pending`}
|
|
67
|
+
</p>
|
|
68
|
+
<p className="text-xs text-blue-600">
|
|
69
|
+
{pendingOperations}{' '}
|
|
70
|
+
{pendingOperations === 1 ? 'change' : 'changes'} to sync
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{/* Last sync time */}
|
|
77
|
+
{lastSyncAt && !isOffline && pendingOperations === 0 && (
|
|
78
|
+
<div className="text-xs text-gray-400 text-right">
|
|
79
|
+
Last synced: {formatRelativeTime(lastSyncAt)}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatRelativeTime(isoString: string): string {
|
|
87
|
+
const date = new Date(isoString);
|
|
88
|
+
const now = new Date();
|
|
89
|
+
const diff = now.getTime() - date.getTime();
|
|
90
|
+
|
|
91
|
+
const seconds = Math.floor(diff / 1000);
|
|
92
|
+
const minutes = Math.floor(seconds / 60);
|
|
93
|
+
const hours = Math.floor(minutes / 60);
|
|
94
|
+
|
|
95
|
+
if (seconds < 10) return 'just now';
|
|
96
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
97
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
98
|
+
if (hours < 24) return `${hours}h ago`;
|
|
99
|
+
|
|
100
|
+
return date.toLocaleDateString();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default OfflineIndicator;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { PresenceUser } from '@affectively/aeon-pages/react';
|
|
2
|
+
|
|
3
|
+
interface PresenceBarProps {
|
|
4
|
+
users: PresenceUser[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function PresenceBar({ users }: PresenceBarProps) {
|
|
8
|
+
const onlineUsers = users.filter((u) => u.status === 'online');
|
|
9
|
+
const awayUsers = users.filter((u) => u.status === 'away');
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="fixed top-4 right-4 flex items-center gap-2 bg-white rounded-full px-4 py-2 shadow-lg border border-gray-200">
|
|
13
|
+
{/* Online indicator */}
|
|
14
|
+
<div className="flex items-center gap-1">
|
|
15
|
+
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
16
|
+
<span className="text-sm text-gray-600">
|
|
17
|
+
{onlineUsers.length} online
|
|
18
|
+
</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{awayUsers.length > 0 && (
|
|
22
|
+
<>
|
|
23
|
+
<div className="w-px h-4 bg-gray-200" />
|
|
24
|
+
<div className="flex items-center gap-1">
|
|
25
|
+
<div className="w-2 h-2 bg-yellow-500 rounded-full" />
|
|
26
|
+
<span className="text-sm text-gray-500">
|
|
27
|
+
{awayUsers.length} away
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
</>
|
|
31
|
+
)}
|
|
32
|
+
|
|
33
|
+
{/* User avatars */}
|
|
34
|
+
<div className="w-px h-4 bg-gray-200 ml-2" />
|
|
35
|
+
<div className="flex -space-x-2">
|
|
36
|
+
{users.slice(0, 5).map((user) => (
|
|
37
|
+
<UserAvatar key={user.userId} user={user} />
|
|
38
|
+
))}
|
|
39
|
+
{users.length > 5 && (
|
|
40
|
+
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center text-xs text-gray-600 border-2 border-white">
|
|
41
|
+
+{users.length - 5}
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function UserAvatar({ user }: { user: PresenceUser }) {
|
|
50
|
+
const isAgent = user.role === 'assistant';
|
|
51
|
+
const statusColor =
|
|
52
|
+
user.status === 'online'
|
|
53
|
+
? 'bg-green-500'
|
|
54
|
+
: user.status === 'away'
|
|
55
|
+
? 'bg-yellow-500'
|
|
56
|
+
: 'bg-gray-400';
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className="relative w-8 h-8 rounded-full border-2 border-white flex items-center justify-center text-xs font-medium"
|
|
61
|
+
style={{
|
|
62
|
+
backgroundColor: isAgent ? '#336699' : '#3b82f6',
|
|
63
|
+
color: 'white',
|
|
64
|
+
}}
|
|
65
|
+
title={`${user.userId.slice(0, 8)} (${user.role})`}
|
|
66
|
+
>
|
|
67
|
+
{isAgent ? '🤖' : user.userId.slice(0, 2).toUpperCase()}
|
|
68
|
+
|
|
69
|
+
{/* Status indicator */}
|
|
70
|
+
<div
|
|
71
|
+
className={`absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full ${statusColor} border border-white`}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default PresenceBar;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@affectively/aeon-pages-example-basic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "aeon dev",
|
|
8
|
+
"build": "aeon build",
|
|
9
|
+
"start": "aeon start"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@affectively/aeon-pages": "workspace:*",
|
|
13
|
+
"react": "^18.0.0",
|
|
14
|
+
"react-dom": "^18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.7.0",
|
|
18
|
+
"@types/react": "^18.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use aeon';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
usePresence,
|
|
5
|
+
useAeonData,
|
|
6
|
+
useCursorTracking,
|
|
7
|
+
} from '@affectively/aeon-pages/react';
|
|
8
|
+
import { Cursor } from '../components/Cursor';
|
|
9
|
+
import { PresenceBar } from '../components/PresenceBar';
|
|
10
|
+
import { OfflineIndicator } from '../components/OfflineIndicator';
|
|
11
|
+
|
|
12
|
+
export default function HomePage() {
|
|
13
|
+
const { presence, localUser } = usePresence();
|
|
14
|
+
const [title, setTitle] = useAeonData<string>('title');
|
|
15
|
+
const [description, setDescription] = useAeonData<string>('description');
|
|
16
|
+
|
|
17
|
+
// Auto-track cursor movement
|
|
18
|
+
useCursorTracking(true);
|
|
19
|
+
|
|
20
|
+
// Filter out self from presence
|
|
21
|
+
const otherUsers = presence.filter((u) => u.userId !== localUser?.userId);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="min-h-screen bg-gray-50 p-8">
|
|
25
|
+
{/* Presence bar showing who's viewing */}
|
|
26
|
+
<PresenceBar users={presence} />
|
|
27
|
+
|
|
28
|
+
{/* Offline indicator */}
|
|
29
|
+
<OfflineIndicator />
|
|
30
|
+
|
|
31
|
+
{/* Main content - editable in place */}
|
|
32
|
+
<main className="max-w-4xl mx-auto mt-8">
|
|
33
|
+
<h1
|
|
34
|
+
contentEditable
|
|
35
|
+
suppressContentEditableWarning
|
|
36
|
+
onBlur={(e) => setTitle(e.currentTarget.textContent || '')}
|
|
37
|
+
className="text-4xl font-bold text-gray-900 outline-none focus:ring-2 focus:ring-blue-500 rounded px-2 py-1"
|
|
38
|
+
>
|
|
39
|
+
{title || 'Welcome to Aeon Pages'}
|
|
40
|
+
</h1>
|
|
41
|
+
|
|
42
|
+
<p
|
|
43
|
+
contentEditable
|
|
44
|
+
suppressContentEditableWarning
|
|
45
|
+
onBlur={(e) => setDescription(e.currentTarget.textContent || '')}
|
|
46
|
+
className="mt-4 text-xl text-gray-600 outline-none focus:ring-2 focus:ring-blue-500 rounded px-2 py-1"
|
|
47
|
+
>
|
|
48
|
+
{description || 'Click any text to edit. Changes sync in real-time.'}
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<div className="mt-8 p-6 bg-white rounded-lg shadow">
|
|
52
|
+
<h2 className="text-2xl font-semibold text-gray-800">Features</h2>
|
|
53
|
+
<ul className="mt-4 space-y-2 text-gray-700">
|
|
54
|
+
<li>Real-time collaborative editing</li>
|
|
55
|
+
<li>See other editors cursors</li>
|
|
56
|
+
<li>Works offline, syncs when back online</li>
|
|
57
|
+
<li>Version history with rollback</li>
|
|
58
|
+
<li>~20KB runtime</li>
|
|
59
|
+
</ul>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="mt-8 text-sm text-gray-500">
|
|
63
|
+
{otherUsers.length > 0 ? (
|
|
64
|
+
<p>
|
|
65
|
+
{otherUsers.length} other{' '}
|
|
66
|
+
{otherUsers.length === 1 ? 'person' : 'people'} viewing this page
|
|
67
|
+
</p>
|
|
68
|
+
) : (
|
|
69
|
+
<p>You're the only one here</p>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</main>
|
|
73
|
+
|
|
74
|
+
{/* Render other users' cursors */}
|
|
75
|
+
{otherUsers.map((user) => (
|
|
76
|
+
<Cursor key={user.userId} user={user} />
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@affectively/aeon-pages",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Collaborative page surface with CRDT-based flux state, Edge Side Inference, and zero-CLS rendering. Pages that think.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"packages/*"
|
|
8
|
+
],
|
|
9
|
+
"main": "./packages/runtime/dist/index.js",
|
|
10
|
+
"types": "./packages/runtime/dist/index.d.ts",
|
|
11
|
+
"bin": {
|
|
12
|
+
"aeon": "packages/cli/dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./packages/runtime/dist/index.js",
|
|
17
|
+
"types": "./packages/runtime/dist/index.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./runtime": {
|
|
20
|
+
"import": "./packages/runtime/dist/index.js",
|
|
21
|
+
"types": "./packages/runtime/dist/index.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./react": {
|
|
24
|
+
"import": "./packages/react/dist/index.js",
|
|
25
|
+
"types": "./packages/react/dist/index.d.ts"
|
|
26
|
+
},
|
|
27
|
+
"./esi": {
|
|
28
|
+
"import": "./packages/runtime/dist/router/index.js",
|
|
29
|
+
"types": "./packages/runtime/dist/router/index.d.ts"
|
|
30
|
+
},
|
|
31
|
+
"./server": {
|
|
32
|
+
"import": "./packages/runtime/dist/server.js",
|
|
33
|
+
"types": "./packages/runtime/dist/server.d.ts"
|
|
34
|
+
},
|
|
35
|
+
"./auth": {
|
|
36
|
+
"import": "@affectively/auth",
|
|
37
|
+
"types": "@affectively/auth"
|
|
38
|
+
},
|
|
39
|
+
"./directives": {
|
|
40
|
+
"import": "./packages/directives/dist/use-aeon.js",
|
|
41
|
+
"types": "./packages/directives/dist/use-aeon.d.ts"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "bun run build:wasm && bun run build:ts",
|
|
46
|
+
"build:wasm": "cd packages/runtime-wasm && wasm-pack build --target web --out-dir ../runtime/wasm",
|
|
47
|
+
"build:ts": "bun run --filter='./packages/*' build",
|
|
48
|
+
"dev": "bun run packages/cli/src/dev.ts",
|
|
49
|
+
"test": "bun test",
|
|
50
|
+
"clean": "rm -rf packages/*/dist packages/runtime/wasm"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@affectively/aeon": "^1.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"typescript": "^5.7.0",
|
|
57
|
+
"@types/bun": "latest",
|
|
58
|
+
"@types/react": "^18.0.0",
|
|
59
|
+
"wasm-pack": "^0.13.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"bun": ">=1.0.0",
|
|
63
|
+
"react": ">=18.0.0",
|
|
64
|
+
"@affectively/auth": ">=0.1.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"@affectively/auth": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"bun": ">=1.0.0"
|
|
73
|
+
},
|
|
74
|
+
"license": "MIT",
|
|
75
|
+
"repository": {
|
|
76
|
+
"type": "git",
|
|
77
|
+
"url": "git+https://github.com/affectively-ai/aeon-flux.git"
|
|
78
|
+
},
|
|
79
|
+
"keywords": [
|
|
80
|
+
"aeon",
|
|
81
|
+
"flux",
|
|
82
|
+
"crdt",
|
|
83
|
+
"framework",
|
|
84
|
+
"collaborative",
|
|
85
|
+
"cms",
|
|
86
|
+
"real-time",
|
|
87
|
+
"bun",
|
|
88
|
+
"cloudflare",
|
|
89
|
+
"d1",
|
|
90
|
+
"durable-objects",
|
|
91
|
+
"wasm",
|
|
92
|
+
"skeleton",
|
|
93
|
+
"zero-cls",
|
|
94
|
+
"performance"
|
|
95
|
+
],
|
|
96
|
+
"author": "AFFECTIVELY",
|
|
97
|
+
"homepage": "https://github.com/affectively-ai/aeon-flux#readme",
|
|
98
|
+
"bugs": {
|
|
99
|
+
"url": "https://github.com/affectively-ai/aeon-flux/issues"
|
|
100
|
+
}
|
|
101
|
+
}
|