@supersuit/artifacts 0.45.0 → 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 CHANGED
@@ -3,6 +3,25 @@
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
+
6
25
  ## 0.45.0
7
26
 
8
27
  **The Comments button shows the comments.** Pressing it now opens a list of every comment on the
@@ -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;
@@ -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
  }
@@ -126,6 +126,7 @@ type PageProps = Params & {
126
126
  searchParams?: Promise<{
127
127
  key?: string | string[];
128
128
  r?: string | string[];
129
+ as?: string | string[];
129
130
  }>;
130
131
  };
131
132
  type VersionParams = {
@@ -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 page = broadcast ? await BroadcastCard({ ...a, markdown }, { reader: me, bot }) : await Body({ ...a, markdown }, { top: true, reader: me });
324
- return _jsx(BrandGround, { pack: pack, mode: a.theme, children: page });
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
- const page = await render(true, reader);
371
- if (n === null)
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.45.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",