@usableapp/cardds 0.7.16 → 0.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +23 -20
  3. package/cardds.css +3 -3
  4. package/css/actions.css +60 -38
  5. package/css/base.css +43 -17
  6. package/css/card.css +18 -13
  7. package/css/choice.css +21 -33
  8. package/css/forms.css +0 -3
  9. package/css/journey.css +0 -2
  10. package/css/layover.css +14 -24
  11. package/css/lists.css +1 -6
  12. package/css/media.css +7 -15
  13. package/css/numbers.css +5 -21
  14. package/css/people.css +1 -8
  15. package/css/sheet.css +12 -14
  16. package/css/stack.css +17 -21
  17. package/css/step.css +5 -8
  18. package/css/tokens.css +19 -4
  19. package/dist/actions/Btn.d.ts +1 -1
  20. package/dist/actions/Dropdown.js +1 -6
  21. package/dist/actions/Fab.d.ts +1 -1
  22. package/dist/actions/Fab.js +3 -2
  23. package/dist/cardds.css +228 -232
  24. package/dist/cards/Card.d.ts +23 -7
  25. package/dist/cards/Card.js +23 -8
  26. package/dist/cards/CardHead.d.ts +0 -1
  27. package/dist/cards/CardHead.js +1 -1
  28. package/dist/choice/Calendar.js +1 -2
  29. package/dist/choice/Mood.js +2 -2
  30. package/dist/forms/Field.js +2 -3
  31. package/dist/forms/FileBtn.js +1 -1
  32. package/dist/forms/Pin.js +6 -10
  33. package/dist/index.js +2 -2
  34. package/dist/layover/Banner.d.ts +10 -3
  35. package/dist/layover/Banner.js +6 -3
  36. package/dist/lists/Row.js +3 -3
  37. package/dist/media/Postcard.js +2 -2
  38. package/dist/numbers/Band.d.ts +3 -1
  39. package/dist/numbers/Picker.js +1 -7
  40. package/dist/scaffold/ActionBar.d.ts +3 -3
  41. package/dist/scaffold/ActionBar.js +2 -2
  42. package/dist/scaffold/Centre.d.ts +1 -1
  43. package/dist/scaffold/Centre.js +1 -1
  44. package/dist/scaffold/PagerAt.d.ts +1 -1
  45. package/dist/scaffold/PagerAt.js +1 -1
  46. package/dist/scaffold/Screen.js +2 -4
  47. package/dist/sheets/Drawer.js +3 -4
  48. package/dist/sheets/Sheet.js +8 -19
  49. package/dist/sheets/SheetStack.d.ts +2 -2
  50. package/dist/sheets/SheetStack.js +18 -10
  51. package/dist/sheets/SheetSteps.js +5 -7
  52. package/dist/sheets/StackBack.js +2 -1
  53. package/dist/sheets/StackSheet.d.ts +3 -2
  54. package/dist/sheets/StackSheet.js +6 -9
  55. package/dist/state.d.ts +19 -2
  56. package/dist/state.js +47 -7
  57. package/dist/templates/CallSheet.js +1 -2
  58. package/dist/templates/Home.d.ts +3 -2
  59. package/dist/templates/Home.js +1 -2
  60. package/dist/templates/Splash.js +2 -3
  61. package/dist/templates/Walkthrough.js +2 -2
  62. package/dist/type/Text.d.ts +1 -1
  63. package/package.json +7 -4
package/css/stack.css CHANGED
@@ -32,7 +32,7 @@
32
32
  bottom, the headroom above is the base's (block below).
33
33
  Direct children only, five at most (slots are :nth-child below):
34
34
  first = the back of the stack, last = the front. README "Sheet stack
35
- markup". cardds.js opens and closes a --tap stack; without it the
35
+ markup". The SheetStack component opens and closes a --tap stack; without it the
36
36
  stack still renders, it just won't open.
37
37
  ============================================================ */
38
38
 
@@ -45,14 +45,14 @@
45
45
  track is being sized), so a long sheet grew the track past the screen and
46
46
  every sheet's 100% then resolved against that — the bottom of every open
47
47
  sheet sat below the screen, unreachable (timebank home, 2026-09-13). */
