@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +29 -28
  3. package/cardds.css +3 -3
  4. package/css/actions.css +67 -22
  5. package/css/base.css +44 -18
  6. package/css/card.css +25 -26
  7. package/css/choice.css +21 -33
  8. package/css/forms.css +0 -3
  9. package/css/journey.css +1 -3
  10. package/css/layover.css +19 -41
  11. package/css/lists.css +1 -6
  12. package/css/media.css +9 -18
  13. package/css/numbers.css +6 -21
  14. package/css/people.css +3 -9
  15. package/css/sheet.css +15 -19
  16. package/css/stack.css +21 -29
  17. package/css/step.css +7 -12
  18. package/css/theme.template.css +2 -1
  19. package/css/tokens.css +22 -4
  20. package/dist/actions/Btn.d.ts +1 -1
  21. package/dist/actions/Dropdown.js +3 -9
  22. package/dist/actions/Fab.d.ts +1 -1
  23. package/dist/actions/Fab.js +3 -2
  24. package/dist/cardds.css +264 -267
  25. package/dist/cards/Card.d.ts +24 -8
  26. package/dist/cards/Card.js +23 -8
  27. package/dist/cards/CardHead.d.ts +0 -1
  28. package/dist/cards/CardHead.js +1 -1
  29. package/dist/choice/Calendar.js +2 -5
  30. package/dist/choice/Mood.d.ts +10 -5
  31. package/dist/choice/Mood.js +4 -2
  32. package/dist/forms/Field.js +2 -3
  33. package/dist/forms/FileBtn.js +1 -1
  34. package/dist/forms/Pin.js +7 -12
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +3 -3
  37. package/dist/layover/Banner.d.ts +10 -3
  38. package/dist/layover/Banner.js +6 -3
  39. package/dist/lists/Row.d.ts +2 -1
  40. package/dist/lists/Row.js +5 -5
  41. package/dist/media/Postcard.js +3 -5
  42. package/dist/numbers/Band.d.ts +3 -1
  43. package/dist/numbers/Picker.js +3 -10
  44. package/dist/numbers/Track.d.ts +4 -2
  45. package/dist/numbers/Track.js +3 -3
  46. package/dist/people/AvatarPick.d.ts +3 -1
  47. package/dist/people/AvatarPick.js +2 -2
  48. package/dist/scaffold/ActionBar.d.ts +3 -3
  49. package/dist/scaffold/ActionBar.js +2 -2
  50. package/dist/scaffold/Centre.d.ts +2 -2
  51. package/dist/scaffold/Centre.js +2 -2
  52. package/dist/scaffold/PagerAt.d.ts +1 -1
  53. package/dist/scaffold/PagerAt.js +1 -1
  54. package/dist/scaffold/Screen.js +4 -7
  55. package/dist/scaffold/TopBar.js +3 -5
  56. package/dist/sheets/Drawer.js +3 -4
  57. package/dist/sheets/Sheet.js +9 -22
  58. package/dist/sheets/SheetStack.d.ts +2 -2
  59. package/dist/sheets/SheetStack.js +20 -13
  60. package/dist/sheets/SheetSteps.js +8 -11
  61. package/dist/sheets/StackBack.js +2 -1
  62. package/dist/sheets/StackSheet.d.ts +3 -2
  63. package/dist/sheets/StackSheet.js +7 -11
  64. package/dist/state.d.ts +24 -0
  65. package/dist/state.js +57 -0
  66. package/dist/templates/CallSheet.js +2 -4
  67. package/dist/templates/Home.d.ts +3 -2
  68. package/dist/templates/Home.js +2 -4
  69. package/dist/templates/Splash.js +3 -3
  70. package/dist/templates/Walkthrough.js +4 -5
  71. package/dist/type/Icon.js +3 -5
  72. package/dist/type/Text.d.ts +1 -1
  73. package/dist/type/textScale.d.ts +6 -0
  74. package/dist/type/textScale.js +12 -0
  75. package/package.json +7 -4
  76. package/dist/layover/Deck.d.ts +0 -9
  77. package/dist/layover/Deck.js +0 -10
