@usableapp/cardds 0.7.20 → 0.7.22

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,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
+ ## 0.7.22 — 2026-09-29
6
+
7
+ ### Changed
8
+ - `StackSheet` (so `Home`): the open sheet's back button now stands at the head's far END, after the tabs, with a chevron pointing DOWN because the sheet folds down (Lh 2026-09-29). Before: at the start, chevron left. `backAt="start"` brings back the old place.
9
+ - `StackBack end`: the same for a head written by hand (`.sheet-stack__back--end`). Without `end` nothing changes.
10
+
11
+ ## 0.7.21 — 2026-09-29
12
+
13
+ ### Fixed
14
+ - `installScreenSlide()`: the phone's back slid the WHOLE screen on every history step, also on an entry of the same screen (an open `Home`/`SheetStack` sheet kept in `history.state` so back closes it) — the page slid while the stack closed. Now an entry with the same path + query goes back without a slide; the component moves itself. Nothing to change in an app (Lh 2026-09-29, timebank home).
15
+
5
16
  ## 0.7.20 — 2026-09-28
6
17
 
7
18
  Lh, from timebank on a real Android phone (card #153). Nothing to change in a screen.
package/css/stack.css CHANGED
@@ -311,7 +311,13 @@
311
311
  /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
312
312
  tappable"): a SMALL DISC — its ring kept, 32px instead of 44, the chevron in it at 20 — so the head gives it 32px + a gap
313
313
  (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
314
- .sheet-stack--tap > .card.is-open > .card__head { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
314
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back:not(.sheet-stack__back--end)) { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
315
+ /* AT THE END (Lh 2026-09-29, the home): after the tabs, chevron down — the open sheet folds down. The head's end gives it its room
316
+ while open; the last tab then sits on the inset (no reach into the padding) */
317
+ .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
318
+ .sheet-stack--tap .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) ease; }
319
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
320
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
315
321
  .sheet-stack--tap .sheet-stack__back > .icon { width: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); height: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); flex: none; } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
316
322
  .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
317
323
 
package/dist/cardds.css CHANGED
@@ -1268,7 +1268,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
1268
1268
  /* ---- the back button, SMALL (Lh 2026-09-23: "give the sheet head more room" — and then "a glyph alone does not read as
1269
1269
  tappable"): a SMALL DISC — its ring kept, 32px instead of 44, the chevron in it at 20 — so the head gives it 32px + a gap
1270
1270
  (was 56px). Its tap target stays 44px: an invisible ::after around the disc. ---- */
1271
- .sheet-stack--tap > .card.is-open > .card__head { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1271
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back:not(.sheet-stack__back--end)) { padding-left: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1272
+ /* AT THE END (Lh 2026-09-29, the home): after the tabs, chevron down — the open sheet folds down. The head's end gives it its room
1273
+ while open; the last tab then sits on the inset (no reach into the padding) */
1274
+ .sheet-stack--tap .sheet-stack__back--end { left: auto; right: 0; }
1275
+ .sheet-stack--tap .card__head:has(> .sheet-stack__back--end) { transition: padding-right var(--motion-sheet) ease; }
1276
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) { padding-right: var(--stack-back-room, calc(var(--_back-size) + var(--sp-2))); }
1277
+ .sheet-stack--tap > .card.is-open > .card__head:has(> .sheet-stack__back--end) > .stack-sheet__tabs { margin-right: 0; }
1272
1278
  .sheet-stack--tap .sheet-stack__back > .icon { width: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); height: var(--stack-back-icon, calc(var(--tap-base) * 13 / 24)); flex: none; } /* 26: the chevron fills the small disc (Lh 2026-09-23) */
1273
1279
  .sheet-stack--tap .sheet-stack__back::after { content: ""; position: absolute; inset: calc((var(--_back-size) - var(--tap-sm)) / 2); }
1274
1280
 
