@supersuit/artifacts 0.4.6 → 0.4.7

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 CHANGED
@@ -3,6 +3,20 @@
3
3
  `@supersuit/artifacts`. One entry per version, newest first. Each entry says what changed, how a
4
4
  host can tell whether it is affected (DETECTOR), what a host does about it (REMEDY), and the tests.
5
5
 
6
+ ## 0.4.7 (2026-09-27)
7
+
8
+ **A page of videos opens as fast as a page of text.**
9
+
10
+ - 0.4.6 marked every inline video `autoplay`, so all of them downloaded the moment the page opened
11
+ (21 diagrams, 6.8 MB, competing with the text and narration on a phone). The markup now carries
12
+ `preload="metadata"` and `data-artifact-video` instead, and a small client component,
13
+ `VideoAutoplay`, mounted only on pages that embed an `.mp4`, loads and plays each video as it
14
+ comes within a screen of view and pauses it when it leaves.
15
+ - **DETECTOR:** a page with many inline videos is slow to become readable on cellular.
16
+ - **REMEDY:** update the dependency; no host change.
17
+ - **Tests:** the markup no longer autoplays and preloads only metadata; `VideoAutoplay` plays a
18
+ video only once it intersects, raises its preload, and pauses it when it leaves.
19
+
6
20
  ## 0.4.6 (2026-09-27)
7
21
 
8
22
  **Pages play video: `![alt](x.mp4)` is a silent, looping, autoplaying inline video.**
@@ -3,6 +3,7 @@ import ReactMarkdown from 'react-markdown';
3
3
  import remarkGfm from 'remark-gfm';
4
4
  import { hasNotesWidget, headingsOf } from './widgets.js';
5
5
  import { HeadingNotes, NoteToggle, NotesEarlier, NotesProvider } from '../widgets/notes.js';
6
+ import { VideoAutoplay } from '../reader/video-autoplay.js';
6
7
  // Every colour is a theme variable (brand/theme.ts), so a page follows its light, dark or system mode.
7
8
  const GOLD = 'var(--a-accent)';
8
9
  function hostOf(url) {
@@ -49,12 +50,14 @@ export function isReachableHref(href) {
49
50
  * is decoded frame by frame on the CPU and stuttered on an iPhone at 24fps (2026-09-27); video
50
51
  * is decoded in hardware. */
51
52
  const VIDEO = /\.mp4(?:[?#].*)?$/i;
53
+ const VIDEO_IN_MD = /\]\([^)\s]+\.mp4(?:[?#][^)]*)?\)/i;
52
54
  const escapeAttr = (s) => s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
53
55
  // Written as markup, not JSX: React renders `muted` as a property and leaves it out of server
54
- // HTML, and iOS refuses to autoplay a video whose markup is not muted. `playsinline` keeps
55
- // iPhone from going full screen.
56
+ // HTML, and iOS refuses to play a video unprompted whose markup is not muted. `playsinline` keeps
57
+ // iPhone from going full screen. No `autoplay`: VideoAutoplay starts each one as it nears the
58
+ // screen, so a page of videos does not download them all on open.
56
59
  function InlineVideo({ src, label }) {
57
- const html = `<video src="${escapeAttr(src)}" aria-label="${escapeAttr(label)}" autoplay muted loop playsinline preload="auto" disablepictureinpicture class="block w-full"></video>`;
60
+ const html = `<video src="${escapeAttr(src)}" aria-label="${escapeAttr(label)}" data-artifact-video muted loop playsinline preload="metadata" disablepictureinpicture class="block w-full"></video>`;
58
61
  return _jsx("span", { className: "my-6 block overflow-hidden rounded-lg border border-[color:var(--a-line)]", dangerouslySetInnerHTML: { __html: html } });
59
62
  }
60
63
  const components = {
@@ -148,5 +151,5 @@ export function ArtifactMarkdown({ markdown, notes }) {
148
151
  const body = (_jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: on ? notesComponents(headings) : pageComponents(headings), children: markdown }));
149
152
  return (
150
153
  // break-words: a bare URL in prose is one unbreakable word and pushed phones 7px sideways (2026-09-26).
151
- _jsx("div", { className: "break-words text-[17px] text-[color:var(--a-body)]", children: on ? (_jsx(NotesProvider, { artifactId: notes.artifactId, headings: headings.map(({ slug, text }) => ({ slug, text })), accent: notes.accent, children: body })) : body }));
154
+ _jsxs("div", { className: "break-words text-[17px] text-[color:var(--a-body)]", children: [on ? (_jsx(NotesProvider, { artifactId: notes.artifactId, headings: headings.map(({ slug, text }) => ({ slug, text })), accent: notes.accent, children: body })) : body, VIDEO_IN_MD.test(markdown) ? _jsx(VideoAutoplay, {}) : null] }));
152
155
  }
@@ -0,0 +1 @@
1
+ export declare function VideoAutoplay(): null;
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ // Plays each inline video only while it is near the screen. With `autoplay` in the markup every
3
+ // video on the page downloads the moment it opens (the Freedom whitepaper: 21 diagrams, 6.8 MB,
4
+ // competing with the text and the narration on a phone). Here a video loads and plays as it
5
+ // comes within a screen of view and pauses when it leaves, so opening the page costs the text.
6
+ import { useEffect } from 'react';
7
+ export function VideoAutoplay() {
8
+ useEffect(() => {
9
+ const videos = Array.from(document.querySelectorAll('video[data-artifact-video]'));
10
+ if (!videos.length || typeof IntersectionObserver === 'undefined') {
11
+ for (const v of videos)
12
+ void v.play?.().catch(() => { });
13
+ return;
14
+ }
15
+ const io = new IntersectionObserver((entries) => {
16
+ for (const e of entries) {
17
+ const v = e.target;
18
+ if (e.isIntersecting) {
19
+ v.preload = 'auto';
20
+ void v.play().catch(() => { });
21
+ }
22
+ else
23
+ v.pause();
24
+ }
25
+ }, { rootMargin: '100% 0px' });
26
+ for (const v of videos)
27
+ io.observe(v);
28
+ return () => io.disconnect();
29
+ }, []);
30
+ return null;
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersuit/artifacts",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Publish markdown as branded, read-aloud pages from a Next.js app: the artifacts store, the route factory, the read-along reader, brand packs, password and confidential pages, and share cards.",
5
5
  "license": "MIT",
6
6
  "type": "module",