@supersuit/artifacts 0.44.0 → 0.44.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,22 @@
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.44.1
7
+
8
+ **Highlighting text works like any page again.** A press in the gap between paragraphs, in a line's
9
+ empty end, around a table or a list used to start the dashed comment box, so dragging to highlight
10
+ or copy drew a box instead of selecting. A box now starts only on content with no text to select:
11
+ an image, an SVG diagram, a canvas, a video, an iframe, a rule, an empty block, or anything marked
12
+ `data-boxable`. Everywhere else the browser's own selection runs untouched, and a selection still
13
+ shows the Comment chip as before.
14
+
15
+ - DETECTOR: on a page with comments on, drag from the space between two paragraphs. Before, a
16
+ dashed box drew; now the text highlights.
17
+ - REMEDY: update the dependency. A host that wants boxes on a custom element marks it
18
+ `data-boxable`.
19
+ - Tests: `startsBox` in comment-layer.test.tsx, text and gaps select natively, image, diagram and
20
+ empty block start a box.
21
+
6
22
  ## 0.44.0
7
23
 
8
24
  **Voice notes with their own words, lit as they are spoken.** An audio line followed directly by a
@@ -3,6 +3,13 @@ import { type VoiceHost } from './voice-recorder.js';
3
3
  import { type DeviceAudio } from './device-audio.js';
4
4
  /** The line a resolved thread collapses to. */
5
5
  export declare const resolvedLine: (r: CommentResolution) => string;
6
+ /**
7
+ * Whether a press here starts a comment box rather than the browser's own selection. Only on
8
+ * content with no text to select: a press in the gap between paragraphs, in a line's empty end,
9
+ * or anywhere in a text block is the start of a normal highlight, and a box there stole it
10
+ * (Gary, 2026-10-15: "it should also just allow people to do normal copying functionality").
11
+ */
12
+ export declare function startsBox(el: Element): boolean;
6
13
  /** A finger rather than a mouse: the comment button then goes where the OS menu is not. */
7
14
  export declare function onTouch(): boolean;
8
15
  export declare function CommentLayer({ artifactId, rootId, ownerName, comments, canShare, signedIn, isOwner, version, accent, ground, signIn, voice, audioStore, closesAt, requireName, knownName, }: {
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  // Comments on any part of a page, Figma style. Select text and a "Comment" chip appears; hold and
4
- // drag over anything that is not text (an image, a table, the gap between paragraphs) and a box
5
- // draws. Either opens the card (comment-card.tsx), which says before anything is typed whether
4
+ // drag on something that is not text (an image, a diagram, a video, an empty block) and a box
5
+ // draws. Everywhere else the browser's own selection runs untouched, gaps between paragraphs
6
+ // included, so copying and highlighting work as on any page. Either opens the card (comment-card.tsx), which says before anything is typed whether
6
7
  // the owner will ever read it.
7
8
  //
8
9
  // Two kinds, kept apart all the way down:
@@ -51,13 +52,21 @@ const resolvedOf = (item) => (item.kind === 'shared' ? resolutionOf(item.value)
51
52
  const DRAG_PX = 8;
52
53
  /** Where a drag never starts a box: controls, and anything that is somebody's words. */
53
54
  const NOT_FOR_BOXES = 'a, button, input, textarea, select, audio, video[controls], summary, label, [data-comment-ui], [data-note-toggle], [data-heading-notes], [data-artifact-notes], .artifact-defined-term, .artifact-word';
54
- function isTextTarget(el) {
55
- if (el.closest(NOT_FOR_BOXES))
55
+ /** What a box may start on: things a native selection cannot highlight. */
56
+ const BOX_TARGETS = 'img, svg, canvas, picture, video, iframe, hr, [data-boxable]';
57
+ /**
58
+ * Whether a press here starts a comment box rather than the browser's own selection. Only on
59
+ * content with no text to select: a press in the gap between paragraphs, in a line's empty end,
60
+ * or anywhere in a text block is the start of a normal highlight, and a box there stole it
61
+ * (Gary, 2026-10-15: "it should also just allow people to do normal copying functionality").
62
+ */
63
+ export function startsBox(el) {
64
+ if (el.closest(NOT_FOR_BOXES) || el.closest('figcaption'))
65
+ return false;
66
+ if (el.closest(BOX_TARGETS))
56
67
  return true;
57
- for (const c of Array.from(el.childNodes))
58
- if (c.nodeType === Node.TEXT_NODE && c.textContent?.trim())
59
- return true;
60
- return false;
68
+ const block = el.closest('[data-block]');
69
+ return !!block && !block.textContent?.trim();
61
70
  }
62
71
  function sharedFrom(body) {
63
72
  const slot = body?.slots?.[COMMENTS_SLOT];
@@ -279,7 +288,7 @@ export function CommentLayer({ artifactId, rootId, ownerName, comments, canShare
279
288
  document.addEventListener('selectionchange', settle);
280
289
  return () => { clearTimeout(timer); root.removeEventListener('mouseup', check); root.removeEventListener('keyup', check); document.removeEventListener('selectionchange', settle); };
281
290
  }, [rootId]);
282
- // Hold and drag over anything that is not text draws a box. A mouse starts after 8px of travel;
291
+ // Hold and drag on content with no text (startsBox) draws a box. A mouse starts after 8px of travel;
283
292
  // a finger after a 400 ms long-press, so an ordinary swipe still scrolls.
284
293
  const dragRef = useRef(null);
285
294
  useEffect(() => {
@@ -287,7 +296,7 @@ export function CommentLayer({ artifactId, rootId, ownerName, comments, canShare
287
296
  if (!root)
288
297
  return;
289
298
  const down = (e) => {
290
- if (e.button !== 0 || !(e.target instanceof Element) || isTextTarget(e.target))
299
+ if (e.button !== 0 || !(e.target instanceof Element) || !startsBox(e.target))
291
300
  return;
292
301
  const d = { x: e.clientX, y: e.clientY, armed: e.pointerType !== 'touch', drawing: false };
293
302
  if (e.pointerType === 'touch')
@@ -561,7 +570,7 @@ export function CommentLayer({ artifactId, rootId, ownerName, comments, canShare
561
570
  // On a phone the button sits at the TOP of the screen, never beside the selection: that is
562
571
  // where iOS draws its own Copy / Look Up menu, and the two fought for the same tap (Gary,
563
572
  // 2026-09-29: "I keep trying to fight with the Apple pop-up"). A mouse keeps it by the text.
564
- _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 anything, 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;
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;
565
574
  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] }));
566
575
  }
567
576
  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.0",
3
+ "version": "0.44.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",