@supersuit/artifacts 0.45.0 → 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 +40 -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/reader/page-menu.d.ts +3 -1
- package/lib/reader/page-menu.js +3 -2
- package/lib/routes/artifacts.d.ts +8 -0
- package/lib/routes/artifacts.js +27 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,46 @@
|
|
|
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
|
+
|
|
27
|
+
## 0.46.1
|
|
28
|
+
|
|
29
|
+
0.46.0 republished: its tag's publish run refused on two settings-menu tests that still listed the
|
|
30
|
+
owner's items without "View as a reader". The tests now expect it; no code changed. Everything in
|
|
31
|
+
0.46.0 below ships in this version.
|
|
32
|
+
|
|
33
|
+
## 0.46.0
|
|
34
|
+
|
|
35
|
+
**View as a reader.** The owner and admins get "View as a reader" in the page menu: the page reloads
|
|
36
|
+
at `?as=reader`, rendered for no one signed in, so the owner sees exactly what the people they send it
|
|
37
|
+
to see (no version number or Version history unless the page shows them, no Copy the markdown, no
|
|
38
|
+
owner menu), with a bar at the top reading "Viewing as a reader · Back to your view". A preview is not
|
|
39
|
+
counted as a view. Anyone else asking for `?as=reader` gets the page they already get, byte for byte.
|
|
40
|
+
Not yet on `access:` pages, which keep rendering for the signed-in owner.
|
|
41
|
+
|
|
42
|
+
- DETECTOR: signed in as the owner, open the page menu. Before, no such item.
|
|
43
|
+
- REMEDY: update the dependency.
|
|
44
|
+
- Tests: routes/page-menu.test.tsx, "view as a reader" (3) and the owner's item list.
|
|
45
|
+
|
|
6
46
|
## 0.45.0
|
|
7
47
|
|
|
8
48
|
**The Comments button shows the comments.** Pressing it now opens a list of every comment on 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;
|
|
@@ -19,6 +19,8 @@ export type PageMenuProps = {
|
|
|
19
19
|
signIn: string | null;
|
|
20
20
|
/** This host's sign-out link, when signed in. */
|
|
21
21
|
signOut: string | null;
|
|
22
|
+
/** The owner's or an admin's link to this page as a reader sees it (`?as=reader`). Null for anyone else. */
|
|
23
|
+
viewAsReader?: string | null;
|
|
22
24
|
/** Every version, when this reader may see past versions and there is more than one. */
|
|
23
25
|
history: {
|
|
24
26
|
items: HistoryItem[];
|
|
@@ -34,5 +36,5 @@ export type PageMenuProps = {
|
|
|
34
36
|
/** The menu's first line: who this reader is to this page. */
|
|
35
37
|
export declare function roleLine(role: PageRole, name: string | null, shutOut?: boolean): string;
|
|
36
38
|
/** The items this reader gets, in order. Exported so the server markup and tests can name them. */
|
|
37
|
-
export declare function menuItems(p: Pick<PageMenuProps, 'share' | 'comments' | 'signIn' | 'signOut' | 'history' | 'markdown' | 'settings'>): string[];
|
|
39
|
+
export declare function menuItems(p: Pick<PageMenuProps, 'share' | 'comments' | 'signIn' | 'signOut' | 'history' | 'markdown' | 'settings' | 'viewAsReader'>): string[];
|
|
38
40
|
export declare function PageMenu(props: PageMenuProps): import("react").JSX.Element;
|
package/lib/reader/page-menu.js
CHANGED
|
@@ -41,6 +41,7 @@ export function menuItems(p) {
|
|
|
41
41
|
p.settings && p.settings.comments !== null ? 'comment-settings' : null,
|
|
42
42
|
p.history ? 'history' : null,
|
|
43
43
|
p.markdown !== null ? 'markdown' : null,
|
|
44
|
+
p.viewAsReader ? 'view-as-reader' : null,
|
|
44
45
|
p.signOut ? 'sign-out' : p.signIn ? 'sign-in' : null,
|
|
45
46
|
].filter((x) => !!x);
|
|
46
47
|
}
|
|
@@ -79,7 +80,7 @@ async function copyText(text) {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
export function PageMenu(props) {
|
|
82
|
-
const { role, name, shutOut, share, comments, signIn, signOut, history, markdown, settings } = props;
|
|
83
|
+
const { role, name, shutOut, share, comments, signIn, signOut, history, markdown, settings, viewAsReader } = props;
|
|
83
84
|
const items = menuItems(props);
|
|
84
85
|
const [mounted, setMounted] = useState(false);
|
|
85
86
|
const [open, setOpen] = useState(false);
|
|
@@ -197,6 +198,6 @@ export function PageMenu(props) {
|
|
|
197
198
|
color: 'var(--a-ink, #111)', background: 'color-mix(in srgb, var(--a-ground, #fff) 82%, transparent)',
|
|
198
199
|
border: '1px solid var(--a-line, rgba(127,127,127,0.3))', boxShadow: '0 1px 4px rgba(0,0,0,0.12)',
|
|
199
200
|
backdropFilter: 'blur(8px)', WebkitBackdropFilter: 'blur(8px)',
|
|
200
|
-
}, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", "aria-hidden": true, focusable: "false", children: [_jsx("circle", { cx: "3.5", cy: "9", r: "1.6", fill: "currentColor" }), _jsx("circle", { cx: "9", cy: "9", r: "1.6", fill: "currentColor" }), _jsx("circle", { cx: "14.5", cy: "9", r: "1.6", fill: "currentColor" })] }) }), open && phone ? _jsx("div", { "aria-hidden": true, "data-page-menu-scrim": true, style: { position: 'fixed', inset: 0, zIndex: 59, background: 'rgba(0,0,0,0.28)' } }) : null, open ? (_jsxs("div", { ref: menu, "data-page-menu": true, role: "menu", "aria-label": "Page menu", style: panelStyle, children: [_jsx("p", { "data-page-menu-role": true, style: { margin: 0, padding: phone ? '10px 12px 8px' : '8px 10px 6px', fontSize: 12, lineHeight: 1.35, opacity: 0.7, overflowWrap: 'anywhere' }, children: line }), items.length ? _jsx("div", { "aria-hidden": true, style: { height: 1, margin: '0 6px 4px', background: 'var(--a-line, rgba(127,127,127,0.3))' } }) : null, share ? (_jsxs("button", { type: "button", role: "menuitem", "data-page-menu-item": "share", onClick: () => void onShare(), style: itemStyle, children: [said === 'Link copied' || said === 'Could not copy the link' ? said : 'Share', share.note ? _jsx("span", { "data-page-menu-note": true, style: sub, children: share.note }) : null] })) : null, comments ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "comments", onClick: onPins, style: itemStyle, children: pins ? 'Hide comments' : 'Show comments' })) : null, settings ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "share-settings", "aria-haspopup": "dialog", onClick: () => onSheet('share'), style: itemStyle, children: "Share settings" })) : null, settings && settings.comments !== null ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "comment-settings", "aria-haspopup": "dialog", onClick: () => onSheet('comments'), style: itemStyle, children: "Comment settings" })) : null, history ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "history", "aria-haspopup": "dialog", onClick: onHistory, style: itemStyle, children: "Version history" })) : null, markdown !== null ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "markdown", onClick: () => void onMarkdown(), style: itemStyle, children: said === 'Markdown copied' || said === 'Could not copy the markdown' ? said : 'Copy the markdown' })) : null, signOut ? (_jsx("a", { role: "menuitem", "data-page-menu-item": "sign-out", href: signOut, style: itemStyle, children: "Sign out" })) : signIn ? (_jsx("a", { role: "menuitem", "data-page-menu-item": "sign-in", href: signIn, onClick: (e) => { e.currentTarget.href = signInBack(signIn); }, style: itemStyle, children: "Sign in" })) : null, _jsx("span", { role: "status", "aria-live": "polite", style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }, children: said ?? '' })] })) : null, sheet && settings ? (_jsx(SettingsSheet, { kind: sheet.kind, settings: settings, saved: sheet.saved, phone: phone, vars: vars, onCopy: copyText, onClose: () => { setSheet(null); button.current?.focus(); } })) : null, historyOpen && history ? (_jsx(HistoryPanel, { items: history.items, base: history.base, viewing: history.viewing, anchor: button, onClose: () => { setHistoryOpen(false); button.current?.focus(); }, onOutside: () => setHistoryOpen(false) })) : null] })) : null;
|
|
201
|
+
}, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 18 18", "aria-hidden": true, focusable: "false", children: [_jsx("circle", { cx: "3.5", cy: "9", r: "1.6", fill: "currentColor" }), _jsx("circle", { cx: "9", cy: "9", r: "1.6", fill: "currentColor" }), _jsx("circle", { cx: "14.5", cy: "9", r: "1.6", fill: "currentColor" })] }) }), open && phone ? _jsx("div", { "aria-hidden": true, "data-page-menu-scrim": true, style: { position: 'fixed', inset: 0, zIndex: 59, background: 'rgba(0,0,0,0.28)' } }) : null, open ? (_jsxs("div", { ref: menu, "data-page-menu": true, role: "menu", "aria-label": "Page menu", style: panelStyle, children: [_jsx("p", { "data-page-menu-role": true, style: { margin: 0, padding: phone ? '10px 12px 8px' : '8px 10px 6px', fontSize: 12, lineHeight: 1.35, opacity: 0.7, overflowWrap: 'anywhere' }, children: line }), items.length ? _jsx("div", { "aria-hidden": true, style: { height: 1, margin: '0 6px 4px', background: 'var(--a-line, rgba(127,127,127,0.3))' } }) : null, share ? (_jsxs("button", { type: "button", role: "menuitem", "data-page-menu-item": "share", onClick: () => void onShare(), style: itemStyle, children: [said === 'Link copied' || said === 'Could not copy the link' ? said : 'Share', share.note ? _jsx("span", { "data-page-menu-note": true, style: sub, children: share.note }) : null] })) : null, comments ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "comments", onClick: onPins, style: itemStyle, children: pins ? 'Hide comments' : 'Show comments' })) : null, settings ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "share-settings", "aria-haspopup": "dialog", onClick: () => onSheet('share'), style: itemStyle, children: "Share settings" })) : null, settings && settings.comments !== null ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "comment-settings", "aria-haspopup": "dialog", onClick: () => onSheet('comments'), style: itemStyle, children: "Comment settings" })) : null, history ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "history", "aria-haspopup": "dialog", onClick: onHistory, style: itemStyle, children: "Version history" })) : null, markdown !== null ? (_jsx("button", { type: "button", role: "menuitem", "data-page-menu-item": "markdown", onClick: () => void onMarkdown(), style: itemStyle, children: said === 'Markdown copied' || said === 'Could not copy the markdown' ? said : 'Copy the markdown' })) : null, viewAsReader ? (_jsx("a", { role: "menuitem", "data-page-menu-item": "view-as-reader", href: viewAsReader, style: itemStyle, children: "View as a reader" })) : null, signOut ? (_jsx("a", { role: "menuitem", "data-page-menu-item": "sign-out", href: signOut, style: itemStyle, children: "Sign out" })) : signIn ? (_jsx("a", { role: "menuitem", "data-page-menu-item": "sign-in", href: signIn, onClick: (e) => { e.currentTarget.href = signInBack(signIn); }, style: itemStyle, children: "Sign in" })) : null, _jsx("span", { role: "status", "aria-live": "polite", style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }, children: said ?? '' })] })) : null, sheet && settings ? (_jsx(SettingsSheet, { kind: sheet.kind, settings: settings, saved: sheet.saved, phone: phone, vars: vars, onCopy: copyText, onClose: () => { setSheet(null); button.current?.focus(); } })) : null, historyOpen && history ? (_jsx(HistoryPanel, { items: history.items, base: history.base, viewing: history.viewing, anchor: button, onClose: () => { setHistoryOpen(false); button.current?.focus(); }, onOutside: () => setHistoryOpen(false) })) : null] })) : null;
|
|
201
202
|
return (_jsxs(_Fragment, { children: [_jsx("span", { ref: marker, hidden: true, "data-page-menu-for": role, "data-page-menu-items": items.join(' '), children: history ? _jsx(VersionList, { items: history.items, base: history.base, marker: true }) : null }), ui ? createPortal(ui, document.body) : null] }));
|
|
202
203
|
}
|
|
@@ -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
|
|
@@ -126,6 +133,7 @@ type PageProps = Params & {
|
|
|
126
133
|
searchParams?: Promise<{
|
|
127
134
|
key?: string | string[];
|
|
128
135
|
r?: string | string[];
|
|
136
|
+
as?: string | string[];
|
|
129
137
|
}>;
|
|
130
138
|
};
|
|
131
139
|
type VersionParams = {
|
package/lib/routes/artifacts.js
CHANGED
|
@@ -62,6 +62,14 @@ async function defaultReadHeader(name) {
|
|
|
62
62
|
const { headers } = await import('next/headers');
|
|
63
63
|
return (await headers()).get(name) ?? '';
|
|
64
64
|
}
|
|
65
|
+
/** `?as=reader` on a page's address: the owner's preview of the page as a reader sees it. */
|
|
66
|
+
function wantsReaderView(sp) {
|
|
67
|
+
return (Array.isArray(sp.as) ? sp.as[0] : sp.as) === 'reader';
|
|
68
|
+
}
|
|
69
|
+
/** The owner's bar while previewing as a reader. Never spoken, never on a reader's page. */
|
|
70
|
+
function ViewingAsReader({ back }) {
|
|
71
|
+
return (_jsx("div", { "data-viewing-as-reader": true, "data-nospeak": true, className: "fixed inset-x-0 top-3 z-50 flex justify-center px-3", children: _jsxs("p", { className: "m-0 rounded-full border border-[color:var(--a-line-strong)] bg-[color:var(--a-surface)] px-4 py-1.5 font-sans text-xs text-[color:var(--a-strong)] shadow-lg", children: ["Viewing as a reader \u00B7 ", _jsx("a", { href: back, className: "underline", children: "Back to your view" })] }) }));
|
|
72
|
+
}
|
|
65
73
|
export function createArtifactRoutes(config) {
|
|
66
74
|
const { store, siteUrl } = config;
|
|
67
75
|
// The pack as configured draws the share card; the page paints with its theme variables, so a
|
|
@@ -84,6 +92,11 @@ export function createArtifactRoutes(config) {
|
|
|
84
92
|
// The version in the URL is what makes a re-publish show up: every unfurler caches by URL.
|
|
85
93
|
const signInOrigin = config.signInOrigin;
|
|
86
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 };
|
|
87
100
|
const ownerEmail = config.ownerEmail?.trim().toLowerCase();
|
|
88
101
|
const admins = new Set([ownerEmail, ...(config.admins ?? []).map((e) => e.trim().toLowerCase())].filter((e) => !!e));
|
|
89
102
|
/** Whether this reader may see this page's version number and past versions: the page's own
|
|
@@ -236,7 +249,7 @@ export function createArtifactRoutes(config) {
|
|
|
236
249
|
: a.password
|
|
237
250
|
? (admin ? { url: `${pageUrl(a.id)}?key=${encodeURIComponent(a.password)}`, title: a.title, note: 'The link carries the password.' } : null)
|
|
238
251
|
: { url: pageUrl(a.id), title: a.title };
|
|
239
|
-
return (_jsx(PageMenu, { role: role, name: name, shutOut: shutOut, share: share, comments: !!comments, signIn: !reader && signInOrigin ? signInUrl(signInOrigin, pageUrl(a.id)) : null, signOut: reader ? signOutUrl(a.id) : null, history: history && history.length > 1 && seesHistory(a, reader) && viewing ? { items: history, base: pagePath(a.id), viewing } : null, markdown: admin && source ? source : null, settings: admin ? settings ?? null : null }));
|
|
252
|
+
return (_jsx(PageMenu, { role: role, name: name, shutOut: shutOut, share: share, comments: !!comments, signIn: !reader && signInOrigin ? signInUrl(signInOrigin, pageUrl(a.id)) : null, signOut: reader ? signOutUrl(a.id) : null, history: history && history.length > 1 && seesHistory(a, reader) && viewing ? { items: history, base: pagePath(a.id), viewing } : null, markdown: admin && source ? source : null, settings: admin ? settings ?? null : null, viewAsReader: admin && source ? `${pagePath(a.id)}?as=reader` : null }));
|
|
240
253
|
}
|
|
241
254
|
/** The kicker over every title: the pack's line, then who the page is for. Never spoken: the
|
|
242
255
|
* whole paragraph is data-nospeak, and narrationText has no `to` to read. */
|
|
@@ -320,8 +333,9 @@ export function createArtifactRoutes(config) {
|
|
|
320
333
|
const me = readerSecret() ? verifyGrant(readerSecret(), await readCookie(GRANT_COOKIE).catch(() => undefined)) : null;
|
|
321
334
|
if (isAdmin(me)) {
|
|
322
335
|
const markdown = await media(a.markdown);
|
|
323
|
-
const
|
|
324
|
-
|
|
336
|
+
const viewer = wantsReaderView(sp) ? null : me;
|
|
337
|
+
const page = broadcast ? await BroadcastCard({ ...a, markdown }, { reader: viewer, bot }) : await Body({ ...a, markdown }, { top: true, reader: viewer });
|
|
338
|
+
return _jsxs(BrandGround, { pack: pack, mode: a.theme, children: [viewer ? null : _jsx(ViewingAsReader, { back: pagePath(id) }), page] });
|
|
325
339
|
}
|
|
326
340
|
const secret = usableSecret(config.recipientSecret?.());
|
|
327
341
|
const g = await perReaderGate({ id, param, cookie: await readCookie(ridCookieName(id)).catch(() => undefined), secret, recipients: config.recipients });
|
|
@@ -367,10 +381,14 @@ export function createArtifactRoutes(config) {
|
|
|
367
381
|
if (!open) {
|
|
368
382
|
return (_jsxs(BrandGround, { pack: pack, mode: a.theme, children: [_jsx(Menu, { a: a, reader: reader, settings: await settingsFor(a, reader) }), _jsxs("div", { className: "mx-auto max-w-2xl px-6 pt-24 pb-8 text-center sm:pt-28", 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 })] }), _jsx(ArtifactDoor, { brand: brand, wrongKey: key !== undefined })] }));
|
|
369
383
|
}
|
|
370
|
-
|
|
371
|
-
|
|
384
|
+
// The owner or an admin previewing the page as the people it is sent to see it (`?as=reader`):
|
|
385
|
+
// rendered for no one signed in, with a bar back to their own view. Anyone else asking gets the
|
|
386
|
+
// page they already get.
|
|
387
|
+
const asReader = isAdmin(reader) && wantsReaderView(sp);
|
|
388
|
+
const page = await render(true, asReader ? null : reader);
|
|
389
|
+
if (n === null && !asReader)
|
|
372
390
|
void store.bumpViews(id);
|
|
373
|
-
return (_jsxs(BrandGround, { pack: pack, mode: a.theme, children: [remember ? _jsx("script", { dangerouslySetInnerHTML: { __html: remember } }) : null, page] }));
|
|
391
|
+
return (_jsxs(BrandGround, { pack: pack, mode: a.theme, children: [remember ? _jsx("script", { dangerouslySetInnerHTML: { __html: remember } }) : null, asReader ? _jsx(ViewingAsReader, { back: n === null ? pagePath(id) : `${pagePath(id)}/v/${n}` }) : null, page] }));
|
|
374
392
|
}
|
|
375
393
|
/** A page with `access:`. The body is rendered only after the reader is known and allowed;
|
|
376
394
|
* everyone else gets the title, the summary, and a door. */
|
|
@@ -452,7 +470,7 @@ export function createArtifactRoutes(config) {
|
|
|
452
470
|
const title = v.title ?? a.title;
|
|
453
471
|
const summary = v.summary ?? a.summary;
|
|
454
472
|
const toc = showToc(v.markdown, a.toc) ? tocOf(v.markdown) : [];
|
|
455
|
-
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 }) })] }));
|
|
456
474
|
}
|
|
457
475
|
/** A broadcast: one message card. The sender's words at the top, then the media (a video with
|
|
458
476
|
* sound and controls, photos, a voice note's player, link cards) in the order the words put
|
|
@@ -467,7 +485,7 @@ export function createArtifactRoutes(config) {
|
|
|
467
485
|
const cover = !bot && a.cover && !/!\[[^\]]*\]\(/.test(a.markdown) ? a.cover : null;
|
|
468
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 ? (
|
|
469
487
|
// eslint-disable-next-line @next/next/no-img-element
|
|
470
|
-
_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] }));
|
|
471
489
|
}
|
|
472
490
|
/** The page itself, shared by open and gated pages. */
|
|
473
491
|
async function Body(a, { top, reader, perReader }) {
|
|
@@ -494,7 +512,7 @@ export function createArtifactRoutes(config) {
|
|
|
494
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] }));
|
|
495
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,
|
|
496
514
|
// Ticks are kept in the reader's browser on any host; Send needs one that keeps answers.
|
|
497
|
-
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 }) }));
|
|
498
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,
|
|
499
517
|
2026-09-29: "NEVER PUT DEFAULT SAFARI VOICE DICTATION"); the audio is Kokoro or ElevenLabs,
|
|
500
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",
|