@usableapp/cardds 0.7.15 → 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 +53 -0
- package/README.md +29 -28
- package/cardds.css +3 -3
- package/css/actions.css +67 -22
- package/css/base.css +44 -18
- package/css/card.css +25 -26
- package/css/choice.css +21 -33
- package/css/forms.css +0 -3
- package/css/journey.css +1 -3
- package/css/layover.css +19 -41
- package/css/lists.css +1 -6
- package/css/media.css +9 -18
- package/css/numbers.css +6 -21
- package/css/people.css +3 -9
- package/css/sheet.css +15 -19
- package/css/stack.css +21 -29
- package/css/step.css +7 -12
- package/css/theme.template.css +2 -1
- package/css/tokens.css +22 -4
- package/dist/actions/Btn.d.ts +1 -1
- package/dist/actions/Dropdown.js +3 -9
- package/dist/actions/Fab.d.ts +1 -1
- package/dist/actions/Fab.js +3 -2
- package/dist/cardds.css +264 -267
- package/dist/cards/Card.d.ts +24 -8
- 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 +2 -5
- package/dist/choice/Mood.d.ts +10 -5
- package/dist/choice/Mood.js +4 -2
- package/dist/forms/Field.js +2 -3
- package/dist/forms/FileBtn.js +1 -1
- package/dist/forms/Pin.js +7 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/layover/Banner.d.ts +10 -3
- package/dist/layover/Banner.js +6 -3
- package/dist/lists/Row.d.ts +2 -1
- package/dist/lists/Row.js +5 -5
- package/dist/media/Postcard.js +3 -5
- package/dist/numbers/Band.d.ts +3 -1
- package/dist/numbers/Picker.js +3 -10
- 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/ActionBar.d.ts +3 -3
- package/dist/scaffold/ActionBar.js +2 -2
- package/dist/scaffold/Centre.d.ts +2 -2
- package/dist/scaffold/Centre.js +2 -2
- package/dist/scaffold/PagerAt.d.ts +1 -1
- package/dist/scaffold/PagerAt.js +1 -1
- package/dist/scaffold/Screen.js +4 -7
- package/dist/scaffold/TopBar.js +3 -5
- package/dist/sheets/Drawer.js +3 -4
- package/dist/sheets/Sheet.js +9 -22
- package/dist/sheets/SheetStack.d.ts +2 -2
- package/dist/sheets/SheetStack.js +20 -13
- package/dist/sheets/SheetSteps.js +8 -11
- package/dist/sheets/StackBack.js +2 -1
- package/dist/sheets/StackSheet.d.ts +3 -2
- package/dist/sheets/StackSheet.js +7 -11
- package/dist/state.d.ts +24 -0
- package/dist/state.js +57 -0
- package/dist/templates/CallSheet.js +2 -4
- package/dist/templates/Home.d.ts +3 -2
- package/dist/templates/Home.js +2 -4
- package/dist/templates/Splash.js +3 -3
- package/dist/templates/Walkthrough.js +4 -5
- package/dist/type/Icon.js +3 -5
- package/dist/type/Text.d.ts +1 -1
- package/dist/type/textScale.d.ts +6 -0
- package/dist/type/textScale.js +12 -0
- package/package.json +7 -4
- package/dist/layover/Deck.d.ts +0 -9
- package/dist/layover/Deck.js +0 -10
package/dist/scaffold/Screen.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement, useEffect, useRef } from 'react';
|
|
2
|
+
import { warnOnce, watchSize } 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. */
|
|
@@ -25,9 +24,7 @@ export function Screen({ fill, palette, as = 'div', top, bottom, className, chil
|
|
|
25
24
|
el?.style.removeProperty('--bottom-h');
|
|
26
25
|
return;
|
|
27
26
|
}
|
|
28
|
-
|
|
29
|
-
ro.observe(bar);
|
|
30
|
-
return () => ro.disconnect();
|
|
27
|
+
return watchSize([bar], () => el.style.setProperty('--bottom-h', `${bar.offsetHeight}px`));
|
|
31
28
|
}, [bottom]);
|
|
32
29
|
const setRef = (node) => {
|
|
33
30
|
own.current = node;
|
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/Drawer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Children, Fragment, useEffect, useRef } from 'react';
|
|
3
|
+
import { useLatest } from '../state.js';
|
|
3
4
|
import { cx } from '../cx.js';
|
|
4
5
|
/**
|
|
5
6
|
* Modal — the ask: ONE card at the centre of a `SheetStage dim`, two parts — a `Lift` (the thing being asked about, light, scrolls
|
|
@@ -11,8 +12,7 @@ import { cx } from '../cx.js';
|
|
|
11
12
|
*/
|
|
12
13
|
export function Modal({ open = true, onClosed, onDismiss, className, ...rest }) {
|
|
13
14
|
const ref = useRef(null);
|
|
14
|
-
const closed =
|
|
15
|
-
closed.current = onClosed;
|
|
15
|
+
const closed = useLatest(onClosed);
|
|
16
16
|
const shown = useRef(true); // the animations stand played forwards
|
|
17
17
|
useEffect(() => {
|
|
18
18
|
const el = ref.current;
|
|
@@ -28,8 +28,7 @@ export function Modal({ open = true, onClosed, onDismiss, className, ...rest })
|
|
|
28
28
|
closed.current?.(); });
|
|
29
29
|
return () => { live = false; };
|
|
30
30
|
}, [open]);
|
|
31
|
-
const dismiss =
|
|
32
|
-
dismiss.current = onDismiss;
|
|
31
|
+
const dismiss = useLatest(onDismiss);
|
|
33
32
|
const has = onDismiss !== undefined;
|
|
34
33
|
useEffect(() => {
|
|
35
34
|
const el = ref.current, stage = el?.parentElement;
|
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, watchSize } 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, request] = useControlled(state, defaultState, onStateChange);
|
|
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
|
|
@@ -29,13 +28,6 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
29
28
|
const list = (typeof states === 'string' ? states.split(/\s+/) : [...(states ?? [])]).filter((s) => ORDER.includes(s));
|
|
30
29
|
return list.length ? ORDER.filter((s) => list.includes(s)) : [...ORDER];
|
|
31
30
|
}, [states]);
|
|
32
|
-
const request = useCallback((next, reason) => {
|
|
33
|
-
if (next === current)
|
|
34
|
-
return;
|
|
35
|
-
if (!controlled)
|
|
36
|
-
setInner(next);
|
|
37
|
-
onStateChange?.(next, reason);
|
|
38
|
-
}, [current, controlled, onStateChange]);
|
|
39
31
|
useEffect(() => {
|
|
40
32
|
if (current !== 'peek' && current !== 'away')
|
|
41
33
|
home.current = current;
|
|
@@ -105,10 +97,10 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
105
97
|
in this sheet has the focus, make sure it still shows */
|
|
106
98
|
useEffect(() => {
|
|
107
99
|
const el = ref.current;
|
|
108
|
-
if (!el
|
|
100
|
+
if (!el)
|
|
109
101
|
return;
|
|
110
|
-
let first = true;
|
|
111
|
-
|
|
102
|
+
let first = true; // the browser's report at the start is not a resize
|
|
103
|
+
return watchSize([el, el.parentElement], () => {
|
|
112
104
|
if (first) {
|
|
113
105
|
first = false;
|
|
114
106
|
return;
|
|
@@ -117,10 +109,6 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
117
109
|
if (at && el.contains(at))
|
|
118
110
|
reveal(at, 'keyboard');
|
|
119
111
|
});
|
|
120
|
-
ro.observe(el);
|
|
121
|
-
if (el.parentElement)
|
|
122
|
-
ro.observe(el.parentElement);
|
|
123
|
-
return () => ro.disconnect();
|
|
124
112
|
}, [reveal]);
|
|
125
113
|
/* the peek strip holds the handle and the WHOLE title row. CSS gives the box the strip for a one-line head (--sheet-peek in
|
|
126
114
|
sheet.css); when the head is taller than that floor — a title wrapped on a narrow phone at a large text size — the sheet
|
|
@@ -128,7 +116,7 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
128
116
|
22-base type scale). Removed when the head fits the floor again, so the CSS number is the one in force. */
|
|
129
117
|
useEffect(() => {
|
|
130
118
|
const el = ref.current, box = el?.parentElement, head = el?.querySelector(':scope > .card__head, :scope > .sheet__head');
|
|
131
|
-
if (!el || !box || !head
|
|
119
|
+
if (!el || !box || !head)
|
|
132
120
|
return;
|
|
133
121
|
const publish = () => {
|
|
134
122
|
box.style.removeProperty('--sheet-peek'); // read the CSS strip, not our own
|
|
@@ -138,9 +126,8 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
138
126
|
if (over > 0.5)
|
|
139
127
|
box.style.setProperty('--sheet-peek', `${strip + over}px`);
|
|
140
128
|
};
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
return () => { ro.disconnect(); box.style.removeProperty('--sheet-peek'); };
|
|
129
|
+
const stop = watchSize([head], publish);
|
|
130
|
+
return () => { stop(); box.style.removeProperty('--sheet-peek'); };
|
|
144
131
|
}, []);
|
|
145
132
|
const focus = (e) => {
|
|
146
133
|
onFocus?.(e);
|
|
@@ -21,7 +21,7 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
21
21
|
* Children are Cards, direct children only, at most five: first = the back of the stack, last = the front (open) sheet.
|
|
22
22
|
* Every sheet is the stack's full height; a peek is the strip the next sheet doesn't cover. A stack head is a CardHead
|
|
23
23
|
* with CHILDREN, in this order: `<StackBack />` (tap mode), `<Text level="h2" caps>Shopping</Text>`, an `<IconRow>` of Icons —
|
|
24
|
-
* never the `heading` prop (
|
|
24
|
+
* never the `heading` prop (a stack head is back · h2 · tabs on ONE line — StackSheet builds it). Put it last in a `Screen fill`, it bleeds
|
|
25
25
|
* off the bottom. `closed`: nothing open on arrival — every head peeks at the bottom and the base above shows `base` (a home screen
|
|
26
26
|
* with a picture over the menu); the `base` slot is rendered after the sheets, so it never takes a sheet's slot. This is the sheet STACK; a single bottom sheet is `Sheet`.
|
|
27
27
|
*
|
|
@@ -30,4 +30,4 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
30
30
|
* sheet's StackBack or its title (folds it). How the sheets MOVE is its own too: sheets are rigid — the tapped one slides up, the ones after it leave below, the ones
|
|
31
31
|
* before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); it sets classes, never a size.
|
|
32
32
|
*/
|
|
33
|
-
export declare function SheetStack({ mode, open, defaultOpen, onOpenChange, fanned, closed, base, className, children, onClick, ...rest }: SheetStackProps): import("react").JSX.Element;
|
|
33
|
+
export declare function SheetStack({ mode, open, defaultOpen, onOpenChange, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }: SheetStackProps): import("react").JSX.Element;
|
|
@@ -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
|
|
@@ -13,7 +14,7 @@ const sheetsOf = (stack) => [...stack.children]
|
|
|
13
14
|
* Children are Cards, direct children only, at most five: first = the back of the stack, last = the front (open) sheet.
|
|
14
15
|
* Every sheet is the stack's full height; a peek is the strip the next sheet doesn't cover. A stack head is a CardHead
|
|
15
16
|
* with CHILDREN, in this order: `<StackBack />` (tap mode), `<Text level="h2" caps>Shopping</Text>`, an `<IconRow>` of Icons —
|
|
16
|
-
* never the `heading` prop (
|
|
17
|
+
* never the `heading` prop (a stack head is back · h2 · tabs on ONE line — StackSheet builds it). Put it last in a `Screen fill`, it bleeds
|
|
17
18
|
* off the bottom. `closed`: nothing open on arrival — every head peeks at the bottom and the base above shows `base` (a home screen
|
|
18
19
|
* with a picture over the menu); the `base` slot is rendered after the sheets, so it never takes a sheet's slot. This is the sheet STACK; a single bottom sheet is `Sheet`.
|
|
19
20
|
*
|
|
@@ -22,25 +23,24 @@ const sheetsOf = (stack) => [...stack.children]
|
|
|
22
23
|
* sheet's StackBack or its title (folds it). How the sheets MOVE is its own too: sheets are rigid — the tapped one slides up, the ones after it leave below, the ones
|
|
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
|
-
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;
|
|
26
|
+
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }) {
|
|
27
|
+
const [current, request] = useControlled(open, defaultOpen, onOpenChange);
|
|
28
28
|
const ref = useRef(null);
|
|
29
29
|
const was = useRef(null);
|
|
30
30
|
const untuck = useRef(null);
|
|
31
|
-
const request = (next) => {
|
|
32
|
-
if (next === current)
|
|
33
|
-
return;
|
|
34
|
-
if (open === undefined)
|
|
35
|
-
setInner(next);
|
|
36
|
-
onOpenChange?.(next);
|
|
37
|
-
};
|
|
38
31
|
/* paint the state onto the sheets — after every render, so a re-rendered Card never loses its place */
|
|
39
32
|
useLayoutEffect(() => {
|
|
40
33
|
const stack = ref.current;
|
|
41
34
|
if (!stack || mode !== 'tap')
|
|
42
35
|
return;
|
|
43
36
|
const sheets = sheetsOf(stack);
|
|
37
|
+
// the keyboard's way in: a head's title takes the focus where a tap on it would do something — every title while none is
|
|
38
|
+
// open (Enter opens that sheet), only the open one's while one is (Enter folds it)
|
|
39
|
+
sheets.forEach((c, i) => {
|
|
40
|
+
const title = c.querySelector(':scope > .card__head > :is(h1, h2, h3)');
|
|
41
|
+
if (title)
|
|
42
|
+
title.tabIndex = current == null || i === current ? 0 : -1;
|
|
43
|
+
});
|
|
44
44
|
const prev = was.current;
|
|
45
45
|
was.current = current;
|
|
46
46
|
if (current != null) {
|
|
@@ -87,5 +87,12 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
87
87
|
if (at >= 0 && current == null)
|
|
88
88
|
request(at);
|
|
89
89
|
};
|
|
90
|
-
|
|
90
|
+
const key = (e) => {
|
|
91
|
+
onKeyDown?.(e);
|
|
92
|
+
if ((e.key === 'Enter' || e.key === ' ') && e.target.matches('.card__head > :is(h1, h2, h3)')) {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
e.target.click();
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
return (_jsxs("div", { ref: ref, onClick: click, onKeyDown: key, className: cx('sheet-stack', mode !== 'static' && `sheet-stack--${mode}`, fanned && 'sheet-stack--fanned', closed && 'sheet-stack--closed', className), ...rest, children: [children, base != null && _jsx("div", { className: "sheet-stack__base", children: base })] }));
|
|
91
98
|
}
|
|
@@ -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, 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,11 +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
|
-
if (at === undefined)
|
|
45
|
-
setInner(i);
|
|
46
|
-
onAtChange?.(i, reason);
|
|
42
|
+
if (i >= 0 && i <= count)
|
|
43
|
+
request(i, reason);
|
|
47
44
|
};
|
|
48
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
|
|
49
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
|
|
@@ -72,7 +69,7 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
|
|
|
72
69
|
if (Math.abs(to - body.scrollTop) <= 1)
|
|
73
70
|
return;
|
|
74
71
|
// 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 = !
|
|
72
|
+
const smooth = !reducedMotion();
|
|
76
73
|
own.current = performance.now() + (smooth ? 700 : 0);
|
|
77
74
|
body.scrollTo({ top: to, behavior: smooth ? 'smooth' : 'auto' });
|
|
78
75
|
};
|
|
@@ -127,7 +124,7 @@ export function SheetSteps({ at, reached, defaultAt = 0, onAtChange, reversible
|
|
|
127
124
|
const onScroll = () => {
|
|
128
125
|
const d = body.scrollTop - last.current;
|
|
129
126
|
last.current = body.scrollTop;
|
|
130
|
-
if (!d || performance.now() > glide.current || performance.now() < own.current ||
|
|
127
|
+
if (!d || performance.now() > glide.current || performance.now() < own.current || reducedMotion())
|
|
131
128
|
return;
|
|
132
129
|
const from = parseFloat(el.style.translate.split(' ')[1] || '0') || 0;
|
|
133
130
|
el.style.transition = 'none';
|
|
@@ -221,7 +218,7 @@ export function SheetStep({ title, summary, skippable, skipLabel = 'Skip', error
|
|
|
221
218
|
e.preventDefault();
|
|
222
219
|
};
|
|
223
220
|
const press = (e) => {
|
|
224
|
-
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
|
|
225
222
|
if (!btn || btn.disabled)
|
|
226
223
|
return;
|
|
227
224
|
let bad = null;
|
|
@@ -243,7 +240,7 @@ export function SheetStep({ title, summary, skippable, skipLabel = 'Skip', error
|
|
|
243
240
|
const enter = (e) => {
|
|
244
241
|
if (e.key !== 'Enter' || e.nativeEvent.isComposing || !(e.target instanceof HTMLInputElement))
|
|
245
242
|
return;
|
|
246
|
-
const btn = e.currentTarget.querySelector('.btn:not(:disabled)');
|
|
243
|
+
const btn = e.currentTarget.querySelector('button.btn:not(:disabled)');
|
|
247
244
|
if (btn) {
|
|
248
245
|
e.preventDefault();
|
|
249
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,7 +1,9 @@
|
|
|
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, watchSize } from '../state.js';
|
|
3
4
|
import { cx } from '../cx.js';
|
|
4
5
|
import { Card } from '../cards/Card.js';
|
|
6
|
+
import { IconBtn } from '../actions/IconBtn.js';
|
|
5
7
|
import { CardHead } from '../cards/CardHead.js';
|
|
6
8
|
import { CardFoot } from '../cards/CardFoot.js';
|
|
7
9
|
import { SheetBody } from './SheetBody.js';
|
|
@@ -16,16 +18,13 @@ import { Badge } from '../numbers/Badge.js';
|
|
|
16
18
|
* is closed — see `Home`). A direct child of `SheetStack`.
|
|
17
19
|
*/
|
|
18
20
|
export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = 'Back', className, children, ...rest }) {
|
|
19
|
-
const [
|
|
20
|
-
const tab = tabProp ?? own;
|
|
21
|
-
const pick = (id) => { if (tabProp === undefined)
|
|
22
|
-
setOwn(id); onTabChange?.(id); };
|
|
21
|
+
const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
|
|
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);
|
|
26
25
|
useEffect(() => {
|
|
27
26
|
const h = titleRef.current, run = h?.firstElementChild;
|
|
28
|
-
if (!h || !run
|
|
27
|
+
if (!h || !run)
|
|
29
28
|
return;
|
|
30
29
|
const measure = () => {
|
|
31
30
|
const over = Math.max(0, Math.ceil(run.scrollWidth - h.clientWidth));
|
|
@@ -33,11 +32,8 @@ export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab,
|
|
|
33
32
|
h.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
|
|
34
33
|
h.classList.toggle('is-long', over > 0);
|
|
35
34
|
};
|
|
36
|
-
const ro = new ResizeObserver(measure);
|
|
37
|
-
ro.observe(h);
|
|
38
|
-
ro.observe(run);
|
|
39
35
|
measure();
|
|
40
|
-
return (
|
|
36
|
+
return watchSize([h, run], measure);
|
|
41
37
|
}, [title]);
|
|
42
|
-
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 })] }));
|
|
43
39
|
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type SyntheticEvent } 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 `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;
|
|
20
|
+
/** a dev hint said once per key, never again */
|
|
21
|
+
export declare const warnOnce: (key: string, text: string) => void;
|
|
22
|
+
/** the member asked for less motion (false where there is no window, e.g. a server render) */
|
|
23
|
+
export declare const reducedMotion: () => boolean;
|
|
24
|
+
export {};
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
|
+
export function useControlled(value, initial, onChange) {
|
|
3
|
+
const [own, setOwn] = useState(initial);
|
|
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();
|
|
49
|
+
}
|
|
50
|
+
const warned = new Set();
|
|
51
|
+
/** a dev hint said once per key, never again */
|
|
52
|
+
export const warnOnce = (key, text) => { if (!warned.has(key)) {
|
|
53
|
+
warned.add(key);
|
|
54
|
+
console.warn(text);
|
|
55
|
+
} };
|
|
56
|
+
/** the member asked for less motion (false where there is no window, e.g. a server render) */
|
|
57
|
+
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,8 @@ 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, 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) => { if (openProp === undefined)
|
|
25
|
-
setOwn(next); onOpenChange?.(next !== null, next); };
|
|
26
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')
|
|
27
25
|
set(null);
|
|
28
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
|
@@ -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, 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,8 +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) => { if (openProp === undefined)
|
|
36
|
-
setOwn(next); onOpenChange?.(next); };
|
|
37
35
|
const [tabs, setTabs] = useState(() => Object.fromEntries(sheets.map((x, i) => [i, x.defaultTab ?? x.tabs?.[0]?.id])));
|
|
38
36
|
const shows = (i) => i === open || i === leaving || (keep && seen.has(i));
|
|
39
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,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, useLatest } from '../state.js';
|
|
3
4
|
import { Screen } from '../scaffold/Screen.js';
|
|
4
5
|
import { cx } from '../cx.js';
|
|
5
6
|
/**
|
|
@@ -12,8 +13,7 @@ import { cx } from '../cx.js';
|
|
|
12
13
|
export function Splash({ mark, text, done, min = 0, onLeft, className, ...rest }) {
|
|
13
14
|
const born = useRef(Date.now());
|
|
14
15
|
const [leaving, setLeaving] = useState(false);
|
|
15
|
-
const left =
|
|
16
|
-
left.current = onLeft;
|
|
16
|
+
const left = useLatest(onLeft);
|
|
17
17
|
useEffect(() => {
|
|
18
18
|
if (!done)
|
|
19
19
|
return;
|
|
@@ -23,7 +23,7 @@ export function Splash({ mark, text, done, min = 0, onLeft, className, ...rest }
|
|
|
23
23
|
useEffect(() => {
|
|
24
24
|
if (!leaving)
|
|
25
25
|
return;
|
|
26
|
-
const still =
|
|
26
|
+
const still = reducedMotion();
|
|
27
27
|
const ms = still ? 0 : parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--motion-sheet')) * 1000 || 350;
|
|
28
28
|
const t = setTimeout(() => left.current?.(), ms);
|
|
29
29
|
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, 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,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', () => turn(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 */
|
package/dist/type/Icon.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { warnOnce } from '../state.js';
|
|
2
3
|
import { cx } from '../cx.js';
|
|
3
4
|
import { ICONS } from './icons.js';
|
|
4
5
|
/* the stroke style every stock symbol shares (Lucide): a project's drawing takes it unless it brings its own attrs */
|
|
@@ -14,7 +15,6 @@ export function registerIcons(icons) {
|
|
|
14
15
|
for (const [name, d] of Object.entries(icons))
|
|
15
16
|
OWN.set(name, typeof d === 'string' ? { attrs: STROKE, body: d } : { attrs: { ...STROKE, ...d.attrs }, body: d.body });
|
|
16
17
|
}
|
|
17
|
-
const warned = new Set();
|
|
18
18
|
/**
|
|
19
19
|
* Icon — a stroke icon, inlined (no sprite file at runtime): one of the stock Lucide symbols, one of the project's OWN (`registerIcons`), or a
|
|
20
20
|
* one-off drawn with `children`. The set is open — a project never comes to cardds for an icon.
|
|
@@ -23,10 +23,8 @@ const warned = new Set();
|
|
|
23
23
|
*/
|
|
24
24
|
export function Icon({ name, children, size = 'md', label, className, ...rest }) {
|
|
25
25
|
const icon = name === undefined ? undefined : OWN.get(name) ?? ICONS[name];
|
|
26
|
-
if (name !== undefined && !icon
|
|
27
|
-
|
|
28
|
-
console.warn(`cardds Icon: no icon named "${name}" — a stock name is misspelt, or the project has not registered it (registerIcons)`);
|
|
29
|
-
}
|
|
26
|
+
if (name !== undefined && !icon)
|
|
27
|
+
warnOnce(`icon:${name}`, `cardds Icon: no icon named "${name}" — a stock name is misspelt, or the project has not registered it (registerIcons)`);
|
|
30
28
|
const shared = { className: cx('icon', size !== 'md' && `icon--${size}`, className), role: label ? 'img' : undefined, 'aria-label': label, 'aria-hidden': label ? undefined : true };
|
|
31
29
|
// an unknown name draws nothing but keeps its box — a missing icon never takes a screen down
|
|
32
30
|
if (!icon)
|