@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,430 @@
1
+ //! Skeleton Rendering for Zero-CLS
2
+ //!
3
+ //! WASM-based skeleton renderer that produces HTML placeholders
4
+ //! matching the exact dimensions of final content.
5
+
6
+ use wasm_bindgen::prelude::*;
7
+ use serde::{Deserialize, Serialize};
8
+ use std::collections::HashMap;
9
+
10
+ /// Skeleton dimensions
11
+ #[derive(Clone, Debug, Default, Serialize, Deserialize)]
12
+ pub struct SkeletonDimensions {
13
+ pub width: Option<String>,
14
+ pub height: Option<String>,
15
+ #[serde(rename = "minHeight")]
16
+ pub min_height: Option<String>,
17
+ #[serde(rename = "aspectRatio")]
18
+ pub aspect_ratio: Option<String>,
19
+ pub padding: Option<String>,
20
+ pub margin: Option<String>,
21
+ #[serde(rename = "borderRadius")]
22
+ pub border_radius: Option<String>,
23
+ pub gap: Option<String>,
24
+ }
25
+
26
+ /// Skeleton metadata attached to nodes
27
+ #[derive(Clone, Debug, Serialize, Deserialize)]
28
+ pub struct SkeletonMetadata {
29
+ pub dimensions: SkeletonDimensions,
30
+ pub shape: String,
31
+ pub lines: Option<u32>,
32
+ #[serde(rename = "isDynamic")]
33
+ pub is_dynamic: bool,
34
+ pub confidence: f32,
35
+ pub source: String,
36
+ }
37
+
38
+ /// Component node with optional skeleton
39
+ #[derive(Clone, Debug, Serialize, Deserialize)]
40
+ pub struct SkeletonNode {
41
+ #[serde(rename = "type")]
42
+ pub node_type: String,
43
+ pub props: Option<HashMap<String, serde_json::Value>>,
44
+ pub children: Option<Vec<SkeletonChild>>,
45
+ #[serde(rename = "_skeleton")]
46
+ pub skeleton: Option<SkeletonMetadata>,
47
+ }
48
+
49
+ #[derive(Clone, Debug, Serialize, Deserialize)]
50
+ #[serde(untagged)]
51
+ pub enum SkeletonChild {
52
+ Text(String),
53
+ Node(Box<SkeletonNode>),
54
+ }
55
+
56
+ /// Render a skeleton tree to HTML
57
+ #[wasm_bindgen]
58
+ pub fn render_skeleton(tree_json: &str) -> String {
59
+ let tree: SkeletonNode = match serde_json::from_str(tree_json) {
60
+ Ok(t) => t,
61
+ Err(_) => return String::new(),
62
+ };
63
+
64
+ render_skeleton_node(&tree)
65
+ }
66
+
67
+ fn render_skeleton_node(node: &SkeletonNode) -> String {
68
+ let skeleton = match &node.skeleton {
69
+ Some(s) if s.is_dynamic => s,
70
+ _ => {
71
+ // Not dynamic or no skeleton - render children skeletons only
72
+ return render_children_skeleton(&node.children);
73
+ }
74
+ };
75
+
76
+ let style = build_skeleton_style(&skeleton.dimensions, &skeleton.shape);
77
+ let class = format!("aeon-skeleton aeon-skeleton--{}", skeleton.shape);
78
+
79
+ match skeleton.shape.as_str() {
80
+ "text-block" => {
81
+ let lines = skeleton.lines.unwrap_or(3);
82
+ let mut html = format!(
83
+ r#"<div class="{}" style="{}" aria-hidden="true">"#,
84
+ class, style
85
+ );
86
+ for i in 0..lines {
87
+ // Last line is shorter to look more natural
88
+ let line_width = if i == lines - 1 { "60%" } else { "100%" };
89
+ html.push_str(&format!(
90
+ r#"<div class="aeon-skeleton--line" style="width: {}; height: 1em; margin-bottom: 0.5em;"></div>"#,
91
+ line_width
92
+ ));
93
+ }
94
+ html.push_str("</div>");
95
+ html
96
+ }
97
+ "container" => {
98
+ // For containers, render skeleton wrapper with child skeletons
99
+ let children_html = render_children_skeleton(&node.children);
100
+ format!(
101
+ r#"<div class="{}" style="{}" aria-hidden="true">{}</div>"#,
102
+ class, style, children_html
103
+ )
104
+ }
105
+ _ => {
106
+ // Simple shapes: rect, circle, text-line
107
+ format!(
108
+ r#"<div class="{}" style="{}" aria-hidden="true"></div>"#,
109
+ class, style
110
+ )
111
+ }
112
+ }
113
+ }
114
+
115
+ fn render_children_skeleton(children: &Option<Vec<SkeletonChild>>) -> String {
116
+ match children {
117
+ None => String::new(),
118
+ Some(children) => children
119
+ .iter()
120
+ .filter_map(|child| match child {
121
+ SkeletonChild::Text(_) => None, // Skip text in skeleton
122
+ SkeletonChild::Node(n) => Some(render_skeleton_node(n)),
123
+ })
124
+ .collect(),
125
+ }
126
+ }
127
+
128
+ fn build_skeleton_style(dims: &SkeletonDimensions, shape: &str) -> String {
129
+ let mut styles = Vec::new();
130
+
131
+ // Apply dimensions
132
+ if let Some(w) = &dims.width {
133
+ styles.push(format!("width: {}", w));
134
+ }
135
+ if let Some(h) = &dims.height {
136
+ styles.push(format!("height: {}", h));
137
+ }
138
+ if let Some(mh) = &dims.min_height {
139
+ styles.push(format!("min-height: {}", mh));
140
+ }
141
+ if let Some(ar) = &dims.aspect_ratio {
142
+ styles.push(format!("aspect-ratio: {}", ar));
143
+ }
144
+ if let Some(p) = &dims.padding {
145
+ styles.push(format!("padding: {}", p));
146
+ }
147
+ if let Some(m) = &dims.margin {
148
+ styles.push(format!("margin: {}", m));
149
+ }
150
+ if let Some(gap) = &dims.gap {
151
+ styles.push(format!("gap: {}", gap));
152
+ }
153
+
154
+ // Shape-specific border radius
155
+ let radius = dims.border_radius.as_deref().unwrap_or(match shape {
156
+ "circle" => "50%",
157
+ "rect" => "0.25rem",
158
+ "text-line" | "text-block" => "0.125rem",
159
+ "container" => "0",
160
+ _ => "0.25rem",
161
+ });
162
+ styles.push(format!("border-radius: {}", radius));
163
+
164
+ // Container display
165
+ if shape == "container" {
166
+ styles.push("display: flex".to_string());
167
+ styles.push("flex-direction: column".to_string());
168
+ }
169
+
170
+ styles.join("; ")
171
+ }
172
+
173
+ /// Generate skeleton CSS with pulse animation
174
+ #[wasm_bindgen]
175
+ pub fn generate_skeleton_css() -> String {
176
+ r#"/* Aeon Skeleton Styles - Zero CLS */
177
+ .aeon-skeleton {
178
+ background: linear-gradient(
179
+ 90deg,
180
+ var(--aeon-skeleton-base, #e5e7eb) 0%,
181
+ var(--aeon-skeleton-highlight, #f3f4f6) 50%,
182
+ var(--aeon-skeleton-base, #e5e7eb) 100%
183
+ );
184
+ background-size: 200% 100%;
185
+ animation: aeon-skeleton-pulse 1.5s ease-in-out infinite;
186
+ }
187
+
188
+ .aeon-skeleton--rect {
189
+ display: block;
190
+ }
191
+
192
+ .aeon-skeleton--circle {
193
+ display: block;
194
+ }
195
+
196
+ .aeon-skeleton--text-line {
197
+ display: block;
198
+ height: 1em;
199
+ }
200
+
201
+ .aeon-skeleton--text-block {
202
+ display: flex;
203
+ flex-direction: column;
204
+ }
205
+
206
+ .aeon-skeleton--line {
207
+ background: inherit;
208
+ background-size: inherit;
209
+ animation: inherit;
210
+ border-radius: 0.125rem;
211
+ }
212
+
213
+ .aeon-skeleton--container {
214
+ background: transparent;
215
+ animation: none;
216
+ }
217
+
218
+ @keyframes aeon-skeleton-pulse {
219
+ 0% { background-position: 200% 0; }
220
+ 100% { background-position: -200% 0; }
221
+ }
222
+
223
+ /* Dark mode support */
224
+ @media (prefers-color-scheme: dark) {
225
+ :root {
226
+ --aeon-skeleton-base: #374151;
227
+ --aeon-skeleton-highlight: #4b5563;
228
+ }
229
+ }
230
+
231
+ /* Reduced motion support */
232
+ @media (prefers-reduced-motion: reduce) {
233
+ .aeon-skeleton,
234
+ .aeon-skeleton--line {
235
+ animation: none;
236
+ background-size: 100% 100%;
237
+ }
238
+ }
239
+ "#
240
+ .to_string()
241
+ }
242
+
243
+ /// Render a complete skeleton page (skeleton HTML + CSS)
244
+ #[wasm_bindgen]
245
+ pub fn render_skeleton_page(tree_json: &str) -> String {
246
+ let skeleton_html = render_skeleton(tree_json);
247
+ let skeleton_css = generate_skeleton_css();
248
+
249
+ format!(
250
+ r#"<style>{}</style>
251
+ <div id="aeon-skeleton" aria-hidden="true">{}</div>"#,
252
+ skeleton_css, skeleton_html
253
+ )
254
+ }
255
+
256
+ /// Get skeleton stats from a tree
257
+ #[wasm_bindgen]
258
+ pub fn get_skeleton_stats(tree_json: &str) -> String {
259
+ let tree: SkeletonNode = match serde_json::from_str(tree_json) {
260
+ Ok(t) => t,
261
+ Err(_) => return r#"{"error": "Invalid tree JSON"}"#.to_string(),
262
+ };
263
+
264
+ let mut total_nodes = 0;
265
+ let mut nodes_with_skeleton = 0;
266
+ let mut confidence_sum = 0.0f32;
267
+ let mut shapes: HashMap<String, u32> = HashMap::new();
268
+
269
+ fn walk(
270
+ node: &SkeletonNode,
271
+ total: &mut u32,
272
+ with_skeleton: &mut u32,
273
+ confidence: &mut f32,
274
+ shapes: &mut HashMap<String, u32>,
275
+ ) {
276
+ *total += 1;
277
+
278
+ if let Some(skeleton) = &node.skeleton {
279
+ if skeleton.is_dynamic {
280
+ *with_skeleton += 1;
281
+ *confidence += skeleton.confidence;
282
+ *shapes.entry(skeleton.shape.clone()).or_insert(0) += 1;
283
+ }
284
+ }
285
+
286
+ if let Some(children) = &node.children {
287
+ for child in children {
288
+ if let SkeletonChild::Node(n) = child {
289
+ walk(n, total, with_skeleton, confidence, shapes);
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ walk(
296
+ &tree,
297
+ &mut total_nodes,
298
+ &mut nodes_with_skeleton,
299
+ &mut confidence_sum,
300
+ &mut shapes,
301
+ );
302
+
303
+ let avg_confidence = if nodes_with_skeleton > 0 {
304
+ confidence_sum / nodes_with_skeleton as f32
305
+ } else {
306
+ 0.0
307
+ };
308
+
309
+ serde_json::json!({
310
+ "totalNodes": total_nodes,
311
+ "nodesWithSkeleton": nodes_with_skeleton,
312
+ "averageConfidence": avg_confidence,
313
+ "shapeDistribution": shapes
314
+ })
315
+ .to_string()
316
+ }
317
+
318
+ #[cfg(test)]
319
+ mod tests {
320
+ use super::*;
321
+
322
+ #[test]
323
+ fn test_render_simple_skeleton() {
324
+ let tree = r#"{
325
+ "type": "div",
326
+ "props": {"className": "w-64 h-12"},
327
+ "children": [],
328
+ "_skeleton": {
329
+ "dimensions": {"width": "16rem", "height": "3rem"},
330
+ "shape": "rect",
331
+ "isDynamic": true,
332
+ "confidence": 0.8,
333
+ "source": "tailwind"
334
+ }
335
+ }"#;
336
+
337
+ let html = render_skeleton(tree);
338
+ assert!(html.contains("aeon-skeleton"));
339
+ assert!(html.contains("aeon-skeleton--rect"));
340
+ assert!(html.contains("width: 16rem"));
341
+ assert!(html.contains("height: 3rem"));
342
+ }
343
+
344
+ #[test]
345
+ fn test_render_text_block() {
346
+ let tree = r#"{
347
+ "type": "p",
348
+ "props": {},
349
+ "children": [],
350
+ "_skeleton": {
351
+ "dimensions": {},
352
+ "shape": "text-block",
353
+ "lines": 3,
354
+ "isDynamic": true,
355
+ "confidence": 1.0,
356
+ "source": "hint"
357
+ }
358
+ }"#;
359
+
360
+ let html = render_skeleton(tree);
361
+ assert!(html.contains("aeon-skeleton--text-block"));
362
+ assert!(html.contains("aeon-skeleton--line"));
363
+ // Should have 3 lines
364
+ assert_eq!(html.matches("aeon-skeleton--line").count(), 3);
365
+ }
366
+
367
+ #[test]
368
+ fn test_render_circle() {
369
+ let tree = r#"{
370
+ "type": "img",
371
+ "props": {},
372
+ "children": [],
373
+ "_skeleton": {
374
+ "dimensions": {"width": "3rem", "height": "3rem"},
375
+ "shape": "circle",
376
+ "isDynamic": true,
377
+ "confidence": 1.0,
378
+ "source": "hint"
379
+ }
380
+ }"#;
381
+
382
+ let html = render_skeleton(tree);
383
+ assert!(html.contains("aeon-skeleton--circle"));
384
+ assert!(html.contains("border-radius: 50%"));
385
+ }
386
+
387
+ #[test]
388
+ fn test_generate_css() {
389
+ let css = generate_skeleton_css();
390
+ assert!(css.contains(".aeon-skeleton"));
391
+ assert!(css.contains("@keyframes aeon-skeleton-pulse"));
392
+ assert!(css.contains("prefers-color-scheme: dark"));
393
+ assert!(css.contains("prefers-reduced-motion"));
394
+ }
395
+
396
+ #[test]
397
+ fn test_skeleton_stats() {
398
+ let tree = r#"{
399
+ "type": "div",
400
+ "children": [
401
+ {
402
+ "type": "img",
403
+ "_skeleton": {
404
+ "dimensions": {},
405
+ "shape": "circle",
406
+ "isDynamic": true,
407
+ "confidence": 0.9,
408
+ "source": "tailwind"
409
+ }
410
+ },
411
+ {
412
+ "type": "p",
413
+ "_skeleton": {
414
+ "dimensions": {},
415
+ "shape": "text-line",
416
+ "isDynamic": true,
417
+ "confidence": 0.7,
418
+ "source": "tailwind"
419
+ }
420
+ }
421
+ ]
422
+ }"#;
423
+
424
+ let stats = get_skeleton_stats(tree);
425
+ let parsed: serde_json::Value = serde_json::from_str(&stats).unwrap();
426
+
427
+ assert_eq!(parsed["totalNodes"], 3);
428
+ assert_eq!(parsed["nodesWithSkeleton"], 2);
429
+ }
430
+ }