@usableapp/cardds 0.7.75 → 0.7.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  > For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
4
4
 
5
+ ## 0.7.76 — 2026-10-09
6
+
7
+ ### Changed
8
+ - **A card with one button is pressed whole** (Lh 2026-10-09) — a `Card` whose ONLY control is its foot's button (`foot` / `CardFoot`), or the end button of a `Card row`, now answers a touch anywhere on it: the button's own action runs once (its `onClick`, its `href`, its `popoverTarget`), and the whole card dims while the finger is down. Before → after: only the button could be pressed, the rest of the card did nothing → the heading, the words and the empty room press the button. Nothing to write in an app, and no prop turns it off. A card `onClick` of your own still runs once per press. Unchanged: a card with two or more controls (a second button, a field, a choice, a button in a card inside it), a card whose one button is in its head or in its content, a disabled button, a `Sheet`, a sheet of a `SheetStack`, a `Modal`, a card that is a link or a label. If a screen relied on a touch beside the lone foot button doing nothing, that touch now presses it. Story `Card · PressedWhole`.
9
+
10
+ - **Cards in a closed stack's base: room under the last one, and no shadow** (Lh 2026-10-09, from timebank's home) — loose things in `SheetStack base` / `Home base` rest against the heads. Before → after: the last one's bottom edge lay ON the first sheet's top edge → it keeps the same room under it as between two of them (20px at the defaults). A base that is one `Page` (reads from the top) or one `Centre` (stands in the middle) is unchanged. And a `Card` standing in the base is FLAT in a coloured theme (`data-palette`) and under `data-finish="lit"`: the base clips at its own edges, so a card's shadow ended in a straight line under the top bar and along the first sheet. Nothing to change in a project. New knob `--stack-base-gap` (the room between the base's things and under the last one; default the card gap, `--sp-5`). The room above the heads is shorter by that gap for loose content — on a short phone check what you put there still fits. Story `SheetStack · ClosedCards`.
11
+
5
12
  ## 0.7.75 — 2026-10-09
6
13
 
7
14
  ### Changed — ten names say what the component is for (BREAKING: the old names are gone)
package/README.md CHANGED
@@ -205,6 +205,10 @@ the card is the material, the sheet is what it is in the stack.
205
205
  front one too, peeks at the stack's bottom; the headroom above is the base's.
206
206
  An optional `<div class="sheet-stack__base">` — written AFTER the sheets, so
207
207
  it takes no sheet slot — is what the base shows there, behind every sheet.
208
+ Loose things in it rest against the heads, with the same room under the
209
+ last one as between two of them (`--stack-base-gap`); one `Page` reads from
210
+ the top, one `Centre` stands in the middle. A card standing there is flat:
211
+ the base clips at its edges, so a shadow would end in a straight line.
208
212
  A tap, or a pull up, opens any sheet over it. The open title or its back
209
213
  button, tapped or pulled down, drops it home. A pull down in the open
210
214
  content, already at the top, asks `onRefresh` — omit it and that pull only
@@ -476,6 +480,17 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
476
480
  `<Card heading caption end foot footLink>` with the content as children —
477
481
  the card writes the order (head first, foot last). A head of its own shape
478
482
  is a `<CardHead>` child instead; never both (Lh 2026-09-27).
