@usableapp/cardds 0.5.1 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,17 @@
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
- ## Unreleased
5
+ ## 0.6.0 — 2026-09-22
6
+
7
+ ### The walk's dots are the Step's, not each page's (Lh 2026-09-22) — BREAKING
8
+ Reverses 0.5.2. A walk of `BaseContent` pages now carries **one** `PagerAt`, written after `Pages` — not one inside every page. `.step > .pager__at` sits at the base's foot, above the bar; `.base-content:has(> .pager__at:last-child)` (the 0.5.2 per-page flex-column form) is gone. An app with a `PagerAt` at the end of each page's `BaseContent` should move it to a single `PagerAt` after the `Pages` block instead.
9
+
10
+ ## 0.5.2 — 2026-09-22
11
+
12
+ ### Dots at the foot (Lh 2026-09-22)
13
+ A `BaseContent` whose last child is `PagerAt` now sits its dots at the bottom of the reading block, above the bar; the block fills the step for it. Nothing to change in an app. Superseded by 0.6.0 — see above.
14
+
15
+ ## 0.5.1 — 2026-09-22
6
16
 
7
17
  ### Field type 26px, 56px tall (Lh 2026-09-22)
8
18
  `--field-min-h` default is `--tap-lg` (56, was `--tap` 48) — every field stands as tall as the action field already did.
package/css/base.css CHANGED
@@ -117,10 +117,9 @@ button { font: inherit; cursor: pointer; }
117
117
  }
118
118
  .base-content > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
119
119
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
120
- the "where am I" dots under the words, centred — the same dots the centred card has under it */
121
- .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
120
+ (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
121
+ .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
122
122
  .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
123
- .base-content > .pager__at { justify-self: center; }
124
123
  .base-content .cta-pack {
125
124
  display: flex;
126
125
  align-items: start; /* buttons keep their own height, never the row's */
package/css/card.css CHANGED
@@ -208,7 +208,8 @@
208
208
  and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
209
209
  simply switches. Only the card is named; the rest of the page must not
210
210
  cross-fade. */
211
- .screen__centre > .card, .pages > [aria-current] { view-transition-name: centre-card; }
211
+ /* the name is set by slideTo() on the moving element for the move only (2026-09-22): a static name on every walk's current
212
+ page made two walks on one page — the gallery — share it, and the browser aborts a transition with a duplicate name */
212
213
 
213
214
  /* ---- pages: the cards of a walk, all one height ----
214
215
  A sequence the member walks (an introduction) reads steadier when every card
package/css/lists.css CHANGED
@@ -32,7 +32,7 @@
32
32
  .row__body { flex: 1; min-width: 0; display: grid; gap: var(--row-body-gap, var(--sp-0)); }
33
33
  .row__title { font: var(--row-title-font, var(--type-title)); overflow-wrap: anywhere; } /* wraps freely — no clip, so no padding trick for Thai marks (Lh 2026-09-16, same call as the card heading) */
34
34
  .row__meta { font: var(--row-meta-font, var(--type-caption)); color: var(--row-meta-ink, var(--card-muted)); display: flex; align-items: center; gap: var(--row-meta-gap, var(--sp-1)); }
35
- .card--3 .row__meta { color: inherit; opacity: 0.7; }
35
+ .card--3 .row__meta { color: var(--card-3-muted); }
36
36
  .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
37
37
  .row--lg { min-height: var(--row-lg-min-h, var(--fab)); } /* menu rows: a taller target, round arrow at the end; .rows gives them more air */
38
38
  .row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
@@ -51,7 +51,7 @@
51
51
  at --tap-sm, but not an input: nothing to press. */
52
52
  .row--done { pointer-events: none; cursor: default; }
53
53
  .row--done .row__title { color: var(--card-muted); }
54
- .card--3 .row--done .row__title { color: inherit; opacity: 0.7; }
54
+ .card--3 .row--done .row__title { color: var(--card-3-muted); }
55
55
  .row__done {
56
56
  width: var(--row-done-size, var(--tap-sm));
57
57
  height: var(--row-done-size, var(--tap-sm));
@@ -93,7 +93,7 @@
93
93
  .kv-cell > .icon { margin-bottom: var(--sp-1); }
94
94
  .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
95
95
  .kv-cell__key { font: var(--kv-cell-key-font, var(--type-caption)); color: var(--kv-cell-key-ink, var(--card-muted)); }
96
- .card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
96
+ .card--3 .kv-cell__key { color: var(--card-3-muted); }
97
97
 
98
98
  /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
99
99
  No tail: a rounded block whose corner nearest the sender is square-ish
@@ -143,7 +143,7 @@
143
143
  .bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--on-surface-muted)); }
