@usableapp/cardds 0.7.32 → 0.7.34
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 +15 -0
- package/README.md +1 -1
- package/css/choice.css +16 -1
- package/css/stack.css +8 -5
- package/css/tokens.css +3 -1
- package/dist/cardds.css +27 -7
- package/dist/forms/Pin.d.ts +4 -2
- package/dist/forms/Pin.js +9 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.34 — 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **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.
|
|
9
|
+
### Added
|
|
10
|
+
- 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`).
|
|
11
|
+
|
|
12
|
+
## 0.7.33 — 2026-10-01
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **A StackSheet tab's count grows with the text** (Lh 2026-10-01): the red circle was a fixed 20px while its number followed `--fs-base` — at a large text size the number ran over it. The circle is now cut from its own number's size (20px at the default, 32px at 160%); a long count ("10", "99+") makes it a capsule. The number sits on the circle's middle (it sat low), and the circle stands 8px out of the tab's corner instead of 4 — beside the icon, not on the disc's edge. Nothing to change in a screen.
|
|
16
|
+
- **A screen fills the phone, edge to edge** (Lh 2026-10-01): the column's cap `--screen-w` is `30rem` (480px), was `25rem` (400) — a phone 412–448px wide showed a strip of bare ground each side of the screen, its bars and its sheets. No phone reaches the cap now; it only keeps a tablet / desktop window to a phone-sized column. Nothing to change in a screen; a project that set `--screen-w` itself keeps its value.
|
|
17
|
+
### Added
|
|
18
|
+
- Knobs `--stack-sheet-count-pad-x` (the room beside the number), `--stack-sheet-count-lift` (the room under it — retune for another font), `--stack-sheet-count-out` (how far it stands out of the corner). `--stack-sheet-count-h` as before; its default is `1.175em`, was `1.25rem`.
|
|
19
|
+
|
|
5
20
|
## 0.7.32 — 2026-09-29
|
|
6
21
|
|
|
7
22
|
### Changed
|
package/README.md
CHANGED
|
@@ -650,7 +650,7 @@ Every length token resolves to rem — a multiplier × one of four rem bases
|
|
|
650
650
|
(`--sp-4` = `--sp-base` × 4 = 1rem, `--tap` = `--tap-base` = 3rem, `--r-card` =
|
|
651
651
|
`--r-base` × 2, `--fs-h1` = `--fs-base` × 1.2; see "A project's theme") — so the
|
|
652
652
|
whole system scales with the root font size. `base.css` keeps it at `100%` (the browser's setting, 16px by
|
|
653
|
-
default) and steps narrow phones (≤360px) to `93.75%`, which keeps the
|
|
653
|
+
default) and steps narrow phones (≤360px) to `93.75%`, which keeps the
|
|
654
654
|
column, its 48px taps and its type in proportion on 320–360 screens. Only
|
|
655
655
|
`--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
|
|
656
656
|
An app with its own text-size setting calls `setTextScale(percent)`, which writes
|
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/css/stack.css
CHANGED
|
@@ -292,11 +292,14 @@
|
|
|
292
292
|
.stack-sheet__tab { position: relative; }
|
|
293
293
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
294
294
|
position: absolute;
|
|
295
|
-
top: calc(-1 * var(--sp-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
295
|
+
top: calc(-1 * var(--stack-sheet-count-out, var(--sp-2))); /* how far it stands OUT of the tab's corner, up and to the right: at sp-1 it
|
|
296
|
+
sat exactly on the chosen tab's disc edge — it reads as a mark beside the icon, not part of the disc (Lh 2026-10-01) */
|
|
297
|
+
right: calc(-1 * var(--stack-sheet-count-out, var(--sp-2)));
|
|
298
|
+
min-width: var(--stack-sheet-count-h, 1.175em); /* em, not rem (Lh 2026-10-01): the circle is cut from ITS OWN number's size — at a
|
|
299
|
+
large text size the number grew and a 20px circle did not; 1.175em of a 17px caption = the 20px it always was */
|
|
300
|
+
height: var(--stack-sheet-count-h, 1.175em);
|
|
301
|
+
padding: 0 var(--stack-sheet-count-pad-x, 0.25em) var(--stack-sheet-count-lift, 0.1em); /* "99+" keeps its room at every text size;
|
|
302
|
+
the room UNDER the number lifts it onto the circle's middle: a digit has no descender, so a centred line box sets it low (Lh 2026-10-01) */
|
|
300
303
|
font: var(--type-caption);
|
|
301
304
|
line-height: 1;
|
|
302
305
|
pointer-events: none;
|
package/css/tokens.css
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
:root {
|
|
14
14
|
/* ---- layout ---- */
|
|
15
|
-
--screen-w:
|
|
15
|
+
--screen-w: 30rem; /* 480: the column's CAP — .screen, .action-bar, .sheet-stage. A phone never reaches it (the widest are ~450 CSS px):
|
|
16
|
+
on a phone the screen is the device, edge to edge; the cap only keeps a tablet / desktop window to a phone-sized
|
|
17
|
+
column. Was 25rem: a 412px phone showed a strip each side (Lh 2026-10-01) */
|
|
16
18
|
--screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
|
|
17
19
|
|
|
18
20
|
/* ---- the four bases: what a project retunes (Lh, 2026-09-20) ----
|
package/dist/cardds.css
CHANGED
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
|
|
54
54
|
:root {
|
|
55
55
|
/* ---- layout ---- */
|
|
56
|
-
--screen-w:
|
|
56
|
+
--screen-w: 30rem; /* 480: the column's CAP — .screen, .action-bar, .sheet-stage. A phone never reaches it (the widest are ~450 CSS px):
|
|
57
|
+
on a phone the screen is the device, edge to edge; the cap only keeps a tablet / desktop window to a phone-sized
|
|
58
|
+
column. Was 25rem: a 412px phone showed a strip each side (Lh 2026-10-01) */
|
|
57
59
|
--screen-centre: 40%; /* the human centre of a screen: the middle of its top four fifths — the bottom fifth is the thumb's (Lh, 2026-09-10). .card--centre sits here */
|
|
58
60
|
|
|
59
61
|
/* ---- the four bases: what a project retunes (Lh, 2026-09-20) ----
|
|
@@ -1249,11 +1251,14 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1249
1251
|
.stack-sheet__tab { position: relative; }
|
|
1250
1252
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
1251
1253
|
position: absolute;
|
|
1252
|
-
top: calc(-1 * var(--sp-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1254
|
+
top: calc(-1 * var(--stack-sheet-count-out, var(--sp-2))); /* how far it stands OUT of the tab's corner, up and to the right: at sp-1 it
|
|
1255
|
+
sat exactly on the chosen tab's disc edge — it reads as a mark beside the icon, not part of the disc (Lh 2026-10-01) */
|
|
1256
|
+
right: calc(-1 * var(--stack-sheet-count-out, var(--sp-2)));
|
|
1257
|
+
min-width: var(--stack-sheet-count-h, 1.175em); /* em, not rem (Lh 2026-10-01): the circle is cut from ITS OWN number's size — at a
|
|
1258
|
+
large text size the number grew and a 20px circle did not; 1.175em of a 17px caption = the 20px it always was */
|
|
1259
|
+
height: var(--stack-sheet-count-h, 1.175em);
|
|
1260
|
+
padding: 0 var(--stack-sheet-count-pad-x, 0.25em) var(--stack-sheet-count-lift, 0.1em); /* "99+" keeps its room at every text size;
|
|
1261
|
+
the room UNDER the number lifts it onto the circle's middle: a digit has no descender, so a centred line box sets it low (Lh 2026-10-01) */
|
|
1257
1262
|
font: var(--type-caption);
|
|
1258
1263
|
line-height: 1;
|
|
1259
1264
|
pointer-events: none;
|
|
@@ -2961,7 +2966,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2961
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. */
|
|
2962
2967
|
}
|
|
2963
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) */
|
|
2964
|
-
|
|
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 */
|
|
2965
2985
|
.pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
|
|
2966
2986
|
.pin__cell:focus:placeholder-shown {
|
|
2967
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.34",
|
|
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.",
|