@usableapp/cardds 0.7.48 → 0.7.49

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
@@ -2,6 +2,17 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.49 — 2026-10-03
6
+
7
+ ### Fixed
8
+ - **`Pin` on an iPhone: the digits can be read** (Lh 2026-10-03, timebank): iOS gives a text field half an em of padding each side — more than a Pin cell is wide — so a typed digit had no room, started at the cell's middle and ran out of it (a few slivers of ink). The cell now has no padding and no platform look; it is also 2:3 on iOS as everywhere (it stood taller). Nothing to change in a screen.
9
+ - **`Pin`: a cell that cannot be typed into yet does not look ready** — a phone opens its keyboard on a tap, not when a script sets the cursor (a sheet arriving, a step opening). On a touch screen, a cell that got the cursor that way with no keyboard up shows the resting border and no caret, until a finger taps a cell, the keyboard is up, or a digit arrives. A desktop is unchanged.
10
+ - **`SheetSteps`: a digit typed while a step is opening is not lost** — it used to be dropped (the cursor waits in a hidden holder while the step folds, and on an iPhone the keyboard could stay there); it now lands in the open step's first field, and the cursor with it.
11
+
12
+ ### Changed
13
+ - **One look on every phone** (Lh 2026-10-03): the browser's own padding, margin, corners, field look and button colour are taken off every `button`, text `input` and `textarea` once, at zero specificity — iOS, Android and a desktop each gave their own numbers (iOS: 0.5em a side on a field, 1em on a button, system-blue words on an unstyled button). cardds's own rules are untouched; on a desktop nothing changes size (the words in a `Field` start 2px further left). **If your app has its own `<button>` or `<input>` with no styling inside a cardds page, it loses the browser's padding and look too — give it a cardds component or its own rule.** `html` also gets `text-size-adjust: 100%` (an iPhone turned sideways no longer enlarges the words).
14
+ - **A `Sheet`'s long title runs sideways** like a song title, instead of being cut with `…` (Lh 2026-10-03): still one line; it waits, runs to its end, waits, starts again. With reduced motion it is cut with `…` as before. Knob `--sheet-title-run-time`. Nothing to change in a screen. Story `Sheet · LongTitle`.
15
+
5
16
  ## 0.7.48 — 2026-10-03
6
17
 
7
18
  ### Changed
package/css/base.css CHANGED
@@ -39,6 +39,16 @@ body {
39
39
 
40
40
  img, svg { display: block; max-width: 100%; }
41
41
  button { font: inherit; }
42
+ /* ONE LOOK ON EVERY PHONE (Lh 2026-10-03, after the Pin on an iPhone): a browser dresses a button and a text field in its
43
+ platform's own clothes — iOS gives a field 0.5em of padding a side, rounded corners, its own ground; a button 1em a side and
44
+ the system's blue words; Android and a desktop give other numbers. cardds draws every control WHOLE, so none of that may show
45
+ through: the platform's padding, margin, corners, look and colour are taken off here, once, at zero specificity — a
46
+ component's own rule always wins, this only fills in where it says nothing. What a control looks like is then cardds's
47
+ alone, the same on iOS, Android and a desktop. (Checkboxes, radios, ranges and file inputs are drawn by their own rules.) */
48
+ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } /* an iPhone turned on its side must not blow the words up */
49
+ :where(button, input, textarea, select) { margin: 0; border-radius: 0; }
50
+ :where(button, input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]), textarea) { padding: 0; -webkit-appearance: none; appearance: none; }
51
+ :where(button) { color: inherit; }
42
52
  button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
43
53
 
