@supersuit/artifacts 0.44.3 → 0.46.1
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 +42 -10
- package/lib/reader/comment-layer.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 +1 -0
- package/lib/routes/artifacts.js +19 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,48 @@
|
|
|
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.46.1
|
|
7
|
+
|
|
8
|
+
0.46.0 republished: its tag's publish run refused on two settings-menu tests that still listed the
|
|
9
|
+
owner's items without "View as a reader". The tests now expect it; no code changed. Everything in
|
|
10
|
+
0.46.0 below ships in this version.
|
|
11
|
+
|
|
12
|
+
## 0.46.0
|
|
13
|
+
|
|
14
|
+
**View as a reader.** The owner and admins get "View as a reader" in the page menu: the page reloads
|
|
15
|
+
at `?as=reader`, rendered for no one signed in, so the owner sees exactly what the people they send it
|
|
16
|
+
to see (no version number or Version history unless the page shows them, no Copy the markdown, no
|
|
17
|
+
owner menu), with a bar at the top reading "Viewing as a reader · Back to your view". A preview is not
|
|
18
|
+
counted as a view. Anyone else asking for `?as=reader` gets the page they already get, byte for byte.
|
|
19
|
+
Not yet on `access:` pages, which keep rendering for the signed-in owner.
|
|
20
|
+
|
|
21
|
+
- DETECTOR: signed in as the owner, open the page menu. Before, no such item.
|
|
22
|
+
- REMEDY: update the dependency.
|
|
23
|
+
- Tests: routes/page-menu.test.tsx, "view as a reader" (3) and the owner's item list.
|
|
24
|
+
|
|
25
|
+
## 0.45.0
|
|
26
|
+
|
|
27
|
+
**The Comments button shows the comments.** Pressing it now opens a list of every comment on the
|
|
28
|
+
page, in page order with open threads first and resolved ones after: the quoted words, the first
|
|
29
|
+
lines of the comment, and who left it. Tapping one closes the list, scrolls to the comment and
|
|
30
|
+
opens its thread, the same jump a `#comment-<id>` link makes. A comment on an earlier version
|
|
31
|
+
jumps to its entry in "Comments on earlier versions". The help line and the Hide comments toggle
|
|
32
|
+
stay below the list.
|
|
33
|
+
|
|
34
|
+
- DETECTOR: on a page with comments, press "Comments · N". Before, only the help text showed.
|
|
35
|
+
- REMEDY: update the dependency.
|
|
36
|
+
- Tests: comment-layer.test.tsx, "the Comments button lists every comment, and a tap opens that thread".
|
|
37
|
+
|
|
38
|
+
## 0.44.3
|
|
39
|
+
|
|
40
|
+
**Escape closes a comment card.** Pressing Escape cancels the comment being written (nothing is
|
|
41
|
+
saved), or closes the open thread, or the comments panel, topmost first. It ignores an Escape
|
|
42
|
+
another layer already handled, so the definition popover and page menu keep theirs.
|
|
43
|
+
|
|
44
|
+
- DETECTOR: select text, press Comment, press Escape. Before, the card stayed open.
|
|
45
|
+
- REMEDY: update the dependency.
|
|
46
|
+
- Tests: comment-layer.test.tsx, "Escape cancels the comment being written, saving nothing".
|
|
47
|
+
|
|
6
48
|
## 0.44.2
|
|
7
49
|
|
|
8
50
|
**An embed's caption no longer throws the read-along out of step.** The caption under a
|
|
@@ -17,16 +59,6 @@ early. A page carrying four embeds ran about sixty words ahead by its end. The e
|
|
|
17
59
|
- Tests: "an embed block is not read aloud" in render.test.tsx, both kinds, with and without a
|
|
18
60
|
player, the wrapped words equal the narrated words.
|
|
19
61
|
|
|
20
|
-
## 0.44.3
|
|
21
|
-
|
|
22
|
-
**Escape closes a comment card.** Pressing Escape cancels the comment being written (nothing is
|
|
23
|
-
saved), or closes the open thread, or the comments panel, topmost first. It ignores an Escape
|
|
24
|
-
another layer already handled, so the definition popover and page menu keep theirs.
|
|
25
|
-
|
|
26
|
-
- DETECTOR: select text, press Comment, press Escape. Before, the card stayed open.
|
|
27
|
-
- REMEDY: update the dependency.
|
|
28
|
-
- Tests: comment-layer.test.tsx, "Escape cancels the comment being written, saving nothing".
|
|
29
|
-
|
|
30
62
|
## 0.44.1
|
|
31
63
|
|
|
32
64
|
**Highlighting text works like any page again.** A press in the gap between paragraphs, in a line's
|
|
@@ -587,11 +587,13 @@ export function CommentLayer({ artifactId, rootId, ownerName, comments, canShare
|
|
|
587
587
|
const open = layout.placed.find((p) => p.item.id === thread);
|
|
588
588
|
const openItem = open?.item ?? layout.earlier.find((i) => i.id === thread);
|
|
589
589
|
const count = tops.filter((i) => !resolvedOf(i)).length;
|
|
590
|
+
const inPageOrder = [...layout.placed.map((p) => p.item).filter((i) => tops.includes(i)), ...layout.earlier.filter((i) => tops.includes(i))];
|
|
591
|
+
const panelList = [...inPageOrder.filter((i) => !resolvedOf(i)), ...inPageOrder.filter((i) => resolvedOf(i))];
|
|
590
592
|
const overlay = mounted ? (_jsxs("div", { "data-comment-ui": true, "data-nospeak": true, className: "font-sans", style: theme, children: [missing ? (_jsxs("div", { "data-comment-missing": true, role: "status", className: "fixed inset-x-3 top-3 z-50 mx-auto max-w-sm rounded-xl border border-[color:var(--a-line-strong)] bg-[color:var(--a-surface)] p-4 font-sans text-sm leading-snug text-[color:var(--a-strong)] shadow-xl", children: [_jsxs("div", { className: "flex items-start gap-3", children: [_jsx("p", { className: "m-0 flex-1", children: "That comment can't be shown to you. Comments on this page are visible to its owner and admins." }), _jsx("button", { type: "button", "aria-label": "Close", onClick: () => setMissing(false), className: "-mr-1 -mt-1 px-1 text-lg leading-none opacity-60", children: "\u00D7" })] }), !signedIn && signIn ? (_jsx("a", { "data-comment-missing-sign-in": true, href: signInBack(signIn), className: "mt-3 inline-block rounded-full px-4 py-1.5 font-medium no-underline", style: { background: accent, color: 'var(--a-on-accent, #111)' }, children: "Sign in" })) : null] })) : null, showPins ? layout.placed.map(({ item, left, top, box }) => (_jsxs("div", { children: [box && !resolvedOf(item) ? (_jsx("div", { "data-comment-box": item.id, "aria-hidden": true, className: "pointer-events-none absolute z-30 rounded-md border-2", style: { left: box.left, top: box.top, width: box.width, height: box.height, borderColor: accent, borderStyle: item.kind === 'personal' ? 'dashed' : 'solid', opacity: thread === item.id ? 1 : 0.55 } })) : null, _jsx(Pin, { item: item, left: left, top: top, accent: accent, ground: ground, onOpen: () => setThread(thread === item.id ? null : item.id) })] }, item.id))) : null, chip && !draft ? (
|
|
591
593
|
// On a phone the button sits at the TOP of the screen, never beside the selection: that is
|
|
592
594
|
// where iOS draws its own Copy / Look Up menu, and the two fought for the same tap (Gary,
|
|
593
595
|
// 2026-09-29: "I keep trying to fight with the Apple pop-up"). A mouse keeps it by the text.
|
|
594
|
-
_jsx("button", { type: "button", "data-comment-chip": true, "data-comment-chip-at": onTouch() ? 'top' : 'selection', className: onTouch() ? 'fixed left-1/2 z-50 -translate-x-1/2 rounded-full px-5 py-2 text-sm font-medium shadow-lg' : 'absolute z-50 -translate-x-full rounded-full px-3 py-1 text-xs font-medium shadow-lg', style: onTouch() ? { top: 'calc(env(safe-area-inset-top, 0px) + 12px)', background: accent, color: 'var(--a-on-accent, #111)' } : { left: chip.x, top: chip.y, background: accent, color: 'var(--a-on-accent, #111)' }, onMouseDown: (e) => e.preventDefault(), onClick: () => { setDraft(chip); setChip(null); }, children: "Comment" })) : null, draft ? (_jsx(Floating, { x: draft.x, y: draft.y, children: _jsx(CommentCard, { ownerName: ownerName, mode: comments, canShare: share, closed: closed, requireName: requireName, knownName: knownName, signedIn: signedIn && onServer, quote: draft.quote, accent: accent, signIn: signIn, voice: host, onSave: (body, share, memo, by) => saveNew(body, share, draft, memo, by), onCancel: () => setDraft(null) }) })) : null, openItem && open ? (_jsx(Floating, { x: open.left + 28, y: open.top, children: _jsx(Thread, { item: openItem, replies: items.filter((i) => i.value.parent === openItem.id), canReply: openItem.kind === 'shared' && share, awaiting: closesAt != null, askName: requireName && !knownName, onJump: jump, canResolve: openItem.kind === 'shared' && isOwner, startExpanded: expanded === openItem.id, accent: accent, audioDb: audioDb, onClose: () => { setThread(null); setExpanded(null); }, onRemove: remove, onEdit: edit, onReply: reply, onResolve: resolveThread }, openItem.id) })) : null, drag ? (_jsx("div", { "aria-hidden": true, className: "pointer-events-none fixed z-40 rounded-md border-2 border-dashed", style: { left: Math.min(drag.x1, drag.x2), top: Math.min(drag.y1, drag.y2), width: Math.abs(drag.x2 - drag.x1), height: Math.abs(drag.y2 - drag.y1), borderColor: accent, background: `color-mix(in srgb, ${accent} 12%, transparent)` } })) : null, _jsxs("div", { className: "fixed right-4 z-40 flex flex-col items-end gap-2", style: { bottom: 'calc(env(safe-area-inset-bottom, 0px) + 5.5rem)' }, children: [panel ? (_jsxs("div", { "data-comment-panel": true, className: "w-
|
|
596
|
+
_jsx("button", { type: "button", "data-comment-chip": true, "data-comment-chip-at": onTouch() ? 'top' : 'selection', className: onTouch() ? 'fixed left-1/2 z-50 -translate-x-1/2 rounded-full px-5 py-2 text-sm font-medium shadow-lg' : 'absolute z-50 -translate-x-full rounded-full px-3 py-1 text-xs font-medium shadow-lg', style: onTouch() ? { top: 'calc(env(safe-area-inset-top, 0px) + 12px)', background: accent, color: 'var(--a-on-accent, #111)' } : { left: chip.x, top: chip.y, background: accent, color: 'var(--a-on-accent, #111)' }, onMouseDown: (e) => e.preventDefault(), onClick: () => { setDraft(chip); setChip(null); }, children: "Comment" })) : null, draft ? (_jsx(Floating, { x: draft.x, y: draft.y, children: _jsx(CommentCard, { ownerName: ownerName, mode: comments, canShare: share, closed: closed, requireName: requireName, knownName: knownName, signedIn: signedIn && onServer, quote: draft.quote, accent: accent, signIn: signIn, voice: host, onSave: (body, share, memo, by) => saveNew(body, share, draft, memo, by), onCancel: () => setDraft(null) }) })) : null, openItem && open ? (_jsx(Floating, { x: open.left + 28, y: open.top, children: _jsx(Thread, { item: openItem, replies: items.filter((i) => i.value.parent === openItem.id), canReply: openItem.kind === 'shared' && share, awaiting: closesAt != null, askName: requireName && !knownName, onJump: jump, canResolve: openItem.kind === 'shared' && isOwner, startExpanded: expanded === openItem.id, accent: accent, audioDb: audioDb, onClose: () => { setThread(null); setExpanded(null); }, onRemove: remove, onEdit: edit, onReply: reply, onResolve: resolveThread }, openItem.id) })) : null, drag ? (_jsx("div", { "aria-hidden": true, className: "pointer-events-none fixed z-40 rounded-md border-2 border-dashed", style: { left: Math.min(drag.x1, drag.x2), top: Math.min(drag.y1, drag.y2), width: Math.abs(drag.x2 - drag.x1), height: Math.abs(drag.y2 - drag.y1), borderColor: accent, background: `color-mix(in srgb, ${accent} 12%, transparent)` } })) : null, _jsxs("div", { className: "fixed right-4 z-40 flex flex-col items-end gap-2", style: { bottom: 'calc(env(safe-area-inset-bottom, 0px) + 5.5rem)' }, children: [panel ? (_jsxs("div", { "data-comment-panel": true, className: "flex max-h-[min(70vh,32rem)] w-80 max-w-[calc(100vw-2rem)] flex-col rounded-xl border border-[color:var(--a-line-strong)] bg-[color:var(--a-surface)] p-3 text-xs text-[color:var(--a-strong)] shadow-xl", children: [panelList.length ? (_jsx("ul", { "data-comment-list": true, className: "-mx-1 mb-2 min-h-0 flex-1 list-none overflow-y-auto p-0", children: panelList.map((item) => (_jsx("li", { className: "m-0", children: _jsxs("button", { type: "button", "data-comment-list-item": item.id, onClick: () => { setPanel(false); setTarget(item.id); }, className: "w-full rounded-lg px-2 py-2 text-left hover:bg-[color:var(--a-line)]", style: resolvedOf(item) ? { opacity: 0.6 } : undefined, children: [_jsxs("span", { className: "block truncate text-[12px] italic opacity-70", children: [resolvedOf(item) ? '✓ ' : '', item.value.anchor.kind === 'text' ? `“${item.value.anchor.quote}”` : 'A box on the page'] }), _jsx("span", { className: "line-clamp-2 block text-[14px] leading-snug", children: item.value.body }), _jsx("span", { className: "block text-[11px] opacity-60", children: item.kind === 'personal' ? 'only you' : item.name })] }) }, item.id))) })) : null, _jsx("p", { className: "opacity-80", children: "Select any text, or hold and drag a box over an image, to leave a comment." }), isOwner ? _jsx("p", { "data-comment-owner": true, className: "mt-1 opacity-80", children: "This is your page: you see every comment, with the name of who left it." }) : null, _jsx("button", { type: "button", "data-comment-pins-toggle": true, className: "mt-2 underline", onClick: () => setShowPins((v) => !v), children: showPins ? 'Hide comments' : 'Show comments' })] })) : null, _jsx("button", { type: "button", "data-comment-button": true, "aria-expanded": panel, onClick: () => setPanel((v) => !v), className: "rounded-full border px-3 py-1.5 text-xs font-medium shadow-lg backdrop-blur", style: { borderColor: accent, color: accent, background: `color-mix(in srgb, ${ground} 85%, transparent)` }, children: count ? `Comments · ${count}` : 'Comment' })] })] })) : null;
|
|
595
597
|
return (_jsxs(_Fragment, { children: [_jsx("span", { hidden: true, "data-comment-layer": comments, "data-comment-can-share": share ? 'yes' : 'no' }), mounted && layout.earlier.length ? (_jsxs("section", { "data-comments-earlier": true, "data-comment-ui": true, "data-nospeak": true, className: "mx-auto max-w-2xl px-6 pb-16 text-sm", children: [_jsx("p", { className: "text-[11px] uppercase tracking-[0.2em]", style: { color: accent }, children: "Comments on earlier versions" }), _jsx("ul", { className: "m-0 mt-2 grid list-none gap-3 p-0", children: layout.earlier.map((item) => (_jsxs("li", { "data-comment-earlier-id": item.id, ...(highlight === item.id ? { 'data-comment-highlight': '' } : {}), className: "m-0 border-l-2 py-1 pl-3", style: { borderColor: accent, borderStyle: item.kind === 'personal' ? 'dashed' : 'solid', ...(highlight === item.id ? { background: `color-mix(in srgb, ${accent} 14%, transparent)` } : {}) }, children: [resolvedOf(item) ? _jsxs("span", { "data-comment-resolved-line": true, className: "block text-[13px] font-medium", style: { color: accent }, children: ["\u2713 ", resolvedLine(resolvedOf(item))] }) : null, item.value.anchor.kind === 'text' ? _jsxs("span", { className: "block text-[13px] italic opacity-70", children: ["\u201C", item.value.anchor.quote, "\u201D"] }) : _jsx("span", { className: "block text-[13px] italic opacity-70", children: "A box on part of the page that has changed" }), _jsx("span", { className: "block whitespace-pre-wrap text-[15px] text-[color:var(--a-strong)]", children: item.value.body }), _jsxs("span", { className: "block text-xs opacity-60", children: [item.kind === 'personal' ? 'only you' : item.name, ` · version ${item.value.version}`, item.mine ? _jsx("button", { type: "button", onClick: () => void remove(item), className: "ml-2 underline", children: "delete" }) : null] })] }, item.id))) })] })) : null, overlay ? createPortal(overlay, document.body) : null] }));
|
|
596
598
|
}
|
|
597
599
|
function Pin({ item, left, top, accent, ground, onOpen }) {
|
|
@@ -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
|
}
|
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
|
|
@@ -236,7 +244,7 @@ export function createArtifactRoutes(config) {
|
|
|
236
244
|
: a.password
|
|
237
245
|
? (admin ? { url: `${pageUrl(a.id)}?key=${encodeURIComponent(a.password)}`, title: a.title, note: 'The link carries the password.' } : null)
|
|
238
246
|
: { 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 }));
|
|
247
|
+
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
248
|
}
|
|
241
249
|
/** The kicker over every title: the pack's line, then who the page is for. Never spoken: the
|
|
242
250
|
* whole paragraph is data-nospeak, and narrationText has no `to` to read. */
|
|
@@ -320,8 +328,9 @@ export function createArtifactRoutes(config) {
|
|
|
320
328
|
const me = readerSecret() ? verifyGrant(readerSecret(), await readCookie(GRANT_COOKIE).catch(() => undefined)) : null;
|
|
321
329
|
if (isAdmin(me)) {
|
|
322
330
|
const markdown = await media(a.markdown);
|
|
323
|
-
const
|
|
324
|
-
|
|
331
|
+
const viewer = wantsReaderView(sp) ? null : me;
|
|
332
|
+
const page = broadcast ? await BroadcastCard({ ...a, markdown }, { reader: viewer, bot }) : await Body({ ...a, markdown }, { top: true, reader: viewer });
|
|
333
|
+
return _jsxs(BrandGround, { pack: pack, mode: a.theme, children: [viewer ? null : _jsx(ViewingAsReader, { back: pagePath(id) }), page] });
|
|
325
334
|
}
|
|
326
335
|
const secret = usableSecret(config.recipientSecret?.());
|
|
327
336
|
const g = await perReaderGate({ id, param, cookie: await readCookie(ridCookieName(id)).catch(() => undefined), secret, recipients: config.recipients });
|
|
@@ -367,10 +376,14 @@ export function createArtifactRoutes(config) {
|
|
|
367
376
|
if (!open) {
|
|
368
377
|
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
378
|
}
|
|
370
|
-
|
|
371
|
-
|
|
379
|
+
// The owner or an admin previewing the page as the people it is sent to see it (`?as=reader`):
|
|
380
|
+
// rendered for no one signed in, with a bar back to their own view. Anyone else asking gets the
|
|
381
|
+
// page they already get.
|
|
382
|
+
const asReader = isAdmin(reader) && wantsReaderView(sp);
|
|
383
|
+
const page = await render(true, asReader ? null : reader);
|
|
384
|
+
if (n === null && !asReader)
|
|
372
385
|
void store.bumpViews(id);
|
|
373
|
-
return (_jsxs(BrandGround, { pack: pack, mode: a.theme, children: [remember ? _jsx("script", { dangerouslySetInnerHTML: { __html: remember } }) : null, page] }));
|
|
386
|
+
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
387
|
}
|
|
375
388
|
/** A page with `access:`. The body is rendered only after the reader is known and allowed;
|
|
376
389
|
* everyone else gets the title, the summary, and a door. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supersuit/artifacts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.1",
|
|
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",
|