@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
|
|
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,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 };
|
package/lib/widgets/notes.js
CHANGED
|
@@ -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.
|
|
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",
|