@supersuit/artifacts 0.33.0 → 0.33.1
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 +28 -0
- package/README.md +1 -1
- package/lib/brand/pack.d.ts +3 -2
- package/lib/brand/pack.js +0 -1
- package/lib/reader/artifact-reader.d.ts +3 -3
- package/lib/reader/artifact-reader.js +2 -3
- package/lib/reader/player-bar.d.ts +5 -4
- package/lib/reader/player-bar.js +11 -4
- package/lib/routes/artifacts.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
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.33.1
|
|
7
|
+
|
|
8
|
+
**The player bar: the speed button no longer reflows the bar, 2x is a speed, and the narrator
|
|
9
|
+
line is gone** (Gary, 2026-10-12: "The bottom bar speed dial shouldn't cause reflow when clicked.
|
|
10
|
+
2x should be an option. And read by freedom should be removed."). Ships after 0.33.0, which was
|
|
11
|
+
already tagged when these were asked for.
|
|
12
|
+
|
|
13
|
+
- **No reflow.** The speed button sized itself to its label, so "1x" (30px) to "1.25x" (48px) made
|
|
14
|
+
it grow and the seek line beside it shrink by 18px on every tap. It is now a fixed 56px
|
|
15
|
+
(`w-14`), centred, never wrapping, with tabular figures, and a tap inserts nothing into the bar.
|
|
16
|
+
Measured in Chromium at 390 and 1280 wide with the bar rendered at every speed: 0.32.0's button
|
|
17
|
+
was 29.86, 47.56, 40.17 and 31.55px wide and the seek line moved with it; 0.33.1's bar, button
|
|
18
|
+
and seek line have identical boxes at every speed.
|
|
19
|
+
- **2x.** The button steps 1x, 1.25x, 1.5x, 2x and back to 1x (`RATES`, exported beside
|
|
20
|
+
`nextRate`).
|
|
21
|
+
- **No "Read aloud by Freedom".** The bar shows no narrator line. `PlayerBar`'s and
|
|
22
|
+
`ArtifactReader`'s `label` and `BrandPack.narratorLabel` are optional and ignored, so a host or
|
|
23
|
+
pack that still sets them compiles and shows nothing; `freedomDefault` no longer sets one. The
|
|
24
|
+
narration itself is unchanged.
|
|
25
|
+
- DETECTOR: on a narrated page, a line above the seek bar reading "Read aloud by ...", or the
|
|
26
|
+
speed button changing width as you tap it, or no 2x: 0.33.0 or earlier.
|
|
27
|
+
- REMEDY: update the dependency. A host may delete its own `narratorLabel` from its packs (it is
|
|
28
|
+
ignored either way). Nothing else.
|
|
29
|
+
- Tests: `reader/player-bar.test.tsx` (no narrator text even when a label is passed; the 1, 1.25,
|
|
30
|
+
1.5, 2 cycle; GUARD: one class list and one node count across every speed, with `w-14`,
|
|
31
|
+
`whitespace-nowrap`, `tabular-nums` and no horizontal padding). jsdom has no layout, so the
|
|
32
|
+
measured proof is the Chromium check above, run against the built `lib` with the host's Tailwind.
|
|
33
|
+
|
|
6
34
|
## 0.33.0
|
|
7
35
|
|
|
8
36
|
**A comment thread can be resolved** (Gary, 2026-10-12: "Are you going to resolve the comments at
|
package/README.md
CHANGED
|
@@ -791,7 +791,7 @@ refuses the query and the error names a link that creates the index. A host with
|
|
|
791
791
|
## Brand packs
|
|
792
792
|
|
|
793
793
|
A `BrandPack` is data plus at most two components: colours, type, the kicker line above a title,
|
|
794
|
-
|
|
794
|
+
an optional full-page `Wrapper` and `Mark`, and an optional `share` block
|
|
795
795
|
(a font loader and a backdrop) that draws the unfurl card. `freedomDefault` is the default look.
|
|
796
796
|
Keep a pack carrying your own trademark in your own app, not in a pull request here.
|
|
797
797
|
|
package/lib/brand/pack.d.ts
CHANGED
|
@@ -38,8 +38,9 @@ export type BrandPack = {
|
|
|
38
38
|
};
|
|
39
39
|
/** The small uppercase line above a title, e.g. "From Sam Rivera". */
|
|
40
40
|
kicker: string;
|
|
41
|
-
/**
|
|
42
|
-
|
|
41
|
+
/** Unused since 0.33.1: the player bar names no narrator (it read "Read aloud by Freedom").
|
|
42
|
+
* Optional so a pack that still sets it compiles; nothing reads it. */
|
|
43
|
+
narratorLabel?: (voice?: string) => string;
|
|
43
44
|
/** Optional full-page backdrop. Receives the page as children. */
|
|
44
45
|
Wrapper?: ComponentType<{
|
|
45
46
|
children: ReactNode;
|
package/lib/brand/pack.js
CHANGED
|
@@ -11,7 +11,6 @@ export const freedomDefault = {
|
|
|
11
11
|
body: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
12
12
|
},
|
|
13
13
|
kicker: 'A page from Freedom',
|
|
14
|
-
narratorLabel: () => 'Read aloud by Freedom',
|
|
15
14
|
// Pages unfurl as their title on the ground colour. A pack with a backdrop of its own
|
|
16
15
|
// overrides this whole field.
|
|
17
16
|
share: { font: newsreader },
|
|
@@ -18,12 +18,12 @@ export declare function litWordIndex(words: WordTiming[], now: number, started:
|
|
|
18
18
|
/** Whether a key press is the reader scrolling the page themselves. Space scrolls only outside a
|
|
19
19
|
* field that takes text. */
|
|
20
20
|
export declare function isUserScrollKey(key: string, target: EventTarget | null): boolean;
|
|
21
|
-
export declare function ArtifactReader({ src, words, rootId,
|
|
21
|
+
export declare function ArtifactReader({ src, words, rootId, accent, ground, }: {
|
|
22
22
|
src: string;
|
|
23
23
|
words: WordTiming[];
|
|
24
24
|
rootId: string;
|
|
25
|
-
/**
|
|
26
|
-
label
|
|
25
|
+
/** Ignored since 0.33.1: the player bar names no narrator. Kept so an older caller compiles. */
|
|
26
|
+
label?: string;
|
|
27
27
|
accent: string;
|
|
28
28
|
ground: string;
|
|
29
29
|
}): import("react").JSX.Element;
|
|
@@ -146,8 +146,7 @@ export function isUserScrollKey(key, target) {
|
|
|
146
146
|
}
|
|
147
147
|
/** The Play from here bar's width, so it can be kept inside the screen. */
|
|
148
148
|
const OFFER_WIDTH = 168;
|
|
149
|
-
export function ArtifactReader({ src, words, rootId,
|
|
150
|
-
const narrator = label;
|
|
149
|
+
export function ArtifactReader({ src, words, rootId, accent, ground, }) {
|
|
151
150
|
const audioRef = useRef(null);
|
|
152
151
|
const mapRef = useRef([]);
|
|
153
152
|
const litRef = useRef(null);
|
|
@@ -301,7 +300,7 @@ export function ArtifactReader({ src, words, rootId, label, accent, ground, }) {
|
|
|
301
300
|
follow(true);
|
|
302
301
|
}, onSeeking: () => {
|
|
303
302
|
startedRef.current = true;
|
|
304
|
-
}, 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,
|
|
303
|
+
}, 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, accent: accent, ground: ground, value: t, max: dur, step: 0.1, onSeek: (v) => {
|
|
305
304
|
if (audioRef.current)
|
|
306
305
|
audioRef.current.currentTime = v;
|
|
307
306
|
}, progress: `${fmt(t)} / ${fmt(dur)}`, rate: rate, onRate: () => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export declare function PlayerBar({ playing, onToggle,
|
|
1
|
+
export declare function PlayerBar({ playing, onToggle, accent, ground, value, max, step, onSeek, progress, rate, onRate, }: {
|
|
2
2
|
playing: boolean;
|
|
3
3
|
onToggle: () => void;
|
|
4
|
-
/**
|
|
5
|
-
label
|
|
4
|
+
/** Ignored since 0.33.1: the bar names no narrator. Kept so an older caller still compiles. */
|
|
5
|
+
label?: string;
|
|
6
6
|
accent: string;
|
|
7
7
|
ground: string;
|
|
8
8
|
value: number;
|
|
@@ -14,5 +14,6 @@ export declare function PlayerBar({ playing, onToggle, label, accent, ground, va
|
|
|
14
14
|
rate: number;
|
|
15
15
|
onRate: () => void;
|
|
16
16
|
}): import("react").JSX.Element;
|
|
17
|
-
/** The speeds the speed button steps through, in order, wrapping back to
|
|
17
|
+
/** The speeds the speed button steps through, in order, wrapping back to the first. */
|
|
18
|
+
export declare const RATES: readonly [1, 1.25, 1.5, 2];
|
|
18
19
|
export declare function nextRate(rate: number): number;
|
package/lib/reader/player-bar.js
CHANGED
|
@@ -3,14 +3,21 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// The bar at the bottom of a page that can be listened to, shared by the recorded-narration
|
|
4
4
|
// reader and the browser read-aloud, so a page looks the same whichever one is speaking. It is
|
|
5
5
|
// pure chrome: each reader owns its engine and hands the bar what to show and what to call.
|
|
6
|
-
|
|
6
|
+
//
|
|
7
|
+
// No narrator line (0.33.1, Gary: "read by freedom should be removed"): the bar is the play
|
|
8
|
+
// button, the seek line, the progress and the speed. The speed button has a FIXED width wide enough
|
|
9
|
+
// for its longest label ("1.25x"), so stepping 1x -> 1.25x -> 2x never resizes it and never shifts
|
|
10
|
+
// the seek line beside it (Gary: "the bottom bar speed dial shouldn't cause reflow when clicked").
|
|
11
|
+
export function PlayerBar({ playing, onToggle, accent, ground, value, max, step, onSeek, progress, rate, onRate, }) {
|
|
7
12
|
return (_jsxs(_Fragment, { children: [_jsx("style", { children: `
|
|
8
13
|
.artifact-word { border-radius: 3px; transition: background-color 120ms; }
|
|
9
14
|
.artifact-word:hover { background: var(--a-surface-strong, rgba(255,255,255,0.08)); cursor: pointer; }
|
|
10
15
|
.artifact-word-lit { background: color-mix(in srgb, ${accent} 35%, transparent); color: var(--a-strong, #fff); }
|
|
11
|
-
` }), _jsx("div", { "data-artifact-player": true, 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: onToggle, "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" }) })) }),
|
|
16
|
+
` }), _jsx("div", { "data-artifact-player": true, 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: onToggle, "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" }) })) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsx("input", { type: "range", min: 0, max: max || 0, step: step, value: Math.min(value, max || 0), onChange: (e) => onSeek(Number(e.target.value)), "aria-label": "Seek", className: "block w-full", style: { accentColor: accent } }) }), _jsx("div", { className: "w-16 shrink-0 text-right text-xs tabular-nums text-[color:var(--a-muted)]", children: progress }), _jsxs("button", { type: "button", onClick: onRate, "data-artifact-rate": true, className: "w-14 shrink-0 whitespace-nowrap rounded border border-[color:var(--a-line-strong)] py-1 text-center text-xs tabular-nums text-[color:var(--a-body)]", "aria-label": `Playback speed, ${rate}x`, children: [rate, "x"] })] }) }), _jsx("div", { className: "h-20", "aria-hidden": true })] }));
|
|
12
17
|
}
|
|
13
|
-
/** The speeds the speed button steps through, in order, wrapping back to
|
|
18
|
+
/** The speeds the speed button steps through, in order, wrapping back to the first. */
|
|
19
|
+
export const RATES = [1, 1.25, 1.5, 2];
|
|
14
20
|
export function nextRate(rate) {
|
|
15
|
-
|
|
21
|
+
const i = RATES.findIndex((r) => r > rate + 1e-9);
|
|
22
|
+
return i === -1 ? RATES[0] : RATES[i];
|
|
16
23
|
}
|
package/lib/routes/artifacts.js
CHANGED
|
@@ -410,7 +410,7 @@ export function createArtifactRoutes(config) {
|
|
|
410
410
|
return (_jsx(Framed, { pack: pack, toc: toc.length ? _jsx(TocInline, { items: toc }) : null, cover: _jsxs(_Fragment, { children: [header, a.cover ? (_jsx("div", { className: "mx-auto max-w-2xl px-6 pb-8", children: _jsx("img", { src: a.cover, alt: "", className: "w-full rounded-xl border border-[color:var(--a-line)]" }) })) : null] }), children: _jsx(ArtifactMarkdown, { markdown: a.markdown, definitions: a.definitions, notes: config.state && a.state ? { artifactId: a.id, accent: brand.accent } : undefined,
|
|
411
411
|
// Ticks are kept in the reader's browser on any host; Send needs one that keeps answers.
|
|
412
412
|
checklist: { artifactId: a.id, send: !!config.state && !!a.state && Object.hasOwn(a.state.slots, CHECKLIST_SLOT), owner, accent: brand.accent }, Section: pack.Section, figure: pack.figure, simulator: config.simulatorEmbed ? { embed: config.simulatorEmbed } : undefined, messages: config.messagesEmbed ? { embed: config.messagesEmbed } : undefined }) }));
|
|
413
|
-
})() }), _jsx(CommentLayer, { artifactId: a.id, rootId: "artifact-narration-root", ownerName: owner, comments: mode, canShare: mode !== 'off' && (reader !== null || effectiveWriters(a.state, a.access, COMMENTS_SLOT) === 'anyone'), signedIn: reader !== null, isOwner: isAdmin(reader), version: a.version ?? (a.versions?.length ?? 0) + 1, accent: brand.accent, ground: brand.ground, signIn: signInOrigin ? signInUrl(signInOrigin, pageUrl(a.id)) : null }), a.narration && words.length > 0 ? (_jsx(ArtifactReader, { src: a.narration, words: words, rootId: "artifact-narration-root",
|
|
413
|
+
})() }), _jsx(CommentLayer, { artifactId: a.id, rootId: "artifact-narration-root", ownerName: owner, comments: mode, canShare: mode !== 'off' && (reader !== null || effectiveWriters(a.state, a.access, COMMENTS_SLOT) === 'anyone'), signedIn: reader !== null, isOwner: isAdmin(reader), version: a.version ?? (a.versions?.length ?? 0) + 1, accent: brand.accent, ground: brand.ground, signIn: signInOrigin ? signInUrl(signInOrigin, pageUrl(a.id)) : null }), a.narration && words.length > 0 ? (_jsx(ArtifactReader, { src: a.narration, words: words, rootId: "artifact-narration-root", accent: brand.accent, ground: brand.ground })) : null /* No recorded narration: no player. Never the browser's own voice (the operator,
|
|
414
414
|
2026-09-29: "NEVER PUT DEFAULT SAFARI VOICE DICTATION"); the audio is Kokoro or ElevenLabs,
|
|
415
415
|
recorded at publish, or there is none. */] }));
|
|
416
416
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.1",
|
|
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",
|