@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 +5 -0
- package/dist/sheets/Sheet.js +31 -22
- package/package.json +1 -1
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
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -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(
|
|
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
|
|
84
|
-
|
|
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.
|
|
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.",
|