@usableapp/cardds 0.2.2 → 0.3.0
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/README.md +26 -102
- package/cardds.js +6 -449
- package/css/actions.css +3 -8
- package/css/card.css +2 -2
- package/css/sheet.css +8 -8
- package/css/step.css +6 -6
- package/css/tokens.css +2 -0
- package/dist/actions/Dropdown.d.ts +34 -17
- package/dist/actions/Dropdown.js +41 -11
- package/dist/actions/IconBtn.d.ts +296 -9
- package/dist/actions/IconBtn.js +6 -8
- package/dist/cardds.css +21 -24
- package/dist/choice/Calendar.d.ts +17 -6
- package/dist/choice/Calendar.js +9 -3
- package/dist/choice/Slider.d.ts +1 -1
- package/dist/choice/Slider.js +12 -8
- package/dist/forms/Pin.d.ts +9 -6
- package/dist/forms/Pin.js +47 -18
- package/dist/media/Postcard.d.ts +8 -4
- package/dist/media/Postcard.js +8 -3
- package/dist/numbers/Picker.d.ts +19 -7
- package/dist/numbers/Picker.js +51 -13
- package/dist/scaffold/BaseContent.d.ts +4 -4
- package/dist/scaffold/BaseContent.js +4 -4
- package/dist/sheets/Drawer.d.ts +14 -3
- package/dist/sheets/Drawer.js +29 -5
- package/dist/sheets/Sheet.d.ts +28 -15
- package/dist/sheets/Sheet.js +157 -11
- package/dist/sheets/SheetStack.d.ts +13 -1
- package/dist/sheets/SheetStack.js +74 -2
- package/package.json +1 -1
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export interface CalendarProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
+
export interface CalendarProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title' | 'onSelect'> {
|
|
3
3
|
/** the month shown ("June") */
|
|
4
4
|
title: ReactNode;
|
|
5
5
|
/** days in the month */
|
|
6
6
|
days?: number;
|
|
7
7
|
/** weekday column of the 1st (1 = first column) */
|
|
8
8
|
start?: number;
|
|
9
|
-
/** the
|
|
10
|
-
selected?: number;
|
|
9
|
+
/** CONTROLLED: the picked day, or `null` for none. The app owns it — pair it with `onSelect`. (`undefined` = uncontrolled.) */
|
|
10
|
+
selected?: number | null;
|
|
11
|
+
/** UNCONTROLLED: the day picked on arrival; the calendar then keeps its own */
|
|
12
|
+
defaultSelected?: number;
|
|
13
|
+
/** a day was tapped */
|
|
14
|
+
onSelect?: (day: number) => void;
|
|
15
|
+
/** the head's two buttons: the app turns the month (it owns `title`, `days`, `start`) */
|
|
16
|
+
onPrev?: () => void;
|
|
17
|
+
onNext?: () => void;
|
|
18
|
+
/** their accessible names */
|
|
19
|
+
prevLabel?: string;
|
|
20
|
+
nextLabel?: string;
|
|
11
21
|
/** softly marked days */
|
|
12
22
|
marks?: number[];
|
|
13
23
|
/** strongly marked days */
|
|
@@ -19,5 +29,6 @@ export interface CalendarProps extends Omit<ComponentPropsWithoutRef<'div'>, 'ti
|
|
|
19
29
|
/** replace the default prev/next icon buttons */
|
|
20
30
|
nav?: ReactNode;
|
|
21
31
|
}
|
|
22
|
-
/** Calendar — a month grid of 44px round cells: one accent day, optional marks; prev/next in the head.
|
|
23
|
-
|
|
32
|
+
/** 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:
|
|
33
|
+
* the app owns which month (`title`, `days`, `start` + `onPrev` / `onNext`) and which day (`selected` + `onSelect`, or `defaultSelected`). */
|
|
34
|
+
export declare function Calendar({ title, days, start, selected, defaultSelected, onSelect, onPrev, onNext, prevLabel, nextLabel, marks, marks2, disabled, weekdays, nav, className, ...rest }: CalendarProps): import("react").JSX.Element;
|
package/dist/choice/Calendar.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { cx } from '../cx.js';
|
|
3
4
|
import { IconBtn } from '../actions/IconBtn.js';
|
|
4
|
-
/** Calendar — a month grid of 44px round cells: one accent day, optional marks; prev/next in the head.
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
* the app owns which month (`title`, `days`, `start` + `onPrev` / `onNext`) and which day (`selected` + `onSelect`, or `defaultSelected`). */
|
|
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 [inner, setInner] = useState(defaultSelected);
|
|
9
|
+
const day = selected !== undefined ? selected : inner;
|
|
10
|
+
const pick = (d) => { if (selected === undefined)
|
|
11
|
+
setInner(d); onSelect?.(d); };
|
|
12
|
+
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)))] })] }));
|
|
7
13
|
}
|
package/dist/choice/Slider.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export interface SliderProps extends Omit<ComponentPropsWithoutRef<'input'>, 'cl
|
|
|
8
8
|
style?: React.CSSProperties;
|
|
9
9
|
}
|
|
10
10
|
/** Slider — one value along a hairline rail with a 48px knob; the whole range is thumb-tall. */
|
|
11
|
-
export declare function Slider({ label, glyph, className, style, ...input }: SliderProps): import("react").JSX.Element;
|
|
11
|
+
export declare function Slider({ label, glyph, className, style, onInput, ...input }: SliderProps): import("react").JSX.Element;
|
package/dist/choice/Slider.js
CHANGED
|
@@ -2,13 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
/** Slider — one value along a hairline rail with a 48px knob; the whole range is thumb-tall. */
|
|
5
|
-
export function Slider({ label, glyph, className, style, ...input }) {
|
|
5
|
+
export function Slider({ label, glyph, className, style, onInput, ...input }) {
|
|
6
6
|
const range = useRef(null);
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
const box = useRef(null);
|
|
8
|
+
// the knob's place along the rail, 0..1, published on the slider as --slider-at so the glyph can ride the knob (choice.css)
|
|
9
|
+
const place = () => {
|
|
10
|
+
const r = range.current;
|
|
11
|
+
if (!r || !box.current)
|
|
12
|
+
return;
|
|
13
|
+
const min = Number(r.min) || 0, max = Number(r.max) || 100;
|
|
14
|
+
box.current.style.setProperty('--slider-at', String(max > min ? Math.min(1, Math.max(0, (Number(r.value) - min) / (max - min))) : 0));
|
|
15
|
+
};
|
|
16
|
+
useEffect(place); // after every render: a controlled value, a new min / max
|
|
17
|
+
return (_jsxs("label", { ref: box, className: cx('slider', className), style: style, children: [label != null && _jsx("span", { className: "slider__label", children: label }), glyph != null && _jsx("span", { className: "slider__glyph", "aria-hidden": "true", children: glyph }), _jsx("input", { ref: range, className: "slider__range", type: "range", onInput: (e) => { place(); onInput?.(e); }, ...input })] }));
|
|
14
18
|
}
|
package/dist/forms/Pin.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef } from 'react';
|
|
2
|
-
export interface PinProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange'> {
|
|
2
|
+
export interface PinProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange' | 'defaultValue'> {
|
|
3
3
|
/** how many digits (4 or 6) */
|
|
4
4
|
length?: number;
|
|
5
|
-
/** digits
|
|
5
|
+
/** CONTROLLED: the digits so far, left to right ("03"). The app owns it — pair it with `onChange`. */
|
|
6
6
|
value?: string;
|
|
7
|
+
/** UNCONTROLLED: the digits it starts with; the Pin then keeps its own */
|
|
8
|
+
defaultValue?: string;
|
|
7
9
|
/** accessible name prefix for each cell ("Digit" → "Digit 1") */
|
|
8
10
|
label?: string;
|
|
9
|
-
/** the code so far, after every
|
|
11
|
+
/** the code so far, after every change; `complete` once every cell holds a digit */
|
|
10
12
|
onChange?: (code: string, complete: boolean) => void;
|
|
11
13
|
/** focus the first empty cell on mount */
|
|
12
14
|
autoFocus?: boolean;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Pin — big code entry, one digit per pill; a filled cell turns accent. Lives in a Sheet.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* Its own behaviour, inside it: typing a digit moves to the next cell, Backspace in an empty cell moves back and clears, the
|
|
19
|
+
* arrows walk the cells, a pasted code fills them from the one it landed in; only digits stay. The first cell takes the SMS
|
|
20
|
+
* one-time-code autofill. The code is a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`.
|
|
18
21
|
*/
|
|
19
|
-
export declare function Pin({ length, value, label, onChange, autoFocus, className, ...rest }: PinProps): import("react").JSX.Element;
|
|
22
|
+
export declare function Pin({ length, value, defaultValue, label, onChange, autoFocus, className, ...rest }: PinProps): import("react").JSX.Element;
|
package/dist/forms/Pin.js
CHANGED
|
@@ -1,27 +1,56 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
/**
|
|
5
5
|
* Pin — big code entry, one digit per pill; a filled cell turns accent. Lives in a Sheet.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Its own behaviour, inside it: typing a digit moves to the next cell, Backspace in an empty cell moves back and clears, the
|
|
7
|
+
* arrows walk the cells, a pasted code fills them from the one it landed in; only digits stay. The first cell takes the SMS
|
|
8
|
+
* one-time-code autofill. The code is a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`.
|
|
8
9
|
*/
|
|
9
|
-
export function Pin({ length = 6, value = '', label = 'Digit', onChange, autoFocus, className, ...rest }) {
|
|
10
|
+
export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onChange, autoFocus, className, ...rest }) {
|
|
10
11
|
const ref = useRef(null);
|
|
12
|
+
const [inner, setInner] = useState(defaultValue);
|
|
13
|
+
const code = (value ?? inner).replace(/\D/g, '').slice(0, length);
|
|
14
|
+
const cell = (i) => ref.current?.querySelectorAll('.pin__cell')[i];
|
|
15
|
+
const set = (next) => {
|
|
16
|
+
next = next.slice(0, length);
|
|
17
|
+
if (value === undefined)
|
|
18
|
+
setInner(next);
|
|
19
|
+
if (next !== code)
|
|
20
|
+
onChange?.(next, next.length === length);
|
|
21
|
+
};
|
|
11
22
|
useEffect(() => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const on = (e) => { const d = e.detail; onChange(d.code, d.complete); };
|
|
16
|
-
el.addEventListener('cardds:pin', on);
|
|
17
|
-
return () => el.removeEventListener('cardds:pin', on);
|
|
18
|
-
}, [onChange]);
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (!autoFocus)
|
|
21
|
-
return;
|
|
22
|
-
const cells = ref.current?.querySelectorAll('.pin__cell');
|
|
23
|
-
if (cells)
|
|
24
|
-
([...cells].find((c) => !c.value) ?? cells[cells.length - 1])?.focus();
|
|
23
|
+
if (autoFocus)
|
|
24
|
+
cell(Math.min(code.length, length - 1))?.focus();
|
|
25
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- on mount only
|
|
25
26
|
}, [autoFocus]);
|
|
26
|
-
|
|
27
|
+
const input = (i) => (e) => {
|
|
28
|
+
const digits = e.target.value.replace(/\D/g, '');
|
|
29
|
+
const at = Math.min(i, code.length); // a code has no gaps: a digit typed further along lands in the first empty cell
|
|
30
|
+
if (!digits) {
|
|
31
|
+
set(code.slice(0, at) + code.slice(at + 1));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
set(code.slice(0, at) + digits + code.slice(at + digits.length)); // a paste (or a typed run) spills over the cells that follow
|
|
35
|
+
cell(Math.min(at + digits.length, length - 1))?.focus();
|
|
36
|
+
};
|
|
37
|
+
const key = (i) => (e) => {
|
|
38
|
+
if (e.key === 'Backspace' && !e.currentTarget.value && i > 0) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
set(code.slice(0, i - 1) + code.slice(i));
|
|
41
|
+
cell(i - 1)?.focus();
|
|
42
|
+
}
|
|
43
|
+
else if (e.key === 'ArrowLeft' && i > 0) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
cell(i - 1)?.focus();
|
|
46
|
+
}
|
|
47
|
+
else if (e.key === 'ArrowRight' && i < length - 1) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
cell(i + 1)?.focus();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
return (_jsx("div", { ref: ref, className: cx('pin', className), ...rest, children: Array.from({ length }, (_, i) => (
|
|
53
|
+
// no maxLength: the browser would cut a pasted code before this ever saw it. A cell holds one digit — typing into a full one
|
|
54
|
+
// replaces it, so its digit is selected whenever the cell takes the focus
|
|
55
|
+
_jsx("input", { className: "pin__cell", inputMode: "numeric", pattern: "[0-9]*", placeholder: " ", value: code[i] ?? '', "aria-label": `${label} ${i + 1}`, autoComplete: i === 0 ? 'one-time-code' : 'off', onChange: input(i), onKeyDown: key(i), onFocus: (e) => e.currentTarget.select() }, i))) }));
|
|
27
56
|
}
|
package/dist/media/Postcard.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
export interface PostcardProps extends ComponentPropsWithoutRef<'div'> {
|
|
3
3
|
/** the photo on the front; none = the wireframe box */
|
|
4
4
|
photo?: string | null;
|
|
@@ -7,14 +7,18 @@ export interface PostcardProps extends ComponentPropsWithoutRef<'div'> {
|
|
|
7
7
|
name: ReactNode;
|
|
8
8
|
/** the words on the back */
|
|
9
9
|
caption?: ReactNode;
|
|
10
|
-
/**
|
|
10
|
+
/** CONTROLLED: the back is showing. The app owns it — pair it with `onFlip`. */
|
|
11
11
|
flipped?: boolean;
|
|
12
|
+
/** UNCONTROLLED: start turned over; the card then keeps its own side */
|
|
13
|
+
defaultFlipped?: boolean;
|
|
14
|
+
/** a face was tapped: the side it asks to show */
|
|
15
|
+
onFlip?: (flipped: boolean) => void;
|
|
12
16
|
frontLabel?: string;
|
|
13
17
|
backLabel?: string;
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
20
|
* Postcard — a member's introduction as a card in the hand: the photo (or the wireframe box) with the name in front,
|
|
17
|
-
* the caption on the back; a tap on either face turns it over (
|
|
21
|
+
* the caption on the back; a tap on either face turns it over (`--motion-flip`). Which side shows is a state: `flipped` + `onFlip`, or `defaultFlipped`.
|
|
18
22
|
* A voice note, a like, a reply go OUTSIDE it, in a Row under it — a face is a button and holds no button.
|
|
19
23
|
*/
|
|
20
|
-
export declare function Postcard({ photo, alt, name, caption, flipped, frontLabel, backLabel, className, ...rest }: PostcardProps): import("react").JSX.Element;
|
|
24
|
+
export declare function Postcard({ photo, alt, name, caption, flipped, defaultFlipped, onFlip, frontLabel, backLabel, className, ...rest }: PostcardProps): import("react").JSX.Element;
|
package/dist/media/Postcard.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { cx } from '../cx.js';
|
|
3
4
|
/**
|
|
4
5
|
* Postcard — a member's introduction as a card in the hand: the photo (or the wireframe box) with the name in front,
|
|
5
|
-
* the caption on the back; a tap on either face turns it over (
|
|
6
|
+
* the caption on the back; a tap on either face turns it over (`--motion-flip`). Which side shows is a state: `flipped` + `onFlip`, or `defaultFlipped`.
|
|
6
7
|
* A voice note, a like, a reply go OUTSIDE it, in a Row under it — a face is a button and holds no button.
|
|
7
8
|
*/
|
|
8
|
-
export function Postcard({ photo, alt = '', name, caption, flipped, frontLabel = 'พลิกดูด้านหลัง', backLabel = 'พลิกดูด้านหน้า', className, ...rest }) {
|
|
9
|
-
|
|
9
|
+
export function Postcard({ photo, alt = '', name, caption, flipped, defaultFlipped = false, onFlip, frontLabel = 'พลิกดูด้านหลัง', backLabel = 'พลิกดูด้านหน้า', className, ...rest }) {
|
|
10
|
+
const [inner, setInner] = useState(defaultFlipped);
|
|
11
|
+
const back = flipped ?? inner;
|
|
12
|
+
const turn = () => { if (flipped === undefined)
|
|
13
|
+
setInner(!back); onFlip?.(!back); };
|
|
14
|
+
return (_jsxs("div", { className: cx('postcard', back && 'is-flipped', className), ...rest, children: [_jsxs("button", { className: "postcard__face postcard__front", 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" }), _jsx("span", { className: "postcard__label", children: name })] }), _jsxs("button", { className: "postcard__face postcard__back", type: "button", "aria-label": backLabel, onClick: turn, children: [caption != null && _jsx("p", { className: "postcard__caption", children: caption }), _jsx("span", { className: "postcard__label", children: name })] })] }));
|
|
10
15
|
}
|
package/dist/numbers/Picker.d.ts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
-
export interface PickerProps extends ComponentPropsWithoutRef<'div'> {
|
|
2
|
+
export interface PickerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange' | 'defaultValue'> {
|
|
3
|
+
/** CONTROLLED: the chosen PickerItem's `value`. The app owns it — pair it with `onChange`. */
|
|
4
|
+
value?: string;
|
|
5
|
+
/** UNCONTROLLED: the value it starts on; the picker then keeps its own */
|
|
6
|
+
defaultValue?: string;
|
|
7
|
+
/** the member settled on a value — scrolled the rail to it, or tapped it */
|
|
8
|
+
onChange?: (value: string) => void;
|
|
3
9
|
/** the unit under the rail ("hours") */
|
|
4
10
|
unit?: ReactNode;
|
|
5
11
|
label?: string;
|
|
6
12
|
}
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Picker — a horizontal snap picker, the centre value big between two accent rules; children are PickerItems.
|
|
15
|
+
* It holds a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`. The value IS the item standing in the centre
|
|
16
|
+
* of the rail: the picker scrolls the chosen one there, and when the member scrolls (or taps an item) it asks for the one that
|
|
17
|
+
* settled in the centre. The CSS lights the centred item by scroll position; the app never reads the DOM to know what was picked.
|
|
18
|
+
*/
|
|
19
|
+
export declare function Picker({ value, defaultValue, onChange, unit, label, className, children, ...rest }: PickerProps): import("react").JSX.Element;
|
|
20
|
+
export interface PickerItemProps extends Omit<ComponentPropsWithoutRef<'button'>, 'value'> {
|
|
21
|
+
/** what choosing this item reports; default = its own text */
|
|
22
|
+
value?: string;
|
|
11
23
|
}
|
|
12
|
-
/** PickerItem — one value on the Picker rail. */
|
|
13
|
-
export declare function PickerItem({
|
|
24
|
+
/** PickerItem — one value on the Picker rail; a tap brings it to the centre. */
|
|
25
|
+
export declare function PickerItem({ value, className, children, onClick, ...rest }: PickerItemProps): import("react").JSX.Element;
|
package/dist/numbers/Picker.js
CHANGED
|
@@ -1,18 +1,56 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const PickerContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* Picker — a horizontal snap picker, the centre value big between two accent rules; children are PickerItems.
|
|
7
|
+
* It holds a VALUE, controlled like an input: `value` + `onChange`, or `defaultValue`. The value IS the item standing in the centre
|
|
8
|
+
* of the rail: the picker scrolls the chosen one there, and when the member scrolls (or taps an item) it asks for the one that
|
|
9
|
+
* 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
|
+
export function Picker({ value, defaultValue, onChange, unit, label, className, children, ...rest }) {
|
|
12
|
+
const [inner, setInner] = useState(defaultValue);
|
|
13
|
+
const current = value ?? inner;
|
|
6
14
|
const rail = useRef(null);
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
const mounted = useRef(false);
|
|
16
|
+
const settle = useRef(null);
|
|
17
|
+
const pick = (next) => {
|
|
18
|
+
if (next === current)
|
|
19
|
+
return;
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
setInner(next);
|
|
22
|
+
onChange?.(next);
|
|
23
|
+
};
|
|
24
|
+
const items = () => [...(rail.current?.querySelectorAll(':scope > .picker__item') ?? [])];
|
|
25
|
+
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
|
|
26
|
+
/* the chosen value stands in the centre — at once on arrival, sliding after */
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
const r = rail.current, i = items().findIndex((el) => el.dataset.value === current);
|
|
29
|
+
if (r && i >= 0 && Math.abs(r.scrollLeft - i * column()) > 1)
|
|
30
|
+
r.scrollTo({ left: i * column(), behavior: mounted.current ? 'smooth' : 'auto' });
|
|
31
|
+
mounted.current = true;
|
|
32
|
+
});
|
|
33
|
+
useEffect(() => () => { if (settle.current)
|
|
34
|
+
clearTimeout(settle.current); }, []);
|
|
35
|
+
/* the rail came to rest: whatever stands in the centre is what the member chose */
|
|
36
|
+
const scrolled = () => {
|
|
37
|
+
if (settle.current)
|
|
38
|
+
clearTimeout(settle.current);
|
|
39
|
+
settle.current = setTimeout(() => {
|
|
40
|
+
const r = rail.current, col = column();
|
|
41
|
+
if (!r || !col)
|
|
42
|
+
return;
|
|
43
|
+
const at = items()[Math.round(r.scrollLeft / col)]?.dataset.value;
|
|
44
|
+
if (at !== undefined)
|
|
45
|
+
pick(at);
|
|
46
|
+
}, 120);
|
|
47
|
+
};
|
|
48
|
+
return (_jsx(PickerContext.Provider, { value: { value: current, pick }, children: _jsxs("div", { className: cx('picker', className), "aria-label": label, "data-value": current, ...rest, children: [_jsx("div", { className: "picker__rail", role: "listbox", ref: rail, onScroll: scrolled, children: children }), unit != null && _jsx("span", { className: "picker__unit", children: unit })] }) }));
|
|
14
49
|
}
|
|
15
|
-
/** PickerItem — one value on the Picker rail. */
|
|
16
|
-
export function PickerItem({
|
|
17
|
-
|
|
50
|
+
/** PickerItem — one value on the Picker rail; a tap brings it to the centre. */
|
|
51
|
+
export function PickerItem({ value, className, children, onClick, ...rest }) {
|
|
52
|
+
const picker = useContext(PickerContext);
|
|
53
|
+
const own = value ?? (typeof children === 'string' || typeof children === 'number' ? String(children) : undefined);
|
|
54
|
+
return (_jsx("button", { type: "button", role: "option", "data-value": own, "aria-selected": own !== undefined && picker?.value === own ? 'true' : 'false', className: cx('picker__item', className), onClick: (e) => { onClick?.(e); if (!e.defaultPrevented && own !== undefined)
|
|
55
|
+
picker?.pick(own); }, ...rest, children: children }));
|
|
18
56
|
}
|
|
@@ -6,12 +6,12 @@ export interface BaseContentProps extends ComponentPropsWithoutRef<'section'> {
|
|
|
6
6
|
* maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
|
|
7
7
|
* Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
|
|
8
8
|
* In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
|
|
9
|
-
* (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of WAYS IN
|
|
10
|
+
* (sign in · new phone · join) — plain `Btn`s; what each one opens (a `Sheet` holding that way's whole form) is the APP's wiring, not
|
|
11
|
+
* cardds's. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
|
|
12
12
|
*/
|
|
13
13
|
export declare function BaseContent({ className, ...rest }: BaseContentProps): import("react").JSX.Element;
|
|
14
14
|
export interface CtaPackProps extends ComponentPropsWithoutRef<'div'> {
|
|
15
15
|
}
|
|
16
|
-
/** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds
|
|
16
|
+
/** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
|
|
17
17
|
export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;
|
|
@@ -5,14 +5,14 @@ import { cx } from '../cx.js';
|
|
|
5
5
|
* maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
|
|
6
6
|
* Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
|
|
7
7
|
* In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
|
|
8
|
-
* (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* (Lh 2026-09-20): several of them in a `Pages` = a walk of words the round pager turns (slideTo); or a CtaPack of WAYS IN
|
|
9
|
+
* (sign in · new phone · join) — plain `Btn`s; what each one opens (a `Sheet` holding that way's whole form) is the APP's wiring, not
|
|
10
|
+
* cardds's. The base itself carries no control; a form is never taken apart across it. Never both: a pager never works beside a sheet.
|
|
11
11
|
*/
|
|
12
12
|
export function BaseContent({ className, ...rest }) {
|
|
13
13
|
return _jsx("section", { className: cx('base-content', className), ...rest });
|
|
14
14
|
}
|
|
15
|
-
/** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds
|
|
15
|
+
/** CtaPack — the buttons under a BaseContent's words, wrapping on one line. In a Step it holds ways in (each opens a sheet — the app wires that); the Step's own move is never here. */
|
|
16
16
|
export function CtaPack({ className, ...rest }) {
|
|
17
17
|
return _jsx("div", { className: cx('cta-pack', className), ...rest });
|
|
18
18
|
}
|
package/dist/sheets/Drawer.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
export interface ModalProps extends ComponentPropsWithoutRef<'article'> {
|
|
3
|
+
/** CONTROLLED: on the screen or not (default `true`). The app owns it. Set it `false` and the modal leaves the way it came — its
|
|
4
|
+
* opening played backwards — then calls `onClosed`, where the app stops rendering it (and its `SheetStage ask`). */
|
|
5
|
+
open?: boolean;
|
|
6
|
+
/** the leaving has finished (at once, when motion is reduced) — unmount here */
|
|
7
|
+
onClosed?: () => void;
|
|
3
8
|
}
|
|
4
|
-
/**
|
|
5
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Modal — the ask: ONE card at the centre of a `SheetStage ask`, two parts — a `Lift` (the thing being asked about, light, scrolls
|
|
11
|
+
* when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above, then the drawer comes
|
|
12
|
+
* out below while the lift moves up.
|
|
13
|
+
* A COMPONENT, not a wiring: it does not know its buttons. They are plain `IconBtn`s / `Btn`s with the app's `onClick` — the app
|
|
14
|
+
* records the answer and sets `open={false}`; the modal's one behaviour is how it leaves.
|
|
15
|
+
*/
|
|
16
|
+
export declare function Modal({ open, onClosed, className, ...rest }: ModalProps): import("react").JSX.Element;
|
|
6
17
|
export interface LiftProps extends ComponentPropsWithoutRef<'div'> {
|
|
7
18
|
}
|
|
8
19
|
/** Lift — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
|
|
@@ -13,7 +24,7 @@ export interface DrawerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'titl
|
|
|
13
24
|
/** the fine print under it */
|
|
14
25
|
caption?: ReactNode;
|
|
15
26
|
}
|
|
16
|
-
/** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons)
|
|
27
|
+
/** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons) — plain buttons with the app's `onClick`; the Drawer does nothing with them. */
|
|
17
28
|
export declare function Drawer({ title, caption, className, children, ...rest }: DrawerProps): import("react").JSX.Element;
|
|
18
29
|
export interface BtnTrioProps extends ComponentPropsWithoutRef<'div'> {
|
|
19
30
|
}
|
package/dist/sheets/Drawer.js
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Children, Fragment } from 'react';
|
|
2
|
+
import { Children, Fragment, useEffect, useRef } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Modal — the ask: ONE card at the centre of a `SheetStage ask`, two parts — a `Lift` (the thing being asked about, light, scrolls
|
|
6
|
+
* when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above, then the drawer comes
|
|
7
|
+
* out below while the lift moves up.
|
|
8
|
+
* A COMPONENT, not a wiring: it does not know its buttons. They are plain `IconBtn`s / `Btn`s with the app's `onClick` — the app
|
|
9
|
+
* records the answer and sets `open={false}`; the modal's one behaviour is how it leaves.
|
|
10
|
+
*/
|
|
11
|
+
export function Modal({ open = true, onClosed, className, ...rest }) {
|
|
12
|
+
const ref = useRef(null);
|
|
13
|
+
const closed = useRef(onClosed);
|
|
14
|
+
closed.current = onClosed;
|
|
15
|
+
const shown = useRef(true); // the animations stand played forwards
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const el = ref.current;
|
|
18
|
+
if (!el || open === shown.current)
|
|
19
|
+
return;
|
|
20
|
+
shown.current = open;
|
|
21
|
+
const anims = el.getAnimations({ subtree: true });
|
|
22
|
+
anims.forEach((a) => a.reverse()); // the same keyframes, backwards to leave, forwards again to return — no closing keyframes to keep in step
|
|
23
|
+
if (open)
|
|
24
|
+
return;
|
|
25
|
+
let live = true;
|
|
26
|
+
void Promise.all(anims.map((a) => a.finished.catch(() => undefined))).then(() => { if (live)
|
|
27
|
+
closed.current?.(); });
|
|
28
|
+
return () => { live = false; };
|
|
29
|
+
}, [open]);
|
|
30
|
+
return _jsx("article", { ref: ref, className: cx('card modal', className), ...rest });
|
|
7
31
|
}
|
|
8
32
|
/** Lift — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
|
|
9
33
|
export function Lift({ className, ...rest }) {
|
|
10
34
|
return _jsx("div", { className: cx('modal__lift', className), ...rest });
|
|
11
35
|
}
|
|
12
|
-
/** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons)
|
|
36
|
+
/** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons) — plain buttons with the app's `onClick`; the Drawer does nothing with them. */
|
|
13
37
|
export function Drawer({ title, caption, className, children, ...rest }) {
|
|
14
38
|
return (_jsxs("div", { className: cx('modal__drawer', className), ...rest, children: [_jsx("h2", { className: "t-h2", children: title }), caption != null && _jsx("p", { className: "t-caption", children: caption }), children] }));
|
|
15
39
|
}
|
package/dist/sheets/Sheet.d.ts
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
/** how far the sheet slid. `away` = not on the screen (slid out, inert) — a state the app sets, never one a drag or a tap lands on */
|
|
2
3
|
export type SheetState = 'peek' | 'half' | '3q' | 'full' | 'away';
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/** what asked for the change: the handle's `tap`, a `drag` released, `focus` in a peeked sheet, the `keyboard` (or anything else) moving the cut */
|
|
5
|
+
export type SheetChangeReason = 'tap' | 'drag' | 'focus' | 'keyboard';
|
|
6
|
+
declare const ORDER: readonly ["peek", "half", "3q", "full"];
|
|
7
|
+
type Resting = (typeof ORDER)[number];
|
|
8
|
+
export interface SheetProps extends Omit<ComponentPropsWithoutRef<'article'>, 'onChange'> {
|
|
9
|
+
/** CONTROLLED: where the sheet is. The app owns it — pair it with `onStateChange`, or the sheet cannot move (like `value` without `onChange`).
|
|
10
|
+
* `peek` (handle + title row) · `half` · `3q` · `full` (its top edge at the top bar) · `away` (not on the screen). A class, never a height. */
|
|
5
11
|
state?: SheetState;
|
|
12
|
+
/** UNCONTROLLED: where it starts (default `half`); the sheet then keeps its own state. Ignored when `state` is given. */
|
|
13
|
+
defaultState?: SheetState;
|
|
14
|
+
/** the sheet ASKS to move — the handle was tapped or dragged, a field in a peeked sheet took focus, the keyboard moved the cut.
|
|
15
|
+
* Controlled: set your state to it (or don't — the sheet stays). This is where an app ties a sheet to its router / back button. */
|
|
16
|
+
onStateChange?: (state: SheetState, reason: SheetChangeReason) => void;
|
|
17
|
+
/** the states a drag snaps to and a reveal may climb through — "peek half full", or an array (default all four) */
|
|
18
|
+
states?: string | readonly Resting[];
|
|
6
19
|
/** floats above a footnote strip on a dim stage (bottom corners and border back) */
|
|
7
20
|
raised?: boolean;
|
|
8
|
-
/** the grab handle
|
|
21
|
+
/** the grab handle; `false` for a sheet with no handle, a string sets its accessible name */
|
|
9
22
|
handle?: boolean | string;
|
|
10
|
-
/** which states a drag snaps to, e.g. "peek half full" (default all four: peek half 3q full) */
|
|
11
|
-
states?: string;
|
|
12
23
|
}
|
|
13
24
|
/**
|
|
14
|
-
* Sheet — the SINGLE sheet: one card as a bottom sheet holding what the member ACTS on
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
25
|
+
* Sheet — the SINGLE sheet: one card as a bottom sheet holding what the member ACTS on. Always the full height of its
|
|
26
|
+
* box — a Step or a SheetStage — a state is only how far it slid (translate; the material never changes size).
|
|
27
|
+
* ONE SHAPE, written top-down: handle · head (a CardHead / SheetHead — pinned, never scrolls) · SheetBody (scrolls, as far as
|
|
28
|
+
* it needs) · CardFoot (the button that finishes the sheet — at the bottom, above the keyboard; gone at peek).
|
|
29
|
+
*
|
|
30
|
+
* It is a COMPONENT, not a wiring: it knows nothing of what opens it. The app owns `state` and sets it from whatever it
|
|
31
|
+
* likes — a button, a route, a timer: `<Btn onClick={() => setSheet('full')}>` … `<Sheet state={sheet} onStateChange={setSheet}>`.
|
|
32
|
+
* Its own behaviour stays inside it and only ever ASKS (`onStateChange`): a tap on the handle toggles its own state ↔ peek,
|
|
33
|
+
* a drag snaps to the nearest of `states`, focus in a peeked sheet raises it, and when the cut moves (the keyboard) it
|
|
34
|
+
* climbs until the focused control has room above the foot.
|
|
23
35
|
*/
|
|
24
|
-
export declare function Sheet({ state,
|
|
36
|
+
export declare function Sheet({ state, defaultState, onStateChange, states, raised, handle, className, children, onFocus, ...rest }: SheetProps): import("react").JSX.Element;
|
|
37
|
+
export {};
|