@supersuit/artifacts 0.12.0 → 0.12.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,17 @@
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.12.1 (2026-09-28)
7
+
8
+ **Tapping into a comment box no longer zooms the page on iPhone.** iOS Safari zooms the page
9
+ when a text field under 16px takes focus, and leaves it zoomed. The comment card, the thread
10
+ reply box and the notes widget are 16px now (`NO_ZOOM_FONT`). Pinch-zoom is untouched.
11
+
12
+ - DETECTOR: on an iPhone, tapping the comment box zooms the page in.
13
+ - REMEDY: bump.
14
+ - Tests: `no-zoom.test.ts` scans every text field the reader and widgets draw, so a new field
15
+ without the size fails the suite (mutation-checked).
16
+
6
17
  ## 0.12.0 (2026-09-28)
7
18
 
8
19
  **Say a comment instead of typing it.** The comment card has a microphone. While you speak, a
@@ -10,6 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
10
10
  // you speak, then the host's transcript when the recording stops, unless you have edited the box
11
11
  // since, in which case your words win. With neither, the memo is kept and the box stays empty.
12
12
  import { useRef, useState } from 'react';
13
+ import { NO_ZOOM_FONT } from './no-zoom.js';
13
14
  import { MAX_COMMENT_CHARS } from '../artifacts/comments.js';
14
15
  import { VoiceRecorder } from './voice-recorder.js';
15
16
  /** The warning, word for word. `They` is fixed copy: the card never guesses a pronoun. */
