@usableapp/cardds 0.2.2 → 0.4.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.
Files changed (103) hide show
  1. package/README.md +35 -107
  2. package/cardds.js +6 -449
  3. package/css/actions.css +41 -35
  4. package/css/base.css +9 -9
  5. package/css/card.css +26 -25
  6. package/css/choice.css +50 -50
  7. package/css/forms.css +18 -18
  8. package/css/journey.css +16 -16
  9. package/css/layover.css +45 -45
  10. package/css/lists.css +43 -43
  11. package/css/media.css +33 -33
  12. package/css/numbers.css +25 -25
  13. package/css/people.css +11 -11
  14. package/css/sheet.css +29 -29
  15. package/css/step.css +6 -6
  16. package/css/theme.template.css +6 -3
  17. package/css/tokens.css +2 -0
  18. package/dist/actions/Btn.d.ts +109 -108
  19. package/dist/actions/BtnRow.d.ts +2 -2
  20. package/dist/actions/Chip.d.ts +102 -101
  21. package/dist/actions/Dot.d.ts +2 -2
  22. package/dist/actions/Dropdown.d.ts +36 -19
  23. package/dist/actions/Dropdown.js +41 -11
  24. package/dist/actions/Fab.d.ts +2 -2
  25. package/dist/actions/IconBtn.d.ts +298 -10
  26. package/dist/actions/IconBtn.js +6 -8
  27. package/dist/actions/Link.d.ts +105 -104
  28. package/dist/actions/Segment.d.ts +3 -3
  29. package/dist/cardds.css +354 -345
  30. package/dist/cards/Card.d.ts +99 -98
  31. package/dist/cards/CardFoot.d.ts +2 -2
  32. package/dist/cards/CardHead.d.ts +2 -2
  33. package/dist/cards/CardParts.d.ts +7 -7
  34. package/dist/cards/Placeholder.d.ts +2 -2
  35. package/dist/cards/Stat.d.ts +7 -5
  36. package/dist/cards/Stat.js +2 -2
  37. package/dist/choice/Calendar.d.ts +17 -6
  38. package/dist/choice/Calendar.js +9 -3
  39. package/dist/choice/Check.d.ts +3 -3
  40. package/dist/choice/DayStrip.d.ts +3 -3
  41. package/dist/choice/Mood.d.ts +2 -2
  42. package/dist/choice/Slider.d.ts +3 -3
  43. package/dist/choice/Slider.js +12 -8
  44. package/dist/forms/AddRow.d.ts +2 -2
  45. package/dist/forms/Composer.d.ts +2 -2
  46. package/dist/forms/Field.d.ts +2 -2
  47. package/dist/forms/FileBtn.d.ts +2 -2
  48. package/dist/forms/Pin.d.ts +10 -7
  49. package/dist/forms/Pin.js +47 -18
  50. package/dist/journey/NoteRow.d.ts +2 -2
  51. package/dist/journey/Route.d.ts +2 -2
  52. package/dist/journey/TileBadge.d.ts +2 -2
  53. package/dist/layover/Banner.d.ts +2 -2
  54. package/dist/layover/Callout.d.ts +2 -2
  55. package/dist/layover/Deck.d.ts +3 -3
  56. package/dist/lists/Bubble.d.ts +3 -3
  57. package/dist/lists/Kv.d.ts +5 -5
  58. package/dist/lists/Legend.d.ts +3 -3
  59. package/dist/lists/Row.d.ts +5 -4
  60. package/dist/lists/Timeline.d.ts +3 -3
  61. package/dist/media/MapArea.d.ts +2 -2
  62. package/dist/media/Mosaic.d.ts +3 -3
  63. package/dist/media/Postcard.d.ts +9 -5
  64. package/dist/media/Postcard.js +8 -3
  65. package/dist/media/Quote.d.ts +2 -2
  66. package/dist/media/Tile.d.ts +5 -4
  67. package/dist/numbers/Badge.d.ts +2 -2
  68. package/dist/numbers/Band.d.ts +3 -3
  69. package/dist/numbers/Bars.d.ts +3 -3
  70. package/dist/numbers/DotGrid.d.ts +2 -2
  71. package/dist/numbers/Picker.d.ts +20 -8
  72. package/dist/numbers/Picker.js +51 -13
  73. package/dist/numbers/Ring.d.ts +2 -2
  74. package/dist/numbers/Track.d.ts +3 -3
  75. package/dist/people/Avatar.d.ts +100 -99
  76. package/dist/people/AvatarPick.d.ts +3 -3
  77. package/dist/scaffold/ActionBar.d.ts +3 -3
  78. package/dist/scaffold/AppBar.d.ts +2 -2
  79. package/dist/scaffold/BaseContent.d.ts +7 -7
  80. package/dist/scaffold/BaseContent.js +4 -4
  81. package/dist/scaffold/Centre.d.ts +3 -3
  82. package/dist/scaffold/EmptyState.d.ts +2 -2
  83. package/dist/scaffold/FilterRow.d.ts +2 -2
  84. package/dist/scaffold/FloatBar.d.ts +105 -104
  85. package/dist/scaffold/PagerAt.d.ts +2 -2
  86. package/dist/scaffold/Screen.d.ts +100 -99
  87. package/dist/scaffold/TopBar.d.ts +2 -2
  88. package/dist/sheets/CardBack.d.ts +2 -2
  89. package/dist/sheets/Drawer.d.ts +19 -8
  90. package/dist/sheets/Drawer.js +29 -5
  91. package/dist/sheets/Sheet.d.ts +29 -16
  92. package/dist/sheets/Sheet.js +157 -11
  93. package/dist/sheets/SheetBody.d.ts +2 -2
  94. package/dist/sheets/SheetHead.d.ts +2 -2
  95. package/dist/sheets/SheetStack.d.ts +15 -3
  96. package/dist/sheets/SheetStack.js +74 -2
  97. package/dist/sheets/SheetStage.d.ts +2 -2
  98. package/dist/sheets/Step.d.ts +2 -2
  99. package/dist/type/Icon.d.ts +22 -7
  100. package/dist/type/Icon.js +27 -4
  101. package/dist/type/Text.d.ts +7 -1
  102. package/dist/type/Text.js +2 -2
  103. package/package.json +3 -2
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
- * Typing a digit moves to the next cell, Backspace in an empty cell moves back, a pasted code fills every cell
7
- * (cardds.js, delegated — the cells stay plain inputs). The first cell takes the SMS one-time-code autofill.
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
- const el = ref.current;
13
- if (!el || !onChange)
14
- return;
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
- return (_jsx("div", { ref: ref, className: cx('pin', className), ...rest, children: Array.from({ length }, (_, i) => (_jsx("input", { className: "pin__cell", inputMode: "numeric", pattern: "[0-9]*", placeholder: " ", defaultValue: value[i] ?? '', "aria-label": `${label} ${i + 1}`, autoComplete: i === 0 ? 'one-time-code' : 'off' }, i))) }));
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
  }
