@usableapp/cardds 0.7.14 → 0.7.16
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 +43 -0
- package/README.md +6 -8
- package/css/actions.css +24 -1
- package/css/base.css +2 -2
- package/css/card.css +9 -13
- package/css/choice.css +2 -2
- package/css/journey.css +3 -3
- package/css/layover.css +5 -17
- package/css/media.css +6 -5
- package/css/numbers.css +4 -3
- package/css/palette.css +81 -40
- package/css/people.css +3 -2
- package/css/sheet.css +4 -6
- package/css/stack.css +6 -10
- package/css/step.css +3 -5
- package/css/theme.template.css +2 -1
- package/css/tokens.css +12 -1
- package/dist/actions/Dropdown.js +4 -5
- package/dist/cardds.css +164 -110
- package/dist/cards/Card.d.ts +3 -3
- package/dist/choice/Calendar.js +3 -5
- package/dist/choice/Mood.d.ts +10 -5
- package/dist/choice/Mood.js +4 -2
- package/dist/forms/Pin.js +7 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lists/Row.d.ts +2 -1
- package/dist/lists/Row.js +2 -2
- package/dist/media/Postcard.js +3 -5
- package/dist/media/Tile.d.ts +1 -1
- package/dist/numbers/Band.d.ts +1 -1
- package/dist/numbers/Picker.js +4 -5
- package/dist/numbers/Track.d.ts +4 -2
- package/dist/numbers/Track.js +3 -3
- package/dist/people/AvatarPick.d.ts +3 -1
- package/dist/people/AvatarPick.js +2 -2
- package/dist/scaffold/Centre.d.ts +1 -1
- package/dist/scaffold/Centre.js +1 -1
- package/dist/scaffold/Screen.d.ts +1 -1
- package/dist/scaffold/Screen.js +3 -4
- package/dist/scaffold/TopBar.js +3 -5
- package/dist/sheets/Sheet.js +5 -7
- package/dist/sheets/SheetStack.js +4 -5
- package/dist/sheets/SheetSteps.js +5 -6
- package/dist/sheets/StackSheet.d.ts +1 -1
- package/dist/sheets/StackSheet.js +4 -5
- package/dist/state.d.ts +7 -0
- package/dist/state.js +17 -0
- package/dist/templates/CallSheet.js +3 -4
- package/dist/templates/Home.d.ts +1 -1
- package/dist/templates/Home.js +3 -4
- package/dist/templates/Splash.js +2 -1
- package/dist/templates/Walkthrough.js +4 -5
- package/dist/type/Icon.js +3 -5
- package/dist/type/textScale.d.ts +6 -0
- package/dist/type/textScale.js +12 -0
- package/package.json +1 -1
- package/dist/layover/Deck.d.ts +0 -9
- package/dist/layover/Deck.js +0 -10
package/dist/choice/Mood.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { ComponentProps } from 'react';
|
|
2
|
-
|
|
2
|
+
type MoodValue = 1 | 2 | 3 | 4 | 5;
|
|
3
|
+
export interface MoodProps extends Omit<ComponentProps<'fieldset'>, 'onChange' | 'defaultValue'> {
|
|
3
4
|
/** the radio group name */
|
|
4
5
|
name?: string;
|
|
5
|
-
/** the picked dot, 1 (worst) to 5 (best) */
|
|
6
|
-
value?:
|
|
6
|
+
/** CONTROLLED: the picked dot, 1 (worst) to 5 (best) — with `onChange`; or `defaultValue` */
|
|
7
|
+
value?: MoodValue;
|
|
8
|
+
/** UNCONTROLLED: the dot picked on arrival; the Mood then keeps its own */
|
|
9
|
+
defaultValue?: MoodValue;
|
|
7
10
|
label?: string;
|
|
8
|
-
|
|
11
|
+
/** the dot the member tapped */
|
|
12
|
+
onChange?: (value: MoodValue) => void;
|
|
9
13
|
}
|
|
10
14
|
/** Mood — five dots in a pill from bad to good; the picked one grows. */
|
|
11
|
-
export declare function Mood({ name, value, label, onChange, className, ...rest }: MoodProps): import("react").JSX.Element;
|
|
15
|
+
export declare function Mood({ name, value, defaultValue, label, onChange, className, ...rest }: MoodProps): import("react").JSX.Element;
|
|
16
|
+
export {};
|
package/dist/choice/Mood.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
+
import { useControlled } from '../state.js';
|
|
3
4
|
/** Mood — five dots in a pill from bad to good; the picked one grows. */
|
|
4
|
-
export function Mood({ name = 'mood', value, label = 'Mood', onChange, className, ...rest }) {
|
|
5
|
-
|
|
5
|
+
export function Mood({ name = 'mood', value, defaultValue, label = 'Mood', onChange, className, ...rest }) {
|
|
6
|
+
const [picked, setOwn] = useControlled(value, defaultValue);
|
|
7
|
+
return (_jsx("fieldset", { className: cx('mood', className), "aria-label": label, ...rest, children: [1, 2, 3, 4, 5].map((n) => (_jsxs("label", { className: "mood__opt", children: [_jsx("input", { type: "radio", name: name, value: n, checked: n === picked, onChange: () => { setOwn(n); onChange?.(n); } }), _jsx("span", { className: "mood__dot" })] }, n))) }));
|
|
6
8
|
}
|
package/dist/forms/Pin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useControlled } from '../state.js';
|
|
3
4
|
import { cx } from '../cx.js';
|
|
4
5
|
import { useCheck } from './check.js';
|
|
5
6
|
const RESET_MS = 900; // a code that does not match: red + shake this long, then the cells empty for a new try
|
|
@@ -11,12 +12,12 @@ const RESET_MS = 900; // a code that does not match: red + shake this long, then
|
|
|
11
12
|
*/
|
|
12
13
|
export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onChange, autoFocus, error, match, formatError, secret, className, onAnimationEnd, ...rest }) {
|
|
13
14
|
const ref = useRef(null);
|
|
14
|
-
const [
|
|
15
|
-
const code =
|
|
15
|
+
const [typed, setOwn] = useControlled(value, defaultValue);
|
|
16
|
+
const code = typed.replace(/\D/g, '').slice(0, length);
|
|
16
17
|
const cell = (i) => ref.current?.querySelectorAll('.pin__cell')[i];
|
|
17
18
|
/* 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({
|
|
19
|
-
latest.current = {
|
|
19
|
+
const latest = useRef({ setOwn, onChange, error });
|
|
20
|
+
latest.current = { setOwn, onChange, error };
|
|
20
21
|
const miss = () => {
|
|
21
22
|
restart.current = true;
|
|
22
23
|
setShake(true);
|
|
@@ -25,8 +26,7 @@ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onC
|
|
|
25
26
|
setBad(false);
|
|
26
27
|
setCleared(latest.current.error);
|
|
27
28
|
restart.current = false;
|
|
28
|
-
|
|
29
|
-
setInner('');
|
|
29
|
+
latest.current.setOwn('');
|
|
30
30
|
latest.current.onChange?.('', false);
|
|
31
31
|
cell(0)?.focus();
|
|
32
32
|
}, RESET_MS);
|
|
@@ -46,8 +46,7 @@ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onC
|
|
|
46
46
|
said.current = true;
|
|
47
47
|
miss();
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
setInner(next);
|
|
49
|
+
setOwn(next);
|
|
51
50
|
if (next !== code)
|
|
52
51
|
onChange?.(next, next.length === length);
|
|
53
52
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { cx } from './cx.js';
|
|
|
4
4
|
export * from './type/Icon.js';
|
|
5
5
|
export { ICON_NAMES } from './type/icons.js';
|
|
6
6
|
export * from './type/Text.js';
|
|
7
|
+
export { setTextScale } from './type/textScale.js';
|
|
7
8
|
export * from './scaffold/Screen.js';
|
|
8
9
|
export * from './scaffold/AppBar.js';
|
|
9
10
|
export * from './scaffold/TopBar.js';
|
|
@@ -72,7 +73,6 @@ export * from './media/Tile.js';
|
|
|
72
73
|
export * from './media/Mosaic.js';
|
|
73
74
|
export * from './layover/Banner.js';
|
|
74
75
|
export * from './layover/Callout.js';
|
|
75
|
-
export * from './layover/Deck.js';
|
|
76
76
|
export * from './journey/Route.js';
|
|
77
77
|
export * from './journey/TileBadge.js';
|
|
78
78
|
export * from './journey/NoteRow.js';
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { cx } from './cx.js';
|
|
|
8
8
|
export * from './type/Icon.js';
|
|
9
9
|
export { ICON_NAMES } from './type/icons.js';
|
|
10
10
|
export * from './type/Text.js';
|
|
11
|
+
export { setTextScale } from './type/textScale.js';
|
|
11
12
|
export * from './scaffold/Screen.js';
|
|
12
13
|
export * from './scaffold/AppBar.js';
|
|
13
14
|
export * from './scaffold/TopBar.js';
|
|
@@ -76,7 +77,6 @@ export * from './media/Tile.js';
|
|
|
76
77
|
export * from './media/Mosaic.js';
|
|
77
78
|
export * from './layover/Banner.js';
|
|
78
79
|
export * from './layover/Callout.js';
|
|
79
|
-
export * from './layover/Deck.js';
|
|
80
80
|
export * from './journey/Route.js';
|
|
81
81
|
export * from './journey/TileBadge.js';
|
|
82
82
|
export * from './journey/NoteRow.js';
|
package/dist/lists/Row.d.ts
CHANGED
|
@@ -41,7 +41,8 @@ export interface RowProps extends Omit<ComponentProps<'div'>, 'title'> {
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function Row({ lead, leadIcon, title, meta, body, end, endValue, endCaption, lg, done, as, href, more, open, defaultOpen, onOpenChange, className, children, ...rest }: RowProps): import("react").JSX.Element;
|
|
43
43
|
export interface RowsProps extends ComponentProps<'div'> {
|
|
44
|
-
/** the group's name, above its rows (Today · Tomorrow · Later): a long list is GROUPED, not spaced out — several Rows one after another
|
|
44
|
+
/** the group's name, above its rows (Today · Tomorrow · Later): a long list is GROUPED, not spaced out — several Rows one after another.
|
|
45
|
+
* Words = the small label; an ELEMENT (`<Text level="title" as="h3">`) is used as the heading itself — the group is labelled by it. */
|
|
45
46
|
heading?: ReactNode;
|
|
46
47
|
}
|
|
47
48
|
/** Rows — a list of Rows separated by hairlines; `heading` names the group (a `role="group"` labelled by it). */
|
package/dist/lists/Row.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { createElement, useId } from 'react';
|
|
2
|
+
import { cloneElement, createElement, isValidElement, useId } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { Icon } from '../type/Icon.js';
|
|
5
5
|
/**
|
|
@@ -22,5 +22,5 @@ export function Row({ lead, leadIcon, title, meta, body, end, endValue, endCapti
|
|
|
22
22
|
export function Rows({ heading, className, children, ...rest }) {
|
|
23
23
|
const id = useId();
|
|
24
24
|
const named = heading != null;
|
|
25
|
-
return (_jsxs("div", { className: cx('rows', className), role: named ? 'group' : undefined, "aria-labelledby": named ? id : undefined, ...rest, children: [named && _jsx("span", { className: "rows__head", id: id, children: heading }), children] }));
|
|
25
|
+
return (_jsxs("div", { className: cx('rows', className), role: named ? 'group' : undefined, "aria-labelledby": named ? id : undefined, ...rest, children: [named && (isValidElement(heading) ? cloneElement(heading, { id }) : _jsx("span", { className: "rows__head", id: id, children: heading })), children] }));
|
|
26
26
|
}
|
package/dist/media/Postcard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useControlled } from '../state.js';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { IconBtn } from '../actions/IconBtn.js';
|
|
5
5
|
/**
|
|
@@ -9,10 +9,8 @@ import { IconBtn } from '../actions/IconBtn.js';
|
|
|
9
9
|
* Who else liked it, a voice note, a reply go OUTSIDE it — a Row under it (`Row more` expands the list).
|
|
10
10
|
*/
|
|
11
11
|
export function Postcard({ photo, alt = '', name, line, caption, voice, flipped, defaultFlipped = false, onFlip, liked, onLike, likeLabel = 'ถูกใจ', frontLabel = 'พลิกดูด้านหลัง', backLabel = 'พลิกดูด้านหน้า', className, ...rest }) {
|
|
12
|
-
const [
|
|
13
|
-
const
|
|
14
|
-
const turn = () => { if (flipped === undefined)
|
|
15
|
-
setInner(!back); onFlip?.(!back); };
|
|
12
|
+
const [back, setOwn] = useControlled(flipped, defaultFlipped);
|
|
13
|
+
const turn = () => { setOwn(!back); onFlip?.(!back); };
|
|
16
14
|
const words = (_jsxs("div", { className: "postcard__words", children: [_jsx("span", { className: "t-h1", children: name }), line != null && _jsx("span", { className: "t-body", children: line })] }));
|
|
17
15
|
return (_jsxs("div", { className: cx('postcard', back && 'is-flipped', className), ...rest, children: [_jsxs("div", { className: "postcard__face postcard__front", children: [_jsx("button", { className: "postcard__turn", type: "button", "aria-label": frontLabel, onClick: turn, children: photo ? _jsx("img", { src: photo, alt: alt }) : alt ? _jsx("div", { className: "ph", role: "img", "aria-label": alt }) : _jsx("div", { className: "ph" }) }), _jsxs("div", { className: "postcard__band", children: [words, (liked !== undefined || onLike) && _jsx(IconBtn, { invert: true, icon: "heart", label: likeLabel, "aria-pressed": !!liked, onClick: () => onLike?.(!liked) })] })] }), _jsxs("div", { className: "postcard__face postcard__back", children: [_jsxs("button", { className: "postcard__turn", type: "button", "aria-label": backLabel, onClick: turn, children: [words, caption != null && _jsx("p", { className: "postcard__caption", children: caption })] }), voice != null && _jsx("div", { className: "postcard__voice", children: voice })] })] }));
|
|
18
16
|
}
|
package/dist/media/Tile.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface TileProps extends Omit<ComponentProps<'div'>, 'onChange'> {
|
|
|
32
32
|
/** spans the whole grid */
|
|
33
33
|
wide?: boolean;
|
|
34
34
|
/** a slot tint */
|
|
35
|
-
tone?: 2 | 3;
|
|
35
|
+
tone?: 2 | 3 | 4 | 5;
|
|
36
36
|
/** div (default), button for a tap tile — pick tiles render as a label */
|
|
37
37
|
as?: 'div' | 'button';
|
|
38
38
|
}
|
package/dist/numbers/Band.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface BandProps extends ComponentProps<'article'> {
|
|
|
6
6
|
num: ReactNode;
|
|
7
7
|
/** the footnote under the label */
|
|
8
8
|
foot?: ReactNode;
|
|
9
|
-
tone?: 1 | 2 | 3;
|
|
9
|
+
tone?: 1 | 2 | 3 | 4 | 5;
|
|
10
10
|
}
|
|
11
11
|
/** Band — a data card: 2-line label · giant number · footnote. Stack several in a BandStack for one boundary with hairlines. */
|
|
12
12
|
export declare function Band({ label, num, foot, tone, className, ...rest }: BandProps): import("react").JSX.Element;
|
package/dist/numbers/Picker.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect, useLayoutEffect, useRef
|
|
2
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef } from 'react';
|
|
3
|
+
import { useControlled } from '../state.js';
|
|
3
4
|
import { cx } from '../cx.js';
|
|
4
5
|
const PickerContext = createContext(null);
|
|
5
6
|
/**
|
|
@@ -9,16 +10,14 @@ const PickerContext = createContext(null);
|
|
|
9
10
|
* settled in the centre. The CSS lights the centred item by scroll position; the app never reads the DOM to know what was picked.
|
|
10
11
|
*/
|
|
11
12
|
export function Picker({ value, defaultValue, onChange, unit, label, className, children, ...rest }) {
|
|
12
|
-
const [
|
|
13
|
-
const current = value ?? inner;
|
|
13
|
+
const [current, setOwn] = useControlled(value, defaultValue);
|
|
14
14
|
const rail = useRef(null);
|
|
15
15
|
const mounted = useRef(false);
|
|
16
16
|
const settle = useRef(null);
|
|
17
17
|
const pick = (next) => {
|
|
18
18
|
if (next === current)
|
|
19
19
|
return;
|
|
20
|
-
|
|
21
|
-
setInner(next);
|
|
20
|
+
setOwn(next);
|
|
22
21
|
onChange?.(next);
|
|
23
22
|
};
|
|
24
23
|
const items = () => [...(rail.current?.querySelectorAll(':scope > .picker__item') ?? [])];
|
package/dist/numbers/Track.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare function Track({ label, className, ...rest }: TrackProps): import
|
|
|
7
7
|
export interface TrackStepProps extends ComponentProps<'li'> {
|
|
8
8
|
/** this step (and the connector into it) is done */
|
|
9
9
|
done?: boolean;
|
|
10
|
+
/** the step's NUMBER in the round instead of an Icon (1, 2, 3…) — a walk whose steps are counted, not pictured */
|
|
11
|
+
num?: number;
|
|
10
12
|
}
|
|
11
|
-
/** TrackStep — one step of a Track: an Icon in a round; lit when done. */
|
|
12
|
-
export declare function TrackStep({ done, className, ...rest }: TrackStepProps): import("react").JSX.Element;
|
|
13
|
+
/** TrackStep — one step of a Track: an Icon (children) or its number (`num`) in a round; lit when done. */
|
|
14
|
+
export declare function TrackStep({ done, num, className, children, ...rest }: TrackStepProps): import("react").JSX.Element;
|
package/dist/numbers/Track.js
CHANGED
|
@@ -4,7 +4,7 @@ import { cx } from '../cx.js';
|
|
|
4
4
|
export function Track({ label = 'Progress', className, ...rest }) {
|
|
5
5
|
return _jsx("ol", { className: cx('track', className), "aria-label": label, ...rest });
|
|
6
6
|
}
|
|
7
|
-
/** TrackStep — one step of a Track: an Icon in a round; lit when done. */
|
|
8
|
-
export function TrackStep({ done, className, ...rest }) {
|
|
9
|
-
return _jsx("li", { className: cx('track__step', done && 'is-done', className), ...rest });
|
|
7
|
+
/** TrackStep — one step of a Track: an Icon (children) or its number (`num`) in a round; lit when done. */
|
|
8
|
+
export function TrackStep({ done, num, className, children, ...rest }) {
|
|
9
|
+
return _jsx("li", { className: cx('track__step', done && 'is-done', className), ...rest, children: num != null ? _jsx("span", { className: "track__dot", children: num }) : children });
|
|
10
10
|
}
|
|
@@ -13,6 +13,8 @@ export interface AvatarPickItemProps extends Omit<ComponentProps<'button'>, 'nam
|
|
|
13
13
|
role?: ReactNode;
|
|
14
14
|
/** the Avatar (lg) */
|
|
15
15
|
children?: ReactNode;
|
|
16
|
+
/** this person is the one picked (aria-pressed; the app keeps which) */
|
|
17
|
+
selected?: boolean;
|
|
16
18
|
}
|
|
17
19
|
/** AvatarPickItem — one person in an AvatarPick: an `Avatar size="lg"`, a name, a role. */
|
|
18
|
-
export declare function AvatarPickItem({ name, role, className, children, ...rest }: AvatarPickItemProps): import("react").JSX.Element;
|
|
20
|
+
export declare function AvatarPickItem({ name, role, selected, className, children, ...rest }: AvatarPickItemProps): import("react").JSX.Element;
|
|
@@ -5,6 +5,6 @@ export function AvatarPick({ bleed, label, className, ...rest }) {
|
|
|
5
5
|
return _jsx("div", { className: cx('avatar-pick', bleed && 'bleed', className), "aria-label": label, ...rest });
|
|
6
6
|
}
|
|
7
7
|
/** AvatarPickItem — one person in an AvatarPick: an `Avatar size="lg"`, a name, a role. */
|
|
8
|
-
export function AvatarPickItem({ name, role, className, children, ...rest }) {
|
|
9
|
-
return (_jsxs("button", { type: "button", className: cx('avatar-pick__item', className), ...rest, children: [children, _jsx("span", { className: "avatar-pick__name", children: name }), role != null && _jsx("span", { className: "avatar-pick__role", children: role })] }));
|
|
8
|
+
export function AvatarPickItem({ name, role, selected, className, children, ...rest }) {
|
|
9
|
+
return (_jsxs("button", { type: "button", className: cx('avatar-pick__item', className), "aria-pressed": selected, ...rest, children: [children, _jsx("span", { className: "avatar-pick__name", children: name }), role != null && _jsx("span", { className: "avatar-pick__role", children: role })] }));
|
|
10
10
|
}
|
|
@@ -3,7 +3,7 @@ export interface CentreProps extends ComponentProps<'div'> {
|
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
5
|
* Centre — the centred group: ONE thing to read at the human centre of the screen, when that thing is not a lone
|
|
6
|
-
* card: an EmptyState; a Card with its quiet second Btn and the PagerAt under it
|
|
6
|
+
* card: an EmptyState; a Card with its quiet second Btn and the PagerAt under it.
|
|
7
7
|
* On a plain `Screen` (the usual case, Lh 2026-09-12) it is in flow: the column is one phone tall, the group takes
|
|
8
8
|
* the room after the TopBar and centres a short card; a long card makes the page longer and scrolls up under the
|
|
9
9
|
* bar — nothing is capped, nothing shrinks. On a `Screen fill` it floats at 40% down (`--screen-centre`), capped
|
package/dist/scaffold/Centre.js
CHANGED
|
@@ -3,7 +3,7 @@ import { flushSync } from 'react-dom';
|
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
/**
|
|
5
5
|
* Centre — the centred group: ONE thing to read at the human centre of the screen, when that thing is not a lone
|
|
6
|
-
* card: an EmptyState; a Card with its quiet second Btn and the PagerAt under it
|
|
6
|
+
* card: an EmptyState; a Card with its quiet second Btn and the PagerAt under it.
|
|
7
7
|
* On a plain `Screen` (the usual case, Lh 2026-09-12) it is in flow: the column is one phone tall, the group takes
|
|
8
8
|
* the room after the TopBar and centres a short card; a long card makes the page longer and scrolls up under the
|
|
9
9
|
* bar — nothing is capped, nothing shrinks. On a `Screen fill` it floats at 40% down (`--screen-centre`), capped
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ComponentProps, type ReactNode, type Ref } from 'react';
|
|
2
|
-
export type Palette = 'clay' | '
|
|
2
|
+
export type Palette = 'clay' | 'roast' | 'hydro' | 'orchard' | 'vers';
|
|
3
3
|
export interface ScreenProps extends ComponentProps<'div'> {
|
|
4
4
|
/** exactly one phone height (100dvh), clipped: the box a Step or a SheetStack fills — an app screen, not a scrolling page */
|
|
5
5
|
fill?: boolean;
|
package/dist/scaffold/Screen.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement, useEffect, useRef } from 'react';
|
|
2
|
+
import { warnOnce } from '../state.js';
|
|
2
3
|
import { cx } from '../cx.js';
|
|
3
|
-
let warnedSheetBar = false;
|
|
4
4
|
/**
|
|
5
5
|
* Screen — the mobile screen in THREE parts: **top** and **content** (the children) in one flex column, the content down to the
|
|
6
6
|
* screen's foot; **bottom** a LAYER over it there (Lh 2026-09-23). Top = `TopBar` / `AppBar`, or `top="lead"` (no bar, the words start
|
|
@@ -12,10 +12,9 @@ export function Screen({ fill, palette, as = 'div', top, bottom, className, chil
|
|
|
12
12
|
const own = useRef(null);
|
|
13
13
|
useEffect(() => {
|
|
14
14
|
const el = own.current;
|
|
15
|
-
if (
|
|
15
|
+
if (!el || !el.querySelector(':scope > .action-bar') || !el.querySelector(':scope > .step > .sheet'))
|
|
16
16
|
return;
|
|
17
|
-
|
|
18
|
-
console.warn('cardds Screen: a bottom bar on a screen with a Sheet — the sheet finishes on its own foot (CardFoot); drop the bar.');
|
|
17
|
+
warnOnce('screen-sheet-bar', 'cardds Screen: a bottom bar on a screen with a Sheet — the sheet finishes on its own foot (CardFoot); drop the bar.');
|
|
19
18
|
});
|
|
20
19
|
/* the BOTTOM layer's height, published on the screen as --bottom-h (Lh 2026-09-23): the walk's pages end with that much room
|
|
21
20
|
more, so every last word scrolls up clear of the layer. Measured, because the layer is as tall as its rows and the text size. */
|
package/dist/scaffold/TopBar.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { warnOnce } from '../state.js';
|
|
3
4
|
import { cx } from '../cx.js';
|
|
4
5
|
/**
|
|
5
6
|
* TopBar — the screen's title bar: back · TITLE · actions. Same 60px floor as the AppBar.
|
|
@@ -9,12 +10,9 @@ export function TopBar({ title, back, actions, className, children, ...rest }) {
|
|
|
9
10
|
const acts = useRef(null);
|
|
10
11
|
useEffect(() => {
|
|
11
12
|
const n = acts.current?.childElementCount ?? 0;
|
|
12
|
-
if (n > MAX_ACTIONS
|
|
13
|
-
|
|
14
|
-
console.warn(`cardds TopBar: ${n} actions — at most ${MAX_ACTIONS} are drawn; put the rest in a Menu behind one button`);
|
|
15
|
-
}
|
|
13
|
+
if (n > MAX_ACTIONS)
|
|
14
|
+
warnOnce('topbar', `cardds TopBar: ${n} actions — at most ${MAX_ACTIONS} are drawn; put the rest in a Menu behind one button`);
|
|
16
15
|
});
|
|
17
16
|
return (_jsxs("header", { className: cx('topbar', className), ...rest, children: [back, _jsx("h1", { className: "t-h1", children: title }), children, actions != null && _jsx("div", { className: "topbar__actions", ref: acts, children: actions })] }));
|
|
18
17
|
}
|
|
19
18
|
const MAX_ACTIONS = 2;
|
|
20
|
-
let warned = false;
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { useControlled } from '../state.js';
|
|
3
4
|
import { cx } from '../cx.js';
|
|
4
5
|
const ORDER = ['peek', 'half', '3q', 'full']; // low → high; `away` is below them all and outside them
|
|
5
6
|
const TAP_REM = 0.5; // = --sp-2: a move shorter than this is a tap, not a drag
|
|
@@ -16,9 +17,7 @@ const TAP_REM = 0.5; // = --sp-2: a move shorter than this is a tap, not a drag
|
|
|
16
17
|
* climbs until the focused control has room above the foot.
|
|
17
18
|
*/
|
|
18
19
|
export function Sheet({ state, defaultState = 'half', onStateChange, states, handle = true, className, children, onFocus, ...rest }) {
|
|
19
|
-
const [
|
|
20
|
-
const current = state ?? inner;
|
|
21
|
-
const controlled = state !== undefined;
|
|
20
|
+
const [current, setOwn] = useControlled(state, defaultState);
|
|
22
21
|
const ref = useRef(null);
|
|
23
22
|
const handleRef = useRef(null);
|
|
24
23
|
const home = useRef(current !== 'peek' && current !== 'away' ? current : 'half'); // where a tap from peek returns to
|
|
@@ -32,10 +31,9 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
32
31
|
const request = useCallback((next, reason) => {
|
|
33
32
|
if (next === current)
|
|
34
33
|
return;
|
|
35
|
-
|
|
36
|
-
setInner(next);
|
|
34
|
+
setOwn(next);
|
|
37
35
|
onStateChange?.(next, reason);
|
|
38
|
-
}, [current,
|
|
36
|
+
}, [current, setOwn, onStateChange]);
|
|
39
37
|
useEffect(() => {
|
|
40
38
|
if (current !== 'peek' && current !== 'away')
|
|
41
39
|
home.current = current;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useLayoutEffect, useRef
|
|
2
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
3
|
+
import { useControlled } from '../state.js';
|
|
3
4
|
import { cx } from '../cx.js';
|
|
4
5
|
const PHASE = 380; // ms before the before-pack un-tucks on close, so the after-pack is clear first
|
|
5
6
|
/* the stack's sheets, in slot order: its direct children — or, where a host wraps each child in one box of its own, the first .card in
|
|
@@ -23,16 +24,14 @@ const sheetsOf = (stack) => [...stack.children]
|
|
|
23
24
|
* before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); it sets classes, never a size.
|
|
24
25
|
*/
|
|
25
26
|
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, fanned, closed, base, className, children, onClick, ...rest }) {
|
|
26
|
-
const [
|
|
27
|
-
const current = open !== undefined ? open : inner;
|
|
27
|
+
const [current, setOwn] = useControlled(open, defaultOpen);
|
|
28
28
|
const ref = useRef(null);
|
|
29
29
|
const was = useRef(null);
|
|
30
30
|
const untuck = useRef(null);
|
|
31
31
|
const request = (next) => {
|
|
32
32
|
if (next === current)
|
|
33
33
|
return;
|
|
34
|
-
|
|
35
|
-
setInner(next);
|
|
34
|
+
setOwn(next);
|
|
36
35
|
onOpenChange?.(next);
|
|
37
36
|
};
|
|
38
37
|
/* paint the state onto the sheets — after every render, so a re-rendered Card never loses its place */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children, cloneElement, createContext, isValidElement, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useControlled, reducedMotion } from '../state.js';
|
|
3
4
|
import { cx } from '../cx.js';
|
|
4
5
|
import { Icon } from '../type/Icon.js';
|
|
5
6
|
import { Link } from '../actions/Link.js';
|
|
@@ -17,8 +18,7 @@ const StepCtx = createContext(null);
|
|
|
17
18
|
* The sheet may be dragged down to peek and back — the body keeps where it was scrolled.
|
|
18
19
|
*/
|
|
19
20
|
export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible = false, numbered = false, finish, className, style, children, onFocus, onBlur, onPointerDown, onPointerUp, onPointerCancel, ...rest }) {
|
|
20
|
-
const [
|
|
21
|
-
const current = at ?? inner;
|
|
21
|
+
const [current, setOwn] = useControlled(at, defaultAt);
|
|
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 */
|
|
@@ -41,8 +41,7 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
|
|
|
41
41
|
const go = (i, reason) => {
|
|
42
42
|
if (i === current || i < 0 || i > count)
|
|
43
43
|
return;
|
|
44
|
-
|
|
45
|
-
setInner(i);
|
|
44
|
+
setOwn(i);
|
|
46
45
|
onAtChange?.(i, reason);
|
|
47
46
|
};
|
|
48
47
|
/* 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
|
|
@@ -72,7 +71,7 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
|
|
|
72
71
|
if (Math.abs(to - body.scrollTop) <= 1)
|
|
73
72
|
return;
|
|
74
73
|
// the pile SCROLLS there, animated (Lh 2026-09-24) — the browser's own smooth scroll; its frames are not replayed as jumps
|
|
75
|
-
const smooth = !
|
|
74
|
+
const smooth = !reducedMotion();
|
|
76
75
|
own.current = performance.now() + (smooth ? 700 : 0);
|
|
77
76
|
body.scrollTo({ top: to, behavior: smooth ? 'smooth' : 'auto' });
|
|
78
77
|
};
|
|
@@ -127,7 +126,7 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
|
|
|
127
126
|
const onScroll = () => {
|
|
128
127
|
const d = body.scrollTop - last.current;
|
|
129
128
|
last.current = body.scrollTop;
|
|
130
|
-
if (!d || performance.now() > glide.current || performance.now() < own.current ||
|
|
129
|
+
if (!d || performance.now() > glide.current || performance.now() < own.current || reducedMotion())
|
|
131
130
|
return;
|
|
132
131
|
const from = parseFloat(el.style.translate.split(' ')[1] || '0') || 0;
|
|
133
132
|
el.style.transition = 'none';
|
|
@@ -12,7 +12,7 @@ export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'
|
|
|
12
12
|
/** the SHEET HEAD's title — an h2 (the page's h1 is its own) */
|
|
13
13
|
title: ReactNode;
|
|
14
14
|
/** the paper: 1 light · 2 tinted · 3 dark */
|
|
15
|
-
tone?: 1 | 2 | 3;
|
|
15
|
+
tone?: 1 | 2 | 3 | 4 | 5;
|
|
16
16
|
/** the head's end when the sheet has no tabs: an Icon, an IconRow */
|
|
17
17
|
icon?: ReactNode;
|
|
18
18
|
/** 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,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { useControlled } from '../state.js';
|
|
3
4
|
import { cx } from '../cx.js';
|
|
4
5
|
import { Card } from '../cards/Card.js';
|
|
5
6
|
import { CardHead } from '../cards/CardHead.js';
|
|
@@ -16,10 +17,8 @@ import { Badge } from '../numbers/Badge.js';
|
|
|
16
17
|
* is closed — see `Home`). A direct child of `SheetStack`.
|
|
17
18
|
*/
|
|
18
19
|
export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = 'Back', className, children, ...rest }) {
|
|
19
|
-
const [
|
|
20
|
-
const
|
|
21
|
-
const pick = (id) => { if (tabProp === undefined)
|
|
22
|
-
setOwn(id); onTabChange?.(id); };
|
|
20
|
+
const [tab, setOwn] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id);
|
|
21
|
+
const pick = (id) => { setOwn(id); onTabChange?.(id); };
|
|
23
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
|
|
24
23
|
// is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
|
|
25
24
|
const titleRef = useRef(null);
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** 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 a setter that only moves the component's own copy — the caller still ASKS the app (onXChange). */
|
|
3
|
+
export declare function useControlled<T>(value: T | undefined, initial: T): [T, (next: T) => void];
|
|
4
|
+
/** a dev hint said once per key, never again */
|
|
5
|
+
export declare const warnOnce: (key: string, text: string) => void;
|
|
6
|
+
/** the member asked for less motion (false where there is no window, e.g. a server render) */
|
|
7
|
+
export declare const reducedMotion: () => boolean;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
/** controlled like an input: `value` given = the app owns it; undefined = the component keeps its own, starting at `initial`.
|
|
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) {
|
|
5
|
+
const [own, setOwn] = useState(initial);
|
|
6
|
+
const controlled = value !== undefined;
|
|
7
|
+
return [controlled ? value : own, (next) => { if (!controlled)
|
|
8
|
+
setOwn(next); }];
|
|
9
|
+
}
|
|
10
|
+
const warned = new Set();
|
|
11
|
+
/** a dev hint said once per key, never again */
|
|
12
|
+
export const warnOnce = (key, text) => { if (!warned.has(key)) {
|
|
13
|
+
warned.add(key);
|
|
14
|
+
console.warn(text);
|
|
15
|
+
} };
|
|
16
|
+
/** the member asked for less motion (false where there is no window, e.g. a server render) */
|
|
17
|
+
export const reducedMotion = () => typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
+
import { useControlled } from '../state.js';
|
|
3
4
|
import { Screen } from '../scaffold/Screen.js';
|
|
4
5
|
import { Step } from '../sheets/Step.js';
|
|
5
6
|
import { Page, CtaPack } from '../scaffold/Page.js';
|
|
@@ -18,11 +19,9 @@ const index = (o) => (o === true ? 0 : typeof o === 'number' ? o : null);
|
|
|
18
19
|
*/
|
|
19
20
|
export function CallSheet({ media, heading, body, action, sheet, ways, open: openProp, defaultOpen = null, onOpenChange, states = 'peek full', children, ...rest }) {
|
|
20
21
|
const list = ways ?? (sheet ? [{ action, sheet }] : []);
|
|
21
|
-
const [
|
|
22
|
-
const on = openProp === undefined ? own : index(openProp);
|
|
22
|
+
const [on, setOwn] = useControlled(openProp === undefined ? undefined : index(openProp), index(defaultOpen));
|
|
23
23
|
const [at, setAt] = useState('full');
|
|
24
|
-
const set = (next) => {
|
|
25
|
-
setOwn(next); onOpenChange?.(next !== null, next); };
|
|
24
|
+
const set = (next) => { setOwn(next); onOpenChange?.(next !== null, next); };
|
|
26
25
|
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')
|
|
27
26
|
set(null);
|
|
28
27
|
else
|
package/dist/templates/Home.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface HomeSheet {
|
|
|
7
7
|
/** the sheet head's end when it has no tabs: an Icon, an IconRow */
|
|
8
8
|
icon?: ReactNode;
|
|
9
9
|
/** the paper: 1 light · 2 tinted · 3 dark (default: 2, 3, 1, 2, 3 by place) */
|
|
10
|
-
tone?: 1 | 2 | 3;
|
|
10
|
+
tone?: 1 | 2 | 3 | 4 | 5;
|
|
11
11
|
/** the sheet's pages as icon tabs in its head (a count shows as a small circle over the icon) — see `StackSheet` */
|
|
12
12
|
tabs?: StackSheetTab[];
|
|
13
13
|
/** the tab shown first (default: the first) */
|
package/dist/templates/Home.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useControlled } from '../state.js';
|
|
3
4
|
import { Screen } from '../scaffold/Screen.js';
|
|
4
5
|
import { SheetStack } from '../sheets/SheetStack.js';
|
|
5
6
|
import { StackSheet } from '../sheets/StackSheet.js';
|
|
@@ -14,8 +15,7 @@ const LEAVE = 800; // ms: a closing sheet keeps its body until it has slid home
|
|
|
14
15
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
15
16
|
*/
|
|
16
17
|
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, base, keep, backLabel = 'Back', children, ...rest }) {
|
|
17
|
-
const [
|
|
18
|
-
const open = openProp !== undefined ? openProp : own;
|
|
18
|
+
const [open, setOwn] = useControlled(openProp, defaultOpen);
|
|
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,8 +32,7 @@ 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) => {
|
|
36
|
-
setOwn(next); onOpenChange?.(next); };
|
|
35
|
+
const change = (next) => { setOwn(next); onOpenChange?.(next); };
|
|
37
36
|
const [tabs, setTabs] = useState(() => Object.fromEntries(sheets.map((x, i) => [i, x.defaultTab ?? x.tabs?.[0]?.id])));
|
|
38
37
|
const shows = (i) => i === open || i === leaving || (keep && seen.has(i));
|
|
39
38
|
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,5 +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
4
|
import { Screen } from '../scaffold/Screen.js';
|
|
4
5
|
import { cx } from '../cx.js';
|
|
5
6
|
/**
|
|
@@ -23,7 +24,7 @@ export function Splash({ mark, text, done, min = 0, onLeft, className, ...rest }
|
|
|
23
24
|
useEffect(() => {
|
|
24
25
|
if (!leaving)
|
|
25
26
|
return;
|
|
26
|
-
const still =
|
|
27
|
+
const still = reducedMotion();
|
|
27
28
|
const ms = still ? 0 : parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--motion-sheet')) * 1000 || 350;
|
|
28
29
|
const t = setTimeout(() => left.current?.(), ms);
|
|
29
30
|
return () => clearTimeout(t);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useLayoutEffect, useRef
|
|
2
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
3
|
+
import { useControlled } from '../state.js';
|
|
3
4
|
import { Screen } from '../scaffold/Screen.js';
|
|
4
5
|
import { Step } from '../sheets/Step.js';
|
|
5
6
|
import { Page } from '../scaffold/Page.js';
|
|
@@ -16,8 +17,7 @@ import { IconBtn } from '../actions/IconBtn.js';
|
|
|
16
17
|
* first and fixed while the words scroll, one CTA row. Yours: the pages, the row, the state.
|
|
17
18
|
*/
|
|
18
19
|
export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta, controls = [], ctaLink, children, backLabel = 'Back', nextLabel = 'Next', ...rest }) {
|
|
19
|
-
const [
|
|
20
|
-
const at = atProp ?? own;
|
|
20
|
+
const [at, setOwn] = useControlled(atProp, defaultAt);
|
|
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,8 +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', () => {
|
|
31
|
-
setOwn(to); onAtChange?.(to); }, step.current ?? document);
|
|
30
|
+
slideTo(to > at ? 'next' : 'back', () => { setOwn(to); onAtChange?.(to); }, step.current ?? document);
|
|
32
31
|
};
|
|
33
32
|
/* a sideways SWIPE turns the page (Lh 2026-09-23): left = next, right = back. The walk still scrolls up and down itself
|
|
34
33
|
(touch-action: pan-y); a swipe must be mostly sideways and long enough, so a scroll never turns a page */
|