@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,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
|
+
}
|