@supersuit/artifacts 0.30.1 → 0.30.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/CHANGELOG.md +14 -0
- package/lib/reader/artifact-reader.js +16 -2
- package/package.json +1 -1
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.30.2
|
|
7
|
+
|
|
8
|
+
**Play from here is kept inside the screen on a host that insets the page.** 0.30.1 clamped the
|
|
9
|
+
bar in page coordinates, but it is placed in its container's, so on the company host (whose card is
|
|
10
|
+
inset from the screen edge) a word near the right edge still pushed the pill past it.
|
|
11
|
+
|
|
12
|
+
- After it renders, the bar measures where it actually landed and moves back inside the screen, once
|
|
13
|
+
per tap.
|
|
14
|
+
- DETECTOR: at iPhone width on a host with an inset card, pause and tap a word at the right edge.
|
|
15
|
+
The pill running off the screen means 0.30.1.
|
|
16
|
+
- REMEDY: update the dependency.
|
|
17
|
+
- Tests: `artifact-reader-controls.test.tsx` (a bar landing 30px right of its placement is moved
|
|
18
|
+
inside a 390px screen). Checked on the live page at iPhone size with Playwright.
|
|
19
|
+
|
|
6
20
|
## 0.30.1
|
|
7
21
|
|
|
8
22
|
**Play from here is one compact pill that stays on screen.** A word tapped near the right edge put
|
|
@@ -8,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
8
8
|
// small bar offers to play from it, so reading and tapping never starts the audio by surprise. While
|
|
9
9
|
// it plays the page follows the voice, until the reader scrolls on their own: then it leaves them
|
|
10
10
|
// where they are and shows a button that brings them back to the voice.
|
|
11
|
-
import { useEffect, useRef, useState } from 'react';
|
|
11
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
12
12
|
import { nextRate, PlayerBar } from './player-bar.js';
|
|
13
13
|
const SKIP = '[data-nospeak], [data-artifact-links], pre, [data-footnotes], sup, img, style, script';
|
|
14
14
|
/** The elements that hold a run of text; a word whose block differs from the previous word's
|
|
@@ -166,6 +166,20 @@ export function ArtifactReader({ src, words, rootId, label, accent, ground, }) {
|
|
|
166
166
|
// The Play from here bar, beside the word a paused reader tapped, in page coordinates so it
|
|
167
167
|
// stays with the word when they scroll.
|
|
168
168
|
const [offer, setOffer] = useState(null);
|
|
169
|
+
const offerRef = useRef(null);
|
|
170
|
+
// The bar is placed in its container's coordinates, which on a host that insets the page are not
|
|
171
|
+
// the screen's; measure where it actually landed and nudge it back inside the screen.
|
|
172
|
+
useLayoutEffect(() => {
|
|
173
|
+
const el = offerRef.current;
|
|
174
|
+
if (!offer || !el || offer.nudged)
|
|
175
|
+
return;
|
|
176
|
+
const vw = document.documentElement.clientWidth || window.innerWidth;
|
|
177
|
+
const r = el.getBoundingClientRect();
|
|
178
|
+
// No layout to measure (a server render, a test DOM): leave the placement as computed.
|
|
179
|
+
const shift = r.width === 0 ? 0 : r.right > vw - 8 ? vw - 8 - r.right : r.left < 8 ? 8 - r.left : 0;
|
|
180
|
+
// Once per tap: measured again, an element with no layout would move forever.
|
|
181
|
+
setOffer({ ...offer, left: offer.left + (Math.abs(shift) >= 1 ? shift : 0), nudged: true });
|
|
182
|
+
}, [offer]);
|
|
169
183
|
useEffect(() => {
|
|
170
184
|
const root = document.getElementById(rootId);
|
|
171
185
|
if (!root)
|
|
@@ -268,7 +282,7 @@ export function ArtifactReader({ src, words, rootId, label, accent, ground, }) {
|
|
|
268
282
|
follow(true);
|
|
269
283
|
}, onSeeking: () => {
|
|
270
284
|
startedRef.current = true;
|
|
271
|
-
}, onPause: () => { playingRef.current = false; setPlaying(false); }, onLoadedMetadata: (e) => setDur(e.target.duration), onRateChange: (e) => setRate(e.target.playbackRate) }), offer ? (_jsxs("div", { "data-nospeak": true, "data-play-offer": true, className: "absolute z-50 flex items-center rounded-full font-sans text-[13px] leading-none shadow-lg", style: { top: offer.top, left: offer.left, width: OFFER_WIDTH, whiteSpace: 'nowrap', background: accent, color: ground }, children: [_jsxs("button", { type: "button", "data-play-from-here": true, onClick: () => playFrom(offer.idx), className: "flex flex-1 items-center gap-1.5 rounded-l-full py-2 pl-3 pr-1 font-medium", children: [_jsx("svg", { "aria-hidden": "true", width: "10", height: "11", viewBox: "0 0 10 11", children: _jsx("path", { d: "M1 1l8 4.5L1 10z", fill: "currentColor" }) }), "Play from here"] }), _jsx("button", { type: "button", "aria-label": "Close", onClick: () => setOffer(null), className: "rounded-r-full py-2 pl-1 pr-3 text-base leading-none opacity-70", children: "\u00D7" })] })) : null, playing && !following ? (_jsx("button", { type: "button", "data-follow-voice": true, "data-nospeak": true, onClick: backToVoice, className: "fixed left-1/2 z-50 -translate-x-1/2 rounded-full px-4 py-2 text-sm font-medium shadow-lg", style: { bottom: 'calc(env(safe-area-inset-bottom, 0px) + 5.5rem)', background: accent, color: ground }, children: "Back to the voice" })) : null, _jsx(PlayerBar, { playing: playing, onToggle: toggle, label: narrator, accent: accent, ground: ground, value: t, max: dur, step: 0.1, onSeek: (v) => {
|
|
285
|
+
}, onPause: () => { playingRef.current = false; setPlaying(false); }, onLoadedMetadata: (e) => setDur(e.target.duration), onRateChange: (e) => setRate(e.target.playbackRate) }), offer ? (_jsxs("div", { ref: offerRef, "data-nospeak": true, "data-play-offer": true, className: "absolute z-50 flex items-center rounded-full font-sans text-[13px] leading-none shadow-lg", style: { top: offer.top, left: offer.left, width: OFFER_WIDTH, whiteSpace: 'nowrap', background: accent, color: ground }, children: [_jsxs("button", { type: "button", "data-play-from-here": true, onClick: () => playFrom(offer.idx), className: "flex flex-1 items-center gap-1.5 rounded-l-full py-2 pl-3 pr-1 font-medium", children: [_jsx("svg", { "aria-hidden": "true", width: "10", height: "11", viewBox: "0 0 10 11", children: _jsx("path", { d: "M1 1l8 4.5L1 10z", fill: "currentColor" }) }), "Play from here"] }), _jsx("button", { type: "button", "aria-label": "Close", onClick: () => setOffer(null), className: "rounded-r-full py-2 pl-1 pr-3 text-base leading-none opacity-70", children: "\u00D7" })] })) : null, playing && !following ? (_jsx("button", { type: "button", "data-follow-voice": true, "data-nospeak": true, onClick: backToVoice, className: "fixed left-1/2 z-50 -translate-x-1/2 rounded-full px-4 py-2 text-sm font-medium shadow-lg", style: { bottom: 'calc(env(safe-area-inset-bottom, 0px) + 5.5rem)', background: accent, color: ground }, children: "Back to the voice" })) : null, _jsx(PlayerBar, { playing: playing, onToggle: toggle, label: narrator, accent: accent, ground: ground, value: t, max: dur, step: 0.1, onSeek: (v) => {
|
|
272
286
|
if (audioRef.current)
|
|
273
287
|
audioRef.current.currentTime = v;
|
|
274
288
|
}, progress: `${fmt(t)} / ${fmt(dur)}`, rate: rate, onRate: () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.2",
|
|
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",
|