@markdy/astro 0.1.0 → 0.1.2
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/package.json +9 -5
- package/src/Markdy.astro +89 -8
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markdy/astro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Astro island component for MarkdyScript animations.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
7
8
|
"files": [
|
|
8
9
|
"src",
|
|
9
10
|
"README.md",
|
|
@@ -17,7 +18,10 @@
|
|
|
17
18
|
"astro",
|
|
18
19
|
"animation",
|
|
19
20
|
"island",
|
|
20
|
-
"component"
|
|
21
|
+
"component",
|
|
22
|
+
"astro-animation",
|
|
23
|
+
"astro-component",
|
|
24
|
+
"declarative"
|
|
21
25
|
],
|
|
22
26
|
"author": "Hoang Yell <hoangyell@gmail.com> (https://hoangyell.com)",
|
|
23
27
|
"homepage": "https://markdy.com",
|
|
@@ -33,13 +37,13 @@
|
|
|
33
37
|
"access": "public"
|
|
34
38
|
},
|
|
35
39
|
"dependencies": {
|
|
36
|
-
"@markdy/renderer-dom": "0.1.
|
|
40
|
+
"@markdy/renderer-dom": "0.1.2"
|
|
37
41
|
},
|
|
38
42
|
"peerDependencies": {
|
|
39
43
|
"astro": ">=4.0.0"
|
|
40
44
|
},
|
|
41
45
|
"devDependencies": {
|
|
42
|
-
"astro": "^
|
|
43
|
-
"typescript": "^5.
|
|
46
|
+
"astro": "^6.1.5",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
44
48
|
}
|
|
45
49
|
}
|
package/src/Markdy.astro
CHANGED
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
* @markdy/astro — <Markdy /> island component
|
|
4
4
|
*
|
|
5
5
|
* SSR: renders a correctly-sized placeholder div so layout is stable.
|
|
6
|
+
* Exposes `title` / `description` for SEO and a `<noscript>` block
|
|
7
|
+
* so search-engine crawlers that do not execute JavaScript still
|
|
8
|
+
* index meaningful text.
|
|
6
9
|
* Client: hydrates via IntersectionObserver when the element enters the
|
|
7
|
-
* viewport
|
|
10
|
+
* viewport. The renderer is dynamically imported so it is fully
|
|
11
|
+
* code-split from the host-page bundle and never blocks the
|
|
12
|
+
* critical rendering path.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
export interface Props {
|
|
@@ -32,6 +37,17 @@ export interface Props {
|
|
|
32
37
|
/** Start playing as soon as the scene is hydrated. Defaults to true. */
|
|
33
38
|
autoplay?: boolean;
|
|
34
39
|
class?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Short human-readable title for the animation.
|
|
42
|
+
* Used as the accessible name (`aria-label`) and treated like `alt` on
|
|
43
|
+
* an image for SEO purposes.
|
|
44
|
+
*/
|
|
45
|
+
title?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Longer description rendered inside `<noscript>` so crawlers that do
|
|
48
|
+
* not run JavaScript still index meaningful content.
|
|
49
|
+
*/
|
|
50
|
+
description?: string;
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
const {
|
|
@@ -42,6 +58,8 @@ const {
|
|
|
42
58
|
assets = {},
|
|
43
59
|
autoplay = true,
|
|
44
60
|
class: className,
|
|
61
|
+
title = "Markdy animation",
|
|
62
|
+
description,
|
|
45
63
|
} = Astro.props;
|
|
46
64
|
---
|
|
47
65
|
|
|
@@ -50,30 +68,54 @@ const {
|
|
|
50
68
|
data-markdy-code={code}
|
|
51
69
|
data-markdy-assets={JSON.stringify(assets)}
|
|
52
70
|
data-markdy-autoplay={String(autoplay)}
|
|
71
|
+
role="img"
|
|
72
|
+
aria-label={title}
|
|
73
|
+
aria-busy="true"
|
|
53
74
|
style={`width:${width}px;height:${height}px;overflow:hidden`}
|
|
54
75
|
>
|
|
55
76
|
<!--
|
|
56
77
|
SSR placeholder: keeps layout stable before the island hydrates.
|
|
57
78
|
Background matches the scene bg prop to avoid a visible flash.
|
|
79
|
+
aria-hidden because the parent role="img" aria-label carries the
|
|
80
|
+
accessible name — this inner text is decorative.
|
|
58
81
|
-->
|
|
59
82
|
<div
|
|
60
83
|
class="markdy-placeholder"
|
|
61
|
-
aria-
|
|
84
|
+
aria-hidden="true"
|
|
62
85
|
style={`width:${width}px;height:${height}px;background:${bg};display:flex;align-items:center;justify-content:center`}
|
|
63
86
|
>
|
|
64
87
|
<span
|
|
65
88
|
style="font-family:sans-serif;font-size:12px;color:#bbb;letter-spacing:0.05em"
|
|
66
|
-
aria-hidden="true"
|
|
67
89
|
>
|
|
68
90
|
▶ markdy
|
|
69
91
|
</span>
|
|
70
92
|
</div>
|
|
93
|
+
|
|
94
|
+
<!--
|
|
95
|
+
noscript fallback: rendered for crawlers / users that do not run JS.
|
|
96
|
+
Provides indexable text content so the animation does not become an
|
|
97
|
+
opaque block in search results.
|
|
98
|
+
-->
|
|
99
|
+
<noscript>
|
|
100
|
+
<div
|
|
101
|
+
style={`width:${width}px;height:${height}px;background:${bg};display:flex;align-items:center;justify-content:center`}
|
|
102
|
+
>
|
|
103
|
+
<p
|
|
104
|
+
style="font-family:sans-serif;font-size:14px;color:#888;text-align:center;padding:1rem;margin:0"
|
|
105
|
+
>
|
|
106
|
+
{description ?? title}
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
</noscript>
|
|
71
110
|
</div>
|
|
72
111
|
|
|
73
112
|
<script>
|
|
74
|
-
|
|
113
|
+
// ── Hydration ────────────────────────────────────────────────────────────
|
|
114
|
+
// The renderer is dynamically imported so it is code-split from the
|
|
115
|
+
// host-page bundle. The browser only downloads it when an element is
|
|
116
|
+
// about to enter the viewport, keeping Time-to-Interactive low.
|
|
75
117
|
|
|
76
|
-
function hydrate(el: HTMLElement): void {
|
|
118
|
+
async function hydrate(el: HTMLElement): Promise<void> {
|
|
77
119
|
const code = el.dataset.markdyCode;
|
|
78
120
|
if (!code) return;
|
|
79
121
|
|
|
@@ -89,23 +131,63 @@ const {
|
|
|
89
131
|
|
|
90
132
|
const autoplay = el.dataset.markdyAutoplay !== "false";
|
|
91
133
|
|
|
92
|
-
//
|
|
134
|
+
// Code-split: renderer-dom is NOT part of the initial page bundle.
|
|
135
|
+
const { createPlayer } = await import("@markdy/renderer-dom");
|
|
136
|
+
|
|
137
|
+
// Clear the SSR placeholder and mount the canvas atomically after the
|
|
138
|
+
// async import resolves, so there is no flash of empty content.
|
|
93
139
|
el.innerHTML = "";
|
|
94
140
|
el.style.overflow = "visible";
|
|
95
141
|
|
|
96
142
|
createPlayer({ container: el, code, assets, autoplay });
|
|
143
|
+
|
|
144
|
+
el.dataset.markdyInit = "done";
|
|
145
|
+
el.removeAttribute("aria-busy");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Task scheduling ──────────────────────────────────────────────────────
|
|
149
|
+
// Yield a macro-task slot before running heavy work so we never block
|
|
150
|
+
// user input or the frame pipeline.
|
|
151
|
+
// 1. Prioritized Task Scheduling API — Chrome 94+, Safari 17+
|
|
152
|
+
// 2. requestIdleCallback — broad support
|
|
153
|
+
// 3. setTimeout(0) — universal fallback
|
|
154
|
+
|
|
155
|
+
type SchedulerLike = {
|
|
156
|
+
postTask: (fn: () => void, opts: { priority: string }) => void;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
function scheduleHydration(fn: () => void): void {
|
|
160
|
+
const sched = (globalThis as unknown as { scheduler?: SchedulerLike })
|
|
161
|
+
.scheduler;
|
|
162
|
+
if (sched?.postTask) {
|
|
163
|
+
sched.postTask(fn, { priority: "background" });
|
|
164
|
+
} else if (typeof requestIdleCallback !== "undefined") {
|
|
165
|
+
requestIdleCallback(fn, { timeout: 2000 });
|
|
166
|
+
} else {
|
|
167
|
+
setTimeout(fn, 0);
|
|
168
|
+
}
|
|
97
169
|
}
|
|
98
170
|
|
|
171
|
+
// ── Intersection observer ────────────────────────────────────────────────
|
|
99
172
|
// Observe every unhydrated .markdy-root element and hydrate it when it
|
|
100
173
|
// enters the viewport (100 px root margin so the scene is ready before
|
|
101
174
|
// the user actually sees it).
|
|
175
|
+
|
|
102
176
|
const observer = new IntersectionObserver(
|
|
103
177
|
(entries) => {
|
|
104
178
|
for (const entry of entries) {
|
|
105
179
|
if (entry.isIntersecting) {
|
|
106
180
|
const el = entry.target as HTMLElement;
|
|
107
181
|
observer.unobserve(el);
|
|
108
|
-
|
|
182
|
+
// Mark immediately to prevent double-scheduling on re-runs.
|
|
183
|
+
el.dataset.markdyInit = "hydrating";
|
|
184
|
+
scheduleHydration(() => {
|
|
185
|
+
hydrate(el).catch(() => {
|
|
186
|
+
// On failure keep the placeholder visible — do not crash the page.
|
|
187
|
+
el.dataset.markdyInit = "error";
|
|
188
|
+
el.removeAttribute("aria-busy");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
109
191
|
}
|
|
110
192
|
}
|
|
111
193
|
},
|
|
@@ -116,7 +198,6 @@ const {
|
|
|
116
198
|
document
|
|
117
199
|
.querySelectorAll<HTMLElement>(".markdy-root:not([data-markdy-init])")
|
|
118
200
|
.forEach((el) => {
|
|
119
|
-
// Flag the element to prevent double-registration across re-runs.
|
|
120
201
|
el.dataset.markdyInit = "pending";
|
|
121
202
|
observer.observe(el);
|
|
122
203
|
});
|