@usableapp/cardds 0.7.49 → 0.7.51
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 +16 -0
- package/README.md +5 -2
- package/cardds.js +6 -1
- package/css/choice.css +4 -3
- package/css/forms.css +3 -2
- package/css/stack.css +20 -4
- package/css/tokens.css +1 -1
- package/dist/cardds.css +28 -10
- package/dist/sheets/Sheet.d.ts +1 -3
- package/dist/sheets/Sheet.js +14 -207
- package/dist/sheets/SheetStack.d.ts +6 -3
- package/dist/sheets/SheetStack.js +328 -4
- package/dist/sheets/SheetSteps.js +70 -92
- package/dist/sheets/sheetDrag.d.ts +9 -0
- package/dist/sheets/sheetDrag.js +49 -0
- package/dist/sheets/sheetMeasure.d.ts +10 -0
- package/dist/sheets/sheetMeasure.js +84 -0
- package/dist/sheets/sheetSlide.d.ts +9 -0
- package/dist/sheets/sheetSlide.js +96 -0
- package/dist/sheets/sheetStepsPile.d.ts +8 -0
- package/dist/sheets/sheetStepsPile.js +70 -0
- package/dist/templates/Home.d.ts +3 -1
- package/dist/templates/Home.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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.51 — 2026-10-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`SheetStack`: a peeked head opens on a pull up, and an open head folds on a pull down** (Lh 2026-10-04). A tap still does both. The finger starts on the title, the back button, or the head itself. A tab, or any other button in the head, stays a click. The card follows the finger, and past the commit it opens or folds through `onOpenChange`, the same ask as a tap. A short pull springs back. Nothing to change in a screen for the open and the fold.
|
|
9
|
+
- **`Home` and `SheetStack` take `onRefresh`**: a pull down in the open sheet's content, already scrolled to the top, asks `onRefresh(index)`. The sheet stays open. The words drop with the finger, then spring back. The stack does not fetch — reload that section yourself. Omit `onRefresh` and that pull only scrolls. If the content is scrolled down, the finger scrolls. `<Home onRefresh={(index) => …}>` forwards it.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **A pull down on a closed stack's empty base no longer reaches the browser's reload.** The base does not scroll and does not chain the gesture. Nothing to change in a screen.
|
|
13
|
+
|
|
14
|
+
## 0.7.50 — 2026-10-04
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **With the keyboard up, the screen's top stays in sight** (Lh 2026-10-04, timebank on an iPhone): when its keyboard opens, an iPhone slides the whole page up to show the field — a sheet's title went over the clock, a `Track` under the camera. cardds now measures that slide (`--vv-top`) and adds it to the screen's top reserve (`--safe-top`), so the top of a fill screen, an `AppBar`, a `Track`, a `Snackbar` stay where they were. Nothing to change in a screen. If your own CSS reads `env(safe-area-inset-top)` directly for something at the top of a cardds screen, read `--safe-top` instead.
|
|
18
|
+
- **`SheetSteps`: the phone's keyboard no longer drops when a step changes** (Lh 2026-10-04, timebank as a home-screen app on an iPhone): the pad went down at every step change and the member had to tap a cell to get it back. On a touch screen the cursor now waits in the steps' hidden holder from the moment the step's button is pressed, and no script moves it after that; the open step's first field shows the cursor (a `Pin`'s caret, a `Field`'s full border) and takes the first key typed. A desktop is unchanged. Nothing to change in a screen — but `document.activeElement` is the holder (`.sheet-steps__holder`) until the first key, not the field: do not test for the field's focus right after a step change on a phone. Arriving on a NEW screen (a route change) still shows no keyboard until a tap — an iPhone allows none.
|
|
19
|
+
- **`Field`: the words keep 12px from the lead icon** — 0.7.49 took the browser's padding off inputs, and the words stood against the icon. The gap is the Field's own now (`--field-gap`, 12px; it was 8px plus the browser's padding: 2px on a desktop, about 13px on an iPhone). If your theme sets `--field-gap`, it still wins.
|
|
20
|
+
|
|
5
21
|
## 0.7.49 — 2026-10-03
|
|
6
22
|
|
|
7
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -178,12 +178,15 @@ the card is the material, the sheet is what it is in the stack.
|
|
|
178
178
|
front one too, peeks at the stack's bottom; the headroom above is the base's.
|
|
179
179
|
An optional `<div class="sheet-stack__base">` — written AFTER the sheets, so
|
|
180
180
|
it takes no sheet slot — is what the base shows there, behind every sheet.
|
|
181
|
-
A tap opens any sheet over it
|
|
181
|
+
A tap, or a pull up, opens any sheet over it. The open title or its back
|
|
182
|
+
button, tapped or pulled down, drops it home. A pull down in the open
|
|
183
|
+
content, already at the top, asks `onRefresh` — omit it and that pull only
|
|
184
|
+
scrolls. A tab in the head stays a click.
|
|
182
185
|
- **Five sheets max.** Slots are declared per `:nth-child` in `stack.css`; a
|
|
183
186
|
sixth would render at slot 0, on top of the first.
|
|
184
187
|
- **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
|
|
185
188
|
hidden until that sheet is open, then slides into a reserved header slot.
|
|
186
|
-
- **No ids, no wiring.** `SheetStack` holds which sheet is open (`open` + `onOpenChange`, or `defaultOpen`).
|
|
189
|
+
- **No ids, no wiring.** `SheetStack` holds which sheet is open (`open` + `onOpenChange`, or `defaultOpen`). A pull down in the open content asks `onRefresh(index)`. The stack does not fetch.
|
|
187
190
|
- **`.card-stack` is gone.** That was this component's first name and the wrong
|
|
188
191
|
one — the members are sheets and the stack is the menu, not a list of cards.
|
|
189
192
|
Never bring it back, not even as an alias.
|
package/cardds.js
CHANGED
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
bar and the sheet's control disappear behind the keyboard unless they
|
|
12
12
|
read this; Android with interactive-widget=resizes-content (set it in the
|
|
13
13
|
viewport meta) shrinks the layout viewport, and this stays 0. A pinch zoom
|
|
14
|
-
also shrinks the visual viewport, and is not a keyboard — ignored.
|
|
14
|
+
also shrinks the visual viewport, and is not a keyboard — ignored.
|
|
15
|
+
--vv-top: how far the phone has PANNED the page up to show a focused field (iOS does it when the keyboard opens, even on
|
|
16
|
+
a page that cannot scroll): everything on the screen slides up by that much, its top under the status bar and the camera
|
|
17
|
+
(timebank on an iPhone, Lh 2026-10-04 — a sheet's title over the clock, a Track under the island). The screen's top
|
|
18
|
+
reserve (--safe-top, tokens.css) adds it, so the top stays where the eye is; --kb already counts it out at the foot. */
|
|
15
19
|
if (window.visualViewport) {
|
|
16
20
|
const vv = window.visualViewport;
|
|
17
21
|
const publish = () => {
|
|
18
22
|
const covered = vv.scale > 1.01 ? 0 : Math.max(0, Math.round(window.innerHeight - vv.height - vv.offsetTop));
|
|
19
23
|
document.documentElement.style.setProperty('--kb', covered + 'px');
|
|
24
|
+
document.documentElement.style.setProperty('--vv-top', (vv.scale > 1.01 ? 0 : Math.max(0, Math.round(vv.offsetTop))) + 'px');
|
|
20
25
|
};
|
|
21
26
|
vv.addEventListener('resize', publish);
|
|
22
27
|
vv.addEventListener('scroll', publish);
|
package/css/choice.css
CHANGED
|
@@ -218,19 +218,20 @@
|
|
|
218
218
|
.pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, 0.5s) steps(1); }
|
|
219
219
|
@keyframes pin-flash { 0% { -webkit-text-fill-color: currentColor; background-size: 0 0; } }
|
|
220
220
|
.pin--secret .pin__cell::selection { background: transparent; } /* its digit is selected when the cell takes the cursor: nothing to show of it */
|
|
221
|
-
.pin__cell:focus { outline: none;
|
|
221
|
+
.pin__cell:focus { outline: none; }
|
|
222
|
+
.pin__cell:is(:focus, [data-cursor]) { border-color: var(--pin-cell-active-border, currentColor); } /* [data-cursor]: SheetSteps holds the phone's keyboard for this cell — it shows the cursor it is about to get */
|
|
222
223
|
/* asleep (Pin sets it): the cursor was put here by a script on a phone and no keyboard is up — nothing can be typed until a tap, so
|
|
223
224
|
the cell does not say "ready": the resting border, no caret */
|
|
224
225
|
.pin--asleep .pin__cell:focus { border-color: var(--pin-cell-border, var(--outline)); }
|
|
225
226
|
.pin--asleep .pin__cell:focus:placeholder-shown { background-image: none; animation: none; }
|
|
226
|
-
.pin__cell:focus:placeholder-shown {
|
|
227
|
+
.pin__cell:is(:focus, [data-cursor]):placeholder-shown {
|
|
227
228
|
background-image: linear-gradient(currentColor, currentColor);
|
|
228
229
|
background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
|
|
229
230
|
background-position: center;
|
|
230
231
|
background-repeat: no-repeat;
|
|
231
232
|
}
|
|
232
233
|
@media (prefers-reduced-motion: no-preference) {
|
|
233
|
-
.pin__cell:focus:placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
|
|
234
|
+
.pin__cell:is(:focus, [data-cursor]):placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
|
|
234
235
|
@keyframes pin-caret { 50% { background-size: 0 0; } }
|
|
235
236
|
}
|
|
236
237
|
|
package/css/forms.css
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
.field {
|
|
8
8
|
display: flex;
|
|
9
9
|
align-items: center;
|
|
10
|
-
gap: var(--field-gap, var(--sp-
|
|
10
|
+
gap: var(--field-gap, var(--sp-3)); /* 12 between the lead icon and the words: the field's OWN room (Lh 2026-10-04) — it was sp-2 plus whatever padding the
|
|
11
|
+
browser gave the input (2px on a desktop, 0.5em on an iPhone); with that padding taken off (base.css) the words stood against the icon */
|
|
11
12
|
border: var(--border-w) solid var(--field-border, var(--action-border));
|
|
12
13
|
border-radius: var(--field-radius, var(--r-control));
|
|
13
14
|
background: var(--field-bg, var(--action-bg));
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
}
|
|
31
32
|
.field input::placeholder { color: inherit; opacity: 0.55; font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
|
|
32
33
|
/* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
|
|
33
|
-
.field:has(:focus) { border-color: var(--field-active-border, currentColor); }
|
|
34
|
+
.field:has(:focus, [data-cursor]) { border-color: var(--field-active-border, currentColor); } /* [data-cursor]: SheetSteps holds the phone's keyboard for this field */
|
|
34
35
|
|
|
35
36
|
/* field with its action inside: date pick, send, go — the button sits flush right */
|
|
36
37
|
.field--action { padding-right: var(--sp-1); min-height: var(--field-action-min-h, var(--tap-lg)); }
|
package/css/stack.css
CHANGED
|
@@ -167,12 +167,22 @@
|
|
|
167
167
|
}
|
|
168
168
|
.sheet-stack--tap > .card.is-open {
|
|
169
169
|
/* up by its own resting offset — pure movement, nothing resizes */
|
|
170
|
-
translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step));
|
|
170
|
+
translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step) + var(--_pull, 0px));
|
|
171
171
|
cursor: default;
|
|
172
172
|
overflow-y: auto;
|
|
173
173
|
overflow-x: clip;
|
|
174
174
|
overscroll-behavior: contain;
|
|
175
175
|
}
|
|
176
|
+
.sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
|
|
177
|
+
.sheet-stack--tap > .card > .card__head { touch-action: none; }
|
|
178
|
+
/* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private. */
|
|
179
|
+
.sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0px); transition: padding-top var(--motion-sheet) ease; }
|
|
180
|
+
.sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
|
|
181
|
+
padding-top: calc(var(--card-pad) + var(--_refresh, 0px));
|
|
182
|
+
transition: translate var(--motion-sheet) ease, padding-top var(--motion-sheet) ease;
|
|
183
|
+
}
|
|
184
|
+
.sheet-stack > .card > .sheet__body.is-refreshing,
|
|
185
|
+
.sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
|
|
176
186
|
/* the front sheet — the one open on arrival — never travels up (it is not
|
|
177
187
|
tappable), so its resting offset would leave its last n steps below the
|
|
178
188
|
screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
|
|
@@ -205,9 +215,13 @@
|
|
|
205
215
|
lands flush at the top. is-before tucks one step below its resting place.
|
|
206
216
|
============================================================ */
|
|
207
217
|
.sheet-stack--closed { --_headroom: calc(100% - var(--_n) * var(--stack-step)); }
|
|
208
|
-
.sheet-stack--closed > .card { translate: 0 var(--_headroom); }
|
|
218
|
+
.sheet-stack--closed > .card { translate: 0 calc(var(--_headroom) - var(--_lift, 0px)); }
|
|
219
|
+
.sheet-stack--closed > .card.is-dragging { transition: none; }
|
|
220
|
+
.sheet-stack--tap:not(.sheet-stack--closed) > .card.is-dragging:not(.is-open) {
|
|
221
|
+
translate: 0 calc(-1 * var(--_lift, 0px));
|
|
222
|
+
transition: none;
|
|
223
|
+
}
|
|
209
224
|
.sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
|
|
210
|
-
.sheet-stack--closed > .card.is-open { translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step)); }
|
|
211
225
|
.sheet-stack--closed > .card.is-after { translate: 0 100vh; }
|
|
212
226
|
|
|
213
227
|
/* .sheet-stack__base — the base's content, in the headroom of a closed stack:
|
|
@@ -224,7 +238,9 @@
|
|
|
224
238
|
display: grid;
|
|
225
239
|
align-content: end;
|
|
226
240
|
gap: var(--card-gap, var(--sp-5));
|
|
227
|
-
overflow:
|
|
241
|
+
overflow: hidden;
|
|
242
|
+
overscroll-behavior: none;
|
|
243
|
+
touch-action: none;
|
|
228
244
|
}
|
|
229
245
|
/* a base that is ONE Page is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
|
|
230
246
|
loose things (a picture, a line) still sit against the peeks (Lh 2026-09-23, the home) */
|
package/css/tokens.css
CHANGED
|
@@ -256,7 +256,7 @@
|
|
|
256
256
|
--signal-error-soft: color-mix(in srgb, var(--signal-error) 14%, var(--surface));
|
|
257
257
|
--signal-info-soft: color-mix(in srgb, var(--signal-info) 14%, var(--surface));
|
|
258
258
|
}
|
|
259
|
-
:root { --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); }
|
|
259
|
+
:root { --safe-top: calc(env(safe-area-inset-top, 0px) + var(--vv-top, 0px)); /* + the phone's pan while a keyboard is up (cardds.js) */ --safe-bottom: env(safe-area-inset-bottom, 0px); }
|
|
260
260
|
* { --_safe-b-open: max(0px, var(--safe-bottom) - var(--kb, 0px)); }
|
|
261
261
|
|
|
262
262
|
@property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
package/dist/cardds.css
CHANGED
|
@@ -297,7 +297,7 @@
|
|
|
297
297
|
--signal-error-soft: color-mix(in srgb, var(--signal-error) 14%, var(--surface));
|
|
298
298
|
--signal-info-soft: color-mix(in srgb, var(--signal-info) 14%, var(--surface));
|
|
299
299
|
}
|
|
300
|
-
:root { --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); }
|
|
300
|
+
:root { --safe-top: calc(env(safe-area-inset-top, 0px) + var(--vv-top, 0px)); /* + the phone's pan while a keyboard is up (cardds.js) */ --safe-bottom: env(safe-area-inset-bottom, 0px); }
|
|
301
301
|
* { --_safe-b-open: max(0px, var(--safe-bottom) - var(--kb, 0px)); }
|
|
302
302
|
|
|
303
303
|
@property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
@@ -1168,12 +1168,22 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1168
1168
|
}
|
|
1169
1169
|
.sheet-stack--tap > .card.is-open {
|
|
1170
1170
|
/* up by its own resting offset — pure movement, nothing resizes */
|
|
1171
|
-
translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step));
|
|
1171
|
+
translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step) + var(--_pull, 0px));
|
|
1172
1172
|
cursor: default;
|
|
1173
1173
|
overflow-y: auto;
|
|
1174
1174
|
overflow-x: clip;
|
|
1175
1175
|
overscroll-behavior: contain;
|
|
1176
1176
|
}
|
|
1177
|
+
.sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
|
|
1178
|
+
.sheet-stack--tap > .card > .card__head { touch-action: none; }
|
|
1179
|
+
/* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private. */
|
|
1180
|
+
.sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0px); transition: padding-top var(--motion-sheet) ease; }
|
|
1181
|
+
.sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
|
|
1182
|
+
padding-top: calc(var(--card-pad) + var(--_refresh, 0px));
|
|
1183
|
+
transition: translate var(--motion-sheet) ease, padding-top var(--motion-sheet) ease;
|
|
1184
|
+
}
|
|
1185
|
+
.sheet-stack > .card > .sheet__body.is-refreshing,
|
|
1186
|
+
.sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
|
|
1177
1187
|
/* the front sheet — the one open on arrival — never travels up (it is not
|
|
1178
1188
|
tappable), so its resting offset would leave its last n steps below the
|
|
1179
1189
|
screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
|
|
@@ -1206,9 +1216,13 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1206
1216
|
lands flush at the top. is-before tucks one step below its resting place.
|
|
1207
1217
|
============================================================ */
|
|
1208
1218
|
.sheet-stack--closed { --_headroom: calc(100% - var(--_n) * var(--stack-step)); }
|
|
1209
|
-
.sheet-stack--closed > .card { translate: 0 var(--_headroom); }
|
|
1219
|
+
.sheet-stack--closed > .card { translate: 0 calc(var(--_headroom) - var(--_lift, 0px)); }
|
|
1220
|
+
.sheet-stack--closed > .card.is-dragging { transition: none; }
|
|
1221
|
+
.sheet-stack--tap:not(.sheet-stack--closed) > .card.is-dragging:not(.is-open) {
|
|
1222
|
+
translate: 0 calc(-1 * var(--_lift, 0px));
|
|
1223
|
+
transition: none;
|
|
1224
|
+
}
|
|
1210
1225
|
.sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
|
|
1211
|
-
.sheet-stack--closed > .card.is-open { translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step)); }
|
|
1212
1226
|
.sheet-stack--closed > .card.is-after { translate: 0 100vh; }
|
|
1213
1227
|
|
|
1214
1228
|
/* .sheet-stack__base — the base's content, in the headroom of a closed stack:
|
|
@@ -1225,7 +1239,9 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1225
1239
|
display: grid;
|
|
1226
1240
|
align-content: end;
|
|
1227
1241
|
gap: var(--card-gap, var(--sp-5));
|
|
1228
|
-
overflow:
|
|
1242
|
+
overflow: hidden;
|
|
1243
|
+
overscroll-behavior: none;
|
|
1244
|
+
touch-action: none;
|
|
1229
1245
|
}
|
|
1230
1246
|
/* a base that is ONE Page is a page's words (a title, a welcome, the news): it reads from the TOP, like any page —
|
|
1231
1247
|
loose things (a picture, a line) still sit against the peeks (Lh 2026-09-23, the home) */
|
|
@@ -1974,7 +1990,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1974
1990
|
.field {
|
|
1975
1991
|
display: flex;
|
|
1976
1992
|
align-items: center;
|
|
1977
|
-
gap: var(--field-gap, var(--sp-
|
|
1993
|
+
gap: var(--field-gap, var(--sp-3)); /* 12 between the lead icon and the words: the field's OWN room (Lh 2026-10-04) — it was sp-2 plus whatever padding the
|
|
1994
|
+
browser gave the input (2px on a desktop, 0.5em on an iPhone); with that padding taken off (base.css) the words stood against the icon */
|
|
1978
1995
|
border: var(--border-w) solid var(--field-border, var(--action-border));
|
|
1979
1996
|
border-radius: var(--field-radius, var(--r-control));
|
|
1980
1997
|
background: var(--field-bg, var(--action-bg));
|
|
@@ -1997,7 +2014,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1997
2014
|
}
|
|
1998
2015
|
.field input::placeholder { color: inherit; opacity: 0.55; font-size: calc(1em * var(--_ph-fit, 1)); } /* --_ph-fit: Field's measure — a placeholder longer than the box shrinks to show whole (Lh 2026-09-25) */
|
|
1999
2016
|
/* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
|
|
2000
|
-
.field:has(:focus) { border-color: var(--field-active-border, currentColor); }
|
|
2017
|
+
.field:has(:focus, [data-cursor]) { border-color: var(--field-active-border, currentColor); } /* [data-cursor]: SheetSteps holds the phone's keyboard for this field */
|
|
2001
2018
|
|
|
2002
2019
|
/* field with its action inside: date pick, send, go — the button sits flush right */
|
|
2003
2020
|
.field--action { padding-right: var(--sp-1); min-height: var(--field-action-min-h, var(--tap-lg)); }
|
|
@@ -3094,19 +3111,20 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
3094
3111
|
.pin--secret .pin__cell.is-fresh:not(:placeholder-shown) { animation: pin-flash var(--pin-flash-time, 0.5s) steps(1); }
|
|
3095
3112
|
@keyframes pin-flash { 0% { -webkit-text-fill-color: currentColor; background-size: 0 0; } }
|
|
3096
3113
|
.pin--secret .pin__cell::selection { background: transparent; } /* its digit is selected when the cell takes the cursor: nothing to show of it */
|
|
3097
|
-
.pin__cell:focus { outline: none;
|
|
3114
|
+
.pin__cell:focus { outline: none; }
|
|
3115
|
+
.pin__cell:is(:focus, [data-cursor]) { border-color: var(--pin-cell-active-border, currentColor); } /* [data-cursor]: SheetSteps holds the phone's keyboard for this cell — it shows the cursor it is about to get */
|
|
3098
3116
|
/* asleep (Pin sets it): the cursor was put here by a script on a phone and no keyboard is up — nothing can be typed until a tap, so
|
|
3099
3117
|
the cell does not say "ready": the resting border, no caret */
|
|
3100
3118
|
.pin--asleep .pin__cell:focus { border-color: var(--pin-cell-border, var(--outline)); }
|
|
3101
3119
|
.pin--asleep .pin__cell:focus:placeholder-shown { background-image: none; animation: none; }
|
|
3102
|
-
.pin__cell:focus:placeholder-shown {
|
|
3120
|
+
.pin__cell:is(:focus, [data-cursor]):placeholder-shown {
|
|
3103
3121
|
background-image: linear-gradient(currentColor, currentColor);
|
|
3104
3122
|
background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
|
|
3105
3123
|
background-position: center;
|
|
3106
3124
|
background-repeat: no-repeat;
|
|
3107
3125
|
}
|
|
3108
3126
|
@media (prefers-reduced-motion: no-preference) {
|
|
3109
|
-
.pin__cell:focus:placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
|
|
3127
|
+
.pin__cell:is(:focus, [data-cursor]):placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
|
|
3110
3128
|
@keyframes pin-caret { 50% { background-size: 0 0; } }
|
|
3111
3129
|
}
|
|
3112
3130
|
|
package/dist/sheets/Sheet.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { ComponentProps } from 'react';
|
|
2
|
+
import { type Resting } from './sheetMeasure.js';
|
|
2
3
|
/** how far the sheet slid. `away` = not on the screen (slid out, inert) — a state the app sets, never one a drag or a tap lands on */
|
|
3
4
|
export type SheetState = 'peek' | 'half' | '3q' | 'full' | 'away';
|
|
4
5
|
/** what asked for the change: the handle's `tap`, a `drag` released, `focus` in a peeked sheet, the `keyboard` (or anything else) moving the cut */
|
|
5
6
|
export type SheetChangeReason = 'tap' | 'drag' | 'focus' | 'keyboard';
|
|
6
|
-
declare const ORDER: readonly ["peek", "half", "3q", "full"];
|
|
7
|
-
type Resting = (typeof ORDER)[number];
|
|
8
7
|
export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'> {
|
|
9
8
|
/** CONTROLLED: where the sheet is. The app owns it — pair it with `onStateChange`, or the sheet cannot move (like `value` without `onChange`).
|
|
10
9
|
* `peek` (handle + title row) · `half` · `3q` · `full` (its top edge at the top bar) · `away` (not on the screen). A class, never a height. */
|
|
@@ -45,4 +44,3 @@ export interface SheetProps extends Omit<ComponentProps<'article'>, 'onChange'>
|
|
|
45
44
|
* first's `onSettled`) — the app adds no timer.
|
|
46
45
|
*/
|
|
47
46
|
export declare function Sheet({ state, defaultState, onStateChange, onSettled, states, handle, className, children, onFocus, ...rest }: SheetProps): import("react").JSX.Element;
|
|
48
|
-
export {};
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect,
|
|
3
|
-
import { focusWhenActive,
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { focusWhenActive, useControlled, watchSize } from '../state.js';
|
|
4
4
|
import { cx } from '../cx.js';
|
|
5
5
|
import { word } from '../words.js';
|
|
6
|
-
|
|
6
|
+
import { ORDER, slid, stopsOf, useFootRoom, usePeekStrip, useTitleRun } from './sheetMeasure.js';
|
|
7
|
+
import { useSheetSlide } from './sheetSlide.js';
|
|
8
|
+
import { useSheetDrag } from './sheetDrag.js';
|
|
7
9
|
const RANK = { away: 0, peek: 1, half: 2, '3q': 3, full: 4 };
|
|
8
|
-
const TAP_REM = 0.5; // = --sp-2: a move shorter than this is a tap, not a drag
|
|
9
|
-
/* A SHEET THAT GOES AWAY IS NOT DONE WHEN IT LANDS (Lh 2026-10-03): it holds its box for a beat more (--motion-sheet-rest) — the base
|
|
10
|
-
is seen — and only then is it settled. `out` = the sheets still on their way out, per box; a sheet asked up in that box waits for it to empty. */
|
|
11
|
-
const out = new WeakMap();
|
|
12
|
-
const waiting = new WeakMap();
|
|
13
|
-
const leave = (box, key) => { if (!out.has(box))
|
|
14
|
-
out.set(box, new Set()); out.get(box).add(key); };
|
|
15
|
-
const gone = (box, key) => { const set = out.get(box); if (set?.delete(key) && !set.size)
|
|
16
|
-
[...(waiting.get(box) ?? [])].forEach((go) => go()); };
|
|
17
|
-
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
|
|
18
10
|
/**
|
|
19
11
|
* Sheet — the SINGLE sheet: one card as a bottom sheet holding what the member ACTS on. Always the full height of its
|
|
20
12
|
* box — a Step or a SheetStage — a state is only how far it slid (translate; the material never changes size).
|
|
@@ -42,90 +34,12 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
42
34
|
const handleRef = useRef(null);
|
|
43
35
|
const home = useRef(current !== 'peek' && current !== 'away' ? current : 'half'); // where a tap from peek returns to
|
|
44
36
|
const was = useRef('away'); // mounted up = it came onto the screen: it rises from away like any arrival
|
|
45
|
-
const skipClick = useRef(false);
|
|
46
|
-
const [entered, setEntered] = useState(false); // false for the first frame: the class says `away`, so a sheet mounted up has somewhere to slide from
|
|
47
|
-
const me = useRef({}).current; // this sheet, in its box's list of sheets on their way out
|
|
48
|
-
const [held, setHeld] = useState(false); // true while it waits for the sheet going away before it to be settled
|
|
49
|
-
const [prev, setPrev] = useState('away'); // asked up from `away` (or mounted up): it holds first, and rises once its box is free
|
|
50
|
-
if (prev !== current) {
|
|
51
|
-
setPrev(current);
|
|
52
|
-
setHeld(prev === 'away' && current !== 'away');
|
|
53
|
-
}
|
|
54
|
-
const shown = entered && !held ? current : 'away';
|
|
55
|
-
const live = held ? 'away' : current; // where it is as far as its own habits go: a held sheet has not come up yet
|
|
56
|
-
const latest = useLatest({ current, onSettled });
|
|
57
|
-
const last = useRef('away'); // where it was last SHOWN (the settle effect keeps it)
|
|
58
|
-
const drag = useRef(null);
|
|
59
37
|
const allowed = useCallback(() => {
|
|
60
38
|
const list = (typeof states === 'string' ? states.split(/\s+/) : [...(states ?? [])]).filter((s) => ORDER.includes(s));
|
|
61
39
|
return list.length ? ORDER.filter((s) => list.includes(s)) : [...ORDER];
|
|
62
40
|
}, [states]);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
setEntered(true);
|
|
66
|
-
}, []);
|
|
67
|
-
/* it starts on its way out: said in the commit itself, before any sheet of the box decides to rise */
|
|
68
|
-
useLayoutEffect(() => {
|
|
69
|
-
const box = ref.current?.parentElement;
|
|
70
|
-
if (box && current === 'away' && last.current !== 'away' && !reducedMotion())
|
|
71
|
-
leave(box, me);
|
|
72
|
-
}, [current, me]);
|
|
73
|
-
/* held: it rises when no sheet of its box is on its way out — at once if none is */
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
const box = ref.current?.parentElement;
|
|
76
|
-
if (!held)
|
|
77
|
-
return;
|
|
78
|
-
if (!box || !out.get(box)?.size) {
|
|
79
|
-
setHeld(false);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const go = () => setHeld(false);
|
|
83
|
-
if (!waiting.has(box))
|
|
84
|
-
waiting.set(box, new Set());
|
|
85
|
-
waiting.get(box).add(go);
|
|
86
|
-
return () => { waiting.get(box)?.delete(go); };
|
|
87
|
-
}, [held]);
|
|
88
|
-
/* it has finished moving: the transition ended (or at once where there is none) — say so once per move; a move cut short by another says nothing */
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
const el = ref.current, box = el?.parentElement;
|
|
91
|
-
if (!el || last.current === shown)
|
|
92
|
-
return;
|
|
93
|
-
last.current = shown;
|
|
94
|
-
const cs = getComputedStyle(el);
|
|
95
|
-
let done = false, rest = 0;
|
|
96
|
-
const settled = () => { if (box)
|
|
97
|
-
gone(box, me); latest.current.onSettled?.(latest.current.current); };
|
|
98
|
-
const end = () => {
|
|
99
|
-
if (done)
|
|
100
|
-
return;
|
|
101
|
-
done = true;
|
|
102
|
-
clearTimeout(t);
|
|
103
|
-
el.removeEventListener('transitionend', onEnd);
|
|
104
|
-
if (shown !== 'away' || reducedMotion())
|
|
105
|
-
return settled();
|
|
106
|
-
rest = window.setTimeout(settled, ms(cs.getPropertyValue('--motion-sheet-rest') || '0s')); // landed: the base is seen for the rest, THEN it is settled
|
|
107
|
-
};
|
|
108
|
-
const onEnd = (e) => { if (e.target === el && e.propertyName === 'translate')
|
|
109
|
-
end(); };
|
|
110
|
-
el.addEventListener('transitionend', onEnd);
|
|
111
|
-
const t = setTimeout(end, ms(cs.transitionDuration) + ms(cs.transitionDelay) + 100); // no transition (motion off) or none came: settled anyway
|
|
112
|
-
return () => { done = true; clearTimeout(t); clearTimeout(rest); el.removeEventListener('transitionend', onEnd); if (box && shown === 'away')
|
|
113
|
-
gone(box, me); }; // cut short on its way out (raised again, unmounted): the box is free
|
|
114
|
-
}, [shown, latest, me]);
|
|
115
|
-
/* taken out of the tree while it is still up — the box stays, the sheet goes: nothing to slide, so it vanishes. Said once in dev.
|
|
116
|
-
(A whole screen leaving takes the box too; StrictMode's trial unmount is over by the time the timer looks.) */
|
|
117
|
-
const alive = useRef(false);
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
alive.current = true;
|
|
120
|
-
const box = ref.current?.parentElement;
|
|
121
|
-
return () => {
|
|
122
|
-
alive.current = false;
|
|
123
|
-
setTimeout(() => {
|
|
124
|
-
if (!alive.current && box?.isConnected && latest.current.current !== 'away')
|
|
125
|
-
warnOnce('sheet-unmount', 'cardds Sheet: unmounted while on the screen, so it vanished instead of sliding out. Set state="away" and unmount it in onSettled (state === "away").');
|
|
126
|
-
});
|
|
127
|
-
};
|
|
128
|
-
}, [latest]);
|
|
41
|
+
/* it arrives and leaves by sliding, with a rest after it went away — sheetSlide.ts */
|
|
42
|
+
const { shown, live, held } = useSheetSlide(ref, current, onSettled);
|
|
129
43
|
/* 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`),
|
|
130
44
|
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
|
|
131
45
|
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
|
|
@@ -143,20 +57,6 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
143
57
|
return () => { if (!cancel())
|
|
144
58
|
was.current = from; }; // cut short (StrictMode's trial run, or a new state mid-slide): the rise still counts from where it began
|
|
145
59
|
}, [live]);
|
|
146
|
-
/* geometry, read once per gesture — never stored: how far it has slid, and where each state rests, in px from its top at full */
|
|
147
|
-
const slid = (el) => {
|
|
148
|
-
const box = (el.offsetParent ?? el.parentElement);
|
|
149
|
-
return el.getBoundingClientRect().top - (box.getBoundingClientRect().top + box.clientTop + el.offsetTop);
|
|
150
|
-
};
|
|
151
|
-
const stopsOf = (el) => {
|
|
152
|
-
const cs = getComputedStyle(el), height = el.getBoundingClientRect().height;
|
|
153
|
-
return {
|
|
154
|
-
full: 0,
|
|
155
|
-
half: height * (parseFloat(cs.getPropertyValue('--sheet-half')) || 0),
|
|
156
|
-
'3q': height * (parseFloat(cs.getPropertyValue('--sheet-3q')) || 0),
|
|
157
|
-
peek: height - (parseFloat(cs.getPropertyValue('--sheet-peek')) || 0), // registered in tokens.css: reads back resolved
|
|
158
|
-
};
|
|
159
|
-
};
|
|
160
60
|
/* bring a control into the part of the body that shows — between the pinned head and the foot's strip on the cut. The sheet asks
|
|
161
61
|
for the lowest of its states, from the current one (or `atLeast`) upward, that has room; if none has, its highest. */
|
|
162
62
|
const reveal = useCallback((target, reason, atLeast) => {
|
|
@@ -201,65 +101,11 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
201
101
|
reveal(at, 'keyboard');
|
|
202
102
|
});
|
|
203
103
|
}, [reveal]);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const el = ref.current, box = el?.parentElement, head = el?.querySelector(':scope > .card__head, :scope > .sheet__head');
|
|
210
|
-
if (!el || !box || !head)
|
|
211
|
-
return;
|
|
212
|
-
const publish = () => {
|
|
213
|
-
box.style.removeProperty('--sheet-peek'); // read the CSS strip, not our own
|
|
214
|
-
const cs = getComputedStyle(el);
|
|
215
|
-
const strip = parseFloat(cs.getPropertyValue('--sheet-peek')) || 0, floor = parseFloat(cs.getPropertyValue('--sheet-head-h')) || 0;
|
|
216
|
-
const over = head.getBoundingClientRect().height - floor;
|
|
217
|
-
if (over > 0.5)
|
|
218
|
-
box.style.setProperty('--sheet-peek', `${strip + over}px`);
|
|
219
|
-
};
|
|
220
|
-
const stop = watchSize([head], publish);
|
|
221
|
-
return () => { stop(); box.style.removeProperty('--sheet-peek'); };
|
|
222
|
-
}, []);
|
|
223
|
-
/* the TITLE is one line; too long for it, it runs sideways like a song title (Lh 2026-10-03): the sheet measures how far its
|
|
224
|
-
words overflow the line and hands it to the CSS (--_over, --_run, .is-long — sheet.css). Measured on the words themselves
|
|
225
|
-
(a range), so the running indent never feeds back into the number. */
|
|
226
|
-
useEffect(() => {
|
|
227
|
-
const el = ref.current, title = el?.querySelector(':scope > .card__head > :is(h1, h2, h3):first-child, :scope > .sheet__head .sheet__title');
|
|
228
|
-
if (!el || !title)
|
|
229
|
-
return;
|
|
230
|
-
const measure = () => {
|
|
231
|
-
const words = document.createRange();
|
|
232
|
-
words.selectNodeContents(title);
|
|
233
|
-
const over = Math.max(0, Math.ceil(words.getBoundingClientRect().width - title.clientWidth));
|
|
234
|
-
title.style.setProperty('--_over', `${over}px`);
|
|
235
|
-
title.style.setProperty('--_run', `${(over / 30 + 3).toFixed(1)}s`);
|
|
236
|
-
title.classList.toggle('is-long', over > 0);
|
|
237
|
-
};
|
|
238
|
-
const stop = watchSize([title], measure);
|
|
239
|
-
const mo = new MutationObserver(measure);
|
|
240
|
-
mo.observe(title, { childList: true, characterData: true, subtree: true }); // the app changed the words
|
|
241
|
-
return () => { stop(); mo.disconnect(); title.classList.remove('is-long'); title.style.removeProperty('--_over'); title.style.removeProperty('--_run'); };
|
|
242
|
-
});
|
|
243
|
-
/* the FOOT's room follows what the foot really holds (timebank, Lh 2026-10-01): a button grows with the member's text size, a
|
|
244
|
-
line may stand over it (an error) — the CSS strip was a fixed sum (--btn-h), so the body's words ran under the button. The
|
|
245
|
-
sheet measures its foot's rows and publishes --sheet-foot-h; the strip, its ground and the body's stop all read it. */
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
const el = ref.current, foot = el?.querySelector(':scope > .card__foot');
|
|
248
|
-
if (!el || !foot) {
|
|
249
|
-
el?.style.removeProperty('--sheet-foot-h');
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
const publish = () => {
|
|
253
|
-
const rows = [...foot.children].map((c) => c.getBoundingClientRect()).filter((r) => r.height > 0);
|
|
254
|
-
if (!rows.length)
|
|
255
|
-
return el.style.removeProperty('--sheet-foot-h');
|
|
256
|
-
el.style.setProperty('--sheet-foot-h', `${Math.max(...rows.map((r) => r.bottom)) - Math.min(...rows.map((r) => r.top))}px`);
|
|
257
|
-
};
|
|
258
|
-
const stop = watchSize([...foot.children], publish);
|
|
259
|
-
const mo = new MutationObserver(publish);
|
|
260
|
-
mo.observe(foot, { childList: true });
|
|
261
|
-
return () => { stop(); mo.disconnect(); };
|
|
262
|
-
}); // every render: the foot's rows are the app's, they come and go
|
|
104
|
+
usePeekStrip(ref);
|
|
105
|
+
useTitleRun(ref);
|
|
106
|
+
useFootRoom(ref);
|
|
107
|
+
/* the handle held and moved: the sheet follows, and asks for the nearest state on release — sheetDrag.ts */
|
|
108
|
+
const { down, move, up, afterDrag } = useSheetDrag(ref, allowed, (to) => request(to, 'drag'));
|
|
263
109
|
const focus = (e) => {
|
|
264
110
|
onFocus?.(e);
|
|
265
111
|
if (current !== 'peek' || e.target.closest('.sheet__handle'))
|
|
@@ -270,48 +116,9 @@ export function Sheet({ state, defaultState = 'half', onStateChange, onSettled,
|
|
|
270
116
|
reveal(e.target, 'focus', home.current);
|
|
271
117
|
};
|
|
272
118
|
const tap = () => {
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
return;
|
|
276
|
-
} // the click that follows a drag is not a tap
|
|
119
|
+
if (afterDrag())
|
|
120
|
+
return; // the click that follows a drag is not a tap
|
|
277
121
|
request(current === 'peek' ? (allowed().includes(home.current) ? home.current : allowed().find((s) => s !== 'peek') ?? 'half') : 'peek', 'tap');
|
|
278
122
|
};
|
|
279
|
-
const down = (e) => {
|
|
280
|
-
const el = ref.current;
|
|
281
|
-
if (!el || !e.isPrimary || e.button !== 0)
|
|
282
|
-
return;
|
|
283
|
-
const list = allowed(), stops = stopsOf(el), y = slid(el); // where it is now — mid-slide too, so a grab never jumps
|
|
284
|
-
drag.current = { startY: e.clientY, y0: y, y, stops, allowed: list, moved: false,
|
|
285
|
-
min: Math.min(...list.map((s) => stops[s])), max: Math.max(...list.map((s) => stops[s])),
|
|
286
|
-
tap: TAP_REM * parseFloat(getComputedStyle(document.documentElement).fontSize) };
|
|
287
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
288
|
-
};
|
|
289
|
-
const move = (e) => {
|
|
290
|
-
const d = drag.current, el = ref.current;
|
|
291
|
-
if (!d || !el || !e.isPrimary)
|
|
292
|
-
return;
|
|
293
|
-
const dy = e.clientY - d.startY;
|
|
294
|
-
if (!d.moved) {
|
|
295
|
-
if (Math.abs(dy) < d.tap)
|
|
296
|
-
return;
|
|
297
|
-
d.moved = true;
|
|
298
|
-
el.classList.add('is-dragging');
|
|
299
|
-
}
|
|
300
|
-
d.y = Math.min(Math.max(d.y0 + dy, d.min), d.max); // never above full, never below its lowest state
|
|
301
|
-
el.style.setProperty('--_drag', `${d.y - d.y0}px`); // the one thing written while held — the CSS adds it to the state's translate
|
|
302
|
-
};
|
|
303
|
-
const up = () => {
|
|
304
|
-
const d = drag.current, el = ref.current;
|
|
305
|
-
drag.current = null;
|
|
306
|
-
if (!d || !el)
|
|
307
|
-
return;
|
|
308
|
-
el.classList.remove('is-dragging');
|
|
309
|
-
if (!d.moved)
|
|
310
|
-
return; // a tap: the click that follows toggles
|
|
311
|
-
el.style.removeProperty('--_drag'); // same state, or the app says no: it eases back on its own
|
|
312
|
-
request(d.allowed.reduce((a, b) => (Math.abs(d.stops[b] - d.y) < Math.abs(d.stops[a] - d.y) ? b : a)), 'drag');
|
|
313
|
-
skipClick.current = true;
|
|
314
|
-
requestAnimationFrame(() => { skipClick.current = false; });
|
|
315
|
-
};
|
|
316
123
|
return (_jsxs("article", { ref: ref, className: cx('card sheet', `sheet--${shown}`, className), inert: current === 'away' || held || undefined, onFocus: focus, ...rest, children: [handle !== false && (_jsx("button", { ref: handleRef, className: "sheet__handle", type: "button", "aria-label": typeof handle === 'string' ? handle : word('resize'), onClick: tap, onPointerDown: down, onPointerMove: move, onPointerUp: up, onPointerCancel: up })), children] }));
|
|
317
124
|
}
|
|
@@ -9,6 +9,8 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
9
9
|
defaultOpen?: number | null;
|
|
10
10
|
/** the stack ASKS: a peeked head was tapped (its index), or the open sheet's `StackBack` (`null`) */
|
|
11
11
|
onOpenChange?: (open: number | null) => void;
|
|
12
|
+
/** the open sheet's scroller was already at the top and the finger pulled down past the commit. The stack asks with that index. It does not fetch. Omit it and a pull in the body only scrolls */
|
|
13
|
+
onRefresh?: (index: number) => void;
|
|
12
14
|
/** wider peeks so more of each title shows (display stacks) */
|
|
13
15
|
fanned?: boolean;
|
|
14
16
|
/** nothing open on arrival (tap mode): every sheet, the front one too, peeks at the stack's bottom; the room above is the base's (`base`). A tap opens any sheet over it */
|
|
@@ -26,8 +28,9 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
26
28
|
* with a picture over the menu); the `base` slot is rendered after the sheets, so it never takes a sheet's slot. This is the sheet STACK; a single bottom sheet is `Sheet`.
|
|
27
29
|
*
|
|
28
30
|
* A COMPONENT, not a wiring: WHICH sheet is open is the app's (`open` + `onOpenChange`, or `defaultOpen`) — so a button anywhere, a route or
|
|
29
|
-
* the phone's back button can open and close a section. Its own behaviour stays inside and only asks: a tap on a peeked head,
|
|
30
|
-
*
|
|
31
|
+
* the phone's back button can open and close a section. Its own behaviour stays inside and only asks: a tap on a peeked head, or a pull up on
|
|
32
|
+
* that head (the title, the back button, or the head itself — not a tab), opens it. The open sheet's StackBack or its title, tapped or pulled
|
|
33
|
+
* down, folds it. A pull down in the content, already at the top, asks `onRefresh` and does not fold. How the sheets MOVE is its own too: sheets are rigid — the tapped one slides up, the ones after it leave below, the ones
|
|
31
34
|
* before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); it sets classes, never a size.
|
|
32
35
|
*/
|
|
33
|
-
export declare function SheetStack({ mode, open, defaultOpen, onOpenChange, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }: SheetStackProps): import("react").JSX.Element;
|
|
36
|
+
export declare function SheetStack({ mode, open, defaultOpen, onOpenChange, onRefresh, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }: SheetStackProps): import("react").JSX.Element;
|