@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 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; its padding
207
- is the stack's own rule, because a pull adds --_refresh on top of this inset.
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; its padding
1620
- is the stack's own rule, because a pull adds --_refresh on top of this inset.
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 */
@@ -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,
@@ -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;
@@ -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.57",
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.",