@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
@@ -1,6 +1,6 @@
1
- import { type ComponentPropsWithoutRef } from 'react';
1
+ import { type ComponentProps } from 'react';
2
2
  export type Palette = 'clay' | 'shine' | 'field' | 'roast' | 'hydro';
3
- export interface ScreenProps extends ComponentPropsWithoutRef<'div'> {
3
+ export interface ScreenProps extends ComponentProps<'div'> {
4
4
  /** exactly one phone height (100dvh), clipped: the box a Step or a SheetStack fills — an app screen, not a scrolling page */
5
5
  fill?: boolean;
6
6
  /** a colour preset from palette.css; omit for the black-and-white wireframe */
@@ -14,74 +14,119 @@ export interface ScreenProps extends ComponentPropsWithoutRef<'div'> {
14
14
  * Put an AppBar/TopBar first, the content next, and an ActionBar AFTER the screen (outside it).
15
15
  */
16
16
  export declare function Screen({ fill, palette, as, className, ...rest }: ScreenProps): import("react").DetailedReactHTMLElement<{
17
- slot?: string | undefined | undefined;
17
+ ref?: import("react").Ref<HTMLDivElement> | undefined;
18
+ key?: import("react").Key | null | undefined;
19
+ defaultChecked?: boolean | undefined;
20
+ defaultValue?: string | number | readonly string[] | undefined;
21
+ suppressContentEditableWarning?: boolean | undefined;
22
+ suppressHydrationWarning?: boolean | undefined;
23
+ accessKey?: string | undefined;
24
+ autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters" | undefined | (string & {});
25
+ autoFocus?: boolean | undefined;
26
+ contentEditable?: (boolean | "true" | "false") | "inherit" | "plaintext-only" | undefined;
27
+ contextMenu?: string | undefined;
28
+ dir?: string | undefined;
29
+ draggable?: (boolean | "true" | "false") | undefined;
30
+ enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined;
31
+ hidden?: boolean | undefined;
32
+ id?: string | undefined;
33
+ lang?: string | undefined;
34
+ nonce?: string | undefined;
35
+ slot?: string | undefined;
36
+ spellCheck?: (boolean | "true" | "false") | undefined;
18
37
  style?: import("react").CSSProperties | undefined;
19
- title?: string | undefined | undefined;
20
- suppressHydrationWarning?: boolean | undefined | undefined;
21
- color?: string | undefined | undefined;
22
- id?: string | undefined | undefined;
23
- lang?: string | undefined | undefined;
24
- nonce?: string | undefined | undefined;
25
- part?: string | undefined | undefined;
38
+ tabIndex?: number | undefined;
39
+ title?: string | undefined;
40
+ translate?: "yes" | "no" | undefined;
41
+ radioGroup?: string | undefined;
26
42
  role?: import("react").AriaRole | undefined;
27
- tabIndex?: number | undefined | undefined;
28
- "aria-activedescendant"?: string | undefined | undefined;
43
+ about?: string | undefined;
44
+ content?: string | undefined;
45
+ datatype?: string | undefined;
46
+ inlist?: any;
47
+ prefix?: string | undefined;
48
+ property?: string | undefined;
49
+ rel?: string | undefined;
50
+ resource?: string | undefined;
51
+ rev?: string | undefined;
52
+ typeof?: string | undefined;
53
+ vocab?: string | undefined;
54
+ autoCorrect?: string | undefined;
55
+ autoSave?: string | undefined;
56
+ color?: string | undefined;
57
+ itemProp?: string | undefined;
58
+ itemScope?: boolean | undefined;
59
+ itemType?: string | undefined;
60
+ itemID?: string | undefined;
61
+ itemRef?: string | undefined;
62
+ results?: number | undefined;
63
+ security?: string | undefined;
64
+ unselectable?: "on" | "off" | undefined;
65
+ popover?: "" | "auto" | "manual" | "hint" | undefined;
66
+ popoverTargetAction?: "toggle" | "show" | "hide" | undefined;
67
+ popoverTarget?: string | undefined;
68
+ inert?: boolean | undefined;
69
+ inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined;
70
+ is?: string | undefined;
71
+ exportparts?: string | undefined;
72
+ part?: string | undefined;
73
+ "aria-activedescendant"?: string | undefined;
29
74
  "aria-atomic"?: (boolean | "true" | "false") | undefined;
30
- "aria-autocomplete"?: "none" | "inline" | "list" | "both" | undefined | undefined;
31
- "aria-braillelabel"?: string | undefined | undefined;
32
- "aria-brailleroledescription"?: string | undefined | undefined;
75
+ "aria-autocomplete"?: "none" | "inline" | "list" | "both" | undefined;
76
+ "aria-braillelabel"?: string | undefined;
77
+ "aria-brailleroledescription"?: string | undefined;
33
78
  "aria-busy"?: (boolean | "true" | "false") | undefined;
34
- "aria-checked"?: boolean | "false" | "mixed" | "true" | undefined | undefined;
35
- "aria-colcount"?: number | undefined | undefined;
36
- "aria-colindex"?: number | undefined | undefined;
37
- "aria-colindextext"?: string | undefined | undefined;
38
- "aria-colspan"?: number | undefined | undefined;
39
- "aria-controls"?: string | undefined | undefined;
40
- "aria-current"?: boolean | "false" | "true" | "page" | "step" | "location" | "date" | "time" | undefined | undefined;
41
- "aria-describedby"?: string | undefined | undefined;
42
- "aria-description"?: string | undefined | undefined;
43
- "aria-details"?: string | undefined | undefined;
79
+ "aria-checked"?: boolean | "false" | "mixed" | "true" | undefined;
80
+ "aria-colcount"?: number | undefined;
81
+ "aria-colindex"?: number | undefined;
82
+ "aria-colindextext"?: string | undefined;
83
+ "aria-colspan"?: number | undefined;
84
+ "aria-controls"?: string | undefined;
85
+ "aria-current"?: boolean | "false" | "true" | "page" | "step" | "location" | "date" | "time" | undefined;
86
+ "aria-describedby"?: string | undefined;
87
+ "aria-description"?: string | undefined;
88
+ "aria-details"?: string | undefined;
44
89
  "aria-disabled"?: (boolean | "true" | "false") | undefined;
45
- "aria-dropeffect"?: "none" | "copy" | "execute" | "link" | "move" | "popup" | undefined | undefined;
46
- "aria-errormessage"?: string | undefined | undefined;
90
+ "aria-dropeffect"?: "none" | "copy" | "execute" | "link" | "move" | "popup" | undefined;
91
+ "aria-errormessage"?: string | undefined;
47
92
  "aria-expanded"?: (boolean | "true" | "false") | undefined;
48
- "aria-flowto"?: string | undefined | undefined;
93
+ "aria-flowto"?: string | undefined;
49
94
  "aria-grabbed"?: (boolean | "true" | "false") | undefined;
50
- "aria-haspopup"?: boolean | "false" | "true" | "menu" | "listbox" | "tree" | "grid" | "dialog" | undefined | undefined;
95
+ "aria-haspopup"?: boolean | "false" | "true" | "menu" | "listbox" | "tree" | "grid" | "dialog" | undefined;
51
96
  "aria-hidden"?: (boolean | "true" | "false") | undefined;
52
- "aria-invalid"?: boolean | "false" | "true" | "grammar" | "spelling" | undefined | undefined;
53
- "aria-keyshortcuts"?: string | undefined | undefined;
54
- "aria-label"?: string | undefined | undefined;
55
- "aria-labelledby"?: string | undefined | undefined;
56
- "aria-level"?: number | undefined | undefined;
57
- "aria-live"?: "off" | "assertive" | "polite" | undefined | undefined;
97
+ "aria-invalid"?: boolean | "false" | "true" | "grammar" | "spelling" | undefined;
98
+ "aria-keyshortcuts"?: string | undefined;
99
+ "aria-label"?: string | undefined;
100
+ "aria-labelledby"?: string | undefined;
101
+ "aria-level"?: number | undefined;
102
+ "aria-live"?: "off" | "assertive" | "polite" | undefined;
58
103
  "aria-modal"?: (boolean | "true" | "false") | undefined;
59
104
  "aria-multiline"?: (boolean | "true" | "false") | undefined;
60
105
  "aria-multiselectable"?: (boolean | "true" | "false") | undefined;
61
- "aria-orientation"?: "horizontal" | "vertical" | undefined | undefined;
62
- "aria-owns"?: string | undefined | undefined;
63
- "aria-placeholder"?: string | undefined | undefined;
64
- "aria-posinset"?: number | undefined | undefined;
65
- "aria-pressed"?: boolean | "false" | "mixed" | "true" | undefined | undefined;
106
+ "aria-orientation"?: "horizontal" | "vertical" | undefined;
107
+ "aria-owns"?: string | undefined;
108
+ "aria-placeholder"?: string | undefined;
109
+ "aria-posinset"?: number | undefined;
110
+ "aria-pressed"?: boolean | "false" | "mixed" | "true" | undefined;
66
111
  "aria-readonly"?: (boolean | "true" | "false") | undefined;
67
- "aria-relevant"?: "additions" | "additions removals" | "additions text" | "all" | "removals" | "removals additions" | "removals text" | "text" | "text additions" | "text removals" | undefined | undefined;
112
+ "aria-relevant"?: "additions" | "additions removals" | "additions text" | "all" | "removals" | "removals additions" | "removals text" | "text" | "text additions" | "text removals" | undefined;
68
113
  "aria-required"?: (boolean | "true" | "false") | undefined;
69
- "aria-roledescription"?: string | undefined | undefined;
70
- "aria-rowcount"?: number | undefined | undefined;
71
- "aria-rowindex"?: number | undefined | undefined;
72
- "aria-rowindextext"?: string | undefined | undefined;
73
- "aria-rowspan"?: number | undefined | undefined;
114
+ "aria-roledescription"?: string | undefined;
115
+ "aria-rowcount"?: number | undefined;
116
+ "aria-rowindex"?: number | undefined;
117
+ "aria-rowindextext"?: string | undefined;
118
+ "aria-rowspan"?: number | undefined;
74
119
  "aria-selected"?: (boolean | "true" | "false") | undefined;
75
- "aria-setsize"?: number | undefined | undefined;
76
- "aria-sort"?: "none" | "ascending" | "descending" | "other" | undefined | undefined;
77
- "aria-valuemax"?: number | undefined | undefined;
78
- "aria-valuemin"?: number | undefined | undefined;
79
- "aria-valuenow"?: number | undefined | undefined;
80
- "aria-valuetext"?: string | undefined | undefined;
81
- children?: import("react").ReactNode;
120
+ "aria-setsize"?: number | undefined;
121
+ "aria-sort"?: "none" | "ascending" | "descending" | "other" | undefined;
122
+ "aria-valuemax"?: number | undefined;
123
+ "aria-valuemin"?: number | undefined;
124
+ "aria-valuenow"?: number | undefined;
125
+ "aria-valuetext"?: string | undefined;
126
+ children?: import("react").ReactNode | undefined;
82
127
  dangerouslySetInnerHTML?: {
83
128
  __html: string | TrustedHTML;
84
- } | undefined | undefined;
129
+ } | undefined;
85
130
  onCopy?: import("react").ClipboardEventHandler<HTMLDivElement> | undefined;
86
131
  onCopyCapture?: import("react").ClipboardEventHandler<HTMLDivElement> | undefined;
87
132
  onCut?: import("react").ClipboardEventHandler<HTMLDivElement> | undefined;
@@ -250,50 +295,6 @@ export declare function Screen({ fill, palette, as, className, ...rest }: Screen
250
295
  onTransitionRunCapture?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
251
296
  onTransitionStart?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
252
297
  onTransitionStartCapture?: import("react").TransitionEventHandler<HTMLDivElement> | undefined;
253
- key?: import("react").Key | null | undefined;
254
- defaultChecked?: boolean | undefined | undefined;
255
- defaultValue?: string | number | readonly string[] | undefined;
256
- suppressContentEditableWarning?: boolean | undefined | undefined;
257
- accessKey?: string | undefined | undefined;
258
- autoCapitalize?: "off" | "none" | "on" | "sentences" | "words" | "characters" | undefined | (string & {}) | undefined;
259
- autoFocus?: boolean | undefined | undefined;
260
- contentEditable?: (boolean | "true" | "false") | "inherit" | "plaintext-only" | undefined;
261
- contextMenu?: string | undefined | undefined;
262
- dir?: string | undefined | undefined;
263
- draggable?: (boolean | "true" | "false") | undefined;
264
- enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
265
- hidden?: boolean | undefined | undefined;
266
- spellCheck?: (boolean | "true" | "false") | undefined;
267
- translate?: "yes" | "no" | undefined | undefined;
268
- radioGroup?: string | undefined | undefined;
269
- about?: string | undefined | undefined;
270
- content?: string | undefined | undefined;
271
- datatype?: string | undefined | undefined;
272
- inlist?: any;
273
- prefix?: string | undefined | undefined;
274
- property?: string | undefined | undefined;
275
- rel?: string | undefined | undefined;
276
- resource?: string | undefined | undefined;
277
- rev?: string | undefined | undefined;
278
- typeof?: string | undefined | undefined;
279
- vocab?: string | undefined | undefined;
280
- autoCorrect?: string | undefined | undefined;
281
- autoSave?: string | undefined | undefined;
282
- itemProp?: string | undefined | undefined;
283
- itemScope?: boolean | undefined | undefined;
284
- itemType?: string | undefined | undefined;
285
- itemID?: string | undefined | undefined;
286
- itemRef?: string | undefined | undefined;
287
- results?: number | undefined | undefined;
288
- security?: string | undefined | undefined;
289
- unselectable?: "on" | "off" | undefined | undefined;
290
- popover?: "" | "auto" | "manual" | "hint" | undefined | undefined;
291
- popoverTargetAction?: "toggle" | "show" | "hide" | undefined | undefined;
292
- popoverTarget?: string | undefined | undefined;
293
- inert?: boolean | undefined | undefined;
294
- inputMode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search" | undefined | undefined;
295
- is?: string | undefined | undefined;
296
- exportparts?: string | undefined | undefined;
297
298
  className: string;
298
299
  'data-palette': Palette | undefined;
299
- }, HTMLElement>;
300
+ }, HTMLDivElement>;
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface TopBarProps extends Omit<ComponentPropsWithoutRef<'header'>, 'title'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface TopBarProps extends Omit<ComponentProps<'header'>, 'title'> {
3
3
  /** the screen title (uppercase, ellipsised on one line) */
4
4
  title: ReactNode;
5
5
  /** a leading control — usually `<IconBtn sm icon="arrow-left" label="Back" />` */
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface CardBackProps extends ComponentPropsWithoutRef<'button'> {
1
+ import type { ComponentProps } from 'react';
2
+ export interface CardBackProps extends ComponentProps<'button'> {
3
3
  /** accessible name */
4
4
  label?: string;
5
5
  }
@@ -1,21 +1,32 @@
1
- import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
- export interface ModalProps extends ComponentPropsWithoutRef<'article'> {
1
+ import { type ComponentProps, type ReactNode } from 'react';
2
+ export interface ModalProps extends ComponentProps<'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
- /** Modal — the ask: ONE card at the centre of a `SheetStage ask`, two parts — a `Lift` (the thing being asked about, light, scrolls when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above to the centre · the drawer comes out below while the lift moves up. A click on any button in the Drawer plays them backwards and fires `cardds:modal` on the stage (`detail.answer` = the button's `data-answer`) for the app to remove it. */
5
- export declare function Modal({ className, ...rest }: ModalProps): import("react").JSX.Element;
6
- export interface LiftProps extends ComponentPropsWithoutRef<'div'> {
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;
17
+ export interface LiftProps extends ComponentProps<'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. */
9
20
  export declare function Lift({ className, ...rest }: LiftProps): import("react").JSX.Element;
10
- export interface DrawerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
21
+ export interface DrawerProps extends Omit<ComponentProps<'div'>, 'title'> {
11
22
  /** the question */
12
23
  title: ReactNode;
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); give each button a `data-answer` — any of them closes the modal. */
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
- export interface BtnTrioProps extends ComponentPropsWithoutRef<'div'> {
29
+ export interface BtnTrioProps extends ComponentProps<'div'> {
19
30
  }
20
31
  /** BtnTrio — three round buttons with dotted connectors between them (× · ? · ✓); the middle one inverted. */
21
32
  export declare function BtnTrio({ className, children, ...rest }: BtnTrioProps): import("react").JSX.Element;
@@ -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
- /** Modal — the ask: ONE card at the centre of a `SheetStage ask`, two parts — a `Lift` (the thing being asked about, light, scrolls when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above to the centre · the drawer comes out below while the lift moves up. A click on any button in the Drawer plays them backwards and fires `cardds:modal` on the stage (`detail.answer` = the button's `data-answer`) for the app to remove it. */
5
- export function Modal({ className, ...rest }) {
6
- return _jsx("article", { className: cx('card modal', className), ...rest });
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); give each button a `data-answer` — any of them closes the modal. */
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
  }
@@ -1,24 +1,37 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
1
+ import type { ComponentProps } 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
- export interface SheetProps extends ComponentPropsWithoutRef<'article'> {
4
- /** how far the sheet slid: `peek` (handle + title row) · `half` (default) · `3q` (three quarters showing) · `full` (its top edge at the top bar) — a class, never a height. `away` = a CALLED sheet not on the screen yet (slid out, inert): a `Btn aria-controls={its id}` in the Step's BaseContent calls it up to full and sends any other away. A tap on the handle toggles between this state and peek; a drag snaps to any of `states` */
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<ComponentProps<'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 (tap / drag / keyboard via cardds.js); `false` for a sheet with no handle, a string sets its accessible name */
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 (controls, or small `Card sm`
15
- * content cards). Always the full height of its box — a Step or a SheetStage — a state is only how far it slid.
16
- * Shape: handle · head (a CardHead, a SheetHead — or ONE label Row carrying a Check/Toggle, when the sheet's
17
- * whole ask is that switch and peek must show it) · SheetBody (the only part that scrolls). Written top-down.
18
- * It FINISHES on itself: a `CardFoot` as its last child holds the one button that completes it — standing at the bottom at
19
- * half / 3q / full, gone at peek. Never an ActionBar in front of a sheet (a bar is the base's). Nothing in the body commits
20
- * on its own. Older note — its own buttons only on a
21
- * screen with no bar (the map sheet). Called sheets: several `Sheet state="away" states="peek full"` in one Step, each with an `id` a
22
- * button names — one on the screen at a time; never beside a pager. This is the single sheet; the menu of stacked sheets is `SheetStack`.
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, raised, handle, states, className, children, ...rest }: SheetProps): import("react").JSX.Element;
36
+ export declare function Sheet({ state, defaultState, onStateChange, states, raised, handle, className, children, onFocus, ...rest }: SheetProps): import("react").JSX.Element;
37
+ export {};
@@ -1,16 +1,162 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
3
  import { cx } from '../cx.js';
4
+ const ORDER = ['peek', 'half', '3q', 'full']; // low → high; `away` is below them all and outside them
5
+ const TAP_REM = 0.5; // = --sp-2: a move shorter than this is a tap, not a drag
3
6
  /**
4
- * Sheet — the SINGLE sheet: one card as a bottom sheet holding what the member ACTS on (controls, or small `Card sm`
5
- * content cards). Always the full height of its box — a Step or a SheetStage — a state is only how far it slid.
6
- * Shape: handle · head (a CardHead, a SheetHead — or ONE label Row carrying a Check/Toggle, when the sheet's
7
- * whole ask is that switch and peek must show it) · SheetBody (the only part that scrolls). Written top-down.
8
- * It FINISHES on itself: a `CardFoot` as its last child holds the one button that completes it — standing at the bottom at
9
- * half / 3q / full, gone at peek. Never an ActionBar in front of a sheet (a bar is the base's). Nothing in the body commits
10
- * on its own. Older note — its own buttons only on a
11
- * screen with no bar (the map sheet). Called sheets: several `Sheet state="away" states="peek full"` in one Step, each with an `id` a
12
- * button names — one on the screen at a time; never beside a pager. This is the single sheet; the menu of stacked sheets is `SheetStack`.
7
+ * Sheet — the SINGLE sheet: one card as a bottom sheet holding what the member ACTS on. Always the full height of its
8
+ * box — a Step or a SheetStage — a state is only how far it slid (translate; the material never changes size).
9
+ * ONE SHAPE, written top-down: handle · head (a CardHead / SheetHead — pinned, never scrolls) · SheetBody (scrolls, as far as
10
+ * it needs) · CardFoot (the button that finishes the sheet — at the bottom, above the keyboard; gone at peek).
11
+ *
12
+ * It is a COMPONENT, not a wiring: it knows nothing of what opens it. The app owns `state` and sets it from whatever it
13
+ * likes — a button, a route, a timer: `<Btn onClick={() => setSheet('full')}>` … `<Sheet state={sheet} onStateChange={setSheet}>`.
14
+ * Its own behaviour stays inside it and only ever ASKS (`onStateChange`): a tap on the handle toggles its own state ↔ peek,
15
+ * a drag snaps to the nearest of `states`, focus in a peeked sheet raises it, and when the cut moves (the keyboard) it
16
+ * climbs until the focused control has room above the foot.
13
17
  */
14
- export function Sheet({ state = 'half', raised, handle = true, states, className, children, ...rest }) {
15
- return (_jsxs("article", { className: cx('card sheet', `sheet--${state}`, raised && 'sheet--raised', className), "data-sheet-states": states, inert: state === 'away' || undefined, ...rest, children: [handle !== false && _jsx("button", { className: "sheet__handle", type: "button", "aria-label": typeof handle === 'string' ? handle : 'Resize' }), children] }));
18
+ export function Sheet({ state, defaultState = 'half', onStateChange, states, raised, handle = true, className, children, onFocus, ...rest }) {
19
+ const [inner, setInner] = useState(defaultState);
20
+ const current = state ?? inner;
21
+ const controlled = state !== undefined;
22
+ const ref = useRef(null);
23
+ const handleRef = useRef(null);
24
+ const home = useRef(current !== 'peek' && current !== 'away' ? current : 'half'); // where a tap from peek returns to
25
+ const was = useRef(current);
26
+ const skipClick = useRef(false);
27
+ const drag = useRef(null);
28
+ const allowed = useCallback(() => {
29
+ const list = (typeof states === 'string' ? states.split(/\s+/) : [...(states ?? [])]).filter((s) => ORDER.includes(s));
30
+ return list.length ? ORDER.filter((s) => list.includes(s)) : [...ORDER];
31
+ }, [states]);
32
+ const request = useCallback((next, reason) => {
33
+ if (next === current)
34
+ return;
35
+ if (!controlled)
36
+ setInner(next);
37
+ onStateChange?.(next, reason);
38
+ }, [current, controlled, onStateChange]);
39
+ useEffect(() => {
40
+ if (current !== 'peek' && current !== 'away')
41
+ home.current = current;
42
+ if (was.current === 'away' && current !== 'away')
43
+ handleRef.current?.focus({ preventScroll: true }); // it just came onto the screen: the keyboard user is on it
44
+ was.current = current;
45
+ }, [current]);
46
+ /* geometry, read once per gesture — never stored: how far it has slid, and where each state rests, in px from its top at full */
47
+ const slid = (el) => {
48
+ const box = (el.offsetParent ?? el.parentElement);
49
+ return el.getBoundingClientRect().top - (box.getBoundingClientRect().top + box.clientTop + el.offsetTop);
50
+ };
51
+ const stopsOf = (el) => {
52
+ const cs = getComputedStyle(el), height = el.getBoundingClientRect().height;
53
+ return {
54
+ full: 0,
55
+ half: height * (parseFloat(cs.getPropertyValue('--sheet-half')) || 0),
56
+ '3q': height * (parseFloat(cs.getPropertyValue('--sheet-3q')) || 0),
57
+ peek: height - (parseFloat(cs.getPropertyValue('--sheet-peek')) || 0), // registered in tokens.css: reads back resolved
58
+ };
59
+ };
60
+ /* bring a control into the part of the body that shows — between the pinned head and the foot's strip on the cut. The sheet asks
61
+ for the lowest of its states, from the current one (or `atLeast`) upward, that has room; if none has, its highest. */
62
+ const reveal = useCallback((target, reason, atLeast) => {
63
+ const el = ref.current;
64
+ if (!el || !target || !el.contains(target) || target.closest('.sheet__handle') || current === 'away')
65
+ return;
66
+ const control = target.closest('.field') ?? target; // the control's box, not the bare input inside it
67
+ const list = allowed(), stops = stopsOf(el);
68
+ const from = Math.max(list.indexOf(current), atLeast ? list.indexOf(atLeast) : -1, 0);
69
+ const rect = el.getBoundingClientRect();
70
+ const restTop = rect.top - slid(el);
71
+ const bodyTop = (el.querySelector(':scope > .sheet__body') ?? el).getBoundingClientRect().top - rect.top;
72
+ const box = el.parentElement.getBoundingClientRect(); // what clips it: the Step or the SheetStage
73
+ const vv = window.visualViewport;
74
+ const edge = Math.min(box.bottom, restTop + rect.height); // the sheet's own bottom edge at rest — above the box's on a stage that gave the keyboard its room
75
+ const cut = Math.min(edge, vv ? vv.offsetTop + vv.height : Infinity);
76
+ const button = el.querySelector(':scope > .card__foot')?.firstElementChild; // the foot's button stands on that edge at every state: the control has to clear its strip too
77
+ const need = control.getBoundingClientRect().height + (button ? edge - button.getBoundingClientRect().top : 0);
78
+ let to = list[list.length - 1];
79
+ for (let i = from; i < list.length; i++)
80
+ if (cut - (restTop + stops[list[i]] + bodyTop) >= need) {
81
+ to = list[i];
82
+ break;
83
+ }
84
+ request(to, reason);
85
+ requestAnimationFrame(() => control.scrollIntoView({ block: 'nearest' })); // after the state (and its scroll-padding) landed
86
+ }, [allowed, current, request]);
87
+ /* the cut moved — the keyboard came up (the Step shrank by --kb, or the layout viewport did), the phone turned: if a control
88
+ in this sheet has the focus, make sure it still shows */
89
+ useEffect(() => {
90
+ const el = ref.current;
91
+ if (!el || typeof ResizeObserver === 'undefined')
92
+ return;
93
+ let first = true;
94
+ const ro = new ResizeObserver(() => {
95
+ if (first) {
96
+ first = false;
97
+ return;
98
+ }
99
+ const at = document.activeElement;
100
+ if (at && el.contains(at))
101
+ reveal(at, 'keyboard');
102
+ });
103
+ ro.observe(el);
104
+ if (el.parentElement)
105
+ ro.observe(el.parentElement);
106
+ return () => ro.disconnect();
107
+ }, [reveal]);
108
+ const focus = (e) => {
109
+ onFocus?.(e);
110
+ if (current !== 'peek' || e.target.closest('.sheet__handle'))
111
+ return;
112
+ const body = ref.current?.querySelector(':scope > .sheet__body');
113
+ if (body)
114
+ body.scrollTop = 0; // the browser's own reveal scrolled it against the peek strip: start over
115
+ reveal(e.target, 'focus', home.current);
116
+ };
117
+ const tap = () => {
118
+ if (skipClick.current) {
119
+ skipClick.current = false;
120
+ return;
121
+ } // the click that follows a drag is not a tap
122
+ request(current === 'peek' ? (allowed().includes(home.current) ? home.current : allowed().find((s) => s !== 'peek') ?? 'half') : 'peek', 'tap');
123
+ };
124
+ const down = (e) => {
125
+ const el = ref.current;
126
+ if (!el || !e.isPrimary || e.button !== 0)
127
+ return;
128
+ const list = allowed(), stops = stopsOf(el), y = slid(el); // where it is now — mid-slide too, so a grab never jumps
129
+ drag.current = { startY: e.clientY, y0: y, y, stops, allowed: list, moved: false,
130
+ min: Math.min(...list.map((s) => stops[s])), max: Math.max(...list.map((s) => stops[s])),
131
+ tap: TAP_REM * parseFloat(getComputedStyle(document.documentElement).fontSize) };
132
+ e.currentTarget.setPointerCapture(e.pointerId);
133
+ };
134
+ const move = (e) => {
135
+ const d = drag.current, el = ref.current;
136
+ if (!d || !el || !e.isPrimary)
137
+ return;
138
+ const dy = e.clientY - d.startY;
139
+ if (!d.moved) {
140
+ if (Math.abs(dy) < d.tap)
141
+ return;
142
+ d.moved = true;
143
+ el.classList.add('is-dragging');
144
+ }
145
+ d.y = Math.min(Math.max(d.y0 + dy, d.min), d.max); // never above full, never below its lowest state
146
+ el.style.setProperty('--_drag', `${d.y - d.y0}px`); // the one thing written while held — the CSS adds it to the state's translate
147
+ };
148
+ const up = () => {
149
+ const d = drag.current, el = ref.current;
150
+ drag.current = null;
151
+ if (!d || !el)
152
+ return;
153
+ el.classList.remove('is-dragging');
154
+ if (!d.moved)
155
+ return; // a tap: the click that follows toggles
156
+ el.style.removeProperty('--_drag'); // same state, or the app says no: it eases back on its own
157
+ request(d.allowed.reduce((a, b) => (Math.abs(d.stops[b] - d.y) < Math.abs(d.stops[a] - d.y) ? b : a)), 'drag');
158
+ skipClick.current = true;
159
+ requestAnimationFrame(() => { skipClick.current = false; });
160
+ };
161
+ return (_jsxs("article", { ref: ref, className: cx('card sheet', `sheet--${current}`, raised && 'sheet--raised', className), inert: current === 'away' || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: "sheet__handle", type: "button", "aria-label": typeof handle === 'string' ? handle : 'Resize', onClick: tap, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up })), children] }));
16
162
  }
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface SheetBodyProps extends ComponentPropsWithoutRef<'div'> {
1
+ import type { ComponentProps } from 'react';
2
+ export interface SheetBodyProps extends ComponentProps<'div'> {
3
3
  }
4
4
  /** SheetBody — everything under a Sheet's head: the one thing that scrolls; its scroll padding keeps a focused control above the cut. Content never goes straight into the Sheet. */
5
5
  export declare function SheetBody({ className, ...rest }: SheetBodyProps): import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
- export interface SheetHeadProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ export interface SheetHeadProps extends Omit<ComponentProps<'div'>, 'title'> {
3
3
  /** the round button on the left (an IconBtn sm) */
4
4
  lead?: ReactNode;
5
5
  /** the centred title */