@supersuit/artifacts 0.38.0 → 0.39.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 +37 -0
- package/lib/artifacts/embed-frame.d.ts +5 -0
- package/lib/artifacts/embed-frame.js +23 -0
- package/lib/artifacts/index.d.ts +1 -1
- package/lib/artifacts/index.js +1 -1
- package/lib/artifacts/render.js +14 -1
- package/lib/artifacts/simulator.d.ts +7 -0
- package/lib/artifacts/simulator.js +32 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,43 @@
|
|
|
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.39.0
|
|
7
|
+
|
|
8
|
+
**A session demo's frame fits its player, and is wider than the reading column on a wide screen.**
|
|
9
|
+
A ```` ```simulator ```` block drew its player in a frame of one fixed height (`height:`, 720px
|
|
10
|
+
unless the block said). A session demo draws a different device for each viewer, a short laptop on
|
|
11
|
+
a desktop and a tall phone on a phone, so one height suited neither: on a desktop a band of
|
|
12
|
+
nothing, some 300px, sat between the laptop and the caption.
|
|
13
|
+
|
|
14
|
+
- The frame now asks its embed page `fit=content` (the address gains `&fit=content`), and a page
|
|
15
|
+
that understands posts `{ type: 'embed:fit', height: <px> }` to its parent. The frame takes that
|
|
16
|
+
height (`EmbedFrame`, a client component; `fitFrom`, `FIT_MESSAGE`): only that message, only from
|
|
17
|
+
the frame's own window and the origin of the page it loaded, held between 240 and 2000px.
|
|
18
|
+
- `height:` still means what it meant: the height the frame opens at, and so the room the player is
|
|
19
|
+
given. A page that posts nothing keeps it. No published page needs a change.
|
|
20
|
+
- From 1024px a framed session demo is up to 64rem wide, centred on the column and clear of the
|
|
21
|
+
contents rail in the left margin from 1280px, with its caption centred under it. In a broadcast's
|
|
22
|
+
card, below 1024px, for the Messages phone, and for a caption with no player, the column is the
|
|
23
|
+
width, as before.
|
|
24
|
+
|
|
25
|
+
DETECTOR: open a page with a ```` ```simulator ```` block on a desktop. Affected: the frame is the
|
|
26
|
+
block's height whatever the device in it, with empty space under a laptop. Or
|
|
27
|
+
`curl -s <page> | grep -o 'simulator/embed[^"]*'` shows no `fit=content`.
|
|
28
|
+
|
|
29
|
+
REMEDY: take 0.39.0. A host whose embed page is not its own does nothing more: an unknown query key
|
|
30
|
+
is ignored and the frame keeps the block's height. A host that serves its own embed page makes it
|
|
31
|
+
answer `fit=content` in two steps, in this order: hold the player at the height the frame opened
|
|
32
|
+
with (inside a frame the viewport IS the frame, so a player sized from it shrinks as the frame
|
|
33
|
+
does and the two chase each other), then post its content's height as `embed:fit` whenever it
|
|
34
|
+
changes. Centre the player in the frame as well, for hosts that never listen.
|
|
35
|
+
|
|
36
|
+
Tests: `simulator.test.ts` (the address asks to fit and the Messages one does not; `fitFrom` takes
|
|
37
|
+
the message from the right origin, refuses every other shape, and clamps), `embed-frame.test.tsx`
|
|
38
|
+
(the frame opens at the block's height, takes a reported one, and ignores another window, origin
|
|
39
|
+
and message), `render.test.tsx` (the opening height with and without `height:`; the wide rule is on
|
|
40
|
+
a framed session demo only, never the Messages phone or a caption with no player). The wide rule
|
|
41
|
+
itself is CSS and is not run by a test here: it was checked in a browser at 390, 1100 and 1440px.
|
|
42
|
+
|
|
6
43
|
## 0.38.0
|
|
7
44
|
|
|
8
45
|
**Readers' voice memos live in the private bucket.** A memo left on a page (`comments/<memo>`, or a
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// The frame an embed block plays in. It opens at the block's height and, when its embed page
|
|
4
|
+
// reports how tall the player is (simulator.ts, "A frame that fits"), takes that height, so a
|
|
5
|
+
// short device has no empty band under it and a tall one is not cut off.
|
|
6
|
+
import { useEffect, useRef, useState } from 'react';
|
|
7
|
+
import { fitFrom } from './simulator.js';
|
|
8
|
+
export function EmbedFrame({ src, title, height }) {
|
|
9
|
+
const ref = useRef(null);
|
|
10
|
+
const [fit, setFit] = useState(null);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const onMessage = (e) => {
|
|
13
|
+
if (!ref.current || e.source !== ref.current.contentWindow)
|
|
14
|
+
return;
|
|
15
|
+
const h = fitFrom(e.data, e.origin, src);
|
|
16
|
+
if (h != null)
|
|
17
|
+
setFit(h);
|
|
18
|
+
};
|
|
19
|
+
window.addEventListener('message', onMessage);
|
|
20
|
+
return () => window.removeEventListener('message', onMessage);
|
|
21
|
+
}, [src]);
|
|
22
|
+
return (_jsx("iframe", { ref: ref, src: src, title: title, loading: "lazy", allow: "microphone; autoplay; fullscreen", className: "block w-full rounded-xl border border-[color:var(--a-line)]", style: { height: fit != null ? `${fit}px` : height, border: 0 } }));
|
|
23
|
+
}
|
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, parseEmbedFence, embedSrc, EMBED_KINDS, type SimulatorBlock, type EmbedBlock, type EmbedKind } from './simulator.js';
|
|
16
|
+
export { parseSimulatorFence, simulatorSrc, parseEmbedFence, embedSrc, fitFrom, FIT_MESSAGE, 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 { createResolveDigests, noticesFor, composeDigest, resolverName, itemsOf, emailKey, burstKey, stopToken, verifyStopToken, maskEmail, RESOLVE_DIGEST_QUIET_MS, RESOLVE_DIGEST_MAX_WAIT_MS, RESOLVE_DIGEST_LEASE_MS, RESOLVE_DIGEST_MAX_ATTEMPTS, RESOLVE_DIGEST_STOP_PATH, DIGEST_BODY_CHARS, DIGEST_QUOTE_CHARS, type ResolveEvent, type DigestItem, type Burst, type Digest, type DigestStatus, type ResolveDigestStore, type DigestMessage, type DigestSender, type ResolveDigestOptions, type ComposeOptions, type FlushReport } from './resolve-digest.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, parseEmbedFence, embedSrc, EMBED_KINDS } from './simulator.js';
|
|
16
|
+
export { parseSimulatorFence, simulatorSrc, parseEmbedFence, embedSrc, fitFrom, FIT_MESSAGE, 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 { createResolveDigests, noticesFor, composeDigest, resolverName, itemsOf, emailKey, burstKey, stopToken, verifyStopToken, maskEmail, RESOLVE_DIGEST_QUIET_MS, RESOLVE_DIGEST_MAX_WAIT_MS, RESOLVE_DIGEST_LEASE_MS, RESOLVE_DIGEST_MAX_ATTEMPTS, RESOLVE_DIGEST_STOP_PATH, DIGEST_BODY_CHARS, DIGEST_QUOTE_CHARS } from './resolve-digest.js';
|
package/lib/artifacts/render.js
CHANGED
|
@@ -17,6 +17,7 @@ import { blocksOf, checklistSyncs, checklistsOf, hasNotesWidget, headingsOf } fr
|
|
|
17
17
|
import { HeadingNotes, NoteToggle, NotesEarlier, NotesProvider } from '../widgets/notes.js';
|
|
18
18
|
import { Checklist } from '../widgets/checklist.js';
|
|
19
19
|
import { embedSrc, parseEmbedFence } from './simulator.js';
|
|
20
|
+
import { EmbedFrame } from './embed-frame.js';
|
|
20
21
|
import { VideoAutoplay } from '../reader/video-autoplay.js';
|
|
21
22
|
import { CodeCopy } from '../reader/code-copy.js';
|
|
22
23
|
import { DefinitionLayer } from '../reader/definition-layer.js';
|
|
@@ -110,6 +111,17 @@ function InlineAudio({ src, label }) {
|
|
|
110
111
|
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>`;
|
|
111
112
|
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 } })] }));
|
|
112
113
|
}
|
|
114
|
+
// A session demo on a wide screen is wider than the reading column, so the device it draws (a
|
|
115
|
+
// laptop, or a phone beside what was made) has room: up to 64rem, centred on the column, and clear
|
|
116
|
+
// of the contents rail fixed in the left margin from 1280px (w-56 at left-6, w-64 at left-12).
|
|
117
|
+
// Below 1024px, in a broadcast's card, for the Messages phone, and for a caption with no player
|
|
118
|
+
// over it (a host that names no embed page), the column is the width.
|
|
119
|
+
const WIDE = '[data-artifact-embed="simulator"][data-wide]';
|
|
120
|
+
const WIDE_EMBED_CSS = `@media (min-width:1024px){${WIDE}{width:max(100%,min(64rem,calc(100vw - 4rem)));margin-left:50%;transform:translateX(-50%)}` +
|
|
121
|
+
`${WIDE} figcaption{text-align:center}` +
|
|
122
|
+
`[data-broadcast] ${WIDE}{width:auto;margin-left:0;transform:none}[data-broadcast] ${WIDE} figcaption{text-align:inherit}}` +
|
|
123
|
+
`@media (min-width:1280px){${WIDE}{width:max(100%,min(64rem,calc(100vw - 36rem)))}}` +
|
|
124
|
+
`@media (min-width:1536px){${WIDE}{width:max(100%,min(64rem,calc(100vw - 40rem)))}}`;
|
|
113
125
|
/** An embed block (```simulator, ```messages): the player in a frame when the host names one, its
|
|
114
126
|
* caption alone when it does not, and nothing when the block does not parse (a page published
|
|
115
127
|
* before validation existed). */
|
|
@@ -118,7 +130,8 @@ function EmbedFence({ kind, source, ctx }) {
|
|
|
118
130
|
if (!r.ok)
|
|
119
131
|
return null;
|
|
120
132
|
const src = ctx ? embedSrc(kind, ctx.embed, r.block.value) : null;
|
|
121
|
-
|
|
133
|
+
const wide = kind === 'simulator' && src != null;
|
|
134
|
+
return (_jsxs("figure", { "data-artifact-embed": kind, ...(kind === 'simulator' ? { 'data-artifact-simulator': '' } : {}), ...(wide ? { 'data-wide': '' } : {}), className: "my-8", children: [wide ? _jsx("style", { children: WIDE_EMBED_CSS }) : null, src ? _jsx(EmbedFrame, { src: src, title: r.block.caption, height: r.block.height }) : null, _jsx("figcaption", { className: "mt-2 text-sm opacity-80", children: r.block.caption })] }));
|
|
122
135
|
}
|
|
123
136
|
/** The one image a paragraph holds and nothing else (whitespace aside), or null. */
|
|
124
137
|
function soleImage(node) {
|
|
@@ -32,3 +32,10 @@ export declare function parseSimulatorFence(value: string, line: number): {
|
|
|
32
32
|
error: string;
|
|
33
33
|
};
|
|
34
34
|
export declare const simulatorSrc: (embed: string, scenario: string) => string | null;
|
|
35
|
+
/** The message an embed page sends its frame, and the heights a frame will take. */
|
|
36
|
+
export declare const FIT_MESSAGE = "embed:fit";
|
|
37
|
+
export declare const FIT_MIN = 240;
|
|
38
|
+
export declare const FIT_MAX = 2000;
|
|
39
|
+
/** The height (px) a frame takes from a message, or null: only `embed:fit` with a finite number,
|
|
40
|
+
* only from the origin of the page the frame loaded, and never outside FIT_MIN..FIT_MAX. */
|
|
41
|
+
export declare function fitFrom(data: unknown, origin: string, src: string): number | null;
|
|
@@ -11,8 +11,16 @@
|
|
|
11
11
|
// (ArtifactRoutesConfig.simulatorEmbed / messagesEmbed), 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
|
+
//
|
|
15
|
+
// A FRAME THAT FITS (0.39.0). `height:` is the height a frame OPENS at, and the room its player is
|
|
16
|
+
// given. A session demo draws a different device for each viewer (a short laptop on a desktop, a
|
|
17
|
+
// tall phone on a phone), so no one height suits both: the frame asked its embed page `fit=content`,
|
|
18
|
+
// and a page that understands posts `{ type: 'embed:fit', height: <px> }` to its parent, which the
|
|
19
|
+
// frame then takes (EmbedFrame). A page that posts nothing keeps the block's height. The embed
|
|
20
|
+
// page must hold its player's size once asked, never size it from its own viewport, or the two
|
|
21
|
+
// chase each other; that is why the frame listens only for this message and for no player's own.
|
|
14
22
|
const KINDS = {
|
|
15
|
-
simulator: { key: 'scenario', required: true, fixed: { timeline: 'off' }, height: '720px' },
|
|
23
|
+
simulator: { key: 'scenario', required: true, fixed: { timeline: 'off', fit: 'content' }, height: '720px' },
|
|
16
24
|
messages: { key: 'chat', required: false, fixed: {}, height: '760px' },
|
|
17
25
|
};
|
|
18
26
|
export const EMBED_KINDS = Object.keys(KINDS);
|
|
@@ -74,3 +82,26 @@ export function parseSimulatorFence(value, line) {
|
|
|
74
82
|
return r.ok ? { ok: true, block: { scenario: r.block.value, caption: r.block.caption, height: r.block.height } } : r;
|
|
75
83
|
}
|
|
76
84
|
export const simulatorSrc = (embed, scenario) => embedSrc('simulator', embed, scenario);
|
|
85
|
+
/** The message an embed page sends its frame, and the heights a frame will take. */
|
|
86
|
+
export const FIT_MESSAGE = 'embed:fit';
|
|
87
|
+
export const FIT_MIN = 240;
|
|
88
|
+
export const FIT_MAX = 2000;
|
|
89
|
+
/** The height (px) a frame takes from a message, or null: only `embed:fit` with a finite number,
|
|
90
|
+
* only from the origin of the page the frame loaded, and never outside FIT_MIN..FIT_MAX. */
|
|
91
|
+
export function fitFrom(data, origin, src) {
|
|
92
|
+
if (!data || typeof data !== 'object')
|
|
93
|
+
return null;
|
|
94
|
+
const d = data;
|
|
95
|
+
if (d.type !== FIT_MESSAGE || typeof d.height !== 'number' || !Number.isFinite(d.height))
|
|
96
|
+
return null;
|
|
97
|
+
let expected;
|
|
98
|
+
try {
|
|
99
|
+
expected = new URL(src).origin;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (origin !== expected)
|
|
105
|
+
return null;
|
|
106
|
+
return Math.max(FIT_MIN, Math.min(FIT_MAX, Math.ceil(d.height)));
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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",
|