@usableapp/cardds 0.7.50 → 0.7.52
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 +14 -0
- package/README.md +5 -2
- package/css/finish.css +5 -0
- package/css/stack.css +39 -6
- package/dist/cardds.css +44 -6
- package/dist/sheets/SheetStack.d.ts +6 -3
- package/dist/sheets/SheetStack.js +328 -4
- package/dist/sheets/StackSheet.d.ts +3 -2
- package/dist/sheets/StackSheet.js +3 -2
- 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,20 @@
|
|
|
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.52 — 2026-10-05
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **A `StackSheet` tab's disc shows only while its sheet is open** (Lh 2026-10-04). On the home, every peeked sheet's tabs are the icon alone — none is filled. The chosen tab fills as the sheet rises (the one that was pressed, or the page the sheet opens on) and the fill leaves as the sheet folds. Which page is current is unchanged; only the paint waits. Nothing to change in a screen.
|
|
9
|
+
|
|
10
|
+
## 0.7.51 — 2026-10-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`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.
|
|
14
|
+
- **`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.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **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.
|
|
18
|
+
|
|
5
19
|
## 0.7.50 — 2026-10-04
|
|
6
20
|
|
|
7
21
|
### 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/css/finish.css
CHANGED
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
0 1px 2px color-mix(in srgb, var(--_shade) 20%, transparent),
|
|
66
66
|
0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
|
|
67
67
|
}
|
|
68
|
+
/* a peeked stack tab is the icon alone: no sheen and no shadow until its sheet is open (the fill fades with the sheet) */
|
|
69
|
+
[data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab {
|
|
70
|
+
background-image: none;
|
|
71
|
+
box-shadow: none;
|
|
72
|
+
}
|
|
68
73
|
[data-finish="lit"] :is(.btn:not(.btn--quiet), .icon-btn:not(.icon-btn-group > *, .stack-sheet__tab:not(.icon-btn--invert))):active:not(:disabled) {
|
|
69
74
|
background-image: none;
|
|
70
75
|
box-shadow: var(--finish-control-down);
|
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) */
|
|
@@ -269,7 +285,7 @@
|
|
|
269
285
|
.sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
|
|
270
286
|
|
|
271
287
|
/* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
|
|
272
|
-
filled (.icon-btn--invert); a count is a small circle over the icon's top right corner ---- */
|
|
288
|
+
filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
|
|
273
289
|
.stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap, var(--sp-2)); flex: none; } /* room between the tabs: with none, the chosen tab's disc touched its neighbour's (Lh 2026-09-24) */
|
|
274
290
|
/* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
|
|
275
291
|
(StackSheet measures --_over, the width it overflows by, and sets .is-long), one way: it waits, runs to the end, waits, and starts again from the first letter — like a song title */
|
|
@@ -288,8 +304,19 @@
|
|
|
288
304
|
@media (prefers-reduced-motion: reduce) {
|
|
289
305
|
.stack-sheet__title.is-long .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
|
|
290
306
|
.stack-sheet__title.is-long { text-overflow: ellipsis; }
|
|
307
|
+
.stack-sheet__tab { transition: none; } /* the disc is simply there while open, and gone while closed */
|
|
308
|
+
}
|
|
309
|
+
.stack-sheet__tab {
|
|
310
|
+
position: relative;
|
|
311
|
+
/* the disc fades with the sheet: in as it rises, out as it folds, and across when another tab is pressed.
|
|
312
|
+
scale stays on the press (actions.css) so the give under a finger stays quick */
|
|
313
|
+
transition:
|
|
314
|
+
scale var(--motion-press) ease-out,
|
|
315
|
+
background-color var(--motion-sheet) ease,
|
|
316
|
+
color var(--motion-sheet) ease,
|
|
317
|
+
border-color var(--motion-sheet) ease,
|
|
318
|
+
box-shadow var(--motion-sheet) ease;
|
|
291
319
|
}
|
|
292
|
-
.stack-sheet__tab { position: relative; }
|
|
293
320
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
294
321
|
position: absolute;
|
|
295
322
|
top: calc(-1 * var(--stack-sheet-count-out, var(--sp-2))); /* how far it stands OUT of the tab's corner, up and to the right: at sp-1 it
|
|
@@ -307,6 +334,12 @@
|
|
|
307
334
|
/* a tab not chosen is the icon alone — no ring, no ground (Lh 2026-09-23: "the circle around is not pretty"); the chosen one turns
|
|
308
335
|
to the opposite colour (.icon-btn--invert). */
|
|
309
336
|
.stack-sheet__tab:not(.icon-btn--invert) { border-color: transparent; background: transparent; box-shadow: none; color: inherit; } /* the sheet's own ink, on any paper */
|
|
337
|
+
/* a CLOSED sheet shows no chosen disc either (Lh 2026-10-04): arriving on the home, every tab of every sheet is the icon
|
|
338
|
+
alone. The fill fades in with the rise — the tab that was pressed, or the one the sheet opens on — and fades out as the
|
|
339
|
+
sheet folds. The class stays (which page it is); only the paint waits for .is-open. */
|
|
340
|
+
.sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab.icon-btn--invert {
|
|
341
|
+
border-color: transparent; background-color: transparent; box-shadow: none; color: inherit;
|
|
342
|
+
}
|
|
310
343
|
/* the last tab's ICON lines up with the inset, not its button's edge: an unchosen tab shows the glyph only, so the button's own
|
|
311
344
|
margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
|
|
312
345
|
.card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
|
package/dist/cardds.css
CHANGED
|
@@ -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) */
|
|
@@ -1270,7 +1286,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1270
1286
|
.sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
|
|
1271
1287
|
|
|
1272
1288
|
/* ---- StackSheet's tabs (Lh 2026-09-23): the head's end is a row of small icon buttons, the sheet's pages — the chosen one
|
|
1273
|
-
filled (.icon-btn--invert); a count is a small circle over the icon's top right corner ---- */
|
|
1289
|
+
filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
|
|
1274
1290
|
.stack-sheet__tabs { display: flex; gap: var(--stack-sheet-tabs-gap, var(--sp-2)); flex: none; } /* room between the tabs: with none, the chosen tab's disc touched its neighbour's (Lh 2026-09-24) */
|
|
1275
1291
|
/* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
|
|
1276
1292
|
(StackSheet measures --_over, the width it overflows by, and sets .is-long), one way: it waits, runs to the end, waits, and starts again from the first letter — like a song title */
|
|
@@ -1289,8 +1305,19 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1289
1305
|
@media (prefers-reduced-motion: reduce) {
|
|
1290
1306
|
.stack-sheet__title.is-long .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
|
|
1291
1307
|
.stack-sheet__title.is-long { text-overflow: ellipsis; }
|
|
1308
|
+
.stack-sheet__tab { transition: none; } /* the disc is simply there while open, and gone while closed */
|
|
1309
|
+
}
|
|
1310
|
+
.stack-sheet__tab {
|
|
1311
|
+
position: relative;
|
|
1312
|
+
/* the disc fades with the sheet: in as it rises, out as it folds, and across when another tab is pressed.
|
|
1313
|
+
scale stays on the press (actions.css) so the give under a finger stays quick */
|
|
1314
|
+
transition:
|
|
1315
|
+
scale var(--motion-press) ease-out,
|
|
1316
|
+
background-color var(--motion-sheet) ease,
|
|
1317
|
+
color var(--motion-sheet) ease,
|
|
1318
|
+
border-color var(--motion-sheet) ease,
|
|
1319
|
+
box-shadow var(--motion-sheet) ease;
|
|
1292
1320
|
}
|
|
1293
|
-
.stack-sheet__tab { position: relative; }
|
|
1294
1321
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
1295
1322
|
position: absolute;
|
|
1296
1323
|
top: calc(-1 * var(--stack-sheet-count-out, var(--sp-2))); /* how far it stands OUT of the tab's corner, up and to the right: at sp-1 it
|
|
@@ -1308,6 +1335,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1308
1335
|
/* a tab not chosen is the icon alone — no ring, no ground (Lh 2026-09-23: "the circle around is not pretty"); the chosen one turns
|
|
1309
1336
|
to the opposite colour (.icon-btn--invert). */
|
|
1310
1337
|
.stack-sheet__tab:not(.icon-btn--invert) { border-color: transparent; background: transparent; box-shadow: none; color: inherit; } /* the sheet's own ink, on any paper */
|
|
1338
|
+
/* a CLOSED sheet shows no chosen disc either (Lh 2026-10-04): arriving on the home, every tab of every sheet is the icon
|
|
1339
|
+
alone. The fill fades in with the rise — the tab that was pressed, or the one the sheet opens on — and fades out as the
|
|
1340
|
+
sheet folds. The class stays (which page it is); only the paint waits for .is-open. */
|
|
1341
|
+
.sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab.icon-btn--invert {
|
|
1342
|
+
border-color: transparent; background-color: transparent; box-shadow: none; color: inherit;
|
|
1343
|
+
}
|
|
1311
1344
|
/* the last tab's ICON lines up with the inset, not its button's edge: an unchosen tab shows the glyph only, so the button's own
|
|
1312
1345
|
margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
|
|
1313
1346
|
.card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
|
|
@@ -4120,6 +4153,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
4120
4153
|
0 1px 2px color-mix(in srgb, var(--_shade) 20%, transparent),
|
|
4121
4154
|
0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
|
|
4122
4155
|
}
|
|
4156
|
+
/* a peeked stack tab is the icon alone: no sheen and no shadow until its sheet is open (the fill fades with the sheet) */
|
|
4157
|
+
[data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab {
|
|
4158
|
+
background-image: none;
|
|
4159
|
+
box-shadow: none;
|
|
4160
|
+
}
|
|
4123
4161
|
[data-finish="lit"] :is(.btn:not(.btn--quiet), .icon-btn:not(.icon-btn-group > *, .stack-sheet__tab:not(.icon-btn--invert))):active:not(:disabled) {
|
|
4124
4162
|
background-image: none;
|
|
4125
4163
|
box-shadow: var(--finish-control-down);
|
|
@@ -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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { flushSync } from 'react-dom';
|
|
4
|
+
import { focusWhenActive, useControlled, useLatest } from '../state.js';
|
|
4
5
|
import { cx } from '../cx.js';
|
|
5
6
|
const PHASE = 380; // ms before the before-pack un-tucks on close, so the after-pack is clear first
|
|
6
7
|
/* the stack's sheets, in slot order: its direct children — or, where a host wraps each child in one box of its own, the first .card in
|
|
@@ -9,6 +10,28 @@ const sheetsOf = (stack) => [...stack.children]
|
|
|
9
10
|
.filter((c) => !c.classList.contains('sheet-stack__base'))
|
|
10
11
|
.map((c) => (c.classList.contains('card') ? c : c.querySelector(':scope > .card')))
|
|
11
12
|
.filter((c) => c instanceof HTMLElement);
|
|
13
|
+
const SLOP_REM = 0.5;
|
|
14
|
+
const COMMIT_REM = 4.5;
|
|
15
|
+
const pxFromRem = (rem) => parseFloat(getComputedStyle(document.documentElement).fontSize) * rem;
|
|
16
|
+
const restPullPx = (card, stack) => {
|
|
17
|
+
if (!stack.classList.contains('sheet-stack--closed'))
|
|
18
|
+
return card.offsetTop;
|
|
19
|
+
const sheets = sheetsOf(stack);
|
|
20
|
+
const step = sheets[1]?.offsetTop ?? 0;
|
|
21
|
+
return Math.max(0, stack.clientHeight - sheets.length * step) + card.offsetTop;
|
|
22
|
+
};
|
|
23
|
+
const readFrame = (card) => { card.getBoundingClientRect(); };
|
|
24
|
+
const along = (dir, startY, y) => (dir === 'down' ? Math.max(0, y - startY) : Math.max(0, startY - y));
|
|
25
|
+
const releaseProp = (card, prop) => {
|
|
26
|
+
card.classList.remove('is-dragging');
|
|
27
|
+
readFrame(card);
|
|
28
|
+
card.style.removeProperty(prop);
|
|
29
|
+
};
|
|
30
|
+
const pullsHead = (target, head) => {
|
|
31
|
+
if (!(target instanceof Element))
|
|
32
|
+
return false;
|
|
33
|
+
return target === head || !!target.closest('.sheet-stack__back') || !!target.closest('.card__head > :is(h1, h2, h3)');
|
|
34
|
+
};
|
|
12
35
|
/**
|
|
13
36
|
* SheetStack — the MENU: sheets stacked like paper at the middle of the home screen, every one a main section.
|
|
14
37
|
* Children are Cards, direct children only, at most five: first = the back of the stack, last = the front (open) sheet.
|
|
@@ -19,21 +42,31 @@ const sheetsOf = (stack) => [...stack.children]
|
|
|
19
42
|
* 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`.
|
|
20
43
|
*
|
|
21
44
|
* A COMPONENT, not a wiring: WHICH sheet is open is the app's (`open` + `onOpenChange`, or `defaultOpen`) — so a button anywhere, a route or
|
|
22
|
-
* 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,
|
|
23
|
-
*
|
|
45
|
+
* 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
|
|
46
|
+
* 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
|
|
47
|
+
* 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
|
|
24
48
|
* before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); it sets classes, never a size.
|
|
25
49
|
*/
|
|
26
|
-
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }) {
|
|
50
|
+
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, onRefresh, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }) {
|
|
27
51
|
const [current, request] = useControlled(open, defaultOpen, onOpenChange);
|
|
28
52
|
const ref = useRef(null);
|
|
29
53
|
const was = useRef(null);
|
|
30
54
|
const untuck = useRef(null);
|
|
55
|
+
const headPull = useRef(null);
|
|
56
|
+
const bodyPull = useRef(null);
|
|
57
|
+
const swallow = useRef(false);
|
|
58
|
+
const refreshRef = useLatest(onRefresh);
|
|
31
59
|
/* paint the state onto the sheets — after every render, so a re-rendered Card never loses its place */
|
|
32
60
|
useLayoutEffect(() => {
|
|
33
61
|
const stack = ref.current;
|
|
34
62
|
if (!stack || mode !== 'tap')
|
|
35
63
|
return;
|
|
36
64
|
const sheets = sheetsOf(stack);
|
|
65
|
+
for (const c of sheets)
|
|
66
|
+
if (!c.classList.contains('is-dragging')) {
|
|
67
|
+
c.style.removeProperty('--_pull');
|
|
68
|
+
c.style.removeProperty('--_lift');
|
|
69
|
+
}
|
|
37
70
|
// the keyboard's way in: a head's title takes the focus where a tap on it would do something — every title while none is
|
|
38
71
|
// open (Enter opens that sheet), only the open one's while one is (Enter folds it)
|
|
39
72
|
sheets.forEach((c, i) => {
|
|
@@ -74,8 +107,299 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
74
107
|
const cancel = focusWhenActive(sheetsOf(ref.current)[current], { scope: ':scope > .sheet__body' });
|
|
75
108
|
return () => { cancel(); };
|
|
76
109
|
}, [current, mode]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const stack = ref.current;
|
|
112
|
+
if (!stack || mode !== 'tap' || current == null)
|
|
113
|
+
return;
|
|
114
|
+
const card = sheetsOf(stack)[current];
|
|
115
|
+
if (!card)
|
|
116
|
+
return;
|
|
117
|
+
const head = card.querySelector(':scope > .card__head');
|
|
118
|
+
const scroller = card.querySelector(':scope > .sheet__body') ?? card;
|
|
119
|
+
const index = current;
|
|
120
|
+
let stopDoc = () => { };
|
|
121
|
+
const listen = (move, up, cancel) => {
|
|
122
|
+
stopDoc();
|
|
123
|
+
document.addEventListener('pointermove', move, { passive: false });
|
|
124
|
+
document.addEventListener('pointerup', up);
|
|
125
|
+
document.addEventListener('pointercancel', cancel);
|
|
126
|
+
stopDoc = () => {
|
|
127
|
+
document.removeEventListener('pointermove', move);
|
|
128
|
+
document.removeEventListener('pointerup', up);
|
|
129
|
+
document.removeEventListener('pointercancel', cancel);
|
|
130
|
+
stopDoc = () => { };
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
const primary = (e) => e.isPrimary && e.button === 0;
|
|
134
|
+
const armSwallow = () => {
|
|
135
|
+
swallow.current = true;
|
|
136
|
+
requestAnimationFrame(() => { swallow.current = false; });
|
|
137
|
+
};
|
|
138
|
+
const onHeadDown = (e) => {
|
|
139
|
+
e.stopPropagation();
|
|
140
|
+
if (!head || !primary(e) || headPull.current || bodyPull.current)
|
|
141
|
+
return;
|
|
142
|
+
if (!pullsHead(e.target, head))
|
|
143
|
+
return;
|
|
144
|
+
const slop = pxFromRem(SLOP_REM);
|
|
145
|
+
const commit = pxFromRem(COMMIT_REM);
|
|
146
|
+
const pull = {
|
|
147
|
+
pointerId: e.pointerId, card, index, dir: 'down', startY: e.clientY, last: 0,
|
|
148
|
+
cap: Math.max(restPullPx(card, stack), commit), commit, slop, moved: false,
|
|
149
|
+
};
|
|
150
|
+
headPull.current = pull;
|
|
151
|
+
const onMove = (ev) => {
|
|
152
|
+
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
153
|
+
return;
|
|
154
|
+
const dy = along('down', pull.startY, ev.clientY);
|
|
155
|
+
pull.last = dy;
|
|
156
|
+
if (dy < pull.slop)
|
|
157
|
+
return;
|
|
158
|
+
pull.moved = true;
|
|
159
|
+
ev.preventDefault();
|
|
160
|
+
try {
|
|
161
|
+
if (head && !head.hasPointerCapture(ev.pointerId))
|
|
162
|
+
head.setPointerCapture(ev.pointerId);
|
|
163
|
+
}
|
|
164
|
+
catch { /* the pointer is already gone */ }
|
|
165
|
+
card.classList.add('is-dragging');
|
|
166
|
+
card.style.setProperty('--_pull', `${Math.min(dy, pull.cap)}px`);
|
|
167
|
+
};
|
|
168
|
+
const onUp = (ev) => {
|
|
169
|
+
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
170
|
+
return;
|
|
171
|
+
const dy = Math.max(pull.last, along('down', pull.startY, ev.clientY));
|
|
172
|
+
const moved = pull.moved || dy >= pull.slop;
|
|
173
|
+
headPull.current = null;
|
|
174
|
+
stopDoc();
|
|
175
|
+
if (moved && dy >= pull.commit) {
|
|
176
|
+
card.classList.remove('is-dragging');
|
|
177
|
+
readFrame(card);
|
|
178
|
+
flushSync(() => request(null));
|
|
179
|
+
if (card.isConnected && card.classList.contains('is-open') && card.style.getPropertyValue('--_pull'))
|
|
180
|
+
releaseProp(card, '--_pull');
|
|
181
|
+
}
|
|
182
|
+
else
|
|
183
|
+
releaseProp(card, '--_pull');
|
|
184
|
+
if (moved)
|
|
185
|
+
armSwallow();
|
|
186
|
+
};
|
|
187
|
+
const onCancel = (ev) => {
|
|
188
|
+
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
189
|
+
return;
|
|
190
|
+
headPull.current = null;
|
|
191
|
+
stopDoc();
|
|
192
|
+
releaseProp(card, '--_pull');
|
|
193
|
+
};
|
|
194
|
+
listen(onMove, onUp, onCancel);
|
|
195
|
+
};
|
|
196
|
+
const onBodyDown = (e) => {
|
|
197
|
+
if (!primary(e) || headPull.current || bodyPull.current || !refreshRef.current)
|
|
198
|
+
return;
|
|
199
|
+
if (scroller.scrollTop > 0)
|
|
200
|
+
return;
|
|
201
|
+
if (e.target instanceof Element && e.target.closest('.card__head'))
|
|
202
|
+
return;
|
|
203
|
+
const slop = pxFromRem(SLOP_REM);
|
|
204
|
+
const commit = pxFromRem(COMMIT_REM);
|
|
205
|
+
const pull = {
|
|
206
|
+
pointerId: e.pointerId, scroller, index, startX: e.clientX, startY: e.clientY,
|
|
207
|
+
lastDy: 0, commit, slop, moved: false, drop: false,
|
|
208
|
+
};
|
|
209
|
+
bodyPull.current = pull;
|
|
210
|
+
const onMove = (ev) => {
|
|
211
|
+
if (ev.pointerId !== pull.pointerId || bodyPull.current !== pull)
|
|
212
|
+
return;
|
|
213
|
+
if (scroller.scrollTop > 0) {
|
|
214
|
+
pull.drop = true;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (pull.drop)
|
|
218
|
+
return;
|
|
219
|
+
const dx = ev.clientX - pull.startX;
|
|
220
|
+
const dy = ev.clientY - pull.startY;
|
|
221
|
+
pull.lastDy = Math.max(0, dy);
|
|
222
|
+
if (!pull.moved && !pull.drop) {
|
|
223
|
+
if (Math.max(Math.abs(dx), Math.abs(dy)) < pull.slop)
|
|
224
|
+
return;
|
|
225
|
+
if (Math.abs(dx) > dy || dy <= 0) {
|
|
226
|
+
pull.drop = true;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
pull.moved = true;
|
|
231
|
+
ev.preventDefault();
|
|
232
|
+
try {
|
|
233
|
+
if (!scroller.hasPointerCapture(ev.pointerId))
|
|
234
|
+
scroller.setPointerCapture(ev.pointerId);
|
|
235
|
+
}
|
|
236
|
+
catch { /* the pointer is already gone */ }
|
|
237
|
+
scroller.classList.add('is-refreshing');
|
|
238
|
+
scroller.style.setProperty('--_refresh', `${Math.min(dy, pull.commit)}px`);
|
|
239
|
+
};
|
|
240
|
+
const onUp = (ev) => {
|
|
241
|
+
if (ev.pointerId !== pull.pointerId || bodyPull.current !== pull)
|
|
242
|
+
return;
|
|
243
|
+
const dy = Math.max(pull.lastDy, Math.max(0, ev.clientY - pull.startY));
|
|
244
|
+
const ask = pull.moved && !pull.drop && scroller.scrollTop <= 0 && dy >= pull.commit;
|
|
245
|
+
bodyPull.current = null;
|
|
246
|
+
stopDoc();
|
|
247
|
+
scroller.classList.remove('is-refreshing');
|
|
248
|
+
readFrame(scroller);
|
|
249
|
+
scroller.style.removeProperty('--_refresh');
|
|
250
|
+
if (ask)
|
|
251
|
+
refreshRef.current?.(pull.index);
|
|
252
|
+
if (pull.moved)
|
|
253
|
+
armSwallow();
|
|
254
|
+
};
|
|
255
|
+
const onCancel = (ev) => {
|
|
256
|
+
if (ev.pointerId !== pull.pointerId || bodyPull.current !== pull)
|
|
257
|
+
return;
|
|
258
|
+
bodyPull.current = null;
|
|
259
|
+
stopDoc();
|
|
260
|
+
scroller.classList.remove('is-refreshing');
|
|
261
|
+
scroller.style.removeProperty('--_refresh');
|
|
262
|
+
};
|
|
263
|
+
listen(onMove, onUp, onCancel);
|
|
264
|
+
};
|
|
265
|
+
head?.addEventListener('pointerdown', onHeadDown);
|
|
266
|
+
scroller.addEventListener('pointerdown', onBodyDown);
|
|
267
|
+
return () => {
|
|
268
|
+
stopDoc();
|
|
269
|
+
const held = headPull.current;
|
|
270
|
+
headPull.current = null;
|
|
271
|
+
bodyPull.current = null;
|
|
272
|
+
if (held?.card.classList.contains('is-dragging'))
|
|
273
|
+
releaseProp(held.card, '--_pull');
|
|
274
|
+
head?.removeEventListener('pointerdown', onHeadDown);
|
|
275
|
+
scroller.removeEventListener('pointerdown', onBodyDown);
|
|
276
|
+
};
|
|
277
|
+
}, [current, mode, request]);
|
|
278
|
+
/* none open: a pull UP on a peeked head (not a tab) follows the finger and, past the commit, opens that sheet */
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
const stack = ref.current;
|
|
281
|
+
if (!stack || mode !== 'tap' || current != null)
|
|
282
|
+
return;
|
|
283
|
+
const sheets = sheetsOf(stack);
|
|
284
|
+
let stopDoc = () => { };
|
|
285
|
+
const listen = (move, up, cancel) => {
|
|
286
|
+
stopDoc();
|
|
287
|
+
document.addEventListener('pointermove', move, { passive: false });
|
|
288
|
+
document.addEventListener('pointerup', up);
|
|
289
|
+
document.addEventListener('pointercancel', cancel);
|
|
290
|
+
stopDoc = () => {
|
|
291
|
+
document.removeEventListener('pointermove', move);
|
|
292
|
+
document.removeEventListener('pointerup', up);
|
|
293
|
+
document.removeEventListener('pointercancel', cancel);
|
|
294
|
+
stopDoc = () => { };
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
const armSwallow = () => {
|
|
298
|
+
swallow.current = true;
|
|
299
|
+
requestAnimationFrame(() => { swallow.current = false; });
|
|
300
|
+
};
|
|
301
|
+
const cleanups = [];
|
|
302
|
+
sheets.forEach((card, index) => {
|
|
303
|
+
const head = card.querySelector(':scope > .card__head');
|
|
304
|
+
if (!head)
|
|
305
|
+
return;
|
|
306
|
+
const onDown = (e) => {
|
|
307
|
+
if (!e.isPrimary || e.button !== 0 || headPull.current)
|
|
308
|
+
return;
|
|
309
|
+
if (!pullsHead(e.target, head)) {
|
|
310
|
+
if (!(e.target instanceof Element) || !e.target.closest('button, a'))
|
|
311
|
+
return;
|
|
312
|
+
const startY = e.clientY;
|
|
313
|
+
const id = e.pointerId;
|
|
314
|
+
const slop = pxFromRem(SLOP_REM);
|
|
315
|
+
const move = (ev) => {
|
|
316
|
+
if (ev.pointerId !== id)
|
|
317
|
+
return;
|
|
318
|
+
if (Math.abs(ev.clientY - startY) >= slop)
|
|
319
|
+
swallow.current = true;
|
|
320
|
+
};
|
|
321
|
+
const up = (ev) => {
|
|
322
|
+
if (ev.pointerId !== id)
|
|
323
|
+
return;
|
|
324
|
+
document.removeEventListener('pointermove', move);
|
|
325
|
+
document.removeEventListener('pointerup', up);
|
|
326
|
+
if (swallow.current)
|
|
327
|
+
requestAnimationFrame(() => { swallow.current = false; });
|
|
328
|
+
};
|
|
329
|
+
document.addEventListener('pointermove', move);
|
|
330
|
+
document.addEventListener('pointerup', up);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
e.stopPropagation();
|
|
334
|
+
const slop = pxFromRem(SLOP_REM);
|
|
335
|
+
const commit = pxFromRem(COMMIT_REM);
|
|
336
|
+
const travel = card.getBoundingClientRect().top - stack.getBoundingClientRect().top;
|
|
337
|
+
const pull = {
|
|
338
|
+
pointerId: e.pointerId, card, index, dir: 'up', startY: e.clientY, last: 0,
|
|
339
|
+
cap: Math.max(travel, commit), commit, slop, moved: false,
|
|
340
|
+
};
|
|
341
|
+
headPull.current = pull;
|
|
342
|
+
const onMove = (ev) => {
|
|
343
|
+
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
344
|
+
return;
|
|
345
|
+
const upPx = along('up', pull.startY, ev.clientY);
|
|
346
|
+
pull.last = upPx;
|
|
347
|
+
if (upPx < pull.slop)
|
|
348
|
+
return;
|
|
349
|
+
pull.moved = true;
|
|
350
|
+
ev.preventDefault();
|
|
351
|
+
try {
|
|
352
|
+
if (!head.hasPointerCapture(ev.pointerId))
|
|
353
|
+
head.setPointerCapture(ev.pointerId);
|
|
354
|
+
}
|
|
355
|
+
catch { /* the pointer is already gone */ }
|
|
356
|
+
card.classList.add('is-dragging');
|
|
357
|
+
card.style.setProperty('--_lift', `${Math.min(upPx, pull.cap)}px`);
|
|
358
|
+
};
|
|
359
|
+
const onUp = (ev) => {
|
|
360
|
+
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
361
|
+
return;
|
|
362
|
+
const upPx = Math.max(pull.last, along('up', pull.startY, ev.clientY));
|
|
363
|
+
const moved = pull.moved || upPx >= pull.slop;
|
|
364
|
+
headPull.current = null;
|
|
365
|
+
stopDoc();
|
|
366
|
+
if (moved && upPx >= pull.commit) {
|
|
367
|
+
card.classList.remove('is-dragging');
|
|
368
|
+
readFrame(card);
|
|
369
|
+
flushSync(() => request(pull.index));
|
|
370
|
+
if (card.isConnected && !card.classList.contains('is-open'))
|
|
371
|
+
releaseProp(card, '--_lift');
|
|
372
|
+
}
|
|
373
|
+
else
|
|
374
|
+
releaseProp(card, '--_lift');
|
|
375
|
+
if (moved)
|
|
376
|
+
armSwallow();
|
|
377
|
+
};
|
|
378
|
+
const onCancel = (ev) => {
|
|
379
|
+
if (ev.pointerId !== pull.pointerId || headPull.current !== pull)
|
|
380
|
+
return;
|
|
381
|
+
headPull.current = null;
|
|
382
|
+
stopDoc();
|
|
383
|
+
releaseProp(card, '--_lift');
|
|
384
|
+
};
|
|
385
|
+
listen(onMove, onUp, onCancel);
|
|
386
|
+
};
|
|
387
|
+
head.addEventListener('pointerdown', onDown);
|
|
388
|
+
cleanups.push(() => head.removeEventListener('pointerdown', onDown));
|
|
389
|
+
});
|
|
390
|
+
return () => {
|
|
391
|
+
stopDoc();
|
|
392
|
+
const held = headPull.current;
|
|
393
|
+
headPull.current = null;
|
|
394
|
+
if (held?.dir === 'up' && held.card.classList.contains('is-dragging'))
|
|
395
|
+
releaseProp(held.card, '--_lift');
|
|
396
|
+
cleanups.forEach((fn) => fn());
|
|
397
|
+
};
|
|
398
|
+
}, [current, mode, request]);
|
|
77
399
|
const click = (e) => {
|
|
78
400
|
onClick?.(e);
|
|
401
|
+
if (swallow.current)
|
|
402
|
+
return;
|
|
79
403
|
const stack = ref.current;
|
|
80
404
|
if (!stack || mode !== 'tap')
|
|
81
405
|
return;
|
|
@@ -32,8 +32,9 @@ export interface StackSheetProps extends Omit<ComponentProps<'article'>, 'title'
|
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* StackSheet — ONE sheet of a `SheetStack`, in the one sheet shape: SHEET HEAD (back · title · tabs or an icon) pinned at the top ·
|
|
35
|
-
* SHEET BODY (the children; scrolls) · SHEET FOOT (`foot`, the CTA) at the bottom. Its tabs are icon buttons
|
|
36
|
-
*
|
|
35
|
+
* SHEET BODY (the children; scrolls) · SHEET FOOT (`foot`, the CTA) at the bottom. Its tabs are icon buttons. The chosen one is filled
|
|
36
|
+
* only while this sheet is open — a closed home shows every icon plain; the disc fades in with the rise and out with the fold.
|
|
37
|
+
* 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
|
|
37
38
|
* is closed — see `Home`). A direct child of `SheetStack`.
|
|
38
39
|
*/
|
|
39
40
|
export declare function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel, backAt, className, children, ...rest }: StackSheetProps): import("react").JSX.Element;
|
|
@@ -14,8 +14,9 @@ import { Badge } from '../numbers/Badge.js';
|
|
|
14
14
|
import { word } from '../words.js';
|
|
15
15
|
/**
|
|
16
16
|
* StackSheet — ONE sheet of a `SheetStack`, in the one sheet shape: SHEET HEAD (back · title · tabs or an icon) pinned at the top ·
|
|
17
|
-
* SHEET BODY (the children; scrolls) · SHEET FOOT (`foot`, the CTA) at the bottom. Its tabs are icon buttons
|
|
18
|
-
*
|
|
17
|
+
* SHEET BODY (the children; scrolls) · SHEET FOOT (`foot`, the CTA) at the bottom. Its tabs are icon buttons. The chosen one is filled
|
|
18
|
+
* only while this sheet is open — a closed home shows every icon plain; the disc fades in with the rise and out with the fold.
|
|
19
|
+
* 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
|
|
19
20
|
* is closed — see `Home`). A direct child of `SheetStack`.
|
|
20
21
|
*/
|
|
21
22
|
export function StackSheet({ title, tone, icon, tabs, tab: tabProp, defaultTab, onTabChange, foot, backLabel = word('back'), backAt = 'none', className, children, ...rest }) {
|
package/dist/templates/Home.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
29
29
|
open?: number | null;
|
|
30
30
|
defaultOpen?: number | null;
|
|
31
31
|
onOpenChange?: (open: number | null) => void;
|
|
32
|
+
/** 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 */
|
|
33
|
+
onRefresh?: (index: number) => void;
|
|
32
34
|
/** CONTENT above the sheet heads — one `Page`: a welcome, the news, a picture. It reads
|
|
33
35
|
* from the top; a sheet coming up covers it */
|
|
34
36
|
base?: ReactNode;
|
|
@@ -48,4 +50,4 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
48
50
|
* at the bottom). A tap on a head brings that sheet up to fill everything under the top. Only the open sheet's body is rendered — three
|
|
49
51
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
50
52
|
*/
|
|
51
|
-
export declare function Home({ top, title, sheets, open: openProp, defaultOpen, onOpenChange, base, keep, backLabel, backAt, children, ...rest }: HomeProps): import("react").JSX.Element;
|
|
53
|
+
export declare function Home({ top, title, sheets, open: openProp, defaultOpen, onOpenChange, onRefresh, base, keep, backLabel, backAt, children, ...rest }: HomeProps): import("react").JSX.Element;
|
package/dist/templates/Home.js
CHANGED
|
@@ -15,7 +15,7 @@ const LEAVE = 800; // ms: a closing sheet keeps its body until it has slid home
|
|
|
15
15
|
* at the bottom). A tap on a head brings that sheet up to fill everything under the top. Only the open sheet's body is rendered — three
|
|
16
16
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
17
17
|
*/
|
|
18
|
-
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, base, keep, backLabel = word('back'), backAt, children, ...rest }) {
|
|
18
|
+
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, onRefresh, base, keep, backLabel = word('back'), backAt, children, ...rest }) {
|
|
19
19
|
const [open, change] = useControlled(openProp, defaultOpen, onOpenChange);
|
|
20
20
|
const [leaving, setLeaving] = useState(null);
|
|
21
21
|
const [seen, setSeen] = useState(() => new Set(open != null ? [open] : []));
|
|
@@ -35,5 +35,5 @@ export function Home({ top, title, sheets, open: openProp, defaultOpen = null, o
|
|
|
35
35
|
}, [leaving]);
|
|
36
36
|
const [tabs, setTabs] = useState(() => Object.fromEntries(sheets.map((x, i) => [i, x.defaultTab ?? x.tabs?.[0]?.id])));
|
|
37
37
|
const shows = (i) => i === open || i === leaving || (keep && seen.has(i));
|
|
38
|
-
return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: open, onOpenChange: change, base: base, children: sheets.map((s, i) => (_jsx(StackSheet, { tone: s.tone ?? TONES[i], title: s.title, icon: s.icon, tabs: s.tabs, tab: tabs[i], onTabChange: (t) => setTabs((v) => ({ ...v, [i]: t })), backLabel: backLabel, backAt: backAt, foot: typeof s.foot === 'function' ? s.foot(tabs[i]) : s.foot, children: shows(i) ? (typeof s.body === 'function' ? s.body(tabs[i]) : s.body) : null }, i))) }), children] }));
|
|
38
|
+
return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: open, onOpenChange: change, onRefresh: onRefresh, base: base, children: sheets.map((s, i) => (_jsx(StackSheet, { tone: s.tone ?? TONES[i], title: s.title, icon: s.icon, tabs: s.tabs, tab: tabs[i], onTabChange: (t) => setTabs((v) => ({ ...v, [i]: t })), backLabel: backLabel, backAt: backAt, foot: typeof s.foot === 'function' ? s.foot(tabs[i]) : s.foot, children: shows(i) ? (typeof s.body === 'function' ? s.body(tabs[i]) : s.body) : null }, i))) }), children] }));
|
|
39
39
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.52",
|
|
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.",
|