@usableapp/cardds 0.7.100 → 0.7.101

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,19 @@
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. Each release section after 0.7.97 carries one `Components:` line, written by `node scripts/components-changed.mjs <previous tag>`: the components the release changed, or `all` when a file changed that any component can read (a token, the build), or `none`; check only the screens that import those.
4
4
 
5
+ ## 0.7.101 — 2026-10-19
6
+
7
+ A list that keeps only a few past entries in sight, a cross that takes a row card off its list, and a postcard that keeps the caret where it was when its paper is full.
8
+
9
+ Components: CallSheet, Card, CardBody, CardEnd, CardFeed, CardFoot, CardHead, CardList, CardMedia, CardThumb, Delta, FeedCard, Home, IconRow, NumberBand, NumberBandStack, Placeholder, PostDesk, PostDeskLine, Sheet, SheetStack, StackSheet, Stat, StatRow, Sticky, Walkthrough
10
+
11
+ ### Added
12
+ - **`CardList show`: a short window of past entries that scrolls in itself** (Lh 2026-10-19, the past requests under a request form). Before → after: a `CardList` was as long as its rows and scrolled only with what held it, so a list of past requests under a form pushed the form's buttons away → `show={2.5}` keeps two whole rows and half of the third in sight (counted in rows, measured, so rows of any height work) and the rest scrolls inside the list. Knob `--card-list-window-pad` (the room inside the scroll box for the focus ring). Gallery `Card/CardListWindow`.
13
+ - **`Card row onRemove` + `removeLabel`: a small cross at a row card's top right corner** that takes the row off its list. Smaller than a button (half `--tap-base`, 24, growing with the size of things), its touch target 44; a touch on it never picks the row, and a row card with only the cross is not pressed whole. The app removes the entry from its own list. Knobs `--card-remove-size` · `--card-remove-inset` · `--card-remove-ink` · `--card-remove-bg`.
14
+
15
+ ### Fixed
16
+ - **`PostDesk` keeps the caret where it was when the paper refuses words** (the paper-is-full rule, now one with `Sticky`'s). Before → after: a line typed in the middle of a full message was refused but the caret jumped to the end of the message, and a Thai or Japanese composition that would not fit could slip in mid-composition → the addition is refused where it was typed, the caret stays, a composition that would not fit is put back whole when it ends (the app is told), `onMessageChange` never hears a refused input and `onFull` is still called. A `StickyField` on a `Sticky` now also refuses a same-length replacement that would overflow (it took it before). No prop, class or knob changes.
17
+
5
18
  ## 0.7.100 — 2026-10-19
6
19
 
7
20
  The check only (`cardds check` and the ESLint plugin); no component, prop, class or token changes. The line below reads `all` only because a script of the build changed (`scripts/ci.mjs`, the release's own check); no screen needs a new look.
package/css/card.css CHANGED
@@ -302,6 +302,49 @@
302
302
  }
303
303
  .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
304
304
 
305
+ /* ---- a short window of past entries (`CardList show`, Lh 2026-10-19): the list scrolls IN ITSELF, a few rows in sight and the next one cut
306
+ in half to say there is more. The height is counted in rows by the component (`--_card-list-h`, px: rows wrap to any height); the room
307
+ round the rows (`--card-list-window-pad`) keeps a picked row's focus ring and a card's shadow inside the scroll box, and the margin gives
308
+ that room back so the rows stand where an ordinary list's would. ---- */
309
+ .card-list--window {
310
+ --_pad: var(--card-list-window-pad, var(--sp-2));
311
+ max-height: var(--_card-list-h, none);
312
+ overflow-y: auto;
313
+ overscroll-behavior: contain;
314
+ scrollbar-width: none;
315
+ padding: var(--_pad);
316
+ margin: calc(-1 * var(--_pad));
317
+ }
318
+
319
+ /* ---- the cross that takes a row off the list (`Card row onRemove`): small, at the row's top right corner, inside it ----
320
+ <label class="card card--row"><input type="radio">…<div class="card__body">…</div><button class="card__remove" aria-label="Remove"><svg class="icon">x</svg></button></label>
321
+ Smaller than any button (a ring half a thing's size, `--tap-base` / 2 = 24) and it grows with the size of things; the touch target is an
322
+ unseen ring round it, never under --tap-floor (44). It stands in the row's flow, so the title wraps before it, tucked into the corner
323
+ by the row's own padding; it sits over the row's radio (which covers the whole card), so a touch on it removes and never picks. */
324
+ .card__remove {
325
+ --_size: var(--card-remove-size, calc(var(--tap-base) / 2));
326
+ position: relative;
327
+ z-index: 1;
328
+ flex: none;
329
+ align-self: flex-start;
330
+ display: grid;
331
+ place-items: center;
332
+ width: var(--_size);
333
+ height: var(--_size);
334
+ margin-block-start: calc(var(--card-remove-inset, var(--sp-2)) - var(--card-row-pad, var(--sp-4)));
335
+ margin-inline-end: calc(var(--card-remove-inset, var(--sp-2)) - var(--card-row-pad, var(--sp-4)));
336
+ padding: 0;
337
+ border: var(--border-w) solid currentColor;
338
+ border-radius: 50%;
339
+ background: var(--card-remove-bg, transparent);
340
+ color: var(--card-remove-ink, currentColor);
341
+ font: inherit;
342
+ cursor: pointer;
343
+ }
344
+ .card__remove::after { content: ""; position: absolute; inset: 50% auto auto 50%; translate: -50% -50%; width: max(var(--tap-floor), var(--_size)); aspect-ratio: 1; border-radius: 50%; }
345
+ .card__remove > .icon { width: 62%; height: 62%; }
346
+ .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
347
+
305
348
  /* leading thumb/icon in a row */
306
349
  .card__thumb {
307
350
  width: var(--card-thumb-size, var(--tap-sm)); height: var(--card-thumb-size, var(--tap-sm));
package/dist/cardds.css CHANGED
@@ -1008,6 +1008,49 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1008
1008
  }
1009
1009
  .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
1010
1010
 
1011
+ /* ---- a short window of past entries (`CardList show`, Lh 2026-10-19): the list scrolls IN ITSELF, a few rows in sight and the next one cut
1012
+ in half to say there is more. The height is counted in rows by the component (`--_card-list-h`, px: rows wrap to any height); the room
1013
+ round the rows (`--card-list-window-pad`) keeps a picked row's focus ring and a card's shadow inside the scroll box, and the margin gives
1014
+ that room back so the rows stand where an ordinary list's would. ---- */
1015
+ .card-list--window {
1016
+ --_pad: var(--card-list-window-pad, var(--sp-2));
1017
+ max-height: var(--_card-list-h, none);
1018
+ overflow-y: auto;
1019
+ overscroll-behavior: contain;
1020
+ scrollbar-width: none;
1021
+ padding: var(--_pad);
1022
+ margin: calc(-1 * var(--_pad));
1023
+ }
1024
+
1025
+ /* ---- the cross that takes a row off the list (`Card row onRemove`): small, at the row's top right corner, inside it ----
1026
+ <label class="card card--row"><input type="radio">…<div class="card__body">…</div><button class="card__remove" aria-label="Remove"><svg class="icon">x</svg></button></label>
1027
+ Smaller than any button (a ring half a thing's size, `--tap-base` / 2 = 24) and it grows with the size of things; the touch target is an
1028
+ unseen ring round it, never under --tap-floor (44). It stands in the row's flow, so the title wraps before it, tucked into the corner
1029
+ by the row's own padding; it sits over the row's radio (which covers the whole card), so a touch on it removes and never picks. */
1030
+ .card__remove {
1031
+ --_size: var(--card-remove-size, calc(var(--tap-base) / 2));
1032
+ position: relative;
1033
+ z-index: 1;
1034
+ flex: none;
1035
+ align-self: flex-start;
1036
+ display: grid;
1037
+ place-items: center;
1038
+ width: var(--_size);
1039
+ height: var(--_size);
1040
+ margin-block-start: calc(var(--card-remove-inset, var(--sp-2)) - var(--card-row-pad, var(--sp-4)));
1041
+ margin-inline-end: calc(var(--card-remove-inset, var(--sp-2)) - var(--card-row-pad, var(--sp-4)));
1042
+ padding: 0;
1043
+ border: var(--border-w) solid currentColor;
1044
+ border-radius: 50%;
1045
+ background: var(--card-remove-bg, transparent);
1046
+ color: var(--card-remove-ink, currentColor);
1047
+ font: inherit;
1048
+ cursor: pointer;
1049
+ }
1050
+ .card__remove::after { content: ""; position: absolute; inset: 50% auto auto 50%; translate: -50% -50%; width: max(var(--tap-floor), var(--_size)); aspect-ratio: 1; border-radius: 50%; }
1051
+ .card__remove > .icon { width: 62%; height: 62%; }
1052
+ .card__remove:focus-visible { outline: var(--focus); outline-offset: var(--sp-0); }
1053
+
1011
1054
  /* leading thumb/icon in a row */
1012
1055
  .card__thumb {
1013
1056
  width: var(--card-thumb-size, var(--tap-sm)); height: var(--card-thumb-size, var(--tap-sm));
@@ -49,6 +49,12 @@ export interface CardProps extends ComponentProps<'article'> {
49
49
  leave?: boolean;
50
50
  /** a card that was asked to `leave` HAS left (its motion ended; at once where motion is off, or when it has no motion) — the app does what follows: changes the page, closes. Once per leave */
51
51
  onLeft?: () => void;
52
+ /** a `row` card the member can take off its list: a small cross at the row's top right corner (smaller than a button, growing with the size of
53
+ * things; its touch target stays 44). A touch on it calls this and never picks the row. The app takes the row out of its own list. Knobs
54
+ * `--card-remove-size` · `--card-remove-inset` · `--card-remove-ink` · `--card-remove-bg` */
55
+ onRemove?: () => void;
56
+ /** the cross's spoken name (default: the words' `remove`) — name what goes: "Remove 'buy medicine'" */
57
+ removeLabel?: string;
52
58
  }
53
59
  /**
54
60
  * Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
@@ -59,7 +65,7 @@ export interface CardProps extends ComponentProps<'article'> {
59
65
  * 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
60
66
  * 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.
61
67
  */
62
- export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, leave, onLeft, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
68
+ export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, leave, onLeft, onRemove, removeLabel, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
63
69
  onClick: (e: MouseEvent<HTMLElement>) => void;
64
70
  inert?: boolean | undefined;
65
71
  "aria-busy"?: (boolean | "true" | "false") | undefined;
@@ -5,6 +5,8 @@ import { useLatest, warnOnce } from '../state.js';
5
5
  import { CardHead } from './CardHead.js';
6
6
  import { CardFoot } from './CardFoot.js';
7
7
  import { goControl } from './cardGo.js';
8
+ import { Icon } from '../type/Icon.js';
9
+ import { word } from '../words.js';
8
10
  import { StackHeadCtx, StackPullCtx, ownHead, stackPaint, useStackCard } from '../sheets/stackCard.js';
9
11
  /**
10
12
  * Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
@@ -15,7 +17,9 @@ import { StackHeadCtx, StackPullCtx, ownHead, stackPaint, useStackCard } from '.
15
17
  * 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
18
  * 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.
17
19
  */
18
- export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, leave, onLeft, className, style, children, onClick, ref, ...rest }) {
20
+ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, leave, onLeft, onRemove, removeLabel, className, style, children, onClick, ref, ...rest }) {
21
+ if (onRemove && !row)
22
+ warnOnce('card-remove-row', 'cardds Card: `onRemove` draws its cross on a `row` card only');
19
23
  const head = heading != null || caption != null || end != null;
20
24
  const tail = foot != null || footLink != null;
21
25
  if (head || tail) {
@@ -96,7 +100,8 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
96
100
  ...(away ? { inert: true } : {}),
97
101
  onClick: click,
98
102
  };
103
+ const cross = row && onRemove ? (_jsx("button", { type: "button", className: "card__remove", "aria-label": removeLabel ?? word('remove'), onClick: (e) => { e.preventDefault(); e.stopPropagation(); onRemove(); }, children: _jsx(Icon, { name: "x" }) }, "remove")) : null;
99
104
  if (!head && !tail)
100
- return createElement(as, props, inner(children));
101
- return createElement(as, props, inner(_jsxs(_Fragment, { children: [head && _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }, "head"), Children.toArray(children), tail && _jsx(CardFoot, { link: footLink, children: foot }, "foot")] })));
105
+ return createElement(as, props, inner(_jsxs(_Fragment, { children: [children, cross] })));
106
+ return createElement(as, props, inner(_jsxs(_Fragment, { children: [head && _jsx(CardHead, { heading: heading, caption: caption, end: end, level: level }, "head"), Children.toArray(children), tail && _jsx(CardFoot, { link: footLink, children: foot }, "foot"), cross] })));
102
107
  }
