@usableapp/cardds 0.6.7 → 0.6.9

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.6.9 — 2026-09-23
6
+
7
+ ### Walkthrough (Lh 2026-09-23)
8
+ - **Swipe:** a sideways swipe turns the page (left = next, right = back). The walk still scrolls up and down; a drag must be mostly sideways and at least 48px to count.
9
+ - **Page turn:** the new page slides in from the side as itself, unscrolled. There is no vertical glide, because the leaving and arriving pages now have two separate transition names (`centre-card-out` / `centre-card-in`, was one `centre-card`).
10
+ - **Picture:** a page that opens on a picture shows it full width from the top edge, as tall as the lead (15%) + `--base-content-media-h`; the words start where they did.
11
+ - **Width:** the walk's scroll box is the whole content, edge to edge. The words keep the screen's inset inside each `Page`.
12
+ - **Scroll room:** each page's bottom padding is exactly the bottom layer's height (`--bottom-h`). A page scrolls only when the layer would cover words; at 100% text nothing scrolls.
13
+
14
+ ## 0.6.8 — 2026-09-23
15
+
16
+ - **`Walkthrough`: a new page starts at its lead.** On a page turn (the pager, or the app changing `at`), the walk scrolls back to the top, so the next page never opens where the last one was scrolled to (Lh 2026-09-23).
17
+
5
18
  ## 0.6.7 — 2026-09-23
6
19
 
7
20
  ### A walk scrolls as one run (Lh 2026-09-23 — answers timebank #138)
package/css/card.css CHANGED
@@ -236,21 +236,21 @@
236
236
  <section class="page" inert>…</section>
237
237
  </div></section> <div class="action-bar action-bar--pager">← →</div> */
238
238
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
239
- ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
239
+ ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in) {
240
240
  animation-duration: var(--motion-slide);
241
241
  animation-timing-function: ease-out;
242
242
  mix-blend-mode: normal;
243
243
  }
244
- :root[data-slide="next"]::view-transition-old(centre-card) { animation-name: centre-card-out-left; }
245
- :root[data-slide="next"]::view-transition-new(centre-card) { animation-name: centre-card-in-right; }
246
- :root[data-slide="back"]::view-transition-old(centre-card) { animation-name: centre-card-out-right; }
247
- :root[data-slide="back"]::view-transition-new(centre-card) { animation-name: centre-card-in-left; }
244
+ :root[data-slide="next"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-left; }
245
+ :root[data-slide="next"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-right; }
246
+ :root[data-slide="back"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-right; }
247
+ :root[data-slide="back"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-left; }
248
248
  @keyframes centre-card-out-left { to { translate: -100% 0; opacity: 0; } }
249
249
  @keyframes centre-card-out-right { to { translate: 100% 0; opacity: 0; } }
250
250
  @keyframes centre-card-in-right { from { translate: 100% 0; opacity: 0; } }
251
251
  @keyframes centre-card-in-left { from { translate: -100% 0; opacity: 0; } }
252
252
  @media (prefers-reduced-motion: reduce) {
253
- ::view-transition-old(centre-card), ::view-transition-new(centre-card) { animation: none; }
253
+ ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in) { animation: none; }
254
254
  }
255
255
  /* the page dots under the card sit centred under it — the indicator belongs
256
256
  with the page that changes, never in the bar (Lh, 2026-09-12) */
package/css/step.css CHANGED
@@ -107,11 +107,12 @@
107
107
  (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
108
108
  padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
109
109
  The walk shows no scrollbar. */
110
- .step > .pages { margin-top: 0; scrollbar-width: none; } /* the scrolling box starts at the step's top edge (Lh 2026-09-23) */
110
+ .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) */
111
+ .step > .pages > .page { padding-inline: var(--screen-pad); } /* the words keep the inset INSIDE it */
111
112
  .step > .pages > .page { 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 */
112
113
  .step > .pages::-webkit-scrollbar { display: none; }
113
114
  .step > .pages > .page {
114
- padding-bottom: calc(var(--screen-lead, 15cqh) + var(--bottom-h, 0rem)); /* + the bottom layer, measured by Screen: the last word scrolls clear of it */
115
+ 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) */
115
116
  }
116
117
 
117
118
  /* ============================================================
@@ -143,3 +144,17 @@
143
144
  them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
144
145
  .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
145
146
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
147
+ /* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
148
+ the lead + the picture's own height (--base-content-media-h), square — so the words start where they always did */
149
+ .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
150
+ .step > .pages > .page > :is(img, .ph):first-child {
151
+ width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
152
+ max-width: none;
153
+ margin-inline: calc(-1 * var(--screen-pad));
154
+ height: calc(var(--_lead, 0rem) + var(--base-content-media-h, 14rem));
155
+ max-height: none;
156
+ border-radius: 0;
157
+ border-inline: 0;
158
+ border-top: 0;
159
+ object-fit: cover;
160
+ }
package/dist/cardds.css CHANGED
@@ -768,21 +768,21 @@ button { font: inherit; cursor: pointer; }
768
768
  <section class="page" inert>…</section>
