@supersuit/artifacts 0.7.1 → 0.9.0
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,32 @@
|
|
|
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.9.0 (2026-09-27)
|
|
7
|
+
|
|
8
|
+
**A host can word its own sign-in door.** `signInNote` on `createArtifactRoutes` replaces the
|
|
9
|
+
door's line on a gated page. The default still names the Freedom account, so every host that
|
|
10
|
+
passes nothing reads exactly as before. A host that signs readers in itself, rather than through
|
|
11
|
+
a Freedom account, needs the door to say so, or readers are told to use an account they do not
|
|
12
|
+
have.
|
|
13
|
+
|
|
14
|
+
- DETECTOR: a host whose `signInOrigin` is not a Freedom account service, and whose door still
|
|
15
|
+
says "your Freedom account".
|
|
16
|
+
- REMEDY: bump and pass `signInNote`.
|
|
17
|
+
- Tests: `gated.test.tsx` pins the default line and a host's own line replacing it.
|
|
18
|
+
|
|
19
|
+
## 0.8.0 (2026-09-27)
|
|
20
|
+
|
|
21
|
+
**Pages play audio clips.** `` renders a captioned player with native
|
|
22
|
+
controls, the way `` already renders a video. An annotated conversation, a voice
|
|
23
|
+
note, or a quote meant to be heard can now sit in the prose it belongs to, instead of on a
|
|
24
|
+
second page or a download link. mp3 was already an accepted asset type; nothing rendered it.
|
|
25
|
+
|
|
26
|
+
- `preload="none"`, never `autoplay`: a page of twenty clips fetches nothing until one is pressed.
|
|
27
|
+
- The alt is the clip's caption, shown above the player and escaped as text.
|
|
28
|
+
- DETECTOR: a page with an `.mp3` in image syntax that renders a broken image.
|
|
29
|
+
- REMEDY: bump; republishing is not needed, the markdown is rendered on request.
|
|
30
|
+
- Tests: `render.test.tsx` pins the audio element, its attributes, the caption and the escaping.
|
|
31
|
+
|
|
6
32
|
## 0.7.1 (2026-09-27)
|
|
7
33
|
|
|
8
34
|
**The narrator reads the subtitle.** `narrationText` built the spoken text from the title, the
|
|
@@ -25,9 +25,13 @@ export declare function Watermark({ email }: {
|
|
|
25
25
|
}): import("react").JSX.Element;
|
|
26
26
|
/** Printing shows this and nothing else, whichever way the print dialog was reached. */
|
|
27
27
|
export declare const NO_PRINT_CSS = "@media print { body * { visibility: hidden !important; } body::after { content: \"This page is confidential and cannot be printed.\"; visibility: visible; position: fixed; top: 40%; left: 0; right: 0; text-align: center; font: 16px sans-serif; color: #000; } }";
|
|
28
|
-
|
|
28
|
+
/** The door's default line, for a host whose readers sign in with a Freedom account. A host
|
|
29
|
+
* that signs readers in some other way passes `signInNote` to say so in its own words. */
|
|
30
|
+
export declare const DEFAULT_SIGN_IN_NOTE = "This page is confidential. Sign in with the Google account your Freedom account uses, or the address it was shared with.";
|
|
31
|
+
export declare function SignInDoor({ brand, href, note }: {
|
|
29
32
|
brand: BrandPack;
|
|
30
33
|
href?: string;
|
|
34
|
+
note?: string;
|
|
31
35
|
}): import("react").JSX.Element;
|
|
32
36
|
export declare function NotAllowedDoor({ brand, reader, signOutUrl }: {
|
|
33
37
|
brand: BrandPack;
|
|
@@ -19,12 +19,15 @@ export function Watermark({ email }) {
|
|
|
19
19
|
}
|
|
20
20
|
/** Printing shows this and nothing else, whichever way the print dialog was reached. */
|
|
21
21
|
export const NO_PRINT_CSS = `@media print { body * { visibility: hidden !important; } body::after { content: "This page is confidential and cannot be printed."; visibility: visible; position: fixed; top: 40%; left: 0; right: 0; text-align: center; font: 16px sans-serif; color: #000; } }`;
|
|
22
|
-
|
|
22
|
+
/** The door's default line, for a host whose readers sign in with a Freedom account. A host
|
|
23
|
+
* that signs readers in some other way passes `signInNote` to say so in its own words. */
|
|
24
|
+
export const DEFAULT_SIGN_IN_NOTE = 'This page is confidential. Sign in with the Google account your Freedom account uses, or the address it was shared with.';
|
|
25
|
+
export function SignInDoor({ brand, href, note = DEFAULT_SIGN_IN_NOTE }) {
|
|
23
26
|
// No href means the host configured no sign-in authority: the door stays, with no way through.
|
|
24
27
|
if (!href) {
|
|
25
28
|
return (_jsx("div", { "data-nospeak": true, className: "mx-auto max-w-sm px-6 pb-24 text-center", children: _jsx("p", { className: "text-sm opacity-70", children: "This page is confidential, and this site cannot sign readers in yet." }) }));
|
|
26
29
|
}
|
|
27
|
-
return (_jsxs("div", { "data-nospeak": true, className: "mx-auto max-w-sm px-6 pb-24 text-center", children: [_jsx("p", { className: "text-sm opacity-70", children:
|
|
30
|
+
return (_jsxs("div", { "data-nospeak": true, className: "mx-auto max-w-sm px-6 pb-24 text-center", children: [_jsx("p", { className: "text-sm opacity-70", children: note }), _jsx("a", { href: href, className: "mt-5 inline-block rounded-lg px-5 py-2 text-sm font-medium", style: { background: brand.accent, color: brand.ground }, children: "Sign in to read" })] }));
|
|
28
31
|
}
|
|
29
32
|
export function NotAllowedDoor({ brand, reader, signOutUrl }) {
|
|
30
33
|
return (_jsxs("div", { "data-nospeak": true, className: "mx-auto max-w-sm px-6 pb-24 text-center text-sm", children: [_jsxs("p", { className: "opacity-80", children: ["You are signed in as ", reader.email, ", and this page has not been shared with that address. If it was sent to a different address of yours, sign out and use that one."] }), _jsx("p", { className: "mt-3 opacity-60", children: "This attempt was recorded." }), _jsx("a", { href: signOutUrl, className: "mt-5 inline-block underline", style: { color: brand.accent }, children: "Sign out" })] }));
|
package/lib/artifacts/render.js
CHANGED
|
@@ -52,6 +52,10 @@ export function isReachableHref(href) {
|
|
|
52
52
|
* is decoded frame by frame on the CPU and stuttered on an iPhone at 24fps (2026-09-27); video
|
|
53
53
|
* is decoded in hardware. */
|
|
54
54
|
const VIDEO = /\.mp4(?:[?#].*)?$/i;
|
|
55
|
+
/** `` is a clip the reader plays: an annotated conversation, a voice note, a
|
|
56
|
+
* quote heard rather than read. Native controls, and `preload="none"` so a page of clips fetches
|
|
57
|
+
* nothing until one is pressed. The alt is its caption, shown above the player. */
|
|
58
|
+
const AUDIO = /\.mp3(?:[?#].*)?$/i;
|
|
55
59
|
const VIDEO_IN_MD = /\]\([^)\s]+\.mp4(?:[?#][^)]*)?\)/i;
|
|
56
60
|
const escapeAttr = (s) => s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
57
61
|
// Written as markup, not JSX: React renders `muted` as a property and leaves it out of server
|
|
@@ -62,6 +66,10 @@ function InlineVideo({ src, label }) {
|
|
|
62
66
|
const html = `<video src="${escapeAttr(src)}" aria-label="${escapeAttr(label)}" data-artifact-video muted loop playsinline preload="metadata" disablepictureinpicture class="block w-full"></video>`;
|
|
63
67
|
return _jsx("span", { className: "my-6 block overflow-hidden rounded-lg border border-[color:var(--a-line)]", dangerouslySetInnerHTML: { __html: html } });
|
|
64
68
|
}
|
|
69
|
+
function InlineAudio({ src, label }) {
|
|
70
|
+
const html = `<audio src="${escapeAttr(src)}" aria-label="${escapeAttr(label)}" data-artifact-audio controls preload="none" class="block w-full"></audio>`;
|
|
71
|
+
return (_jsxs("span", { className: "my-6 block rounded-lg border border-[color:var(--a-line)] bg-[color:var(--a-surface)] px-4 py-3", children: [label ? _jsx("span", { className: "mb-2 block text-sm text-[color:var(--a-strong)]", children: label }) : null, _jsx("span", { className: "block", dangerouslySetInnerHTML: { __html: html } })] }));
|
|
72
|
+
}
|
|
65
73
|
const components = {
|
|
66
74
|
p: ({ children }) => _jsx("p", { className: "my-4 leading-relaxed", children: children }),
|
|
67
75
|
ul: ({ children }) => _jsx("ul", { className: "my-4 list-disc space-y-1 pl-6", children: children }),
|
|
@@ -70,7 +78,7 @@ const components = {
|
|
|
70
78
|
table: ({ children }) => (_jsx("div", { className: "my-6 overflow-x-auto", children: _jsx("table", { className: "w-full border-collapse text-sm", children: children }) })),
|
|
71
79
|
th: ({ children }) => (_jsx("th", { className: "border-b border-[color:var(--a-line-strong)] px-3 py-2 text-left font-semibold text-[color:var(--a-strong)]", children: children })),
|
|
72
80
|
td: ({ children }) => _jsx("td", { className: "border-b border-[color:var(--a-line)] px-3 py-2 align-top", children: children }),
|
|
73
|
-
img: ({ src, alt }) => typeof src === 'string' && VIDEO.test(src) ? (_jsx(InlineVideo, { src: src, label: alt ?? '' })) : (
|
|
81
|
+
img: ({ src, alt }) => typeof src === 'string' && VIDEO.test(src) ? (_jsx(InlineVideo, { src: src, label: alt ?? '' })) : typeof src === 'string' && AUDIO.test(src) ? (_jsx(InlineAudio, { src: src, label: alt ?? '' })) : (
|
|
74
82
|
// eslint-disable-next-line @next/next/no-img-element
|
|
75
83
|
_jsx("img", { src: typeof src === 'string' ? src : undefined, alt: alt ?? '', className: "my-6 w-full rounded-lg border border-[color:var(--a-line)]" })),
|
|
76
84
|
a: ({ href, children }) => !isReachableHref(href) ? (_jsx("span", { children: children })) : (_jsxs("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: "text-[color:var(--a-strong)] underline decoration-[color:var(--a-line-strong)] underline-offset-4 hover:decoration-[color:var(--a-strong)]", children: [children, href && hostOf(href) ? (_jsxs("span", { "data-nospeak": true, className: "ml-1 text-xs text-[color:var(--a-muted)]", children: ["(", hostOf(href), ")"] })) : null] })),
|
|
@@ -32,6 +32,9 @@ export type ArtifactRoutesConfig = {
|
|
|
32
32
|
/** The sign-in authority's origin; readers go to `<signInOrigin>/artifact/sign-in?to=<page>`.
|
|
33
33
|
* No default: without it a gated page shows its door with no way through (fails closed). */
|
|
34
34
|
signInOrigin?: string;
|
|
35
|
+
/** The line on the sign-in door, for a host whose readers do not sign in with a Freedom
|
|
36
|
+
* account. Default names the Freedom account. */
|
|
37
|
+
signInNote?: string;
|
|
35
38
|
/** Who the banner says grants access, e.g. "Example Co". Default the brand name. */
|
|
36
39
|
owner?: string;
|
|
37
40
|
/** The address the state routes' rate limit counts by. Default reads the first hop of
|
package/lib/routes/artifacts.js
CHANGED
|
@@ -151,7 +151,7 @@ export function createArtifactRoutes(config) {
|
|
|
151
151
|
void config.readers.flag({ artifactId: id, reader: d.reader, kind: 'refused' }).catch(() => { });
|
|
152
152
|
return (_jsxs(BrandGround, { pack: pack, mode: a.theme, children: [header, d.why === 'not-allowed'
|
|
153
153
|
? _jsx(NotAllowedDoor, { brand: brand, reader: d.reader, signOutUrl: signOutUrl(id) })
|
|
154
|
-
: _jsx(SignInDoor, { brand: brand, href: signInOrigin ? signInUrl(signInOrigin, here) : undefined })] }));
|
|
154
|
+
: _jsx(SignInDoor, { brand: brand, href: signInOrigin ? signInUrl(signInOrigin, here) : undefined, note: config.signInNote })] }));
|
|
155
155
|
}
|
|
156
156
|
const r = reader;
|
|
157
157
|
// The agreement comes before the body, every reader, once per page. No agreement, no body.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|