@supersuit/artifacts 0.46.1 → 0.47.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 +21 -0
- package/README.md +37 -0
- package/lib/artifacts/index.d.ts +1 -1
- package/lib/artifacts/render.d.ts +8 -1
- package/lib/artifacts/render.js +9 -7
- package/lib/artifacts/simulator.d.ts +4 -2
- package/lib/artifacts/simulator.js +25 -4
- package/lib/artifacts/widgets.js +3 -1
- package/lib/routes/artifacts.d.ts +7 -0
- package/lib/routes/artifacts.js +8 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
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.47.0
|
|
7
|
+
|
|
8
|
+
**Figures and Helm scenes play inline, as two new embed fences.** A ```` ```figure ```` block
|
|
9
|
+
(`figure: <id>`, `caption:`, optional `height:`, default 560px) plays an interactive figure, and a
|
|
10
|
+
```` ```helm ```` block (`scene: <id>`, `caption:`, optional `height:`, default 760px, optional
|
|
11
|
+
`autoplay: true`) plays a scene on a Helm phone, both in the same fitted, lazy frame and under the
|
|
12
|
+
same caption as `simulator` and `messages`. They are rows in the one table of embed kinds, which
|
|
13
|
+
now also carries per-kind options (helm's `autoplay`, passed to the embed page only when true), so
|
|
14
|
+
a fifth kind is one line. Publish refuses a block with no id or no caption, naming the line. Embed
|
|
15
|
+
blocks, the old two included, no longer take a comment anchor: the player and its caption were
|
|
16
|
+
never selectable text. The host names the pages with two new config fields, `figureEmbed` and
|
|
17
|
+
`helmEmbed`; there is no default. `ArtifactMarkdown` takes every kind's page as `embeds`
|
|
18
|
+
(`simulator` and `messages` still work).
|
|
19
|
+
|
|
20
|
+
- DETECTOR: a page carrying a ```` ```figure ```` or ```` ```helm ```` block. Before, it drew as a code block.
|
|
21
|
+
- REMEDY: update the dependency, and set `figureEmbed` / `helmEmbed` in `createArtifactRoutes` to
|
|
22
|
+
the host's figure and Helm embed pages. Without them the blocks show their caption only.
|
|
23
|
+
- Tests: simulator.test.ts ("the figure block", "the helm block", "every embed kind"),
|
|
24
|
+
render.test.tsx ("the figure and helm blocks", "an embed block is not read aloud"),
|
|
25
|
+
widgets.test.ts ("scanWidgets and the figure and helm blocks", "blocksOf and embed blocks").
|
|
26
|
+
|
|
6
27
|
## 0.46.1
|
|
7
28
|
|
|
8
29
|
0.46.0 republished: its tag's publish run refused on two settings-menu tests that still listed the
|
package/README.md
CHANGED
|
@@ -676,6 +676,43 @@ Paste it under the audio line, then correct any word (a name, a phrase in anothe
|
|
|
676
676
|
and keep its two times. `transcriptFenceFromWords` and `wordsFromWhisperJson` (from
|
|
677
677
|
`@supersuit/artifacts/artifacts`) do the same from code.
|
|
678
678
|
|
|
679
|
+
## Embedded players (`simulator`, `messages`, `figure`, `helm`)
|
|
680
|
+
|
|
681
|
+
Four fences play a page the HOST serves, inline, in a frame under a caption:
|
|
682
|
+
|
|
683
|
+
| Fence | Names | Default height | Plays |
|
|
684
|
+
| --- | --- | --- | --- |
|
|
685
|
+
| `simulator` | `scenario: <id>` (required) | 720px | a Supersuit Session demo |
|
|
686
|
+
| `messages` | `chat: <id>` (optional) | 760px | a Messages-app phone, open on one thread or the list |
|
|
687
|
+
| `figure` | `figure: <id>` (required) | 560px | an interactive figure |
|
|
688
|
+
| `helm` | `scene: <id>` (required), `autoplay: true` (optional) | 760px | a scene on a Helm phone |
|
|
689
|
+
|
|
690
|
+
````markdown
|
|
691
|
+
```figure
|
|
692
|
+
figure: doug-week
|
|
693
|
+
caption: A week of Doug, before and after.
|
|
694
|
+
height: 560px # optional
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
```helm
|
|
698
|
+
scene: doug-walk
|
|
699
|
+
caption: A year later, on the reservoir loop.
|
|
700
|
+
autoplay: true # optional, true or false
|
|
701
|
+
```
|
|
702
|
+
````
|
|
703
|
+
|
|
704
|
+
- **Every block needs a `caption`**, the line a reader sees under the player. Publish refuses a
|
|
705
|
+
block with no caption, no required id, an id that is not lowercase letters, digits and dashes, a
|
|
706
|
+
height that is not pixels, or an unknown key, naming the line in the file.
|
|
707
|
+
- **The host names each player's embed page** (`simulatorEmbed`, `messagesEmbed`, `figureEmbed`,
|
|
708
|
+
`helmEmbed` in `createArtifactRoutes`, each an https URL; or `embeds` on `ArtifactMarkdown`). The
|
|
709
|
+
frame loads it with the id (`?figure=doug-week`, `?scene=doug-walk`) and `fit=content`. There is
|
|
710
|
+
no default: a host that names no page for a kind shows that block's caption alone, never its
|
|
711
|
+
source as code, which is also what print and a page with the player blocked show.
|
|
712
|
+
- **The frame fits its player.** It opens at `height:` and takes the height the embed page posts
|
|
713
|
+
as `{ type: 'embed:fit', height }` from its own origin.
|
|
714
|
+
- **Narration skips it**, caption included, and it carries no comment anchor.
|
|
715
|
+
|
|
679
716
|
## Comments and personal notes
|
|
680
717
|
|
|
681
718
|
Every page takes comments pinned to any part of it. A reader selects text (a **Comment** chip
|
package/lib/artifacts/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { parseArtifactSource, ARTIFACT_KINDS, PAGE_HISTORY, type ArtifactMeta, type ArtifactKind, type PageHistory } from './front-matter.js';
|
|
2
|
-
export { ArtifactMarkdown, artifactUrlTransform } from './render.js';
|
|
2
|
+
export { ArtifactMarkdown, artifactUrlTransform, type EmbedCtx } from './render.js';
|
|
3
3
|
export { narrationText, narrationTextFromMarkdown, normalizeWord } from './narration.js';
|
|
4
4
|
export { createArtifactStore, newArtifactId, cleanNote, kindOf, listOfKind, MAX_CHANGE_NOTE_CHARS, type ArtifactListItem, type ArtifactStore, type ArtifactRecord, type SaveResult, type SaveInput, type VersionEntry, type VersionRecord } from './store.js';
|
|
5
5
|
export { isPublishAuthed, isPublisherAuthed, type PublishAuth } from './auth.js';
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { type ComponentType } from 'react';
|
|
2
2
|
import type { FigureProps, SectionOutline, SectionProps } from '../brand/pack.js';
|
|
3
3
|
import { type Heading } from './widgets.js';
|
|
4
|
+
import { type EmbedKind } from './simulator.js';
|
|
4
5
|
import { type Definition } from './definitions.js';
|
|
5
6
|
export declare function isReachableHref(href: unknown): href is string;
|
|
6
7
|
export declare function artifactUrlTransform(url: string): string;
|
|
7
8
|
type SimulatorCtx = {
|
|
8
9
|
embed: string;
|
|
9
10
|
};
|
|
11
|
+
/** The host's embed page for each embed kind it serves (ArtifactMarkdown `embeds`). */
|
|
12
|
+
export type EmbedCtx = Partial<Record<EmbedKind, SimulatorCtx>>;
|
|
10
13
|
/** A pack's figure resolver (BrandPack.figure). */
|
|
11
14
|
export type FigureFor = (src: string, alt: string) => ComponentType<FigureProps> | undefined;
|
|
12
15
|
/** The page's top-level sections: every heading among the body's top-level blocks at the
|
|
@@ -14,7 +17,7 @@ export type FigureFor = (src: string, alt: string) => ComponentType<FigureProps>
|
|
|
14
17
|
export declare function sectionOutline(markdown: string, headings?: Heading[]): SectionOutline[];
|
|
15
18
|
/** `notes` turns on the notes widget, for a host that keeps answers; it draws only when the
|
|
16
19
|
* page carries a ```notes block. */
|
|
17
|
-
export declare function ArtifactMarkdown({ markdown, notes, checklist, definitions, Section, figure, simulator, messages, }: {
|
|
20
|
+
export declare function ArtifactMarkdown({ markdown, notes, checklist, definitions, Section, figure, simulator, messages, embeds: hosted, }: {
|
|
18
21
|
markdown: string;
|
|
19
22
|
/** The page a ```checklist keeps its ticks under, and whether its Send can reach the owner
|
|
20
23
|
* (a host that keeps answers, on a page that declares the slot). On a page whose checklists
|
|
@@ -40,5 +43,9 @@ export declare function ArtifactMarkdown({ markdown, notes, checklist, definitio
|
|
|
40
43
|
simulator?: SimulatorCtx;
|
|
41
44
|
/** The host's Messages phone, for ```messages blocks. Without it a block shows its caption only. */
|
|
42
45
|
messages?: SimulatorCtx;
|
|
46
|
+
/** The host's embed page for every embed kind it serves (```simulator, ```messages, ```figure,
|
|
47
|
+
* ```helm). A kind with none shows its caption only. `simulator` and `messages` above still work
|
|
48
|
+
* and win over this. */
|
|
49
|
+
embeds?: EmbedCtx;
|
|
43
50
|
}): import("react").JSX.Element;
|
|
44
51
|
export {};
|
package/lib/artifacts/render.js
CHANGED
|
@@ -17,7 +17,7 @@ import { unified } from 'unified';
|
|
|
17
17
|
import { blocksOf, checklistSyncs, checklistsOf, hasNotesWidget, headingsOf } from './widgets.js';
|
|
18
18
|
import { HeadingNotes, NoteToggle, NotesEarlier, NotesProvider } from '../widgets/notes.js';
|
|
19
19
|
import { Checklist } from '../widgets/checklist.js';
|
|
20
|
-
import { embedSrc, parseEmbedFence } from './simulator.js';
|
|
20
|
+
import { embedSrc, isEmbedKind, parseEmbedFence, EMBED_KINDS } from './simulator.js';
|
|
21
21
|
import { EmbedFrame } from './embed-frame.js';
|
|
22
22
|
import { VideoAutoplay } from '../reader/video-autoplay.js';
|
|
23
23
|
import { CodeCopy } from '../reader/code-copy.js';
|
|
@@ -114,6 +114,8 @@ function InlineAudio({ src, label }) {
|
|
|
114
114
|
const html = `<audio src="${escapeAttr(src)}" aria-label="${escapeAttr(label)}" data-artifact-audio data-artifact-media="${escapeAttr(mediaName(src))}" controls preload="none" class="block w-full"></audio>`;
|
|
115
115
|
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 } })] }));
|
|
116
116
|
}
|
|
117
|
+
/** Fences that draw something other than code, so `pre` must not wrap them. */
|
|
118
|
+
const OWN_BLOCK = new RegExp(`language-(links|notes|checklist|transcript|${EMBED_KINDS.join('|')})\\b`);
|
|
117
119
|
// A session demo on a wide screen is wider than the reading column, so the device it draws (a
|
|
118
120
|
// laptop, or a phone beside what was made) has room: up to 64rem, centred on the column, and clear
|
|
119
121
|
// of the contents rail fixed in the left margin from 1280px (w-56 at left-6, w-64 at left-12).
|
|
@@ -125,14 +127,14 @@ const WIDE_EMBED_CSS = `@media (min-width:1024px){${WIDE}{width:max(100%,min(64r
|
|
|
125
127
|
`[data-broadcast] ${WIDE}{width:auto;margin-left:0;transform:none}[data-broadcast] ${WIDE} figcaption{text-align:inherit}}` +
|
|
126
128
|
`@media (min-width:1280px){${WIDE}{width:max(100%,min(64rem,calc(100vw - 36rem)))}}` +
|
|
127
129
|
`@media (min-width:1536px){${WIDE}{width:max(100%,min(64rem,calc(100vw - 40rem)))}}`;
|
|
128
|
-
/** An embed block (```simulator, ```messages): the player in a frame when the host names one, its
|
|
130
|
+
/** An embed block (```simulator, ```messages, ```figure, ```helm): the player in a frame when the host names one, its
|
|
129
131
|
* caption alone when it does not, and nothing when the block does not parse (a page published
|
|
130
132
|
* before validation existed). */
|
|
131
133
|
function EmbedFence({ kind, source, ctx }) {
|
|
132
134
|
const r = parseEmbedFence(kind, source.replace(/\n$/, ''), 0);
|
|
133
135
|
if (!r.ok)
|
|
134
136
|
return null;
|
|
135
|
-
const src = ctx ? embedSrc(kind, ctx.embed, r.block.value) : null;
|
|
137
|
+
const src = ctx ? embedSrc(kind, ctx.embed, r.block.value, r.block.options) : null;
|
|
136
138
|
const wide = kind === 'simulator' && src != null;
|
|
137
139
|
return (
|
|
138
140
|
// data-nospeak: the narration skips every fenced block (narration.ts), so the caption must not
|
|
@@ -219,7 +221,7 @@ function baseComponents(block, figure, checklists, embeds, voiceNotes) {
|
|
|
219
221
|
// A transcript not under an audio line draws nothing (publish refuses one; an old page may hold it).
|
|
220
222
|
if (lang === TRANSCRIPT_LANG)
|
|
221
223
|
return null;
|
|
222
|
-
if (lang
|
|
224
|
+
if (isEmbedKind(lang))
|
|
223
225
|
return _jsx(EmbedFence, { kind: lang, source: textOf(children), ctx: embeds?.[lang] });
|
|
224
226
|
if (!className) {
|
|
225
227
|
return _jsx("code", { className: "rounded bg-[color:var(--a-surface-strong)] px-1.5 py-0.5 text-[0.9em] text-[color:var(--a-strong)]", children: children });
|
|
@@ -230,7 +232,7 @@ function baseComponents(block, figure, checklists, embeds, voiceNotes) {
|
|
|
230
232
|
// A links fence renders its own block; do not wrap it in <pre>.
|
|
231
233
|
const inner = Array.isArray(children) ? children[0] : children;
|
|
232
234
|
const cls = inner?.props?.className ?? '';
|
|
233
|
-
if (
|
|
235
|
+
if (OWN_BLOCK.test(cls))
|
|
234
236
|
return _jsx(_Fragment, { children: children });
|
|
235
237
|
return (_jsx("pre", { ...block(node), "data-artifact-code": true,
|
|
236
238
|
// whitespace-pre-wrap, because a fence here usually holds PROSE meant to be pasted
|
|
@@ -355,9 +357,9 @@ const DEFINED_TERM_CSS = '.artifact-defined-term{text-decoration-line:underline;
|
|
|
355
357
|
'clip:rect(0,0,0,0);white-space:nowrap;border:0}';
|
|
356
358
|
/** `notes` turns on the notes widget, for a host that keeps answers; it draws only when the
|
|
357
359
|
* page carries a ```notes block. */
|
|
358
|
-
export function ArtifactMarkdown({ markdown, notes, checklist, definitions, Section, figure, simulator, messages, }) {
|
|
360
|
+
export function ArtifactMarkdown({ markdown, notes, checklist, definitions, Section, figure, simulator, messages, embeds: hosted, }) {
|
|
359
361
|
const on = !!notes && hasNotesWidget(markdown);
|
|
360
|
-
const embeds = { simulator, messages };
|
|
362
|
+
const embeds = { ...hosted, ...(simulator ? { simulator } : {}), ...(messages ? { messages } : {}) };
|
|
361
363
|
const headings = headingsOf(markdown);
|
|
362
364
|
const blocks = blocksOf(markdown);
|
|
363
365
|
const block = (node) => {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export type EmbedKind = 'simulator' | 'messages';
|
|
1
|
+
export type EmbedKind = 'simulator' | 'messages' | 'figure' | 'helm';
|
|
2
2
|
export declare const EMBED_KINDS: EmbedKind[];
|
|
3
3
|
export declare const isEmbedKind: (k: unknown) => k is EmbedKind;
|
|
4
|
+
/** `options` holds a kind's further keys (helm's `autoplay`) as written, and is absent when none were. */
|
|
4
5
|
export type EmbedBlock = {
|
|
5
6
|
value: string | null;
|
|
6
7
|
caption: string;
|
|
7
8
|
height: string;
|
|
9
|
+
options?: Record<string, string>;
|
|
8
10
|
};
|
|
9
11
|
export type SimulatorBlock = {
|
|
10
12
|
scenario: string;
|
|
@@ -22,7 +24,7 @@ export declare function parseEmbedFence(kind: EmbedKind, value: string, line: nu
|
|
|
22
24
|
};
|
|
23
25
|
/** The address a player loads: the host's embed page, what to open, any fixed switches. Null for an
|
|
24
26
|
* embed address that is not https, so a misconfigured host frames nothing. */
|
|
25
|
-
export declare function embedSrc(kind: EmbedKind, embed: string, value: string | null): string | null;
|
|
27
|
+
export declare function embedSrc(kind: EmbedKind, embed: string, value: string | null, options?: Record<string, string>): string | null;
|
|
26
28
|
/** The ```simulator block (kept for callers of 0.25.0). */
|
|
27
29
|
export declare function parseSimulatorFence(value: string, line: number): {
|
|
28
30
|
ok: true;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// `simulator` is a Supersuit Session demo; `messages` is a Messages-app phone of real threads. The
|
|
10
10
|
// block names what to open and a caption; the HOST names each player's embed page
|
|
11
|
-
// (ArtifactRoutesConfig.simulatorEmbed / messagesEmbed), so this public package points at nobody's
|
|
11
|
+
// (ArtifactRoutesConfig.simulatorEmbed / messagesEmbed / figureEmbed / helmEmbed), so this public package points at nobody's
|
|
12
12
|
// service by default, and a host without the player draws the caption alone rather than the block's
|
|
13
13
|
// source as code. `simulator` keeps the syntax the Superskills book's reader takes.
|
|
14
14
|
//
|
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
// frame then takes (EmbedFrame). A page that posts nothing keeps the block's height. The embed
|
|
20
20
|
// page must hold its player's size once asked, never size it from its own viewport, or the two
|
|
21
21
|
// chase each other; that is why the frame listens only for this message and for no player's own.
|
|
22
|
+
const BOOL = { values: ['true', 'false'], off: 'false' };
|
|
22
23
|
const KINDS = {
|
|
23
24
|
simulator: { key: 'scenario', required: true, fixed: { timeline: 'off', fit: 'content' }, height: '720px' },
|
|
24
25
|
messages: { key: 'chat', required: false, fixed: {}, height: '760px' },
|
|
26
|
+
figure: { key: 'figure', required: true, fixed: { fit: 'content' }, height: '560px' },
|
|
27
|
+
helm: { key: 'scene', required: true, fixed: { fit: 'content' }, height: '760px', options: { autoplay: BOOL } },
|
|
25
28
|
};
|
|
26
29
|
export const EMBED_KINDS = Object.keys(KINDS);
|
|
27
30
|
export const isEmbedKind = (k) => typeof k === 'string' && Object.hasOwn(KINDS, k);
|
|
@@ -31,7 +34,8 @@ const HEIGHT = /^\d{3,4}px$/;
|
|
|
31
34
|
* line the author sees. */
|
|
32
35
|
export function parseEmbedFence(kind, value, line) {
|
|
33
36
|
const spec = KINDS[kind];
|
|
34
|
-
const
|
|
37
|
+
const opts = spec.options ?? {};
|
|
38
|
+
const keys = [spec.key, 'caption', 'height', ...Object.keys(opts)];
|
|
35
39
|
const got = {};
|
|
36
40
|
const rows = value ? value.split('\n') : [];
|
|
37
41
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -55,11 +59,23 @@ export function parseEmbedFence(kind, value, line) {
|
|
|
55
59
|
return { ok: false, error: `line ${line}: the ${kind} block needs a caption, the line a reader sees under it` };
|
|
56
60
|
if (got.height && !HEIGHT.test(got.height.v))
|
|
57
61
|
return { ok: false, error: `line ${got.height.at}: a ${kind} height is a number of pixels, like 760px` };
|
|
58
|
-
|
|
62
|
+
const options = {};
|
|
63
|
+
for (const [name, o] of Object.entries(opts)) {
|
|
64
|
+
const g = got[name];
|
|
65
|
+
if (!g)
|
|
66
|
+
continue;
|
|
67
|
+
if (!o.values.includes(g.v))
|
|
68
|
+
return { ok: false, error: `line ${g.at}: a ${kind} ${name} is ${o.values.join(' or ')}` };
|
|
69
|
+
options[name] = g.v;
|
|
70
|
+
}
|
|
71
|
+
const block = { value: id?.v ?? null, caption: got.caption.v, height: got.height?.v ?? spec.height };
|
|
72
|
+
if (Object.keys(options).length)
|
|
73
|
+
block.options = options;
|
|
74
|
+
return { ok: true, block };
|
|
59
75
|
}
|
|
60
76
|
/** The address a player loads: the host's embed page, what to open, any fixed switches. Null for an
|
|
61
77
|
* embed address that is not https, so a misconfigured host frames nothing. */
|
|
62
|
-
export function embedSrc(kind, embed, value) {
|
|
78
|
+
export function embedSrc(kind, embed, value, options) {
|
|
63
79
|
let u;
|
|
64
80
|
try {
|
|
65
81
|
u = new URL(embed);
|
|
@@ -72,6 +88,11 @@ export function embedSrc(kind, embed, value) {
|
|
|
72
88
|
const spec = KINDS[kind];
|
|
73
89
|
if (value)
|
|
74
90
|
u.searchParams.set(spec.key, value);
|
|
91
|
+
for (const [name, o] of Object.entries(spec.options ?? {})) {
|
|
92
|
+
const v = options?.[name];
|
|
93
|
+
if (v && v !== o.off && o.values.includes(v))
|
|
94
|
+
u.searchParams.set(name, v);
|
|
95
|
+
}
|
|
75
96
|
for (const [k, v] of Object.entries(spec.fixed))
|
|
76
97
|
u.searchParams.set(k, v);
|
|
77
98
|
return u.toString();
|
package/lib/artifacts/widgets.js
CHANGED
|
@@ -292,7 +292,9 @@ export function blocksOf(markdown) {
|
|
|
292
292
|
content = plain(n);
|
|
293
293
|
}
|
|
294
294
|
else if (n.type === 'code') {
|
|
295
|
-
|
|
295
|
+
// An embed block draws a player and a caption the reader cannot comment on (data-nospeak, no
|
|
296
|
+
// data-block), so it takes no anchor either.
|
|
297
|
+
if (n.lang === 'links' || n.lang === NOTES_SLOT || n.lang === 'checklist' || n.lang === TRANSCRIPT_LANG || isEmbedKind(n.lang))
|
|
296
298
|
continue;
|
|
297
299
|
kind = 'code';
|
|
298
300
|
content = n.value;
|
|
@@ -37,6 +37,13 @@ export type ArtifactRoutesConfig = {
|
|
|
37
37
|
/** The embed page of this host's Messages phone (an https URL), for ```messages blocks: a
|
|
38
38
|
* `chat=<id>` opens one thread. Without it a block shows its caption only. No default. */
|
|
39
39
|
messagesEmbed?: string;
|
|
40
|
+
/** The embed page of this host's interactive figures (an https URL, e.g.
|
|
41
|
+
* https://example.com/figures/embed), for ```figure blocks: `figure=<id>` opens one. Without it
|
|
42
|
+
* a block shows its caption only. No default. */
|
|
43
|
+
figureEmbed?: string;
|
|
44
|
+
/** The embed page of this host's Helm player (an https URL), for ```helm blocks: `scene=<id>`
|
|
45
|
+
* plays one, `autoplay=true` when the block asks. Without it a block shows its caption only. No default. */
|
|
46
|
+
helmEmbed?: string;
|
|
40
47
|
/** Read at request time, so a rotated key needs no rebuild. */
|
|
41
48
|
publishKey: () => string | undefined;
|
|
42
49
|
/** Verifies publisher passes (`p1`, see artifacts/publisher.ts): a signed, hour-long stand-in
|
package/lib/routes/artifacts.js
CHANGED
|
@@ -92,6 +92,11 @@ export function createArtifactRoutes(config) {
|
|
|
92
92
|
// The version in the URL is what makes a re-publish show up: every unfurler caches by URL.
|
|
93
93
|
const signInOrigin = config.signInOrigin;
|
|
94
94
|
const owner = config.owner ?? brand.name;
|
|
95
|
+
// The embed page of each player this host serves, for every ArtifactMarkdown below.
|
|
96
|
+
const embeds = {};
|
|
97
|
+
for (const [kind, url] of [['simulator', config.simulatorEmbed], ['messages', config.messagesEmbed], ['figure', config.figureEmbed], ['helm', config.helmEmbed]])
|
|
98
|
+
if (url)
|
|
99
|
+
embeds[kind] = { embed: url };
|
|
95
100
|
const ownerEmail = config.ownerEmail?.trim().toLowerCase();
|
|
96
101
|
const admins = new Set([ownerEmail, ...(config.admins ?? []).map((e) => e.trim().toLowerCase())].filter((e) => !!e));
|
|
97
102
|
/** Whether this reader may see this page's version number and past versions: the page's own
|
|
@@ -465,7 +470,7 @@ export function createArtifactRoutes(config) {
|
|
|
465
470
|
const title = v.title ?? a.title;
|
|
466
471
|
const summary = v.summary ?? a.summary;
|
|
467
472
|
const toc = showToc(v.markdown, a.toc) ? tocOf(v.markdown) : [];
|
|
468
|
-
return (_jsxs(_Fragment, { children: [_jsx(Menu, { a: a, reader: reader, history: history, viewing: n, settings: await settingsFor(a, reader), source: isAdmin(reader) ? sourceOf(a.id, { title, subtitle: v.subtitle, summary, to: v.to }, v.markdown) : null }), toc.length ? _jsx(TocRail, { items: toc }) : null, _jsx("div", { "data-nospeak": true, className: `mx-auto max-w-2xl px-6 ${top ? 'pt-20 sm:pt-24' : 'pt-8'}`, children: _jsxs("p", { "data-version-banner": true, role: "status", className: "rounded-lg border px-4 py-3 text-sm", style: { borderColor: brand.accent }, children: [`You are reading version ${n} of ${of}. `, _jsx("a", { href: pagePath(a.id), className: "underline underline-offset-4", style: { color: brand.accent }, children: "Read the current version" })] }) }), _jsx(Framed, { pack: pack, toc: toc.length ? _jsx(TocInline, { items: toc }) : null, cover: pack.Cover ? (_jsx(pack.Cover, { title: title, subtitle: v.subtitle, summary: summary, to: v.to, markdown: v.markdown, kicker: _jsx(Kicker, { to: v.to, brand: brand }), meta: _jsx(VersionLine, { n: n, at: v.at, numbered: true }), version: n, showVersion: true, updatedAt: v.at, createdAt: a.createdAt })) : (_jsxs("div", { className: `mx-auto max-w-2xl px-6 ${top ? 'pt-10 sm:pt-12' : 'pt-10'} pb-8 text-center`, children: [_jsx(Kicker, { to: v.to, brand: brand }), _jsx("h1", { className: "text-4xl sm:text-5xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: title }), v.subtitle ? (_jsx("p", { className: "mx-auto mt-4 max-w-xl text-xl sm:text-2xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: v.subtitle })) : null, _jsx("p", { className: "mx-auto mt-6 max-w-xl text-lg italic opacity-80", children: summary }), _jsx(VersionLine, { n: n, at: v.at, numbered: true })] })), children: _jsx(ArtifactMarkdown, { markdown: v.markdown,
|
|
473
|
+
return (_jsxs(_Fragment, { children: [_jsx(Menu, { a: a, reader: reader, history: history, viewing: n, settings: await settingsFor(a, reader), source: isAdmin(reader) ? sourceOf(a.id, { title, subtitle: v.subtitle, summary, to: v.to }, v.markdown) : null }), toc.length ? _jsx(TocRail, { items: toc }) : null, _jsx("div", { "data-nospeak": true, className: `mx-auto max-w-2xl px-6 ${top ? 'pt-20 sm:pt-24' : 'pt-8'}`, children: _jsxs("p", { "data-version-banner": true, role: "status", className: "rounded-lg border px-4 py-3 text-sm", style: { borderColor: brand.accent }, children: [`You are reading version ${n} of ${of}. `, _jsx("a", { href: pagePath(a.id), className: "underline underline-offset-4", style: { color: brand.accent }, children: "Read the current version" })] }) }), _jsx(Framed, { pack: pack, toc: toc.length ? _jsx(TocInline, { items: toc }) : null, cover: pack.Cover ? (_jsx(pack.Cover, { title: title, subtitle: v.subtitle, summary: summary, to: v.to, markdown: v.markdown, kicker: _jsx(Kicker, { to: v.to, brand: brand }), meta: _jsx(VersionLine, { n: n, at: v.at, numbered: true }), version: n, showVersion: true, updatedAt: v.at, createdAt: a.createdAt })) : (_jsxs("div", { className: `mx-auto max-w-2xl px-6 ${top ? 'pt-10 sm:pt-12' : 'pt-10'} pb-8 text-center`, children: [_jsx(Kicker, { to: v.to, brand: brand }), _jsx("h1", { className: "text-4xl sm:text-5xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: title }), v.subtitle ? (_jsx("p", { className: "mx-auto mt-4 max-w-xl text-xl sm:text-2xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: v.subtitle })) : null, _jsx("p", { className: "mx-auto mt-6 max-w-xl text-lg italic opacity-80", children: summary }), _jsx(VersionLine, { n: n, at: v.at, numbered: true })] })), children: _jsx(ArtifactMarkdown, { markdown: v.markdown, embeds: embeds, definitions: a.definitions, checklist: { artifactId: a.id, accent: brand.accent }, Section: pack.Section, figure: pack.figure }) })] }));
|
|
469
474
|
}
|
|
470
475
|
/** A broadcast: one message card. The sender's words at the top, then the media (a video with
|
|
471
476
|
* sound and controls, photos, a voice note's player, link cards) in the order the words put
|
|
@@ -480,7 +485,7 @@ export function createArtifactRoutes(config) {
|
|
|
480
485
|
const cover = !bot && a.cover && !/!\[[^\]]*\]\(/.test(a.markdown) ? a.cover : null;
|
|
481
486
|
return (_jsxs(_Fragment, { children: [admin ? (_jsx(PageMenu, { role: roleOf(reader), name: reader ? reader.name ?? reader.email : null, share: null, comments: mode !== 'off', signIn: null, signOut: signOutUrl(a.id), history: null, markdown: null, settings: null })) : null, _jsx("main", { "data-broadcast": true, className: "mx-auto max-w-lg px-4 pt-16 pb-20 sm:pt-24", children: _jsxs("article", { id: "broadcast-root", "data-broadcast-card": true, className: "rounded-2xl border border-[color:var(--a-line)] bg-[color:var(--a-surface)] px-5 py-6 sm:px-7 sm:py-8", children: [_jsx("p", { "data-nospeak": true, "data-broadcast-from": true, className: "text-[11px] font-medium uppercase tracking-[0.3em]", style: { color: brand.accent }, children: `From ${owner}` }), _jsx("h1", { className: "mt-3 text-2xl sm:text-3xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: a.title }), cover ? (
|
|
482
487
|
// eslint-disable-next-line @next/next/no-img-element
|
|
483
|
-
_jsx("img", { src: cover, alt: "", className: "mt-5 w-full rounded-xl border border-[color:var(--a-line)]" })) : null, _jsx("div", { "data-broadcast-body": true, className: "mt-4", children: _jsx(ArtifactMarkdown, { markdown: a.markdown, checklist: { artifactId: a.id, owner, accent: brand.accent },
|
|
488
|
+
_jsx("img", { src: cover, alt: "", className: "mt-5 w-full rounded-xl border border-[color:var(--a-line)]" })) : null, _jsx("div", { "data-broadcast-body": true, className: "mt-4", children: _jsx(ArtifactMarkdown, { markdown: a.markdown, checklist: { artifactId: a.id, owner, accent: brand.accent }, embeds: embeds }) }), perReader?.note ? (_jsxs("div", { "data-for-reader": true, className: "mt-6 rounded-xl border border-[color:var(--a-line)] px-4 py-3", children: [_jsx("p", { "data-nospeak": true, className: "text-[11px] uppercase tracking-[0.2em] opacity-70", children: `For ${perReader.label}` }), _jsx("p", { className: "mt-1 text-lg", children: perReader.note })] })) : null] }) }), mode !== 'off' ? (_jsx(CommentLayer, { artifactId: a.id, rootId: "broadcast-root", ownerName: owner, comments: mode, canShare: reader !== null || effectiveWriters(a.state, a.access, COMMENTS_SLOT) === 'anyone', signedIn: reader !== null, isOwner: admin, version: a.version ?? (a.versions?.length ?? 0) + 1, accent: brand.accent, ground: brand.ground, signIn: null, closesAt: perReader && !perReader.owner ? perReader.closesAt : null, requireName: a.commentName === 'required' && perReader?.group === true, knownName: a.commentName === 'required' && perReader && !perReader.group && !perReader.owner ? perReader.label : null })) : null, perReader && !perReader.owner ? _jsx(QuietView, { id: a.id, rid: perReader.rid, sig: perReader.sig, token: perReader.token }) : null] }));
|
|
484
489
|
}
|
|
485
490
|
/** The page itself, shared by open and gated pages. */
|
|
486
491
|
async function Body(a, { top, reader, perReader }) {
|
|
@@ -507,7 +512,7 @@ export function createArtifactRoutes(config) {
|
|
|
507
512
|
const header = pack.Cover ? (_jsx(pack.Cover, { title: a.title, subtitle: a.subtitle, summary: a.summary, to: a.to, markdown: a.markdown, kicker: _jsx(Kicker, { to: a.to, brand: brand }), meta: meta, version: n, showVersion: seesHistory(a, reader), updatedAt: a.updatedAt, createdAt: a.createdAt })) : (_jsxs("div", { className: `mx-auto max-w-2xl px-6 ${top ? 'pt-24 sm:pt-28' : 'pt-12'} pb-8 text-center`, children: [_jsx(Kicker, { to: a.to, brand: brand }), _jsx("h1", { className: "text-4xl sm:text-5xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: a.title }), a.subtitle ? (_jsx("p", { className: "mx-auto mt-4 max-w-xl text-xl sm:text-2xl", style: { fontFamily: brand.type.display, color: brand.ink }, children: a.subtitle })) : null, _jsx("p", { className: "mx-auto mt-6 max-w-xl text-lg italic opacity-80", children: a.summary }), meta] }));
|
|
508
513
|
return (_jsx(Framed, { pack: pack, toc: toc.length ? _jsx(TocInline, { items: toc }) : null, cover: _jsxs(_Fragment, { children: [perReader?.closesAt && !perReader.owner ? (_jsx(FeedbackWindow, { owner: owner, label: perReader.label, note: perReader.note, group: perReader.group, closesAt: perReader.closesAt })) : perReader?.note ? (_jsx("p", { "data-for-reader": true, className: "mx-auto mt-6 mb-0 max-w-2xl px-6 text-lg", children: `For ${perReader.label}: ${perReader.note}` })) : null, 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,
|
|
509
514
|
// Ticks are kept in the reader's browser on any host; Send needs one that keeps answers.
|
|
510
|
-
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,
|
|
515
|
+
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, embeds: embeds }) }));
|
|
511
516
|
})() }), _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, closesAt: perReader && !perReader.owner ? perReader.closesAt : null, requireName: a.commentName === 'required' && perReader?.group === true, knownName: a.commentName === 'required' && perReader && !perReader.group && !perReader.owner ? perReader.label : null }), perReader && !perReader.owner ? _jsx(QuietView, { id: a.id, rid: perReader.rid, sig: perReader.sig, token: perReader.token }) : 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,
|
|
512
517
|
2026-09-29: "NEVER PUT DEFAULT SAFARI VOICE DICTATION"); the audio is Kokoro or ElevenLabs,
|
|
513
518
|
recorded at publish, or there is none. */] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.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",
|