769
769
  </div></section> <div class="action-bar action-bar--pager">← →</div> */
770
770
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
771
- ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
771
+ ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in) {
772
772
  animation-duration: var(--motion-slide);
773
773
  animation-timing-function: ease-out;
774
774
  mix-blend-mode: normal;
775
775
  }
776
- :root[data-slide="next"]::view-transition-old(centre-card) { animation-name: centre-card-out-left; }
777
- :root[data-slide="next"]::view-transition-new(centre-card) { animation-name: centre-card-in-right; }
778
- :root[data-slide="back"]::view-transition-old(centre-card) { animation-name: centre-card-out-right; }
779
- :root[data-slide="back"]::view-transition-new(centre-card) { animation-name: centre-card-in-left; }
776
+ :root[data-slide="next"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-left; }
777
+ :root[data-slide="next"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-right; }
778
+ :root[data-slide="back"]::view-transition-old(centre-card-out) { animation-name: centre-card-out-right; }
779
+ :root[data-slide="back"]::view-transition-new(centre-card-in) { animation-name: centre-card-in-left; }
780
780
  @keyframes centre-card-out-left { to { translate: -100% 0; opacity: 0; } }
781
781
  @keyframes centre-card-out-right { to { translate: 100% 0; opacity: 0; } }
782
782
  @keyframes centre-card-in-right { from { translate: 100% 0; opacity: 0; } }
783
783
  @keyframes centre-card-in-left { from { translate: -100% 0; opacity: 0; } }
784
784
  @media (prefers-reduced-motion: reduce) {
785
- ::view-transition-old(centre-card), ::view-transition-new(centre-card) { animation: none; }
785
+ ::view-transition-old(centre-card-out), ::view-transition-new(centre-card-in) { animation: none; }
786
786
  }
787
787
  /* the page dots under the card sit centred under it — the indicator belongs
788
788
  with the page that changes, never in the bar (Lh, 2026-09-12) */
@@ -1633,11 +1633,12 @@ button { font: inherit; cursor: pointer; }
1633
1633
  (the picture is never pinned). Each page starts at the screen's lead and ends with the same room under it — the lead as
1634
1634
  padding above AND below — so the words begin near the middle and, when a big text size runs them long, scroll up clear.
1635
1635
  The walk shows no scrollbar. */
1636
- .step > .pages { margin-top: 0; scrollbar-width: none; } /* the scrolling box starts at the step's top edge (Lh 2026-09-23) */
1636
+ .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) */
1637
+ .step > .pages > .page { padding-inline: var(--screen-pad); } /* the words keep the inset INSIDE it */
1637
1638
  .step > .pages > .page { 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 */
1638
1639
  .step > .pages::-webkit-scrollbar { display: none; }
1639
1640
  .step > .pages > .page {
1640
- padding-bottom: calc(var(--screen-lead, 15cqh) + var(--bottom-h, 0rem)); /* + the bottom layer, measured by Screen: the last word scrolls clear of it */
1641
+ 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) */
1641
1642
  }
1642
1643
 
1643
1644
  /* ============================================================
@@ -1669,6 +1670,20 @@ button { font: inherit; cursor: pointer; }
1669
1670
  them — timebank #137); the words keep the screen's gap as their lead (--_lead, .appbar + .step above) */
1670
1671
  .screen--fill > .appbar:has(+ .step) { margin-bottom: calc(-1 * var(--screen-gap)); }
1671
1672
  .screen--fill > .appbar + .sheet-stage { margin-top: 0; }
1673
+ /* a walk page that OPENS on a picture (Lh 2026-09-23): the picture takes the lead's room — as wide as the content (edge to edge), from the top edge, as tall as
1674
+ the lead + the picture's own height (--base-content-media-h), square — so the words start where they always did */
1675
+ .step > .pages > .page:has(> :is(img, .ph):first-child) { margin-top: 0; }
1676
+ .step > .pages > .page > :is(img, .ph):first-child {
1677
+ width: calc(100% + 2 * var(--screen-pad)); /* the content's width: the page's inset given back */
1678
+ max-width: none;
1679
+ margin-inline: calc(-1 * var(--screen-pad));
1680
+ height: calc(var(--_lead, 0rem) + var(--base-content-media-h, 14rem));
1681
+ max-height: none;
1682
+ border-radius: 0;
1683
+ border-inline: 0;
1684
+ border-top: 0;
1685
+ object-fit: cover;
1686
+ }
1672
1687
 
1673
1688
  /* ---- css/forms.css — outlined fields, add-row ---- */
1674
1689
  /* ============================================================
@@ -41,11 +41,14 @@ export function slideTo(direction, update, scope = document) {
41
41
  const moving = () => scope.querySelector('.pages > [aria-current], .screen__centre > .card');
42
42
  const name = (el, v) => { if (el)
43
43
  el.style.viewTransitionName = v; };
44
+ /* the leaving page and the arriving one are TWO names, never one: one name makes the browser morph the box from where the old
45
+ page was (scrolled up) to where the new one is (at its top) — a vertical glide. Two names = each only slides sideways, the new
46
+ one arriving as itself, unscrolled (Lh 2026-09-23). */
44
47
  const before = moving();
