@usableapp/cardds 0.7.33 → 0.7.35
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 +12 -0
- package/css/choice.css +16 -1
- package/dist/cardds.css +16 -1
- package/dist/forms/Pin.d.ts +4 -2
- package/dist/forms/Pin.js +9 -2
- package/dist/templates/CallSheet.d.ts +1 -1
- package/dist/templates/CallSheet.js +9 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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.35 — 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`CallSheet`: a sheet on its way out keeps its body until it has slid away** (Lh 2026-10-01). Before, the body was taken out the moment `open` turned null (or another way was called), so an empty sheet slid down. Nothing to change in a screen.
|
|
9
|
+
|
|
10
|
+
## 0.7.34 — 2026-10-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **A secret `Pin`: each typed digit flashes, then is a dot** (Lh 2026-10-01): the digit is readable for half a second, and turns into a dot at once when the next digit lands. Before, the phone's browser decided: it shows the last typed character of a masked field for a second or more, per field — and a Pin is six fields, so several digits of a PIN stood readable together (Android Chrome). The Pin now draws the dot and times the flash itself; `-webkit-text-security` is no longer used. A pasted code does not flash. Nothing to change in a screen.
|
|
14
|
+
### Added
|
|
15
|
+
- Knobs `--pin-flash-time` (how long a typed digit stays readable, `0.5s`; `0s` = never shown) and `--pin-dot` (the dot's size, `0.24em`).
|
|
16
|
+
|
|
5
17
|
## 0.7.33 — 2026-10-01
|
|
6
18
|
|
|
7
19
|
### Changed
|
package/css/choice.css
CHANGED
|
@@ -198,7 +198,22 @@
|
|
|
198
198
|
is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
|
|
199
199
|
}
|
|
200
200
|
.pin__cell:not(:placeholder-shown) { color: var(--pin-cell-filled-ink, var(--accent)); } /* a typed digit: coloured words on the cell's own ground — no fill (Lh 2026-09-24; was an accent-filled pill) */
|
|
201
|
-
|
|
201
|
+
/* a PIN (secret): the cell DRAWS the dot, the character itself is not painted — except for a moment, when it has just been typed
|
|
202
|
+
(Lh 2026-10-01: "each digit must flash, then go at once"). It was `-webkit-text-security: disc`: a phone's browser then shows
|
|
203
|
+
the last typed character of a masked field for a second or more ("password echo", Android) — per FIELD, and a Pin is six
|
|
204
|
+
fields, so several digits stood readable together. Now the timing is the Pin's own: `.is-fresh` (set by Pin on the cell just
|
|
205
|
+
typed, taken off when the next digit lands) plays `pin-flash`; after it, and in every other cell, the fill is transparent and
|
|
206
|
+
the dot is the cell's background, in currentColor — a wrong code's red reaches it. */
|
|
207
|
+
.pin--secret .pin__cell:not(:placeholder-shown) {
|
|
208
|
+
-webkit-text-fill-color: transparent;
|
|
209
|
+
background-image: radial-gradient(circle closest-side, currentColor 96%, transparent);
|
|
210
|
+
background-size: var(--pin-dot, 0.24em) var(--pin-dot, 0.24em);
|
|
211
|
+
background-position: center;
|
|
212
|
+
background-repeat: no-repeat;
|
|
213
|
+
}
|
|
214
|
+
.pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, 0.5s) steps(1); }
|
|
215
|
+
@keyframes pin-flash { 0% { -webkit-text-fill-color: currentColor; background-size: 0 0; } }
|
|
216
|
+
.pin--secret .pin__cell::selection { background: transparent; } /* its digit is selected when the cell takes the cursor: nothing to show of it */
|
|
202
217
|
.pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
|
|
203
218
|
.pin__cell:focus:placeholder-shown {
|
|
204
219
|
background-image: linear-gradient(currentColor, currentColor);
|
package/dist/cardds.css
CHANGED
|
@@ -2966,7 +2966,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2966
2966
|
is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
|
|
2967
2967
|
}
|
|
2968
2968
|
.pin__cell:not(:placeholder-shown) { color: var(--pin-cell-filled-ink, var(--accent)); } /* a typed digit: coloured words on the cell's own ground — no fill (Lh 2026-09-24; was an accent-filled pill) */
|
|
2969
|
-
|
|
2969
|
+
/* a PIN (secret): the cell DRAWS the dot, the character itself is not painted — except for a moment, when it has just been typed
|
|
2970
|
+
(Lh 2026-10-01: "each digit must flash, then go at once"). It was `-webkit-text-security: disc`: a phone's browser then shows
|
|
2971
|
+
the last typed character of a masked field for a second or more ("password echo", Android) — per FIELD, and a Pin is six
|
|
2972
|
+
fields, so several digits stood readable together. Now the timing is the Pin's own: `.is-fresh` (set by Pin on the cell just
|
|
2973
|
+
typed, taken off when the next digit lands) plays `pin-flash`; after it, and in every other cell, the fill is transparent and
|
|
2974
|
+
the dot is the cell's background, in currentColor — a wrong code's red reaches it. */
|
|
2975
|
+
.pin--secret .pin__cell:not(:placeholder-shown) {
|
|
2976
|
+
-webkit-text-fill-color: transparent;
|
|
2977
|
+
background-image: radial-gradient(circle closest-side, currentColor 96%, transparent);
|
|
2978
|
+
background-size: var(--pin-dot, 0.24em) var(--pin-dot, 0.24em);
|
|
2979
|
+
background-position: center;
|
|
2980
|
+
background-repeat: no-repeat;
|
|
2981
|
+
}
|
|
2982
|
+
.pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, 0.5s) steps(1); }
|
|
2983
|
+
@keyframes pin-flash { 0% { -webkit-text-fill-color: currentColor; background-size: 0 0; } }
|
|
2984
|
+
.pin--secret .pin__cell::selection { background: transparent; } /* its digit is selected when the cell takes the cursor: nothing to show of it */
|
|
2970
2985
|
.pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
|
|
2971
2986
|
.pin__cell:focus:placeholder-shown {
|
|
2972
2987
|
background-image: linear-gradient(currentColor, currentColor);
|
package/dist/forms/Pin.d.ts
CHANGED
|
@@ -22,8 +22,10 @@ export interface PinProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defa
|
|
|
22
22
|
match?: string;
|
|
23
23
|
/** the words when a check fails: the code does not match, or the step's button was pressed with cells still empty */
|
|
24
24
|
formatError?: ReactNode;
|
|
25
|
-
/** a SECRET code (a PIN, not an OTP): each typed digit
|
|
26
|
-
*
|
|
25
|
+
/** a SECRET code (a PIN, not an OTP): each typed digit FLASHES — readable for a moment (`--pin-flash-time`), then a dot, and a
|
|
26
|
+
* dot at once when the next digit lands (Lh 2026-10-01). The cell draws the dot itself: the field stays a text field, so the
|
|
27
|
+
* keyboard stays the number pad (a password field brings a full keyboard on some phones) and the browser's own reveal of a
|
|
28
|
+
* masked field — a second per cell, six cells readable together — has no part in it; no SMS autofill, no password manager */
|
|
27
29
|
secret?: boolean;
|
|
28
30
|
/** focus the first empty cell on mount */
|
|
29
31
|
autoFocus?: boolean;
|
package/dist/forms/Pin.js
CHANGED
|
@@ -73,6 +73,7 @@ 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
|
+
const [fresh, setFresh] = useState(null); // the cell whose digit was just typed: a secret Pin shows it for a moment
|
|
76
77
|
useEffect(() => {
|
|
77
78
|
if (!errOn) {
|
|
78
79
|
setCleared(undefined);
|
|
@@ -103,15 +104,18 @@ export function Pin({ length = 6, value, defaultValue = '', label = word('digit'
|
|
|
103
104
|
// the cell still held its red digit (not selected, so it stays visible): what came in is the value minus that digit — one
|
|
104
105
|
// typed digit, or a whole pasted / SMS-autofilled code (then it fills every cell, never just the last digit: timebank 142)
|
|
105
106
|
const fresh = digits.length >= length ? digits.slice(-length) : old && digits.length > 1 && digits.startsWith(old) ? digits.slice(old.length) : old && digits.length > 1 && digits.endsWith(old) ? digits.slice(0, -old.length) : digits;
|
|
107
|
+
setFresh(fresh.length === 1 ? 0 : null);
|
|
106
108
|
set(fresh);
|
|
107
109
|
cell(Math.min(fresh.length, length - 1))?.focus();
|
|
108
110
|
return;
|
|
109
111
|
}
|
|
110
112
|
const at = Math.min(i, code.length); // a code has no gaps: a digit typed further along lands in the first empty cell
|
|
111
113
|
if (!digits) {
|
|
114
|
+
setFresh(null);
|
|
112
115
|
set(code.slice(0, at) + code.slice(at + 1));
|
|
113
116
|
return;
|
|
114
117
|
}
|
|
118
|
+
setFresh(digits.length === 1 ? at : null); // one typed digit flashes; a pasted run does not
|
|
115
119
|
set(code.slice(0, at) + digits + code.slice(at + digits.length)); // a paste (or a typed run) spills over the cells that follow
|
|
116
120
|
cell(Math.min(at + digits.length, length - 1))?.focus();
|
|
117
121
|
};
|
|
@@ -124,12 +128,14 @@ export function Pin({ length = 6, value, defaultValue = '', label = word('digit'
|
|
|
124
128
|
return;
|
|
125
129
|
e.preventDefault();
|
|
126
130
|
restart.current = false;
|
|
131
|
+
setFresh(null);
|
|
127
132
|
set(digits);
|
|
128
133
|
cell(Math.min(digits.length, length - 1))?.focus();
|
|
129
134
|
};
|
|
130
135
|
const key = (i) => (e) => {
|
|
131
136
|
if (e.key === 'Backspace' && !e.currentTarget.value && i > 0) {
|
|
132
137
|
e.preventDefault();
|
|
138
|
+
setFresh(null);
|
|
133
139
|
set(code.slice(0, i - 1) + code.slice(i));
|
|
134
140
|
cell(i - 1)?.focus();
|
|
135
141
|
}
|
|
@@ -143,9 +149,10 @@ export function Pin({ length = 6, value, defaultValue = '', label = word('digit'
|
|
|
143
149
|
}
|
|
144
150
|
};
|
|
145
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')
|
|
146
|
-
setShake(false);
|
|
152
|
+
setShake(false); if (e.animationName === 'pin-flash')
|
|
153
|
+
setFresh(null); onAnimationEnd?.(e); }, children: Array.from({ length }, (_, i) => (
|
|
147
154
|
// no maxLength: the browser would cut a pasted code before this ever saw it. A cell holds one digit — typing into a full one
|
|
148
155
|
// replaces it, so its digit is selected whenever the cell takes the focus
|
|
149
|
-
_jsx("input", { className:
|
|
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)
|
|
150
157
|
e.currentTarget.select(); } }, i))) }));
|
|
151
158
|
}
|
|
@@ -4,7 +4,7 @@ import { type SheetProps } from '../sheets/Sheet.js';
|
|
|
4
4
|
export interface CallSheetSheet {
|
|
5
5
|
/** the sheet head's title */
|
|
6
6
|
title: ReactNode;
|
|
7
|
-
/** the sheet body — rendered only while the sheet is up; a function is called then */
|
|
7
|
+
/** the sheet body — rendered only while the sheet is up (and while it slides away); a function is called then */
|
|
8
8
|
body: ReactNode | (() => ReactNode);
|
|
9
9
|
/** the sheet foot: the one button that finishes the sheet */
|
|
10
10
|
foot?: ReactNode;
|
|
@@ -21,8 +21,16 @@ export function CallSheet({ media, heading, body, action, sheet, ways, open: ope
|
|
|
21
21
|
const list = ways ?? (sheet ? [{ action, sheet }] : []);
|
|
22
22
|
const [on, set] = useControlled(openProp === undefined ? undefined : index(openProp), index(defaultOpen), onOpenChange && ((next) => onOpenChange(next !== null, next)));
|
|
23
23
|
const [at, setAt] = useState('full');
|
|
24
|
+
// a sheet on its way out keeps its body until it has slid away — an emptied sheet sliding down read as a broken one
|
|
25
|
+
const [leaving, setLeaving] = useState(null), [was, setWas] = useState(on);
|
|
26
|
+
if (was !== on) {
|
|
27
|
+
setWas(on);
|
|
28
|
+
if (was !== null)
|
|
29
|
+
setLeaving(was);
|
|
30
|
+
}
|
|
24
31
|
return (_jsxs(Screen, { fill: true, ...rest, children: [_jsxs(Step, { children: [_jsxs(Page, { children: [media, _jsx(CardHead, { level: "h1", heading: heading }), typeof body === 'string' ? _jsx(Text, { level: "body", children: body }) : body, _jsx(CtaPack, { children: list.map((w, i) => (_jsx(Btn, { primary: i === 0, quiet: i > 0, "aria-expanded": on === i, onClick: () => { setAt('full'); set(i); }, children: w.action }, i))) })] }), list.map(({ sheet: sh }, i) => (_jsxs(Sheet, { state: on === i ? at : 'away', onStateChange: (s) => { if (s === 'away')
|
|
25
32
|
set(null);
|
|
26
33
|
else
|
|
27
|
-
setAt(s); },
|
|
34
|
+
setAt(s); }, onSettled: (s) => { if (s === 'away' && leaving === i)
|
|
35
|
+
setLeaving(null); }, states: states, handle: sh.handle ?? handle, children: [_jsx(CardHead, { heading: sh.title }), _jsx(SheetBody, { children: on === i || leaving === i ? (typeof sh.body === 'function' ? sh.body() : sh.body) : null }), (sh.foot != null || sh.footLink != null) && _jsx(CardFoot, { link: sh.footLink, children: sh.foot })] }, i)))] }), children] }));
|
|
28
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.35",
|
|
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.",
|