@usableapp/cardds 0.7.7 → 0.7.9
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 +32 -0
- package/README.md +2 -0
- package/css/base.css +5 -0
- package/css/card.css +8 -2
- package/css/choice.css +1 -0
- package/css/lists.css +2 -0
- package/css/stack.css +1 -1
- package/dist/actions/Link.d.ts +4 -0
- package/dist/actions/WaitLink.d.ts +14 -0
- package/dist/actions/WaitLink.js +22 -0
- package/dist/cardds.css +17 -3
- package/dist/forms/Pin.d.ts +8 -3
- package/dist/forms/Pin.js +32 -17
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/scaffold/slideScreen.d.ts +15 -0
- package/dist/scaffold/slideScreen.js +65 -0
- package/dist/sheets/SheetSteps.d.ts +7 -1
- package/dist/sheets/SheetSteps.js +4 -2
- package/dist/type/Text.d.ts +4 -1
- package/dist/type/Text.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
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.9 — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- A `Screen` reads like a native app: words are no longer highlighted by a long press or a drag, no Copy / Web search
|
|
9
|
+
callout. Fields (input, textarea, contenteditable) stay selectable. The open sheet of a stack is not selectable any more
|
|
10
|
+
either (it was). `Text selectable` opens the few words a member may copy (a code, an address). Knob `--screen-select: text`
|
|
11
|
+
= the old way everywhere. Story `Text · Selectable`.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `slideScreen(update, 'next' | 'back')` + `installScreenSlide()` — one screen to the next slides WHOLE (top · content ·
|
|
15
|
+
bottom), the way a walk's pages do; the default for every route change (Lh 2026-09-25). Before: a route change just
|
|
16
|
+
swapped the screen. Wrap every navigate in `slideScreen`; call `installScreenSlide()` once at start for the back button.
|
|
17
|
+
`data-slide="off"` on a Screen opts it out. Story `Step · WalksOn`.
|
|
18
|
+
- `Pin secret` — a PIN: a dot per typed digit (`-webkit-text-security`), the number pad kept, no SMS autofill. An OTP stays
|
|
19
|
+
in sight (no `secret`). Story `Pin · Secret`; the PIN steps of the SheetSteps stories use it.
|
|
20
|
+
|
|
21
|
+
## 0.7.8 — 2026-09-25
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- `Pin error` (the app's answer, a refused OTP): before — the red digits stayed until the next one was typed. After — the Pin
|
|
25
|
+
shakes red, then EMPTIES itself (`onChange('', false)`), the cursor in the first cell, on the same step — as a `match` miss.
|
|
26
|
+
Give a new `error` value per refusal (a counter) so a second one shakes again.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `WaitLink` — a Link that waits: `until` (ms time) → counts down ("ขอรหัสใหม่ได้ใน 0:27", `waiting={(t) => …}`), takes no
|
|
30
|
+
tap until then. The app owns the rule (30 s, +30 s per ask) and sets the next `until` in its onClick. `Link disabled`
|
|
31
|
+
(muted, knob `--link-disabled-ink`). Stories `WaitLink · Resend`, `SheetSteps · Errors` (ask for a new code).
|
|
32
|
+
- `SheetSteps reached` — controlled progress: how many steps are done. Before: the steps remembered the furthest step reached,
|
|
33
|
+
so after the app sent `at` back (the server refused the code at the end) the later steps still showed ✓ and their summary.
|
|
34
|
+
After: `reached={1}` with `at={1}` → the steps after it are "next" again. Once set the app owns it — keep
|
|
35
|
+
`reached = max(reached, at)` beside `at`. Without it: as before. Story `SheetSteps · Reached` (timebank card 142).
|
|
36
|
+
|
|
5
37
|
## 0.7.7 — 2026-09-25
|
|
6
38
|
|
|
7
39
|
### Changed (breaking for a confirm PIN)
|
package/README.md
CHANGED
|
@@ -322,6 +322,8 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
322
322
|
- **Page that moves** (Lh 2026-09-20) — two ways, never both on one screen:
|
|
323
323
|
- *Pages of words:* `.step > .pages > .page` (one `aria-current="step"`, the rest `inert`) — every page
|
|
324
324
|
one height, the round pager turns them (`slideTo()`), the words slide like a page. No sheet there.
|
|
325
|
+
ONE SCREEN TO THE NEXT (a route change) slides the WHOLE screen the same way: `slideScreen(() => navigate(to), 'next' | 'back')`,
|
|
326
|
+
and `installScreenSlide()` once at app start for the back button (story `Step · WalksOn`). `data-slide="off"` on a Screen = no slide.
|
|
325
327
|
- *Sheets the app calls:* the base offers WAYS IN — sign in · new phone · join — as buttons (`.cta-pack`); each
|
|
326
328
|
way has a sheet that holds its WHOLE form and its own submit on its foot, starting `.sheet--away` (not on the
|
|
327
329
|
screen: slid out, hidden once gone, `inert`). **The wiring is the app's, not cardds's**: a `useState` of which
|
package/css/base.css
CHANGED
|
@@ -107,6 +107,11 @@ button { font: inherit; cursor: pointer; }
|
|
|
107
107
|
.grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
|
|
108
108
|
/* read, not shown (Lh 2026-09-23): words a screen reader needs — a page's h1 the design leaves out — kept in the page, taking no room.
|
|
109
109
|
The 1px is the one the platform needs to keep it in the accessibility tree, not a length of the design. */
|
|
110
|
+
/* a screen reads like a native app (Lh 2026-09-25): words are not highlighted by a long press or a drag, no copy / web-search
|
|
111
|
+
callout — only what is typed into, and words marked selectable (`Text selectable` → .t-select: a code, an address).
|
|
112
|
+
Knob --screen-select: `text` = the web page's old way everywhere. */
|
|
113
|
+
.screen { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); -webkit-touch-callout: none; }
|
|
114
|
+
.screen :is(input, textarea, [contenteditable="true"]), .screen .t-select { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
|
|
110
115
|
.t-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
111
116
|
|
|
112
117
|
/* ---- base content: words straight on the base, no card around them — an
|
package/css/card.css
CHANGED
|
@@ -240,7 +240,8 @@
|
|
|
240
240
|
<section class="page" inert>…</section>
|
|
241
241
|
</div></section> <div class="action-bar action-bar--pager">← →</div> */
|
|
242
242
|
::view-transition-old(root), ::view-transition-new(root) { animation: none; }
|
|
243
|
-
::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in)
|
|
243
|
+
::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
|
|
244
|
+
::view-transition-old(screen-out), ::view-transition-new(screen-in) { /* screen-*: slideScreen — one whole screen to the next (Lh 2026-09-25) */
|
|
244
245
|
animation-duration: var(--motion-slide);
|
|
245
246
|
animation-timing-function: ease-out;
|
|
246
247
|
mix-blend-mode: normal;
|
|
@@ -249,12 +250,17 @@
|
|
|
249
250
|
:root[data-slide="next"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-right; }
|
|
250
251
|
:root[data-slide="back"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-right; }
|
|
251
252
|
:root[data-slide="back"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-left; }
|
|
253
|
+
:root[data-slide="next"]::view-transition-old(screen-out) { animation-name: centre-card-out-left; }
|
|
254
|
+
:root[data-slide="next"]::view-transition-new(screen-in) { animation-name: centre-card-in-right; }
|
|
255
|
+
:root[data-slide="back"]::view-transition-old(screen-out) { animation-name: centre-card-out-right; }
|
|
256
|
+
:root[data-slide="back"]::view-transition-new(screen-in) { animation-name: centre-card-in-left; }
|
|
252
257
|
@keyframes centre-card-out-left { to { translate: -100% 0; opacity: 0; } }
|
|
253
258
|
@keyframes centre-card-out-right { to { translate: 100% 0; opacity: 0; } }
|
|
254
259
|
@keyframes centre-card-in-right { from { translate: 100% 0; opacity: 0; } }
|
|
255
260
|
@keyframes centre-card-in-left { from { translate: -100% 0; opacity: 0; } }
|
|
256
261
|
@media (prefers-reduced-motion: reduce) {
|
|
257
|
-
::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in)
|
|
262
|
+
::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
|
|
263
|
+
::view-transition-old(screen-out), ::view-transition-new(screen-in) { animation: none; }
|
|
258
264
|
}
|
|
259
265
|
/* the page dots under the card sit centred under it — the indicator belongs
|
|
260
266
|
with the page that changes, never in the bar (Lh, 2026-09-12) */
|
package/css/choice.css
CHANGED
|
@@ -210,6 +210,7 @@
|
|
|
210
210
|
edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
|
|
211
211
|
is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
|
|
212
212
|
.pin__cell { caret-color: transparent; }
|
|
213
|
+
.pin--secret .pin__cell { -webkit-text-security: disc; text-security: disc; } /* a PIN: a dot per digit, the number pad kept (Lh 2026-09-25) */
|
|
213
214
|
.pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
|
|
214
215
|
.pin__cell:focus:placeholder-shown {
|
|
215
216
|
background-image: linear-gradient(currentColor, currentColor);
|
package/css/lists.css
CHANGED
|
@@ -206,4 +206,6 @@
|
|
|
206
206
|
.link--sm { position: relative; font-size: var(--link-sm-font-size, var(--fs-2xs)); min-height: var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)); }
|
|
207
207
|
.link--sm::after { content: ""; position: absolute; inset: calc((var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) - var(--tap-sm)) / 2) 0; }
|
|
208
208
|
.link--muted { color: var(--link-muted-ink, var(--card-muted)); }
|
|
209
|
+
.link:disabled { color: var(--link-disabled-ink, var(--card-muted)); cursor: default; } /* inactive = muted (a WaitLink counting down) */
|
|
210
|
+
.link--wait { font-variant-numeric: tabular-nums; } /* the countdown's digits keep their width: the words do not jiggle each second */
|
|
209
211
|
.card--3 .link--muted { color: var(--card-3-muted); }
|
package/css/stack.css
CHANGED
|
@@ -328,4 +328,4 @@
|
|
|
328
328
|
/* a stacked sheet is a control until it is open — tapped, never read into: no text selection (a drag, a double tap, a long press),
|
|
329
329
|
no grey flash, no iOS callout; its head stays one when open. Only the OPEN sheet's body is text to select (Lh 2026-09-23) */
|
|
330
330
|
.sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; }
|
|
331
|
-
.sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select:
|
|
331
|
+
.sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); } /* the open sheet reads as the rest of the screen: not selectable (Lh 2026-09-25; it was text) */
|
package/dist/actions/Link.d.ts
CHANGED
|
@@ -9,9 +9,13 @@ export interface LinkProps extends ComponentProps<'a'> {
|
|
|
9
9
|
sm?: boolean;
|
|
10
10
|
/** without an href it renders as a button */
|
|
11
11
|
href?: string;
|
|
12
|
+
/** a button link that takes no taps now (reads inactive: muted) — `WaitLink` while it counts down */
|
|
13
|
+
disabled?: boolean;
|
|
12
14
|
}
|
|
13
15
|
/** Link — an inline text action big enough to tap (40px): "See all ↗" in a card head, "Skip" between pager buttons. */
|
|
14
16
|
export declare function Link({ muted, sm, icon, href, className, children, ...rest }: LinkProps): import("react").DetailedReactHTMLElement<{
|
|
17
|
+
/** a button link that takes no taps now (reads inactive: muted) — `WaitLink` while it counts down */
|
|
18
|
+
disabled?: boolean;
|
|
15
19
|
ref?: import("react").Ref<HTMLAnchorElement> | undefined;
|
|
16
20
|
key?: import("react").Key | null | undefined;
|
|
17
21
|
download?: any;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type LinkProps } from './Link.js';
|
|
3
|
+
export interface WaitLinkProps extends Omit<LinkProps, 'href'> {
|
|
4
|
+
/** CONTROLLED: when it may be pressed again (a time in ms, `Date.now()` based). Before that it counts down and takes no taps;
|
|
5
|
+
* null / past = ready. The APP owns the rule — 30 s, +30 s each time — and sets a new `until` in its onClick. */
|
|
6
|
+
until?: number | null;
|
|
7
|
+
/** the words while it waits, given the time left as "0:27" (default: the children, then the time) */
|
|
8
|
+
waiting?: (left: string) => ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* WaitLink — a Link that waits: "ขอรหัสใหม่ได้ใน 0:27", counting down each second, then "ขอรหัสใหม่" to press (a new OTP, a retry).
|
|
12
|
+
* A link, not a button: it is the step's second way, under its control; in a SheetStep it stands centred on its own row.
|
|
13
|
+
*/
|
|
14
|
+
export declare function WaitLink({ until, waiting, children, className, disabled, ...rest }: WaitLinkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { cx } from '../cx.js';
|
|
4
|
+
import { Link } from './Link.js';
|
|
5
|
+
/**
|
|
6
|
+
* WaitLink — a Link that waits: "ขอรหัสใหม่ได้ใน 0:27", counting down each second, then "ขอรหัสใหม่" to press (a new OTP, a retry).
|
|
7
|
+
* A link, not a button: it is the step's second way, under its control; in a SheetStep it stands centred on its own row.
|
|
8
|
+
*/
|
|
9
|
+
export function WaitLink({ until, waiting, children, className, disabled, ...rest }) {
|
|
10
|
+
const [now, setNow] = useState(() => Date.now());
|
|
11
|
+
const left = until != null ? Math.ceil((until - now) / 1000) : 0;
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (until == null || until <= Date.now())
|
|
14
|
+
return;
|
|
15
|
+
setNow(Date.now());
|
|
16
|
+
const t = setInterval(() => { setNow(Date.now()); if (Date.now() >= until)
|
|
17
|
+
clearInterval(t); }, 250);
|
|
18
|
+
return () => clearInterval(t);
|
|
19
|
+
}, [until]);
|
|
20
|
+
const time = `${Math.floor(Math.max(left, 0) / 60)}:${String(Math.max(left, 0) % 60).padStart(2, '0')}`;
|
|
21
|
+
return (_jsx(Link, { className: cx('link--wait', className), disabled: left > 0 || disabled, ...rest, children: left > 0 ? (waiting ? waiting(time) : _jsxs(_Fragment, { children: [children, " ", time] })) : children }));
|
|
22
|
+
}
|
package/dist/cardds.css
CHANGED
|
@@ -488,6 +488,11 @@ button { font: inherit; cursor: pointer; }
|
|
|
488
488
|
.grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
|
|
489
489
|
/* read, not shown (Lh 2026-09-23): words a screen reader needs — a page's h1 the design leaves out — kept in the page, taking no room.
|
|
490
490
|
The 1px is the one the platform needs to keep it in the accessibility tree, not a length of the design. */
|
|
491
|
+
/* a screen reads like a native app (Lh 2026-09-25): words are not highlighted by a long press or a drag, no copy / web-search
|
|
492
|
+
callout — only what is typed into, and words marked selectable (`Text selectable` → .t-select: a code, an address).
|
|
493
|
+
Knob --screen-select: `text` = the web page's old way everywhere. */
|
|
494
|
+
.screen { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); -webkit-touch-callout: none; }
|
|
495
|
+
.screen :is(input, textarea, [contenteditable="true"]), .screen .t-select { -webkit-user-select: text; user-select: text; -webkit-touch-callout: default; }
|
|
491
496
|
.t-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
492
497
|
|
|
493
498
|
/* ---- base content: words straight on the base, no card around them — an
|
|
@@ -773,7 +778,8 @@ button { font: inherit; cursor: pointer; }
|
|
|
773
778
|
<section class="page" inert>…</section>
|
|
774
779
|
</div></section> <div class="action-bar action-bar--pager">← →</div> */
|
|
775
780
|
::view-transition-old(root), ::view-transition-new(root) { animation: none; }
|
|
776
|
-
::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in)
|
|
781
|
+
::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
|
|
782
|
+
::view-transition-old(screen-out), ::view-transition-new(screen-in) { /* screen-*: slideScreen — one whole screen to the next (Lh 2026-09-25) */
|
|
777
783
|
animation-duration: var(--motion-slide);
|
|
778
784
|
animation-timing-function: ease-out;
|
|
779
785
|
mix-blend-mode: normal;
|
|
@@ -782,12 +788,17 @@ button { font: inherit; cursor: pointer; }
|
|
|
782
788
|
:root[data-slide="next"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-right; }
|
|
783
789
|
:root[data-slide="back"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-right; }
|
|
784
790
|
:root[data-slide="back"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-left; }
|
|
791
|
+
:root[data-slide="next"]::view-transition-old(screen-out) { animation-name: centre-card-out-left; }
|
|
792
|
+
:root[data-slide="next"]::view-transition-new(screen-in) { animation-name: centre-card-in-right; }
|
|
793
|
+
:root[data-slide="back"]::view-transition-old(screen-out) { animation-name: centre-card-out-right; }
|
|
794
|
+
:root[data-slide="back"]::view-transition-new(screen-in) { animation-name: centre-card-in-left; }
|
|
785
795
|
@keyframes centre-card-out-left { to { translate: -100% 0; opacity: 0; } }
|
|
786
796
|
@keyframes centre-card-out-right { to { translate: 100% 0; opacity: 0; } }
|
|
787
797
|
@keyframes centre-card-in-right { from { translate: 100% 0; opacity: 0; } }
|
|
788
798
|
@keyframes centre-card-in-left { from { translate: -100% 0; opacity: 0; } }
|
|
789
799
|
@media (prefers-reduced-motion: reduce) {
|
|
790
|
-
::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in)
|
|
800
|
+
::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in),
|
|
801
|
+
::view-transition-old(screen-out), ::view-transition-new(screen-in) { animation: none; }
|
|
791
802
|
}
|
|
792
803
|
/* the page dots under the card sit centred under it — the indicator belongs
|
|
793
804
|
with the page that changes, never in the bar (Lh, 2026-09-12) */
|
|
@@ -1172,7 +1183,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1172
1183
|
/* a stacked sheet is a control until it is open — tapped, never read into: no text selection (a drag, a double tap, a long press),
|
|
1173
1184
|
no grey flash, no iOS callout; its head stays one when open. Only the OPEN sheet's body is text to select (Lh 2026-09-23) */
|
|
1174
1185
|
.sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; }
|
|
1175
|
-
.sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select:
|
|
1186
|
+
.sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); } /* the open sheet reads as the rest of the screen: not selectable (Lh 2026-09-25; it was text) */
|
|
1176
1187
|
|
|
1177
1188
|
/* ---- css/sheet.css — sheet single: .sheet — one card as a bottom sheet, peek / half / full ---- */
|
|
1178
1189
|
/* ============================================================
|
|
@@ -2809,6 +2820,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2809
2820
|
edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
|
|
2810
2821
|
is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
|
|
2811
2822
|
.pin__cell { caret-color: transparent; }
|
|
2823
|
+
.pin--secret .pin__cell { -webkit-text-security: disc; text-security: disc; } /* a PIN: a dot per digit, the number pad kept (Lh 2026-09-25) */
|
|
2812
2824
|
.pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
|
|
2813
2825
|
.pin__cell:focus:placeholder-shown {
|
|
2814
2826
|
background-image: linear-gradient(currentColor, currentColor);
|
|
@@ -3123,6 +3135,8 @@ button { font: inherit; cursor: pointer; }
|
|
|
3123
3135
|
.link--sm { position: relative; font-size: var(--link-sm-font-size, var(--fs-2xs)); min-height: var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)); }
|
|
3124
3136
|
.link--sm::after { content: ""; position: absolute; inset: calc((var(--link-sm-min-h, calc(var(--tap-base) * 7 / 12)) - var(--tap-sm)) / 2) 0; }
|
|
3125
3137
|
.link--muted { color: var(--link-muted-ink, var(--card-muted)); }
|
|
3138
|
+
.link:disabled { color: var(--link-disabled-ink, var(--card-muted)); cursor: default; } /* inactive = muted (a WaitLink counting down) */
|
|
3139
|
+
.link--wait { font-variant-numeric: tabular-nums; } /* the countdown's digits keep their width: the words do not jiggle each second */
|
|
3126
3140
|
.card--3 .link--muted { color: var(--card-3-muted); }
|
|
3127
3141
|
|
|
3128
3142
|
/* ---- css/media.css — cover card, quote, tile grid, fold, mosaic, wave ---- */
|
package/dist/forms/Pin.d.ts
CHANGED
|
@@ -10,8 +10,10 @@ export interface PinProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defa
|
|
|
10
10
|
label?: string;
|
|
11
11
|
/** the code so far, after every change; `complete` once every cell holds a digit */
|
|
12
12
|
onChange?: (code: string, complete: boolean) => void;
|
|
13
|
-
/** the code is wrong
|
|
14
|
-
*
|
|
13
|
+
/** the code is wrong (the app's answer — an OTP the server refused): the Pin SHAKES with red edges and its digits in sight,
|
|
14
|
+
* then empties itself (`onChange('', false)`) and the cursor waits in the first cell for a new try — the same as a `match`
|
|
15
|
+
* miss (Lh 2026-09-25; the red digits used to stay until the next one was typed). Give a NEW value for each refusal (a
|
|
16
|
+
* counter) so a second one shakes again. The words are the step's (`SheetStep error`): the app keeps them until it clears `error`. */
|
|
15
17
|
error?: unknown;
|
|
16
18
|
/** a FORMAT check on the client: the code it must equal (the first PIN, when this one confirms it). Once every cell is filled
|
|
17
19
|
* and it differs, the Pin SHAKES with red edges and its digits in sight, the step shows `formatError` under it, then the Pin
|
|
@@ -20,6 +22,9 @@ export interface PinProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defa
|
|
|
20
22
|
match?: string;
|
|
21
23
|
/** the words when a check fails: the code does not match, or the step's button was pressed with cells still empty */
|
|
22
24
|
formatError?: ReactNode;
|
|
25
|
+
/** a SECRET code (a PIN, not an OTP): each typed digit shows as a dot — `-webkit-text-security`, so the keyboard stays the
|
|
26
|
+
* number pad (a password field brings a full keyboard on some phones); no SMS autofill, no password manager (Lh 2026-09-25) */
|
|
27
|
+
secret?: boolean;
|
|
23
28
|
/** focus the first empty cell on mount */
|
|
24
29
|
autoFocus?: boolean;
|
|
25
30
|
}
|
|
@@ -29,4 +34,4 @@ export interface PinProps extends Omit<ComponentProps<'div'>, 'onChange' | 'defa
|
|
|
29
34
|
* arrows walk the cells, a pasted code fills them from the one it landed in; only digits stay. The first cell takes the SMS
|
|
30
35
|
* one-time-code autofill. The code is a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`.
|
|
31
36
|
*/
|
|
32
|
-
export declare function Pin({ length, value, defaultValue, label, onChange, autoFocus, error, match, formatError, className, onAnimationEnd, ...rest }: PinProps): import("react").JSX.Element;
|
|
37
|
+
export declare function Pin({ length, value, defaultValue, label, onChange, autoFocus, error, match, formatError, secret, className, onAnimationEnd, ...rest }: PinProps): import("react").JSX.Element;
|
package/dist/forms/Pin.js
CHANGED
|
@@ -9,11 +9,28 @@ const RESET_MS = 900; // a code that does not match: red + shake this long, then
|
|
|
9
9
|
* arrows walk the cells, a pasted code fills them from the one it landed in; only digits stay. The first cell takes the SMS
|
|
10
10
|
* one-time-code autofill. The code is a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`.
|
|
11
11
|
*/
|
|
12
|
-
export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onChange, autoFocus, error, match, formatError, className, onAnimationEnd, ...rest }) {
|
|
12
|
+
export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onChange, autoFocus, error, match, formatError, secret, className, onAnimationEnd, ...rest }) {
|
|
13
13
|
const ref = useRef(null);
|
|
14
14
|
const [inner, setInner] = useState(defaultValue);
|
|
15
15
|
const code = (value ?? inner).replace(/\D/g, '').slice(0, length);
|
|
16
16
|
const cell = (i) => ref.current?.querySelectorAll('.pin__cell')[i];
|
|
17
|
+
/* a miss — a match that failed, or the app's error: shake, red, then the cells empty for a new try on the same step */
|
|
18
|
+
const latest = useRef({ value, onChange, error });
|
|
19
|
+
latest.current = { value, onChange, error };
|
|
20
|
+
const miss = () => {
|
|
21
|
+
restart.current = true;
|
|
22
|
+
setShake(true);
|
|
23
|
+
clearTimeout(reset.current);
|
|
24
|
+
reset.current = setTimeout(() => {
|
|
25
|
+
setBad(false);
|
|
26
|
+
setCleared(latest.current.error);
|
|
27
|
+
restart.current = false;
|
|
28
|
+
if (latest.current.value === undefined)
|
|
29
|
+
setInner('');
|
|
30
|
+
latest.current.onChange?.('', false);
|
|
31
|
+
cell(0)?.focus();
|
|
32
|
+
}, RESET_MS);
|
|
33
|
+
};
|
|
17
34
|
const set = (next) => {
|
|
18
35
|
next = next.slice(0, length);
|
|
19
36
|
if (bad || said.current) {
|
|
@@ -22,20 +39,12 @@ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onC
|
|
|
22
39
|
said.current = false;
|
|
23
40
|
}
|
|
24
41
|
clearTimeout(reset.current);
|
|
42
|
+
setCleared(error); // typed before the cells emptied: the new try has begun, the red goes
|
|
25
43
|
if (match != null && next.length === length && next !== match) {
|
|
26
44
|
setBad(true);
|
|
27
45
|
say(words);
|
|
28
46
|
said.current = true;
|
|
29
|
-
|
|
30
|
-
setShake(true);
|
|
31
|
-
reset.current = setTimeout(() => {
|
|
32
|
-
setBad(false);
|
|
33
|
-
restart.current = false;
|
|
34
|
-
if (value === undefined)
|
|
35
|
-
setInner('');
|
|
36
|
-
onChange?.('', false);
|
|
37
|
-
cell(0)?.focus();
|
|
38
|
-
}, RESET_MS);
|
|
47
|
+
miss();
|
|
39
48
|
}
|
|
40
49
|
if (value === undefined)
|
|
41
50
|
setInner(next);
|
|
@@ -64,15 +73,21 @@ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onC
|
|
|
64
73
|
}
|
|
65
74
|
return null;
|
|
66
75
|
}, () => cell(Math.min(code.length, length - 1))?.focus());
|
|
67
|
-
const
|
|
76
|
+
const errOn = error != null && error !== false && error !== '';
|
|
77
|
+
const [cleared, setCleared] = useState(undefined); // the app's error whose miss has played out (its cells emptied)
|
|
78
|
+
const wrong = (errOn && error !== cleared) || bad;
|
|
68
79
|
const restart = useRef(false);
|
|
69
80
|
useEffect(() => {
|
|
70
|
-
if (!
|
|
81
|
+
if (!errOn) {
|
|
82
|
+
setCleared(undefined);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (error === cleared)
|
|
71
86
|
return;
|
|
72
|
-
restart.current = true; // the wrong code stays in sight (red); the next digit typed starts a new one from the first cell
|
|
73
87
|
cell(0)?.focus();
|
|
88
|
+
miss(); // each new error, once
|
|
74
89
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- each new error, once
|
|
75
|
-
}, [
|
|
90
|
+
}, [error]);
|
|
76
91
|
/* the app emptied the code (a new try: it went back after a mismatch): the red goes with it */
|
|
77
92
|
useEffect(() => {
|
|
78
93
|
if (code || !bad)
|
|
@@ -131,10 +146,10 @@ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onC
|
|
|
131
146
|
cell(i + 1)?.focus();
|
|
132
147
|
}
|
|
133
148
|
};
|
|
134
|
-
return (_jsx("div", { ref: ref, className: cx('pin', wrong && 'pin--error', shake && 'pin--shake', className), ...rest, onAnimationEnd: (e) => { if (e.animationName === 'pin-shake')
|
|
149
|
+
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')
|
|
135
150
|
setShake(false); onAnimationEnd?.(e); }, children: Array.from({ length }, (_, i) => (
|
|
136
151
|
// no maxLength: the browser would cut a pasted code before this ever saw it. A cell holds one digit — typing into a full one
|
|
137
152
|
// replaces it, so its digit is selected whenever the cell takes the focus
|
|
138
|
-
_jsx("input", { className: "pin__cell", inputMode: "numeric", pattern: "[0-9]*", placeholder: " ", value: code[i] ?? '', "aria-label": `${label} ${i + 1}`, "aria-invalid": wrong || undefined, autoComplete: i === 0 ? 'one-time-code' : 'off', onChange: input(i), onPaste: paste, onKeyDown: key(i), onFocus: (e) => { if (!restart.current)
|
|
153
|
+
_jsx("input", { className: "pin__cell", 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)
|
|
139
154
|
e.currentTarget.select(); } }, i))) }));
|
|
140
155
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './scaffold/FilterRow.js';
|
|
|
14
14
|
export * from './scaffold/Page.js';
|
|
15
15
|
export * from './scaffold/EmptyState.js';
|
|
16
16
|
export * from './scaffold/Centre.js';
|
|
17
|
+
export * from './scaffold/slideScreen.js';
|
|
17
18
|
export * from './cards/Card.js';
|
|
18
19
|
export * from './cards/CardHead.js';
|
|
19
20
|
export * from './cards/CardFoot.js';
|
|
@@ -38,6 +39,7 @@ export * from './actions/Segment.js';
|
|
|
38
39
|
export * from './actions/Fab.js';
|
|
39
40
|
export * from './actions/BtnRow.js';
|
|
40
41
|
export * from './actions/Link.js';
|
|
42
|
+
export * from './actions/WaitLink.js';
|
|
41
43
|
export * from './forms/Field.js';
|
|
42
44
|
export * from './forms/FileBtn.js';
|
|
43
45
|
export * from './forms/AddRow.js';
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export * from './scaffold/FilterRow.js';
|
|
|
18
18
|
export * from './scaffold/Page.js';
|
|
19
19
|
export * from './scaffold/EmptyState.js';
|
|
20
20
|
export * from './scaffold/Centre.js';
|
|
21
|
+
export * from './scaffold/slideScreen.js';
|
|
21
22
|
export * from './cards/Card.js';
|
|
22
23
|
export * from './cards/CardHead.js';
|
|
23
24
|
export * from './cards/CardFoot.js';
|
|
@@ -42,6 +43,7 @@ export * from './actions/Segment.js';
|
|
|
42
43
|
export * from './actions/Fab.js';
|
|
43
44
|
export * from './actions/BtnRow.js';
|
|
44
45
|
export * from './actions/Link.js';
|
|
46
|
+
export * from './actions/WaitLink.js';
|
|
45
47
|
export * from './forms/Field.js';
|
|
46
48
|
export * from './forms/FileBtn.js';
|
|
47
49
|
export * from './forms/AddRow.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SlideDirection } from './Centre.js';
|
|
2
|
+
/**
|
|
3
|
+
* slideScreen — go to the NEXT SCREEN (a route change) with the whole screen sliding, as a walk's pages do. Wrap the app's
|
|
4
|
+
* navigate in it: `slideScreen(() => navigate('/download'))` — `back` for a way back: `slideScreen(() => navigate(-1), 'back')`.
|
|
5
|
+
* The update may render at once or later (a route's chunk loading): the slide waits for the new Screen (at most 0.8 s).
|
|
6
|
+
* A Screen with `data-slide="off"` (or none at all) is not slid. `scope`: where the one Screen is (default the document — an
|
|
7
|
+
* app has one). Without view transitions (the browser floor) it just runs the update.
|
|
8
|
+
*/
|
|
9
|
+
export declare function slideScreen(update: () => unknown, direction?: SlideDirection, scope?: ParentNode): void;
|
|
10
|
+
/**
|
|
11
|
+
* installScreenSlide — call ONCE when the app starts: the phone's / browser's BACK (and forward) buttons slide the screen too,
|
|
12
|
+
* the way `back` (and `next`) does. It listens to `popstate` before the router renders; the direction is read from the history
|
|
13
|
+
* entry's index (`history.state.idx`, which React Router keeps; without it: back). Returns the uninstall.
|
|
14
|
+
*/
|
|
15
|
+
export declare function installScreenSlide(): () => void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* ONE SCREEN TO THE NEXT (Lh 2026-09-25): the whole screen slides — top, content, bottom — the way a walk's pages do (`next`: the
|
|
2
|
+
old one out to the left, the new one in from the right; `back`: the reverse; `--motion-slide`, card.css). It is a view
|
|
3
|
+
transition named for the move only: the leaving `.screen` is `screen-out`, the arriving one `screen-in`. */
|
|
4
|
+
const screenIn = (scope) => scope.querySelector('.screen:not(.screen .screen)');
|
|
5
|
+
/** wait until the screen in `scope` CHANGED — another element, or new content in the same one (React may keep the element when
|
|
6
|
+
both routes render a Screen in the same place) — a route rendered, sync or not; at most `ms` */
|
|
7
|
+
function newScreen(scope, old, ms = 800) {
|
|
8
|
+
return new Promise((done) => {
|
|
9
|
+
const now = screenIn(scope);
|
|
10
|
+
if (now !== old)
|
|
11
|
+
return done(now);
|
|
12
|
+
const root = scope instanceof Document ? scope.documentElement : scope;
|
|
13
|
+
const mo = new MutationObserver(() => { mo.disconnect(); clearTimeout(t); done(screenIn(scope)); });
|
|
14
|
+
mo.observe(root, { childList: true, subtree: true });
|
|
15
|
+
const t = setTimeout(() => { mo.disconnect(); done(screenIn(scope)); }, ms);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
let running = false;
|
|
19
|
+
/**
|
|
20
|
+
* slideScreen — go to the NEXT SCREEN (a route change) with the whole screen sliding, as a walk's pages do. Wrap the app's
|
|
21
|
+
* navigate in it: `slideScreen(() => navigate('/download'))` — `back` for a way back: `slideScreen(() => navigate(-1), 'back')`.
|
|
22
|
+
* The update may render at once or later (a route's chunk loading): the slide waits for the new Screen (at most 0.8 s).
|
|
23
|
+
* A Screen with `data-slide="off"` (or none at all) is not slid. `scope`: where the one Screen is (default the document — an
|
|
24
|
+
* app has one). Without view transitions (the browser floor) it just runs the update.
|
|
25
|
+
*/
|
|
26
|
+
export function slideScreen(update, direction = 'next', scope = document) {
|
|
27
|
+
const root = document.documentElement;
|
|
28
|
+
const before = screenIn(scope);
|
|
29
|
+
if (typeof document.startViewTransition !== 'function' || running || !before || before.dataset.slide === 'off') {
|
|
30
|
+
update();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
running = true;
|
|
34
|
+
before.style.viewTransitionName = 'screen-out';
|
|
35
|
+
root.dataset.slide = direction;
|
|
36
|
+
let after = null;
|
|
37
|
+
const t = document.startViewTransition(async () => {
|
|
38
|
+
await update();
|
|
39
|
+
after = await newScreen(scope, before);
|
|
40
|
+
before.style.viewTransitionName = '';
|
|
41
|
+
if (after && after.dataset.slide !== 'off')
|
|
42
|
+
after.style.viewTransitionName = 'screen-in'; // the same element too: out, then in
|
|
43
|
+
});
|
|
44
|
+
void t.finished.finally(() => { running = false; delete root.dataset.slide; if (after)
|
|
45
|
+
after.style.viewTransitionName = ''; });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* installScreenSlide — call ONCE when the app starts: the phone's / browser's BACK (and forward) buttons slide the screen too,
|
|
49
|
+
* the way `back` (and `next`) does. It listens to `popstate` before the router renders; the direction is read from the history
|
|
50
|
+
* entry's index (`history.state.idx`, which React Router keeps; without it: back). Returns the uninstall.
|
|
51
|
+
*/
|
|
52
|
+
export function installScreenSlide() {
|
|
53
|
+
let idx = history.state?.idx;
|
|
54
|
+
const push = history.pushState.bind(history), replace = history.replaceState.bind(history);
|
|
55
|
+
history.pushState = (...a) => { push(...a); idx = history.state?.idx; };
|
|
56
|
+
history.replaceState = (...a) => { replace(...a); idx = history.state?.idx; };
|
|
57
|
+
const pop = () => {
|
|
58
|
+
const to = history.state?.idx;
|
|
59
|
+
const dir = to != null && idx != null && to > idx ? 'next' : 'back';
|
|
60
|
+
idx = to;
|
|
61
|
+
slideScreen(() => undefined, dir); // the router renders on its own, right after this: the slide waits for its new Screen
|
|
62
|
+
};
|
|
63
|
+
addEventListener('popstate', pop, { capture: true });
|
|
64
|
+
return () => { removeEventListener('popstate', pop, { capture: true }); history.pushState = push; history.replaceState = replace; };
|
|
65
|
+
}
|
|
@@ -9,6 +9,12 @@ export interface SheetStepsProps extends ComponentProps<'div'> {
|
|
|
9
9
|
* work is really finished (the code checked by the server, the two PINs match), `setAt(at + 1)`; after the last step the app sends. `at = count` = every step done —
|
|
10
10
|
* the app then closes its Sheet (`state="away"`, a beat after the fold) and says so on the page: the steps never close it themselves. */
|
|
11
11
|
at?: number;
|
|
12
|
+
/** CONTROLLED progress: how many steps are DONE (0-based: the steps before this one show ✓). Without it the steps remember the
|
|
13
|
+
* furthest step reached — a step once passed stays done when `at` goes back. Set it when that progress is GONE: the server
|
|
14
|
+
* refused the code at the end, the app sends `at` back to the code step and clears the PINs → `reached={1}`, and the steps
|
|
15
|
+
* after it are "next" again, no ✓, no summary. Once set, the APP owns it: raise it as the member goes on (keep
|
|
16
|
+
* `reached = max(reached, at)` beside `at`), or a tap back in a `reversible` walk forgets what is after. Read as at least `at`. Lh 2026-09-25 */
|
|
17
|
+
reached?: number;
|
|
12
18
|
/** UNCONTROLLED: the step it starts on (default 0) */
|
|
13
19
|
defaultAt?: number;
|
|
14
20
|
/** the steps ASK to move — a tap on another step, a swipe, a skip. Controlled: set `at` to it (or don't). */
|
|
@@ -24,7 +30,7 @@ export interface SheetStepsProps extends ComponentProps<'div'> {
|
|
|
24
30
|
* steps on the last one. Goes in a `SheetBody`.
|
|
25
31
|
* The sheet may be dragged down to peek and back — the body keeps where it was scrolled.
|
|
26
32
|
*/
|
|
27
|
-
export declare function SheetSteps({ at, defaultAt, onAtChange, reversible, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }: SheetStepsProps): import("react").JSX.Element;
|
|
33
|
+
export declare function SheetSteps({ at, reached, defaultAt, onAtChange, reversible, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }: SheetStepsProps): import("react").JSX.Element;
|
|
28
34
|
export interface SheetStepProps extends Omit<ComponentProps<'li'>, 'title'> {
|
|
29
35
|
/** the step's name — always shown: over the open step, on the folded line of a done one */
|
|
30
36
|
title: ReactNode;
|
|
@@ -16,7 +16,7 @@ const StepCtx = createContext(null);
|
|
|
16
16
|
* steps on the last one. Goes in a `SheetBody`.
|
|
17
17
|
* The sheet may be dragged down to peek and back — the body keeps where it was scrolled.
|
|
18
18
|
*/
|
|
19
|
-
export function SheetSteps({ at, defaultAt = 0, onAtChange, reversible = false, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }) {
|
|
19
|
+
export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible = false, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }) {
|
|
20
20
|
const [inner, setInner] = useState(defaultAt);
|
|
21
21
|
const current = at ?? inner;
|
|
22
22
|
const steps = Children.toArray(children).filter(isValidElement);
|
|
@@ -25,7 +25,9 @@ export function SheetSteps({ at, defaultAt = 0, onAtChange, reversible = false,
|
|
|
25
25
|
const [furthest, setFurthest] = useState(current);
|
|
26
26
|
if (current > furthest)
|
|
27
27
|
setFurthest(current);
|
|
28
|
-
|
|
28
|
+
if (reached != null && furthest !== Math.max(reached, current))
|
|
29
|
+
setFurthest(Math.max(reached, current)); // the app's word wins, and is remembered from there
|
|
30
|
+
const reach = reached != null ? Math.max(reached, current) : Math.max(furthest, current);
|
|
29
31
|
const ref = useRef(null);
|
|
30
32
|
const first = useRef(true);
|
|
31
33
|
const holderRef = useRef(null);
|
package/dist/type/Text.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ export interface TextProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
17
17
|
/** the element; defaults per level (h2 for display/h1/h2, p for title/body/stat, span for the rest) */
|
|
18
18
|
/** READ, NOT SHOWN: the words stay in the page for a screen reader (a page's h1 that the design does not show), and take no room */
|
|
19
19
|
sr?: boolean;
|
|
20
|
+
/** the member may select and copy these words (a code, an address, a reference number) — everything else in a Screen is not
|
|
21
|
+
* selectable, like a native app (Lh 2026-09-25) */
|
|
22
|
+
selectable?: boolean;
|
|
20
23
|
as?: 'h1' | 'h2' | 'h3' | 'p' | 'span' | 'strong' | 'div';
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
@@ -24,7 +27,7 @@ export interface TextProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
24
27
|
* Headings inside a card head are h2 by default; a screen title in a TopBar is h1.
|
|
25
28
|
* Everything scales from the root font-size, so there are no pixel sizes to pick.
|
|
26
29
|
*/
|
|
27
|
-
export declare function Text({ level, xl, mono, muted, caps, grow, sr, as, className, ...rest }: TextProps): import("react").DetailedReactHTMLElement<{
|
|
30
|
+
export declare function Text({ level, xl, mono, muted, caps, grow, sr, selectable, as, className, ...rest }: TextProps): import("react").DetailedReactHTMLElement<{
|
|
28
31
|
/** the element it renders (React 19 passes `ref` as a prop) */
|
|
29
32
|
ref?: React.Ref<HTMLElement>;
|
|
30
33
|
defaultChecked?: boolean | undefined;
|
package/dist/type/Text.js
CHANGED
|
@@ -8,9 +8,9 @@ const DEFAULT_AS = {
|
|
|
8
8
|
* Headings inside a card head are h2 by default; a screen title in a TopBar is h1.
|
|
9
9
|
* Everything scales from the root font-size, so there are no pixel sizes to pick.
|
|
10
10
|
*/
|
|
11
|
-
export function Text({ level, xl, mono, muted, caps, grow, sr, as, className, ...rest }) {
|
|
11
|
+
export function Text({ level, xl, mono, muted, caps, grow, sr, selectable, as, className, ...rest }) {
|
|
12
12
|
return createElement(as ?? DEFAULT_AS[level] ?? 'span', {
|
|
13
|
-
className: cx(`t-${level}`, level === 'stat' && xl && 't-stat--xl', level === 'stat' && mono && 't-stat--mono', muted && 't-muted', caps && 't-caps', grow && 'grow', sr && 't-sr', className),
|
|
13
|
+
className: cx(`t-${level}`, level === 'stat' && xl && 't-stat--xl', level === 'stat' && mono && 't-stat--mono', muted && 't-muted', caps && 't-caps', grow && 'grow', sr && 't-sr', selectable && 't-select', className),
|
|
14
14
|
...rest,
|
|
15
15
|
});
|
|
16
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
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.",
|