@@ -1,14 +1,17 @@
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: 1 (default, light) · 2 (tinted) · 3 (dark / accent — controls invert automatically) · 4 · 5 (more colours, from the palette) */
4
- tone?: 1 | 2 | 3 | 4 | 5;
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 */
8
11
  row?: boolean;
9
12
  /** the image IS the card: a Placeholder or img first, text over a scrim */
10
13
  cover?: boolean;
11
- /** a sticky-note corner, no border (swipe decks, quotes) */
14
+ /** a sticky-note corner, no border (quotes) */
12
15
  fold?: boolean;
13
16
  /** one card at the human centre of a `Screen fill` (the introduction screens) */
14
17
  centre?: boolean;
@@ -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
- * Children: a CardHead first (heading left, contextual info far right), the content (frameless rows, kv, a ring,
24
- * a paragraph — never a second card boundary), an optional CardFoot (≤4 buttons on one line).
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;
@@ -1,14 +1,29 @@
1
- import { createElement } from 'react';
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
- * Children: a CardHead first (heading left, contextual info far right), the content (frameless rows, kv, a ring,
6
- * a paragraph — never a second card boundary), an optional CardFoot (≤4 buttons on one line).
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
- return createElement(as, {
11
- 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),
12
- ...rest,
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
  }
@@ -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) */
@@ -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: level === 'display' ? 't-display' : `t-${level}`, children: heading }), caption != null && _jsx("span", { className: "t-caption t-muted", children: caption }), children, end] }));
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
  }
@@ -1,13 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useControlled } from '../state.js';
3
3
  import { cx } from '../cx.js';
4
4
  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 [inner, setInner] = useState(defaultSelected);
9
- const day = selected !== undefined ? selected : inner;
10
- const pick = (d) => { if (selected === undefined)
11
- setInner(d); onSelect?.(d); };
8
+ const [day, pick] = useControlled(selected, defaultSelected, onSelect);
12
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)))] })] }));
13
10
  }
@@ -1,11 +1,16 @@
1
1
  import type { ComponentProps } from 'react';
2
- export interface MoodProps extends Omit<ComponentProps<'fieldset'>, 'onChange'> {
2
+ type MoodValue = 1 | 2 | 3 | 4 | 5;
3
+ export interface MoodProps extends Omit<ComponentProps<'fieldset'>, 'onChange' | 'defaultValue'> {
3
4
  /** the radio group name */
4
5
  name?: string;
5
- /** the picked dot, 1 (worst) to 5 (best) */
6
- value?: 1 | 2 | 3 | 4 | 5;
6
+ /** CONTROLLED: the picked dot, 1 (worst) to 5 (best) — with `onChange`; or `defaultValue` */
7
+ value?: MoodValue;
8
+ /** UNCONTROLLED: the dot picked on arrival; the Mood then keeps its own */
9
+ defaultValue?: MoodValue;
7
10
  label?: string;
8
- onChange?: React.ChangeEventHandler<HTMLInputElement>;
11
+ /** the dot the member tapped */
12
+ onChange?: (value: MoodValue) => void;
9
13
  }
10
14
  /** Mood — five dots in a pill from bad to good; the picked one grows. */
11
- export declare function Mood({ name, value, label, onChange, className, ...rest }: MoodProps): import("react").JSX.Element;
15
+ export declare function Mood({ name, value, defaultValue, label, onChange, className, ...rest }: MoodProps): import("react").JSX.Element;
16
+ export {};
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
+ import { useControlled } from '../state.js';
3
4
  /** Mood — five dots in a pill from bad to good; the picked one grows. */
4
- export function Mood({ name = 'mood', value, label = 'Mood', onChange, className, ...rest }) {
5
- 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, defaultChecked: n === value, onChange: onChange }), _jsx("span", { className: "mood__dot" })] }, n))) }));
5
+ export function Mood({ name = 'mood', value, defaultValue, label = 'Mood', onChange, className, ...rest }) {
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))) }));
6
8
  }
