@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
package/dist/cards/Card.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { type ComponentProps } from 'react';
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import { type CardHeadProps } from './CardHead.js';
|
|
3
|
+
/** a card's paper: 1 (default, light) · 2 (tinted) · 3 (dark / accent — controls invert) · 4 · 5 (more colours, from the palette) */
|
|
4
|
+
export type Tone = 1 | 2 | 3 | 4 | 5;
|
|
2
5
|
export interface CardProps extends ComponentProps<'article'> {
|
|
3
|
-
/** the tone slot
|
|
4
|
-
tone?:
|
|
6
|
+
/** the tone slot — see `Tone` */
|
|
7
|
+
tone?: Tone;
|
|
5
8
|
/** compact: a small card inside a card or a sheet body (a note, a promo) */
|
|
6
9
|
sm?: boolean;
|
|
7
10
|
/** a list row that is its own card: thumb · body · end, in a CardList */
|
|
@@ -17,14 +20,28 @@ export interface CardProps extends ComponentProps<'article'> {
|
|
|
17
20
|
/** the element: article (default), a when it links, label when it wraps an input, div/section */
|
|
18
21
|
as?: 'article' | 'div' | 'a' | 'label' | 'section' | 'li';
|
|
19
22
|
href?: string;
|
|
23
|
+
/** THE HEAD, filled in place: the heading (wraps freely) — the card writes its CardHead first. For a head of your own shape
|
|
24
|
+
* (a chip before the words, a tile badge) write `<CardHead>` as the first child instead, and leave these out */
|
|
25
|
+
heading?: ReactNode;
|
|
26
|
+
/** the muted line under the heading */
|
|
27
|
+
caption?: ReactNode;
|
|
28
|
+
/** the head's far-right thing: a Chip, an IconBtn, a Badge, a small Ring */
|
|
29
|
+
end?: ReactNode;
|
|
30
|
+
/** the heading's level (h2 default; h1 for the one-feature page) */
|
|
31
|
+
level?: CardHeadProps['level'];
|
|
32
|
+
/** THE FOOT, filled in place: its Btns (≤4, one line) — the card writes its CardFoot last */
|
|
33
|
+
foot?: ReactNode;
|
|
34
|
+
/** the small Link under the foot's buttons (CardFoot `link`) */
|
|
35
|
+
footLink?: ReactNode;
|
|
20
36
|
}
|
|
21
37
|
/**
|
|
22
38
|
* Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
39
|
+
* The head and the foot are SLOTS: `heading` · `caption` · `end` (the card writes its CardHead first) and `foot` · `footLink`
|
|
40
|
+
* (its CardFoot last); the children are the content between them (frameless rows, kv, a ring, a paragraph — never a second card
|
|
41
|
+
* boundary). The order is the card's, never the writer's. A head of its own shape: write `<CardHead>` as the first child instead.
|
|
25
42
|
* A page with one feature and one CTA may be a single card. Keep everything big.
|
|
26
43
|
*/
|
|
27
|
-
export declare function Card({ tone, sm, row, cover, fold, centre, band, as, className, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
|
|
44
|
+
export declare function Card({ tone, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, className, children, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
|
|
28
45
|
href?: string;
|
|
29
46
|
ref?: import("react").Ref<HTMLElement> | undefined;
|
|
30
47
|
key?: import("react").Key | null | undefined;
|
|
@@ -135,7 +152,6 @@ export declare function Card({ tone, sm, row, cover, fold, centre, band, as, cla
|
|
|
135
152
|
"aria-valuemin"?: number | undefined;
|
|
136
153
|
"aria-valuenow"?: number | undefined;
|
|
137
154
|
"aria-valuetext"?: string | undefined;
|
|
138
|
-
children?: import("react").ReactNode | undefined;
|
|
139
155
|
dangerouslySetInnerHTML?: {
|
|
140
156
|
__html: string | TrustedHTML;
|
|
141
157
|
} | undefined;
|
package/dist/cards/Card.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Children, createElement, isValidElement } from 'react';
|
|
2
3
|
import { cx } from '../cx.js';
|
|
4
|
+
import { warnOnce } from '../state.js';
|
|
5
|
+
import { CardHead } from './CardHead.js';
|
|
6
|
+
import { CardFoot } from './CardFoot.js';
|
|
3
7
|
/**
|
|
4
8
|
* Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
9
|
+
* The head and the foot are SLOTS: `heading` · `caption` · `end` (the card writes its CardHead first) and `foot` · `footLink`
|
|
10
|
+
* (its CardFoot last); the children are the content between them (frameless rows, kv, a ring, a paragraph — never a second card
|
|
11
|
+
* boundary). The order is the card's, never the writer's. A head of its own shape: write `<CardHead>` as the first child instead.
|
|
7
12
|
* A page with one feature and one CTA may be a single card. Keep everything big.
|
|
8
13
|
*/
|
|
9
|
-
export function Card({ tone, sm, row, cover, fold, centre, band, as = 'article', className, ...rest }) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
export function Card({ tone, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, className, children, ...rest }) {
|
|
15
|
+
const head = heading != null || caption != null || end != null;
|
|
16
|
+
const tail = foot != null || footLink != null;
|
|
17
|
+
if (head || tail) {
|
|
18
|
+
const parts = Children.toArray(children).filter(isValidElement); // dev hint only
|
|
19
|
+
if (head && parts.some((c) => c.type === CardHead))
|
|
20
|
+
warnOnce('card-head-twice', 'cardds Card: `heading`/`caption`/`end` AND a <CardHead> child — one head per card; use one way');
|
|
21
|
+
if (tail && parts.some((c) => c.type === CardFoot))
|
|
22
|
+
warnOnce('card-foot-twice', 'cardds Card: `foot`/`footLink` AND a <CardFoot> child — one foot per card; use one way');
|
|
23
|
+
}
|
|
24
|
+
const props = { className: cx('card', tone && tone !== 1 && `card--${tone}`, sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', className), ...rest };
|
|
25
|
+
if (!head && !tail)
|
|
26
|
+
return createElement(as, props, children);
|
|
27
|
+
return createElement(as, props, head && _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }, "head"), ...Children.toArray(children), // keyed: the slots sit around them
|
|
28
|
+
tail && _jsx(CardFoot, { link: footLink, children: foot }, "foot"));
|
|
14
29
|
}
|
package/dist/cards/CardHead.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { ComponentProps, ReactNode } from 'react';
|
|
|
2
2
|
export interface CardHeadProps extends ComponentProps<'div'> {
|
|
3
3
|
/** the heading (a string becomes an h2 at level `t-h2`) — wraps freely, as many lines as the words need */
|
|
4
4
|
heading?: ReactNode;
|
|
5
|
-
/** small caps above the heading ("Step 1 of 4", "Time bank") */
|
|
6
5
|
/** the muted line under the heading (a date, a subtitle) */
|
|
7
6
|
caption?: ReactNode;
|
|
8
7
|
/** the contextual thing at the far right: a chip, an IconBtn (more), a Link, a small Ring, an Avatar, a Badge (a count in a stack head) */
|
package/dist/cards/CardHead.js
CHANGED
|
@@ -7,5 +7,5 @@ import { cx } from '../cx.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export function CardHead({ heading, caption, end, top, level = 'h2', className, children, ...rest }) {
|
|
9
9
|
const H = level === 'h1' ? 'h1' : 'h2';
|
|
10
|
-
return (_jsxs("div", { className: cx('card__head', top && 'card__head--top', className), ...rest, children: [heading != null && _jsx(H, { className:
|
|
10
|
+
return (_jsxs("div", { className: cx('card__head', top && 'card__head--top', className), ...rest, children: [heading != null && _jsx(H, { className: `t-${level}`, children: heading }), caption != null && _jsx("span", { className: "t-caption t-muted", children: caption }), children, end] }));
|
|
11
11
|
}
|
package/dist/choice/Calendar.js
CHANGED
|
@@ -5,7 +5,6 @@ import { IconBtn } from '../actions/IconBtn.js';
|
|
|
5
5
|
/** Calendar — a month grid of 44px round cells: one accent day, optional marks; prev/next in the head. It shows ONE month and knows no dates:
|
|
6
6
|
* the app owns which month (`title`, `days`, `start` + `onPrev` / `onNext`) and which day (`selected` + `onSelect`, or `defaultSelected`). */
|
|
7
7
|
export function Calendar({ title, days = 30, start = 1, selected, defaultSelected, onSelect, onPrev, onNext, prevLabel = 'Previous', nextLabel = 'Next', marks = [], marks2 = [], disabled = [], weekdays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], nav, className, ...rest }) {
|
|
8
|
-
const [day,
|
|
9
|
-
const pick = (d) => { setOwn(d); onSelect?.(d); };
|
|
8
|
+
const [day, pick] = useControlled(selected, defaultSelected, onSelect);
|
|
10
9
|
return (_jsxs("div", { className: cx('calendar', className), ...rest, children: [_jsxs("div", { className: "calendar__head", children: [_jsx("span", { className: "t-title", children: title }), nav ?? (_jsxs("span", { className: "icon-row", children: [_jsx(IconBtn, { sm: true, icon: "chevron-left", label: prevLabel, onClick: onPrev }), _jsx(IconBtn, { sm: true, icon: "chevron-right", label: nextLabel, onClick: onNext })] }))] }), _jsxs("div", { className: "calendar__grid", children: [weekdays.map((d) => _jsx("span", { className: "calendar__wd", children: d }, d)), Array.from({ length: days }, (_, i) => i + 1).map((d) => (_jsx("button", { type: "button", className: cx('calendar__day', marks.includes(d) && 'calendar__day--mark', marks2.includes(d) && 'calendar__day--mark2'), "aria-selected": d === day ? 'true' : undefined, onClick: () => pick(d), disabled: disabled.includes(d), style: d === 1 ? { '--start': start } : undefined, children: d }, d)))] })] }));
|
|
11
10
|
}
|
package/dist/choice/Mood.js
CHANGED
|
@@ -3,6 +3,6 @@ import { cx } from '../cx.js';
|
|
|
3
3
|
import { useControlled } from '../state.js';
|
|
4
4
|
/** Mood — five dots in a pill from bad to good; the picked one grows. */
|
|
5
5
|
export function Mood({ name = 'mood', value, defaultValue, label = 'Mood', onChange, className, ...rest }) {
|
|
6
|
-
const [picked,
|
|
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: () =>
|
|
6
|
+
const [picked, pick] = useControlled(value, defaultValue, onChange);
|
|
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: () => pick(n) }), _jsx("span", { className: "mood__dot" })] }, n))) }));
|
|
8
8
|
}
|
package/dist/forms/Field.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { watchSize } from '../state.js';
|
|
3
4
|
import { formatOk, useCheck } from './check.js';
|
|
4
5
|
import { cx } from '../cx.js';
|
|
5
6
|
import { Icon } from '../type/Icon.js';
|
|
@@ -59,10 +60,8 @@ export function Field({ icon, action, label, error, format, digits, formatError,
|
|
|
59
60
|
el.style.removeProperty('--_ph-fit');
|
|
60
61
|
};
|
|
61
62
|
fit();
|
|
62
|
-
const ro = new ResizeObserver(fit);
|
|
63
|
-
ro.observe(el);
|
|
64
63
|
document.fonts?.ready.then(fit);
|
|
65
|
-
return (
|
|
64
|
+
return watchSize([el], fit);
|
|
66
65
|
}, [input.placeholder]);
|
|
67
66
|
useEffect(() => {
|
|
68
67
|
if (wrong) {
|
package/dist/forms/FileBtn.js
CHANGED
|
@@ -6,5 +6,5 @@ import { cx } from '../cx.js';
|
|
|
6
6
|
* opens the camera; without `capture` the gallery. The picked file arrives in `onChange` like any input.
|
|
7
7
|
*/
|
|
8
8
|
export function FileBtn({ primary, quiet, block, className, children, ...input }) {
|
|
9
|
-
return (_jsxs("label", { className: cx('btn', primary && 'btn--primary', quiet && 'btn--quiet', block && 'btn--block', className), children: [_jsx("input", { type: "file",
|
|
9
|
+
return (_jsxs("label", { className: cx('btn', primary && 'btn--primary', quiet && 'btn--quiet', block && 'btn--block', className), children: [_jsx("input", { type: "file", className: "t-sr", ...input }), children] }));
|
|
10
10
|
}
|
package/dist/forms/Pin.js
CHANGED
|
@@ -1,6 +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
|
+
import { useControlled, useLatest } from '../state.js';
|
|
4
4
|
import { cx } from '../cx.js';
|
|
5
5
|
import { useCheck } from './check.js';
|
|
6
6
|
const RESET_MS = 900; // a code that does not match: red + shake this long, then the cells empty for a new try
|
|
@@ -12,22 +12,20 @@ const RESET_MS = 900; // a code that does not match: red + shake this long, then
|
|
|
12
12
|
*/
|
|
13
13
|
export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onChange, autoFocus, error, match, formatError, secret, className, onAnimationEnd, ...rest }) {
|
|
14
14
|
const ref = useRef(null);
|
|
15
|
-
const [typed,
|
|
15
|
+
const [typed, request] = useControlled(value, defaultValue, onChange);
|
|
16
16
|
const code = typed.replace(/\D/g, '').slice(0, length);
|
|
17
17
|
const cell = (i) => ref.current?.querySelectorAll('.pin__cell')[i];
|
|
18
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 */
|
|
19
|
-
const latest =
|
|
20
|
-
latest.current = { setOwn, onChange, error };
|
|
19
|
+
const latest = useLatest(error);
|
|
21
20
|
const miss = () => {
|
|
22
21
|
restart.current = true;
|
|
23
22
|
setShake(true);
|
|
24
23
|
clearTimeout(reset.current);
|
|
25
24
|
reset.current = setTimeout(() => {
|
|
26
25
|
setBad(false);
|
|
27
|
-
setCleared(latest.current
|
|
26
|
+
setCleared(latest.current);
|
|
28
27
|
restart.current = false;
|
|
29
|
-
|
|
30
|
-
latest.current.onChange?.('', false);
|
|
28
|
+
request('', false);
|
|
31
29
|
cell(0)?.focus();
|
|
32
30
|
}, RESET_MS);
|
|
33
31
|
};
|
|
@@ -46,9 +44,7 @@ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onC
|
|
|
46
44
|
said.current = true;
|
|
47
45
|
miss();
|
|
48
46
|
}
|
|
49
|
-
|
|
50
|
-
if (next !== code)
|
|
51
|
-
onChange?.(next, next.length === length);
|
|
47
|
+
request(next, next.length === length);
|
|
52
48
|
};
|
|
53
49
|
useEffect(() => {
|
|
54
50
|
if (autoFocus)
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cardds — the React adapter over the CSS. Thin: every component emits the
|
|
2
|
-
// markup contract the CSS documents
|
|
3
|
-
// the
|
|
2
|
+
// markup contract the CSS documents; a component's own behaviour is its own React code.
|
|
3
|
+
// cardds.js rides along for the environment only: the keyboard's height (--kb) and .bleed's scrollbar.
|
|
4
4
|
import '../cardds.js';
|
|
5
5
|
// Popover for Safari/iOS 16.4–16.x (native from 17): applies itself only where the platform lacks it.
|
|
6
6
|
import '@oddbird/popover-polyfill';
|
package/dist/layover/Banner.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
-
export interface BannerProps extends Omit<ComponentProps<'details'>, 'summary'> {
|
|
2
|
+
export interface BannerProps extends Omit<ComponentProps<'details'>, 'summary' | 'open' | 'onToggle'> {
|
|
3
3
|
/** the line that opens it — words (and an inline Icon); the chevron is the Banner's own */
|
|
4
4
|
summary: ReactNode;
|
|
5
|
+
/** CONTROLLED: open — with `onOpenChange`; or `defaultOpen` */
|
|
6
|
+
open?: boolean;
|
|
7
|
+
/** UNCONTROLLED: start open */
|
|
8
|
+
defaultOpen?: boolean;
|
|
9
|
+
/** the summary was tapped: the state it asks for */
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
5
11
|
}
|
|
6
12
|
/** Banner — ONE band: the line that opens it and what it opens share the same ground, so they read as one piece (Lh 2026-09-24).
|
|
7
|
-
* A light tint with a darker rule above and below, edge to edge of its card or screen — a band, never a card in a card.
|
|
8
|
-
|
|
13
|
+
* A light tint with a darker rule above and below, edge to edge of its card or screen — a band, never a card in a card.
|
|
14
|
+
* A native details, controlled like an input: `open` + `onOpenChange`, or `defaultOpen`. */
|
|
15
|
+
export declare function Banner({ summary, open, defaultOpen, onOpenChange, className, children, ...rest }: BannerProps): import("react").JSX.Element;
|
package/dist/layover/Banner.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
import { Icon } from '../type/Icon.js';
|
|
4
|
+
import { useDisclosure } from '../state.js';
|
|
4
5
|
/** Banner — ONE band: the line that opens it and what it opens share the same ground, so they read as one piece (Lh 2026-09-24).
|
|
5
|
-
* A light tint with a darker rule above and below, edge to edge of its card or screen — a band, never a card in a card.
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
* A light tint with a darker rule above and below, edge to edge of its card or screen — a band, never a card in a card.
|
|
7
|
+
* A native details, controlled like an input: `open` + `onOpenChange`, or `defaultOpen`. */
|
|
8
|
+
export function Banner({ summary, open, defaultOpen, onOpenChange, className, children, ...rest }) {
|
|
9
|
+
const disclosure = useDisclosure(open, defaultOpen, onOpenChange);
|
|
10
|
+
return (_jsxs("details", { className: cx('banner', className), ...rest, ...disclosure, children: [_jsxs("summary", { className: "banner__summary", children: [summary, _jsx(Icon, { name: "chevron-down", size: "xs" })] }), _jsx("div", { className: "banner__body", children: children })] }));
|
|
8
11
|
}
|
package/dist/lists/Row.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cloneElement, createElement, isValidElement, useId } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { Icon } from '../type/Icon.js';
|
|
5
|
+
import { useDisclosure } from '../state.js';
|
|
5
6
|
/**
|
|
6
7
|
* Row — a frameless row inside a card: lead · title + meta · end. 48px+ tall, a tap target.
|
|
7
8
|
* Several go in Rows (hairlines between). A row that IS a card is `Card row`.
|
|
@@ -10,13 +11,12 @@ import { Icon } from '../type/Icon.js';
|
|
|
10
11
|
* An `end` chevron turns when it is open — the app fetches whatever `more` lists; the row only shows it.
|
|
11
12
|
*/
|
|
12
13
|
export function Row({ lead, leadIcon, title, meta, body, end, endValue, endCaption, lg, done, as, href, more, open, defaultOpen, onOpenChange, className, children, ...rest }) {
|
|
14
|
+
const disclosure = useDisclosure(open, defaultOpen, onOpenChange);
|
|
13
15
|
const el = more !== undefined ? 'summary' : as ?? (href ? 'a' : 'div');
|
|
14
16
|
const row = createElement(el, { href, className: cx('row', lg && 'row--lg', done && 'row--done', className), 'aria-disabled': done || undefined, ...(more === undefined ? rest : {}) }, leadIcon ? _jsx("span", { className: "row__lead", children: _jsx(Icon, { name: leadIcon }) }) : lead, (title != null || meta != null || body != null) && (_jsxs("div", { className: "row__body", children: [title != null && _jsx("span", { className: "row__title", children: title }), meta != null && _jsx("span", { className: "row__meta", children: meta }), body] })), children, (endValue != null || endCaption != null) && (_jsxs("span", { className: "row__end", children: [endValue != null && _jsx("span", { className: "t-title", children: endValue }), endCaption != null && _jsx("span", { className: "t-caption t-muted", children: endCaption })] })), end, done && _jsx("span", { className: "row__done", children: _jsx(Icon, { name: "check", size: "sm" }) }));
|
|
15
17
|
if (more === undefined)
|
|
16
18
|
return row;
|
|
17
|
-
return (_jsxs("details", { className: "row-expand",
|
|
18
|
-
e.currentTarget.open = open; if (is !== (open ?? !is))
|
|
19
|
-
onOpenChange?.(is); }, children: [row, _jsx("div", { className: "row-expand__more", children: more })] }));
|
|
19
|
+
return (_jsxs("details", { className: "row-expand", ...rest, ...disclosure, children: [row, _jsx("div", { className: "row-expand__more", children: more })] }));
|
|
20
20
|
}
|
|
21
21
|
/** Rows — a list of Rows separated by hairlines; `heading` names the group (a `role="group"` labelled by it). */
|
|
22
22
|
export function Rows({ heading, className, children, ...rest }) {
|
package/dist/media/Postcard.js
CHANGED
|
@@ -9,8 +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 [back,
|
|
13
|
-
const turn = () =>
|
|
12
|
+
const [back, flip] = useControlled(flipped, defaultFlipped, onFlip);
|
|
13
|
+
const turn = () => flip(!back);
|
|
14
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 })] }));
|
|
15
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 })] })] }));
|
|
16
16
|
}
|
package/dist/numbers/Band.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import type { Tone } from '../cards/Card.js';
|
|
2
3
|
export interface BandProps extends ComponentProps<'article'> {
|
|
3
4
|
/** the two-line label (use a `<br />` to break it) */
|
|
4
5
|
label: ReactNode;
|
|
@@ -6,7 +7,8 @@ export interface BandProps extends ComponentProps<'article'> {
|
|
|
6
7
|
num: ReactNode;
|
|
7
8
|
/** the footnote under the label */
|
|
8
9
|
foot?: ReactNode;
|
|
9
|
-
|
|
10
|
+
/** the paper — see `Tone` */
|
|
11
|
+
tone?: Tone;
|
|
10
12
|
}
|
|
11
13
|
/** Band — a data card: 2-line label · giant number · footnote. Stack several in a BandStack for one boundary with hairlines. */
|
|
12
14
|
export declare function Band({ label, num, foot, tone, className, ...rest }: BandProps): import("react").JSX.Element;
|
package/dist/numbers/Picker.js
CHANGED
|
@@ -10,16 +10,10 @@ const PickerContext = createContext(null);
|
|
|
10
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.
|
|
11
11
|
*/
|
|
12
12
|
export function Picker({ value, defaultValue, onChange, unit, label, className, children, ...rest }) {
|
|
13
|
-
const [current,
|
|
13
|
+
const [current, pick] = useControlled(value, defaultValue, onChange);
|
|
14
14
|
const rail = useRef(null);
|
|
15
15
|
const mounted = useRef(false);
|
|
16
16
|
const settle = useRef(null);
|
|
17
|
-
const pick = (next) => {
|
|
18
|
-
if (next === current)
|
|
19
|
-
return;
|
|
20
|
-
setOwn(next);
|
|
21
|
-
onChange?.(next);
|
|
22
|
-
};
|
|
23
17
|
const items = () => [...(rail.current?.querySelectorAll(':scope > .picker__item') ?? [])];
|
|
24
18
|
const column = () => (rail.current ? rail.current.clientWidth / 3 : 0); // the rail is three columns wide; item i stands in the centre at i × one column
|
|
25
19
|
/* the chosen value stands in the centre — at once on arrival, sliding after */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentProps } from 'react';
|
|
2
2
|
export interface ActionBarProps extends ComponentProps<'div'> {
|
|
3
|
-
/** `pager`: previous / next as two equal buttons at the edges (a Link may sit between them
|
|
3
|
+
/** `pager`: previous / next as two equal buttons at the edges (a Link may sit between them) · `tiers`: rows stacked — a control tier above the buttons */
|
|
4
4
|
variant?: 'pager' | 'tiers';
|
|
5
5
|
/** a bare bar for a preview: no position, full width, no side padding (a real bar is a Screen's `bottom`) */
|
|
6
6
|
inline?: boolean;
|
|
@@ -8,8 +8,8 @@ export interface ActionBarProps extends ComponentProps<'div'> {
|
|
|
8
8
|
/**
|
|
9
9
|
* ActionBar — the screen's ONE move: the Screen's `bottom` (`<Screen fill bottom={<ActionBar…>}>`), the last part of its flex
|
|
10
10
|
* column, as tall as its rows. Never after `</Screen>`, never positioned by hand.
|
|
11
|
-
* Plain: one wide primary Btn (the step that commits).
|
|
12
|
-
*
|
|
11
|
+
* Plain: one wide primary Btn (the step that commits).
|
|
12
|
+
* `pager`: Back + Next as peers — buttons only. The PagerAt dots may be the bar's first row (a Walkthrough).
|
|
13
13
|
* `tiers`: an ActionBarTier with a control (a Slider) above the button tier.
|
|
14
14
|
* In a `Screen fill` it is a layer over the content's foot; on a scrolling Screen a sticky last row.
|
|
15
15
|
*/
|
|
@@ -3,8 +3,8 @@ import { cx } from '../cx.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* ActionBar — the screen's ONE move: the Screen's `bottom` (`<Screen fill bottom={<ActionBar…>}>`), the last part of its flex
|
|
5
5
|
* column, as tall as its rows. Never after `</Screen>`, never positioned by hand.
|
|
6
|
-
* Plain: one wide primary Btn (the step that commits).
|
|
7
|
-
*
|
|
6
|
+
* Plain: one wide primary Btn (the step that commits).
|
|
7
|
+
* `pager`: Back + Next as peers — buttons only. The PagerAt dots may be the bar's first row (a Walkthrough).
|
|
8
8
|
* `tiers`: an ActionBarTier with a control (a Slider) above the button tier.
|
|
9
9
|
* In a `Screen fill` it is a layer over the content's foot; on a scrolling Screen a sticky last row.
|
|
10
10
|
*/
|
|
@@ -8,7 +8,7 @@ export interface CentreProps extends ComponentProps<'div'> {
|
|
|
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
|
|
10
10
|
* and scrolling inside, for a screen that must never scroll. Its children stack at the screen's rhythm.
|
|
11
|
-
* Put it after the TopBar; the ActionBar, if any,
|
|
11
|
+
* Put it after the TopBar; the ActionBar, if any, is the Screen's `bottom`.
|
|
12
12
|
*/
|
|
13
13
|
export declare function Centre({ className, ...rest }: CentreProps): import("react").JSX.Element;
|
|
14
14
|
export interface PagesProps extends ComponentProps<'div'> {
|
package/dist/scaffold/Centre.js
CHANGED
|
@@ -8,7 +8,7 @@ import { cx } from '../cx.js';
|
|
|
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
|
|
10
10
|
* and scrolling inside, for a screen that must never scroll. Its children stack at the screen's rhythm.
|
|
11
|
-
* Put it after the TopBar; the ActionBar, if any,
|
|
11
|
+
* Put it after the TopBar; the ActionBar, if any, is the Screen's `bottom`.
|
|
12
12
|
*/
|
|
13
13
|
export function Centre({ className, ...rest }) {
|
|
14
14
|
return _jsx("div", { className: cx('screen__centre', className), ...rest });
|
|
@@ -7,5 +7,5 @@ export interface PagerAtProps extends Omit<ComponentProps<'span'>, 'children'> {
|
|
|
7
7
|
/** accessible name; defaults to "Page {at} of {of}" */
|
|
8
8
|
label?: string;
|
|
9
9
|
}
|
|
10
|
-
/** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside an ActionBar,
|
|
10
|
+
/** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside a page; in a Walkthrough it is the bottom layer's first row (an ActionBar child, centred). */
|
|
11
11
|
export declare function PagerAt({ at, of, label, className, ...rest }: PagerAtProps): import("react").JSX.Element;
|
package/dist/scaffold/PagerAt.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
-
/** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside an ActionBar,
|
|
3
|
+
/** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside a page; in a Walkthrough it is the bottom layer's first row (an ActionBar child, centred). */
|
|
4
4
|
export function PagerAt({ at, of, label, className, ...rest }) {
|
|
5
5
|
return (_jsx("span", { className: cx('pager__at', className), role: "img", "aria-label": label ?? `Page ${at} of ${of}`, ...rest, children: Array.from({ length: of }, (_, i) => _jsx("i", { className: i + 1 === at ? 'is-at' : undefined }, i)) }));
|
|
6
6
|
}
|
package/dist/scaffold/Screen.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createElement, useEffect, useRef } from 'react';
|
|
2
|
-
import { warnOnce } from '../state.js';
|
|
2
|
+
import { warnOnce, watchSize } from '../state.js';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
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
|
|
@@ -24,9 +24,7 @@ export function Screen({ fill, palette, as = 'div', top, bottom, className, chil
|
|
|
24
24
|
el?.style.removeProperty('--bottom-h');
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
ro.observe(bar);
|
|
29
|
-
return () => ro.disconnect();
|
|
27
|
+
return watchSize([bar], () => el.style.setProperty('--bottom-h', `${bar.offsetHeight}px`));
|
|
30
28
|
}, [bottom]);
|
|
31
29
|
const setRef = (node) => {
|
|
32
30
|
own.current = node;
|
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,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, 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
|
const ORDER = ['peek', 'half', '3q', 'full']; // low → high; `away` is below them all and outside them
|
|
6
6
|
const TAP_REM = 0.5; // = --sp-2: a move shorter than this is a tap, not a drag
|
|
@@ -17,7 +17,7 @@ const TAP_REM = 0.5; // = --sp-2: a move shorter than this is a tap, not a drag
|
|
|
17
17
|
* climbs until the focused control has room above the foot.
|
|
18
18
|
*/
|
|
19
19
|
export function Sheet({ state, defaultState = 'half', onStateChange, states, handle = true, className, children, onFocus, ...rest }) {
|
|
20
|
-
const [current,
|
|
20
|
+
const [current, request] = useControlled(state, defaultState, onStateChange);
|
|
21
21
|
const ref = useRef(null);
|
|
22
22
|
const handleRef = useRef(null);
|
|
23
23
|
const home = useRef(current !== 'peek' && current !== 'away' ? current : 'half'); // where a tap from peek returns to
|
|
@@ -28,12 +28,6 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
28
28
|
const list = (typeof states === 'string' ? states.split(/\s+/) : [...(states ?? [])]).filter((s) => ORDER.includes(s));
|
|
29
29
|
return list.length ? ORDER.filter((s) => list.includes(s)) : [...ORDER];
|
|
30
30
|
}, [states]);
|
|
31
|
-
const request = useCallback((next, reason) => {
|
|
32
|
-
if (next === current)
|
|
33
|
-
return;
|
|
34
|
-
setOwn(next);
|
|
35
|
-
onStateChange?.(next, reason);
|
|
36
|
-
}, [current, setOwn, onStateChange]);
|
|
37
31
|
useEffect(() => {
|
|
38
32
|
if (current !== 'peek' && current !== 'away')
|
|
39
33
|
home.current = current;
|
|
@@ -103,10 +97,10 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
103
97
|
in this sheet has the focus, make sure it still shows */
|
|
104
98
|
useEffect(() => {
|
|
105
99
|
const el = ref.current;
|
|
106
|
-
if (!el
|
|
100
|
+
if (!el)
|
|
107
101
|
return;
|
|
108
|
-
let first = true;
|
|
109
|
-
|
|
102
|
+
let first = true; // the browser's report at the start is not a resize
|
|
103
|
+
return watchSize([el, el.parentElement], () => {
|
|
110
104
|
if (first) {
|
|
111
105
|
first = false;
|
|
112
106
|
return;
|
|
@@ -115,10 +109,6 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
115
109
|
if (at && el.contains(at))
|
|
116
110
|
reveal(at, 'keyboard');
|
|
117
111
|
});
|
|
118
|
-
ro.observe(el);
|
|
119
|
-
if (el.parentElement)
|
|
120
|
-
ro.observe(el.parentElement);
|
|
121
|
-
return () => ro.disconnect();
|
|
122
112
|
}, [reveal]);
|
|
123
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
|
|
124
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
|
|
@@ -126,7 +116,7 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
126
116
|
22-base type scale). Removed when the head fits the floor again, so the CSS number is the one in force. */
|
|
127
117
|
useEffect(() => {
|
|
128
118
|
const el = ref.current, box = el?.parentElement, head = el?.querySelector(':scope > .card__head, :scope > .sheet__head');
|
|
129
|
-
if (!el || !box || !head
|
|
119
|
+
if (!el || !box || !head)
|
|
130
120
|
return;
|
|
131
121
|
const publish = () => {
|
|
132
122
|
box.style.removeProperty('--sheet-peek'); // read the CSS strip, not our own
|
|
@@ -136,9 +126,8 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
136
126
|
if (over > 0.5)
|
|
137
127
|
box.style.setProperty('--sheet-peek', `${strip + over}px`);
|
|
138
128
|
};
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
return () => { ro.disconnect(); box.style.removeProperty('--sheet-peek'); };
|
|
129
|
+
const stop = watchSize([head], publish);
|
|
130
|
+
return () => { stop(); box.style.removeProperty('--sheet-peek'); };
|
|
142
131
|
}, []);
|
|
143
132
|
const focus = (e) => {
|
|
144
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;
|
|
@@ -14,7 +14,7 @@ const sheetsOf = (stack) => [...stack.children]
|
|
|
14
14
|
* Children are Cards, direct children only, at most five: first = the back of the stack, last = the front (open) sheet.
|
|
15
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
|
|
16
16
|
* with CHILDREN, in this order: `<StackBack />` (tap mode), `<Text level="h2" caps>Shopping</Text>`, an `<IconRow>` of Icons —
|
|
17
|
-
* 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
|
|
18
18
|
* off the bottom. `closed`: nothing open on arrival — every head peeks at the bottom and the base above shows `base` (a home screen
|
|
19
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`.
|
|
20
20
|
*
|
|
@@ -23,23 +23,24 @@ const sheetsOf = (stack) => [...stack.children]
|
|
|
23
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
|
|
24
24
|
* before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); it sets classes, never a size.
|
|
25
25
|
*/
|
|
26
|
-
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, fanned, closed, base, className, children, onClick, ...rest }) {
|
|
27
|
-
const [current,
|
|
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
|
-
setOwn(next);
|
|
35
|
-
onOpenChange?.(next);
|
|
36
|
-
};
|
|
37
31
|
/* paint the state onto the sheets — after every render, so a re-rendered Card never loses its place */
|
|
38
32
|
useLayoutEffect(() => {
|
|
39
33
|
const stack = ref.current;
|
|
40
34
|
if (!stack || mode !== 'tap')
|
|
41
35
|
return;
|
|
42
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
|
+
});
|
|
43
44
|
const prev = was.current;
|
|
44
45
|
was.current = current;
|
|
45
46
|
if (current != null) {
|
|
@@ -86,5 +87,12 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
86
87
|
if (at >= 0 && current == null)
|
|
87
88
|
request(at);
|
|
88
89
|
};
|
|
89
|
-
|
|
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 })] }));
|
|
90
98
|
}
|