@@ -10,6 +10,7 @@ export declare function slideScreen(update: () => unknown, direction?: SlideDire
10
10
  /**
11
11
  * installScreenSlide — call ONCE when the app starts: the phone's / browser's BACK (and forward) buttons slide the screen too,
12
12
  * the way `back` (and `next`) does. It listens to `popstate` before the router renders; the direction is read from the history
13
- * entry's index (`history.state.idx`, which React Router keeps; without it: back). Returns the uninstall.
13
+ * entry's index (`history.state.idx`, which React Router keeps; without it: back). An entry of the SAME screen (same path and
14
+ * query, only its state differs — an open sheet pushed so the phone's back closes it) does not slide. Returns the uninstall.
14
15
  */
15
16
  export declare function installScreenSlide(): () => void;
@@ -47,17 +47,25 @@ export function slideScreen(update, direction = 'next', scope = document) {
47
47
  /**
48
48
  * installScreenSlide — call ONCE when the app starts: the phone's / browser's BACK (and forward) buttons slide the screen too,
49
49
  * the way `back` (and `next`) does. It listens to `popstate` before the router renders; the direction is read from the history
50
- * entry's index (`history.state.idx`, which React Router keeps; without it: back). Returns the uninstall.
50
+ * entry's index (`history.state.idx`, which React Router keeps; without it: back). An entry of the SAME screen (same path and
51
+ * query, only its state differs — an open sheet pushed so the phone's back closes it) does not slide. Returns the uninstall.
51
52
  */
52
53
  export function installScreenSlide() {
53
- let idx = history.state?.idx;
54
+ const where = () => location.pathname + location.search;
55
+ let idx = history.state?.idx, at = where();
54
56
  const push = history.pushState.bind(history), replace = history.replaceState.bind(history);
55
- history.pushState = (...a) => { push(...a); idx = history.state?.idx; };
56
- history.replaceState = (...a) => { replace(...a); idx = history.state?.idx; };
57
+ const seen = () => { idx = history.state?.idx; at = where(); };
58
+ history.pushState = (...a) => { push(...a); seen(); };
59
+ history.replaceState = (...a) => { replace(...a); seen(); };
57
60
  const pop = () => {
58
- const to = history.state?.idx;
61
+ const to = history.state?.idx, same = where() === at;
59
62
  const dir = to != null && idx != null && to > idx ? 'next' : 'back';
60
63
  idx = to;
64
+ at = where();
65
+ /* the SAME screen (path + query unchanged, only the entry's state — an open sheet, a tab): the screen stays, the
66
+ component moves itself (a SheetStack closes as a stack does). Lh 2026-09-29, timebank home. */
67
+ if (same)
68
+ return;
61
69
  slideScreen(() => undefined, dir); // the router renders on its own, right after this: the slide waits for its new Screen
62
70
  };
63
71
  addEventListener('popstate', pop, { capture: true });
@@ -2,7 +2,10 @@ import type { ComponentProps } from 'react';
2
2
  export interface StackBackProps extends ComponentProps<'button'> {
3
3
  /** accessible name */
4
4
  label?: string;
5
+ /** at the head's END (after its tabs / icons), chevron DOWN — the open sheet folds down (Lh 2026-09-29, the home);
6
+ * default: at its start, chevron left */
7
+ end?: boolean;
5
8
  }
6
9
  /** StackBack — the back button a tapped stack sheet reveals in its head; first child of that sheet's CardHead. Hidden until the sheet is open.
7
10
  * A SMALL disc (Lh 2026-09-23): 32px with a chevron — the head keeps its room for the title and the tabs; the 44px tap target stays. */
8
- export declare function StackBack({ label, className, ...rest }: StackBackProps): import("react").JSX.Element;
11
+ export declare function StackBack({ label, end, className, ...rest }: StackBackProps): import("react").JSX.Element;
@@ -4,6 +4,6 @@ import { Icon } from '../type/Icon.js';
4
4
  import { IconBtn } from '../actions/IconBtn.js';
5
5
  /** StackBack — the back button a tapped stack sheet reveals in its head; first child of that sheet's CardHead. Hidden until the sheet is open.
6
6
  * A SMALL disc (Lh 2026-09-23): 32px with a chevron — the head keeps its room for the title and the tabs; the 44px tap target stays. */
7
- export function StackBack({ label = 'Back', className, ...rest }) {
8
- return _jsx(IconBtn, { sm: true, label: label, className: cx('sheet-stack__back', className), ...rest, children: _jsx(Icon, { name: "chevron-left" }) });
7
+ export function StackBack({ label = 'Back', end, className, ...rest }) {
8
+ return _jsx(IconBtn, { sm: true, label: label, className: cx('sheet-stack__back', end && 'sheet-stack__back--end', className), ...rest, children: _jsx(Icon, { name: end ? 'chevron-down' : 'chevron-left' }) });
9
9
  }
@@ -26,6 +26,8 @@ export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'
26
26
  foot?: ReactNode;
27
27
  /** the back button's label (shown once the sheet is open) */
28
28
  backLabel?: string;
29
+ /** where the back button stands: `end` (default — after the tabs, chevron DOWN: the sheet folds down, Lh 2026-09-29) or `start` (before the title, chevron left) */
30
+ backAt?: 'start' | 'end';
29
31
  }
30
32
  /**
31
33
  * StackSheet — ONE sheet of a `SheetStack`, in the one sheet shape: SHEET HEAD (back · title · tabs or an icon) pinned at the top ·
@@ -33,4 +35,4 @@ export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'
33
35
  * 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
34
36
  * is closed — see `Home`). A direct child of `SheetStack`.
35
37
  */
36
- export declare function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel, className, children, ...rest }: StackSheetProps): import("react").JSX.Element;
38
+ export declare function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel, backAt, className, children, ...rest }: StackSheetProps): import("react").JSX.Element;
@@ -17,7 +17,7 @@ import { Badge } from '../numbers/Badge.js';
17
17
  * 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
18
18
  * is closed — see `Home`). A direct child of `SheetStack`.
19
19
  */
20
- export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = 'Back', className, children, ...rest }) {
20
+ export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = 'Back', backAt = 'end', className, children, ...rest }) {
21
21
  const [tab, pick] = useControlled(tabProp, defaultTab ?? tabs?.[0]?.id, onTabChange);
22
22
  // 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
23
23
  // is measured here (the tabs may grow with the text size) and handed to the CSS as --_over
@@ -35,5 +35,5 @@ export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab,
35
35
  measure();
36
36
  return watchSize([h, run], measure);
37
37
  }, [title]);
38
- return (_jsxs(Card, { tone: tone, className: cx('stack-sheet', className), ...rest, children: [_jsxs(CardHead, { children: [_jsx(StackBack, { label: backLabel }), _jsx(Text, { level: "h2", ref: titleRef, className: "stack-sheet__title", 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] }), _jsx(SheetBody, { role: tabs?.length ? 'tabpanel' : undefined, children: children }), foot != null && _jsx(CardFoot, { children: foot })] }));
38
+ return (_jsxs(Card, { 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: "stack-sheet__title", 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 })] }));
39
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.20",
3
+ "version": "0.7.22",
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.",