48
- .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: 5.5rem; }
49
-
50
48
  /* the stack breaks out of the screen inset, edge to edge, always: the peek
51
49
  strips and the opened sheet span the screen's full width whatever the
52
50
  viewport (it used to bleed only under a 27em media query — a stack in a
53
51
  phone frame on a desktop kept the inset, and read as a narrower card;
54
- Lh, 2026-09-11) */
55
- .sheet-stack { margin-inline: calc(-1 * var(--screen-pad)); }
52
+ Lh, 2026-09-11). --_n: how many sheets the stack holds, read from the DOM
53
+ by :has below so the closed geometry needs no script and no per-stack
54
+ declaration. */
55
+ .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: 5.5rem; margin-inline: calc(-1 * var(--screen-pad)); --_n: 1; }
56
56
  .sheet-stack > .card {
57
57
  grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
58
58
  border-bottom-left-radius: 0;
@@ -69,9 +69,6 @@
69
69
  .sheet-stack > .card:not(:has(~ .card)) {
70
70
  align-self: stretch; /* the front card fills the remaining stack height */
71
71
  }
72
- /* --_n: how many sheets the stack holds, read from the DOM by :has so the
73
- closed geometry needs no script and no per-stack declaration */
74
- .sheet-stack { --_n: 1; }
75
72
  .sheet-stack:has(> .card:nth-child(2)) { --_n: 2; }
76
73
  .sheet-stack:has(> .card:nth-child(3)) { --_n: 3; }
77
74
  .sheet-stack:has(> .card:nth-child(4)) { --_n: 4; }
@@ -118,7 +115,6 @@
118
115
  /* fanned stack: a toggle opens the stack wider so every peek shows more */
119
116
  .sheet-stack--fanned { --stack-step: 9.5rem; }
120
117
 
121
- .sheet-stack > .card:has(~ .card) { cursor: pointer; }
122
118
  /* a peeked card in a plain stack hides its own body. A --tap stack does
123
119
  NOT: there, every card is full height and full content always, and a
124
120
  peek is simply the strip of it the next card doesn't cover — the way a
@@ -164,9 +160,10 @@
164
160
  by n steps (clipped by .screen--fill), so translating it up by exactly
165
161
  that much lands it filling the stack precisely. */
166
162
  overflow: clip;
167
- transition: translate 0.35s ease; /* the only animated property here */
163
+ transition: translate var(--motion-sheet) ease; /* the only animated property here */
168
164
  transition-delay: calc(var(--stagger, 0) * 70ms); /* 0 for everything
169
165
  except a staggered is-before card, set in JS as it tucks or un-tucks */
166
+ --_back-size: var(--stack-back-size, calc(var(--tap-base) * 2 / 3)); /* read once, shared by the back button and the head's padding below */
170
167
  }
171
168
  .sheet-stack--tap > .card.is-open {
172
169
  /* up by its own resting offset — pure movement, nothing resizes */
@@ -188,9 +185,8 @@
188
185
  overscroll-behavior: contain;
189
186
  }
190
187
  /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
191
- down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself */
192
- .sheet-stack--tap > .card { scrollbar-width: none; }
193
- .sheet-stack--tap > .card::-webkit-scrollbar { display: none; }
188
+ down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself
189
+ (the hiding rule is base.css's, shared with every other scrolling track) */
194
190
  .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
195
191
  .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
196
192
 
@@ -209,7 +205,7 @@
209
205
  lands flush at the top. is-before tucks one step below its resting place.
210
206
  ============================================================ */
211
207
  .sheet-stack--closed { --_headroom: calc(100% - var(--_n) * var(--stack-step)); }
212
- .sheet-stack--closed > .card { translate: 0 var(--_headroom); cursor: pointer; }
208
+ .sheet-stack--closed > .card { translate: 0 var(--_headroom); }
213
209
  .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
214
210
  .sheet-stack--closed > .card.is-open { translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step)); }
215
211
  .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
@@ -241,7 +237,7 @@
241
237
  .sheet-stack--tap .card__head {
242
238
  position: relative;
243
239
  padding-left: 0;
244
- transition: padding-left 0.35s ease;
240
+ transition: padding-left var(--motion-sheet) ease;
245
241
  }