@@ -66,6 +67,6 @@ export function CommentCard({ ownerName, mode, canShare, signedIn, quote, initia
66
67
  setBusy(false);
67
68
  setError(err);
68
69
  };
69
- return (_jsxs("form", { "data-comment-card": true, "data-comment-ui": true, "data-nospeak": true, onSubmit: submit, className: "w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-[color:var(--a-line-strong)] bg-[color:var(--a-surface)] p-3 text-left font-sans text-sm text-[color:var(--a-strong)] shadow-xl", children: [!canShare ? (_jsx("p", { "data-comment-warning": true, role: "note", className: "mb-2 rounded-lg border px-3 py-2 text-[13px] leading-snug", style: { borderColor: accent }, children: warningLine(ownerName, mode) })) : null, quote ? _jsx("p", { className: "mb-2 line-clamp-3 border-l-2 pl-2 text-[13px] italic opacity-75", style: { borderColor: accent }, children: quote }) : null, _jsx("textarea", { value: text, autoFocus: true, onChange: (e) => { setText(e.target.value); edited.current = true; setTranscript('typed'); }, onKeyDown: (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey))
70
+ return (_jsxs("form", { "data-comment-card": true, "data-comment-ui": true, "data-nospeak": true, onSubmit: submit, className: "w-[min(22rem,calc(100vw-2rem))] rounded-xl border border-[color:var(--a-line-strong)] bg-[color:var(--a-surface)] p-3 text-left font-sans text-sm text-[color:var(--a-strong)] shadow-xl", children: [!canShare ? (_jsx("p", { "data-comment-warning": true, role: "note", className: "mb-2 rounded-lg border px-3 py-2 text-[13px] leading-snug", style: { borderColor: accent }, children: warningLine(ownerName, mode) })) : null, quote ? _jsx("p", { className: "mb-2 line-clamp-3 border-l-2 pl-2 text-[13px] italic opacity-75", style: { borderColor: accent }, children: quote }) : null, _jsx("textarea", { value: text, autoFocus: true, style: NO_ZOOM_FONT, onChange: (e) => { setText(e.target.value); edited.current = true; setTranscript('typed'); }, onKeyDown: (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey))
70
71
  void submit(); }, maxLength: MAX_COMMENT_CHARS, rows: 3, "aria-label": personal ? 'A personal note' : `A comment for ${ownerName}`, placeholder: personal ? 'A note for yourself' : `A comment for ${ownerName}`, className: "w-full rounded-lg border border-[color:var(--a-line)] bg-[color:var(--a-ground,transparent)] p-2 text-[15px]" }), _jsx(VoiceRecorder, { accent: accent, onStart: () => { base.current = text.trim(); setMemo(null); setTranscript(undefined); setError(null); }, onLive: (heard) => { setText(base.current ? `${base.current} ${heard}` : heard); setTranscript('browser'); }, onStop: (m) => void recorded(m), onError: setError }), memo ? (_jsxs("p", { "data-voice-kept": true, className: "m-0 mt-1 text-xs opacity-70", children: [status ?? 'Recording kept with this comment.', _jsx("button", { type: "button", "data-voice-discard": true, className: "ml-2 underline", onClick: () => { setMemo(null); uploaded.current = null; }, children: "Discard recording" })] })) : null, canShare ? (_jsxs("fieldset", { className: "mt-2 flex flex-wrap gap-x-4 gap-y-1 text-[13px]", children: [_jsx("legend", { className: "sr-only", children: "Who sees this" }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("input", { type: "radio", name: "comment-share", "data-comment-share": "share", checked: share, onChange: () => setShare(true) }), `Share with ${ownerName}`] }), _jsxs("label", { className: "flex items-center gap-1.5", children: [_jsx("input", { type: "radio", name: "comment-share", "data-comment-share": "mine", checked: !share, onChange: () => setShare(false) }), "Just for me"] })] })) : null, personal && !signedIn ? (_jsx("p", { "data-comment-device": true, className: "mt-2 text-xs opacity-70", children: signIn ? _jsxs(_Fragment, { children: ["Saved on this device. ", _jsx("a", { href: signIn, className: "underline", children: "Sign in to keep it everywhere." })] }) : DEVICE_LINE })) : null, error ? _jsx("p", { role: "alert", className: "mt-2 text-xs text-amber-500", children: error }) : null, _jsxs("div", { className: "mt-3 flex items-center gap-3", children: [_jsx("button", { type: "submit", "data-comment-save": true, disabled: busy || (!text.trim() && !memo), className: "rounded-full px-4 py-1 text-sm font-medium disabled:opacity-40", style: { background: accent, color: 'var(--a-on-accent, #111)' }, children: personal ? 'Save note' : 'Comment' }), _jsx("button", { type: "button", onClick: onCancel, className: "text-sm opacity-70 hover:opacity-100", children: "Cancel" })] })] }));
71
72
  }
@@ -27,6 +27,7 @@ import { AudioPlay, hostVoice, newMemoId } from './voice-recorder.js';
27
27
  import { deviceAudio as defaultDeviceAudio, isDeviceAudio } from './device-audio.js';
28
28
  import { audioExtFor } from '../artifacts/audio.js';
29
29
  import { themeVarsAround } from './theme-vars.js';
30
+ import { NO_ZOOM_FONT } from './no-zoom.js';
30
31
  import { addDeviceNote, deviceNotes, removeDeviceNote, replaceDeviceNote } from './device-notes.js';
31
32
  import { boxOf, headingAt, indexText, offsetAt, placeAnchor, regionFrom } from './comment-place.js';
32
33
  const isValue = (v) => !!v && typeof v === 'object' && typeof v.body === 'string' && !!v.anchor && typeof v.anchor === 'object';
@@ -462,5 +463,5 @@ function InlineForm({ label, initial = '', accent = 'currentColor', onSubmit, on
462
463
  const [text, setText] = useState(initial);
463
464
  const [error, setError] = useState(null);
464
465
  return (_jsxs("form", { className: "mt-2", onSubmit: async (e) => { e.preventDefault(); if (text.trim())
465
- setError(await onSubmit(text.trim())); }, children: [_jsx("textarea", { value: text, autoFocus: true, rows: 2, onChange: (e) => setText(e.target.value), "aria-label": label, className: "w-full rounded-lg border border-[color:var(--a-line)] bg-transparent p-2 text-[15px]" }), error ? _jsx("p", { role: "alert", className: "text-xs text-amber-500", children: error }) : null, _jsxs("div", { className: "mt-1 flex gap-3 text-xs", children: [_jsx("button", { type: "submit", disabled: !text.trim(), className: "rounded-full px-3 py-0.5 font-medium disabled:opacity-40", style: { background: accent, color: 'var(--a-on-accent, #111)' }, children: label }), _jsx("button", { type: "button", onClick: onCancel, className: "opacity-70", children: "Cancel" })] })] }));
466
+ setError(await onSubmit(text.trim())); }, children: [_jsx("textarea", { value: text, autoFocus: true, rows: 2, onChange: (e) => setText(e.target.value), "aria-label": label, className: "w-full rounded-lg border border-[color:var(--a-line)] bg-transparent p-2", style: NO_ZOOM_FONT }), error ? _jsx("p", { role: "alert", className: "text-xs text-amber-500", children: error }) : null, _jsxs("div", { className: "mt-1 flex gap-3 text-xs", children: [_jsx("button", { type: "submit", disabled: !text.trim(), className: "rounded-full px-3 py-0.5 font-medium disabled:opacity-40", style: { background: accent, color: 'var(--a-on-accent, #111)' }, children: label }), _jsx("button", { type: "button", onClick: onCancel, className: "opacity-70", children: "Cancel" })] })] }));
466
467
  }
