@usableapp/cardds 0.7.16 → 0.7.17
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 +27 -0
- package/README.md +23 -20
- package/cardds.css +3 -3
- package/css/actions.css +60 -38
- package/css/base.css +43 -17
- package/css/card.css +18 -13
- package/css/choice.css +21 -33
- package/css/forms.css +0 -3
- package/css/journey.css +0 -2
- package/css/layover.css +14 -24
- package/css/lists.css +1 -6
- package/css/media.css +7 -15
- package/css/numbers.css +5 -21
- package/css/people.css +1 -8
- package/css/sheet.css +12 -14
- package/css/stack.css +17 -21
- package/css/step.css +5 -8
- package/css/tokens.css +19 -4
- package/dist/actions/Btn.d.ts +1 -1
- package/dist/actions/Dropdown.js +1 -6
- package/dist/actions/Fab.d.ts +1 -1
- package/dist/actions/Fab.js +3 -2
- package/dist/cardds.css +228 -232
- package/dist/cards/Card.d.ts +23 -7
- package/dist/cards/Card.js +23 -8
- package/dist/cards/CardHead.d.ts +0 -1
- package/dist/cards/CardHead.js +1 -1
- package/dist/choice/Calendar.js +1 -2
- package/dist/choice/Mood.js +2 -2
- package/dist/forms/Field.js +2 -3
- package/dist/forms/FileBtn.js +1 -1
- package/dist/forms/Pin.js +6 -10
- package/dist/index.js +2 -2
- package/dist/layover/Banner.d.ts +10 -3
- package/dist/layover/Banner.js +6 -3
- package/dist/lists/Row.js +3 -3
- package/dist/media/Postcard.js +2 -2
- package/dist/numbers/Band.d.ts +3 -1
- package/dist/numbers/Picker.js +1 -7
- package/dist/scaffold/ActionBar.d.ts +3 -3
- package/dist/scaffold/ActionBar.js +2 -2
- package/dist/scaffold/Centre.d.ts +1 -1
- package/dist/scaffold/Centre.js +1 -1
- package/dist/scaffold/PagerAt.d.ts +1 -1
- package/dist/scaffold/PagerAt.js +1 -1
- package/dist/scaffold/Screen.js +2 -4
- package/dist/sheets/Drawer.js +3 -4
- package/dist/sheets/Sheet.js +8 -19
- package/dist/sheets/SheetStack.d.ts +2 -2
- package/dist/sheets/SheetStack.js +18 -10
- package/dist/sheets/SheetSteps.js +5 -7
- package/dist/sheets/StackBack.js +2 -1
- package/dist/sheets/StackSheet.d.ts +3 -2
- package/dist/sheets/StackSheet.js +6 -9
- package/dist/state.d.ts +19 -2
- package/dist/state.js +47 -7
- package/dist/templates/CallSheet.js +1 -2
- package/dist/templates/Home.d.ts +3 -2
- package/dist/templates/Home.js +1 -2
- package/dist/templates/Splash.js +2 -3
- package/dist/templates/Walkthrough.js +2 -2
- package/dist/type/Text.d.ts +1 -1
- package/package.json +7 -4
|
@@ -18,7 +18,7 @@ const StepCtx = createContext(null);
|
|
|
18
18
|
* The sheet may be dragged down to peek and back — the body keeps where it was scrolled.
|
|
19
19
|
*/
|
|
20
20
|
export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible = false, numbered = false, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }) {
|
|
21
|
-
const [current,
|
|
21
|
+
const [current, request] = useControlled(at, defaultAt, onAtChange);
|
|
22
22
|
const steps = Children.toArray(children).filter(isValidElement);
|
|
23
23
|
const count = steps.length;
|
|
24
24
|
/* how far the member has come: a step before it was DONE (its work, or skipped) — it stays done when they go back to look */
|
|
@@ -39,10 +39,8 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
|
|
|
39
39
|
} // from the very render that changes it: the old field's blur and the keyboard's jump come in that same commit
|
|
40
40
|
const press = useRef(null);
|
|
41
41
|
const go = (i, reason) => {
|
|
42
|
-
if (i
|
|
43
|
-
|
|
44
|
-
setOwn(i);
|
|
45
|
-
onAtChange?.(i, reason);
|
|
42
|
+
if (i >= 0 && i <= count)
|
|
43
|
+
request(i, reason);
|
|
46
44
|
};
|
|
47
45
|
/* how far the body is scrolled when a step opens or a field in it takes the keyboard: the TWO latest done rows always show
|
|
48
46
|
above the open step, and half of the one before — older ones go up out of sight, one row at a time, so what was done never seems to vanish (Lh
|
|
@@ -220,7 +218,7 @@ export function SheetStep({ title, summary, skippable, skipLabel = 'Skip', error
|
|
|
220
218
|
e.preventDefault();
|
|
221
219
|
};
|
|
222
220
|
const press = (e) => {
|
|
223
|
-
const btn = e.target.closest('.btn');
|
|
221
|
+
const btn = e.target.closest('button.btn'); // a FileBtn is a label.btn: picking a file is not the step's press
|
|
224
222
|
if (!btn || btn.disabled)
|
|
225
223
|
return;
|
|
226
224
|
let bad = null;
|
|
@@ -242,7 +240,7 @@ export function SheetStep({ title, summary, skippable, skipLabel = 'Skip', error
|
|
|
242
240
|
const enter = (e) => {
|
|
243
241
|
if (e.key !== 'Enter' || e.nativeEvent.isComposing || !(e.target instanceof HTMLInputElement))
|
|
244
242
|
return;
|
|
245
|
-
const btn = e.currentTarget.querySelector('.btn:not(:disabled)');
|
|
243
|
+
const btn = e.currentTarget.querySelector('button.btn:not(:disabled)');
|
|
246
244
|
if (btn) {
|
|
247
245
|
e.preventDefault();
|
|
248
246
|
btn.click();
|
package/dist/sheets/StackBack.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
import { Icon } from '../type/Icon.js';
|
|
4
|
+
import { IconBtn } from '../actions/IconBtn.js';
|
|
4
5
|
/** StackBack — the back button a tapped stack sheet reveals in its head; first child of that sheet's CardHead. Hidden until the sheet is open.
|
|
5
6
|
* A SMALL disc (Lh 2026-09-23): 32px with a chevron — the head keeps its room for the title and the tabs; the 44px tap target stays. */
|
|
6
7
|
export function StackBack({ label = 'Back', className, ...rest }) {
|
|
7
|
-
return
|
|
8
|
+
return _jsx(IconBtn, { sm: true, label: label, className: cx('sheet-stack__back', className), ...rest, children: _jsx(Icon, { name: "chevron-left" }) });
|
|
8
9
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import { type Tone } from '../cards/Card.js';
|
|
2
3
|
import { type IconName } from '../type/Icon.js';
|
|
3
4
|
export interface StackSheetTab {
|
|
4
5
|
id: string;
|
|
@@ -11,8 +12,8 @@ export interface StackSheetTab {
|
|
|
11
12
|
export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'> {
|
|
12
13
|
/** the SHEET HEAD's title — an h2 (the page's h1 is its own) */
|
|
13
14
|
title: ReactNode;
|
|
14
|
-
/** the paper
|
|
15
|
-
tone?:
|
|
15
|
+
/** the paper — see `Tone` */
|
|
16
|
+
tone?: Tone;
|
|
16
17
|
/** the head's end when the sheet has no tabs: an Icon, an IconRow */
|
|
17
18
|
icon?: ReactNode;
|
|
18
19
|
/** TABS in the head: icon buttons at the end — the sheet's pages. A tap on one in a peeked head opens the sheet on it */
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
|
-
import { useControlled } from '../state.js';
|
|
3
|
+
import { useControlled, watchSize } from '../state.js';
|
|
4
4
|
import { cx } from '../cx.js';
|
|
5
5
|
import { Card } from '../cards/Card.js';
|
|
6
|
+
import { IconBtn } from '../actions/IconBtn.js';
|
|
6
7
|
import { CardHead } from '../cards/CardHead.js';
|
|
7
8
|
import { CardFoot } from '../cards/CardFoot.js';
|
|
8
9
|
import { SheetBody } from './SheetBody.js';
|
|
@@ -17,14 +18,13 @@ import { Badge } from '../numbers/Badge.js';
|
|
|
17
18
|
* is closed — see `Home`). A direct child of `SheetStack`.
|
|
18
19
|
*/
|
|
19
20
|
export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = 'Back', className, children, ...rest }) {
|
|
20
|
-
const [tab,
|
|
21
|
-
const pick = (id) => { setOwn(id); onTabChange?.(id); };
|
|
21
|
+
const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
|
|
22
22
|
// a title longer than its room does not wrap: it runs sideways, like a song title in a short slot — the room it overflows by
|
|
23
23
|
// is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
|
|
24
24
|
const titleRef = useRef(null);
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
const h = titleRef.current, run = h?.firstElementChild;
|
|
27
|
-
if (!h || !run
|
|
27
|
+
if (!h || !run)
|
|
28
28
|
return;
|
|
29
29
|
const measure = () => {
|
|
30
30
|
const over = Math.max(0, Math.ceil(run.scrollWidth - h.clientWidth));
|
|
@@ -32,11 +32,8 @@ export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab,
|
|
|
32
32
|
h.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
|
|
33
33
|
h.classList.toggle('is-long', over > 0);
|
|
34
34
|
};
|
|
35
|
-
const ro = new ResizeObserver(measure);
|
|
36
|
-
ro.observe(h);
|
|
37
|
-
ro.observe(run);
|
|
38
35
|
measure();
|
|
39
|
-
return (
|
|
36
|
+
return watchSize([h, run], measure);
|
|
40
37
|
}, [title]);
|
|
41
|
-
return (_jsxs(Card, { tone: tone, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [_jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: "stack-sheet__title", children: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs(
|
|
38
|
+
return (_jsxs(Card, { tone: tone, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [_jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: "stack-sheet__title", children: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs(IconBtn, { sm: true, invert: t.id === tab, role: "tab", "aria-selected": t.id === tab, label: t.label, className: "stack-sheet__tab", onClick: () => pick(t.id), children: [_jsx(Icon, { name: t.icon }), t.count ? _jsx(Badge, { className: "stack-sheet__count", children: t.count > 99 ? '99+' : t.count }) : null] }, t.id))) })) : icon] }), _jsx(SheetBody, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(CardFoot, { children: foot })] }));
|
|
42
39
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
import { type SyntheticEvent } from 'react';
|
|
1
2
|
/** controlled like an input: `value` given = the app owns it; undefined = the component keeps its own, starting at `initial`.
|
|
2
|
-
* Returns the value shown and
|
|
3
|
-
|
|
3
|
+
* Returns the value shown and `request(next, …extra)`: nothing when `next` is what shows; else it moves the component's own copy
|
|
4
|
+
* (uncontrolled only) and ASKS the app — `onChange(next, …extra)`. `request` keeps one identity for the component's life. */
|
|
5
|
+
type Asks<T, A extends unknown[]> = {
|
|
6
|
+
ask(next: T, ...extra: A): void;
|
|
7
|
+
}['ask'];
|
|
8
|
+
export declare function useControlled<T, A extends unknown[] = []>(value: T | undefined, initial: T, onChange?: Asks<T, A>): readonly [T, (next: T, ...extra: A) => void];
|
|
9
|
+
/** a native `<details>` controlled like an input: `open` + `onOpenChange`, or `defaultOpen`. Spread the result on the details.
|
|
10
|
+
* The browser opens it on a tap by itself — a controlled one is put back until the app says so. */
|
|
11
|
+
export declare function useDisclosure(open: boolean | undefined, defaultOpen?: boolean, onOpenChange?: (open: boolean) => void): {
|
|
12
|
+
open: boolean;
|
|
13
|
+
onToggle: (e: SyntheticEvent<HTMLDetailsElement>) => void;
|
|
14
|
+
};
|
|
15
|
+
/** the latest value in a ref — for a handler that a timer, an observer or a listener set up once calls later */
|
|
16
|
+
export declare function useLatest<T>(value: T): import("react").RefObject<T>;
|
|
17
|
+
/** watch boxes for a size change (the keyboard, a font arriving, a wrap): `fn` on each — once at the start too, as the browser
|
|
18
|
+
* reports. Returns the stop, for an effect's cleanup; nothing is watched where the browser has no ResizeObserver. */
|
|
19
|
+
export declare function watchSize(targets: Array<Element | null | undefined>, fn: () => void): () => void;
|
|
4
20
|
/** a dev hint said once per key, never again */
|
|
5
21
|
export declare const warnOnce: (key: string, text: string) => void;
|
|
6
22
|
/** the member asked for less motion (false where there is no window, e.g. a server render) */
|
|
7
23
|
export declare const reducedMotion: () => boolean;
|
|
24
|
+
export {};
|
package/dist/state.js
CHANGED
|
@@ -1,11 +1,51 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
* Returns the value shown and a setter that only moves the component's own copy — the caller still ASKS the app (onXChange). */
|
|
4
|
-
export function useControlled(value, initial) {
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
export function useControlled(value, initial, onChange) {
|
|
5
3
|
const [own, setOwn] = useState(initial);
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
const shown = value !== undefined ? value : own;
|
|
5
|
+
const live = useRef({ controlled: value !== undefined, shown, onChange });
|
|
6
|
+
live.current = { controlled: value !== undefined, shown, onChange };
|
|
7
|
+
const request = useCallback((next, ...extra) => {
|
|
8
|
+
const { controlled, shown, onChange } = live.current;
|
|
9
|
+
if (Object.is(next, shown))
|
|
10
|
+
return;
|
|
11
|
+
if (!controlled)
|
|
12
|
+
setOwn(next);
|
|
13
|
+
onChange?.(next, ...extra);
|
|
14
|
+
}, []);
|
|
15
|
+
return [shown, request];
|
|
16
|
+
}
|
|
17
|
+
/** a native `<details>` controlled like an input: `open` + `onOpenChange`, or `defaultOpen`. Spread the result on the details.
|
|
18
|
+
* The browser opens it on a tap by itself — a controlled one is put back until the app says so. */
|
|
19
|
+
export function useDisclosure(open, defaultOpen = false, onOpenChange) {
|
|
20
|
+
const [shown, request] = useControlled(open, defaultOpen, onOpenChange);
|
|
21
|
+
return {
|
|
22
|
+
open: shown,
|
|
23
|
+
onToggle: (e) => {
|
|
24
|
+
const el = e.currentTarget;
|
|
25
|
+
if (el.open === shown)
|
|
26
|
+
return;
|
|
27
|
+
request(el.open);
|
|
28
|
+
if (open !== undefined)
|
|
29
|
+
el.open = open;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** the latest value in a ref — for a handler that a timer, an observer or a listener set up once calls later */
|
|
34
|
+
export function useLatest(value) {
|
|
35
|
+
const ref = useRef(value);
|
|
36
|
+
ref.current = value;
|
|
37
|
+
return ref;
|
|
38
|
+
}
|
|
39
|
+
/** watch boxes for a size change (the keyboard, a font arriving, a wrap): `fn` on each — once at the start too, as the browser
|
|
40
|
+
* reports. Returns the stop, for an effect's cleanup; nothing is watched where the browser has no ResizeObserver. */
|
|
41
|
+
export function watchSize(targets, fn) {
|
|
42
|
+
if (typeof ResizeObserver === 'undefined')
|
|
43
|
+
return () => { };
|
|
44
|
+
const ro = new ResizeObserver(fn);
|
|
45
|
+
for (const t of targets)
|
|
46
|
+
if (t)
|
|
47
|
+
ro.observe(t);
|
|
48
|
+
return () => ro.disconnect();
|
|
9
49
|
}
|
|
10
50
|
const warned = new Set();
|
|
11
51
|
/** a dev hint said once per key, never again */
|
|
@@ -19,9 +19,8 @@ const index = (o) => (o === true ? 0 : typeof o === 'number' ? o : null);
|
|
|
19
19
|
*/
|
|
20
20
|
export function CallSheet({ media, heading, body, action, sheet, ways, open: openProp, defaultOpen = null, onOpenChange, states = 'peek full', children, ...rest }) {
|
|
21
21
|
const list = ways ?? (sheet ? [{ action, sheet }] : []);
|
|
22
|
-
const [on,
|
|
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
|
-
const set = (next) => { setOwn(next); onOpenChange?.(next !== null, next); };
|
|
25
24
|
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')
|
|
26
25
|
set(null);
|
|
27
26
|
else
|
package/dist/templates/Home.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import type { Tone } from '../cards/Card.js';
|
|
2
3
|
import { Screen } from '../scaffold/Screen.js';
|
|
3
4
|
import { type StackSheetTab } from '../sheets/StackSheet.js';
|
|
4
5
|
export interface HomeSheet {
|
|
@@ -6,8 +7,8 @@ export interface HomeSheet {
|
|
|
6
7
|
title: ReactNode;
|
|
7
8
|
/** the sheet head's end when it has no tabs: an Icon, an IconRow */
|
|
8
9
|
icon?: ReactNode;
|
|
9
|
-
/** the paper
|
|
10
|
-
tone?:
|
|
10
|
+
/** the paper — see `Tone` (default: 2, 3, 1, 2, 3 by place) */
|
|
11
|
+
tone?: Tone;
|
|
11
12
|
/** the sheet's pages as icon tabs in its head (a count shows as a small circle over the icon) — see `StackSheet` */
|
|
12
13
|
tabs?: StackSheetTab[];
|
|
13
14
|
/** the tab shown first (default: the first) */
|
package/dist/templates/Home.js
CHANGED
|
@@ -15,7 +15,7 @@ const LEAVE = 800; // ms: a closing sheet keeps its body until it has slid home
|
|
|
15
15
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
16
16
|
*/
|
|
17
17
|
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, base, keep, backLabel = 'Back', children, ...rest }) {
|
|
18
|
-
const [open,
|
|
18
|
+
const [open, change] = useControlled(openProp, defaultOpen, onOpenChange);
|
|
19
19
|
const [leaving, setLeaving] = useState(null);
|
|
20
20
|
const [seen, setSeen] = useState(() => new Set(open != null ? [open] : []));
|
|
21
21
|
const [was, setWas] = useState(open);
|
|
@@ -32,7 +32,6 @@ export function Home({ top, title, sheets, open: openProp, defaultOpen = null, o
|
|
|
32
32
|
const t = setTimeout(() => setLeaving(null), LEAVE);
|
|
33
33
|
return () => clearTimeout(t);
|
|
34
34
|
}, [leaving]);
|
|
35
|
-
const change = (next) => { setOwn(next); onOpenChange?.(next); };
|
|
36
35
|
const [tabs, setTabs] = useState(() => Object.fromEntries(sheets.map((x, i) => [i, x.defaultTab ?? x.tabs?.[0]?.id])));
|
|
37
36
|
const shows = (i) => i === open || i === leaving || (keep && seen.has(i));
|
|
38
37
|
return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: open, onOpenChange: change, base: base, children: sheets.map((s, i) => (_jsx(StackSheet, { tone: s.tone ?? TONES[i], title: s.title, icon: s.icon, tabs: s.tabs, tab: tabs[i], onTabChange: (t) => setTabs((v) => ({ ...v, [i]: t })), backLabel: backLabel, foot: typeof s.foot === 'function' ? s.foot(tabs[i]) : s.foot, children: shows(i) ? (typeof s.body === 'function' ? s.body(tabs[i]) : s.body) : null }, i))) }), children] }));
|
package/dist/templates/Splash.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { reducedMotion } from '../state.js';
|
|
3
|
+
import { reducedMotion, useLatest } from '../state.js';
|
|
4
4
|
import { Screen } from '../scaffold/Screen.js';
|
|
5
5
|
import { cx } from '../cx.js';
|
|
6
6
|
/**
|
|
@@ -13,8 +13,7 @@ import { cx } from '../cx.js';
|
|
|
13
13
|
export function Splash({ mark, text, done, min = 0, onLeft, className, ...rest }) {
|
|
14
14
|
const born = useRef(Date.now());
|
|
15
15
|
const [leaving, setLeaving] = useState(false);
|
|
16
|
-
const left =
|
|
17
|
-
left.current = onLeft;
|
|
16
|
+
const left = useLatest(onLeft);
|
|
18
17
|
useEffect(() => {
|
|
19
18
|
if (!done)
|
|
20
19
|
return;
|
|
@@ -17,7 +17,7 @@ import { IconBtn } from '../actions/IconBtn.js';
|
|
|
17
17
|
* first and fixed while the words scroll, one CTA row. Yours: the pages, the row, the state.
|
|
18
18
|
*/
|
|
19
19
|
export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta, controls = [], ctaLink, children, backLabel = 'Back', nextLabel = 'Next', ...rest }) {
|
|
20
|
-
const [at,
|
|
20
|
+
const [at, turn] = useControlled(atProp, defaultAt, onAtChange);
|
|
21
21
|
const step = useRef(null);
|
|
22
22
|
// a new page starts where a page starts — at its lead, not where the last one was scrolled to (Lh 2026-09-23). A LAYOUT effect:
|
|
23
23
|
// it runs inside the page turn's commit, before the view transition takes its picture — only the sideways slide shows, no scroll
|
|
@@ -27,7 +27,7 @@ export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta,
|
|
|
27
27
|
const go = (to) => {
|
|
28
28
|
if (to < 0 || to >= count || to === at)
|
|
29
29
|
return;
|
|
30
|
-
slideTo(to > at ? 'next' : 'back', () =>
|
|
30
|
+
slideTo(to > at ? 'next' : 'back', () => turn(to), step.current ?? document);
|
|
31
31
|
};
|
|
32
32
|
/* a sideways SWIPE turns the page (Lh 2026-09-23): left = next, right = back. The walk still scrolls up and down itself
|
|
33
33
|
(touch-action: pan-y); a swipe must be mostly sideways and long enough, so a scroll never turns a page */
|
package/dist/type/Text.d.ts
CHANGED
|
@@ -14,12 +14,12 @@ export interface TextProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
14
14
|
grow?: boolean;
|
|
15
15
|
/** uppercase */
|
|
16
16
|
caps?: boolean;
|
|
17
|
-
/** the element; defaults per level (h2 for display/h1/h2, p for title/body/stat, span for the rest) */
|
|
18
17
|
/** 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
18
|
sr?: boolean;
|
|
20
19
|
/** the member may select and copy these words (a code, an address, a reference number) — everything else in a Screen is not
|
|
21
20
|
* selectable, like a native app (Lh 2026-09-25) */
|
|
22
21
|
selectable?: boolean;
|
|
22
|
+
/** the element; defaults per level (h2 for display/h1/h2, p for title/body/stat, span for the rest) */
|
|
23
23
|
as?: 'h1' | 'h2' | 'h3' | 'p' | 'span' | 'strong' | 'div';
|
|
24
24
|
}
|
|
25
25
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.17",
|
|
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.",
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
"test": "playwright test",
|
|
15
15
|
"validate:inbox": "node scripts/validate-inbox.mjs",
|
|
16
16
|
"prepack": "npm run build",
|
|
17
|
-
"knobs": "node scripts/knobs.mjs"
|
|
17
|
+
"knobs": "node scripts/knobs.mjs",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p gallery/tsconfig.json --noEmit",
|
|
19
|
+
"snapshot:write": "CARDDS_SNAPSHOT=write playwright test tests/style-snapshot.spec.js",
|
|
20
|
+
"snapshot:check": "CARDDS_SNAPSHOT=check playwright test tests/style-snapshot.spec.js"
|
|
18
21
|
},
|
|
19
22
|
"devDependencies": {
|
|
20
23
|
"@playwright/test": "1.58.2",
|
|
@@ -52,8 +55,8 @@
|
|
|
52
55
|
"CHANGELOG.md"
|
|
53
56
|
],
|
|
54
57
|
"peerDependencies": {
|
|
55
|
-
"react": ">=
|
|
56
|
-
"react-dom": ">=
|
|
58
|
+
"react": ">=19",
|
|
59
|
+
"react-dom": ">=19"
|
|
57
60
|
},
|
|
58
61
|
"publishConfig": {
|
|
59
62
|
"access": "public"
|