483
+ - **A card that goes on is pressed whole** (Lh 2026-10-09). A card whose ONLY
484
+ control is its foot's button, or the end button of a row card, is pressed
485
+ anywhere: a touch off the button runs that button's own action once, and the
486
+ whole card dims under the finger (`.card--go`, the card-wide press answer of
487
+ a row or a card link). `Card` does it by itself — the app wires the button
488
+ and nothing else, and there is no prop to turn it off. The keyboard and a
489
+ screen reader meet the button only. Every other card is only read: two or
490
+ more controls anywhere in it (a field, a choice, a nested card's button),
491
+ the one button in the head or in the content, a disabled button, a sheet, a
492
+ stack's sheet, a modal. A card that is itself a link or a label was pressed
493
+ whole already. Story `Card · PressedWhole`.
479
494
  - A page with **one feature and one CTA** (an install page, a single
480
495
  decision) may be contained in one card: head, the pitch, foot.
481
496
  - **A bottom sheet holds what the member acts on** — controls, or small
package/css/actions.css CHANGED
@@ -361,7 +361,10 @@
361
361
  press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
362
362
  something as wide as its card (a row, a summary line, a card, a step bar, a peeked sheet) fades under the finger (--press-dim)
363
363
  instead, because a whole row shrinking reads as the card moving. A choice's fill fades in as it is picked. Disabled = no
364
- pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here. */
364
+ pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
365
+ .card--go (Lh 2026-10-09) = a card pressed whole: `Card` renders it while its ONE control is its foot's button or a row card's end
366
+ button (src/cards/cardGo.ts). It is in the DIM pointer and :active lists only — the focus ring and the disabled look stay the
367
+ button's, the card itself is never focused or disabled. */
365
368
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
366
369
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
367
370
  a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach) {
@@ -369,7 +372,7 @@
369
372
  touch-action: manipulation;
370
373
  transition: scale var(--motion-press) ease-out;
371
374
  }
372
- :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge) {
375
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge) {
373
376
  cursor: pointer;
374
377
  touch-action: manipulation;
375
378
  transition: opacity var(--motion-press) ease-out;
@@ -384,7 +387,7 @@
384
387
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
385
388
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
386
389
  a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
387
- :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
390
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
388
391
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
389
392
  /* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
390
393
  it darkens a touch instead (--press-shade) */
package/css/stack.css CHANGED
@@ -280,9 +280,10 @@
280
280
  z-index: 0;
281
281
  height: var(--_headroom, 0px);
282
282
  padding-inline: var(--screen-pad);
283
+ --_gap: var(--card-gap, var(--sp-5));
283
284
  display: grid;
284
285
  align-content: end;
285
- gap: var(--card-gap, var(--sp-5));
286
+ gap: var(--stack-base-gap, var(--_gap));
286
287
  overflow: hidden;
287
288
  overscroll-behavior: none;
288
289
  touch-action: none;
@@ -295,6 +296,12 @@
295
296
  (Lh 2026-10-06). The base already keeps the screen's inset, so the centre block gives its own back. */
296
297
  .sheet-stack__base:has(> .screen__centre:only-child) { align-content: center; }
297
298
  .sheet-stack__base > .screen__centre { padding-inline: 0; }
299
+ /* loose things rest against the peeks, so the room under the last one is the room between two of them (Lh 2026-10-09: a card's
300
+ bottom edge lay on the first sheet's top edge). A lone Page reads from the top and a lone Centre stands in the middle: neither rests there. */
301
+ .sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) { padding-bottom: var(--stack-base-gap, var(--_gap)); }
302
+ /* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
303
+ line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
304
+ :is([data-palette], [data-finish="lit"]) .sheet-stack__base .card { box-shadow: none; }
298
305
 
299
306
  /* ============================================================
300
307
  .sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
package/dist/cardds.css CHANGED
@@ -1304,9 +1304,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1304
1304
  z-index: 0;
1305
1305
  height: var(--_headroom, 0px);
1306
1306
  padding-inline: var(--screen-pad);
1307
+ --_gap: var(--card-gap, var(--sp-5));
1307
1308
  display: grid;
1308
1309
  align-content: end;
1309
- gap: var(--card-gap, var(--sp-5));
1310
+ gap: var(--stack-base-gap, var(--_gap));
1310
1311
  overflow: hidden;
1311
1312
  overscroll-behavior: none;
1312
1313
  touch-action: none;
@@ -1319,6 +1320,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1319
1320
  (Lh 2026-10-06). The base already keeps the screen's inset, so the centre block gives its own back. */
1320
1321
  .sheet-stack__base:has(> .screen__centre:only-child) { align-content: center; }
1321
1322
  .sheet-stack__base > .screen__centre { padding-inline: 0; }