@@ -1,4 +1,4 @@
1
- import type { ComponentProps } from 'react';
1
+ import { type ComponentProps } from 'react';
2
2
  export interface CardMediaProps extends ComponentProps<'div'> {
3
3
  }
4
4
  /** CardMedia — rounded media inside a card (a Placeholder or img); a corner JourneyBadge pins to it. */
@@ -16,10 +16,16 @@ export interface CardEndProps extends ComponentProps<'div'> {
16
16
  /** CardEnd — the trailing value + caption, right-aligned (in a head or a row card). */
17
17
  export declare function CardEnd({ className, ...rest }: CardEndProps): import("react").JSX.Element;
18
18
  export interface CardListProps extends ComponentProps<'div'> {
19
+ /** how many rows show before the list scrolls IN ITSELF, counted in rows, not a height: `2.5` = two whole rows and half of the third, the cut row
20
+ * saying there is more. The list measures its own rows (they wrap to any height) and keeps the window right when they change. Leave it out and the
21
+ * list is as long as its rows and scrolls with what holds it */
22
+ show?: number;
19
23
  }
20
24
  /** CardList — a stack of `Card row`s, each its own boundary: a list of things to open or to pick (past requests to reuse). It stands on the base or straight in a sheet's
21
- * body and scrolls with it; never inside a card (a card never wraps a card — `cardds check` rule `card-in-card`). Rows inside a card are `Rows`. */
22
- export declare function CardList({ className, ...rest }: CardListProps): import("react").JSX.Element;
25
+ * body and scrolls with it; never inside a card (a card never wraps a card — `cardds check` rule `card-in-card`). Rows inside a card are `Rows`.
26
+ * A SHORT WINDOW of past entries under a form (`show={2.5}`): the list scrolls in itself, a few rows in sight; each row a `Card row as="label"` round a radio
27
+ * (pick one) with `onRemove` for its corner cross (take it off the list) — the app owns the list, the picked one and what a removal does. */
28
+ export declare function CardList({ show, className, style, ref, ...rest }: CardListProps): import("react").JSX.Element;
23
29
  export interface IconRowProps extends ComponentProps<'span'> {
24
30
  }
25
31
  /** IconRow — a trailing cluster of Icons in a card or stack head. */
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef } from 'react';
2
3
  import { cx } from '../cx.js';
