@usableapp/cardds 0.7.77 → 0.7.79
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 +15 -0
- package/README.md +32 -0
- package/css/actions.css +4 -3
- package/css/base.css +2 -2
- package/css/card.css +30 -0
- package/css/stack.css +23 -3
- package/dist/cardds.css +59 -8
- package/dist/cards/CardFeed.d.ts +46 -0
- package/dist/cards/CardFeed.js +105 -0
- package/dist/cards/cardGo.js +7 -5
- package/dist/catalog.json +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sheets/SheetStack.d.ts +1 -1
- package/dist/sheets/SheetStack.js +15 -2
- package/dist/templates/Home.d.ts +5 -1
- package/dist/templates/Home.js +2 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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.79 — 2026-10-10
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **A closed stack's base too short for what rests in it scrolls** (Lh 2026-10-10) — before → after: loose things in `SheetStack base` / `Home base` stood on the heads, so on a short phone a content taller than the room ran out over the room's top, cut, and nothing scrolled (two feed cards at 320×568) → nothing is squeezed, the upper thing shows whole from the room's top, the rest lies under the sheet heads, and the member scrolls the room up until the last thing stands clear of the heads by `--stack-base-gap`. A base that holds its content is unchanged, to the pixel, and still takes no scroll and no pull. One `Page` and one `Centre` in a base are unchanged. Nothing to do in an app; a base no longer needs its own short-phone layout.
|
|
9
|
+
- **`FeedCard foot` takes more than one button** — one button: the big card is pressed whole, as before. Two or more (a question: yes · no): only the buttons act. Small, the card hides them all. Story `CardFeed · Question`.
|
|
10
|
+
|
|
11
|
+
## 0.7.78 — 2026-10-10
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`CardFeed` + `FeedCard`: a queue of cards, two in sight** (Lh 2026-10-09) — before → after: a home that offers several things in turn had to lay out its own cards and decide by hand which one is open → `<CardFeed>` takes the WHOLE queue as `<FeedCard id heading caption icon tone foot>` children in the app's order and shows the first two, one big (heading, content, its button) and one small (the same card cut down to its head). A touch anywhere on the small one swaps the sizes, neither changes place. When the two cards shown change, what is left slides up, the next comes in as the lower card and the feed asks for the upper one to be big. One card alone is big; with none it draws nothing. The app owns the queue (stop rendering a card and it leaves) and may own the size: `big` + `onBigChange(id, 'tap' | 'queue')`, or `defaultBig`. Nothing to wire for the press: the big card is pressed whole (0.7.76), the small card is itself a button. Knobs `--card-feed-gap` (the card gap), `--feed-card-head-h` (`--tap`).
|
|
15
|
+
- **`Home sheets[n].tab` + `onTabChange`** — before → after: a Home sheet's tab was the Home's own (`defaultTab` only), so nothing outside the stack could bring a sheet up on a chosen tab → give a sheet `tab` and it shows that tab; a tapped tab asks `onTabChange(tab)`. Set `tab` and `open` together to open a sheet on a named tab. A sheet without `tab` behaves as before.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **The pressed-whole rule reads a card more exactly** — a control inside an `inert` part of a card no longer counts as one of its controls; a card that is itself `role="button"` is never pressed whole; and the card's foot counts as its own when a part of the card holds it (not only as a direct child). No card of the gallery changed class.
|
|
19
|
+
|
|
5
20
|
## 0.7.77 — 2026-10-10
|
|
6
21
|
|
|
7
22
|
### Added
|
package/README.md
CHANGED
|
@@ -209,6 +209,13 @@ the card is the material, the sheet is what it is in the stack.
|
|
|
209
209
|
last one as between two of them (`--stack-base-gap`); one `Page` reads from
|
|
210
210
|
the top, one `Centre` stands in the middle. A card standing there is flat:
|
|
211
211
|
the base clips at its edges, so a shadow would end in a straight line.
|
|
212
|
+
A room too short for the loose things in it squeezes nothing: the upper
|
|
213
|
+
thing shows whole from the room's top, the rest lies under the heads, and
|
|
214
|
+
the room scrolls up until the last thing stands clear of the heads by
|
|
215
|
+
`--stack-base-gap`. `SheetStack` measures this and renders `is-long` on the
|
|
216
|
+
base; only then does the base scroll and run under the first sheet's round
|
|
217
|
+
corners. A room that holds its content does not scroll, and a finger on it
|
|
218
|
+
moves nothing.
|
|
212
219
|
A tap, or a pull up, opens any sheet over it. The open title or its back
|
|
213
220
|
button, tapped or pulled down, drops it home. A pull down in the open
|
|
214
221
|
content, already at the top, asks `onRefresh` — omit it and that pull only
|
|
@@ -225,6 +232,11 @@ the card is the material, the sheet is what it is in the stack.
|
|
|
225
232
|
under the sheets (hidden while one is open), nothing resizes. At the screen's
|
|
226
233
|
top the open sheet's top corners are square; folded, they are round again. It needs
|
|
227
234
|
`--closed` in a `Screen fill`.
|
|
235
|
+
- **`Home` takes a sheet's tab from the app.** Each of `Home sheets` keeps its
|
|
236
|
+
own tab (`defaultTab`, else the first) unless the app gives it `tab` — then
|
|
237
|
+
that tab shows, and a tapped tab asks `onTabChange(tab)`. Something outside
|
|
238
|
+
the stack (a card in the base) brings a sheet up on a named tab by setting
|
|
239
|
+
that sheet's `tab` and the Home's `open` together. Story `Home · Feed`.
|
|
228
240
|
- **Five sheets max.** Slots are declared per `:nth-child` in `stack.css`; a
|
|
229
241
|
sixth would render at slot 0, on top of the first.
|
|
230
242
|
- **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
|
|
@@ -491,6 +503,26 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
491
503
|
the one button in the head or in the content, a disabled button, a sheet, a
|
|
492
504
|
stack's sheet, a modal. A card that is itself a link or a label was pressed
|
|
493
505
|
whole already. Story `Card · PressedWhole`.
|
|
506
|
+
- **A feed is a queue of cards, two in sight** (Lh 2026-10-09). `CardFeed`
|
|
507
|
+
takes the WHOLE queue as `FeedCard` children, in the app's order, and shows
|
|
508
|
+
the first two: one BIG (heading, content, its button) and one SMALL (the
|
|
509
|
+
same card cut down to its head — no content, no button). A touch anywhere
|
|
510
|
+
on the small one makes it the big one and the other the small one; neither
|
|
511
|
+
changes place. The small card is itself the button (named by its heading,
|
|
512
|
+
Enter and Space) and what it does not show cannot be pressed, focused or
|
|
513
|
+
read; the big card is a plain card, pressed whole by the rule above. When
|
|
514
|
+
the two cards shown change — a card left the queue, a new one came — what
|
|
515
|
+
is left slides up, the next comes in as the lower card, and the UPPER card
|
|
516
|
+
is big, whichever was big before. One card alone is big; no card, nothing
|
|
517
|
+
is drawn. The queue is the app's: a card leaves when the app stops
|
|
518
|
+
rendering it. Which card is big is controlled like an input: `big` (a
|
|
519
|
+
card's `id`) + `onBigChange(id, 'tap' | 'queue')`, or `defaultBig`. A
|
|
520
|
+
`FeedCard` is `id` · `heading` · `caption` · `icon` (its thumb, in both
|
|
521
|
+
sizes) · `tone` · `foot` (one `Btn`: the big card is pressed whole; two
|
|
522
|
+
or more: only the buttons act) · children (the content); its size is
|
|
523
|
+
the feed's, never a prop. Knobs `--card-feed-gap`, `--feed-card-head-h`.
|
|
524
|
+
Reduced motion: nothing slides or fades. Stories `CardFeed · Default`,
|
|
525
|
+
`Home · Feed`.
|
|
494
526
|
- A page with **one feature and one CTA** (an install page, a single
|
|
495
527
|
decision) may be contained in one card: head, the pitch, foot.
|
|
496
528
|
- **A bottom sheet holds what the member acts on** — controls, or small
|
package/css/actions.css
CHANGED
|
@@ -362,6 +362,7 @@
|
|
|
362
362
|
something as wide as its card (a row, a summary line, a card, a step bar, a peeked sheet) fades under the finger (--press-dim)
|
|
363
363
|
instead, because a whole row shrinking reads as the card moving. A choice's fill fades in as it is picked. Disabled = no
|
|
364
364
|
pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
|
|
365
|
+
.feed-card--small (Lh 2026-10-09) = the small card of a CardFeed: the card itself is the button that grows it, so it has the ring too.
|
|
365
366
|
.card--go (Lh 2026-10-09) = a card pressed whole: `Card` renders it while its ONE control is its foot's button or a row card's end
|
|
366
367
|
button (src/cards/cardGo.ts). It is in the DIM pointer and :active lists only — the focus ring and the disabled look stay the
|
|
367
368
|
button's, the card itself is never focused or disabled. */
|
|
@@ -372,7 +373,7 @@
|
|
|
372
373
|
touch-action: manipulation;
|
|
373
374
|
transition: scale var(--motion-press) ease-out;
|
|
374
375
|
}
|
|
375
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge) {
|
|
376
|
+
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge) {
|
|
376
377
|
cursor: pointer;
|
|
377
378
|
touch-action: manipulation;
|
|
378
379
|
transition: opacity var(--motion-press) ease-out;
|
|
@@ -387,7 +388,7 @@
|
|
|
387
388
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
388
389
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
389
390
|
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
|
|
390
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
|
|
391
|
+
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
|
|
391
392
|
.sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
|
|
392
393
|
/* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
|
|
393
394
|
it darkens a touch instead (--press-shade) */
|
|
@@ -398,7 +399,7 @@
|
|
|
398
399
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
399
400
|
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):focus-visible,
|
|
400
401
|
:where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
401
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
402
|
+
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .feed-card--small, .toggle-field, .post-box__edge):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
402
403
|
/* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
|
|
403
404
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
404
405
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
package/css/base.css
CHANGED
|
@@ -70,8 +70,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
70
70
|
|
|
71
71
|
/* scrollbar hidden, scroll kept: the track scrolls by touch/drag, never by a visible bar —
|
|
72
72
|
.bleed (card.css) is the one exception, its bar is designed, not hidden */
|
|
73
|
-
:where(html, .sheet-stack--tap > .card, .step > .pages, .picker__rail, .avatar-pick) { scrollbar-width: none; }
|
|
74
|
-
:where(html, .sheet-stack--tap > .card, .step > .pages, .picker__rail, .avatar-pick)::-webkit-scrollbar { display: none; }
|
|
73
|
+
:where(html, .sheet-stack--tap > .card, .sheet-stack__base, .step > .pages, .picker__rail, .avatar-pick) { scrollbar-width: none; }
|
|
74
|
+
:where(html, .sheet-stack--tap > .card, .sheet-stack__base, .step > .pages, .picker__rail, .avatar-pick)::-webkit-scrollbar { display: none; }
|
|
75
75
|
|
|
76
76
|
/* icon from icons.svg sprite: <svg class="icon"><use href="icons.svg#bell"/></svg> */
|
|
77
77
|
.icon { width: var(--icon-size, var(--icon)); height: var(--icon-size, var(--icon)); flex: none; }
|
package/css/card.css
CHANGED
|
@@ -309,6 +309,36 @@
|
|
|
309
309
|
opacity: 0.85;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
/* ---- card feed: a queue of cards, two in sight — one big, one small (Lh 2026-10-09) ----
|
|
313
|
+
<div class="card-feed">
|
|
314
|
+
<article class="card feed-card"><div class="card__head feed-card__head">thumb · h2 · caption</div>
|
|
315
|
+
<div class="feed-card__more"><div class="feed-card__rest">content · <div class="card__foot">…</div></div></div></article>
|
|
316
|
+
<article class="card feed-card feed-card--small" role="button" tabindex="0">… <div class="feed-card__more" inert>…</div></article>
|
|
317
|
+
</div>
|
|
318
|
+
ONE markup in both sizes: the head always shows, the rest is a grid row that goes 1fr ↔ 0fr (never a height: Safari 16.4 has no
|
|
319
|
+
interpolate-size). The card's own row gap is 0 and the rest carries it as its first thing's margin, so a small card ends on its
|
|
320
|
+
head with nothing under it. The rest is clipped by clip-path while it shows: the clip runs out to the card's edges and a little
|
|
321
|
+
under the foot, so the button's focus ring is whole. Closed, it is `overflow: clip` as well — a clip-path alone leaves the
|
|
322
|
+
shut content in the scrollable overflow of whatever holds the feed — and hidden once the row has shut. */
|
|
323
|
+
.card-feed { display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: var(--card-feed-gap, var(--card-gap-rest)); }
|
|
324
|
+
.feed-card { row-gap: 0; }
|
|
325
|
+
.feed-card__head { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; min-height: var(--feed-card-head-h, var(--tap)); }
|
|
326
|
+
.feed-card__head:has(> .card__thumb) { grid-template-columns: auto minmax(0, 1fr); }
|
|
327
|
+
.feed-card__head:has(> .t-caption) > .card__thumb { grid-row: span 2; }
|
|
328
|
+
.feed-card__more { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr; }
|
|
329
|
+
.feed-card--small > .feed-card__more { grid-template-rows: 0fr; }
|
|
330
|
+
.feed-card__rest {
|
|
331
|
+
min-height: 0;
|
|
332
|
+
display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: var(--card-gap);
|
|
333
|
+
clip-path: inset(0 calc(-1 * var(--card-pad)) calc(-1 * var(--sp-2)));
|
|
334
|
+
}
|
|
335
|
+
.feed-card__rest > :first-child { margin-top: var(--card-gap); }
|
|
336
|
+
.feed-card--small .feed-card__rest { overflow: clip; visibility: hidden; }
|
|
337
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
338
|
+
.feed-card__more { transition: grid-template-rows var(--motion-sheet) ease; }
|
|
339
|
+
.feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
|
|
340
|
+
}
|
|
341
|
+
|
|
312
342
|
/* ---- inline stat block (revenue-style) ---- */
|
|
313
343
|
.stat { display: grid; gap: var(--stat-gap, var(--sp-1)); }
|
|
314
344
|
.stat--centre { text-align: center; justify-items: center; } /* the one number of a sheet or a stage, under a centred avatar */
|
package/css/stack.css
CHANGED
|
@@ -278,7 +278,8 @@
|
|
|
278
278
|
grid-area: 1 / 1;
|
|
279
279
|
align-self: start;
|
|
280
280
|
z-index: 0;
|
|
281
|
-
|
|
281
|
+
--_under: 0px;
|
|
282
|
+
height: calc(var(--_headroom, 0px) + var(--_under));
|
|
282
283
|
padding-inline: var(--screen-pad);
|
|
283
284
|
--_gap: var(--card-gap, var(--sp-5));
|
|
284
285
|
display: grid;
|
|
@@ -298,7 +299,26 @@
|
|
|
298
299
|
.sheet-stack__base > .screen__centre { padding-inline: 0; }
|
|
299
300
|
/* loose things rest against the peeks, so the room under the last one is the room between two of them (Lh 2026-10-09: a card's
|
|
300
301
|
bottom edge lay on the first sheet's top edge). A lone Page reads from the top and a lone Centre stands in the middle: neither rests there. */
|
|
301
|
-
.sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) {
|
|
302
|
+
.sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) {
|
|
303
|
+
padding-bottom: calc(var(--stack-base-gap, var(--_gap)) + var(--_under));
|
|
304
|
+
/* the first row takes the free room and its thing stands at that row's end: on the peeks while there is room, from the room's TOP when
|
|
305
|
+
there is none. `align-content: end` ran a long content out over the top, where no scroll reaches; the `safe` keyword is not relied
|
|
306
|
+
on, the floor is Safari 16.4. */
|
|
307
|
+
grid-template-rows: 1fr;
|
|
308
|
+
align-content: start;
|
|
309
|
+
align-items: end;
|
|
310
|
+
}
|
|
311
|
+
/* a room too short for what rests in it (Lh 2026-10-10; `is-long`, measured and rendered by SheetStack): nothing is squeezed, the upper
|
|
312
|
+
thing shows whole, the rest lies under the sheets and the room scrolls up to it. Only then: a room that holds its content keeps
|
|
313
|
+
touch-action: none, so a finger there pulls nothing. The box runs under the first sheet by its corner's radius, or a straight cut at
|
|
314
|
+
the sheet's top edge would show ground in the two round corners; the padding grows by the same, so the last thing still scrolls
|
|
315
|
+
clear of the heads by the gap, and the content box (cqh) is the one it was. */
|
|
316
|
+
.sheet-stack__base.is-long:not(:has(> .page:only-child, > .screen__centre:only-child)) {
|
|
317
|
+
--_under: var(--card-radius, var(--r-card));
|
|
318
|
+
overflow-y: auto;
|
|
319
|
+
overscroll-behavior: contain;
|
|
320
|
+
touch-action: pan-y;
|
|
321
|
+
}
|
|
302
322
|
/* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
|
|
303
323
|
line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
|
|
304
324
|
:is([data-palette], [data-finish="lit"]) .sheet-stack__base .card { box-shadow: none; }
|
|
@@ -333,7 +353,7 @@
|
|
|
333
353
|
.sheet-stack--flood { --_bar: calc(var(--top-h, var(--header-h)) + var(--_bar-gap)); }
|
|
334
354
|
.sheet-stack--flood > .sheet-stack__base {
|
|
335
355
|
margin-top: var(--_bar);
|
|
336
|
-
height: calc(var(--_headroom, 0px) - var(--_bar));
|
|
356
|
+
height: calc(var(--_headroom, 0px) - var(--_bar) + var(--_under));
|
|
337
357
|
}
|
|
338
358
|
.screen--fill:has(> .sheet-stack--flood > .stack-card.is-open, > .sheet-stack--flood > :not(.stack-card) > .stack-card.is-open) > :is(.appbar, .topbar):first-child {
|
|
339
359
|
visibility: hidden;
|
package/dist/cardds.css
CHANGED
|
@@ -540,8 +540,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
540
540
|
|
|
541
541
|
/* scrollbar hidden, scroll kept: the track scrolls by touch/drag, never by a visible bar —
|
|
542
542
|
.bleed (card.css) is the one exception, its bar is designed, not hidden */
|
|
543
|
-
:where(html, .sheet-stack--tap > .card, .step > .pages, .picker__rail, .avatar-pick) { scrollbar-width: none; }
|
|
544
|
-
:where(html, .sheet-stack--tap > .card, .step > .pages, .picker__rail, .avatar-pick)::-webkit-scrollbar { display: none; }
|
|
543
|
+
:where(html, .sheet-stack--tap > .card, .sheet-stack__base, .step > .pages, .picker__rail, .avatar-pick) { scrollbar-width: none; }
|
|
544
|
+
:where(html, .sheet-stack--tap > .card, .sheet-stack__base, .step > .pages, .picker__rail, .avatar-pick)::-webkit-scrollbar { display: none; }
|
|
545
545
|
|
|
546
546
|
/* icon from icons.svg sprite: <svg class="icon"><use href="icons.svg#bell"/></svg> */
|
|
547
547
|
.icon { width: var(--icon-size, var(--icon)); height: var(--icon-size, var(--icon)); flex: none; }
|
|
@@ -995,6 +995,36 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
995
995
|
opacity: 0.85;
|
|
996
996
|
}
|
|
997
997
|
|
|
998
|
+
/* ---- card feed: a queue of cards, two in sight — one big, one small (Lh 2026-10-09) ----
|
|
999
|
+
<div class="card-feed">
|
|
1000
|
+
<article class="card feed-card"><div class="card__head feed-card__head">thumb · h2 · caption</div>
|
|
1001
|
+
<div class="feed-card__more"><div class="feed-card__rest">content · <div class="card__foot">…</div></div></div></article>
|
|
1002
|
+
<article class="card feed-card feed-card--small" role="button" tabindex="0">… <div class="feed-card__more" inert>…</div></article>
|
|
1003
|
+
</div>
|
|
1004
|
+
ONE markup in both sizes: the head always shows, the rest is a grid row that goes 1fr ↔ 0fr (never a height: Safari 16.4 has no
|
|
1005
|
+
interpolate-size). The card's own row gap is 0 and the rest carries it as its first thing's margin, so a small card ends on its
|
|
1006
|
+
head with nothing under it. The rest is clipped by clip-path while it shows: the clip runs out to the card's edges and a little
|
|
1007
|
+
under the foot, so the button's focus ring is whole. Closed, it is `overflow: clip` as well — a clip-path alone leaves the
|
|
1008
|
+
shut content in the scrollable overflow of whatever holds the feed — and hidden once the row has shut. */
|
|
1009
|
+
.card-feed { display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: var(--card-feed-gap, var(--card-gap-rest)); }
|
|
1010
|
+
.feed-card { row-gap: 0; }
|
|
1011
|
+
.feed-card__head { display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; min-height: var(--feed-card-head-h, var(--tap)); }
|
|
1012
|
+
.feed-card__head:has(> .card__thumb) { grid-template-columns: auto minmax(0, 1fr); }
|
|
1013
|
+
.feed-card__head:has(> .t-caption) > .card__thumb { grid-row: span 2; }
|
|
1014
|
+
.feed-card__more { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr; }
|
|
1015
|
+
.feed-card--small > .feed-card__more { grid-template-rows: 0fr; }
|
|
1016
|
+
.feed-card__rest {
|
|
1017
|
+
min-height: 0;
|
|
1018
|
+
display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; gap: var(--card-gap);
|
|
1019
|
+
clip-path: inset(0 calc(-1 * var(--card-pad)) calc(-1 * var(--sp-2)));
|
|
1020
|
+
}
|
|
1021
|
+
.feed-card__rest > :first-child { margin-top: var(--card-gap); }
|
|
1022
|
+
.feed-card--small .feed-card__rest { overflow: clip; visibility: hidden; }
|
|
1023
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1024
|
+
.feed-card__more { transition: grid-template-rows var(--motion-sheet) ease; }
|
|
1025
|
+
.feed-card--small .feed-card__rest { transition: visibility 0s var(--motion-sheet); }
|
|
1026
|
+
}
|
|
1027
|
+
|
|
998
1028
|
/* ---- inline stat block (revenue-style) ---- */
|
|
999
1029
|
.stat { display: grid; gap: var(--stat-gap, var(--sp-1)); }
|
|
1000
1030
|
.stat--centre { text-align: center; justify-items: center; } /* the one number of a sheet or a stage, under a centred avatar */
|
|
@@ -1311,7 +1341,8 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1311
1341
|
grid-area: 1 / 1;
|
|
1312
1342
|
align-self: start;
|
|
1313
1343
|
z-index: 0;
|
|
1314
|
-
|
|
1344
|
+
--_under: 0px;
|
|
1345
|
+
height: calc(var(--_headroom, 0px) + var(--_under));
|
|
1315
1346
|
padding-inline: var(--screen-pad);
|
|
1316
1347
|
--_gap: var(--card-gap, var(--sp-5));
|
|
1317
1348
|
display: grid;
|
|
@@ -1331,7 +1362,26 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1331
1362
|
.sheet-stack__base > .screen__centre { padding-inline: 0; }
|
|
1332
1363
|
/* loose things rest against the peeks, so the room under the last one is the room between two of them (Lh 2026-10-09: a card's
|
|
1333
1364
|
bottom edge lay on the first sheet's top edge). A lone Page reads from the top and a lone Centre stands in the middle: neither rests there. */
|
|
1334
|
-
.sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) {
|
|
1365
|
+
.sheet-stack__base:not(:has(> .page:only-child, > .screen__centre:only-child)) {
|
|
1366
|
+
padding-bottom: calc(var(--stack-base-gap, var(--_gap)) + var(--_under));
|
|
1367
|
+
/* the first row takes the free room and its thing stands at that row's end: on the peeks while there is room, from the room's TOP when
|
|
1368
|
+
there is none. `align-content: end` ran a long content out over the top, where no scroll reaches; the `safe` keyword is not relied
|
|
1369
|
+
on, the floor is Safari 16.4. */
|
|
1370
|
+
grid-template-rows: 1fr;
|
|
1371
|
+
align-content: start;
|
|
1372
|
+
align-items: end;
|
|
1373
|
+
}
|
|
1374
|
+
/* a room too short for what rests in it (Lh 2026-10-10; `is-long`, measured and rendered by SheetStack): nothing is squeezed, the upper
|
|
1375
|
+
thing shows whole, the rest lies under the sheets and the room scrolls up to it. Only then: a room that holds its content keeps
|
|
1376
|
+
touch-action: none, so a finger there pulls nothing. The box runs under the first sheet by its corner's radius, or a straight cut at
|
|
1377
|
+
the sheet's top edge would show ground in the two round corners; the padding grows by the same, so the last thing still scrolls
|
|
1378
|
+
clear of the heads by the gap, and the content box (cqh) is the one it was. */
|
|
1379
|
+
.sheet-stack__base.is-long:not(:has(> .page:only-child, > .screen__centre:only-child)) {
|
|
1380
|
+
--_under: var(--card-radius, var(--r-card));
|
|
1381
|
+
overflow-y: auto;
|
|
1382
|
+
overscroll-behavior: contain;
|
|
1383
|
+
touch-action: pan-y;
|
|
1384
|
+
}
|
|
1335
1385
|
/* a card standing in this room is FLAT (Lh 2026-10-09): the room clips at its own edges, and a card's shadow cut there drew a straight
|
|
1336
1386
|
line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
|
|
1337
1387
|
:is([data-palette], [data-finish="lit"]) .sheet-stack__base .card { box-shadow: none; }
|
|
@@ -1366,7 +1416,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1366
1416
|
.sheet-stack--flood { --_bar: calc(var(--top-h, var(--header-h)) + var(--_bar-gap)); }
|
|
1367
1417
|
.sheet-stack--flood > .sheet-stack__base {
|
|
1368
1418
|
margin-top: var(--_bar);
|
|
1369
|
-
height: calc(var(--_headroom, 0px) - var(--_bar));
|
|
1419
|
+
height: calc(var(--_headroom, 0px) - var(--_bar) + var(--_under));
|
|
1370
1420
|
}
|
|
1371
1421
|
.screen--fill:has(> .sheet-stack--flood > .stack-card.is-open, > .sheet-stack--flood > :not(.stack-card) > .stack-card.is-open) > :is(.appbar, .topbar):first-child {
|
|
1372
1422
|
visibility: hidden;
|
|
@@ -2737,6 +2787,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2737
2787
|
something as wide as its card (a row, a summary line, a card, a step bar, a peeked sheet) fades under the finger (--press-dim)
|
|
2738
2788
|
instead, because a whole row shrinking reads as the card moving. A choice's fill fades in as it is picked. Disabled = no
|
|
2739
2789
|
pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
|
|
2790
|
+
.feed-card--small (Lh 2026-10-09) = the small card of a CardFeed: the card itself is the button that grows it, so it has the ring too.
|
|
2740
2791
|
.card--go (Lh 2026-10-09) = a card pressed whole: `Card` renders it while its ONE control is its foot's button or a row card's end
|
|
2741
2792
|
button (src/cards/cardGo.ts). It is in the DIM pointer and :active lists only — the focus ring and the disabled look stay the
|
|
2742
2793
|
button's, the card itself is never focused or disabled. */
|
|
@@ -2747,7 +2798,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2747
2798
|
touch-action: manipulation;
|
|
2748
2799
|
transition: scale var(--motion-press) ease-out;
|
|
2749
2800
|
}
|
|
2750
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge) {
|
|
2801
|
+
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge) {
|
|
2751
2802
|
cursor: pointer;
|
|
2752
2803
|
touch-action: manipulation;
|
|
2753
2804
|
transition: opacity var(--motion-press) ease-out;
|
|
@@ -2762,7 +2813,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2762
2813
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2763
2814
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2764
2815
|
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):active:not(:disabled, [aria-disabled="true"], :has(:disabled)) { scale: var(--press-scale); }
|
|
2765
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
|
|
2816
|
+
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .feed-card--small, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
|
|
2766
2817
|
.sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
|
|
2767
2818
|
/* SHADE — what has something BEHIND it (a peeked sheet over the pile, a postcard's face over its back, a post letter's sheet over the other) must not turn see-through:
|
|
2768
2819
|
it darkens a touch instead (--press-shade) */
|
|
@@ -2773,7 +2824,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2773
2824
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2774
2825
|
a.avatar, button.avatar, .menu__item, button.snackbar__body, .stamp--tap, button.stamp-cell, button.stamp-place, .post-letter__turn, .post-desk__slot, .post-desk__pick, .post-desk__reach):focus-visible,
|
|
2775
2826
|
:where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
2776
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
2827
|
+
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .feed-card--small, .toggle-field, .post-box__edge):not(label, .toggle-field):focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
2777
2828
|
/* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
|
|
2778
2829
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2779
2830
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode } from 'react';
|
|
2
|
+
import { type Tone } from './Card.js';
|
|
3
|
+
import { type IconName } from '../type/Icon.js';
|
|
4
|
+
/** why the feed asks for another big card: the member touched the small one · the two cards shown changed */
|
|
5
|
+
export type CardFeedReason = 'tap' | 'queue';
|
|
6
|
+
export interface CardFeedProps extends ComponentProps<'div'> {
|
|
7
|
+
/** CONTROLLED: the `id` of the card shown big — with `onBigChange`; or `defaultBig`. An id that is not one of the two cards shown reads as the upper one */
|
|
8
|
+
big?: string;
|
|
9
|
+
/** UNCONTROLLED: the card big on arrival (default: the upper one); the feed then keeps its own. Pair of `big` */
|
|
10
|
+
defaultBig?: string;
|
|
11
|
+
/** the feed asks for another big card: its `id`, and why — `tap` (the member touched the small card) · `queue` (the two cards shown changed: the upper one is asked for) */
|
|
12
|
+
onBigChange?: (id: string, reason: CardFeedReason) => void;
|
|
13
|
+
/** the WHOLE queue, in the app's order: one `FeedCard` each, as direct children. The feed draws the first two */
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* CardFeed — a QUEUE of cards the system hands the member one after another; at most two are on the screen: one BIG (heading, content,
|
|
18
|
+
* its button) and one SMALL (the same card cut down to its head). A touch anywhere on the small one makes it the big one and the other
|
|
19
|
+
* the small one; neither changes place. The queue is the app's: when it changes (a card left, a new one came) what is left slides up,
|
|
20
|
+
* the next comes in as the lower card, and the feed asks for the UPPER card to be big, whichever was big before. One card alone is big;
|
|
21
|
+
* no card, nothing is drawn. Which card is big is controlled like an input (`big` + `onBigChange`, or `defaultBig`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function CardFeed({ big: bigProp, defaultBig, onBigChange, className, children, ref, ...rest }: CardFeedProps): import("react").JSX.Element | null;
|
|
24
|
+
export interface FeedCardProps extends Omit<ComponentProps<'article'>, 'id' | 'role' | 'tabIndex'> {
|
|
25
|
+
/** which card of the queue this is — the app's own id for it, unique in the feed. `CardFeed big` / `onBigChange` speak in these */
|
|
26
|
+
id: string;
|
|
27
|
+
/** the card's heading — shown in both sizes; it names the small card */
|
|
28
|
+
heading: ReactNode;
|
|
29
|
+
/** the muted line under the heading, shown in both sizes */
|
|
30
|
+
caption?: ReactNode;
|
|
31
|
+
/** the card's thumb at the head's start: an icon by name, drawn in both sizes */
|
|
32
|
+
icon?: IconName;
|
|
33
|
+
/** the tone slot — see `Tone` */
|
|
34
|
+
tone?: Tone;
|
|
35
|
+
/** the card's button, or its buttons — shown only while the card is big. ONE `Btn`: the big card is pressed whole, a touch anywhere on it runs that button. Two or more (a question: yes · no): only the buttons act, a touch elsewhere on the card does nothing */
|
|
36
|
+
foot?: ReactNode;
|
|
37
|
+
/** the content — words, a picture, a Stamp. Shown only while the card is big */
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* FeedCard — ONE card of a `CardFeed`'s queue, the same markup in both sizes: its head (thumb · heading · caption) always shows, its
|
|
42
|
+
* content and its button are one part that opens while the card is big. The SIZE IS THE FEED'S, never a prop. Small, the card itself
|
|
43
|
+
* is the control (a button named by its heading; Enter and Space) and its closed part cannot be pressed, focused or read. Big, it is a
|
|
44
|
+
* plain card: with one button in its foot it is pressed whole; with two or more only the buttons act.
|
|
45
|
+
*/
|
|
46
|
+
export declare function FeedCard({ id, heading, caption, icon, tone, foot, className, children, onClick, onKeyDown, ref, ...rest }: FeedCardProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Children, createContext, isValidElement, useContext, useId, useLayoutEffect, useRef } from 'react';
|
|
3
|
+
import { cx } from '../cx.js';
|
|
4
|
+
import { reducedMotion, useControlled, warnOnce } from '../state.js';
|
|
5
|
+
import { Card } from './Card.js';
|
|
6
|
+
import { CardHead } from './CardHead.js';
|
|
7
|
+
import { CardFoot } from './CardFoot.js';
|
|
8
|
+
import { CardThumb } from './CardParts.js';
|
|
9
|
+
import { Icon } from '../type/Icon.js';
|
|
10
|
+
import { Text } from '../type/Text.js';
|
|
11
|
+
const FeedCtx = createContext(null);
|
|
12
|
+
/**
|
|
13
|
+
* CardFeed — a QUEUE of cards the system hands the member one after another; at most two are on the screen: one BIG (heading, content,
|
|
14
|
+
* its button) and one SMALL (the same card cut down to its head). A touch anywhere on the small one makes it the big one and the other
|
|
15
|
+
* the small one; neither changes place. The queue is the app's: when it changes (a card left, a new one came) what is left slides up,
|
|
16
|
+
* the next comes in as the lower card, and the feed asks for the UPPER card to be big, whichever was big before. One card alone is big;
|
|
17
|
+
* no card, nothing is drawn. Which card is big is controlled like an input (`big` + `onBigChange`, or `defaultBig`).
|
|
18
|
+
*/
|
|
19
|
+
export function CardFeed({ big: bigProp, defaultBig, onBigChange, className, children, ref, ...rest }) {
|
|
20
|
+
const queue = Children.toArray(children).filter((c) => isValidElement(c) && typeof c.props.id === 'string');
|
|
21
|
+
if (queue.length !== Children.count(children))
|
|
22
|
+
warnOnce('card-feed-child', 'cardds CardFeed: every child is a <FeedCard id="…"> — a child without a string `id` is not drawn');
|
|
23
|
+
const shown = queue.slice(0, 2);
|
|
24
|
+
const ids = shown.map((c) => c.props.id);
|
|
25
|
+
const [asked, request] = useControlled(bigProp, defaultBig, onBigChange);
|
|
26
|
+
const big = asked !== undefined && ids.includes(asked) ? asked : ids[0];
|
|
27
|
+
const own = useRef(null);
|
|
28
|
+
const tops = useRef(new Map());
|
|
29
|
+
const was = useRef(null);
|
|
30
|
+
const key = ids.join('\n');
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
const box = own.current;
|
|
33
|
+
const cards = box ? [...box.children] : [];
|
|
34
|
+
const changed = was.current !== null && was.current !== key;
|
|
35
|
+
was.current = key;
|
|
36
|
+
if (changed) {
|
|
37
|
+
if (ids.length)
|
|
38
|
+
request(ids[0], 'queue');
|
|
39
|
+
cards.forEach((el) => el.getAnimations().forEach((a) => { if (a.id === 'feed-move')
|
|
40
|
+
a.cancel(); })); // read the resting places
|
|
41
|
+
}
|
|
42
|
+
const at = new Map(cards.map((el) => [el, el.offsetTop]));
|
|
43
|
+
if (changed && box && !reducedMotion()) {
|
|
44
|
+
const ms = (parseFloat(getComputedStyle(box).getPropertyValue('--motion-sheet')) || 0.35) * 1000;
|
|
45
|
+
for (const el of cards) {
|
|
46
|
+
const from = tops.current.get(el);
|
|
47
|
+
const move = from === undefined
|
|
48
|
+
? el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: ms, easing: 'ease-out' }) // it came in: no place to come from
|
|
49
|
+
: from !== at.get(el) ? el.animate([{ translate: `0 ${from - (at.get(el) ?? 0)}px` }, { translate: '0 0' }], { duration: ms, easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)' }) : null;
|
|
50
|
+
if (move)
|
|
51
|
+
move.id = 'feed-move';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
tops.current = at;
|
|
55
|
+
});
|
|
56
|
+
const setRef = (node) => {
|
|
57
|
+
own.current = node;
|
|
58
|
+
if (typeof ref === 'function')
|
|
59
|
+
ref(node);
|
|
60
|
+
else if (ref)
|
|
61
|
+
ref.current = node;
|
|
62
|
+
};
|
|
63
|
+
if (!shown.length)
|
|
64
|
+
return null;
|
|
65
|
+
return (_jsx(FeedCtx.Provider, { value: { big, grow: (id) => request(id, 'tap') }, children: _jsx("div", { ref: setRef, className: cx('card-feed', className), ...rest, children: shown }) }));
|
|
66
|
+
}
|
|
67
|
+
const onSmallKey = (e) => {
|
|
68
|
+
if (e.target !== e.currentTarget || (e.key !== 'Enter' && e.key !== ' '))
|
|
69
|
+
return;
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.currentTarget.click();
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* FeedCard — ONE card of a `CardFeed`'s queue, the same markup in both sizes: its head (thumb · heading · caption) always shows, its
|
|
75
|
+
* content and its button are one part that opens while the card is big. The SIZE IS THE FEED'S, never a prop. Small, the card itself
|
|
76
|
+
* is the control (a button named by its heading; Enter and Space) and its closed part cannot be pressed, focused or read. Big, it is a
|
|
77
|
+
* plain card: with one button in its foot it is pressed whole; with two or more only the buttons act.
|
|
78
|
+
*/
|
|
79
|
+
export function FeedCard({ id, heading, caption, icon, tone, foot, className, children, onClick, onKeyDown, ref, ...rest }) {
|
|
80
|
+
const feed = useContext(FeedCtx);
|
|
81
|
+
if (!feed)
|
|
82
|
+
warnOnce('feed-card-alone', 'cardds FeedCard: outside a <CardFeed> it has nobody to say its size — it is drawn big');
|
|
83
|
+
const small = feed != null && feed.big !== id;
|
|
84
|
+
const name = useId();
|
|
85
|
+
const own = useRef(null);
|
|
86
|
+
const byKey = useRef(false);
|
|
87
|
+
useLayoutEffect(() => {
|
|
88
|
+
// grown from the keyboard: the card stopped being a control under the cursor, so the cursor goes on to its button
|
|
89
|
+
if (!small && byKey.current)
|
|
90
|
+
own.current?.querySelector('.card__foot :is(button, a[href])')?.focus({ preventScroll: true });
|
|
91
|
+
byKey.current = false;
|
|
92
|
+
}, [small]);
|
|
93
|
+
const setRef = (node) => {
|
|
94
|
+
own.current = node;
|
|
95
|
+
if (typeof ref === 'function')
|
|
96
|
+
ref(node);
|
|
97
|
+
else if (ref)
|
|
98
|
+
ref.current = node;
|
|
99
|
+
};
|
|
100
|
+
return (_jsxs(Card, { ref: setRef, tone: tone, className: cx('feed-card', small && 'feed-card--small', className), ...rest, ...(small ? { role: 'button', tabIndex: 0, 'aria-expanded': false, 'aria-labelledby': name } : {}), onClick: (e) => { onClick?.(e); if (small && !e.defaultPrevented) {
|
|
101
|
+
byKey.current = e.detail === 0;
|
|
102
|
+
feed.grow(id);
|
|
103
|
+
} }, onKeyDown: (e) => { onKeyDown?.(e); if (small && !e.defaultPrevented)
|
|
104
|
+
onSmallKey(e); }, children: [_jsxs(CardHead, { className: "feed-card__head", children: [icon && _jsx(CardThumb, { children: _jsx(Icon, { name: icon, size: "sm" }) }), _jsx(Text, { level: "h2", id: name, children: heading }), caption != null && _jsx(Text, { level: "caption", muted: true, children: caption })] }), _jsx("div", { className: "feed-card__more", inert: small, children: _jsxs("div", { className: "feed-card__rest", children: [children, foot != null && _jsx(CardFoot, { children: foot })] }) })] }));
|
|
105
|
+
}
|
package/dist/cards/cardGo.js
CHANGED
|
@@ -8,20 +8,22 @@ const CONTROLS = [
|
|
|
8
8
|
'[tabindex]:not([tabindex="-1"])', '[contenteditable]',
|
|
9
9
|
'.stamp-on', // dragged
|
|
10
10
|
].join(', ');
|
|
11
|
-
/** a card that is pressed whole already (a link, a label, a button
|
|
12
|
-
* acts, a modal asks, a touch on a stack's sheet opens it (`stack-card` is what `Card`
|
|
13
|
-
|
|
11
|
+
/** a card that is pressed whole already (a link, a label, a button, a card that is itself the control), or where a touch means
|
|
12
|
+
* something else: a sheet is where the member acts, a modal asks, a touch on a stack's sheet opens it (`stack-card` is what `Card`
|
|
13
|
+
* renders from its `stack` prop) */
|
|
14
|
+
const NOT_A_GO_CARD = 'a, label, button, [role="button"], .sheet, .modal, .stack-card';
|
|
14
15
|
/** the control this card presses when it is touched off that control, or null: the card is only read */
|
|
15
16
|
export function goControl(card) {
|
|
16
17
|
if (card.matches(NOT_A_GO_CARD))
|
|
17
18
|
return null;
|
|
18
|
-
const controls = card.querySelectorAll(CONTROLS);
|
|
19
|
+
const controls = [...card.querySelectorAll(CONTROLS)].filter((c) => !c.closest('[inert]')); // an inert part cannot be pressed, focused or read: nothing in it is a control
|
|
19
20
|
if (controls.length !== 1)
|
|
20
21
|
return null;
|
|
21
22
|
const control = controls[0];
|
|
22
23
|
if (control.matches(':disabled, [aria-disabled="true"]'))
|
|
23
24
|
return null;
|
|
24
|
-
const
|
|
25
|
+
const foot = control.parentElement;
|
|
26
|
+
const inFoot = foot?.matches('.card__foot') && foot.parentElement?.closest('.card') === card; // the card's OWN foot, also when a part of the card holds it (a FeedCard's region)
|
|
25
27
|
const atRowEnd = card.matches('.card--row') && card.lastElementChild === control;
|
|
26
28
|
return inFoot || atRowEnd ? control : null;
|
|
27
29
|
}
|