44
54
  /* zero-specificity reset for controls that draw nothing of their own (2026-09-27 review):
package/css/choice.css CHANGED
@@ -186,6 +186,10 @@
186
186
  min-width: 0;
187
187
  max-width: var(--pin-cell-max-w, var(--fab));
188
188
  aspect-ratio: 2 / 3;
189
+ padding: 0; /* iPhone (Lh 2026-10-03): WebKit on iOS gives a text field 0.5em of padding each side — of a 40px digit that is more
190
+ than the pill is wide, so the field had NO room for its text: the digit started at the pill's middle and ran out of it, a few
191
+ slivers of ink at the edge. The pill centres its digit itself; it needs no padding on any platform. */
192
+ -webkit-appearance: none; appearance: none; /* the pill is drawn here, whole — no platform field look under it */
189
193
  border: var(--border-w) solid var(--pin-cell-border, var(--outline));
190
194
  border-radius: var(--pin-cell-radius, var(--r-chip));
191
195
  background: var(--pin-cell-bg, var(--action-bg));
@@ -215,6 +219,10 @@
215
219
  @keyframes pin-flash { 0% { -webkit-text-fill-color: currentColor; background-size: 0 0; } }
216
220
  .pin--secret .pin__cell::selection { background: transparent; } /* its digit is selected when the cell takes the cursor: nothing to show of it */
217
221
  .pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
