@supersuit/artifacts 0.44.2 → 0.45.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 CHANGED
@@ -3,6 +3,29 @@
3
3
  `@supersuit/artifacts`. One entry per version, newest first. Each entry says what changed, how a
4
4
  host can tell whether it is affected (DETECTOR), what a host does about it (REMEDY), and the tests.
5
5
 
6
+ ## 0.45.0
7
+
8
+ **The Comments button shows the comments.** Pressing it now opens a list of every comment on the
9
+ page, in page order with open threads first and resolved ones after: the quoted words, the first
10
+ lines of the comment, and who left it. Tapping one closes the list, scrolls to the comment and
11
+ opens its thread, the same jump a `#comment-<id>` link makes. A comment on an earlier version
12
+ jumps to its entry in "Comments on earlier versions". The help line and the Hide comments toggle
13
+ stay below the list.
14
+
15
+ - DETECTOR: on a page with comments, press "Comments · N". Before, only the help text showed.
16
+ - REMEDY: update the dependency.
17
+ - Tests: comment-layer.test.tsx, "the Comments button lists every comment, and a tap opens that thread".
18
+
19
+ ## 0.44.3
20
+
21
+ **Escape closes a comment card.** Pressing Escape cancels the comment being written (nothing is
22
+ saved), or closes the open thread, or the comments panel, topmost first. It ignores an Escape
23
+ another layer already handled, so the definition popover and page menu keep theirs.
24
+
25
+ - DETECTOR: select text, press Comment, press Escape. Before, the card stayed open.
26
+ - REMEDY: update the dependency.
27
+ - Tests: comment-layer.test.tsx, "Escape cancels the comment being written, saving nothing".
28
+
6
29
  ## 0.44.2
7
30
 
8
31
  **An embed's caption no longer throws the read-along out of step.** The caption under a
@@ -251,6 +251,27 @@ export function CommentLayer({ artifactId, rootId, ownerName, comments, canShare
251
251
  ro.observe(root);
252
252
  return () => { live = false; window.removeEventListener('resize', relayout); ro?.disconnect(); };
253
253
  }, [artifactId, comments, signedIn, stateUrl, personalUrl, rootId, call, refreshDevice, host, audioDb]);
254
+ // Escape closes the top card: the comment being written, else the open thread, else the panel.
255
+ // Writing a comment is a draft nobody has saved, so Escape is its Cancel.
256
+ useEffect(() => {
257
+ if (!draft && !thread && !panel)
258
+ return;
259
+ const onKey = (e) => {
260
+ if (e.key !== 'Escape' || e.defaultPrevented)
261
+ return;
262
+ e.preventDefault();
263
+ if (draft)
264
+ setDraft(null);
265
+ else if (thread) {
266
+ setThread(null);
267
+ setExpanded(null);
268
+ }
269
+ else
270
+ setPanel(false);
271
+ };
272
+ document.addEventListener('keydown', onKey);
273
+ return () => document.removeEventListener('keydown', onKey);
274
+ }, [draft, thread, panel]);
254
275
  // Text selection inside the page shows the Comment chip.
255
276
  useEffect(() => {
256
277
  const root = document.getElementById(rootId);
@@ -566,11 +587,13 @@ export function CommentLayer({ artifactId, rootId, ownerName, comments, canShare
566
587
  const open = layout.placed.find((p) => p.item.id === thread);
567
588
  const openItem = open?.item ?? layout.earlier.find((i) => i.id === thread);
568
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))];
569
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 ? (
570
593
  // On a phone the button sits at the TOP of the screen, never beside the selection: that is
571
594
  // where iOS draws its own Copy / Look Up menu, and the two fought for the same tap (Gary,
572
595
  // 2026-09-29: "I keep trying to fight with the Apple pop-up"). A mouse keeps it by the text.
573
- _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-64 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: [_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;
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;
574
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] }));
575
598
  }
576
599
  function Pin({ item, left, top, accent, ground, onOpen }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersuit/artifacts",
3
- "version": "0.44.2",
3
+ "version": "0.45.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",