@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 +11 -0
- package/css/base.css +10 -0
- package/css/choice.css +8 -0
- package/css/sheet.css +15 -3
- package/dist/cardds.css +33 -3
- package/dist/forms/Pin.js +25 -3
- package/dist/sheets/Sheet.js +20 -0
- package/dist/sheets/SheetSteps.js +14 -1
- package/package.json +1 -1
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,
|
|
215
|
-
|
|
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:
|
|
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,
|
|
1540
|
-
|
|
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:
|
|
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,
|
|
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) => {
|
|
157
|
-
|
|
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
|
}
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -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) => {
|
|
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.
|
|
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.",
|