@@ -0,0 +1,3 @@
1
+ export declare const NO_ZOOM_FONT: {
2
+ readonly fontSize: 16;
3
+ };
@@ -0,0 +1,6 @@
1
+ // iOS Safari zooms the whole page when a text field under 16px takes focus, and does not zoom
2
+ // back out, so a reader who taps into a comment box loses the page they were commenting on
3
+ // (reported from an iPhone on 2026-09-28). Every text field the reader draws uses this size.
4
+ // A viewport `maximum-scale` would also stop the zoom, but it takes pinch-zoom away from readers
5
+ // who need it, so the size is the fix.
6
+ export const NO_ZOOM_FONT = { fontSize: 16 };
@@ -8,6 +8,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
8
8
  // Everything drawn here carries data-nospeak: narration reads the prose, never the notes, and
9
9
  // the read-along highlighter skips the same nodes. Notes are shown as text, never as markup.
10
10
  import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
11
+ import { NO_ZOOM_FONT } from '../reader/no-zoom.js';
11
12
  import { MAX_NOTE_CHARS, NOTES_SLOT, placeNotes } from '../artifacts/notes-place.js';
12
13
  const NotesContext = createContext(null);
13
14
  const isNote = (v) => !!v && typeof v === 'object' && typeof v.slug === 'string' &&
@@ -112,7 +113,7 @@ function NoteForm({ slug, heading, c }) {
112
113
  if (!c.canWrite) {
113
114
  return c.signIn ? (_jsx("p", { className: "my-2 text-sm", children: _jsx("a", { href: c.signIn, className: "underline", style: { color: c.accent }, children: "Sign in to leave a note" }) })) : (_jsx("p", { className: "my-2 text-sm opacity-70", children: "Notes here are open to signed-in readers." }));
114
115
  }
115
- return (_jsxs("form", { onSubmit: submit, className: "my-2", children: [_jsx("textarea", { value: text, onChange: (e) => setText(e.target.value), maxLength: MAX_NOTE_CHARS, rows: 3, "aria-label": `A note on ${heading}`, placeholder: `A note on “${heading}”`, className: "w-full rounded-lg border border-[color:var(--a-line-strong)] bg-[color:var(--a-surface)] p-3 text-[15px] text-[color:var(--a-strong)]" }), c.error ? _jsx("p", { className: "mt-1 text-xs text-amber-500", children: c.error }) : null, _jsxs("div", { className: "mt-2 flex gap-3", children: [_jsx("button", { type: "submit", disabled: c.busy || !text.trim(), className: "rounded-full px-4 py-1 text-sm font-medium disabled:opacity-40", style: { background: c.accent, color: 'var(--a-on-accent, #111)' }, children: "Save note" }), _jsx("button", { type: "button", onClick: () => c.setOpen(null), className: "text-sm opacity-70 hover:opacity-100", children: "Cancel" })] })] }));
116
+ return (_jsxs("form", { onSubmit: submit, className: "my-2", children: [_jsx("textarea", { value: text, onChange: (e) => setText(e.target.value), style: NO_ZOOM_FONT, maxLength: MAX_NOTE_CHARS, rows: 3, "aria-label": `A note on ${heading}`, placeholder: `A note on “${heading}”`, className: "w-full rounded-lg border border-[color:var(--a-line-strong)] bg-[color:var(--a-surface)] p-3 text-[15px] text-[color:var(--a-strong)]" }), c.error ? _jsx("p", { className: "mt-1 text-xs text-amber-500", children: c.error }) : null, _jsxs("div", { className: "mt-2 flex gap-3", children: [_jsx("button", { type: "submit", disabled: c.busy || !text.trim(), className: "rounded-full px-4 py-1 text-sm font-medium disabled:opacity-40", style: { background: c.accent, color: 'var(--a-on-accent, #111)' }, children: "Save note" }), _jsx("button", { type: "button", onClick: () => c.setOpen(null), className: "text-sm opacity-70 hover:opacity-100", children: "Cancel" })] })] }));
116
117
  }
117
118
  /** After each heading: its notes, and the box when its control is open. */
118
119
  export function HeadingNotes({ slug, text }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersuit/artifacts",
3
- "version": "0.12.0",
3
+ "version": "0.12.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",