@usableapp/cardds 0.7.57 → 0.7.59
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 +13 -0
- package/README.md +7 -1
- package/css/sheet.css +2 -2
- package/css/stack.css +38 -5
- package/dist/cardds.css +40 -7
- package/dist/sheets/SheetStack.d.ts +3 -1
- package/dist/sheets/SheetStack.js +67 -3
- package/dist/sheets/stackCard.d.ts +2 -0
- package/dist/sheets/stackCard.js +1 -1
- 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,19 @@
|
|
|
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.59 — 2026-10-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`SheetStack` / `Home`: a tap right after a fold opens the head you aimed at** (Lh 2026-10-06, reported by timebank). For about half a second after a sheet folded, the pile was still coming back: a tap where another head rests reopened the sheet that had just folded, did nothing, or opened a third sheet. While the pile comes back, a tap is now answered by where each head RESTS — the head that will stand under the finger opens. Nothing to change in a screen. A test that waited for `is-before` / `is-after` to leave before tapping still passes; it no longer has to wait. Still as before: a tap on a tab's place during that moment opens the sheet on its current tab, not that tab.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`SheetStack` and `Home` take `mark`** — the grip at the middle of every sheet's top edge: `dots` (default, the four dots) or `line` (one line, the single `Sheet`'s, at the same place — the title does not move). While a sheet is `refreshing`, the line changes colour the same way the dots do. One mark for the whole stack: `<Home mark="line" …>`. Omit it and nothing changes.
|
|
12
|
+
|
|
13
|
+
## 0.7.58 — 2026-10-06
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **A closed paper sheet in a stack shows the card's colour where the next sheet's round top does not cover it.** The white body used to show in those corners. While the sheet is closed that body is clear. On the way back into the stack the card colour fades in over `--motion-quick`, and the fade ends as the sheet stops. Open, the body is white at once. The first row still sits `--sp-6` down. Nothing to change in a screen.
|
|
17
|
+
|
|
5
18
|
## 0.7.57 — 2026-10-06
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -183,7 +183,11 @@ the card is the material, the sheet is what it is in the stack.
|
|
|
183
183
|
content, already at the top, asks `onRefresh` — omit it and that pull only
|
|
184
184
|
scrolls. A tab in the head stays a click. Every sheet wears a grip — four
|
|
185
185
|
small dots at the middle of its top edge, a mark that it slides, not a
|
|
186
|
-
control.
|
|
186
|
+
control. `mark="line"` on the `SheetStack` (or `Home`) draws one line
|
|
187
|
+
there instead, as a single `Sheet`'s; while a sheet is `refreshing`, either
|
|
188
|
+
mark changes colour the same way. Right after a fold, while the
|
|
189
|
+
pile is still coming back, a tap is answered by where each head RESTS: the
|
|
190
|
+
head that will stand under the finger opens, whatever is passing there.
|
|
187
191
|
- **Five sheets max.** Slots are declared per `:nth-child` in `stack.css`; a
|
|
188
192
|
sixth would render at slot 0, on top of the first.
|
|
189
193
|
- **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
|
|
@@ -278,6 +282,8 @@ whose markup has rules, because the sheet's box is the step itself:
|
|
|
278
282
|
the content white while the head keeps the sheet's colour (`tone`, as a card).
|
|
279
283
|
The same prop on a `StackSheet`, or on a `Card` in a stack: the head keeps the
|
|
280
284
|
tone, the body and the foot are white, and the gap under the head stays the tone.
|
|
285
|
+
While that sheet is closed, the corners the next sheet does not cover show the
|
|
286
|
+
card's colour. That colour fades in as the sheet returns, and is done when it stops.
|
|
281
287
|
A sheet with no head (the dim page sheet) is a body alone.
|
|
282
288
|
- **The step follows its top bar directly, and touches it.** `full` means
|
|
283
289
|
the sheet's top edge at the bar's bottom edge — nothing above it but the
|
package/css/sheet.css
CHANGED
|
@@ -203,8 +203,8 @@
|
|
|
203
203
|
The flex gap under the head is outside the body, so it stays the sheet's tint.
|
|
204
204
|
The first field would sit on that colour seam. The white inset is --sp-6 (24px at
|
|
205
205
|
the default spacing), the body's own top padding, and only when the sheet is paper.
|
|
206
|
-
A plain sheet keeps padding-top 0. A sheet in a stack wears the same class
|
|
207
|
-
|
|
206
|
+
A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
|
|
207
|
+
inset: the stack's own gap under the head is the card's colour, so the row still needs this white.
|
|
208
208
|
The ink falls back to the wireframe word colour: an unset --color-ink would otherwise
|
|
209
209
|
inherit a dark card's light ink, and the words would vanish on the white. */
|
|
210
210
|
.sheet--paper > .sheet__body {
|
package/css/stack.css
CHANGED
|
@@ -175,12 +175,10 @@
|
|
|
175
175
|
}
|
|
176
176
|
.sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
|
|
177
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.
|
|
178
|
+
/* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
|
|
179
|
+
A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
|
|
180
|
+
card's colour, so without the white inset the first row sits on that seam. */
|
|
179
181
|
.sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0px); transition: padding-top var(--motion-sheet) ease; }
|
|
180
|
-
/* paper on a stacked sheet is the same class as the single sheet (Lh 2026-10-06). The head keeps the card's
|
|
181
|
-
colour. The body is white, from the rule in sheet.css, and its top inset is --sp-6. A pull still adds
|
|
182
|
-
--_refresh: this rule wins on padding, because the pull's rule above is more specific than the single sheet's. */
|
|
183
|
-
.sheet-stack > .card.sheet--paper > .sheet__body { padding-top: calc(var(--sp-6) + var(--_refresh, 0px)); }
|
|
184
182
|
.sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
|
|
185
183
|
padding-top: calc(var(--card-pad) + var(--_refresh, 0px));
|
|
186
184
|
transition: translate var(--motion-sheet) ease, padding-top var(--motion-sheet) ease;
|
|
@@ -212,6 +210,18 @@
|
|
|
212
210
|
animation: stack-grip-dissolve 0.4s ease-in-out infinite alternate;
|
|
213
211
|
}
|
|
214
212
|
@keyframes stack-grip-dissolve { from { --_grip: 0; } to { --_grip: 1; } }
|
|
213
|
+
/* the grip as ONE LINE (`mark="line"`, Lh 2026-10-06): the single Sheet's line, at the same place and the same length. It reads the
|
|
214
|
+
same --_grip-ink, so while its sheet reloads it changes colour exactly as the dots do. A bar and two round ends; the resting
|
|
215
|
+
ink is mixed into the card's own colour (opaque), so the ends do not show darker where they lie on the bar. */
|
|
216
|
+
.stack-card--line {
|
|
217
|
+
--_grip-w: calc(var(--sp-base) * 18);
|
|
218
|
+
--_grip-ink: color-mix(in srgb, currentColor 30%, var(--card-bg));
|
|
219
|
+
--_grip-bar: linear-gradient(var(--_grip-ink), var(--_grip-ink));
|
|
220
|
+
--_grip-end: calc((var(--_grip-w) - var(--_grip-dot)) / 2);
|
|
221
|
+
background-image: var(--_grip-img), var(--_grip-bar), var(--_grip-img);
|
|
222
|
+
background-size: var(--_grip-dot) var(--_grip-dot), calc(var(--_grip-w) - var(--_grip-dot)) var(--_grip-dot), var(--_grip-dot) var(--_grip-dot);
|
|
223
|
+
background-position: calc(50% - var(--_grip-end)) var(--_grip-y), 50% var(--_grip-y), calc(50% + var(--_grip-end)) var(--_grip-y);
|
|
224
|
+
}
|
|
215
225
|
@media (prefers-reduced-motion: reduce) {
|
|
216
226
|
.stack-card.is-open.is-loading { animation: none; --_grip-ink: currentColor; }
|
|
217
227
|
}
|
|
@@ -315,6 +325,29 @@
|
|
|
315
325
|
.sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
|
|
316
326
|
.sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
|
|
317
327
|
.sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
|
|
328
|
+
/* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
|
|
329
|
+
cover the corners, and a white body showed there beside the head. Clear, the card's own colour
|
|
330
|
+
shows in that pocket. Open, the body is white, and the first row sits --sp-6 down, the same white
|
|
331
|
+
inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
|
|
332
|
+
would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
|
|
333
|
+
into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
|
|
334
|
+
sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
|
|
335
|
+
at the moment the sheet stops. Rising, the body is white at once. */
|
|
336
|
+
.sheet-stack > .card.sheet--paper > .sheet__body {
|
|
337
|
+
padding-top: calc(var(--sp-6) + var(--_refresh, 0px));
|
|
338
|
+
transition: padding-top var(--motion-sheet) ease, background-color 0s linear;
|
|
339
|
+
}
|
|
340
|
+
.sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
|
|
341
|
+
background-color: transparent;
|
|
342
|
+
transition:
|
|
343
|
+
padding-top var(--motion-sheet) ease,
|
|
344
|
+
background-color var(--motion-quick) ease calc(var(--stagger, 0) * 70ms + var(--motion-sheet) - var(--motion-quick));
|
|
345
|
+
}
|
|
346
|
+
@media (prefers-reduced-motion: reduce) {
|
|
347
|
+
.sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
|
|
348
|
+
transition: padding-top var(--motion-sheet) ease, background-color 0s linear;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
318
351
|
/* a paper stack's foot is white to the card's edges, and it covers the tinted flex gap above it so the
|
|
319
352
|
button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
|
|
320
353
|
where it was — the padding gives back what the bleed takes. */
|
package/dist/cardds.css
CHANGED
|
@@ -1176,12 +1176,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1176
1176
|
}
|
|
1177
1177
|
.sheet-stack--tap > .card.is-open.is-dragging { transition: none; }
|
|
1178
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.
|
|
1179
|
+
/* a pull in the content, already at the top: the words move down, the card stays. --_refresh is the finger, private.
|
|
1180
|
+
A paper body adds its own --sp-6 on top of this (the rule below): the stack's gap under the head is the
|
|
1181
|
+
card's colour, so without the white inset the first row sits on that seam. */
|
|
1180
1182
|
.sheet-stack > .card > .sheet__body { padding-top: var(--_refresh, 0px); transition: padding-top var(--motion-sheet) ease; }
|
|
1181
|
-
/* paper on a stacked sheet is the same class as the single sheet (Lh 2026-10-06). The head keeps the card's
|
|
1182
|
-
colour. The body is white, from the rule in sheet.css, and its top inset is --sp-6. A pull still adds
|
|
1183
|
-
--_refresh: this rule wins on padding, because the pull's rule above is more specific than the single sheet's. */
|
|
1184
|
-
.sheet-stack > .card.sheet--paper > .sheet__body { padding-top: calc(var(--sp-6) + var(--_refresh, 0px)); }
|
|
1185
1183
|
.sheet-stack--tap > .card.is-open:not(:has(> .sheet__body)) {
|
|
1186
1184
|
padding-top: calc(var(--card-pad) + var(--_refresh, 0px));
|
|
1187
1185
|
transition: translate var(--motion-sheet) ease, padding-top var(--motion-sheet) ease;
|
|
@@ -1213,6 +1211,18 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1213
1211
|
animation: stack-grip-dissolve 0.4s ease-in-out infinite alternate;
|
|
1214
1212
|
}
|
|
1215
1213
|
@keyframes stack-grip-dissolve { from { --_grip: 0; } to { --_grip: 1; } }
|
|
1214
|
+
/* the grip as ONE LINE (`mark="line"`, Lh 2026-10-06): the single Sheet's line, at the same place and the same length. It reads the
|
|
1215
|
+
same --_grip-ink, so while its sheet reloads it changes colour exactly as the dots do. A bar and two round ends; the resting
|
|
1216
|
+
ink is mixed into the card's own colour (opaque), so the ends do not show darker where they lie on the bar. */
|
|
1217
|
+
.stack-card--line {
|
|
1218
|
+
--_grip-w: calc(var(--sp-base) * 18);
|
|
1219
|
+
--_grip-ink: color-mix(in srgb, currentColor 30%, var(--card-bg));
|
|
1220
|
+
--_grip-bar: linear-gradient(var(--_grip-ink), var(--_grip-ink));
|
|
1221
|
+
--_grip-end: calc((var(--_grip-w) - var(--_grip-dot)) / 2);
|
|
1222
|
+
background-image: var(--_grip-img), var(--_grip-bar), var(--_grip-img);
|
|
1223
|
+
background-size: var(--_grip-dot) var(--_grip-dot), calc(var(--_grip-w) - var(--_grip-dot)) var(--_grip-dot), var(--_grip-dot) var(--_grip-dot);
|
|
1224
|
+
background-position: calc(50% - var(--_grip-end)) var(--_grip-y), 50% var(--_grip-y), calc(50% + var(--_grip-end)) var(--_grip-y);
|
|
1225
|
+
}
|
|
1216
1226
|
@media (prefers-reduced-motion: reduce) {
|
|
1217
1227
|
.stack-card.is-open.is-loading { animation: none; --_grip-ink: currentColor; }
|
|
1218
1228
|
}
|
|
@@ -1316,6 +1326,29 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1316
1326
|
.sheet-stack--tap > .card:has(> .sheet__body).is-open { overflow: clip; }
|
|
1317
1327
|
.sheet-stack > .card:has(> .sheet__body) > :not(.sheet__body) { flex-shrink: 0; }
|
|
1318
1328
|
.sheet-stack > .card > .sheet__body { --_cover: 0rem; } /* nothing slid under here: the stack's sheet is always whole */
|
|
1329
|
+
/* a closed paper sheet's body stays clear (Lh 2026-10-06). The next sheet's round top does not
|
|
1330
|
+
cover the corners, and a white body showed there beside the head. Clear, the card's own colour
|
|
1331
|
+
shows in that pocket. Open, the body is white, and the first row sits --sp-6 down, the same white
|
|
1332
|
+
inset a single paper sheet keeps: the flex gap under the head is the card's colour, so the row
|
|
1333
|
+
would otherwise sit on that seam. A pull adds --_refresh on top of the inset. On the way back
|
|
1334
|
+
into the stack the card colour fades in quickly (--motion-quick) and that fade ends with the
|
|
1335
|
+
sheet's own slide (--motion-sheet, plus the same stagger), so the corner is the head colour
|
|
1336
|
+
at the moment the sheet stops. Rising, the body is white at once. */
|
|
1337
|
+
.sheet-stack > .card.sheet--paper > .sheet__body {
|
|
1338
|
+
padding-top: calc(var(--sp-6) + var(--_refresh, 0px));
|
|
1339
|
+
transition: padding-top var(--motion-sheet) ease, background-color 0s linear;
|
|
1340
|
+
}
|
|
1341
|
+
.sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
|
|
1342
|
+
background-color: transparent;
|
|
1343
|
+
transition:
|
|
1344
|
+
padding-top var(--motion-sheet) ease,
|
|
1345
|
+
background-color var(--motion-quick) ease calc(var(--stagger, 0) * 70ms + var(--motion-sheet) - var(--motion-quick));
|
|
1346
|
+
}
|
|
1347
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1348
|
+
.sheet-stack > .card.sheet--paper:not(.is-open) > .sheet__body {
|
|
1349
|
+
transition: padding-top var(--motion-sheet) ease, background-color 0s linear;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1319
1352
|
/* a paper stack's foot is white to the card's edges, and it covers the tinted flex gap above it so the
|
|
1320
1353
|
button does not sit on a colour stripe. The head's gap stays: it is outside this foot. The button stays
|
|
1321
1354
|
where it was — the padding gives back what the bleed takes. */
|
|
@@ -1616,8 +1649,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1616
1649
|
The flex gap under the head is outside the body, so it stays the sheet's tint.
|
|
1617
1650
|
The first field would sit on that colour seam. The white inset is --sp-6 (24px at
|
|
1618
1651
|
the default spacing), the body's own top padding, and only when the sheet is paper.
|
|
1619
|
-
A plain sheet keeps padding-top 0. A sheet in a stack wears the same class
|
|
1620
|
-
|
|
1652
|
+
A plain sheet keeps padding-top 0. A sheet in a stack wears the same class and the same
|
|
1653
|
+
inset: the stack's own gap under the head is the card's colour, so the row still needs this white.
|
|
1621
1654
|
The ink falls back to the wireframe word colour: an unset --color-ink would otherwise
|
|
1622
1655
|
inherit a dark card's light ink, and the words would vanish on the white. */
|
|
1623
1656
|
.sheet--paper > .sheet__body {
|
|
@@ -13,6 +13,8 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
13
13
|
onRefresh?: (index: number) => void;
|
|
14
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
15
|
refreshing?: number | null;
|
|
16
|
+
/** the grip every sheet wears at the middle of its top edge: `dots` (default, four small dots) or `line` (one line, as a `Sheet`'s). Either one changes colour the same way while its sheet is `refreshing` */
|
|
17
|
+
mark?: 'dots' | 'line';
|
|
16
18
|
/** wider peeks so more of each title shows (display stacks) */
|
|
17
19
|
fanned?: boolean;
|
|
18
20
|
/** 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 */
|
|
@@ -34,4 +36,4 @@ export interface SheetStackProps extends ComponentProps<'div'> {
|
|
|
34
36
|
* the place classes, the grip, the pull. The stack only says which index. Sheets are rigid — the open one slides up, the ones after it leave
|
|
35
37
|
* below, the ones before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); never a size.
|
|
36
38
|
*/
|
|
37
|
-
export declare function SheetStack({ mode, open, defaultOpen, onOpenChange, onRefresh, refreshing, fanned, closed, base, className, children, ...rest }: SheetStackProps): import("react").JSX.Element;
|
|
39
|
+
export declare function SheetStack({ mode, open, defaultOpen, onOpenChange, onRefresh, refreshing, mark, fanned, closed, base, className, children, onClickCapture, onPointerDownCapture, ref: passed, ...rest }: SheetStackProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Children, cloneElement, isValidElement, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Children, cloneElement, isValidElement, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useControlled, warnOnce } from '../state.js';
|
|
4
4
|
import { cx } from '../cx.js';
|
|
5
5
|
import { Card } from '../cards/Card.js';
|
|
@@ -7,6 +7,24 @@ import { StackSheet } from './StackSheet.js';
|
|
|
7
7
|
const PHASE = 380; // ms before the before-pack un-tucks on close, so the after-pack is clear first
|
|
8
8
|
const UNTUCK = 700; // the stagger stays for the return: the delay is read when is-before leaves, and a later render must not take it away mid-move
|
|
9
9
|
const SHEET = new Set([Card, StackSheet]);
|
|
10
|
+
const cardsIn = (stack) => [...stack.children]
|
|
11
|
+
.filter((c) => !c.classList.contains('sheet-stack__base'))
|
|
12
|
+
.map((c) => (c.classList.contains('card') ? c : c.querySelector(':scope > .card')))
|
|
13
|
+
.filter((c) => c instanceof HTMLElement);
|
|
14
|
+
function measureRests(stack) {
|
|
15
|
+
const box = stack.getBoundingClientRect();
|
|
16
|
+
const cards = cardsIn(stack);
|
|
17
|
+
const tops = cards.map((c) => c.getBoundingClientRect().top - box.top);
|
|
18
|
+
return {
|
|
19
|
+
height: box.height,
|
|
20
|
+
strips: cards.map((c, i) => {
|
|
21
|
+
const head = c.querySelector(':scope > .card__head')?.getBoundingClientRect();
|
|
22
|
+
const own = head ? head.bottom - box.top + (head.top - box.top - tops[i]) : tops[i];
|
|
23
|
+
return { top: tops[i], end: i < cards.length - 1 ? tops[i + 1] : own };
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const SLOP = 8; // px a pointer may travel and still be a tap
|
|
10
28
|
/* one wrapper around a sheet is fine (a host's own box). The props land on the Card or StackSheet inside it, so the card renders them. */
|
|
11
29
|
function stamp(child, place) {
|
|
12
30
|
if (SHEET.has(child.type))
|
|
@@ -37,7 +55,7 @@ function stamp(child, place) {
|
|
|
37
55
|
* the place classes, the grip, the pull. The stack only says which index. Sheets are rigid — the open one slides up, the ones after it leave
|
|
38
56
|
* below, the ones before tuck under nearest-first; closing reverses in two beats. Only `translate` (stack.css); never a size.
|
|
39
57
|
*/
|
|
40
|
-
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, onRefresh, refreshing = null, fanned, closed, base, className, children, ...rest }) {
|
|
58
|
+
export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChange, onRefresh, refreshing = null, mark = 'dots', fanned, closed, base, className, children, onClickCapture, onPointerDownCapture, ref: passed, ...rest }) {
|
|
41
59
|
const [current, request] = useControlled(open, defaultOpen, onOpenChange);
|
|
42
60
|
const prev = useRef(current);
|
|
43
61
|
const [closingFrom, setClosingFrom] = useState(null);
|
|
@@ -73,6 +91,51 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
73
91
|
}, Math.max(0, held.until - performance.now()));
|
|
74
92
|
return () => clearTimeout(t);
|
|
75
93
|
});
|
|
94
|
+
/* a tap while the pile comes back from a fold (Lh 2026-10-06). The sheets are not where they rest yet — the folded one is passing,
|
|
95
|
+
the tucked ones stand a step low — so what is under the finger is not what the eye aimed at. The tap is answered by the RESTING
|
|
96
|
+
place: the head that will stand there opens. The places are measured while the pile is still. */
|
|
97
|
+
const ref = useRef(null);
|
|
98
|
+
const setRef = (node) => {
|
|
99
|
+
ref.current = node;
|
|
100
|
+
if (typeof passed === 'function')
|
|
101
|
+
passed(node);
|
|
102
|
+
else if (passed)
|
|
103
|
+
passed.current = node;
|
|
104
|
+
};
|
|
105
|
+
const rests = useRef(null);
|
|
106
|
+
const down = useRef(null);
|
|
107
|
+
const settling = mode === 'tap' && current == null && (closingFrom != null || back.current != null);
|
|
108
|
+
const still = mode === 'tap' && current == null && !settling;
|
|
109
|
+
useLayoutEffect(() => {
|
|
110
|
+
if (still && ref.current)
|
|
111
|
+
rests.current = measureRests(ref.current);
|
|
112
|
+
});
|
|
113
|
+
const pointerDownCapture = (e) => {
|
|
114
|
+
onPointerDownCapture?.(e);
|
|
115
|
+
down.current = { x: e.clientX, y: e.clientY };
|
|
116
|
+
};
|
|
117
|
+
const clickCapture = (e) => {
|
|
118
|
+
onClickCapture?.(e);
|
|
119
|
+
const stack = ref.current;
|
|
120
|
+
const at = rests.current;
|
|
121
|
+
const from = down.current;
|
|
122
|
+
if (!settling || !stack || !at || e.detail === 0)
|
|
123
|
+
return; // detail 0: the keyboard's click has no place
|
|
124
|
+
if (from && Math.hypot(e.clientX - from.x, e.clientY - from.y) > SLOP)
|
|
125
|
+
return; // a pull that sprang back, not a tap
|
|
126
|
+
const box = stack.getBoundingClientRect();
|
|
127
|
+
if (Math.abs(box.height - at.height) > 1)
|
|
128
|
+
return; // the stack changed size since it was still: the places are stale
|
|
129
|
+
const y = e.clientY - box.top;
|
|
130
|
+
const i = at.strips.findIndex((s) => y >= s.top && y < s.end);
|
|
131
|
+
if (i < 0)
|
|
132
|
+
return;
|
|
133
|
+
const cards = cardsIn(stack);
|
|
134
|
+
if (cards[i]?.contains(e.target) && !cards[i].classList.contains('is-before'))
|
|
135
|
+
return; // already home: its own tap, a tab's too
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
request(i);
|
|
138
|
+
};
|
|
76
139
|
const closing = current == null && closingFrom != null;
|
|
77
140
|
const anchor = current ?? closingFrom;
|
|
78
141
|
const items = Children.toArray(children);
|
|
@@ -85,6 +148,7 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
85
148
|
const place = {
|
|
86
149
|
open: !closing && current === i,
|
|
87
150
|
refreshing: refreshing === i,
|
|
151
|
+
mark,
|
|
88
152
|
before,
|
|
89
153
|
after: !closing && current != null && i > current,
|
|
90
154
|
stagger: before && anchor != null ? anchor - 1 - i : returning != null && i < returning ? returning - 1 - i : 0,
|
|
@@ -94,5 +158,5 @@ export function SheetStack({ mode = 'tap', open, defaultOpen = null, onOpenChang
|
|
|
94
158
|
};
|
|
95
159
|
return stamp(child, place);
|
|
96
160
|
}) : items;
|
|
97
|
-
return (_jsxs("div", { className: cx('sheet-stack', mode !== 'static' && `sheet-stack--${mode}`, fanned && 'sheet-stack--fanned', closed && 'sheet-stack--closed', className), ...rest, children: [stamped, base != null && _jsx("div", { className: "sheet-stack__base", children: base })] }));
|
|
161
|
+
return (_jsxs("div", { className: cx('sheet-stack', mode !== 'static' && `sheet-stack--${mode}`, fanned && 'sheet-stack--fanned', closed && 'sheet-stack--closed', className), ...rest, ref: setRef, onClickCapture: clickCapture, onPointerDownCapture: pointerDownCapture, children: [stamped, base != null && _jsx("div", { className: "sheet-stack__base", children: base })] }));
|
|
98
162
|
}
|
|
@@ -5,6 +5,8 @@ export interface StackPlace {
|
|
|
5
5
|
open: boolean;
|
|
6
6
|
/** the app is reloading this sheet */
|
|
7
7
|
refreshing: boolean;
|
|
8
|
+
/** the grip at the card's top edge: four dots, or one line */
|
|
9
|
+
mark: 'dots' | 'line';
|
|
8
10
|
/** tucked under the open sheet. Stays through the first beat of a close */
|
|
9
11
|
before: boolean;
|
|
10
12
|
/** in front of the open sheet, leaving below */
|
package/dist/sheets/stackCard.js
CHANGED
|
@@ -14,7 +14,7 @@ export function stackPaint(place, pulling = false) {
|
|
|
14
14
|
if (!place)
|
|
15
15
|
return {};
|
|
16
16
|
const loading = place.open && place.refreshing;
|
|
17
|
-
const className = ['stack-card', place.open && 'is-open', place.before && 'is-before', place.after && 'is-after', loading && 'is-loading', pulling && 'is-refreshing'].filter(Boolean).join(' ');
|
|
17
|
+
const className = ['stack-card', place.mark === 'line' && 'stack-card--line', place.open && 'is-open', place.before && 'is-before', place.after && 'is-after', loading && 'is-loading', pulling && 'is-refreshing'].filter(Boolean).join(' ');
|
|
18
18
|
return {
|
|
19
19
|
className,
|
|
20
20
|
style: place.stagger > 0 ? { '--stagger': String(place.stagger) } : undefined,
|
package/dist/templates/Home.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
35
35
|
onRefresh?: (index: number) => void;
|
|
36
36
|
/** the sheet the app is reloading (its index), or `null` — as `SheetStack refreshing` */
|
|
37
37
|
refreshing?: number | null;
|
|
38
|
+
/** the grip on every sheet: `dots` (default) or `line` — as `SheetStack mark` */
|
|
39
|
+
mark?: 'dots' | 'line';
|
|
38
40
|
/** CONTENT above the sheet heads — one `Page`: a welcome, the news, a picture. It reads
|
|
39
41
|
* from the top; a sheet coming up covers it */
|
|
40
42
|
base?: ReactNode;
|
|
@@ -54,4 +56,4 @@ export interface HomeProps extends Omit<ComponentProps<typeof Screen>, 'children
|
|
|
54
56
|
* at the bottom). A tap on a head brings that sheet up to fill everything under the top. With a LayerHistory above and no `open` prop, that open sheet is one history entry named `home`. Only the open sheet's body is rendered — three
|
|
55
57
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
56
58
|
*/
|
|
57
|
-
export declare function Home({ top, title, sheets, open: openProp, defaultOpen, onOpenChange, onRefresh, refreshing, base, keep, backLabel, backAt, children, ...rest }: HomeProps): import("react").JSX.Element;
|
|
59
|
+
export declare function Home({ top, title, sheets, open: openProp, defaultOpen, onOpenChange, onRefresh, refreshing, mark, base, keep, backLabel, backAt, children, ...rest }: HomeProps): import("react").JSX.Element;
|
package/dist/templates/Home.js
CHANGED
|
@@ -16,7 +16,7 @@ const LEAVE = 800; // ms: a closing sheet keeps its body until it has slid home
|
|
|
16
16
|
* at the bottom). A tap on a head brings that sheet up to fill everything under the top. With a LayerHistory above and no `open` prop, that open sheet is one history entry named `home`. Only the open sheet's body is rendered — three
|
|
17
17
|
* sections on one home never cost three pages (`keep` holds a body once opened).
|
|
18
18
|
*/
|
|
19
|
-
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, onRefresh, refreshing, base, keep, backLabel = word('back'), backAt, children, ...rest }) {
|
|
19
|
+
export function Home({ top, title, sheets, open: openProp, defaultOpen = null, onOpenChange, onRefresh, refreshing, mark, base, keep, backLabel = word('back'), backAt, children, ...rest }) {
|
|
20
20
|
const layer = useOptionalLayer('home');
|
|
21
21
|
const [local, change] = useControlled(openProp, defaultOpen, onOpenChange);
|
|
22
22
|
// A passed `open` is the parent's. With no LayerHistory the index stays local, so a story without a router still opens sheets.
|
|
@@ -50,5 +50,5 @@ export function Home({ top, title, sheets, open: openProp, defaultOpen = null, o
|
|
|
50
50
|
}, [leaving]);
|
|
51
51
|
const [tabs, setTabs] = useState(() => Object.fromEntries(sheets.map((x, i) => [i, x.defaultTab ?? x.tabs?.[0]?.id])));
|
|
52
52
|
const shows = (i) => i === shown || i === leaving || (keep && seen.has(i));
|
|
53
|
-
return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: shown, onOpenChange: request, onRefresh: onRefresh, refreshing: refreshing, base: base, children: sheets.map((s, i) => (_jsx(StackSheet, { tone: s.tone ?? TONES[i], paper: s.paper, 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] }));
|
|
53
|
+
return (_jsxs(Screen, { fill: true, top: top, ...rest, children: [_jsx(Text, { level: "h1", as: "h1", sr: true, children: title }), _jsx(SheetStack, { closed: true, open: shown, onOpenChange: request, onRefresh: onRefresh, refreshing: refreshing, mark: mark, base: base, children: sheets.map((s, i) => (_jsx(StackSheet, { tone: s.tone ?? TONES[i], paper: s.paper, 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] }));
|
|
54
54
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.59",
|
|
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.",
|