246
242
  .sheet-stack--tap .sheet-stack__back {
247
243
  position: absolute;
@@ -250,10 +246,10 @@
250
246
  translate: 0 -50%;
251
247
  opacity: 0;
252
248
  visibility: hidden;
253
- transition: opacity 0.35s ease, visibility 0.35s;
249
+ transition: opacity var(--motion-sheet) ease, visibility var(--motion-sheet);
254
250
  /* SMALL (see "the back button, SMALL" below) */
255
- width: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
256
- height: var(--stack-back-size, calc(var(--tap-base) * 2 / 3));
251
+ width: var(--_back-size);
252
+ height: var(--_back-size);
257
253
  min-width: 0;
258
254
  min-height: 0;
259
255
  padding: 0;
@@ -315,13 +311,13 @@
315
311
  /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
316
312
  tappable"): a SMALL DISC — its ring kept, 32px instead of 44, the chevron in it at 20 — so the head gives it 32px + a gap
317
313
  (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
318
- .sheet-stack--tap > .card.is-open > .card__head { padding-left: var(--stack-back-room, calc(var(--stack-back-size, calc(var(--tap-base) * 2 / 3)) + var(--sp-2))); }
314
+ .sheet-stack--tap > .card.is-open > .card__head { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
319
315
  .sheet-stack--tap .sheet-stack__back > .icon { width: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); height: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); flex: none; } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
320
- .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--stack-back-size, calc(var(--tap-base) * 2 / 3)) - var(--tap-sm)) / 2); }
316
+ .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
321
317
 
322
318
  /* the open sheet's title folds it (SheetStack.tsx): it says so to a pointer */
323
319
  .sheet-stack--tap > .card.is-open > .card__head > :is(h1, h2, h3) { cursor: pointer; }
324
320
  /* a stacked sheet is a control until it is open — tapped, never read into: no text selection (a drag, a double tap, a long press),
325
321
  no grey flash, no iOS callout; its head stays one when open. Only the OPEN sheet's body is text to select (Lh 2026-09-23) */
326
- .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; }
322
+ .sheet-stack--tap > .card, .sheet-stack > .card > .card__head { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
327
323
  .sheet-stack--tap > .card.is-open > :not(.card__head) { -webkit-user-select: var(--screen-select, none); user-select: var(--screen-select, none); } /* the open sheet reads as the rest of the screen: not selectable (Lh 2026-09-25; it was text) */
package/css/step.css CHANGED
@@ -59,9 +59,6 @@
59
59
  more air and a short one less; a screen with a bar keeps the bar's own room */
60
60
  .screen > .step:first-child, .screen--lead > .step { --_lead: var(--screen-lead, 15cqh); } /* top="lead" says it; a Step first says it too */
61
61
 
62
- /* a walk (the dots float on its foot): the reading block runs right to the bar's top edge — the reserve's breathing gap (--sp-6)
63
- is given back, the dots keep the words clear of the bar (Lh 2026-09-23) */
64
-
65
62
  /* LAYERS (Lh, 2026-09-20): the base, then the sheet over it, then the top bar. A bar is the BASE's — the
66
63
  move of a page of words — so a step with a sheet has NO bar: the sheet finishes on its own .card__foot
67
64
  (sheet.css), and a form lives whole in ONE sheet, never item by item on the base. That is how cardds's
@@ -81,8 +78,7 @@
81
78
  --card-gap: var(--sp-4);
82
79
  gap: var(--card-gap);
83
80
  padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
84
- align-content: start;
85
- color: var(--on-surface);
81
+ color: var(--on-surface); /* align-content: start is .page's own (base.css) — not restated here */
86
82
  }
87
83
  /* the reading block is the .page — or the .pages that holds several of them, a walk of words
88
84
  (card.css). Behind a PEEKED sheet the words keep the strip's room under them, so whatever is down there
@@ -91,22 +87,23 @@
91
87
  .step > :is(.page, .pages) {
92
88
  flex: 0 1 auto;
93
89
  min-height: 0;
94
- margin: var(--_lead, 0) var(--screen-pad) 0; /* the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
95
90
  overflow-y: auto;
96
91
  overscroll-behavior: contain;
97
92
  }
93
+ /* a lone .page carries the margin a walk's .pages does not (its own .page children do, below) —
94
+ the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
95
+ .step > .page { margin: var(--_lead, 0) var(--screen-pad) 0; }
98
96
 
99
97
  /* a WALK (Pages of Page) — Lh 2026-09-23, third call: no top; the picture and the words are ONE run that scrolls up together
100
98
  (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
101
99
  padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
102
100
  The walk shows no scrollbar. */
