@supersuit/artifacts 0.26.0 → 0.27.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 +23 -0
- package/lib/artifacts/index.d.ts +1 -1
- package/lib/artifacts/index.js +1 -1
- package/lib/artifacts/render.d.ts +3 -1
- package/lib/artifacts/render.js +19 -17
- package/lib/artifacts/simulator.d.ts +22 -5
- package/lib/artifacts/simulator.js +49 -32
- package/lib/artifacts/widgets.js +3 -3
- package/lib/routes/artifacts.d.ts +3 -0
- package/lib/routes/artifacts.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
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.27.0
|
|
7
|
+
|
|
8
|
+
**A page can play a Messages-app phone of real threads inline: the ```messages block.** The
|
|
9
|
+
second embedded player after ```simulator, so both now run through one embed mechanism.
|
|
10
|
+
|
|
11
|
+
```messages
|
|
12
|
+
chat: fritz # optional: open on one chat; without it the chat list
|
|
13
|
+
caption: Fritz, in his own words.
|
|
14
|
+
height: 760px # optional, default 760px
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- The HOST names the player: new `ArtifactRoutesConfig.messagesEmbed` (an https embed page). The
|
|
18
|
+
block draws as a lazy frame on `<embed>?chat=<id>`, with the caption under it; a host without it
|
|
19
|
+
draws the caption alone, never the block as code. Malformed blocks are refused at publish.
|
|
20
|
+
- `simulator` and `messages` share `parseEmbedFence` and `embedSrc` (new exports, with
|
|
21
|
+
`EMBED_KINDS`, `EmbedBlock`, `EmbedKind`); `parseSimulatorFence` and `simulatorSrc` keep working.
|
|
22
|
+
`ArtifactMarkdown` takes `messages?: { embed }`.
|
|
23
|
+
- DETECTOR: a page with a ```messages block showing `chat:` as code means 0.26.0 or earlier, or no
|
|
24
|
+
`messagesEmbed`.
|
|
25
|
+
- REMEDY: update the dependency and set `messagesEmbed`.
|
|
26
|
+
- Tests: `simulator.test.ts` (messages parse, optional chat, refusals, src with and without a chat),
|
|
27
|
+
`render.test.tsx`, `widgets.test.ts`. `test:packed` green.
|
|
28
|
+
|
|
6
29
|
## 0.26.0
|
|
7
30
|
|
|
8
31
|
**Read-along that leaves the reader in charge.** A tap on any word started the audio, and while it
|
package/lib/artifacts/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { parseStateConfig, effectiveWriters, slotVisibility, checkValue, shapeCh
|
|
|
13
13
|
export { createStateStore, createMemoryStateStore, readerKeyFor, type StateStore, type StateEntry, type Writer } from './state-store.js';
|
|
14
14
|
export { stateView, responsesOf, responsesCsv, type SlotView, type Tally, type SharedEntry, type Response as StateResponse } from './state-view.js';
|
|
15
15
|
export { slugify, headingsOf, blocksOf, scanWidgets, mergeWidgetState, mergeChecklistState, checklistsOf, checklistSendIds, checklistAnswerIds, checklistSyncs, hasNotesWidget, checkNoteValue, placeNotes, NOTES_SLOT, MAX_NOTE_CHARS, type Heading, type NotesWidget, type NoteValue, type PlacedNote } from './widgets.js';
|
|
16
|
-
export { parseSimulatorFence, simulatorSrc, type SimulatorBlock } from './simulator.js';
|
|
16
|
+
export { parseSimulatorFence, simulatorSrc, parseEmbedFence, embedSrc, EMBED_KINDS, type SimulatorBlock, type EmbedBlock, type EmbedKind } from './simulator.js';
|
|
17
17
|
export { CHECKLIST_SLOT, checklistStorageKey, checkChecklistValue, parseChecklistFence, type ChecklistBlock, type ChecklistItem, type ChecklistValue } from './checklist.js';
|
|
18
18
|
export { textAnchorFrom, findText, blockId, parseAnchor, CONTEXT_CHARS, MAX_QUOTE_CHARS, BLOCK_ID, type Anchor, type TextAnchor, type RegionAnchor } from './anchor.js';
|
|
19
19
|
export { COMMENTS_SLOT, COMMENTS_MODES, COMMENT_TRANSCRIPTS, MAX_COMMENT_CHARS, mergeCommentsState, validateComment, type CommentsMode, type CommentsVisible, type CommentValue } from './comments.js';
|
package/lib/artifacts/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { parseStateConfig, effectiveWriters, slotVisibility, checkValue, shapeCh
|
|
|
13
13
|
export { createStateStore, createMemoryStateStore, readerKeyFor } from './state-store.js';
|
|
14
14
|
export { stateView, responsesOf, responsesCsv } from './state-view.js';
|
|
15
15
|
export { slugify, headingsOf, blocksOf, scanWidgets, mergeWidgetState, mergeChecklistState, checklistsOf, checklistSendIds, checklistAnswerIds, checklistSyncs, hasNotesWidget, checkNoteValue, placeNotes, NOTES_SLOT, MAX_NOTE_CHARS } from './widgets.js';
|
|
16
|
-
export { parseSimulatorFence, simulatorSrc } from './simulator.js';
|
|
16
|
+
export { parseSimulatorFence, simulatorSrc, parseEmbedFence, embedSrc, EMBED_KINDS } from './simulator.js';
|
|
17
17
|
export { CHECKLIST_SLOT, checklistStorageKey, checkChecklistValue, parseChecklistFence } from './checklist.js';
|
|
18
18
|
export { textAnchorFrom, findText, blockId, parseAnchor, CONTEXT_CHARS, MAX_QUOTE_CHARS, BLOCK_ID } from './anchor.js';
|
|
19
19
|
export { COMMENTS_SLOT, COMMENTS_MODES, COMMENT_TRANSCRIPTS, MAX_COMMENT_CHARS, mergeCommentsState, validateComment } from './comments.js';
|
|
@@ -14,7 +14,7 @@ export type FigureFor = (src: string, alt: string) => ComponentType<FigureProps>
|
|
|
14
14
|
export declare function sectionOutline(markdown: string, headings?: Heading[]): SectionOutline[];
|
|
15
15
|
/** `notes` turns on the notes widget, for a host that keeps answers; it draws only when the
|
|
16
16
|
* page carries a ```notes block. */
|
|
17
|
-
export declare function ArtifactMarkdown({ markdown, notes, checklist, definitions, Section, figure, simulator, }: {
|
|
17
|
+
export declare function ArtifactMarkdown({ markdown, notes, checklist, definitions, Section, figure, simulator, messages, }: {
|
|
18
18
|
markdown: string;
|
|
19
19
|
/** The page a ```checklist keeps its ticks under, and whether its Send can reach the owner
|
|
20
20
|
* (a host that keeps answers, on a page that declares the slot). On a page whose checklists
|
|
@@ -38,5 +38,7 @@ export declare function ArtifactMarkdown({ markdown, notes, checklist, definitio
|
|
|
38
38
|
definitions?: Definition[];
|
|
39
39
|
/** The host's Supersuit Session demo, for ```simulator blocks. Without it a block shows its caption only. */
|
|
40
40
|
simulator?: SimulatorCtx;
|
|
41
|
+
/** The host's Messages phone, for ```messages blocks. Without it a block shows its caption only. */
|
|
42
|
+
messages?: SimulatorCtx;
|
|
41
43
|
}): import("react").JSX.Element;
|
|
42
44
|
export {};
|
package/lib/artifacts/render.js
CHANGED
|
@@ -16,7 +16,7 @@ import { unified } from 'unified';
|
|
|
16
16
|
import { blocksOf, checklistSyncs, checklistsOf, hasNotesWidget, headingsOf } from './widgets.js';
|
|
17
17
|
import { HeadingNotes, NoteToggle, NotesEarlier, NotesProvider } from '../widgets/notes.js';
|
|
18
18
|
import { Checklist } from '../widgets/checklist.js';
|
|
19
|
-
import {
|
|
19
|
+
import { embedSrc, parseEmbedFence } from './simulator.js';
|
|
20
20
|
import { VideoAutoplay } from '../reader/video-autoplay.js';
|
|
21
21
|
import { CodeCopy } from '../reader/code-copy.js';
|
|
22
22
|
import { DefinitionLayer } from '../reader/definition-layer.js';
|
|
@@ -95,14 +95,15 @@ function InlineAudio({ src, label }) {
|
|
|
95
95
|
const html = `<audio src="${escapeAttr(src)}" aria-label="${escapeAttr(label)}" data-artifact-audio controls preload="none" class="block w-full"></audio>`;
|
|
96
96
|
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 } })] }));
|
|
97
97
|
}
|
|
98
|
-
/**
|
|
99
|
-
* not, and nothing when the block does not parse (a page published
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
/** An embed block (```simulator, ```messages): the player in a frame when the host names one, its
|
|
99
|
+
* caption alone when it does not, and nothing when the block does not parse (a page published
|
|
100
|
+
* before validation existed). */
|
|
101
|
+
function EmbedFence({ kind, source, ctx }) {
|
|
102
|
+
const r = parseEmbedFence(kind, source.replace(/\n$/, ''), 0);
|
|
102
103
|
if (!r.ok)
|
|
103
104
|
return null;
|
|
104
|
-
const src = ctx ?
|
|
105
|
-
return (_jsxs("figure", { "data-artifact-
|
|
105
|
+
const src = ctx ? embedSrc(kind, ctx.embed, r.block.value) : null;
|
|
106
|
+
return (_jsxs("figure", { "data-artifact-embed": kind, ...(kind === 'simulator' ? { 'data-artifact-simulator': '' } : {}), className: "my-8", children: [src ? (_jsx("iframe", { src: src, title: r.block.caption, loading: "lazy", allow: "microphone; autoplay; fullscreen", className: "block w-full rounded-xl border border-[color:var(--a-line)]", style: { height: r.block.height, border: 0 } })) : null, _jsx("figcaption", { className: "mt-2 text-sm opacity-80", children: r.block.caption })] }));
|
|
106
107
|
}
|
|
107
108
|
/** The one image a paragraph holds and nothing else (whitespace aside), or null. */
|
|
108
109
|
function soleImage(node) {
|
|
@@ -132,7 +133,7 @@ function ChecklistFence({ line, ctx }) {
|
|
|
132
133
|
return null;
|
|
133
134
|
return (_jsx(Checklist, { items: c.items.map((it) => ({ id: it.id, title: checklistMarkdown(it.text, true), description: it.description ? checklistMarkdown(it.description, false) : null })), artifactId: ctx.artifactId, send: ctx.send && ctx.artifactId && !ctx.sync ? c.send : undefined, sync: ctx.sync && !!ctx.artifactId, owner: ctx.owner, accent: ctx.accent }));
|
|
134
135
|
}
|
|
135
|
-
function baseComponents(block, figure, checklists,
|
|
136
|
+
function baseComponents(block, figure, checklists, embeds) {
|
|
136
137
|
return {
|
|
137
138
|
p: ({ node, children }) => {
|
|
138
139
|
// An image alone in its paragraph that the pack draws itself replaces the paragraph, so the
|
|
@@ -172,8 +173,8 @@ function baseComponents(block, figure, checklists, simulator) {
|
|
|
172
173
|
// answers) it draws nothing: its settings are not prose and never shown as code.
|
|
173
174
|
if (lang === 'notes')
|
|
174
175
|
return null;
|
|
175
|
-
if (lang === 'simulator')
|
|
176
|
-
return _jsx(
|
|
176
|
+
if (lang === 'simulator' || lang === 'messages')
|
|
177
|
+
return _jsx(EmbedFence, { kind: lang, source: textOf(children), ctx: embeds?.[lang] });
|
|
177
178
|
if (!className) {
|
|
178
179
|
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 });
|
|
179
180
|
}
|
|
@@ -183,7 +184,7 @@ function baseComponents(block, figure, checklists, simulator) {
|
|
|
183
184
|
// A links fence renders its own block; do not wrap it in <pre>.
|
|
184
185
|
const inner = Array.isArray(children) ? children[0] : children;
|
|
185
186
|
const cls = inner?.props?.className ?? '';
|
|
186
|
-
if (/language-(links|notes|checklist|simulator)/.test(cls))
|
|
187
|
+
if (/language-(links|notes|checklist|simulator|messages)/.test(cls))
|
|
187
188
|
return _jsx(_Fragment, { children: children });
|
|
188
189
|
return (_jsx("pre", { ...block(node), "data-artifact-code": true,
|
|
189
190
|
// whitespace-pre-wrap, because a fence here usually holds PROSE meant to be pasted
|
|
@@ -216,20 +217,20 @@ const HEADING_CLASS = {
|
|
|
216
217
|
};
|
|
217
218
|
/** Components for any page: every heading gets its slug as an id, the one headingsOf derived,
|
|
218
219
|
* so a contents link and a note both land on it. */
|
|
219
|
-
function pageComponents(headings, block, figure, checklists,
|
|
220
|
+
function pageComponents(headings, block, figure, checklists, embeds) {
|
|
220
221
|
const byLine = new Map(headings.map((h) => [h.line, h]));
|
|
221
222
|
const heading = (depth) => function Heading({ node, children }) {
|
|
222
223
|
const Tag = `h${depth}`;
|
|
223
224
|
const h = byLine.get(node?.position?.start.line ?? -1);
|
|
224
225
|
return _jsx(Tag, { id: h?.slug, ...block(node), className: HEADING_CLASS[depth], children: children });
|
|
225
226
|
};
|
|
226
|
-
return { ...baseComponents(block, figure, checklists,
|
|
227
|
+
return { ...baseComponents(block, figure, checklists, embeds), h1: heading(1), h2: heading(2), h3: heading(3), h4: heading(4), h5: heading(5), h6: heading(6) };
|
|
227
228
|
}
|
|
228
229
|
/** Components for a page with a notes block: every heading gets its slug as an id, a note
|
|
229
230
|
* control, and its notes after it. The heading is found by its source line, so the slug is the
|
|
230
231
|
* one headingsOf derived, the same one a note stores. */
|
|
231
|
-
function notesComponents(headings, block, figure, checklists,
|
|
232
|
-
const components = baseComponents(block, figure, checklists,
|
|
232
|
+
function notesComponents(headings, block, figure, checklists, embeds) {
|
|
233
|
+
const components = baseComponents(block, figure, checklists, embeds);
|
|
233
234
|
const byLine = new Map(headings.map((h) => [h.line, h]));
|
|
234
235
|
const heading = (depth) => function NotedHeading({ node, children }) {
|
|
235
236
|
const Tag = `h${depth}`;
|
|
@@ -308,8 +309,9 @@ const DEFINED_TERM_CSS = '.artifact-defined-term{text-decoration-line:underline;
|
|
|
308
309
|
'clip:rect(0,0,0,0);white-space:nowrap;border:0}';
|
|
309
310
|
/** `notes` turns on the notes widget, for a host that keeps answers; it draws only when the
|
|
310
311
|
* page carries a ```notes block. */
|
|
311
|
-
export function ArtifactMarkdown({ markdown, notes, checklist, definitions, Section, figure, simulator, }) {
|
|
312
|
+
export function ArtifactMarkdown({ markdown, notes, checklist, definitions, Section, figure, simulator, messages, }) {
|
|
312
313
|
const on = !!notes && hasNotesWidget(markdown);
|
|
314
|
+
const embeds = { simulator, messages };
|
|
313
315
|
const headings = headingsOf(markdown);
|
|
314
316
|
const blocks = blocksOf(markdown);
|
|
315
317
|
const block = (node) => {
|
|
@@ -325,7 +327,7 @@ export function ArtifactMarkdown({ markdown, notes, checklist, definitions, Sect
|
|
|
325
327
|
// page whose checklists sync, that same slot is the shared set, so it turns sync on instead.
|
|
326
328
|
? { byLine: lists, artifactId: checklist?.artifactId, send: !!checklist?.send, sync: !!checklist?.send && checklistSyncs(lists.values()), owner: checklist?.owner, accent: checklist?.accent }
|
|
327
329
|
: undefined;
|
|
328
|
-
const components = on ? notesComponents(headings, block, figure, checklists,
|
|
330
|
+
const components = on ? notesComponents(headings, block, figure, checklists, embeds) : pageComponents(headings, block, figure, checklists, embeds);
|
|
329
331
|
if (outline.length)
|
|
330
332
|
components.section = sectionComponent(Section, outline, block);
|
|
331
333
|
const body = (_jsx(ReactMarkdown, { remarkPlugins: plugins, components: components, urlTransform: artifactUrlTransform, children: markdown }));
|
|
@@ -1,10 +1,29 @@
|
|
|
1
|
+
export type EmbedKind = 'simulator' | 'messages';
|
|
2
|
+
export declare const EMBED_KINDS: EmbedKind[];
|
|
3
|
+
export declare const isEmbedKind: (k: unknown) => k is EmbedKind;
|
|
4
|
+
export type EmbedBlock = {
|
|
5
|
+
value: string | null;
|
|
6
|
+
caption: string;
|
|
7
|
+
height: string;
|
|
8
|
+
};
|
|
1
9
|
export type SimulatorBlock = {
|
|
2
10
|
scenario: string;
|
|
3
11
|
caption: string;
|
|
4
12
|
height: string;
|
|
5
13
|
};
|
|
6
|
-
/** Parses
|
|
7
|
-
*
|
|
14
|
+
/** Parses an embed fence's body. `line` is the line the fence opens on, so every error names the
|
|
15
|
+
* line the author sees. */
|
|
16
|
+
export declare function parseEmbedFence(kind: EmbedKind, value: string, line: number): {
|
|
17
|
+
ok: true;
|
|
18
|
+
block: EmbedBlock;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
/** The address a player loads: the host's embed page, what to open, any fixed switches. Null for an
|
|
24
|
+
* 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;
|
|
26
|
+
/** The ```simulator block (kept for callers of 0.25.0). */
|
|
8
27
|
export declare function parseSimulatorFence(value: string, line: number): {
|
|
9
28
|
ok: true;
|
|
10
29
|
block: SimulatorBlock;
|
|
@@ -12,6 +31,4 @@ export declare function parseSimulatorFence(value: string, line: number): {
|
|
|
12
31
|
ok: false;
|
|
13
32
|
error: string;
|
|
14
33
|
};
|
|
15
|
-
|
|
16
|
-
* an embed address that is not https, so a misconfigured host frames nothing. */
|
|
17
|
-
export declare function simulatorSrc(embed: string, scenario: string): string | null;
|
|
34
|
+
export declare const simulatorSrc: (embed: string, scenario: string) => string | null;
|
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
//
|
|
2
|
-
// scenario and a caption; the HOST names the demo's embed page (ArtifactRoutesConfig.simulatorEmbed),
|
|
3
|
-
// so this public package points at nobody's service by default, and a host without a demo draws
|
|
4
|
-
// the caption alone rather than the block's source as code.
|
|
1
|
+
// Embedded players: fenced blocks that play a host-named page inline, in a frame.
|
|
5
2
|
//
|
|
6
|
-
// ```simulator
|
|
7
|
-
// scenario: diane-meeting-prep
|
|
8
|
-
// caption: Step into the hour
|
|
9
|
-
// height: 760px
|
|
10
|
-
// ```
|
|
3
|
+
// ```simulator ```messages
|
|
4
|
+
// scenario: diane-meeting-prep chat: fritz # optional: open on one chat
|
|
5
|
+
// caption: Step into the hour ... caption: Fritz, in his own words.
|
|
6
|
+
// height: 760px # optional height: 760px # optional, default 760px (simulator 720px)
|
|
7
|
+
// ``` ```
|
|
11
8
|
//
|
|
12
|
-
//
|
|
13
|
-
|
|
9
|
+
// `simulator` is a Supersuit Session demo; `messages` is a Messages-app phone of real threads. The
|
|
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
|
|
12
|
+
// service by default, and a host without the player draws the caption alone rather than the block's
|
|
13
|
+
// source as code. `simulator` keeps the syntax the Superskills book's reader takes.
|
|
14
|
+
const KINDS = {
|
|
15
|
+
simulator: { key: 'scenario', required: true, fixed: { timeline: 'off' }, height: '720px' },
|
|
16
|
+
messages: { key: 'chat', required: false, fixed: {}, height: '760px' },
|
|
17
|
+
};
|
|
18
|
+
export const EMBED_KINDS = Object.keys(KINDS);
|
|
19
|
+
export const isEmbedKind = (k) => typeof k === 'string' && Object.hasOwn(KINDS, k);
|
|
20
|
+
const ID = /^[a-z0-9-]+$/;
|
|
14
21
|
const HEIGHT = /^\d{3,4}px$/;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
/** Parses an embed fence's body. `line` is the line the fence opens on, so every error names the
|
|
23
|
+
* line the author sees. */
|
|
24
|
+
export function parseEmbedFence(kind, value, line) {
|
|
25
|
+
const spec = KINDS[kind];
|
|
26
|
+
const keys = [spec.key, 'caption', 'height'];
|
|
19
27
|
const got = {};
|
|
20
28
|
const rows = value ? value.split('\n') : [];
|
|
21
29
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -25,25 +33,25 @@ export function parseSimulatorFence(value, line) {
|
|
|
25
33
|
const at = line + 1 + i;
|
|
26
34
|
const m = /^([A-Za-z_-]+)\s*:\s*(.*)$/.exec(raw);
|
|
27
35
|
if (!m)
|
|
28
|
-
return { ok: false, error: `line ${at}: the
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
got[key] = { v: m[2].replace(/\s+#.*$/, '').trim(), at };
|
|
36
|
+
return { ok: false, error: `line ${at}: the ${kind} block takes lines like "${spec.key}: <id>"` };
|
|
37
|
+
if (!keys.includes(m[1]))
|
|
38
|
+
return { ok: false, error: `line ${at}: the ${kind} block has an unknown key: ${m[1]}` };
|
|
39
|
+
got[m[1]] = { v: m[2].replace(/\s+#.*$/, '').trim(), at };
|
|
33
40
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
const id = got[spec.key];
|
|
42
|
+
if (!id && spec.required)
|
|
43
|
+
return { ok: false, error: `line ${line}: the ${kind} block needs a ${spec.key}` };
|
|
44
|
+
if (id && !ID.test(id.v))
|
|
45
|
+
return { ok: false, error: `line ${id.at}: a ${kind} ${spec.key} is lowercase letters, digits and dashes` };
|
|
38
46
|
if (!got.caption?.v)
|
|
39
|
-
return { ok: false, error: `line ${line}: the
|
|
47
|
+
return { ok: false, error: `line ${line}: the ${kind} block needs a caption, the line a reader sees under it` };
|
|
40
48
|
if (got.height && !HEIGHT.test(got.height.v))
|
|
41
|
-
return { ok: false, error: `line ${got.height.at}: a
|
|
42
|
-
return { ok: true, block: {
|
|
49
|
+
return { ok: false, error: `line ${got.height.at}: a ${kind} height is a number of pixels, like 760px` };
|
|
50
|
+
return { ok: true, block: { value: id?.v ?? null, caption: got.caption.v, height: got.height?.v ?? spec.height } };
|
|
43
51
|
}
|
|
44
|
-
/** The address
|
|
45
|
-
*
|
|
46
|
-
export function
|
|
52
|
+
/** The address a player loads: the host's embed page, what to open, any fixed switches. Null for an
|
|
53
|
+
* embed address that is not https, so a misconfigured host frames nothing. */
|
|
54
|
+
export function embedSrc(kind, embed, value) {
|
|
47
55
|
let u;
|
|
48
56
|
try {
|
|
49
57
|
u = new URL(embed);
|
|
@@ -53,7 +61,16 @@ export function simulatorSrc(embed, scenario) {
|
|
|
53
61
|
}
|
|
54
62
|
if (u.protocol !== 'https:')
|
|
55
63
|
return null;
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
const spec = KINDS[kind];
|
|
65
|
+
if (value)
|
|
66
|
+
u.searchParams.set(spec.key, value);
|
|
67
|
+
for (const [k, v] of Object.entries(spec.fixed))
|
|
68
|
+
u.searchParams.set(k, v);
|
|
58
69
|
return u.toString();
|
|
59
70
|
}
|
|
71
|
+
/** The ```simulator block (kept for callers of 0.25.0). */
|
|
72
|
+
export function parseSimulatorFence(value, line) {
|
|
73
|
+
const r = parseEmbedFence('simulator', value, line);
|
|
74
|
+
return r.ok ? { ok: true, block: { scenario: r.block.value, caption: r.block.caption, height: r.block.height } } : r;
|
|
75
|
+
}
|
|
76
|
+
export const simulatorSrc = (embed, scenario) => embedSrc('simulator', embed, scenario);
|
package/lib/artifacts/widgets.js
CHANGED
|
@@ -16,7 +16,7 @@ import remarkParse from 'remark-parse';
|
|
|
16
16
|
import remarkGfm from 'remark-gfm';
|
|
17
17
|
import { blockId } from './anchor.js';
|
|
18
18
|
import { MAX_NOTE_CHARS, NOTES_SLOT } from './notes-place.js';
|
|
19
|
-
import {
|
|
19
|
+
import { isEmbedKind, parseEmbedFence } from './simulator.js';
|
|
20
20
|
import { CHECKLIST_SLOT, assignChecklistIds, parseChecklistFence } from './checklist.js';
|
|
21
21
|
export { MAX_NOTE_CHARS, NOTES_SLOT, placeNotes } from './notes-place.js';
|
|
22
22
|
/** Widgets the design names and this version does not draw yet. A fence using one is refused at
|
|
@@ -102,9 +102,9 @@ export function scanWidgets(body, offset) {
|
|
|
102
102
|
notes = w;
|
|
103
103
|
}
|
|
104
104
|
for (const f of fences) {
|
|
105
|
-
if (f.lang
|
|
105
|
+
if (!isEmbedKind(f.lang))
|
|
106
106
|
continue;
|
|
107
|
-
const r =
|
|
107
|
+
const r = parseEmbedFence(f.lang, f.value, (f.position?.start.line ?? 1) + offset);
|
|
108
108
|
if (!r.ok)
|
|
109
109
|
return r;
|
|
110
110
|
}
|
|
@@ -31,6 +31,9 @@ export type ArtifactRoutesConfig = {
|
|
|
31
31
|
* https://example.com/simulator/embed), for ```simulator blocks. Without it a block shows its
|
|
32
32
|
* caption only. No default: this package points at nobody's service. */
|
|
33
33
|
simulatorEmbed?: string;
|
|
34
|
+
/** The embed page of this host's Messages phone (an https URL), for ```messages blocks: a
|
|
35
|
+
* `chat=<id>` opens one thread. Without it a block shows its caption only. No default. */
|
|
36
|
+
messagesEmbed?: string;
|
|
34
37
|
/** Read at request time, so a rotated key needs no rebuild. */
|
|
35
38
|
publishKey: () => string | undefined;
|
|
36
39
|
/** Verifies publisher passes (`p1`, see artifacts/publisher.ts): a signed, hour-long stand-in
|
package/lib/routes/artifacts.js
CHANGED
|
@@ -303,7 +303,7 @@ export function createArtifactRoutes(config) {
|
|
|
303
303
|
const title = v.title ?? a.title;
|
|
304
304
|
const summary = v.summary ?? a.summary;
|
|
305
305
|
const toc = showToc(v.markdown, a.toc) ? tocOf(v.markdown) : [];
|
|
306
|
-
return (_jsxs(_Fragment, { children: [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, { a: a, n: n, at: v.at, history: history, reader: reader }), version: n, 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, { a: a, n: n, at: v.at, history: history, reader: reader })] })), children: _jsx(ArtifactMarkdown, { markdown: v.markdown, simulator: config.simulatorEmbed ? { embed: config.simulatorEmbed } : undefined, definitions: a.definitions, checklist: { artifactId: a.id, accent: brand.accent }, Section: pack.Section, figure: pack.figure }) })] }));
|
|
306
|
+
return (_jsxs(_Fragment, { children: [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, { a: a, n: n, at: v.at, history: history, reader: reader }), version: n, 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, { a: a, n: n, at: v.at, history: history, reader: reader })] })), children: _jsx(ArtifactMarkdown, { markdown: v.markdown, simulator: config.simulatorEmbed ? { embed: config.simulatorEmbed } : undefined, messages: config.messagesEmbed ? { embed: config.messagesEmbed } : undefined, definitions: a.definitions, checklist: { artifactId: a.id, accent: brand.accent }, Section: pack.Section, figure: pack.figure }) })] }));
|
|
307
307
|
}
|
|
308
308
|
/** The page itself, shared by open and gated pages. */
|
|
309
309
|
async function Body(a, { top, reader }) {
|
|
@@ -328,7 +328,7 @@ export function createArtifactRoutes(config) {
|
|
|
328
328
|
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, 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] }));
|
|
329
329
|
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,
|
|
330
330
|
// Ticks are kept in the reader's browser on any host; Send needs one that keeps answers.
|
|
331
|
-
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 }) }));
|
|
331
|
+
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 }) }));
|
|
332
332
|
})() }), _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: !!reader && !!ownerEmail && reader.email === ownerEmail, 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", label: brand.narratorLabel(a.voice), accent: brand.accent, ground: brand.ground })) : null /* No recorded narration: no player. Never the browser's own voice (the operator,
|
|
333
333
|
2026-09-29: "NEVER PUT DEFAULT SAFARI VOICE DICTATION"); the audio is Kokoro or ElevenLabs,
|
|
334
334
|
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.27.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",
|