@usableapp/cardds 0.7.76 → 0.7.78
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 +22 -0
- package/README.md +32 -2
- package/css/actions.css +4 -3
- package/css/card.css +39 -0
- package/css/stamp.css +31 -8
- package/dist/cardds.css +74 -11
- 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/stamp/Stamp.d.ts +5 -2
- package/dist/stamp/Stamp.js +6 -3
- package/dist/stamp/StampBook.js +1 -1
- package/dist/templates/Home.d.ts +5 -1
- package/dist/templates/Home.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
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.78 — 2026-10-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`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`).
|
|
9
|
+
- **`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.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **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.
|
|
13
|
+
|
|
14
|
+
## 0.7.77 — 2026-10-10
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **A neighbour as one card: `Card heading caption end={<Stamp/>} foot`** (timebank, 2026-10-09) — the words at the left, a large stamp at the right, the one button in the foot. Before → after: a `CardHead` with a hand-written `<div>` of two `Text`s and a `Stamp` with `--stamp-max-w` in a `style` (the stamp had no width in a head: it takes the head's whole width up to its cap) → `<Card heading={name} caption={greeting} end={<Stamp bare {...art} />} foot={<Btn primary block>…</Btn>} />`, no markup and no `style`. The stamp is as tall as the heading and the caption together. New knob `--card-head-stamp-w` (the stamp's width in a head; default two taps). The card's only control is the foot's button, so a touch anywhere presses it; the stamp is a picture, not a button. The same goes for a `CardHead` of your own with a `Stamp` as a direct child. Story `StampScreens · Neighbours`.
|
|
18
|
+
- **`StampPlace` in a `Centre`** — a neighbour with no stamp yet: `<Centre><StampPlace>…</StampPlace><Text>…</Text><Link>…</Link></Centre>`. Before → after: the frame was centred but the name and the link stood at the left, and its size was the plain cap → it takes the size a `Stamp` takes there (`--stamp-centre-max-w`) and the words and the link centre under it, as under a stamp. Story `StampScreens · NoStampYet`.
|
|
19
|
+
- **`--stamp-place-dash`, `--stamp-place-gap`** — the dashes of a `StampPlace`'s edge: how long each dash is, and the gap between (defaults `--sp-2` and `--sp-3`).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **The dashes of a `StampPlace` stand further apart** (Lh 2026-10-09) — the edge was a CSS dashed border (the browser's dash and gap: 6px dash and 4px gap in Chrome, 6px and 6px in Safari, at 2px wide). Before → after: the same edge drawn as a stroke with a gap of 12px and a dash of 8px, still round at `--stamp-place-radius`, the same colours (`--stamp-place-border`, `--stamp-place-faint-border`, a card's muted ink), the same 4:5 size, the words and icon inside where they were. A project that drew the edge itself with `border-style` on `.stamp-place` sets `--stamp-place-border` or the two new knobs instead: its border is now clear. A `StampPlace` has an `<svg aria-hidden>` as its first child.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **`StampBook places={9}` drew eight places when a child was `false` or `null`** (timebank, 2026-10-09) — `{owned && <StampCell/>}` that was `false` still counted as a place, so one empty place too few was drawn. Before → after: nine boxes (the cells that render, the rest empty). Story `StampBook · Conditional`.
|
|
26
|
+
|
|
5
27
|
## 0.7.76 — 2026-10-09
|
|
6
28
|
|
|
7
29
|
### Changed
|
package/README.md
CHANGED
|
@@ -225,6 +225,11 @@ the card is the material, the sheet is what it is in the stack.
|
|
|
225
225
|
under the sheets (hidden while one is open), nothing resizes. At the screen's
|
|
226
226
|
top the open sheet's top corners are square; folded, they are round again. It needs
|
|
227
227
|
`--closed` in a `Screen fill`.
|
|
228
|
+
- **`Home` takes a sheet's tab from the app.** Each of `Home sheets` keeps its
|
|
229
|
+
own tab (`defaultTab`, else the first) unless the app gives it `tab` — then
|
|
230
|
+
that tab shows, and a tapped tab asks `onTabChange(tab)`. Something outside
|
|
231
|
+
the stack (a card in the base) brings a sheet up on a named tab by setting
|
|
232
|
+
that sheet's `tab` and the Home's `open` together. Story `Home · Feed`.
|
|
228
233
|
- **Five sheets max.** Slots are declared per `:nth-child` in `stack.css`; a
|
|
229
234
|
sixth would render at slot 0, on top of the first.
|
|
230
235
|
- **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
|
|
@@ -491,6 +496,25 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
491
496
|
the one button in the head or in the content, a disabled button, a sheet, a
|
|
492
497
|
stack's sheet, a modal. A card that is itself a link or a label was pressed
|
|
493
498
|
whole already. Story `Card · PressedWhole`.
|
|
499
|
+
- **A feed is a queue of cards, two in sight** (Lh 2026-10-09). `CardFeed`
|
|
500
|
+
takes the WHOLE queue as `FeedCard` children, in the app's order, and shows
|
|
501
|
+
the first two: one BIG (heading, content, its button) and one SMALL (the
|
|
502
|
+
same card cut down to its head — no content, no button). A touch anywhere
|
|
503
|
+
on the small one makes it the big one and the other the small one; neither
|
|
504
|
+
changes place. The small card is itself the button (named by its heading,
|
|
505
|
+
Enter and Space) and what it does not show cannot be pressed, focused or
|
|
506
|
+
read; the big card is a plain card, pressed whole by the rule above. When
|
|
507
|
+
the two cards shown change — a card left the queue, a new one came — what
|
|
508
|
+
is left slides up, the next comes in as the lower card, and the UPPER card
|
|
509
|
+
is big, whichever was big before. One card alone is big; no card, nothing
|
|
510
|
+
is drawn. The queue is the app's: a card leaves when the app stops
|
|
511
|
+
rendering it. Which card is big is controlled like an input: `big` (a
|
|
512
|
+
card's `id`) + `onBigChange(id, 'tap' | 'queue')`, or `defaultBig`. A
|
|
513
|
+
`FeedCard` is `id` · `heading` · `caption` · `icon` (its thumb, in both
|
|
514
|
+
sizes) · `tone` · `foot` (one `Btn`) · children (the content); its size is
|
|
515
|
+
the feed's, never a prop. Knobs `--card-feed-gap`, `--feed-card-head-h`.
|
|
516
|
+
Reduced motion: nothing slides or fades. Stories `CardFeed · Default`,
|
|
517
|
+
`Home · Feed`.
|
|
494
518
|
- A page with **one feature and one CTA** (an install page, a single
|
|
495
519
|
decision) may be contained in one card: head, the pitch, foot.
|
|
496
520
|
- **A bottom sheet holds what the member acts on** — controls, or small
|
|
@@ -543,10 +567,16 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
|
|
|
543
567
|
`words` the small line above it. `tilt` turns it, `onClick` makes the whole stamp a button. It fills its column up to
|
|
544
568
|
`--stamp-max-w`, and every inner length follows its width, so one stamp reads in a tray and in a sheet.
|
|
545
569
|
- **`StampPlace`** — an empty dashed place the size of a stamp: a word inside, `onClick` for "make yours", `faint` for a
|
|
546
|
-
gap nobody is asked to fill.
|
|
570
|
+
gap nobody is asked to fill. The edge is a drawn stroke, so its dashes are yours: `--stamp-place-dash` and
|
|
571
|
+
`--stamp-place-gap` (defaults 8px and 12px), its corner is `--stamp-place-radius`.
|
|
547
572
|
- **`StampBook`** + **`StampCell`** — the album page: cells three across on an album ground, a `label` under each stamp,
|
|
548
573
|
the free places drawn up to `places`. A cell with `onClick` is a button; `on` marks it pressed. `strip` is one line
|
|
549
|
-
of small stamps that scrolls sideways, the tray a member picks from.
|
|
574
|
+
of small stamps that scrolls sideways, the tray a member picks from. A `false` or `null` child holds no place.
|
|
575
|
+
- A stamp stands for a person in three more places. In a `Centre` — a `Stamp` or a `StampPlace` — it takes the screen's
|
|
576
|
+
size and the words and link under it centre. In a card's head — `<Card heading={name} caption={greeting}
|
|
577
|
+
end={<Stamp bare …/>} foot={<Btn primary block>…</Btn>} />` — the words stand at the left and the stamp at the right,
|
|
578
|
+
as tall as both lines, `--card-head-stamp-w` wide (default two taps); the card's one button presses it whole. In an
|
|
579
|
+
`AvatarStack` it is a head (`more="+9"` for the count).
|
|
550
580
|
- **`StampOn`** — a stamp stuck on a surface. `at` is the stamp's middle as a share of the surface (`{ x, y }`, 0 to 1),
|
|
551
581
|
`turn` its angle in degrees. With `onAtChange` the member drags it (the arrow keys move it too) and it asks with the
|
|
552
582
|
new place when the finger lets go; without it the stamp is still. The surface is the nearest positioned box:
|
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/card.css
CHANGED
|
@@ -84,6 +84,15 @@
|
|
|
84
84
|
height, only the row gap. */
|
|
85
85
|
.card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
|
|
86
86
|
|
|
87
|
+
/* a Stamp in a card's head (`Card end={<Stamp …/>}`): the words stand at the left, the stamp at the right and AS TALL AS BOTH LINES — the heading and the
|
|
88
|
+
caption pair up beside it, not the stamp beside the heading alone with the caption left under it. A stamp has no size of its own in a flex
|
|
89
|
+
row (a width of 100% of the head, up to its cap), so it takes --card-head-stamp-w here, as a stamp in an AvatarStack takes a width of its own.
|
|
90
|
+
<div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
|
|
91
|
+
.card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
|
|
92
|
+
.card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
|
|
93
|
+
.card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
|
|
94
|
+
.card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; width: var(--card-head-stamp-w, calc(var(--tap) * 2)); margin-inline: 0; }
|
|
95
|
+
|
|
87
96
|
/* ---- bleed: a scrolling track that runs to the container's edges ----
|
|
88
97
|
For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
|
|
89
98
|
a tile grid: the TRACK reaches the edges so it reads as scrollable, while
|
|
@@ -300,6 +309,36 @@
|
|
|
300
309
|
opacity: 0.85;
|
|
301
310
|
}
|
|
302
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
|
+
|
|
303
342
|
/* ---- inline stat block (revenue-style) ---- */
|
|
304
343
|
.stat { display: grid; gap: var(--stat-gap, var(--sp-1)); }
|
|
305
344
|
.stat--centre { text-align: center; justify-items: center; } /* the one number of a sheet or a stage, under a centred avatar */
|
package/css/stamp.css
CHANGED
|
@@ -82,29 +82,52 @@
|
|
|
82
82
|
/* --tap: the whole stamp is the button (`onClick`); the press and the ring are the PRESS list's (actions.css) */
|
|
83
83
|
.stamp--tap { appearance: none; border: 0; padding: 0; background: none; font: inherit; text-align: start; -webkit-tap-highlight-color: transparent; }
|
|
84
84
|
|
|
85
|
-
/* a stamp handed over in a Centre: sized by the SCREEN (a little over half its width inside the inset), its greeting centred under it */
|
|
86
|
-
.screen__centre:has(> .stamp) { justify-items: center; text-align: center; }
|
|
87
|
-
.screen__centre > .stamp { max-width: var(--stamp-centre-max-w, 56cqw); }
|
|
85
|
+
/* a stamp — or the empty place for one — handed over in a Centre: sized by the SCREEN (a little over half its width inside the inset), its greeting centred under it */
|
|
86
|
+
.screen__centre:has(> :is(.stamp, .stamp-place)) { justify-items: center; text-align: center; }
|
|
87
|
+
.screen__centre > :is(.stamp, .stamp-place) { max-width: var(--stamp-centre-max-w, 56cqw); }
|
|
88
88
|
/* in the room above a closed stack's heads the stamp is held by that room's HEIGHT too: its height at most 45% of it, so two lines of words under it
|
|
89
89
|
still fit on a short phone (320×568 left 253px for a group 314 tall — reported by timebank, 2026-10-07). cqh reads the base, a size container. */
|
|
90
|
-
.sheet-stack__base > .screen__centre > .stamp { max-width: min(var(--stamp-centre-max-w, 56cqw), 36cqh); }
|
|
90
|
+
.sheet-stack__base > .screen__centre > :is(.stamp, .stamp-place) { max-width: min(var(--stamp-centre-max-w, 56cqw), 36cqh); }
|
|
91
91
|
|
|
92
92
|
/* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
|
|
93
|
-
<span class="stamp-place">สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint"
|
|
93
|
+
<span class="stamp-place"><svg class="stamp-place__edge" aria-hidden="true"><rect/></svg>สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint">…</span>
|
|
94
|
+
The dashed edge is a <rect> of its own, not a border: a dashed border's dash and gap are the browser's to choose, a stroke's are two
|
|
95
|
+
knobs (--stamp-place-dash, --stamp-place-gap) and its corners still follow --stamp-place-radius. The element keeps a clear border
|
|
96
|
+
of the same width so what is inside sits where it did; the svg lies on that border, over it, and takes no part in the layout. */
|
|
94
97
|
.stamp-place {
|
|
98
|
+
position: relative;
|
|
95
99
|
display: grid;
|
|
96
100
|
place-content: center;
|
|
97
101
|
justify-items: center;
|
|
98
102
|
padding: var(--sp-1);
|
|
99
|
-
border: var(--rule-w)
|
|
103
|
+
border: var(--rule-w) solid transparent;
|
|
100
104
|
border-radius: var(--stamp-place-radius, var(--sp-2));
|
|
101
105
|
background: none;
|
|
102
106
|
color: var(--stamp-place-ink, var(--on-surface-muted));
|
|
103
107
|
font: var(--stamp-place-font, var(--type-caption));
|
|
104
108
|
text-align: center;
|
|
109
|
+
--_edge: var(--stamp-place-border, var(--on-surface-muted));
|
|
110
|
+
}
|
|
111
|
+
.stamp-place__edge {
|
|
112
|
+
position: absolute;
|
|
113
|
+
inset: calc(var(--rule-w) / -2) auto auto calc(var(--rule-w) / -2);
|
|
114
|
+
width: calc(100% + var(--rule-w));
|
|
115
|
+
height: calc(100% + var(--rule-w));
|
|
116
|
+
max-width: none; /* base.css caps every svg at its box */
|
|
117
|
+
overflow: visible; /* the stroke is centred on the rect's own edge: half of it falls outside the svg */
|
|
118
|
+
pointer-events: none;
|
|
119
|
+
}
|
|
120
|
+
.stamp-place__edge > rect {
|
|
121
|
+
width: 100%;
|
|
122
|
+
height: 100%;
|
|
123
|
+
rx: calc(var(--stamp-place-radius, var(--sp-2)) - var(--rule-w) / 2); /* the corner of the stroke's middle, so its outer edge is the radius */
|
|
124
|
+
fill: none;
|
|
125
|
+
stroke: var(--_edge);
|
|
126
|
+
stroke-width: var(--rule-w);
|
|
127
|
+
stroke-dasharray: var(--stamp-place-dash, var(--sp-2)) var(--stamp-place-gap, var(--sp-3));
|
|
105
128
|
}
|
|
106
|
-
.card .stamp-place, .post-paper .stamp-place {
|
|
107
|
-
.stamp-place--faint {
|
|
129
|
+
.card .stamp-place, .post-paper .stamp-place { --_edge: var(--stamp-place-border, var(--card-muted)); color: var(--stamp-place-ink, var(--card-muted)); }
|
|
130
|
+
.stamp-place--faint { --_edge: var(--stamp-place-faint-border, var(--hairline)); }
|
|
108
131
|
|
|
109
132
|
/* ---- stamp book: the album page — three stamps across on an album ground, who each is from under it ----
|
|
110
133
|
<div class="stamp-book">
|
package/dist/cardds.css
CHANGED
|
@@ -770,6 +770,15 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
770
770
|
height, only the row gap. */
|
|
771
771
|
.card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
|
|
772
772
|
|
|
773
|
+
/* a Stamp in a card's head (`Card end={<Stamp …/>}`): the words stand at the left, the stamp at the right and AS TALL AS BOTH LINES — the heading and the
|
|
774
|
+
caption pair up beside it, not the stamp beside the heading alone with the caption left under it. A stamp has no size of its own in a flex
|
|
775
|
+
row (a width of 100% of the head, up to its cap), so it takes --card-head-stamp-w here, as a stamp in an AvatarStack takes a width of its own.
|
|
776
|
+
<div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><span class="stamp">…</span></div> */
|
|
777
|
+
.card__head:has(> .stamp) { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
|
|
778
|
+
.card__head:has(> .stamp) > :is(h1, h2, h3) { grid-column: 1; grid-row: 1; align-self: end; }
|
|
779
|
+
.card__head:has(> .stamp) > :is(.t-caption, .t-body) { grid-column: 1; grid-row: 2; align-self: start; }
|
|
780
|
+
.card__head > .stamp { grid-column: 2; grid-row: 1 / span 2; width: var(--card-head-stamp-w, calc(var(--tap) * 2)); margin-inline: 0; }
|
|
781
|
+
|
|
773
782
|
/* ---- bleed: a scrolling track that runs to the container's edges ----
|
|
774
783
|
For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
|
|
775
784
|
a tile grid: the TRACK reaches the edges so it reads as scrollable, while
|
|
@@ -986,6 +995,36 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
986
995
|
opacity: 0.85;
|
|
987
996
|
}
|
|
988
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
|
+
|
|
989
1028
|
/* ---- inline stat block (revenue-style) ---- */
|
|
990
1029
|
.stat { display: grid; gap: var(--stat-gap, var(--sp-1)); }
|
|
991
1030
|
.stat--centre { text-align: center; justify-items: center; } /* the one number of a sheet or a stage, under a centred avatar */
|
|
@@ -2728,6 +2767,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2728
2767
|
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)
|
|
2729
2768
|
instead, because a whole row shrinking reads as the card moving. A choice's fill fades in as it is picked. Disabled = no
|
|
2730
2769
|
pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
|
|
2770
|
+
.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.
|
|
2731
2771
|
.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
|
|
2732
2772
|
button (src/cards/cardGo.ts). It is in the DIM pointer and :active lists only — the focus ring and the disabled look stay the
|
|
2733
2773
|
button's, the card itself is never focused or disabled. */
|
|
@@ -2738,7 +2778,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2738
2778
|
touch-action: manipulation;
|
|
2739
2779
|
transition: scale var(--motion-press) ease-out;
|
|
2740
2780
|
}
|
|
2741
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge) {
|
|
2781
|
+
: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) {
|
|
2742
2782
|
cursor: pointer;
|
|
2743
2783
|
touch-action: manipulation;
|
|
2744
2784
|
transition: opacity var(--motion-press) ease-out;
|
|
@@ -2753,7 +2793,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2753
2793
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2754
2794
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2755
2795
|
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); }
|
|
2756
|
-
: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); }
|
|
2796
|
+
: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); }
|
|
2757
2797
|
.sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
|
|
2758
2798
|
/* 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:
|
|
2759
2799
|
it darkens a touch instead (--press-shade) */
|
|
@@ -2764,7 +2804,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2764
2804
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2765
2805
|
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,
|
|
2766
2806
|
:where(.chip--pick, .tile--pick, label.btn, label.row, .toggle-field):has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
|
|
2767
|
-
: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); }
|
|
2807
|
+
: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); }
|
|
2768
2808
|
/* disabled: no pointer anywhere; a control fades (a link and a row keep their own muted look) */
|
|
2769
2809
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2770
2810
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
@@ -3996,29 +4036,52 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
3996
4036
|
/* --tap: the whole stamp is the button (`onClick`); the press and the ring are the PRESS list's (actions.css) */
|
|
3997
4037
|
.stamp--tap { appearance: none; border: 0; padding: 0; background: none; font: inherit; text-align: start; -webkit-tap-highlight-color: transparent; }
|
|
3998
4038
|
|
|
3999
|
-
/* a stamp handed over in a Centre: sized by the SCREEN (a little over half its width inside the inset), its greeting centred under it */
|
|
4000
|
-
.screen__centre:has(> .stamp) { justify-items: center; text-align: center; }
|
|
4001
|
-
.screen__centre > .stamp { max-width: var(--stamp-centre-max-w, 56cqw); }
|
|
4039
|
+
/* a stamp — or the empty place for one — handed over in a Centre: sized by the SCREEN (a little over half its width inside the inset), its greeting centred under it */
|
|
4040
|
+
.screen__centre:has(> :is(.stamp, .stamp-place)) { justify-items: center; text-align: center; }
|
|
4041
|
+
.screen__centre > :is(.stamp, .stamp-place) { max-width: var(--stamp-centre-max-w, 56cqw); }
|
|
4002
4042
|
/* in the room above a closed stack's heads the stamp is held by that room's HEIGHT too: its height at most 45% of it, so two lines of words under it
|
|
4003
4043
|
still fit on a short phone (320×568 left 253px for a group 314 tall — reported by timebank, 2026-10-07). cqh reads the base, a size container. */
|
|
4004
|
-
.sheet-stack__base > .screen__centre > .stamp { max-width: min(var(--stamp-centre-max-w, 56cqw), 36cqh); }
|
|
4044
|
+
.sheet-stack__base > .screen__centre > :is(.stamp, .stamp-place) { max-width: min(var(--stamp-centre-max-w, 56cqw), 36cqh); }
|
|
4005
4045
|
|
|
4006
4046
|
/* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
|
|
4007
|
-
<span class="stamp-place">สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint"
|
|
4047
|
+
<span class="stamp-place"><svg class="stamp-place__edge" aria-hidden="true"><rect/></svg>สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint">…</span>
|
|
4048
|
+
The dashed edge is a <rect> of its own, not a border: a dashed border's dash and gap are the browser's to choose, a stroke's are two
|
|
4049
|
+
knobs (--stamp-place-dash, --stamp-place-gap) and its corners still follow --stamp-place-radius. The element keeps a clear border
|
|
4050
|
+
of the same width so what is inside sits where it did; the svg lies on that border, over it, and takes no part in the layout. */
|
|
4008
4051
|
.stamp-place {
|
|
4052
|
+
position: relative;
|
|
4009
4053
|
display: grid;
|
|
4010
4054
|
place-content: center;
|
|
4011
4055
|
justify-items: center;
|
|
4012
4056
|
padding: var(--sp-1);
|
|
4013
|
-
border: var(--rule-w)
|
|
4057
|
+
border: var(--rule-w) solid transparent;
|
|
4014
4058
|
border-radius: var(--stamp-place-radius, var(--sp-2));
|
|
4015
4059
|
background: none;
|
|
4016
4060
|
color: var(--stamp-place-ink, var(--on-surface-muted));
|
|
4017
4061
|
font: var(--stamp-place-font, var(--type-caption));
|
|
4018
4062
|
text-align: center;
|
|
4063
|
+
--_edge: var(--stamp-place-border, var(--on-surface-muted));
|
|
4064
|
+
}
|
|
4065
|
+
.stamp-place__edge {
|
|
4066
|
+
position: absolute;
|
|
4067
|
+
inset: calc(var(--rule-w) / -2) auto auto calc(var(--rule-w) / -2);
|
|
4068
|
+
width: calc(100% + var(--rule-w));
|
|
4069
|
+
height: calc(100% + var(--rule-w));
|
|
4070
|
+
max-width: none; /* base.css caps every svg at its box */
|
|
4071
|
+
overflow: visible; /* the stroke is centred on the rect's own edge: half of it falls outside the svg */
|
|
4072
|
+
pointer-events: none;
|
|
4019
4073
|
}
|
|
4020
|
-
.
|
|
4021
|
-
|
|
4074
|
+
.stamp-place__edge > rect {
|
|
4075
|
+
width: 100%;
|
|
4076
|
+
height: 100%;
|
|
4077
|
+
rx: calc(var(--stamp-place-radius, var(--sp-2)) - var(--rule-w) / 2); /* the corner of the stroke's middle, so its outer edge is the radius */
|
|
4078
|
+
fill: none;
|
|
4079
|
+
stroke: var(--_edge);
|
|
4080
|
+
stroke-width: var(--rule-w);
|
|
4081
|
+
stroke-dasharray: var(--stamp-place-dash, var(--sp-2)) var(--stamp-place-gap, var(--sp-3));
|
|
4082
|
+
}
|
|
4083
|
+
.card .stamp-place, .post-paper .stamp-place { --_edge: var(--stamp-place-border, var(--card-muted)); color: var(--stamp-place-ink, var(--card-muted)); }
|
|
4084
|
+
.stamp-place--faint { --_edge: var(--stamp-place-faint-border, var(--hairline)); }
|
|
4022
4085
|
|
|
4023
4086
|
/* ---- stamp book: the album page — three stamps across on an album ground, who each is from under it ----
|
|
4024
4087
|
<div class="stamp-book">
|
|
@@ -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 (one `Btn`) — shown only while the card is big, and then the card is pressed whole: a touch anywhere on it runs this button */
|
|
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: its one control is its foot's button, so it is pressed whole.
|
|
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: its one control is its foot's button, so it is pressed whole.
|
|
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
|
}
|