@usableapp/cardds 0.7.55 → 0.7.57

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.
4
4
 
5
+ ## 0.7.57 — 2026-10-06
6
+
7
+ ### Added
8
+ - **`paper` on a sheet in a stack.** The same prop as the single sheet, on `StackSheet`, on a `Card` in a `SheetStack`, and on each sheet of `Home`. The head keeps `tone`. The body and the foot are white. The gap under the head stays the sheet's colour. The first row sits `--sp-6` (24px at the default spacing) down into the white. A pull to refresh adds its own room on top of that inset. Pass `paper` on the sheets that should look this way. A sheet without it is unchanged.
9
+
10
+ ### Fixed
11
+ - **A dark `paper` head no longer paints its light ink onto the white body.** The words on the white use the page's ink (`--color-ink`, or the wireframe dark when that is unset). A theme that already sets `--color-ink` is unchanged.
12
+
13
+ ## 0.7.56 — 2026-10-06
14
+
15
+ ### Changed
16
+ - **A `paper` sheet keeps 24px of white above its first field.** The head stays the sheet's colour. The gap under the head stays that colour (`--card-gap`). The body's top padding is `--sp-6`, so a field no longer sits on the colour seam. A sheet without `paper` is unchanged. Nothing to change in a screen.
17
+
5
18
  ## 0.7.55 — 2026-10-06
6
19
 
7
20
  ### Fixed
package/README.md CHANGED
@@ -275,8 +275,10 @@ whose markup has rules, because the sheet's box is the step itself:
275
275
  pinned; the body is the one thing that scrolls, to the card's own edges. The
276
276
  handle is a line, or four dots (`mark="dots"`): both sit in the card's top
277
277
  padding, the same distance from the edge, so the title stays put. `paper` makes
278
- the content white while the head keeps the sheet's colour (`tone`, as a card). A
279
- sheet with no head (the dim page sheet) is a body alone.
278
+ the content white while the head keeps the sheet's colour (`tone`, as a card).
279
+ The same prop on a `StackSheet`, or on a `Card` in a stack: the head keeps the
280
+ tone, the body and the foot are white, and the gap under the head stays the tone.
281
+ A sheet with no head (the dim page sheet) is a body alone.
280
282
  - **The step follows its top bar directly, and touches it.** `full` means
281
283
  the sheet's top edge at the bar's bottom edge — nothing above it but the
282
284
  bar. So the room the screen would have left between the bar and the step
package/css/sheet.css CHANGED
@@ -199,15 +199,23 @@
199
199
  cursor: grab;
200
200
  }
