@usableapp/cardds 0.7.51 → 0.7.53
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 +4 -2
- package/css/finish.css +5 -0
- package/css/stack.css +42 -2
- package/dist/cardds.css +47 -2
- package/dist/sheets/SheetStack.d.ts +4 -2
- package/dist/sheets/SheetStack.js +35 -3
- 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,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.53 — 2026-10-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`SheetStack` / `Home`: the pull down that asks `onRefresh` works with a finger** (Lh 2026-10-05). In 0.7.51 it only worked with a mouse: on a phone the browser took the touch for its own scroll and the content only jerked. A pull down from the top of the open content now follows the finger and asks; any other move still scrolls. Nothing to change in a screen.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`SheetStack` and `Home` take `refreshing`** — the index of the sheet you are reloading, or `null`. Set it in `onRefresh`, clear it when your data is in: `onRefresh={(i) => { setRefreshing(i); load(i).finally(() => setRefreshing(null)); }}` with `refreshing={refreshing}`. While it is the open sheet, its grip dots (below) fade up and down, the card is `aria-busy`, and a pull there does not ask again. Without `refreshing` nothing shows that a reload is running. Reload the pulled sheet alone or everything — that is yours; the stack does not fetch.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Every sheet of a tap stack wears a grip: four small grey dots at the middle of its top edge** (Lh 2026-10-05) — a sign that the card slides up and down. A mark, not a control; nothing is added to the markup. Every `SheetStack` (tap mode) and `Home` shows it after the update — nothing to change in a screen, and there is no knob for it yet: if your screen needs it tuned or off, report it.
|
|
15
|
+
|
|
16
|
+
## 0.7.52 — 2026-10-05
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **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.
|
|
20
|
+
|
|
5
21
|
## 0.7.51 — 2026-10-04
|
|
6
22
|
|
|
7
23
|
### Added
|
package/README.md
CHANGED
|
@@ -181,12 +181,14 @@ the card is the material, the sheet is what it is in the stack.
|
|
|
181
181
|
A tap, or a pull up, opens any sheet over it. The open title or its back
|
|
182
182
|
button, tapped or pulled down, drops it home. A pull down in the open
|
|
183
183
|
content, already at the top, asks `onRefresh` — omit it and that pull only
|
|
184
|
-
scrolls. A tab in the head stays a click.
|
|
184
|
+
scrolls. A tab in the head stays a click. Every sheet wears a grip — four
|
|
185
|
+
small dots at the middle of its top edge, a mark that it slides, not a
|
|
186
|
+
control.
|
|
185
187
|
- **Five sheets max.** Slots are declared per `:nth-child` in `stack.css`; a
|
|
186
188
|
sixth would render at slot 0, on top of the first.
|
|
187
189
|
- **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
|
|
188
190
|
hidden until that sheet is open, then slides into a reserved header slot.
|
|
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.
|
|
191
|
+
- **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: the app reloads (that sheet alone, or more — its call) and says so with `refreshing` (that index, then `null` when the data is in) — the sheet's grip dots fade up and down meanwhile, and a pull there does not ask again.
|
|
190
192
|
- **`.card-stack` is gone.** That was this component's first name and the wrong
|
|
191
193
|
one — the members are sheets and the stack is the menu, not a list of cards.
|
|
192
194
|
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
|
@@ -183,6 +183,29 @@
|
|
|
183
183
|
}
|
|
184
184
|
.sheet-stack > .card > .sheet__body.is-refreshing,
|
|
185
185
|
.sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
|
|
186
|
+
/* the GRIP (Lh 2026-10-05): four small grey dots at the middle of every sheet's top edge — just enough of a sign that this card
|
|
187
|
+
slides up and down. A mark, not a control: nothing to press, nothing added to the sheet (the card's own background, in the
|
|
188
|
+
room above its head). While the app reloads the open sheet (.is-loading, from `refreshing`) the same four dots fade up and
|
|
189
|
+
down until it clears — nothing travels, nobody knows how long a reload takes. --_grip is their strength, private. */
|
|
190
|
+
@property --_grip { syntax: "<number>"; inherits: false; initial-value: 0.35; }
|
|
191
|
+
.sheet-stack--tap > .card {
|
|
192
|
+
--_grip-dot: var(--sp-1);
|
|
193
|
+
--_grip-step: calc(var(--sp-1) * 2.5);
|
|
194
|
+
--_grip-y: calc((var(--card-pad) - var(--_grip-dot)) / 2);
|
|
195
|
+
--_grip-ink: color-mix(in srgb, currentColor calc(var(--_grip) * 100%), transparent);
|
|
196
|
+
--_grip-img: radial-gradient(circle closest-side, var(--_grip-ink) 85%, transparent);
|
|
197
|
+
background-image: var(--_grip-img), var(--_grip-img), var(--_grip-img), var(--_grip-img);
|
|
198
|
+
background-repeat: no-repeat;
|
|
199
|
+
background-size: var(--_grip-dot) var(--_grip-dot);
|
|
200
|
+
background-position:
|
|
201
|
+
calc(50% - 1.5 * var(--_grip-step)) var(--_grip-y), calc(50% - 0.5 * var(--_grip-step)) var(--_grip-y),
|
|
202
|
+
calc(50% + 0.5 * var(--_grip-step)) var(--_grip-y), calc(50% + 1.5 * var(--_grip-step)) var(--_grip-y);
|
|
203
|
+
}
|
|
204
|
+
.sheet-stack--tap > .card.is-open.is-loading { animation: stack-grip-breath 0.8s ease-in-out infinite alternate; }
|
|
205
|
+
@keyframes stack-grip-breath { from { --_grip: 0.2; } to { --_grip: 1; } }
|
|
206
|
+
@media (prefers-reduced-motion: reduce) {
|
|
207
|
+
.sheet-stack--tap > .card.is-open.is-loading { animation: none; --_grip: 1; } /* no fade: the dots are simply full while it reloads */
|
|
208
|
+
}
|
|
186
209
|
/* the front sheet — the one open on arrival — never travels up (it is not
|
|
187
210
|
tappable), so its resting offset would leave its last n steps below the
|
|
188
211
|
screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
|
|
@@ -285,7 +308,7 @@
|
|
|
285
308
|
.sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
|
|
286
309
|
|
|
287
310
|
/* ---- 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
|
|
288
|
-
filled (.icon-btn--invert); a count is a small circle over the icon's top right corner ---- */
|
|
311
|
+
filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
|
|
289
312
|
.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) */
|
|
290
313
|
/* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
|
|
291
314
|
(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 */
|
|
@@ -304,8 +327,19 @@
|
|
|
304
327
|
@media (prefers-reduced-motion: reduce) {
|
|
305
328
|
.stack-sheet__title.is-long .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
|
|
306
329
|
.stack-sheet__title.is-long { text-overflow: ellipsis; }
|
|
330
|
+
.stack-sheet__tab { transition: none; } /* the disc is simply there while open, and gone while closed */
|
|
331
|
+
}
|
|
332
|
+
.stack-sheet__tab {
|
|
333
|
+
position: relative;
|
|
334
|
+
/* the disc fades with the sheet: in as it rises, out as it folds, and across when another tab is pressed.
|
|
335
|
+
scale stays on the press (actions.css) so the give under a finger stays quick */
|
|
336
|
+
transition:
|
|
337
|
+
scale var(--motion-press) ease-out,
|
|
338
|
+
background-color var(--motion-sheet) ease,
|
|
339
|
+
color var(--motion-sheet) ease,
|
|
340
|
+
border-color var(--motion-sheet) ease,
|
|
341
|
+
box-shadow var(--motion-sheet) ease;
|
|
307
342
|
}
|
|
308
|
-
.stack-sheet__tab { position: relative; }
|
|
309
343
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
310
344
|
position: absolute;
|
|
311
345
|
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
|
|
@@ -323,6 +357,12 @@
|
|
|
323
357
|
/* 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
|
|
324
358
|
to the opposite colour (.icon-btn--invert). */
|
|
325
359
|
.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 */
|
|
360
|
+
/* a CLOSED sheet shows no chosen disc either (Lh 2026-10-04): arriving on the home, every tab of every sheet is the icon
|
|
361
|
+
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
|
|
362
|
+
sheet folds. The class stays (which page it is); only the paint waits for .is-open. */
|
|
363
|
+
.sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab.icon-btn--invert {
|
|
364
|
+
border-color: transparent; background-color: transparent; box-shadow: none; color: inherit;
|
|
365
|
+
}
|
|
326
366
|
/* 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
|
|
327
367
|
margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
|
|
328
368
|
.card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
|
package/dist/cardds.css
CHANGED
|
@@ -1184,6 +1184,29 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1184
1184
|
}
|
|
1185
1185
|
.sheet-stack > .card > .sheet__body.is-refreshing,
|
|
1186
1186
|
.sheet-stack--tap > .card.is-open.is-refreshing:not(:has(> .sheet__body)) { transition: none; }
|
|
1187
|
+
/* the GRIP (Lh 2026-10-05): four small grey dots at the middle of every sheet's top edge — just enough of a sign that this card
|
|
1188
|
+
slides up and down. A mark, not a control: nothing to press, nothing added to the sheet (the card's own background, in the
|
|
1189
|
+
room above its head). While the app reloads the open sheet (.is-loading, from `refreshing`) the same four dots fade up and
|
|
1190
|
+
down until it clears — nothing travels, nobody knows how long a reload takes. --_grip is their strength, private. */
|
|
1191
|
+
@property --_grip { syntax: "<number>"; inherits: false; initial-value: 0.35; }
|
|
1192
|
+
.sheet-stack--tap > .card {
|
|
1193
|
+
--_grip-dot: var(--sp-1);
|
|
1194
|
+
--_grip-step: calc(var(--sp-1) * 2.5);
|
|
1195
|
+
--_grip-y: calc((var(--card-pad) - var(--_grip-dot)) / 2);
|
|
1196
|
+
--_grip-ink: color-mix(in srgb, currentColor calc(var(--_grip) * 100%), transparent);
|
|
1197
|
+
--_grip-img: radial-gradient(circle closest-side, var(--_grip-ink) 85%, transparent);
|
|
1198
|
+
background-image: var(--_grip-img), var(--_grip-img), var(--_grip-img), var(--_grip-img);
|
|
1199
|
+
background-repeat: no-repeat;
|
|
1200
|
+
background-size: var(--_grip-dot) var(--_grip-dot);
|
|
1201
|
+
background-position:
|
|
1202
|
+
calc(50% - 1.5 * var(--_grip-step)) var(--_grip-y), calc(50% - 0.5 * var(--_grip-step)) var(--_grip-y),
|
|
1203
|
+
calc(50% + 0.5 * var(--_grip-step)) var(--_grip-y), calc(50% + 1.5 * var(--_grip-step)) var(--_grip-y);
|
|
1204
|
+
}
|
|
1205
|
+
.sheet-stack--tap > .card.is-open.is-loading { animation: stack-grip-breath 0.8s ease-in-out infinite alternate; }
|
|
1206
|
+
@keyframes stack-grip-breath { from { --_grip: 0.2; } to { --_grip: 1; } }
|
|
1207
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1208
|
+
.sheet-stack--tap > .card.is-open.is-loading { animation: none; --_grip: 1; } /* no fade: the dots are simply full while it reloads */
|
|
1209
|
+
}
|
|
1187
1210
|
/* the front sheet — the one open on arrival — never travels up (it is not
|
|
1188
1211
|
tappable), so its resting offset would leave its last n steps below the
|
|
1189
1212
|
screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
|
|
@@ -1286,7 +1309,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1286
1309
|
.sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
|
|
1287
1310
|
|
|
1288
1311
|
/* ---- 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
|
|
1289
|
-
filled (.icon-btn--invert); a count is a small circle over the icon's top right corner ---- */
|
|
1312
|
+
filled (.icon-btn--invert) only while the sheet is open; a count is a small circle over the icon's top right corner ---- */
|
|
1290
1313
|
.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) */
|
|
1291
1314
|
/* the head is ONE line, always (Lh 2026-09-24): the tabs keep their size, the TITLE gives — it never wraps, it runs sideways
|
|
1292
1315
|
(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 */
|
|
@@ -1305,8 +1328,19 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1305
1328
|
@media (prefers-reduced-motion: reduce) {
|
|
1306
1329
|
.stack-sheet__title.is-long .stack-sheet__run { animation: none; display: inline; } /* inline: an ellipsis cannot cut an inline-block */
|
|
1307
1330
|
.stack-sheet__title.is-long { text-overflow: ellipsis; }
|
|
1331
|
+
.stack-sheet__tab { transition: none; } /* the disc is simply there while open, and gone while closed */
|
|
1332
|
+
}
|
|
1333
|
+
.stack-sheet__tab {
|
|
1334
|
+
position: relative;
|
|
1335
|
+
/* the disc fades with the sheet: in as it rises, out as it folds, and across when another tab is pressed.
|
|
1336
|
+
scale stays on the press (actions.css) so the give under a finger stays quick */
|
|
1337
|
+
transition:
|
|
1338
|
+
scale var(--motion-press) ease-out,
|
|
1339
|
+
background-color var(--motion-sheet) ease,
|
|
1340
|
+
color var(--motion-sheet) ease,
|
|
1341
|
+
border-color var(--motion-sheet) ease,
|
|
1342
|
+
box-shadow var(--motion-sheet) ease;
|
|
1308
1343
|
}
|
|
1309
|
-
.stack-sheet__tab { position: relative; }
|
|
1310
1344
|
.stack-sheet__tab > .stack-sheet__count { /* outranks .badge (numbers.css loads after this file) */
|
|
1311
1345
|
position: absolute;
|
|
1312
1346
|
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
|
|
@@ -1324,6 +1358,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1324
1358
|
/* 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
|
|
1325
1359
|
to the opposite colour (.icon-btn--invert). */
|
|
1326
1360
|
.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 */
|
|
1361
|
+
/* a CLOSED sheet shows no chosen disc either (Lh 2026-10-04): arriving on the home, every tab of every sheet is the icon
|
|
1362
|
+
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
|
|
1363
|
+
sheet folds. The class stays (which page it is); only the paint waits for .is-open. */
|
|
1364
|
+
.sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab.icon-btn--invert {
|
|
1365
|
+
border-color: transparent; background-color: transparent; box-shadow: none; color: inherit;
|
|
1366
|
+
}
|
|
1327
1367
|
/* 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
|
|
1328
1368
|
margin around it reaches into the card's padding — and an open head (back · title · three tabs) keeps one line (Lh 2026-09-23) */
|
|
1329
1369
|
.card__head > .stack-sheet__tabs { margin-right: calc(-1 * var(--sp-2)); }
|
|
@@ -4136,6 +4176,11 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
4136
4176
|
0 1px 2px color-mix(in srgb, var(--_shade) 20%, transparent),
|
|
4137
4177
|
0 0.5rem 1rem -0.25rem color-mix(in srgb, var(--accent) 45%, transparent);
|
|
4138
4178
|
}
|
|
4179
|
+
/* a peeked stack tab is the icon alone: no sheen and no shadow until its sheet is open (the fill fades with the sheet) */
|
|
4180
|
+
[data-finish="lit"] .sheet-stack--tap > .card:not(.is-open) > .card__head .stack-sheet__tab {
|
|
4181
|
+
background-image: none;
|
|
4182
|
+
box-shadow: none;
|
|
4183
|
+
}
|
|
4139
4184
|
[data-finish="lit"] :is(.btn:not(.btn--quiet), .icon-btn:not(.icon-btn-group > *, .stack-sheet__tab:not(.icon-btn--invert))):active:not(:disabled) {
|
|
4140
4185
|
background-image: none;
|
|
4141
4186
|
box-shadow: var(--finish-control-down);
|
|
@@ -11,6 +11,8 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
11
11
|
onOpenChange?: (open: number | null) => void;
|
|
12
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
13
|
onRefresh?: (index: number) => void;
|
|
14
|
+
/** the sheet the app is reloading (its index), or `null`: while it is the open one its four grip dots fade up and down, at rest again when the app clears it, and a pull there does not ask again. The app sets it in `onRefresh` and clears it when its data is in */
|
|
15
|
+
refreshing?: number | null;
|
|
14
16
|
/** wider peeks so more of each title shows (display stacks) */
|
|
15
17
|
fanned?: boolean;
|
|
16
18
|
/** 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 */
|
|
@@ -30,7 +32,7 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
30
32
|
* A COMPONENT, not a wiring: WHICH sheet is open is the app's (`open` + `onOpenChange`, or `defaultOpen`) — so a button anywhere, a route or
|
|
31
33
|
* 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
34
|
* 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
|
|
35
|
+
* down, folds it. A pull down in the content, already at the top, asks `onRefresh` and does not fold; `refreshing` fades that sheet's four grip dots up and down while the app reloads. How the sheets MOVE is its own too: sheets are rigid — the tapped one slides up, the ones after it leave below, the ones
|
|
34
36
|
* before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); it sets classes, never a size.
|
|
35
37
|
*/
|
|
36
|
-
export declare function SheetStack({ mode, open, defaultOpen, onOpenChange, onRefresh, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }: SheetStackProps): import("react").JSX.Element;
|
|
38
|
+
export declare function SheetStack({ mode, open, defaultOpen, onOpenChange, onRefresh, refreshing, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }: SheetStackProps): import("react").JSX.Element;
|
|
@@ -44,10 +44,10 @@ const pullsHead = (target, head) => {
|
|
|
44
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
|
|
45
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
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
|
|
47
|
+
* down, folds it. A pull down in the content, already at the top, asks `onRefresh` and does not fold; `refreshing` fades that sheet's four grip dots up and down while the app reloads. How the sheets MOVE is its own too: sheets are rigid — the tapped one slides up, the ones after it leave below, the ones
|
|
48
48
|
* before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); it sets classes, never a size.
|
|
49
49
|
*/
|
|
50
|
-
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, onRefresh, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }) {
|
|
50
|
+
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, onRefresh, refreshing = null, fanned, closed, base, className, children, onClick, onKeyDown, ...rest }) {
|
|
51
51
|
const [current, request] = useControlled(open, defaultOpen, onOpenChange);
|
|
52
52
|
const ref = useRef(null);
|
|
53
53
|
const was = useRef(null);
|
|
@@ -56,6 +56,7 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
56
56
|
const bodyPull = useRef(null);
|
|
57
57
|
const swallow = useRef(false);
|
|
58
58
|
const refreshRef = useLatest(onRefresh);
|
|
59
|
+
const loadingRef = useLatest(refreshing);
|
|
59
60
|
/* paint the state onto the sheets — after every render, so a re-rendered Card never loses its place */
|
|
60
61
|
useLayoutEffect(() => {
|
|
61
62
|
const stack = ref.current;
|
|
@@ -74,6 +75,14 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
74
75
|
if (title)
|
|
75
76
|
title.tabIndex = current == null || i === current ? 0 : -1;
|
|
76
77
|
});
|
|
78
|
+
sheets.forEach((c, i) => {
|
|
79
|
+
const loading = refreshing === i && current === i;
|
|
80
|
+
c.classList.toggle('is-loading', loading);
|
|
81
|
+
if (loading)
|
|
82
|
+
c.setAttribute('aria-busy', 'true');
|
|
83
|
+
else
|
|
84
|
+
c.removeAttribute('aria-busy');
|
|
85
|
+
});
|
|
77
86
|
const prev = was.current;
|
|
78
87
|
was.current = current;
|
|
79
88
|
if (current != null) {
|
|
@@ -194,7 +203,7 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
194
203
|
listen(onMove, onUp, onCancel);
|
|
195
204
|
};
|
|
196
205
|
const onBodyDown = (e) => {
|
|
197
|
-
if (!primary(e) || headPull.current || bodyPull.current || !refreshRef.current)
|
|
206
|
+
if (!primary(e) || headPull.current || bodyPull.current || !refreshRef.current || loadingRef.current === index)
|
|
198
207
|
return;
|
|
199
208
|
if (scroller.scrollTop > 0)
|
|
200
209
|
return;
|
|
@@ -262,9 +271,32 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
262
271
|
};
|
|
263
272
|
listen(onMove, onUp, onCancel);
|
|
264
273
|
};
|
|
274
|
+
/* a FINGER: the browser takes a touch for its own scroll at the first move and cancels the pointer — unless that first move is
|
|
275
|
+
refused. So the first move decides, once: down and at the top = the pull's (refused, no scroll); anything else = the scroll's */
|
|
276
|
+
let touch = null;
|
|
277
|
+
const onTouchStart = (e) => {
|
|
278
|
+
const t = e.touches[0];
|
|
279
|
+
const inHead = e.target instanceof Element && !!e.target.closest('.card__head');
|
|
280
|
+
touch = e.touches.length === 1 && t && refreshRef.current && loadingRef.current !== index && scroller.scrollTop <= 0 && !inHead ? { x: t.clientX, y: t.clientY, ours: null } : null;
|
|
281
|
+
};
|
|
282
|
+
const onTouchMove = (e) => {
|
|
283
|
+
const t = e.touches[0];
|
|
284
|
+
if (!touch || !t)
|
|
285
|
+
return;
|
|
286
|
+
const dx = t.clientX - touch.x;
|
|
287
|
+
const dy = t.clientY - touch.y;
|
|
288
|
+
if (touch.ours == null)
|
|
289
|
+
touch.ours = dy > 0 && dy >= Math.abs(dx) && scroller.scrollTop <= 0;
|
|
290
|
+
if (touch.ours && e.cancelable)
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
};
|
|
265
293
|
head?.addEventListener('pointerdown', onHeadDown);
|
|
266
294
|
scroller.addEventListener('pointerdown', onBodyDown);
|
|
295
|
+
scroller.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
296
|
+
scroller.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
267
297
|
return () => {
|
|
298
|
+
scroller.removeEventListener('touchstart', onTouchStart);
|
|
299
|
+
scroller.removeEventListener('touchmove', onTouchMove);
|
|
268
300
|
stopDoc();
|
|
269
301
|
const held = headPull.current;
|
|
270
302
|
headPull.current = null;
|
|
@@ -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
|
@@ -31,6 +31,8 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
31
31
|
onOpenChange?: (open: number | null) => void;
|
|
32
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
33
|
onRefresh?: (index: number) => void;
|
|
34
|
+
/** the sheet the app is reloading (its index), or `null` — as `SheetStack refreshing` */
|
|
35
|
+
refreshing?: number | null;
|
|
34
36
|
/** CONTENT above the sheet heads — one `Page`: a welcome, the news, a picture. It reads
|
|
35
37
|
* from the top; a sheet coming up covers it */
|
|
36
38
|
base?: ReactNode;
|
|
@@ -50,4 +52,4 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
50
52
|
* 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
|
|
51
53
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
52
54
|
*/
|
|
53
|
-
export declare function Home({ top, title, sheets, open: openProp, defaultOpen, onOpenChange, onRefresh, base, keep, backLabel, backAt, children, ...rest }: HomeProps): import("react").JSX.Element;
|
|
55
|
+
export declare function Home({ top, title, sheets, open: openProp, defaultOpen, onOpenChange, onRefresh, refreshing, 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, onRefresh, base, keep, backLabel = word('back'), backAt, children, ...rest }) {
|
|
18
|
+
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, onRefresh, refreshing, 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, 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] }));
|
|
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, refreshing: refreshing, 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.53",
|
|
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.",
|