1323
+ /* loose things rest against the peeks, so the room under the last one is the room between two of them (Lh 2026-10-09: a card's
1324
+ bottom edge lay on the first sheet's top edge). A lone Page reads from the top and a lone Centre stands in the middle: neither rests there. */
1325
+ .sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) { padding-bottom: var(--stack-base-gap, var(--_gap)); }
1326
+ /* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
1327
+ line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
1328
+ :is([data-palette], [data-finish="lit"]) .sheet-stack__base .card { box-shadow: none; }
1322
1329
 
1323
1330
  /* ============================================================
1324
1331
  .sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
@@ -2720,7 +2727,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2720
2727
  press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
2721
2728
  something as wide as its card (a row, a summary line, a card, a step bar, a peeked sheet) fades under the finger (--press-dim)
2722
2729
  instead, because a whole row shrinking reads as the card moving. A choice's fill fades in as it is picked. Disabled = no
2723
- pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here. */
2730
+ pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
2731
+ .card--go (Lh 2026-10-09) = a card pressed whole: `Card` renders it while its ONE control is its foot's button or a row card's end
2732
+ button (src/cards/cardGo.ts). It is in the DIM pointer and :active lists only — the focus ring and the disabled look stay the
2733
+ button's, the card itself is never focused or disabled. */
2724
2734
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2725
2735
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2726
2736
  a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach) {
@@ -2728,7 +2738,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2728
2738
  touch-action: manipulation;
2729
2739
  transition: scale var(--motion-press) ease-out;
2730
2740
  }
2731
- :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge) {
2741
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge) {
2732
2742
  cursor: pointer;
2733
2743
  touch-action: manipulation;
2734
2744
  transition: opacity var(--motion-press) ease-out;
@@ -2743,7 +2753,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
2743
2753
  :where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
2744
2754
  .float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
2745
2755
  a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
2746
- :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
2756
+ :where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
2747
2757
  .sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
2748
2758
  /* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
2749
2759
  it darkens a touch instead (--press-shade) */
@@ -1,4 +1,4 @@
1
- import { type ComponentProps, type ReactNode } from 'react';
1
+ import { type ComponentProps, type MouseEvent, type ReactNode } from 'react';
2
2
  import { type CardHeadProps } from './CardHead.js';
3
3
  import { type StackPlace } from '../sheets/stackCard.js';
4
4
  export type { StackPlace };
@@ -47,9 +47,11 @@ export interface CardProps extends ComponentProps<'article'> {
47
47
  * (its CardFoot last); the children are the content between them (frameless rows, kv, a ring, a paragraph — never a second card
48
48
  * boundary). The order is the card's, never the writer's. A head of its own shape: write `<CardHead>` as the first child instead.
49
49
  * A page with one feature and one CTA may be a single card. Keep everything big.
50
+ * PRESSED WHOLE: a card whose ONLY control is its foot's button, or a row card's end button, is pressed anywhere — a touch off the
51
+ * button runs that button's own action once, and the card dims under the finger. The app writes nothing; any other card is only read.
50
52
  */
51
53
  export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
52
- onClick: import("react").MouseEventHandler<HTMLElement> | undefined;
54
+ onClick: (e: MouseEvent<HTMLElement>) => void;
53
55
  "aria-busy"?: (boolean | "true" | "false") | undefined;
54
56
  ref: (node: HTMLElement | null) => void;
55
57
  className: string;
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, createElement, isValidElement, useRef } from 'react';
2
+ import { Children, createElement, isValidElement, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { cx } from '../cx.js';
4
4
  import { warnOnce } from '../state.js';
5
5
  import { CardHead } from './CardHead.js';
6
6
  import { CardFoot } from './CardFoot.js';
7
+ import { goControl } from './cardGo.js';
7
8
  import { StackHeadCtx, StackPullCtx, stackPaint, useStackCard } from '../sheets/stackCard.js';
8
9
  /**
9
10
  * Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
@@ -11,6 +12,8 @@ import { StackHeadCtx, StackPullCtx, stackPaint, useStackCard } from '../sheets/
11
12
  * (its CardFoot last); the children are the content between them (frameless rows, kv, a ring, a paragraph — never a second card
12
13
  * boundary). The order is the card's, never the writer's. A head of its own shape: write `<CardHead>` as the first child instead.
13
14
  * A page with one feature and one CTA may be a single card. Keep everything big.
15
+ * PRESSED WHOLE: a card whose ONLY control is its foot's button, or a row card's end button, is pressed anywhere — a touch off the
16
+ * button runs that button's own action once, and the card dims under the finger. The app writes nothing; any other card is only read.
14
17
  */
15
18
  export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }) {
16
19
  const head = heading != null || caption != null || end != null;
@@ -34,11 +37,30 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
34
37
  else if (passed)
35
38
  passed.current = node;
36
39
  };