222
+ /* asleep (Pin sets it): the cursor was put here by a script on a phone and no keyboard is up — nothing can be typed until a tap, so
223
+ the cell does not say "ready": the resting border, no caret */
224
+ .pin--asleep .pin__cell:focus { border-color: var(--pin-cell-border, var(--outline)); }
225
+ .pin--asleep .pin__cell:focus:placeholder-shown { background-image: none; animation: none; }
218
226
  .pin__cell:focus:placeholder-shown {
219
227
  background-image: linear-gradient(currentColor, currentColor);
220
228
  background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
package/css/sheet.css CHANGED
@@ -211,10 +211,22 @@
211
211
  whole ask is that switch: peek shows it (a row is --tap tall plus its own
212
212
  padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
213
213
  .sheet > .card__head { min-height: var(--sheet-head-h); }
214
- /* a sheet's title is ONE line, cut with … when the phone is too narrow (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน"
215
- wrapped to two lines at 390px) — the thing at its end keeps its place beside it; a caption still takes its own row */
214
+ /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
215
+ thing at its end keeps its place beside it; a caption still takes its own row. A title too long for the line RUNS sideways
216
+ like a song title (Lh 2026-10-03, as the StackSheet's — it was cut with …, and the end of it could never be read): the Sheet
217
+ measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
218
+ from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
219
+ Before the Sheet has measured, and where motion is off, it is cut with …. */
216
220
  .sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { flex-wrap: nowrap; }
217
- .sheet > .card__head > :is(h1, h2, h3):first-child, .sheet__title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
221
+ .sheet > .card__head > :is(h1, h2, h3):first-child, .sheet__title { white-space: nowrap; overflow-x: clip; overflow-y: visible; text-overflow: ellipsis; min-width: 0; } /* clip SIDEWAYS only: hidden cut the Thai tone marks over the line */
222
+ .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long {
223
+ text-overflow: clip;
224
+ animation: sheet-title-run var(--sheet-title-run-time, var(--_run, 6s)) ease-in-out infinite; /* --_run: from the overflow, 30px a second + the pauses */
225
+ }
226
+ @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
227
+ @media (prefers-reduced-motion: reduce) {
228
+ .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long { animation: none; text-overflow: ellipsis; }
229
+ }
218
230
  .sheet__head {
219
231
  display: grid;
220
232
  grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
package/dist/cardds.css CHANGED
@@ -508,6 +508,16 @@ body {
508
508
 
509
509
  img, svg { display: block; max-width: 100%; }
510
510
  button { font: inherit; }
511
+ /* ONE LOOK ON EVERY PHONE (Lh 2026-10-03, after the Pin on an iPhone): a browser dresses a button and a text field in its
512
+ platform's own clothes — iOS gives a field 0.5em of padding a side, rounded corners, its own ground; a button 1em a side and
513
+ the system's blue words; Android and a desktop give other numbers. cardds draws every control WHOLE, so none of that may show
514
+ through: the platform's padding, margin, corners, look and colour are taken off here, once, at zero specificity — a
515
+ component's own rule always wins, this only fills in where it says nothing. What a control looks like is then cardds's
516
+ alone, the same on iOS, Android and a desktop. (Checkboxes, radios, ranges and file inputs are drawn by their own rules.) */
517
+ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } /* an iPhone turned on its side must not blow the words up */
518
+ :where(button, input, textarea, select) { margin: 0; border-radius: 0; }
519
+ :where(button, input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]), textarea) { padding: 0; -webkit-appearance: none; appearance: none; }
520
+ :where(button) { color: inherit; }
511
521
  button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
512
522
 
513
523
  /* zero-specificity reset for controls that draw nothing of their own (2026-09-27 review):
@@ -1536,10 +1546,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1536
1546
  whole ask is that switch: peek shows it (a row is --tap tall plus its own
1537
1547
  padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
1538
1548
  .sheet > .card__head { min-height: var(--sheet-head-h); }
1539
- /* a sheet's title is ONE line, cut with … when the phone is too narrow (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน"
1540
- wrapped to two lines at 390px) — the thing at its end keeps its place beside it; a caption still takes its own row */
1549
+ /* a sheet's title is ONE line, always (Lh 2026-09-28, timebank #153: "ข้อกำหนดและเงื่อนไขการใช้งาน" wrapped to two lines at 390px) — the
1550
+ thing at its end keeps its place beside it; a caption still takes its own row. A title too long for the line RUNS sideways
1551
+ like a song title (Lh 2026-10-03, as the StackSheet's — it was cut with …, and the end of it could never be read): the Sheet
1552
+ measures --_over, the width it overflows by, and sets .is-long; one way — it waits, runs to its end, waits, and starts again
1553
+ from the first letter. The move is text-indent: the heading is the app's own element, there is no inner box to slide.
1554
+ Before the Sheet has measured, and where motion is off, it is cut with …. */
1541
1555
  .sheet > .card__head:not(:has(> :is(.t-caption, .t-body))) { flex-wrap: nowrap; }
1542
- .sheet > .card__head > :is(h1, h2, h3):first-child, .sheet__title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
1556
+ .sheet > .card__head > :is(h1, h2, h3):first-child, .sheet__title { white-space: nowrap; overflow-x: clip; overflow-y: visible; text-overflow: ellipsis; min-width: 0; } /* clip SIDEWAYS only: hidden cut the Thai tone marks over the line */
1557
+ .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long {
1558
+ text-overflow: clip;
1559
+ animation: sheet-title-run var(--sheet-title-run-time, var(--_run, 6s)) ease-in-out infinite; /* --_run: from the overflow, 30px a second + the pauses */
1560
+ }
1561
+ @keyframes sheet-title-run { 0%, 20% { text-indent: 0; } 85%, 100% { text-indent: calc(-1 * var(--_over)); } }
1562
+ @media (prefers-reduced-motion: reduce) {
1563
+ .sheet > .card__head > :is(h1, h2, h3):first-child.is-long, .sheet__title.is-long { animation: none; text-overflow: ellipsis; }
1564
+ }
1543
1565
  .sheet__head {
1544
1566
  display: grid;
1545
1567
  grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
@@ -3040,6 +3062,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3040
3062
  min-width: 0;
3041
3063
  max-width: var(--pin-cell-max-w, var(--fab));
3042
3064
  aspect-ratio: 2 / 3;
3065
+ padding: 0; /* iPhone (Lh 2026-10-03): WebKit on iOS gives a text field 0.5em of padding each side — of a 40px digit that is more
3066
+ than the pill is wide, so the field had NO room for its text: the digit started at the pill's middle and ran out of it, a few
3067
+ slivers of ink at the edge. The pill centres its digit itself; it needs no padding on any platform. */
3068
+ -webkit-appearance: none; appearance: none; /* the pill is drawn here, whole — no platform field look under it */
3043
3069
  border: var(--border-w) solid var(--pin-cell-border, var(--outline));
3044
3070
  border-radius: var(--pin-cell-radius, var(--r-chip));
3045
3071
  background: var(--pin-cell-bg, var(--action-bg));
@@ -3069,6 +3095,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
3069
3095
  @keyframes pin-flash { 0% { -webkit-text-fill-color: currentColor; background-size: 0 0; } }
3070
3096
  .pin--secret .pin__cell::selection { background: transparent; } /* its digit is selected when the cell takes the cursor: nothing to show of it */
3071
3097
  .pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
3098
+ /* asleep (Pin sets it): the cursor was put here by a script on a phone and no keyboard is up — nothing can be typed until a tap, so
3099
+ the cell does not say "ready": the resting border, no caret */
3100
+ .pin--asleep .pin__cell:focus { border-color: var(--pin-cell-border, var(--outline)); }
3101
+ .pin--asleep .pin__cell:focus:placeholder-shown { background-image: none; animation: none; }
3072
3102
  .pin__cell:focus:placeholder-shown {
3073
3103
  background-image: linear-gradient(currentColor, currentColor);
3074
3104
  background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
package/dist/forms/Pin.js CHANGED
@@ -73,6 +73,21 @@ export function Pin({ length = 6, value, defaultValue = '', label = word('digit'
73
73
  const [cleared, setCleared] = useState(undefined); // the app's error whose miss has played out (its cells emptied)
74
74
  const wrong = (errOn && error !== cleared) || bad;
75
75
  const restart = useRef(false);
76
+ /* ASLEEP (iPhone, Lh 2026-10-03): a phone opens its keyboard on a TAP, not when a script sets the cursor (a sheet arriving, a step
77
+ opening). A cell that got the cursor that way with no keyboard up cannot be typed into yet — it must not look ready: no full
78
+ border, no caret, until a finger taps a cell, the keyboard is up, or a digit arrives. Never on a desktop (a key is at hand). */
79
+ const [asleep, setAsleep] = useState(false);
80
+ const tapped = useRef(0);
81
+ const keyboardUp = () => { const vv = window.visualViewport; return !!vv && window.innerHeight - vv.height > 120; };
82
+ const wake = () => { tapped.current = performance.now(); setAsleep(false); };
83
+ useEffect(() => {
84
+ if (!asleep)
85
+ return;
86
+ const vv = window.visualViewport, check = () => { if (keyboardUp())
87
+ setAsleep(false); };
88
+ vv?.addEventListener('resize', check);
89
+ return () => vv?.removeEventListener('resize', check);
90
+ }, [asleep]);
76
91
  const [fresh, setFresh] = useState(null); // the cell whose digit was just typed: a secret Pin shows it for a moment
77
92
  useEffect(() => {
78
93
  if (!errOn) {
@@ -95,6 +110,7 @@ export function Pin({ length = 6, value, defaultValue = '', label = word('digit'
95
110
  // eslint-disable-next-line react-hooks/exhaustive-deps -- when the code is emptied from outside
96
111
  }, [code]);
97
112
  const input = (i) => (e) => {
113
+ wake(); // a digit arrived: the keyboard is there — and the cell the Pin moves on to is ready too
98
114
  const digits = e.target.value.replace(/\D/g, '');
99
115
  if (restart.current) { // a new try after a wrong code: what arrives starts a new code from the first cell
100
116
  restart.current = false;
@@ -133,6 +149,7 @@ export function Pin({ length = 6, value, defaultValue = '', label = word('digit'
133
149
  cell(Math.min(digits.length, length - 1))?.focus();
134
150
  };
135
151
  const key = (i) => (e) => {
152
+ wake();
136
153
  if (e.key === 'Backspace' && !e.currentTarget.value && i > 0) {
137
154
  e.preventDefault();
138
155
  setFresh(null);
@@ -148,11 +165,16 @@ export function Pin({ length = 6, value, defaultValue = '', label = word('digit'
148
165
  cell(i + 1)?.focus();
149
166
  }
150
167
  };
151
- return (_jsx("div", { ref: ref, className: cx('pin', secret && 'pin--secret', wrong && 'pin--error', shake && 'pin--shake', className), ...rest, onAnimationEnd: (e) => { if (e.animationName === 'pin-shake')
168
+ return (_jsx("div", { ref: ref, className: cx('pin', secret && 'pin--secret', wrong && 'pin--error', shake && 'pin--shake', asleep && 'pin--asleep', className), ...rest, onPointerDown: (e) => { wake(); rest.onPointerDown?.(e); }, onBlur: (e) => { if (!e.currentTarget.contains(e.relatedTarget))
169
+ setAsleep(false); rest.onBlur?.(e); }, onAnimationEnd: (e) => { if (e.animationName === 'pin-shake')
152
170
  setShake(false); if (e.animationName === 'pin-flash')
153
171
  setFresh(null); onAnimationEnd?.(e); }, children: Array.from({ length }, (_, i) => (
154
172
  // no maxLength: the browser would cut a pasted code before this ever saw it. A cell holds one digit — typing into a full one
155
173
  // replaces it, so its digit is selected whenever the cell takes the focus
156
- _jsx("input", { className: cx('pin__cell', secret && fresh === i && 'is-fresh'), inputMode: "numeric", pattern: "[0-9]*", placeholder: " ", value: code[i] ?? '', "aria-label": `${label} ${i + 1}`, "aria-invalid": wrong || undefined, autoComplete: i === 0 && !secret ? 'one-time-code' : 'off', onChange: input(i), onPaste: paste, onKeyDown: key(i), onFocus: (e) => { if (!restart.current)
157
- e.currentTarget.select(); } }, i))) }));
174
+ _jsx("input", { className: cx('pin__cell', secret && fresh === i && 'is-fresh'), inputMode: "numeric", pattern: "[0-9]*", placeholder: " ", value: code[i] ?? '', "aria-label": `${label} ${i + 1}`, "aria-invalid": wrong || undefined, autoComplete: i === 0 && !secret ? 'one-time-code' : 'off', onChange: input(i), onPaste: paste, onKeyDown: key(i), onFocus: (e) => {
175
+ if (!restart.current)
176
+ e.currentTarget.select();
177
+ if (performance.now() - tapped.current > 700 && window.matchMedia('(pointer: coarse)').matches && !keyboardUp())
178
+ setAsleep(true); // set by a script, on a phone, no keyboard: not ready
179
+ } }, i))) }));
158
180
  }
@@ -220,6 +220,26 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
220
220
  const stop = watchSize([head], publish);
221
221
  return () => { stop(); box.style.removeProperty('--sheet-peek'); };
222
222
  }, []);
223
+ /* the TITLE is one line; too long for it, it runs sideways like a song title (Lh 2026-10-03): the sheet measures how far its
224
+ words overflow the line and hands it to the CSS (--_over, --_run, .is-long — sheet.css). Measured on the words themselves
225
+ (a range), so the running indent never feeds back into the number. */
226
+ useEffect(() => {
227
+ const el = ref.current, title = el?.querySelector(':scope > .card__head > :is(h1, h2, h3):first-child, :scope > .sheet__head .sheet__title');
228
+ if (!el || !title)
229
+ return;
230
+ const measure = () => {
231
+ const words = document.createRange();
232
+ words.selectNodeContents(title);
233
+ const over = Math.max(0, Math.ceil(words.getBoundingClientRect().width - title.clientWidth));
234
+ title.style.setProperty('--_over', `${over}px`);
235
+ title.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
236
+ title.classList.toggle('is-long', over > 0);
237
+ };
238
+ const stop = watchSize([title], measure);
239
+ const mo = new MutationObserver(measure);
240
+ mo.observe(title, { childList: true, characterData: true, subtree: true }); // the app changed the words
241
+ return () => { stop(); mo.disconnect(); title.classList.remove('is-long'); title.style.removeProperty('--_over'); title.style.removeProperty('--_run'); };
242
+ });
223
243
  /* the FOOT's room follows what the foot really holds (timebank, Lh 2026-10-01): a button grows with the member's text size, a
224
244
  line may stand over it (an error) — the CSS strip was a fixed sum (--btn-h), so the body's words ran under the button. The
225
245
  sheet measures its foot's rows and publishes --sheet-foot-h; the strip, its ground and the body's stop all read it. */
@@ -180,7 +180,20 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
180
180
  else if (dy < 0 && ref.current?.querySelector('.sheet-step--now[data-skippable]'))
181
181
  go(to, 'skip'); // forward only, and only past a step that may be skipped
182
182
  };
183
- return (_jsxs("div", { ref: ref, className: cx('sheet-steps', !reversible && 'sheet-steps--locked', className), style: style, onFocus: focusIn, onBlur: focusOut, onPointerDown: down, onPointerUp: up, onPointerCancel: (e) => { onPointerCancel?.(e); press.current = null; }, ...rest, children: [_jsx("input", { ref: holderRef, className: "sheet-steps__holder", "aria-hidden": true, tabIndex: -1, autoComplete: "off", onInput: (e) => { e.currentTarget.value = ''; } }), _jsx("ol", { className: "sheet-steps__track", children: steps.map((step, i) => (_jsx(StepCtx.Provider, { value: { numbered, i, at: current, furthest: reach, count, reversible, go }, children: step }, step.key ?? i))) }), finish != null && ( // always in sight; it takes taps only once every step is done (or skipped)
183
+ return (_jsxs("div", { ref: ref, className: cx('sheet-steps', !reversible && 'sheet-steps--locked', className), style: style, onFocus: focusIn, onBlur: focusOut, onPointerDown: down, onPointerUp: up, onPointerCancel: (e) => { onPointerCancel?.(e); press.current = null; }, ...rest, children: [_jsx("input", { ref: holderRef, className: "sheet-steps__holder", "aria-hidden": true, tabIndex: -1, autoComplete: "off", onInput: (e) => {
184
+ /* typed while the cursor waits here (the fold is playing — or, on an iPhone, the keyboard never followed the cursor into the
185
+ new step: a focus set by script does not always take the keyboard with it, and every digit landed here and was dropped —
186
+ timebank, Lh 2026-10-03): what was typed belongs to the open step's first field. Hand it over, and the cursor with it —
187
+ from inside the key's own event, which the phone does follow. */
188
+ const typed = e.currentTarget.value;
189
+ e.currentTarget.value = '';
190
+ const field = ref.current?.querySelector('.sheet-step--now .sheet-step__inner input:not([type=checkbox], [type=radio], [type=hidden])');
191
+ if (!typed || !field)
192
+ return;
193
+ field.focus({ preventScroll: true });
194
+ Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(field, field.value + typed);
195
+ field.dispatchEvent(new Event('input', { bubbles: true }));
196
+ } }), _jsx("ol", { className: "sheet-steps__track", children: steps.map((step, i) => (_jsx(StepCtx.Provider, { value: { numbered, i, at: current, furthest: reach, count, reversible, go }, children: step }, step.key ?? i))) }), finish != null && ( // always in sight; it takes taps only once every step is done (or skipped)
184
197
  _jsx("div", { className: "sheet-steps__finish", children: isValidElement(finish) ? cloneElement(finish, { disabled: reach < count || finish.props.disabled }) : finish }))] }));
185
198
  }
186
199
  /** SheetStep — one step of a SheetSteps: its title, then what the member does (a Field, a Pin, a Toggle, the step's own Btn). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.48",
3
+ "version": "0.7.49",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",