@@ -1,6 +1,6 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
1
+ import type { ComponentProps } from 'react';
2
2
  import { type IconName } from '../type/Icon.js';
3
- export interface NoteRowProps extends ComponentPropsWithoutRef<'div'> {
3
+ export interface NoteRowProps extends ComponentProps<'div'> {
4
4
  icon?: IconName;
5
5
  }
6
6
  /** NoteRow — icon + caption: transit hints, fine print. */
@@ -1,4 +1,4 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
1
+ import type { ComponentProps, ReactNode } from 'react';
2
2
  import { type IconName } from '../type/Icon.js';
3
3
  export interface RoutePoint {
4
4
  /** the place name (caption) */
@@ -8,7 +8,7 @@ export interface RoutePoint {
8
8
  /** the time */
9
9
  time: ReactNode;
10
10
  }
11
- export interface RouteProps extends ComponentPropsWithoutRef<'div'> {
11
+ export interface RouteProps extends ComponentProps<'div'> {
12
12
  from: RoutePoint;
13
13
  to: RoutePoint;
14
14
  /** the icon on the dotted connector (plane by default) */
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface TileBadgeProps extends ComponentPropsWithoutRef<'span'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface TileBadgeProps extends ComponentProps<'span'> {
3
3
  /** the small label above ("Gate") */
4
4
  label: ReactNode;
5
5
  /** the big value ("21") */
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface BannerProps extends ComponentPropsWithoutRef<'details'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface BannerProps extends ComponentProps<'details'> {
3
3
  /** the chip that opens it (a `Chip on` with a chevron Icon xs) */
4
4
  summary: ReactNode;
5
5
  /** the tone of the opened card */
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface CalloutProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface CalloutProps extends Omit<ComponentProps<'div'>, 'title'> {
3
3
  title: ReactNode;
4
4
  sub?: ReactNode;
5
5
  /** position over the stage background (CSS lengths / percentages) */
@@ -1,9 +1,9 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface DeckProps extends ComponentPropsWithoutRef<'div'> {
1
+ import type { ComponentProps } from 'react';
2
+ export interface DeckProps extends ComponentProps<'div'> {
3
3
  }
4
4
  /** Deck — swipe cards fanned behind each other; the LAST child is on top. Children are Cards (the front one may `fold`). */
5
5
  export declare function Deck({ className, ...rest }: DeckProps): import("react").JSX.Element;
6
- export interface DeckActionsProps extends ComponentPropsWithoutRef<'div'> {
6
+ export interface DeckActionsProps extends ComponentProps<'div'> {
7
7
  }
8
8
  /** DeckActions — the two big round buttons under a Deck (skip · keep). */
9
9
  export declare function DeckActions({ className, ...rest }: DeckActionsProps): import("react").JSX.Element;
@@ -1,9 +1,9 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface ThreadProps extends ComponentPropsWithoutRef<'div'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface ThreadProps extends ComponentProps<'div'> {
3
3
  }
4
4
  /** Thread — a stack of Bubbles: a conversation. */
5
5
  export declare function Thread({ className, ...rest }: ThreadProps): import("react").JSX.Element;
6
- export interface BubbleProps extends ComponentPropsWithoutRef<'div'> {
6
+ export interface BubbleProps extends ComponentProps<'div'> {
7
7
  /** whose: theirs on the left (default), mine on the right */
8
8
  side?: 'left' | 'right';
9
9
  /** the caption under the message — a time, a tick (an Icon) */
@@ -1,12 +1,12 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
1
+ import type { ComponentProps, ReactNode } from 'react';
2
2
  import { type IconName } from '../type/Icon.js';
3
- export interface KvProps extends ComponentPropsWithoutRef<'dl'> {
3
+ export interface KvProps extends ComponentProps<'dl'> {
4
4
  /** hairlines between rows */
5
5
  lined?: boolean;
6
6
  }
7
7
  /** Kv — key-value rows: label left, value right (Pay with · Time credits); children are KVRows. */
8
8
  export declare function Kv({ lined, className, ...rest }: KvProps): import("react").JSX.Element;
9
- export interface KvRowProps extends ComponentPropsWithoutRef<'div'> {
9
+ export interface KvRowProps extends ComponentProps<'div'> {
10
10
  /** the key */
11
11
  label: ReactNode;
12
12
  /** the value */
@@ -14,11 +14,11 @@ export interface KvRowProps extends ComponentPropsWithoutRef<'div'> {
14
14
  }
15
15
  /** KvRow — one key · value line (48px tall); the value may hold a Chip, an Avatar, an Icon. */
16
16
  export declare function KvRow({ label, className, children, ...rest }: KvRowProps): import("react").JSX.Element;
17
- export interface KvGridProps extends ComponentPropsWithoutRef<'div'> {
17
+ export interface KvGridProps extends ComponentProps<'div'> {
18
18
  }
19
19
  /** KvGrid — value-over-label cells split by a rule (Sex | Weight); children are KVCells. */
20
20
  export declare function KvGrid({ className, ...rest }: KvGridProps): import("react").JSX.Element;
21
- export interface KvCellProps extends ComponentPropsWithoutRef<'div'> {
21
+ export interface KvCellProps extends ComponentProps<'div'> {
22
22
  icon?: IconName;
23
23
  value: ReactNode;
24
24
  label: ReactNode;
@@ -1,9 +1,9 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface LegendProps extends ComponentPropsWithoutRef<'div'> {
1
+ import type { ComponentProps } from 'react';
2
+ export interface LegendProps extends ComponentProps<'div'> {
3
3
  }
4
4
  /** Legend — signal dots with captions under a chart; children are LegendItems. */
5
5
  export declare function Legend({ className, ...rest }: LegendProps): import("react").JSX.Element;
6
- export interface LegendItemProps extends ComponentPropsWithoutRef<'span'> {
6
+ export interface LegendItemProps extends ComponentProps<'span'> {
7
7
  /** which signal dot: `a` (default) or `b` */
8
8
  dot?: 'a' | 'b';
9
9
  }
@@ -1,6 +1,6 @@
1
- import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
2
  import { type IconName } from '../type/Icon.js';
3
- export interface RowProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
3
+ export interface RowProps extends Omit<ComponentProps<'div'>, 'title'> {
4
4
  /** the leading visual: an Avatar, an AvatarStack, an Icon */
5
5
  lead?: ReactNode;
6
6
  /** an icon in the outlined 48px lead box (menu rows) */
@@ -264,6 +264,7 @@ export declare function Row({ lead, leadIcon, title, meta, body, end, endValue,
264
264
  onTransitionRunCapture?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
265
265
  onTransitionStart?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
266
266
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
267
+ ref?: import("react").Ref<HTMLDivElement> | undefined;
267
268
  key?: import("react").Key | null | undefined;
268
269
  defaultChecked?: boolean | undefined | undefined;
269
270
  defaultValue?: string | number | readonly string[] | undefined;
@@ -310,8 +311,8 @@ export declare function Row({ lead, leadIcon, title, meta, body, end, endValue,
310
311
  exportparts?: string | undefined | undefined;
311
312
  href: string | undefined;
312
313
  className: string;
313
- }, HTMLElement>;
314
- export interface RowsProps extends ComponentPropsWithoutRef<'div'> {
314
+ }, HTMLDivElement>;
315
+ export interface RowsProps extends ComponentProps<'div'> {
315
316
  }
316
317
  /** Rows — a list of Rows separated by hairlines. */
317
318
  export declare function Rows({ className, ...rest }: RowsProps): import("react").JSX.Element;
@@ -1,9 +1,9 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface TimelineProps extends ComponentPropsWithoutRef<'ol'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface TimelineProps extends ComponentProps<'ol'> {
3
3
  }
4
4
  /** Timeline — a hairline with accent dots, date marks and entries to the right; children are TimelineItems. */
5
5
  export declare function Timeline({ className, ...rest }: TimelineProps): import("react").JSX.Element;
6
- export interface TimelineItemProps extends ComponentPropsWithoutRef<'li'> {
6
+ export interface TimelineItemProps extends ComponentProps<'li'> {
7
7
  /** the date chip ("Wed 14") */
8
8
  mark: ReactNode;
9
9
  }
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode, Ref } from 'react';
2
- export interface MapAreaProps extends ComponentPropsWithoutRef<'div'> {
1
+ import type { ComponentProps, ReactNode, Ref } from 'react';
2
+ export interface MapAreaProps extends ComponentProps<'div'> {
3
3
  /** the words over the map, under the bar's line */
4
4
  hint?: ReactNode;
5
5
  /** the tile provider's credit, bottom right (OSM requires it) — a caption, never the engine's own control */
@@ -1,10 +1,10 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface MosaicProps extends ComponentPropsWithoutRef<'div'> {
1
+ import type { ComponentProps } from 'react';
2
+ export interface MosaicProps extends ComponentProps<'div'> {
3
3
  label?: string;
4
4
  }
5
5
  /** Mosaic — one big photo + two small in a card (three Placeholders or imgs). */
6
6
  export declare function Mosaic({ label, className, ...rest }: MosaicProps): import("react").JSX.Element;
7
- export interface WaveProps extends ComponentPropsWithoutRef<'span'> {
7
+ export interface WaveProps extends ComponentProps<'span'> {
8
8
  label?: string;
9
9
  }
10
10
  /** Wave — the waveform stand-in for a voice note, filling a Row's body. */
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface PostcardProps extends ComponentPropsWithoutRef<'div'> {
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ export interface PostcardProps extends ComponentProps<'div'> {
3
3
  /** the photo on the front; none = the wireframe box */
4
4
  photo?: string | null;
5
5
  alt?: string;
@@ -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
- /** start turned over */
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 (cardds.js toggles `is-flipped`, `--motion-flip`).
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;
@@ -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 (cardds.js toggles `is-flipped`, `--motion-flip`).
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
- return (_jsxs("div", { className: cx('postcard', flipped && 'is-flipped', className), ...rest, children: [_jsxs("button", { className: "postcard__face postcard__front", type: "button", "aria-label": frontLabel, 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, children: [caption != null && _jsx("p", { className: "postcard__caption", children: caption }), _jsx("span", { className: "postcard__label", children: name })] })] }));
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
  }
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface QuoteProps extends ComponentPropsWithoutRef<'blockquote'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface QuoteProps extends ComponentProps<'blockquote'> {
3
3
  /** the words */
4
4
  text: ReactNode;
5
5
  /** the attribution */
@@ -1,13 +1,13 @@
1
- import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
2
  import { type IconName } from '../type/Icon.js';
3
- export interface TileGridProps extends ComponentPropsWithoutRef<'div'> {
3
+ export interface TileGridProps extends ComponentProps<'div'> {
4
4
  /** three across on a phone (attachments, upload kinds) */
5
5
  cols3?: boolean;
6
6
  bleed?: boolean;
7
7
  }
8
8
  /** TileGrid — big square Tiles in a responsive grid. */
9
9
  export declare function TileGrid({ cols3, bleed, className, ...rest }: TileGridProps): import("react").JSX.Element;
10
- export interface TileProps extends Omit<ComponentPropsWithoutRef<'div'>, 'onChange'> {
10
+ export interface TileProps extends Omit<ComponentProps<'div'>, 'onChange'> {
11
11
  /** the icon at the top */
12
12
  icon?: IconName;
13
13
  /** the label at the bottom */
@@ -272,6 +272,7 @@ export declare function Tile({ icon, label, value, sub, pick, type, name, checke
272
272
  onTransitionRunCapture?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
273
273
  onTransitionStart?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
274
274
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
275
+ ref?: import("react").Ref<HTMLDivElement> | undefined;
275
276
  key?: import("react").Key | null | undefined;
276
277
  defaultValue?: string | number | readonly string[] | undefined;
277
278
  suppressContentEditableWarning?: boolean | undefined | undefined;
@@ -317,4 +318,4 @@ export declare function Tile({ icon, label, value, sub, pick, type, name, checke
317
318
  exportparts?: string | undefined | undefined;
318
319
  type: string | undefined;
319
320
  className: string;
320
- }, HTMLElement>;
321
+ }, HTMLDivElement>;
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface BadgeProps extends ComponentPropsWithoutRef<'span'> {
1
+ import type { ComponentProps } from 'react';
2
+ export interface BadgeProps extends ComponentProps<'span'> {
3
3
  /** tinted instead of accent */
4
4
  soft?: boolean;
5
5
  }
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface BandProps extends ComponentPropsWithoutRef<'article'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface BandProps extends ComponentProps<'article'> {
3
3
  /** the two-line label (use a `<br />` to break it) */
4
4
  label: ReactNode;
5
5
  /** the giant number; wrap a unit in `<small>` */
@@ -10,7 +10,7 @@ export interface BandProps extends ComponentPropsWithoutRef<'article'> {
10
10
  }
11
11
  /** Band — a data card: 2-line label · giant number · footnote. Stack several in a BandStack for one boundary with hairlines. */
12
12
  export declare function Band({ label, num, foot, tone, className, ...rest }: BandProps): import("react").JSX.Element;
13
- export interface BandStackProps extends ComponentPropsWithoutRef<'div'> {
13
+ export interface BandStackProps extends ComponentProps<'div'> {
14
14
  }
15
15
  /** BandStack — several Bands as ONE card boundary, a hairline between each. */
16
16
  export declare function BandStack({ className, ...rest }: BandStackProps): import("react").JSX.Element;
@@ -1,11 +1,11 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface BarsProps extends ComponentPropsWithoutRef<'div'> {
1
+ import type { ComponentProps } from 'react';
2
+ export interface BarsProps extends ComponentProps<'div'> {
3
3
  /** the value of the tallest bar */
4
4
  max: number;
5
5
  }
6
6
  /** Bars — three or four bars with a count on top, the glance chart; children are Bar. */
7
7
  export declare function Bars({ max, className, style, ...rest }: BarsProps): import("react").JSX.Element;
8
- export interface BarProps extends ComponentPropsWithoutRef<'div'> {
8
+ export interface BarProps extends ComponentProps<'div'> {
9
9
  value: number;
10
10
  /** the highlighted bar (accent, its count in a pill) */
11
11
  on?: boolean;
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface DotGridProps extends ComponentPropsWithoutRef<'div'> {
1
+ import type { ComponentProps } from 'react';
2
+ export interface DotGridProps extends ComponentProps<'div'> {
3
3
  /** how many dots */
4
4
  total: number;
5
5
  /** how many are done (filled, from the top-left) */
@@ -1,13 +1,25 @@
1
- import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
- export interface PickerProps extends ComponentPropsWithoutRef<'div'> {
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ export interface PickerProps extends Omit<ComponentProps<'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
- /** Picker — a horizontal snap picker, the centre value big between two accent rules; children are PickerItems. */
8
- export declare function Picker({ unit, label, className, children, ...rest }: PickerProps): import("react").JSX.Element;
9
- export interface PickerItemProps extends ComponentPropsWithoutRef<'button'> {
10
- selected?: boolean;
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<ComponentProps<'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({ selected, className, ...rest }: PickerItemProps): import("react").JSX.Element;
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;
@@ -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
- /** Picker — a horizontal snap picker, the centre value big between two accent rules; children are PickerItems. */
5
- export function Picker({ unit, label, className, children, ...rest }) {
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
- useEffect(() => {
8
- // start on the selected value, as the demo page does: the CSS lights the item by scroll position
9
- const r = rail.current, on = r?.querySelector('[aria-selected="true"]');
10
- if (r && on)
11
- r.scrollLeft = on.offsetLeft - r.clientWidth / 3;
12
- }, []);
13
- return (_jsxs("div", { className: cx('picker', className), "aria-label": label, ...rest, children: [_jsx("div", { className: "picker__rail", role: "listbox", ref: rail, children: children }), unit != null && _jsx("span", { className: "picker__unit", children: unit })] }));
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({ selected, className, ...rest }) {
17
- return _jsx("button", { type: "button", role: "option", "aria-selected": selected ? 'true' : 'false', className: cx('picker__item', className), ...rest });
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
  }
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface RingProps extends ComponentPropsWithoutRef<'div'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface RingProps extends ComponentProps<'div'> {
3
3
  /** 0–100 */
4
4
  value: number;
5
5
  /** sm = an avatar-xl ring · xs = an inline counter in a head (2/3) */
@@ -1,10 +1,10 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface TrackProps extends ComponentPropsWithoutRef<'ol'> {
1
+ import type { ComponentProps } from 'react';
2
+ export interface TrackProps extends ComponentProps<'ol'> {
3
3
  label?: string;
4
4
  }
5
5
  /** Track — step progress in a dark pill: done · done · todo; children are TrackSteps holding an Icon each. */
6
6
  export declare function Track({ label, className, ...rest }: TrackProps): import("react").JSX.Element;
7
- export interface TrackStepProps extends ComponentPropsWithoutRef<'li'> {
7
+ export interface TrackStepProps extends ComponentProps<'li'> {
8
8
  /** this step (and the connector into it) is done */
9
9
  done?: boolean;
10
10
  }