40
+ const [go, setGo] = useState(false);
41
+ useLayoutEffect(() => { setGo(own.current != null && goControl(own.current) != null); }); // every render: what the card holds is its children's
42
+ const pressing = useRef(false);
37
43
  const click = (e) => {
44
+ if (pressing.current)
45
+ return; // the control's click, pressed from here, coming back up: the app's onClick has run for this press
38
46
  onClick?.(e);
47
+ const target = e.target;
48
+ const card = own.current;
49
+ if (!e.defaultPrevented && card?.contains(target)) {
50
+ const control = goControl(card);
51
+ if (control && !control.contains(target)) {
52
+ pressing.current = true;
53
+ try {
54
+ control.click();
55
+ }
56
+ finally {
57
+ pressing.current = false;
58
+ }
59
+ return;
60
+ }
61
+ }
39
62
  if (!stack || e.defaultPrevented || swallow.current)
40
63
  return;
41
- const target = e.target;
42
64
  if (target.closest('.sheet-stack__back')) {
43
65
  stack.onClose();
44
66
  return;
@@ -54,10 +76,10 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
54
76
  const props = {
55
77
  ...rest,
56
78
  ref: setRef,
57
- className: cx('card', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', painted.className, className),
79
+ className: cx('card', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', go && 'card--go', painted.className, className),
58
80
  style: painted.style || style ? { ...painted.style, ...style } : undefined,
59
81
  ...(stack ? { 'aria-busy': painted.busy } : {}),
60
- onClick: stack ? click : onClick,
82
+ onClick: click,
61
83
  };
62
84
  if (!head && !tail)
63
85
  return createElement(as, props, inner(children));
@@ -0,0 +1,2 @@
1
+ /** the control this card presses when it is touched off that control, or null: the card is only read */
2
+ export declare function goControl(card: HTMLElement): HTMLElement | null;
@@ -0,0 +1,27 @@
1
+ /* THE RULE (Lh 2026-10-09): a card that holds exactly ONE control, standing where the card's own move sits (its foot, or the end of a
2
+ row card), is pressed whole — a touch anywhere on it is a press on that control. Asked by `Card` only, once for the class it
3
+ renders and once, fresh, for each click. */
4
+ /** everything a member presses, types in or drags. One more of these anywhere in the card (a nested card's too) and the card is only read */
5
+ const CONTROLS = [
6
+ 'button', 'a[href]', 'input:not([type=hidden])', 'textarea', 'select', 'summary',
7
+ '[role=button]', '[role=link]', '[role=tab]', '[role=radio]', '[role=checkbox]', '[role=switch]', '[role=slider]', '[role=menuitem]',
8
+ '[tabindex]:not([tabindex="-1"])', '[contenteditable]',
9
+ '.stamp-on', // dragged
10
+ ].join(', ');
11
+ /** a card that is pressed whole already (a link, a label, a button), or where a touch means something else: a sheet is where the member
12
+ * acts, a modal asks, a touch on a stack's sheet opens it (`stack-card` is what `Card` renders from its `stack` prop) */
13
+ const NOT_A_GO_CARD = 'a, label, button, .sheet, .modal, .stack-card';
14
+ /** the control this card presses when it is touched off that control, or null: the card is only read */
15
+ export function goControl(card) {
16
+ if (card.matches(NOT_A_GO_CARD))
17
+ return null;
18
+ const controls = card.querySelectorAll(CONTROLS);
19
+ if (controls.length !== 1)
20
+ return null;
21
+ const control = controls[0];
22
+ if (control.matches(':disabled, [aria-disabled="true"]'))
23
+ return null;
24
+ const inFoot = control.parentElement?.matches('.card__foot') && control.parentElement.parentElement === card;
25
+ const atRowEnd = card.matches('.card--row') && card.lastElementChild === control;
26
+ return inFoot || atRowEnd ? control : null;
27
+ }