@@ -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 () => ro.disconnect();
64
+ return watchSize([el], fit);
66
65
  }, [input.placeholder]);
67
66
  useEffect(() => {
68
67
  if (wrong) {
@@ -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", hidden: true, ...input }), children] }));
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,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { useControlled, useLatest } from '../state.js';
3
4
  import { cx } from '../cx.js';
4
5
  import { useCheck } from './check.js';
5
6
  const RESET_MS = 900; // a code that does not match: red + shake this long, then the cells empty for a new try
@@ -11,23 +12,20 @@ const RESET_MS = 900; // a code that does not match: red + shake this long, then
11
12
  */
12
13
  export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onChange, autoFocus, error, match, formatError, secret, className, onAnimationEnd, ...rest }) {
13
14
  const ref = useRef(null);
14
- const [inner, setInner] = useState(defaultValue);
15
- const code = (value ?? inner).replace(/\D/g, '').slice(0, length);
15
+ const [typed, request] = useControlled(value, defaultValue, onChange);
16
+ const code = typed.replace(/\D/g, '').slice(0, length);
16
17
  const cell = (i) => ref.current?.querySelectorAll('.pin__cell')[i];
17
18
  /* a miss — a match that failed, or the app's error: shake, red, then the cells empty for a new try on the same step */
18
- const latest = useRef({ value, onChange, error });
19
- latest.current = { value, onChange, error };
19
+ const latest = useLatest(error);
20
20
  const miss = () => {
21
21
  restart.current = true;
22
22
  setShake(true);
23
23
  clearTimeout(reset.current);
24
24
  reset.current = setTimeout(() => {
25
25
  setBad(false);
26
- setCleared(latest.current.error);
26
+ setCleared(latest.current);
27
27
  restart.current = false;
28
- if (latest.current.value === undefined)
29
- setInner('');
30
- latest.current.onChange?.('', false);
28
+ request('', false);
31
29
  cell(0)?.focus();
32
30
  }, RESET_MS);
33
31
  };
@@ -46,10 +44,7 @@ export function Pin({ length = 6, value, defaultValue = '', label = 'Digit', onC
46
44
  said.current = true;
47
45
  miss();
48
46
  }
49
- if (value === undefined)
50
- setInner(next);
51
- if (next !== code)
52
- onChange?.(next, next.length === length);
47
+ request(next, next.length === length);
53
48
  };
54
49
  useEffect(() => {
55
50
  if (autoFocus)
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { cx } from './cx.js';
4
4
  export * from './type/Icon.js';
5
5
  export { ICON_NAMES } from './type/icons.js';
6
6
  export * from './type/Text.js';
7
+ export { setTextScale } from './type/textScale.js';
7
8
  export * from './scaffold/Screen.js';
8
9
  export * from './scaffold/AppBar.js';
9
10
  export * from './scaffold/TopBar.js';
@@ -72,7 +73,6 @@ export * from './media/Tile.js';
72
73
  export * from './media/Mosaic.js';
73
74
  export * from './layover/Banner.js';
74
75
  export * from './layover/Callout.js';
75
- export * from './layover/Deck.js';
76
76
  export * from './journey/Route.js';
77
77
  export * from './journey/TileBadge.js';
78
78
  export * from './journey/NoteRow.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // cardds — the React adapter over the CSS. Thin: every component emits the
2
- // markup contract the CSS documents, nothing more. cardds.js rides along for
3
- // the behaviours (sheet handle, stack open/close, dropdown value sync).
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';
@@ -8,6 +8,7 @@ export { cx } from './cx.js';
8
8
  export * from './type/Icon.js';
9
9
  export { ICON_NAMES } from './type/icons.js';
10
10
  export * from './type/Text.js';
11
+ export { setTextScale } from './type/textScale.js';
11
12
  export * from './scaffold/Screen.js';
12
13
  export * from './scaffold/AppBar.js';
13
14
  export * from './scaffold/TopBar.js';
@@ -76,7 +77,6 @@ export * from './media/Tile.js';
76
77
  export * from './media/Mosaic.js';
77
78
  export * from './layover/Banner.js';
78
79
  export * from './layover/Callout.js';
79
- export * from './layover/Deck.js';
80
80
  export * from './journey/Route.js';
81
81
  export * from './journey/TileBadge.js';
82
82
  export * from './journey/NoteRow.js';
@@ -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. A native details, no JS. */
8
- export declare function Banner({ summary, className, children, ...rest }: BannerProps): import("react").JSX.Element;
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;
@@ -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. A native details, no JS. */
6
- export function Banner({ summary, className, children, ...rest }) {
7
- return (_jsxs("details", { className: cx('banner', className), ...rest, children: [_jsxs("summary", { className: "banner__summary", children: [summary, _jsx(Icon, { name: "chevron-down", size: "xs" })] }), _jsx("div", { className: "banner__body", children: children })] }));
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
  }
@@ -41,7 +41,8 @@ export interface RowProps extends Omit<ComponentProps<'div'>, 'title'> {
41
41
  */
42
42
  export declare function Row({ lead, leadIcon, title, meta, body, end, endValue, endCaption, lg, done, as, href, more, open, defaultOpen, onOpenChange, className, children, ...rest }: RowProps): import("react").JSX.Element;
43
43
  export interface RowsProps extends ComponentProps<'div'> {
44
- /** the group's name, above its rows (Today · Tomorrow · Later): a long list is GROUPED, not spaced out — several Rows one after another */
44
+ /** the group's name, above its rows (Today · Tomorrow · Later): a long list is GROUPED, not spaced out — several Rows one after another.
45
+ * Words = the small label; an ELEMENT (`<Text level="title" as="h3">`) is used as the heading itself — the group is labelled by it. */
45
46
  heading?: ReactNode;
46
47
  }
47
48
  /** Rows — a list of Rows separated by hairlines; `heading` names the group (a `role="group"` labelled by it). */
package/dist/lists/Row.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createElement, useId } from 'react';
2
+ import { cloneElement, createElement, isValidElement, useId } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { Icon } from '../type/Icon.js';
5
+ 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,17 +11,16 @@ 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", open: open ?? defaultOpen, ...rest, onToggle: (e) => { const is = e.currentTarget.open; if (open !== undefined && is !== open)
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 }) {
23
23
  const id = useId();
24
24
  const named = heading != null;
25
- return (_jsxs("div", { className: cx('rows', className), role: named ? 'group' : undefined, "aria-labelledby": named ? id : undefined, ...rest, children: [named && _jsx("span", { className: "rows__head", id: id, children: heading }), children] }));
25
+ return (_jsxs("div", { className: cx('rows', className), role: named ? 'group' : undefined, "aria-labelledby": named ? id : undefined, ...rest, children: [named && (isValidElement(heading) ? cloneElement(heading, { id }) : _jsx("span", { className: "rows__head", id: id, children: heading })), children] }));
26
26
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useControlled } from '../state.js';
3
3
  import { cx } from '../cx.js';
4
4
  import { IconBtn } from '../actions/IconBtn.js';
5
5
  /**
@@ -9,10 +9,8 @@ import { IconBtn } from '../actions/IconBtn.js';
9
9
  * Who else liked it, a voice note, a reply go OUTSIDE it — a Row under it (`Row more` expands the list).
10
10
  */
11
11
  export function Postcard({ photo, alt = '', name, line, caption, voice, flipped, defaultFlipped = false, onFlip, liked, onLike, likeLabel = 'ถูกใจ', frontLabel = 'พลิกดูด้านหลัง', backLabel = 'พลิกดูด้านหน้า', className, ...rest }) {
12
- const [inner, setInner] = useState(defaultFlipped);
13
- const back = flipped ?? inner;
14
- const turn = () => { if (flipped === undefined)
15
- setInner(!back); onFlip?.(!back); };
12
+ const [back, flip] = useControlled(flipped, defaultFlipped, onFlip);
13
+ const turn = () => flip(!back);
16
14
  const words = (_jsxs("div", { className: "postcard__words", children: [_jsx("span", { className: "t-h1", children: name }), line != null && _jsx("span", { className: "t-body", children: line })] }));
17
15
  return (_jsxs("div", { className: cx('postcard', back && 'is-flipped', className), ...rest, children: [_jsxs("div", { className: "postcard__face postcard__front", children: [_jsx("button", { className: "postcard__turn", type: "button", "aria-label": frontLabel, onClick: turn, children: photo ? _jsx("img", { src: photo, alt: alt }) : alt ? _jsx("div", { className: "ph", role: "img", "aria-label": alt }) : _jsx("div", { className: "ph" }) }), _jsxs("div", { className: "postcard__band", children: [words, (liked !== undefined || onLike) && _jsx(IconBtn, { invert: true, icon: "heart", label: likeLabel, "aria-pressed": !!liked, onClick: () => onLike?.(!liked) })] })] }), _jsxs("div", { className: "postcard__face postcard__back", children: [_jsxs("button", { className: "postcard__turn", type: "button", "aria-label": backLabel, onClick: turn, children: [words, caption != null && _jsx("p", { className: "postcard__caption", children: caption })] }), voice != null && _jsx("div", { className: "postcard__voice", children: voice })] })] }));
18
16
  }
@@ -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
- tone?: 1 | 2 | 3 | 4 | 5;
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;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { createContext, useContext, useEffect, useLayoutEffect, useRef } from 'react';
3
+ import { useControlled } from '../state.js';
3
4
  import { cx } from '../cx.js';
4
5
  const PickerContext = createContext(null);
5
6
  /**
@@ -9,18 +10,10 @@ const PickerContext = createContext(null);
9
10
  * settled in the centre. The CSS lights the centred item by scroll position; the app never reads the DOM to know what was picked.
10
11
  */
11
12
  export function Picker({ value, defaultValue, onChange, unit, label, className, children, ...rest }) {
12
- const [inner, setInner] = useState(defaultValue);
13
- const current = value ?? inner;
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
- if (value === undefined)
21
- setInner(next);
22
- onChange?.(next);
23
- };
24
17
  const items = () => [...(rail.current?.querySelectorAll(':scope > .picker__item') ?? [])];
25
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
26
19
  /* the chosen value stands in the centre — at once on arrival, sliding after */
@@ -7,6 +7,8 @@ export declare function Track({ label, className, ...rest }: TrackProps): import
7
7
  export interface TrackStepProps extends ComponentProps<'li'> {
8
8
  /** this step (and the connector into it) is done */
9
9
  done?: boolean;
10
+ /** the step's NUMBER in the round instead of an Icon (1, 2, 3…) — a walk whose steps are counted, not pictured */
11
+ num?: number;
10
12
  }
11
- /** TrackStep — one step of a Track: an Icon in a round; lit when done. */
12
- export declare function TrackStep({ done, className, ...rest }: TrackStepProps): import("react").JSX.Element;
13
+ /** TrackStep — one step of a Track: an Icon (children) or its number (`num`) in a round; lit when done. */
14
+ export declare function TrackStep({ done, num, className, children, ...rest }: TrackStepProps): import("react").JSX.Element;
@@ -4,7 +4,7 @@ import { cx } from '../cx.js';
4
4
  export function Track({ label = 'Progress', className, ...rest }) {
5
5
  return _jsx("ol", { className: cx('track', className), "aria-label": label, ...rest });
6
6
  }
7
- /** TrackStep — one step of a Track: an Icon in a round; lit when done. */
8
- export function TrackStep({ done, className, ...rest }) {
9
- return _jsx("li", { className: cx('track__step', done && 'is-done', className), ...rest });
7
+ /** TrackStep — one step of a Track: an Icon (children) or its number (`num`) in a round; lit when done. */
8
+ export function TrackStep({ done, num, className, children, ...rest }) {
9
+ return _jsx("li", { className: cx('track__step', done && 'is-done', className), ...rest, children: num != null ? _jsx("span", { className: "track__dot", children: num }) : children });
10
10
  }
@@ -13,6 +13,8 @@ export interface AvatarPickItemProps extends Omit<ComponentProps<'button'>, 'nam
13
13
  role?: ReactNode;
14
14
  /** the Avatar (lg) */
15
15
  children?: ReactNode;
16
+ /** this person is the one picked (aria-pressed; the app keeps which) */
17
+ selected?: boolean;
16
18
  }
17
19
  /** AvatarPickItem — one person in an AvatarPick: an `Avatar size="lg"`, a name, a role. */
18
- export declare function AvatarPickItem({ name, role, className, children, ...rest }: AvatarPickItemProps): import("react").JSX.Element;
20
+ export declare function AvatarPickItem({ name, role, selected, className, children, ...rest }: AvatarPickItemProps): import("react").JSX.Element;
@@ -5,6 +5,6 @@ export function AvatarPick({ bleed, label, className, ...rest }) {
5
5
  return _jsx("div", { className: cx('avatar-pick', bleed && 'bleed', className), "aria-label": label, ...rest });
6
6
  }
7
7
  /** AvatarPickItem — one person in an AvatarPick: an `Avatar size="lg"`, a name, a role. */
8
- export function AvatarPickItem({ name, role, className, children, ...rest }) {
9
- return (_jsxs("button", { type: "button", className: cx('avatar-pick__item', className), ...rest, children: [children, _jsx("span", { className: "avatar-pick__name", children: name }), role != null && _jsx("span", { className: "avatar-pick__role", children: role })] }));
8
+ export function AvatarPickItem({ name, role, selected, className, children, ...rest }) {
9
+ return (_jsxs("button", { type: "button", className: cx('avatar-pick__item', className), "aria-pressed": selected, ...rest, children: [children, _jsx("span", { className: "avatar-pick__name", children: name }), role != null && _jsx("span", { className: "avatar-pick__role", children: role })] }));
10
10
  }
@@ -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; the PagerAt dots never do — they go under the card) · `tiers`: rows stacked — a control tier above the buttons */
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). `pager`: Back + Next as peers — buttons only; where you are is the
12
- * PagerAt under the card in a Centre group, never in the bar (Lh, 2026-09-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). `pager`: Back + Next as peers — buttons only; where you are is the
7
- * PagerAt under the card in a Centre group, never in the bar (Lh, 2026-09-12).
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
  */
@@ -3,12 +3,12 @@ export interface CentreProps extends ComponentProps<'div'> {
3
3
  }
4
4
  /**
5
5
  * Centre — the centred group: ONE thing to read at the human centre of the screen, when that thing is not a lone
6
- * card: an EmptyState; a Card with its quiet second Btn and the PagerAt under it; a Deck with its DeckActions.
6
+ * card: an EmptyState; a Card with its quiet second Btn and the PagerAt under it.
7
7
  * On a plain `Screen` (the usual case, Lh 2026-09-12) it is in flow: the column is one phone tall, the group takes
8
8
  * the room after the TopBar and centres a short card; a long card makes the page longer and scrolls up under the
9
9
  * bar — nothing is capped, nothing shrinks. On a `Screen fill` it floats at 40% down (`--screen-centre`), capped
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, stays outside the Screen.
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'> {
@@ -3,12 +3,12 @@ import { flushSync } from 'react-dom';
3
3
  import { cx } from '../cx.js';
4
4
  /**
5
5
  * Centre — the centred group: ONE thing to read at the human centre of the screen, when that thing is not a lone
6
- * card: an EmptyState; a Card with its quiet second Btn and the PagerAt under it; a Deck with its DeckActions.
6
+ * card: an EmptyState; a Card with its quiet second Btn and the PagerAt under it.
7
7
  * On a plain `Screen` (the usual case, Lh 2026-09-12) it is in flow: the column is one phone tall, the group takes
8
8
  * the room after the TopBar and centres a short card; a long card makes the page longer and scrolls up under the
9
9
  * bar — nothing is capped, nothing shrinks. On a `Screen fill` it floats at 40% down (`--screen-centre`), capped
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, stays outside the Screen.
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, never inside a page. */
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;
@@ -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, never inside a page. */
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
  }