144
144
  .bubble--right > .bubble__meta { justify-self: end; }
145
145
  .bubble__meta > .icon { color: var(--accent); }
146
- .card--3 .bubble__meta { color: inherit; opacity: 0.7; }
146
+ .card--3 .bubble__meta { color: var(--card-3-muted); }
147
147
 
148
148
  /* ---- timeline: a hairline with date chips, entries to the right ----
149
149
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
@@ -206,4 +206,4 @@
206
206
  cursor: pointer;
207
207
  }
208
208
  .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
209
- .card--3 .link--muted { color: inherit; opacity: 0.7; }
209
+ .card--3 .link--muted { color: var(--card-3-muted); }
package/css/step.css CHANGED
@@ -89,6 +89,9 @@
89
89
  .step > :is(.base-content, .pages) {
90
90
  flex: 0 1 auto;
91
91
  }
92
+ /* the "where am I" dots of a walk are the STEP's, one for all its pages — written after .pages, pinned to the base's foot above the bar's
93
+ room, so a short page never leaves them stranded under its last line (Lh 2026-09-22: the dots belong to the page, not a card, not a paragraph) */
94
+ .step > .pager__at { margin-top: auto; align-self: center; padding-block: var(--sp-3); flex: none; }
92
95
  .step > :is(.base-content, .pages) {
93
96
  min-height: 0;
94
97
  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 */
package/css/tokens.css CHANGED
@@ -155,6 +155,7 @@
155
155
  --_muted-on-paper: color-mix(in srgb, var(--color-ink) 66%, var(--color-paper, #ffffff));
156
156
  --_tone-2: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface, #f2f2f0));
157
157
  --_muted-on-tone-2: color-mix(in srgb, var(--color-ink, #141414) 72%, var(--_tone-2));
158
+ --_muted-on-brand: color-mix(in srgb, var(--color-on-brand) 70%, var(--color-brand)); /* the tone-3 card's secondary words: on-brand faded into brand — was opacity 0.7, which no theme could set (timebank #132, 2026-09-22) */
158
159
 
159
160
  /* app background ("earth-toned surface or shiny color, up to the app") */
160
161
  --surface: var(--color-surface, #f2f2f0);
@@ -168,6 +169,7 @@
168
169
 
169
170
  --card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
170
171
  --card-muted: var(--_muted-on-paper, #6b6b6b); /* secondary text inside cards (the default, tone-1 card) */
172
+ --card-3-muted: var(--_muted-on-brand, #b9b9b9); /* the same text on the tone-3 card — a real colour, like the two above: a theme whose brand is not near-black sets it to reach its contrast (the wireframe's #b9b9b9 = white at 0.7 on #141414, what the opacity gave) */
171
173
  --card-2-muted: var(--_muted-on-tone-2, #5f5f5f); /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
172
174
  a palette whose tone-2 ground is strong
173
175
  sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
package/dist/cardds.css CHANGED
@@ -196,6 +196,7 @@
196
196
  --_muted-on-paper: color-mix(in srgb, var(--color-ink) 66%, var(--color-paper, #ffffff));
197
197
  --_tone-2: color-mix(in srgb, var(--color-brand) 14%, var(--color-surface, #f2f2f0));
198
198
  --_muted-on-tone-2: color-mix(in srgb, var(--color-ink, #141414) 72%, var(--_tone-2));
199
+ --_muted-on-brand: color-mix(in srgb, var(--color-on-brand) 70%, var(--color-brand)); /* the tone-3 card's secondary words: on-brand faded into brand — was opacity 0.7, which no theme could set (timebank #132, 2026-09-22) */
199
200
 
200
201
  /* app background ("earth-toned surface or shiny color, up to the app") */
201
202
  --surface: var(--color-surface, #f2f2f0);
@@ -209,6 +210,7 @@
209
210
 
210
211
  --card-border: #141414; /* wireframe: visible outline; palettes may set transparent */
211
212
  --card-muted: var(--_muted-on-paper, #6b6b6b); /* secondary text inside cards (the default, tone-1 card) */
213
+ --card-3-muted: var(--_muted-on-brand, #b9b9b9); /* the same text on the tone-3 card — a real colour, like the two above: a theme whose brand is not near-black sets it to reach its contrast (the wireframe's #b9b9b9 = white at 0.7 on #141414, what the opacity gave) */
212
214
  --card-2-muted: var(--_muted-on-tone-2, #5f5f5f); /* the same text on the tone-2 card — 5.1:1 on its grey (#6b6b6b is 4.3:1, under AA);
213
215
  a palette whose tone-2 ground is strong
214
216
  sets this to its tone-2 ink — a muted shade cannot reach AA there (clay) */
@@ -493,10 +495,9 @@ button { font: inherit; cursor: pointer; }
493
495
  }
494
496
  .base-content > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
495
497
  /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
496
- the "where am I" dots under the words, centred — the same dots the centred card has under it */
497
- .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
498
+ (the walk's dots are NOT here — they are the Step's, after its Pages: step.css, Lh 2026-09-22) */
499
+ .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; align-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
498
500
  .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
499
- .base-content > .pager__at { justify-self: center; }
500
501
  .base-content .cta-pack {
501
502
  display: flex;
502
503
  align-items: start; /* buttons keep their own height, never the row's */
@@ -730,7 +731,8 @@ button { font: inherit; cursor: pointer; }
730
731
  and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
731
732
  simply switches. Only the card is named; the rest of the page must not
732
733
  cross-fade. */
733
- .screen__centre > .card, .pages > [aria-current] { view-transition-name: centre-card; }
734
+ /* the name is set by slideTo() on the moving element for the move only (2026-09-22): a static name on every walk's current
735
+ page made two walks on one page — the gallery — share it, and the browser aborts a transition with a duplicate name */
734
736
 
735
737
  /* ---- pages: the cards of a walk, all one height ----
736
738
  A sequence the member walks (an introduction) reads steadier when every card
@@ -1538,6 +1540,9 @@ button { font: inherit; cursor: pointer; }
1538
1540
  .step > :is(.base-content, .pages) {
1539
1541
  flex: 0 1 auto;
1540
1542
  }
1543
+ /* the "where am I" dots of a walk are the STEP's, one for all its pages — written after .pages, pinned to the base's foot above the bar's
1544
+ room, so a short page never leaves them stranded under its last line (Lh 2026-09-22: the dots belong to the page, not a card, not a paragraph) */
1545
+ .step > .pager__at { margin-top: auto; align-self: center; padding-block: var(--sp-3); flex: none; }
1541
1546
  .step > :is(.base-content, .pages) {
1542
1547
  min-height: 0;
1543
1548
  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 */
@@ -2669,7 +2674,7 @@ button { font: inherit; cursor: pointer; }
2669
2674
  .row__body { flex: 1; min-width: 0; display: grid; gap: var(--row-body-gap, var(--sp-0)); }
2670
2675
  .row__title { font: var(--row-title-font, var(--type-title)); overflow-wrap: anywhere; } /* wraps freely — no clip, so no padding trick for Thai marks (Lh 2026-09-16, same call as the card heading) */
2671
2676
  .row__meta { font: var(--row-meta-font, var(--type-caption)); color: var(--row-meta-ink, var(--card-muted)); display: flex; align-items: center; gap: var(--row-meta-gap, var(--sp-1)); }
2672
- .card--3 .row__meta { color: inherit; opacity: 0.7; }
2677
+ .card--3 .row__meta { color: var(--card-3-muted); }
2673
2678
  .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
2674
2679
  .row--lg { min-height: var(--row-lg-min-h, var(--fab)); } /* menu rows: a taller target, round arrow at the end; .rows gives them more air */
2675
2680
  .row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
@@ -2688,7 +2693,7 @@ button { font: inherit; cursor: pointer; }
2688
2693
  at --tap-sm, but not an input: nothing to press. */
2689
2694
  .row--done { pointer-events: none; cursor: default; }
2690
2695
  .row--done .row__title { color: var(--card-muted); }
2691
- .card--3 .row--done .row__title { color: inherit; opacity: 0.7; }
2696
+ .card--3 .row--done .row__title { color: var(--card-3-muted); }
2692
2697
  .row__done {
2693
2698
  width: var(--row-done-size, var(--tap-sm));
2694
2699
  height: var(--row-done-size, var(--tap-sm));
@@ -2730,7 +2735,7 @@ button { font: inherit; cursor: pointer; }
2730
2735
  .kv-cell > .icon { margin-bottom: var(--sp-1); }
2731
2736
  .kv-cell__val { font: var(--kv-cell-val-font, var(--type-h2)); letter-spacing: var(--track-h2); font-variant-numeric: tabular-nums; }
2732
2737
  .kv-cell__key { font: var(--kv-cell-key-font, var(--type-caption)); color: var(--kv-cell-key-ink, var(--card-muted)); }
2733
- .card--3 .kv-cell__key { color: inherit; opacity: 0.7; }
2738
+ .card--3 .kv-cell__key { color: var(--card-3-muted); }
2734
2739
 
2735
2740
  /* ---- bubble: a chat message — theirs on the left, mine on the right (Lh 2026-09-18, replaces the chip's tail) ----
2736
2741
  No tail: a rounded block whose corner nearest the sender is square-ish
@@ -2780,7 +2785,7 @@ button { font: inherit; cursor: pointer; }
2780
2785
  .bubble__meta { display: flex; align-items: center; gap: var(--bubble-meta-gap, var(--sp-1)); font: var(--bubble-meta-font, var(--type-caption)); color: var(--bubble-meta-ink, var(--on-surface-muted)); }
2781
2786
  .bubble--right > .bubble__meta { justify-self: end; }
2782
2787
  .bubble__meta > .icon { color: var(--accent); }
2783
- .card--3 .bubble__meta { color: inherit; opacity: 0.7; }
2788
+ .card--3 .bubble__meta { color: var(--card-3-muted); }
2784
2789
 
2785
2790
  /* ---- timeline: a hairline with date chips, entries to the right ----
2786
2791
  <ol class="timeline"><li class="timeline__item"><span class="timeline__mark">Wed 14</span><p class="t-body">…</p></li>…</ol> */
@@ -2843,7 +2848,7 @@ button { font: inherit; cursor: pointer; }
2843
2848
  cursor: pointer;
2844
2849
  }
2845
2850
  .link--muted { color: var(--link-muted-ink, var(--card-muted)); }
2846
- .card--3 .link--muted { color: inherit; opacity: 0.7; }
2851
+ .card--3 .link--muted { color: var(--card-3-muted); }
2847
2852
 
2848
2853
  /* ---- css/media.css — cover card, quote, tile grid, fold, mosaic, wave ---- */
2849
2854
  /* ============================================================
@@ -29,4 +29,4 @@ export type SlideDirection = 'next' | 'back';
29
29
  * (card.css, `--motion-slide`); the heading and the dots stay still. Without view transitions (the browser
30
30
  * floor) it just runs the update.
31
31
  */
32
- export declare function slideTo(direction: SlideDirection, update: () => void): void;
32
+ export declare function slideTo(direction: SlideDirection, update: () => void, scope?: ParentNode): void;
@@ -29,15 +29,25 @@ export function Pages({ className, ...rest }) {
29
29
  * (card.css, `--motion-slide`); the heading and the dots stay still. Without view transitions (the browser
30
30
  * floor) it just runs the update.
31
31
  */
32
- export function slideTo(direction, update) {
32
+ export function slideTo(direction, update, scope = document) {
33
33
  const root = document.documentElement;
34
34
  if (typeof document.startViewTransition !== 'function') {
35
35
  update();
36
36
  return;
37
37
  }
38
+ /* the moving thing is NAMED for the transition only while it moves, and only inside `scope` (the walk's Step or Centre —
39
+ an app has one walk per screen and can leave it out; the gallery shows several on one page, and two elements with the
40
+ same name abort the transition). CSS names nothing (2026-09-22). */
41
+ const moving = () => scope.querySelector('.pages > [aria-current], .screen__centre > .card');
42
+ const name = (el, v) => { if (el)
43
+ el.style.viewTransitionName = v; };
44
+ const before = moving();
45
+ name(before, 'centre-card');
38
46
  root.dataset.slide = direction;
39
- const transition = document.startViewTransition(() => flushSync(update));
47
+ let after = null;
48
+ const transition = document.startViewTransition(() => { flushSync(update); name(before, ''); after = moving(); name(after, 'centre-card'); });
40
49
  void transition.finished.finally(() => {
41
50
  delete root.dataset.slide;
51
+ name(after, '');
42
52
  });
43
53
  }
@@ -7,5 +7,5 @@ export interface PagerAtProps extends Omit<ComponentProps<'span'>, 'children'> {
7
7
  /** accessible name; defaults to "Page {at} of {of}" */
8
8
  label?: string;
9
9
  }
10
- /** PagerAt — the "where am I" dots, centred under the card that changes (a child of Centre): one accent dot at the current page. Never inside an ActionBar. */
10
+ /** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside an ActionBar, never inside a page. */
11
11
  export declare function PagerAt({ at, of, label, className, ...rest }: PagerAtProps): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** PagerAt — the "where am I" dots, centred under the card that changes (a child of Centre): one accent dot at the current page. Never inside an ActionBar. */
3
+ /** PagerAt — the "where am I" dots: one accent dot at the current page. Under the card that changes (a child of Centre), or a Step's child after its Pages — pinned to the base's foot. One per walk, never inside an ActionBar, never inside a page. */
4
4
  export function PagerAt({ at, of, label, className, ...rest }) {
5
5
  return (_jsx("span", { className: cx('pager__at', className), role: "img", "aria-label": label ?? `Page ${at} of ${of}`, ...rest, children: Array.from({ length: of }, (_, i) => _jsx("i", { className: i + 1 === at ? 'is-at' : undefined }, i)) }));
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
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.",