3
4
  /** CardMedia — rounded media inside a card (a Placeholder or img); a corner JourneyBadge pins to it. */
4
5
  export function CardMedia({ className, ...rest }) {
@@ -17,9 +18,46 @@ export function CardEnd({ className, ...rest }) {
17
18
  return _jsx("div", { className: cx('card__end', className), ...rest });
18
19
  }
19
20
  /** CardList — a stack of `Card row`s, each its own boundary: a list of things to open or to pick (past requests to reuse). It stands on the base or straight in a sheet's
20
- * body and scrolls with it; never inside a card (a card never wraps a card — `cardds check` rule `card-in-card`). Rows inside a card are `Rows`. */
21
- export function CardList({ className, ...rest }) {
22
- return _jsx("div", { className: cx('card-list', className), ...rest });
21
+ * body and scrolls with it; never inside a card (a card never wraps a card — `cardds check` rule `card-in-card`). Rows inside a card are `Rows`.
22
+ * A SHORT WINDOW of past entries under a form (`show={2.5}`): the list scrolls in itself, a few rows in sight; each row a `Card row as="label"` round a radio
23
+ * (pick one) with `onRemove` for its corner cross (take it off the list) — the app owns the list, the picked one and what a removal does. */
24
+ export function CardList({ show, className, style, ref, ...rest }) {
25
+ const own = useRef(null);
26
+ useLayoutEffect(() => {
27
+ const list = own.current;
28
+ if (!list || show == null)
29
+ return;
30
+ const fit = () => {
31
+ const rows = [...list.children];
32
+ const whole = Math.floor(show);
33
+ const cut = rows[whole];
34
+ if (!cut) {
35
+ list.style.removeProperty('--_card-list-h');
36
+ return;
37
+ }
38
+ const box = cut.getBoundingClientRect();
39
+ const at = box.top - list.getBoundingClientRect().top + list.scrollTop; // from the list's top edge, wherever it is scrolled
40
+ list.style.setProperty('--_card-list-h', `${at + (show - whole) * box.height}px`);
41
+ };
42
+ fit();
43
+ const watch = new ResizeObserver(fit);
44
+ watch.observe(list);
45
+ for (const row of list.children)
46
+ watch.observe(row);
47
+ const rows = new MutationObserver(() => { for (const row of list.children)
48
+ watch.observe(row); fit(); });
49
+ rows.observe(list, { childList: true });
50
+ return () => { watch.disconnect(); rows.disconnect(); };
51
+ }, [show]);
52
+ const setRef = (node) => {
53
+ own.current = node;
54
+ const passed = ref;
55
+ if (typeof passed === 'function')
56
+ passed(node);
57
+ else if (passed)
58
+ passed.current = node;
59
+ };
60
+ return _jsx("div", { ref: setRef, className: cx('card-list', show != null && 'card-list--window', className), style: style, ...rest });
23
61
  }
24
62
  /** IconRow — a trailing cluster of Icons in a card or stack head. */
25
63
  export function IconRow({ className, ...rest }) {
@@ -24,6 +24,6 @@ export function goControl(card) {
24
24
  return null;
25
25
  const foot = control.parentElement;
26
26
  const inFoot = foot?.matches('.card__foot') && foot.parentElement?.closest('.card') === card; // the card's OWN foot, also when a part of the card holds it (a FeedCard's region)
27
- const atRowEnd = card.matches('.card--row') && card.lastElementChild === control;
27
+ const atRowEnd = card.matches('.card--row') && card.lastElementChild === control && !control.matches('.card__remove'); // a row's cross takes it off its list: never pressed by a touch elsewhere
28
28
  return inFoot || atRowEnd ? control : null;
29
29
  }