103
- .step > .pages { margin: 0; scrollbar-width: none; touch-action: pan-y; } /* pan-y: the walk scrolls itself up and down; a sideways swipe is the Walkthrough's (it turns the page) */ /* the scrolling box is the whole content: from the step's top edge, edge to edge — no margin (Lh 2026-09-23) */
101
+ .step > .pages { touch-action: pan-y; } /* pan-y: the walk scrolls itself up and down; a sideways swipe is the Walkthrough's (it turns the page) */ /* the scrolling box is the whole content: from the step's top edge, edge to edge — no margin, the default (Lh 2026-09-23) */
104
102
  .step > .pages > .page {
105
103
  padding-inline: var(--screen-pad); /* the words keep the inset INSIDE it */
106
104
  margin-top: var(--_lead, 0rem); /* the lead is each PAGE's margin (15% with no top, the screen's gap under a bar): the words start there and scroll up to the box's edge */
107
105
  padding-bottom: var(--bottom-h, 0rem); /* exactly the bottom layer (measured by Screen): a page scrolls ONLY when the layer would cover its words, and then just far enough to clear them — at 100% nothing scrolls (Lh 2026-09-23) */
108
106
  }
109
- .step > .pages::-webkit-scrollbar { display: none; }
110
107
 
111
108
  /* ============================================================
112
109
  TOP + CONTENT in one flex column; BOTTOM is a LAYER over them (Lh 2026-09-23, second call — replaces "the bar is a row"):
package/css/tokens.css CHANGED
@@ -93,11 +93,26 @@
93
93
 
94
94
  /* ---- motion ---- */
95
95
  --motion-sheet: 0.35s;
96
+ --motion-quick: 0.2s; /* a small state flip that isn't a press: a chevron turning, a popover/banner opening */
96
97
  --motion-slide: 0.22s; /* a centred card sliding out and the next in (card.css) — quick, a page turn (Lh, 2026-09-12) */
97
98
  --motion-flip: 0.45s; /* a postcard turning over (media.css) — slow enough to read as a card in the hand */
98
99
  --dim: 0.7; /* secondary words dimmed on a ground whose muted ink does not fit (the dark card --3, a drawer, a tile's sub line): ONE share for all */
100
+ --rest: 0.6; /* an unpicked choice at rest: .day, .segment__item, .float-bar__item */
101
+ --faint: 0.3; /* further still: rails, far dots, unselected picker items */
99
102
  --motion-press: 0.12s; /* a control giving under the finger and springing back (actions.css PRESS) */
100
- --press-scale: 0.96; /* how far it gives — a share of its size */
103
+ --press-scale: 0.96; /* how far a finger-sized control gives — a share of its size */
104
+ --press-dim: 0.6; /* how far a card-wide one (a row, a summary line, a card) fades under the finger */
105
+ --press-shade: 0.93; /* how far something with a thing behind it (a peeked sheet, a postcard) darkens under the finger */
106
+ --off: 0.4; /* a disabled control's fade */
107
+
108
+ /* ---- z-index: the few layers that stack ABOVE ordinary content, low to high ---- */
109
+ --z-bar: 30; /* .action-bar, .float-bar — above .sheet-stack sheets (--level 1-5) */
110
+ --z-fab: 31; /* .fab — above the bars it floats near */
111
+ --z-top: 40; /* .appbar, a stage laid OVER its screen (.sheet-stage--over) — topmost, card animations never cover it */
112
+
113
+ /* ---- shared private sizes (not a knob: two components share the exact same
114
+ literal by design and must not drift apart) ---- */
115
+ --_cover-min-h: 22.5rem; /* .card--cover's min-height; --postcard-min-h falls back to it (media.css) */
101
116
 
102
117
  /* ---- typography: one scale, rem so it follows the root ---- */
103
118
  --font-ui: "PK Nonthaburi", -apple-system, system-ui, sans-serif; /* fonts.css ships it; the rest is the fallback while it loads */
@@ -212,7 +227,7 @@
212
227
 
213
228
  /* ============================================================
214
229
  REGISTERED TOKENS — the two sheet lengths are typed, so that
215
- cardds.js can read them back RESOLVED: getComputedStyle returns a
230
+ the Sheet component can read them back RESOLVED: getComputedStyle returns a
216
231
  registered <length> in px and a <number> as a number, where an
217
232
  unregistered custom property only ever reads back as its text
218
233
  ("calc(1.25rem + …)"). Drag-to-snap needs to know where the states
@@ -224,7 +239,7 @@
224
239
  falls back to when its own declaration cannot resolve.
225
240
  ============================================================ */