45
- name(before, 'centre-card');
48
+ name(before, 'centre-card-out');
46
49
  root.dataset.slide = direction;
47
50
  let after = null;
48
- const transition = document.startViewTransition(() => { flushSync(update); name(before, ''); after = moving(); name(after, 'centre-card'); });
51
+ const transition = document.startViewTransition(() => { flushSync(update); name(before, ''); after = moving(); name(after, 'centre-card-in'); });
49
52
  void transition.finished.finally(() => {
50
53
  delete root.dataset.slide;
51
54
  name(after, '');
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState } from 'react';
2
+ import { useLayoutEffect, useRef, useState } from 'react';
3
3
  import { Screen } from '../scaffold/Screen.js';
4
4
  import { Step } from '../sheets/Step.js';
5
5
  import { Page } from '../scaffold/Page.js';
@@ -19,6 +19,10 @@ export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta,
19
19
  const [own, setOwn] = useState(defaultAt);
20
20
  const at = atProp ?? own;
21
21
  const step = useRef(null);
22
+ // a new page starts where a page starts — at its lead, not where the last one was scrolled to (Lh 2026-09-23). A LAYOUT effect:
23
+ // it runs inside the page turn's commit, before the view transition takes its picture — only the sideways slide shows, no scroll
24
+ useLayoutEffect(() => { const p = step.current?.querySelector(':scope > .pages'); if (p)
25
+ p.scrollTop = 0; }, [at]);
22
26
  const count = pages.length;
23
27
  const go = (to) => {
24
28
  if (to < 0 || to >= count || to === at)
@@ -26,10 +30,27 @@ export function Walkthrough({ pages, at: atProp, defaultAt = 0, onAtChange, cta,
26
30
  slideTo(to > at ? 'next' : 'back', () => { if (atProp === undefined)
27
31
  setOwn(to); onAtChange?.(to); }, step.current ?? document);
28
32
  };
33
+ /* a sideways SWIPE turns the page (Lh 2026-09-23): left = next, right = back. The walk still scrolls up and down itself
34
+ (touch-action: pan-y); a swipe must be mostly sideways and long enough, so a scroll never turns a page */
35
+ const start = useRef(null);
36
+ const swipe = {
37
+ onPointerDown: (e) => { start.current = { x: e.clientX, y: e.clientY }; },
38
+ onPointerUp: (e) => {
39
+ const s0 = start.current;
40
+ start.current = null;
41
+ if (!s0)
42
+ return;
43
+ const dx = e.clientX - s0.x, dy = e.clientY - s0.y;
44
+ if (Math.abs(dx) < 48 || Math.abs(dx) < 1.5 * Math.abs(dy))
45
+ return;
46
+ go(dx < 0 ? at + 1 : at - 1);
47
+ },
48
+ onPointerCancel: () => { start.current = null; },
49
+ };
29
50
  const row = typeof cta === 'function' ? cta({ at, count, go }) : cta;
30
51
  const custom = row !== undefined && row !== null;
31
52
  const pager = (_jsxs(_Fragment, { children: [_jsx(IconBtn, { icon: "arrow-left", label: backLabel, disabled: at === 0, onClick: () => go(at - 1) }), _jsx(IconBtn, { invert: true, icon: "arrow-right", label: nextLabel, disabled: at === count - 1, onClick: () => go(at + 1) })] }));
32
53
  const dots = count > 1 ? _jsx(PagerAt, { at: at + 1, of: count }) : null;
33
54
  const bottom = dots || controls.length || ctaLink != null ? (_jsxs(ActionBar, { variant: "tiers", children: [dots, controls.map((c, i) => _jsx(ActionBarTier, { children: c }, i)), _jsx(ActionBarTier, { pager: true, children: custom ? row : pager }), ctaLink != null && _jsx(ActionBarTier, { className: "action-bar__tier--link", children: ctaLink })] })) : (_jsx(ActionBar, { variant: "pager", children: custom ? row : pager }));
34
- return (_jsxs(Screen, { fill: true, bottom: bottom, ...rest, children: [_jsx(Step, { ref: step, children: _jsx(Pages, { children: pages.map((p, i) => (_jsxs(Page, { "aria-current": i === at ? 'step' : undefined, inert: i !== at || undefined, children: [p.media, _jsx(CardHead, { level: "h1", heading: p.heading }), typeof p.body === 'string' ? _jsx(Text, { level: "body", children: p.body }) : p.body] }, i))) }) }), children] }));
55
+ return (_jsxs(Screen, { fill: true, bottom: bottom, ...rest, children: [_jsx(Step, { ref: step, children: _jsx(Pages, { ...swipe, children: pages.map((p, i) => (_jsxs(Page, { "aria-current": i === at ? 'step' : undefined, inert: i !== at || undefined, children: [p.media, _jsx(CardHead, { level: "h1", heading: p.heading }), typeof p.body === 'string' ? _jsx(Text, { level: "body", children: p.body }) : p.body] }, i))) }) }), children] }));
35
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
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.",