@usableapp/cardds 0.7.29 → 0.7.30

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,11 @@
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.30 — 2026-09-29
6
+
7
+ ### Changed
8
+ - `Sheet`: **the sheet comes up → its field has the cursor, always** (Lh 2026-09-29 — a habit of the Sheet, not of a screen). Whenever it rises — onto the screen (mounted up, or from `away`), or from peek to any open state, by a button, the handle, a drag or the app — once it has finished sliding the cursor goes to the field marked `data-autofocus` (SheetSteps marks its open step's first field), else the first field in its `SheetBody`. With no field, a sheet that came onto the screen gives it to its handle. A field that already has the cursor keeps it. Before: only an arrival from `away` (0.7.29: and a raise from peek) moved it, and only to a marked field. Nothing to change in a screen — remove any focus code of your own for a sheet's field.
9
+
5
10
  ## 0.7.29 — 2026-09-29
6
11
 
7
12
  ### Fixed
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
3
3
  import { useControlled, useLatest, warnOnce, watchSize } from '../state.js';
4
4
  import { cx } from '../cx.js';
5
5
  const ORDER = ['peek', 'half', '3q', 'full']; // low → high; `away` is below them all and outside them
6
+ const RANK = { away: 0, peek: 1, half: 2, '3q': 3, full: 4 };
6
7
  const TAP_REM = 0.5; // = --sp-2: a move shorter than this is a tap, not a drag
7
8
  const ms = (t) => Math.max(0, ...t.split(',').map((v) => parseFloat(v) * (v.trim().endsWith('ms') ? 1 : 1000) || 0)); // a computed `0.35s, 0s` list → its longest, in ms
8
9
  /**
@@ -26,7 +27,7 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
26
27
  const ref = useRef(null);
27
28
  const handleRef = useRef(null);
28
29
  const home = useRef(current !== 'peek' && current !== 'away' ? current : 'half'); // where a tap from peek returns to
29
- const was = useRef(current);
30
+ const was = useRef('away'); // mounted up = it came onto the screen: it rises from away like any arrival
30
31
  const skipClick = useRef(false);
31
32
  const [entered, setEntered] = useState(false); // false for the first frame: the class says `away`, so a sheet mounted up has somewhere to slide from
32
33
  const shown = entered ? current : 'away';
@@ -77,31 +78,39 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
77
78
  });
78
79
  };
79
80
  }, [latest]);
81
+ /* THE SHEET COMES UP → ITS FIELD HAS THE CURSOR, always (Lh 2026-09-29): whenever it rises — onto the screen (mounted up, or from `away`),
82
+ or from peek to any open state, by a button, the handle, a drag or the app — once it has finished sliding (Lh 2026-09-24: the keyboard
83
+ must not come up while it still moves) the cursor goes to the field marked `data-autofocus` (a SheetSteps marks its open step's first
84
+ field), else the body's first field; with no field, a sheet that came onto the screen gives it to its handle. A field that already
85
+ has the cursor keeps it. */
80
86
  useEffect(() => {
81
87
  if (current !== 'peek' && current !== 'away')
82
88
  home.current = current;
83
- const back = was.current === 'peek' && current !== 'peek' && current !== 'away' && !!ref.current?.querySelector('[data-autofocus]:not([inert] *)')
84
- && (!ref.current?.contains(document.activeElement) || document.activeElement === handleRef.current); // raised from peek by the handle: the marked field takes the cursor again (timebank login, Lh 2026-09-29) — never taken from a field that has it
85
- if ((was.current === 'away' && current !== 'away') || back) { // it just came onto the screen: the cursor goes to what is marked to take it first (`data-autofocus` — a SheetSteps marks its open step's first field), else the handle, for the keyboard user
86
- // …once it has finished sliding in (Lh 2026-09-24): the keyboard must not come up while the sheet still moves
87
- const el = ref.current;
88
- let done = false;
89
- const land = () => {
90
- if (done)
91
- return;
92
- done = true;
93
- el?.removeEventListener('transitionend', onEnd);
94
- const first = el?.querySelector('[data-autofocus]:not([inert] *)');
95
- (first ?? handleRef.current)?.focus({ preventScroll: true });
96
- };
97
- const onEnd = (e) => { if (e.target === el && e.propertyName === 'translate')
98
- land(); };
99
- el?.addEventListener('transitionend', onEnd);
100
- const t = setTimeout(land, 600); // no transition (reduced motion): land anyway
101
- was.current = current;
102
- return () => { clearTimeout(t); el?.removeEventListener('transitionend', onEnd); };
103
- }
89
+ const rose = current !== 'peek' && current !== 'away' && RANK[current] > RANK[was.current];
90
+ const from = was.current;
104
91
  was.current = current;
92
+ if (!rose)
93
+ return;
94
+ const el = ref.current;
95
+ let done = false;
96
+ const land = () => {
97
+ if (done || !el)
98
+ return;
99
+ done = true;
100
+ el.removeEventListener('transitionend', onEnd);
101
+ const at = document.activeElement;
102
+ if (at && el.contains(at) && at !== handleRef.current && at.matches('input, textarea, select, [contenteditable]'))
103
+ return;
104
+ const field = el.querySelector('[data-autofocus]:not([inert] *)')
105
+ ?? el.querySelector(':scope > .sheet__body :is(input:not([type=checkbox], [type=radio], [type=hidden], [disabled]), textarea, select):not([inert] *)');
106
+ (field ?? (from === 'away' ? handleRef.current : null))?.focus({ preventScroll: true });
107
+ };
108
+ const onEnd = (e) => { if (e.target === el && e.propertyName === 'translate')
109
+ land(); };
110
+ el?.addEventListener('transitionend', onEnd);
111
+ const t = setTimeout(land, 600); // no transition (reduced motion): land anyway
112
+ return () => { clearTimeout(t); el?.removeEventListener('transitionend', onEnd); if (!done)
113
+ was.current = from; }; // cut short (StrictMode's trial run, or a new state mid-slide): the rise still counts from where it began
105
114
  }, [current]);
106
115
  /* geometry, read once per gesture — never stored: how far it has slid, and where each state rests, in px from its top at full */
107
116
  const slid = (el) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.7.29",
3
+ "version": "0.7.30",
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.",