@markdy/astro 0.7.0 → 0.7.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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/Markdy.astro +97 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/astro",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Astro island component for MarkdyScript animations.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@markdy/renderer-dom": "0.7.0"
40
+ "@markdy/renderer-dom": "0.7.2"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "astro": ">=4.0.0"
package/src/Markdy.astro CHANGED
@@ -88,20 +88,20 @@ const {
88
88
  <!--
89
89
  SSR placeholder: keeps layout stable before the island hydrates.
90
90
  Background matches the scene bg prop to avoid a visible flash.
91
- aria-hidden because the parent role="img" aria-label carries the
92
- accessible name — this inner text is decorative.
91
+ A <button> so it is keyboard-accessible and semantically interactive.
93
92
  -->
94
- <div
93
+ <button
95
94
  class="markdy-placeholder"
96
- aria-hidden="true"
97
- style={`width:100%;height:100%;background:${bg};display:flex;align-items:center;justify-content:center`}
95
+ type="button"
96
+ aria-label={`Play ${title}`}
97
+ style={`width:100%;height:100%;background:${bg};display:flex;align-items:center;justify-content:center;cursor:pointer;border:none;padding:0`}
98
98
  >
99
99
  <span
100
- style="font-family:sans-serif;font-size:12px;color:#bbb;letter-spacing:0.05em"
100
+ style="font-family:sans-serif;font-size:12px;color:#bbb;letter-spacing:0.05em;pointer-events:none"
101
101
  >
102
102
  ▶ markdy
103
103
  </span>
104
- </div>
104
+ </button>
105
105
 
106
106
  <!--
107
107
  noscript fallback: rendered for crawlers / users that do not run JS.
@@ -121,13 +121,59 @@ const {
121
121
  </noscript>
122
122
  </div>
123
123
 
124
+ <script is:inline>
125
+ // ── Rocket Loader rescue ──────────────────────────────────────────────────
126
+ // Cloudflare Rocket Loader rewrites <script type="module" src="..."> to a
127
+ // proprietary type (e.g. "abc123-module") and then fails to execute them,
128
+ // which silently kills ALL hydration logic in this component.
129
+ //
130
+ // This inline script is NOT touched by Rocket Loader (no src, no type attr).
131
+ // It detects mangled module scripts by the Rocket Loader type pattern and
132
+ // re-injects them dynamically — dynamically-created scripts also bypass
133
+ // Rocket Loader entirely.
134
+ (function () {
135
+ if (window.__markdyRescue) return;
136
+ window.__markdyRescue = true;
137
+
138
+ var rescued = false;
139
+ function rescueModuleScripts() {
140
+ if (rescued) return;
141
+ rescued = true;
142
+ // Rocket Loader mangles type="module" → "{uuid}-module".
143
+ // Re-inject as real module scripts so the browser executes them.
144
+ document.querySelectorAll('script[type$="-module"][src]').forEach(function (s) {
145
+ var fix = document.createElement('script');
146
+ fix.type = 'module';
147
+ fix.src = s.src;
148
+ document.head.appendChild(fix);
149
+ });
150
+ }
151
+
152
+ // Rescue when user clicks the placeholder (fallback if autoplay failed).
153
+ document.addEventListener('click', function (e) {
154
+ var t = e.target;
155
+ if (t && typeof t.closest === 'function' && t.closest('.markdy-placeholder')) {
156
+ rescueModuleScripts();
157
+ }
158
+ });
159
+
160
+ // Rescue automatically after a short delay for autoplay.
161
+ // Only fires if initAll() never ran (no data-markdy-init on any root).
162
+ setTimeout(function () {
163
+ if (document.querySelector('.markdy-root:not([data-markdy-init])')) {
164
+ rescueModuleScripts();
165
+ }
166
+ }, 1500);
167
+ }());
168
+ </script>
169
+
124
170
  <script>
125
171
  // ── Hydration ────────────────────────────────────────────────────────────
126
172
  // The renderer is dynamically imported so it is code-split from the
127
173
  // host-page bundle. The browser only downloads it when an element is
128
174
  // about to enter the viewport, keeping Time-to-Interactive low.
129
175
 
130
- async function hydrate(el: HTMLElement): Promise<void> {
176
+ async function hydrate(el: HTMLElement, forcePlay = false): Promise<void> {
131
177
  const code = el.dataset.markdyCode;
132
178
  if (!code) return;
133
179
 
@@ -141,7 +187,7 @@ const {
141
187
  // Leave assets empty on parse failure — renderer falls back to DSL paths.
142
188
  }
143
189
 
144
- const autoplay = el.dataset.markdyAutoplay !== "false";
190
+ const autoplay = forcePlay || el.dataset.markdyAutoplay !== "false";
145
191
  const loop = el.dataset.markdyLoop !== "false";
146
192
  const copyright = el.dataset.markdyCopyright !== "false";
147
193
  const progressBar = el.dataset.markdyProgressBar !== "false";
@@ -183,10 +229,10 @@ const {
183
229
  }
184
230
 
185
231
  // ── Intersection observer ────────────────────────────────────────────────
186
- // Observe every unhydrated .markdy-root element. We use threshold: 1.0
187
- // so hydration (and autoplay) only triggers once the element's bottom
188
- // edge is fully inside the viewport the animation is never started
189
- // while partially cut off.
232
+ // threshold: 0.25 trigger as soon as a quarter of the element is visible.
233
+ // We intentionally avoid threshold: 1.0 because it is unreliable: on first
234
+ // navigation (fresh link, no F5) the browser may not fire the callback when
235
+ // the element is very close to — but not fully inside — the viewport.
190
236
 
191
237
  const observer = new IntersectionObserver(
192
238
  (entries) => {
@@ -206,15 +252,40 @@ const {
206
252
  }
207
253
  }
208
254
  },
209
- { threshold: 1.0 },
255
+ { threshold: 0.25 },
210
256
  );
211
257
 
258
+ function doHydrate(el: HTMLElement, forcePlay = false): void {
259
+ observer.unobserve(el);
260
+ el.dataset.markdyInit = "hydrating";
261
+ scheduleHydration(() => {
262
+ hydrate(el, forcePlay).catch(() => {
263
+ el.dataset.markdyInit = "error";
264
+ el.removeAttribute("aria-busy");
265
+ });
266
+ });
267
+ }
268
+
212
269
  function initAll(): void {
213
270
  document
214
271
  .querySelectorAll<HTMLElement>(".markdy-root:not([data-markdy-init])")
215
272
  .forEach((el) => {
216
273
  el.dataset.markdyInit = "pending";
217
- observer.observe(el);
274
+ // Proactively hydrate elements already in the viewport so we never
275
+ // depend on the observer firing for elements visible at page load.
276
+ // This fixes autoplay on first navigation from an external site where
277
+ // IntersectionObserver callbacks may be delayed or skipped.
278
+ const rect = el.getBoundingClientRect();
279
+ const inViewport =
280
+ rect.bottom > 0 &&
281
+ rect.right > 0 &&
282
+ rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
283
+ rect.left < (window.innerWidth || document.documentElement.clientWidth);
284
+ if (inViewport) {
285
+ doHydrate(el);
286
+ } else {
287
+ observer.observe(el);
288
+ }
218
289
  });
219
290
  }
220
291
 
@@ -227,4 +298,15 @@ const {
227
298
 
228
299
  // Re-run after Astro View Transitions / client-side navigation.
229
300
  document.addEventListener("astro:page-load", initAll);
301
+
302
+ // Click on placeholder to play immediately without waiting for full visibility.
303
+ document.addEventListener("click", (e) => {
304
+ const placeholder = (e.target as Element).closest(".markdy-placeholder");
305
+ if (!placeholder) return;
306
+ const root = placeholder.closest<HTMLElement>(".markdy-root");
307
+ if (!root) return;
308
+ const state = root.dataset.markdyInit;
309
+ if (state === "done" || state === "hydrating") return;
310
+ doHydrate(root, true);
311
+ });
230
312
  </script>