@supersuit/artifacts 0.4.1 → 0.4.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.
|
@@ -3,6 +3,9 @@ export type WordTiming = {
|
|
|
3
3
|
s: number;
|
|
4
4
|
e: number;
|
|
5
5
|
};
|
|
6
|
+
/** The word to light at `now`, or -1. Nothing lights until narration has started: the first
|
|
7
|
+
* word begins at 0, so an untouched page would otherwise sit with its first word highlighted. */
|
|
8
|
+
export declare function litWordIndex(words: WordTiming[], now: number, started: boolean): number;
|
|
6
9
|
export declare function ArtifactReader({ src, words, rootId, label, accent, ground, }: {
|
|
7
10
|
src: string;
|
|
8
11
|
words: WordTiming[];
|
|
@@ -70,6 +70,25 @@ function align(spans, words) {
|
|
|
70
70
|
}
|
|
71
71
|
return out;
|
|
72
72
|
}
|
|
73
|
+
/** The word to light at `now`, or -1. Nothing lights until narration has started: the first
|
|
74
|
+
* word begins at 0, so an untouched page would otherwise sit with its first word highlighted. */
|
|
75
|
+
export function litWordIndex(words, now, started) {
|
|
76
|
+
if (!started)
|
|
77
|
+
return -1;
|
|
78
|
+
let lo = 0;
|
|
79
|
+
let hi = words.length - 1;
|
|
80
|
+
let idx = -1;
|
|
81
|
+
while (lo <= hi) {
|
|
82
|
+
const mid = (lo + hi) >> 1;
|
|
83
|
+
if (words[mid].s <= now) {
|
|
84
|
+
idx = mid;
|
|
85
|
+
lo = mid + 1;
|
|
86
|
+
}
|
|
87
|
+
else
|
|
88
|
+
hi = mid - 1;
|
|
89
|
+
}
|
|
90
|
+
return idx >= 0 && now <= words[idx].e + 0.25 ? idx : -1;
|
|
91
|
+
}
|
|
73
92
|
function fmt(t) {
|
|
74
93
|
const m = Math.floor(t / 60);
|
|
75
94
|
const s = Math.floor(t % 60);
|
|
@@ -80,6 +99,8 @@ export function ArtifactReader({ src, words, rootId, label, accent, ground, }) {
|
|
|
80
99
|
const audioRef = useRef(null);
|
|
81
100
|
const mapRef = useRef([]);
|
|
82
101
|
const litRef = useRef(null);
|
|
102
|
+
// Set on the first play or seek; the highlight stays off until then.
|
|
103
|
+
const startedRef = useRef(false);
|
|
83
104
|
const [playing, setPlaying] = useState(false);
|
|
84
105
|
const [t, setT] = useState(0);
|
|
85
106
|
const [dur, setDur] = useState(0);
|
|
@@ -111,20 +132,8 @@ export function ArtifactReader({ src, words, rootId, label, accent, ground, }) {
|
|
|
111
132
|
const tick = () => {
|
|
112
133
|
const now = a.currentTime;
|
|
113
134
|
setT(now);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let hi = words.length - 1;
|
|
117
|
-
let idx = -1;
|
|
118
|
-
while (lo <= hi) {
|
|
119
|
-
const mid = (lo + hi) >> 1;
|
|
120
|
-
if (words[mid].s <= now) {
|
|
121
|
-
idx = mid;
|
|
122
|
-
lo = mid + 1;
|
|
123
|
-
}
|
|
124
|
-
else
|
|
125
|
-
hi = mid - 1;
|
|
126
|
-
}
|
|
127
|
-
const el = idx >= 0 && now <= words[idx].e + 0.25 ? mapRef.current[idx] : null;
|
|
135
|
+
const idx = litWordIndex(words, now, startedRef.current);
|
|
136
|
+
const el = idx >= 0 ? mapRef.current[idx] : null;
|
|
128
137
|
if (el !== litRef.current) {
|
|
129
138
|
litRef.current?.classList.remove('artifact-word-lit');
|
|
130
139
|
el?.classList.add('artifact-word-lit');
|
|
@@ -154,7 +163,12 @@ export function ArtifactReader({ src, words, rootId, label, accent, ground, }) {
|
|
|
154
163
|
.artifact-word { border-radius: 3px; transition: background-color 120ms; }
|
|
155
164
|
.artifact-word:hover { background: var(--a-surface-strong, rgba(255,255,255,0.08)); cursor: pointer; }
|
|
156
165
|
.artifact-word-lit { background: color-mix(in srgb, ${accent} 35%, transparent); color: var(--a-strong, #fff); }
|
|
157
|
-
` }), _jsx("audio", { ref: audioRef, src: src, preload: "metadata", onPlay: () =>
|
|
166
|
+
` }), _jsx("audio", { ref: audioRef, src: src, preload: "metadata", onPlay: () => {
|
|
167
|
+
startedRef.current = true;
|
|
168
|
+
setPlaying(true);
|
|
169
|
+
}, onSeeking: () => {
|
|
170
|
+
startedRef.current = true;
|
|
171
|
+
}, onPause: () => setPlaying(false), onLoadedMetadata: (e) => setDur(e.target.duration), onRateChange: (e) => setRate(e.target.playbackRate) }), _jsx("div", { className: "fixed inset-x-0 bottom-0 z-40 border-t border-[color:var(--a-line)] backdrop-blur", style: { background: `color-mix(in srgb, ${ground} 90%, transparent)` }, children: _jsxs("div", { className: "mx-auto flex max-w-2xl items-center gap-4 px-6 py-3", children: [_jsx("button", { type: "button", onClick: toggle, "aria-label": playing ? 'Pause narration' : 'Play narration', className: "flex h-11 w-11 shrink-0 items-center justify-center rounded-full", style: { background: accent, color: ground }, children: playing ? (_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: [_jsx("rect", { x: "6", y: "5", width: "4", height: "14" }), _jsx("rect", { x: "14", y: "5", width: "4", height: "14" })] })) : (_jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: _jsx("path", { d: "M8 5v14l11-7z" }) })) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-[11px] uppercase tracking-[0.2em]", style: { color: accent }, children: narrator }), _jsx("input", { type: "range", min: 0, max: dur || 0, step: 0.1, value: Math.min(t, dur || 0), onChange: (e) => {
|
|
158
172
|
if (audioRef.current)
|
|
159
173
|
audioRef.current.currentTime = Number(e.target.value);
|
|
160
174
|
}, "aria-label": "Seek", className: "mt-1 w-full", style: { accentColor: accent } })] }), _jsxs("div", { className: "w-16 shrink-0 text-right text-xs tabular-nums text-[color:var(--a-muted)]", children: [fmt(t), " / ", fmt(dur)] }), _jsxs("button", { type: "button", onClick: () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.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",
|