201
201
  /* paper: the head keeps the sheet's own colour, the content is white (Lh 2026-10-05).
202
- The body and the foot read as paper; the handle and the head stay on the card. */
202
+ The body and the foot read as paper; the handle and the head stay on the card.
203
+ The flex gap under the head is outside the body, so it stays the sheet's tint.
204
+ The first field would sit on that colour seam. The white inset is --sp-6 (24px at
205
+ the default spacing), the body's own top padding, and only when the sheet is paper.
206
+ A plain sheet keeps padding-top 0. A sheet in a stack wears the same class; its padding
207
+ is the stack's own rule, because a pull adds --_refresh on top of this inset.
208
+ The ink falls back to the wireframe word colour: an unset --color-ink would otherwise
209
+ inherit a dark card's light ink, and the words would vanish on the white. */
203
210
  .sheet--paper > .sheet__body {
204
211
  --card-bg: var(--color-paper, #fff);
205
- --card-ink: var(--color-ink);
206
- --card-muted: var(--_muted-on-paper);
212
+ --card-ink: var(--color-ink, #141414);
213
+ --card-muted: var(--_muted-on-paper, #555555);
207
214
  background: var(--card-bg);
208
215
  color: var(--card-ink);
216
+ padding-top: var(--sp-6);
209
217
  }
210
- .sheet--paper > .card__foot { --card-bg: var(--color-paper, #fff); color: var(--color-ink); }
218
+ .sheet--paper > .card__foot { --card-bg: var(--color-paper, #fff); color: var(--color-ink, #141414); }
211
219
  .sheet__handle--dots {
212
220
  --_dot: var(--sp-1);
213
221
  --_step: calc(var(--_dot) * 2.5);
package/css/stack.css CHANGED
@@ -177,6 +177,10 @@
177
177
  .sheet-stack--tap > .card > .card__head { touch-action: none; }
178
178
  /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private. */
179
179
  .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0px); transition: padding-top var(--motion-sheet) ease; }
180
+ /* paper on a stacked sheet is the same class as the single sheet (Lh 2026-10-06). The head keeps the card's
181
+ colour. The body is white, from the rule in sheet.css, and its top inset is --sp-6. A pull still adds
182
+ --_refresh: this rule wins on padding, because the pull's rule above is more specific than the single sheet's. */
183
+ .sheet-stack > .card.sheet--paper > .sheet__body { padding-top: calc(var(--sp-6) + var(--_refresh, 0px)); }
180
184
  .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
181
185
  padding-top: calc(var(--card-pad) + var(--_refresh, 0px));
182
186
  transition: translate var(--motion-sheet) ease, padding-top var(--motion-sheet) ease;
@@ -311,6 +315,19 @@
311
315
  .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
312
316
  .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
313
317
  .sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
318
+ /* a paper stack's foot is white to the card's edges, and it covers the tinted flex gap above it so the
319
+ button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
320
+ where it was — the padding gives back what the bleed takes. */
321
+ .sheet-stack > .card.sheet--paper > .card__foot {
322
+ background: var(--color-paper, #fff);
323
+ color: var(--color-ink, #141414);
324
+ margin-top: calc(-1 * var(--card-gap));
325
+ margin-inline: calc(-1 * var(--card-pad));
326
+ margin-bottom: calc(-1 * var(--card-pad));
327
+ padding-top: var(--card-gap);
328
+ padding-inline: var(--card-pad);
329
+ padding-bottom: var(--card-pad);
330
+ }
314
331
 
315
332
  /* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
316
333
  filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
package/dist/cardds.css CHANGED
@@ -1178,6 +1178,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1178
1178
  .sheet-stack--tap > .card > .card__head { touch-action: none; }
1179
1179
  /* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private. */
1180
1180
  .sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0px); transition: padding-top var(--motion-sheet) ease; }
1181
+ /* paper on a stacked sheet is the same class as the single sheet (Lh 2026-10-06). The head keeps the card's
1182
+ colour. The body is white, from the rule in sheet.css, and its top inset is --sp-6. A pull still adds
1183
+ --_refresh: this rule wins on padding, because the pull's rule above is more specific than the single sheet's. */
1184
+ .sheet-stack > .card.sheet--paper > .sheet__body { padding-top: calc(var(--sp-6) + var(--_refresh, 0px)); }
1181
1185
  .sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
1182
1186
  padding-top: calc(var(--card-pad) + var(--_refresh, 0px));
1183
1187
  transition: translate var(--motion-sheet) ease, padding-top var(--motion-sheet) ease;
@@ -1312,6 +1316,19 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1312
1316
  .sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
1313
1317
  .sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
1314
1318
  .sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
1319
+ /* a paper stack's foot is white to the card's edges, and it covers the tinted flex gap above it so the
1320
+ button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
1321
+ where it was — the padding gives back what the bleed takes. */
1322
+ .sheet-stack > .card.sheet--paper > .card__foot {
1323
+ background: var(--color-paper, #fff);
1324
+ color: var(--color-ink, #141414);
1325
+ margin-top: calc(-1 * var(--card-gap));
1326
+ margin-inline: calc(-1 * var(--card-pad));
1327
+ margin-bottom: calc(-1 * var(--card-pad));
1328
+ padding-top: var(--card-gap);
1329
+ padding-inline: var(--card-pad);
1330
+ padding-bottom: var(--card-pad);
1331
+ }
1315
1332
 
1316
1333
  /* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
1317
1334
  filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
@@ -1595,15 +1612,23 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1595
1612
  cursor: grab;
1596
1613
  }
1597
1614
  /* paper: the head keeps the sheet's own colour, the content is white (Lh 2026-10-05).
1598
- The body and the foot read as paper; the handle and the head stay on the card. */
1615
+ The body and the foot read as paper; the handle and the head stay on the card.
1616
+ The flex gap under the head is outside the body, so it stays the sheet's tint.
1617
+ The first field would sit on that colour seam. The white inset is --sp-6 (24px at
1618
+ the default spacing), the body's own top padding, and only when the sheet is paper.
1619
+ A plain sheet keeps padding-top 0. A sheet in a stack wears the same class; its padding
1620
+ is the stack's own rule, because a pull adds --_refresh on top of this inset.
1621
+ The ink falls back to the wireframe word colour: an unset --color-ink would otherwise
1622
+ inherit a dark card's light ink, and the words would vanish on the white. */
1599
1623
  .sheet--paper > .sheet__body {
1600
1624
  --card-bg: var(--color-paper, #fff);
1601
- --card-ink: var(--color-ink);
1602
- --card-muted: var(--_muted-on-paper);
1625
+ --card-ink: var(--color-ink, #141414);
1626
+ --card-muted: var(--_muted-on-paper, #555555);
1603
1627
  background: var(--card-bg);
1604
1628
  color: var(--card-ink);
1629
+ padding-top: var(--sp-6);
1605
1630
  }
1606
- .sheet--paper > .card__foot { --card-bg: var(--color-paper, #fff); color: var(--color-ink); }
1631
+ .sheet--paper > .card__foot { --card-bg: var(--color-paper, #fff); color: var(--color-ink, #141414); }
1607
1632
  .sheet__handle--dots {
1608
1633
  --_dot: var(--sp-1);
1609
1634
  --_step: calc(var(--_dot) * 2.5);
@@ -7,6 +7,8 @@ export type Tone = 1 | 2 | 3 | 4 | 5;
7
7
  export interface CardProps extends ComponentProps<'article'> {
8
8
  /** the tone slot — see `Tone` */
9
9
  tone?: Tone;
10
+ /** the content is white paper and the head keeps the card's colour. The same look as `Sheet paper`. Omit it and the content matches the head */
11
+ paper?: boolean;
10
12
  /** compact: a small card inside a card or a sheet body (a note, a promo) */
11
13
  sm?: boolean;
12
14
  /** a list row that is its own card: thumb · body · end, in a CardList */
@@ -45,7 +47,7 @@ export interface CardProps extends ComponentProps<'article'> {
45
47
  * boundary). The order is the card's, never the writer's. A head of its own shape: write `<CardHead>` as the first child instead.
46
48
  * A page with one feature and one CTA may be a single card. Keep everything big.
47
49
  */
48
- export declare function Card({ tone, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
50
+ 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<{
49
51
  onClick: import("react").MouseEventHandler<HTMLElement> | undefined;
50
52
  "aria-busy"?: (boolean | "true" | "false") | undefined;
51
53
  ref: (node: HTMLElement | null) => void;
@@ -12,7 +12,7 @@ import { StackHeadCtx, StackPullCtx, stackPaint, useStackCard } from '../sheets/
12
12
  * 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
13
  * A page with one feature and one CTA may be a single card. Keep everything big.
14
14
  */
15
- export function Card({ tone, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }) {
15
+ 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
16
  const head = heading != null || caption != null || end != null;
17
17
  const tail = foot != null || footLink != null;
18
18
  if (head || tail) {
@@ -54,7 +54,7 @@ export function Card({ tone, sm, row, cover, fold, centre, band, as = 'article',
54
54
  const props = {
55
55
  ...rest,
56
56
  ref: setRef,
57
- className: cx('card', tone && tone !== 1 && `card--${tone}`, sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', painted.className, className),
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),
58
58
  style: painted.style || style ? { ...painted.style, ...style } : undefined,
59
59
  ...(stack ? { 'aria-busy': painted.busy } : {}),
60
60
  onClick: stack ? click : onClick,
@@ -25,7 +25,7 @@ export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'>
25
25
  mark?: 'line' | 'dots';
26
26
  /** the card tone slot, as `Card tone` — 2 and up add `card--N`. The head shows it; with `paper` the content does not */
27
27
  tone?: 1 | 2 | 3 | 4 | 5;
28
- /** the content is white paper and the head keeps the sheet's colour. Omit it and the content is the same colour as the head */
28
+ /** the content is white paper and the head keeps the sheet's colour. The same prop on a `StackSheet` or a `Card` in a stack. Omit it and the content is the same colour as the head */
29
29
  paper?: boolean;
30
30
  }
31
31
  /**
@@ -13,8 +13,10 @@ export interface StackSheetTab {
13
13
  export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'> {
14
14
  /** the SHEET HEAD's title — an h2 (the page's h1 is its own) */
15
15
  title: ReactNode;
16
- /** the paper — see `Tone` */
16
+ /** the tone slot — see `Tone`. The head shows it */
17
17
  tone?: Tone;
18
+ /** the content is white paper and the head keeps the sheet's colour. The same look as `Sheet paper`. Omit it and the content matches the head */
19
+ paper?: boolean;
18
20
  /** the head's end when the sheet has no tabs: an Icon, an IconRow */
19
21
  icon?: ReactNode;
20
22
  /** TABS in the head: icon buttons at the end — the sheet's pages. A tap on one in a peeked head opens the sheet on it */
@@ -40,4 +42,4 @@ export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'
40
42
  * A count shows as a small circle over its icon. The body is the app's: render the page of the chosen tab (and nothing while the sheet
41
43
  * is closed — see `Home`). A direct child of `SheetStack`.
42
44
  */
43
- export declare function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel, backAt, stack, className, children, ...rest }: StackSheetProps): import("react").JSX.Element;
45
+ export declare function StackSheet({ title, tone, paper, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel, backAt, stack, className, children, ...rest }: StackSheetProps): import("react").JSX.Element;
@@ -19,7 +19,7 @@ import { word } from '../words.js';
19
19
  * A count shows as a small circle over its icon. The body is the app's: render the page of the chosen tab (and nothing while the sheet
20
20
  * is closed — see `Home`). A direct child of `SheetStack`.
21
21
  */
22
- export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = word('back'), backAt = 'none', stack, className, children, ...rest }) {
22
+ export function StackSheet({ title, tone, paper, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = word('back'), backAt = 'none', stack, className, children, ...rest }) {
23
23
  const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
24
24
  // a title longer than its room does not wrap: it runs sideways, like a song title in a short slot — the room it overflows by
25
25
  // is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
@@ -38,5 +38,5 @@ export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab,
38
38
  measure();
39
39
  return watchSize([h, run], measure);
40
40
  }, [title]);
41
- return (_jsxs(Card, { stack: stack, tone: tone, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [backAt === 'start' && _jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: cx('stack-sheet__title', long && 'is-long'), children: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs(IconBtn, { sm: true, invert: t.id === tab, role: "tab", "aria-selected": t.id === tab, label: t.label, className: "stack-sheet__tab", onClick: () => pick(t.id), children: [_jsx(Icon, { name: t.icon }), t.count ? _jsx(Badge, { className: "stack-sheet__count", children: t.count > 99 ? '99+' : t.count }) : null] }, t.id))) })) : icon, backAt === 'end' && _jsx(StackBack, { end: true, label: backLabel })] }), _jsx(SheetBody, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(CardFoot, { children: foot })] }));
41
+ return (_jsxs(Card, { stack: stack, tone: tone, paper: paper, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [backAt === 'start' && _jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: cx('stack-sheet__title', long && 'is-long'), children: _jsx("span", { className: "stack-sheet__run", children: title }) }), tabs?.length ? (_jsx("span", { className: "stack-sheet__tabs", role: "tablist", children: tabs.map((t) => (_jsxs(IconBtn, { sm: true, invert: t.id === tab, role: "tab", "aria-selected": t.id === tab, label: t.label, className: "stack-sheet__tab", onClick: () => pick(t.id), children: [_jsx(Icon, { name: t.icon }), t.count ? _jsx(Badge, { className: "stack-sheet__count", children: t.count > 99 ? '99+' : t.count }) : null] }, t.id))) })) : icon, backAt === 'end' && _jsx(StackBack, { end: true, label: backLabel })] }), _jsx(SheetBody, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(CardFoot, { children: foot })] }));
42
42
  }
@@ -7,8 +7,10 @@ export interface HomeSheet {
7
7
  title: ReactNode;
8
8
  /** the sheet head's end when it has no tabs: an Icon, an IconRow */
9
9
  icon?: ReactNode;
10
- /** the paper — see `Tone` (default: 2, 3, 1, 2, 3 by place) */
10
+ /** the tone slot — see `Tone` (default: 2, 3, 1, 2, 3 by place). The head shows it */
11
11
  tone?: Tone;
12
+ /** white content, the head keeps `tone`. The same look as `Sheet paper` */
13
+ paper?: boolean;
12
14
  /** the sheet's pages as icon tabs in its head (a count shows as a small circle over the icon) — see `StackSheet` */
13
15
  tabs?: StackSheetTab[];
14
16
  /** the tab shown first (default: the first) */
@@ -50,5 +50,5 @@ export function Home({ top, title, sheets, open: openProp, defaultOpen = null, o
50
50
  }, [leaving]);
51
51
  const [tabs, setTabs] = useState(() => Object.fromEntries(sheets.map((x, i) => [i, x.defaultTab ?? x.tabs?.[0]?.id])));
52
52
  const shows = (i) => i === shown || i === leaving || (keep && seen.has(i));
53
- return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: shown, onOpenChange: request, onRefresh: onRefresh, refreshing: refreshing, base: base, children: sheets.map((s, i) => (_jsx(StackSheet, { tone: s.tone ?? TONES[i], title: s.title, icon: s.icon, tabs: s.tabs, tab: tabs[i], onTabChange: (t) => setTabs((v) => ({ ...v, [i]: t })), backLabel: backLabel, backAt: backAt, foot: typeof s.foot === 'function' ? s.foot(tabs[i]) : s.foot, children: shows(i) ? (typeof s.body === 'function' ? s.body(tabs[i]) : s.body) : null }, i))) }), children] }));
53
+ return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: shown, onOpenChange: request, onRefresh: onRefresh, refreshing: refreshing, base: base, children: sheets.map((s, i) => (_jsx(StackSheet, { tone: s.tone ?? TONES[i], paper: s.paper, title: s.title, icon: s.icon, tabs: s.tabs, tab: tabs[i], onTabChange: (t) => setTabs((v) => ({ ...v, [i]: t })), backLabel: backLabel, backAt: backAt, foot: typeof s.foot === 'function' ? s.foot(tabs[i]) : s.foot, children: shows(i) ? (typeof s.body === 'function' ? s.body(tabs[i]) : s.body) : null }, i))) }), children] }));
54
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.55",
3
+ "version": "0.7.57",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",