226
241
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
227
- @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; }
228
- @property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */ /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
242
+ @property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
243
+ @property --_lead { syntax: "<length>"; inherits: true; initial-value: 0px; } /* the room above a step's words: registered so a cqh share (a no-bar step, 15% of the SCREEN) resolves where it is declared — on the step, against the screen — not on the words, against the step */
229
244
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
230
245
  @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
@@ -19,6 +19,7 @@ export interface BtnProps extends Omit<ComponentProps<'button'>, 'form'> {
19
19
  * In a CardFoot up to four share the line; in an ActionBar it takes the width. An Icon xs may lead or trail the label.
20
20
  */
21
21
  export declare function Btn({ primary, quiet, block, form, xl, href, className, type, ...rest }: BtnProps): import("react").DetailedReactHTMLElement<{
22
+ onChange?: import("react").ChangeEventHandler<HTMLButtonElement, Element> | undefined;
22
23
  name?: string | undefined | undefined;
23
24
  slot?: string | undefined | undefined;
24
25
  style?: import("react").CSSProperties | undefined;
@@ -104,7 +105,6 @@ export declare function Btn({ primary, quiet, block, form, xl, href, className,
104
105
  onFocusCapture?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
105
106
  onBlur?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
106
107
  onBlurCapture?: import("react").FocusEventHandler<HTMLButtonElement> | undefined;
107
- onChange?: import("react").ChangeEventHandler<HTMLButtonElement, Element> | undefined;
108
108
  onChangeCapture?: import("react").ChangeEventHandler<HTMLButtonElement, Element> | undefined;
109
109
  onBeforeInput?: import("react").InputEventHandler<HTMLButtonElement> | undefined;
110
110
  onBeforeInputCapture?: import("react").InputEventHandler<HTMLButtonElement> | undefined;
@@ -6,12 +6,7 @@ import { Icon } from '../type/Icon.js';
6
6
  /* what a Menu tells its items: the value it holds (so the matching item marks itself current) and how to pick one */
7
7
  const MenuContext = createContext(null);
8
8
  /* a menu's value is controlled (`value` + `onChange`) or its own (`defaultValue`) — like an input */
9
- function useValue(value, defaultValue, onChange) {
10
- const [current, setOwn] = useControlled(value, defaultValue);
11
- const pick = (next) => { setOwn(next); if (next !== current)
12
- onChange?.(next); };
13
- return [current, pick];
14
- }
9
+ const useValue = (value, defaultValue, onChange) => useControlled(value, defaultValue, onChange);
15
10
  /**
16
11
  * Dropdown — THE select: a pill trigger + its Menu popover anchored under it (native popover: light dismiss, Esc). Never a native
17
12
  * `<select>`. ONE component, like `<select>` is — the pill and the panel are its own anatomy. It holds a VALUE, controlled like an
@@ -5,5 +5,5 @@ export interface FabProps extends ComponentProps<'button'> {
5
5
  icon?: IconName;
6
6
  invert?: boolean;
7
7
  }
8
- /** Fab — the square floating action at the bottom-right of a stage or screen (above the bar). */
8
+ /** Fab — the square floating action at the bottom-right of a stage or screen (above the bar): an IconBtn with the fab's place. */
9
9
  export declare function Fab({ label, icon, invert, className, children, ...rest }: FabProps): import("react").JSX.Element;
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  import { Icon } from '../type/Icon.js';
4
- /** Fab — the square floating action at the bottom-right of a stage or screen (above the bar). */
4
+ import { IconBtn } from './IconBtn.js';
5
+ /** Fab — the square floating action at the bottom-right of a stage or screen (above the bar): an IconBtn with the fab's place. */
5
6
  export function Fab({ label, icon = 'plus', invert = true, className, children, ...rest }) {
6
- return (_jsx("button", { type: "button", "aria-label": label, className: cx('icon-btn fab', invert && 'icon-btn--invert', className), ...rest, children: children ?? _jsx(Icon, { name: icon }) }));
7
+ return _jsx(IconBtn, { label: label, invert: invert, className: cx('fab', className), ...rest, children: children ?? _jsx(Icon, { name: icon }) });
7
8
  }