@usableapp/cardds 0.7.75 → 0.7.77
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 +20 -0
- package/README.md +23 -2
- package/css/actions.css +6 -3
- package/css/card.css +9 -0
- package/css/stack.css +8 -1
- package/css/stamp.css +31 -8
- package/dist/cardds.css +54 -12
- package/dist/cards/Card.d.ts +4 -2
- package/dist/cards/Card.js +26 -4
- package/dist/cards/cardGo.d.ts +2 -0
- package/dist/cards/cardGo.js +27 -0
- package/dist/catalog.json +1 -1
- package/dist/stamp/Stamp.d.ts +5 -2
- package/dist/stamp/Stamp.js +6 -3
- package/dist/stamp/StampBook.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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.77 — 2026-10-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **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`.
|
|
9
|
+
- **`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`.
|
|
10
|
+
- **`--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`).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **`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`.
|
|
17
|
+
|
|
18
|
+
## 0.7.76 — 2026-10-09
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **A card with one button is pressed whole** (Lh 2026-10-09) — a `Card` whose ONLY control is its foot's button (`foot` / `CardFoot`), or the end button of a `Card row`, now answers a touch anywhere on it: the button's own action runs once (its `onClick`, its `href`, its `popoverTarget`), and the whole card dims while the finger is down. Before → after: only the button could be pressed, the rest of the card did nothing → the heading, the words and the empty room press the button. Nothing to write in an app, and no prop turns it off. A card `onClick` of your own still runs once per press. Unchanged: a card with two or more controls (a second button, a field, a choice, a button in a card inside it), a card whose one button is in its head or in its content, a disabled button, a `Sheet`, a sheet of a `SheetStack`, a `Modal`, a card that is a link or a label. If a screen relied on a touch beside the lone foot button doing nothing, that touch now presses it. Story `Card · PressedWhole`.
|
|
22
|
+
|
|
23
|
+
- **Cards in a closed stack's base: room under the last one, and no shadow** (Lh 2026-10-09, from timebank's home) — loose things in `SheetStack base` / `Home base` rest against the heads. Before → after: the last one's bottom edge lay ON the first sheet's top edge → it keeps the same room under it as between two of them (20px at the defaults). A base that is one `Page` (reads from the top) or one `Centre` (stands in the middle) is unchanged. And a `Card` standing in the base is FLAT in a coloured theme (`data-palette`) and under `data-finish="lit"`: the base clips at its own edges, so a card's shadow ended in a straight line under the top bar and along the first sheet. Nothing to change in a project. New knob `--stack-base-gap` (the room between the base's things and under the last one; default the card gap, `--sp-5`). The room above the heads is shorter by that gap for loose content — on a short phone check what you put there still fits. Story `SheetStack · ClosedCards`.
|
|
24
|
+
|
|
5
25
|
## 0.7.75 — 2026-10-09
|
|
6
26
|
|
|
7
27
|
### Changed — ten names say what the component is for (BREAKING: the old names are gone)
|
package/README.md
CHANGED
|
@@ -205,6 +205,10 @@ the card is the material, the sheet is what it is in the stack.
|
|
|
205
205
|
front one too, peeks at the stack's bottom; the headroom above is the base's.
|
|
206
206
|
An optional `<div class="sheet-stack__base">` — written AFTER the sheets, so
|
|
207
207
|
it takes no sheet slot — is what the base shows there, behind every sheet.
|
|
208
|
+
Loose things in it rest against the heads, with the same room under the
|
|
209
|
+
last one as between two of them (`--stack-base-gap`); one `Page` reads from
|
|
210
|
+
the top, one `Centre` stands in the middle. A card standing there is flat:
|
|
211
|
+
the base clips at its edges, so a shadow would end in a straight line.
|
|
208
212
|
A tap, or a pull up, opens any sheet over it. The open title or its back
|
|
209
213
|
button, tapped or pulled down, drops it home. A pull down in the open
|
|
210
214
|
content, already at the top, asks `onRefresh` — omit it and that pull only
|
|
@@ -476,6 +480,17 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
476
480
|
`<Card heading caption end foot footLink>` with the content as children —
|
|
477
481
|
the card writes the order (head first, foot last). A head of its own shape
|
|
478
482
|
is a `<CardHead>` child instead; never both (Lh 2026-09-27).
|
|
483
|
+
- **A card that goes on is pressed whole** (Lh 2026-10-09). A card whose ONLY
|
|
484
|
+
control is its foot's button, or the end button of a row card, is pressed
|
|
485
|
+
anywhere: a touch off the button runs that button's own action once, and the
|
|
486
|
+
whole card dims under the finger (`.card--go`, the card-wide press answer of
|
|
487
|
+
a row or a card link). `Card` does it by itself — the app wires the button
|
|
488
|
+
and nothing else, and there is no prop to turn it off. The keyboard and a
|
|
489
|
+
screen reader meet the button only. Every other card is only read: two or
|
|
490
|
+
more controls anywhere in it (a field, a choice, a nested card's button),
|
|
491
|
+
the one button in the head or in the content, a disabled button, a sheet, a
|
|
492
|
+
stack's sheet, a modal. A card that is itself a link or a label was pressed
|
|
493
|
+
whole already. Story `Card · PressedWhole`.
|
|
479
494
|
- A page with **one feature and one CTA** (an install page, a single
|
|
480
495
|
decision) may be contained in one card: head, the pitch, foot.
|
|
481
496
|
- **A bottom sheet holds what the member acts on** — controls, or small
|
|
@@ -528,10 +543,16 @@ it up. cardds draws the paper; the pictures, their words and which stamp a membe
|
|
|
528
543
|
`words` the small line above it. `tilt` turns it, `onClick` makes the whole stamp a button. It fills its column up to
|
|
529
544
|
`--stamp-max-w`, and every inner length follows its width, so one stamp reads in a tray and in a sheet.
|
|
530
545
|
- **`StampPlace`** — an empty dashed place the size of a stamp: a word inside, `onClick` for "make yours", `faint` for a
|
|
531
|
-
gap nobody is asked to fill.
|
|
546
|
+
gap nobody is asked to fill. The edge is a drawn stroke, so its dashes are yours: `--stamp-place-dash` and
|
|
547
|
+
`--stamp-place-gap` (defaults 8px and 12px), its corner is `--stamp-place-radius`.
|
|
532
548
|
- **`StampBook`** + **`StampCell`** — the album page: cells three across on an album ground, a `label` under each stamp,
|
|
533
549
|
the free places drawn up to `places`. A cell with `onClick` is a button; `on` marks it pressed. `strip` is one line
|
|
534
|
-
of small stamps that scrolls sideways, the tray a member picks from.
|
|
550
|
+
of small stamps that scrolls sideways, the tray a member picks from. A `false` or `null` child holds no place.
|
|
551
|
+
- A stamp stands for a person in three more places. In a `Centre` — a `Stamp` or a `StampPlace` — it takes the screen's
|
|
552
|
+
size and the words and link under it centre. In a card's head — `<Card heading={name} caption={greeting}
|
|
553
|
+
end={<Stamp bare …/>} foot={<Btn primary block>…</Btn>} />` — the words stand at the left and the stamp at the right,
|
|
554
|
+
as tall as both lines, `--card-head-stamp-w` wide (default two taps); the card's one button presses it whole. In an
|
|
555
|
+
`AvatarStack` it is a head (`more="+9"` for the count).
|
|
535
556
|
- **`StampOn`** — a stamp stuck on a surface. `at` is the stamp's middle as a share of the surface (`{ x, y }`, 0 to 1),
|
|
536
557
|
`turn` its angle in degrees. With `onAtChange` the member drags it (the arrow keys move it too) and it asks with the
|
|
537
558
|
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
|
@@ -361,7 +361,10 @@
|
|
|
361
361
|
press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
|
|
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
|
-
pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
|
|
364
|
+
pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
|
|
365
|
+
.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
|
+
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
|
+
button's, the card itself is never focused or disabled. */
|
|
365
368
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
366
369
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
367
370
|
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) {
|
|
@@ -369,7 +372,7 @@
|
|
|
369
372
|
touch-action: manipulation;
|
|
370
373
|
transition: scale var(--motion-press) ease-out;
|
|
371
374
|
}
|
|
372
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge) {
|
|
375
|
+
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge) {
|
|
373
376
|
cursor: pointer;
|
|
374
377
|
touch-action: manipulation;
|
|
375
378
|
transition: opacity var(--motion-press) ease-out;
|
|
@@ -384,7 +387,7 @@
|
|
|
384
387
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
385
388
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
386
389
|
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); }
|
|
387
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
|
|
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); }
|
|
388
391
|
.sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
|
|
389
392
|
/* 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:
|
|
390
393
|
it darkens a touch instead (--press-shade) */
|
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
|
package/css/stack.css
CHANGED
|
@@ -280,9 +280,10 @@
|
|
|
280
280
|
z-index: 0;
|
|
281
281
|
height: var(--_headroom, 0px);
|
|
282
282
|
padding-inline: var(--screen-pad);
|
|
283
|
+
--_gap: var(--card-gap, var(--sp-5));
|
|
283
284
|
display: grid;
|
|
284
285
|
align-content: end;
|
|
285
|
-
gap: var(--
|
|
286
|
+
gap: var(--stack-base-gap, var(--_gap));
|
|
286
287
|
overflow: hidden;
|
|
287
288
|
overscroll-behavior: none;
|
|
288
289
|
touch-action: none;
|
|
@@ -295,6 +296,12 @@
|
|
|
295
296
|
(Lh 2026-10-06). The base already keeps the screen's inset, so the centre block gives its own back. */
|
|
296
297
|
.sheet-stack__base:has(> .screen__centre:only-child) { align-content: center; }
|
|
297
298
|
.sheet-stack__base > .screen__centre { padding-inline: 0; }
|
|
299
|
+
/* 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
|
+
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)) { padding-bottom: var(--stack-base-gap, var(--_gap)); }
|
|
302
|
+
/* 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
|
+
line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
|
|
304
|
+
:is([data-palette], [data-finish="lit"]) .sheet-stack__base .card { box-shadow: none; }
|
|
298
305
|
|
|
299
306
|
/* ============================================================
|
|
300
307
|
.sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
|
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
|
|
@@ -1304,9 +1313,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1304
1313
|
z-index: 0;
|
|
1305
1314
|
height: var(--_headroom, 0px);
|
|
1306
1315
|
padding-inline: var(--screen-pad);
|
|
1316
|
+
--_gap: var(--card-gap, var(--sp-5));
|
|
1307
1317
|
display: grid;
|
|
1308
1318
|
align-content: end;
|
|
1309
|
-
gap: var(--
|
|
1319
|
+
gap: var(--stack-base-gap, var(--_gap));
|
|
1310
1320
|
overflow: hidden;
|
|
1311
1321
|
overscroll-behavior: none;
|
|
1312
1322
|
touch-action: none;
|
|
@@ -1319,6 +1329,12 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
1319
1329
|
(Lh 2026-10-06). The base already keeps the screen's inset, so the centre block gives its own back. */
|
|
1320
1330
|
.sheet-stack__base:has(> .screen__centre:only-child) { align-content: center; }
|
|
1321
1331
|
.sheet-stack__base > .screen__centre { padding-inline: 0; }
|
|
1332
|
+
/* 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
|
+
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)) { padding-bottom: var(--stack-base-gap, var(--_gap)); }
|
|
1335
|
+
/* 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
|
+
line under the bar and along the first sheet. Its ground and its radius are what say "card" here. */
|
|
1337
|
+
:is([data-palette], [data-finish="lit"]) .sheet-stack__base .card { box-shadow: none; }
|
|
1322
1338
|
|
|
1323
1339
|
/* ============================================================
|
|
1324
1340
|
.sheet-stack--flood — a closed stack whose open sheet FLOODS the screen: it rises over the top bar and fills the whole screen
|
|
@@ -2720,7 +2736,10 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2720
2736
|
press). Two answers, by size: GIVE — a control the size of a finger shrinks under it (--press-scale) and springs back; DIM —
|
|
2721
2737
|
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)
|
|
2722
2738
|
instead, because a whole row shrinking reads as the card moving. A choice's fill fades in as it is picked. Disabled = no
|
|
2723
|
-
pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
|
|
2739
|
+
pointer, no press, --off. Zero specificity everywhere: a component's own rule always wins. A NEW control joins a list here.
|
|
2740
|
+
.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
|
+
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
|
+
button's, the card itself is never focused or disabled. */
|
|
2724
2743
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2725
2744
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2726
2745
|
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) {
|
|
@@ -2728,7 +2747,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2728
2747
|
touch-action: manipulation;
|
|
2729
2748
|
transition: scale var(--motion-press) ease-out;
|
|
2730
2749
|
}
|
|
2731
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge) {
|
|
2750
|
+
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .card--go, .toggle-field, .post-box__edge) {
|
|
2732
2751
|
cursor: pointer;
|
|
2733
2752
|
touch-action: manipulation;
|
|
2734
2753
|
transition: opacity var(--motion-press) ease-out;
|
|
@@ -2743,7 +2762,7 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
2743
2762
|
:where(.btn, .icon-btn, .link, a.chip, button.chip, .chip--pick, a.tile, button.tile, .tile--pick, .day, .segment__item, .calendar__day,
|
|
2744
2763
|
.float-bar__item, .avatar-pick__item, .add-row, .picker__item, .dropdown__btn, .appbar__who, .check, .toggle, .mood__opt, .sheet__handle,
|
|
2745
2764
|
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); }
|
|
2746
|
-
:where(a.row, button.row, label.row, .row-expand > summary, .banner__summary, a.card, label.card, .toggle-field, .post-box__edge):active:not([aria-disabled="true"], :has(:disabled)) { opacity: var(--press-dim); }
|
|
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); }
|
|
2747
2766
|
.sheet-step:has(> .sheet-step__go:active) { opacity: var(--press-dim); } /* the step bar's button is invisible over it: the bar answers */
|
|
2748
2767
|
/* 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:
|
|
2749
2768
|
it darkens a touch instead (--press-shade) */
|
|
@@ -3986,29 +4005,52 @@ button:enabled { cursor: pointer; } /* a disabled one keeps the arrow */
|
|
|
3986
4005
|
/* --tap: the whole stamp is the button (`onClick`); the press and the ring are the PRESS list's (actions.css) */
|
|
3987
4006
|
.stamp--tap { appearance: none; border: 0; padding: 0; background: none; font: inherit; text-align: start; -webkit-tap-highlight-color: transparent; }
|
|
3988
4007
|
|
|
3989
|
-
/* 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 */
|
|
3990
|
-
.screen__centre:has(> .stamp) { justify-items: center; text-align: center; }
|
|
3991
|
-
.screen__centre > .stamp { max-width: var(--stamp-centre-max-w, 56cqw); }
|
|
4008
|
+
/* 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 */
|
|
4009
|
+
.screen__centre:has(> :is(.stamp, .stamp-place)) { justify-items: center; text-align: center; }
|
|
4010
|
+
.screen__centre > :is(.stamp, .stamp-place) { max-width: var(--stamp-centre-max-w, 56cqw); }
|
|
3992
4011
|
/* 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
|
|
3993
4012
|
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. */
|
|
3994
|
-
.sheet-stack__base > .screen__centre > .stamp { max-width: min(var(--stamp-centre-max-w, 56cqw), 36cqh); }
|
|
4013
|
+
.sheet-stack__base > .screen__centre > :is(.stamp, .stamp-place) { max-width: min(var(--stamp-centre-max-w, 56cqw), 36cqh); }
|
|
3995
4014
|
|
|
3996
4015
|
/* ---- stamp place: an empty place for a stamp — an album's gap, "make yours" ----
|
|
3997
|
-
<span class="stamp-place">สร้าง</span> · <button class="stamp-place">…</button> · <span class="stamp-place stamp-place--faint"
|
|
4016
|
+
<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>
|
|
4017
|
+
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
|
|
4018
|
+
knobs (--stamp-place-dash, --stamp-place-gap) and its corners still follow --stamp-place-radius. The element keeps a clear border
|
|
4019
|
+
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. */
|
|
3998
4020
|
.stamp-place {
|
|
4021
|
+
position: relative;
|
|
3999
4022
|
display: grid;
|
|
4000
4023
|
place-content: center;
|
|
4001
4024
|
justify-items: center;
|
|
4002
4025
|
padding: var(--sp-1);
|
|
4003
|
-
border: var(--rule-w)
|
|
4026
|
+
border: var(--rule-w) solid transparent;
|
|
4004
4027
|
border-radius: var(--stamp-place-radius, var(--sp-2));
|
|
4005
4028
|
background: none;
|
|
4006
4029
|
color: var(--stamp-place-ink, var(--on-surface-muted));
|
|
4007
4030
|
font: var(--stamp-place-font, var(--type-caption));
|
|
4008
4031
|
text-align: center;
|
|
4032
|
+
--_edge: var(--stamp-place-border, var(--on-surface-muted));
|
|
4009
4033
|
}
|
|
4010
|
-
.
|
|
4011
|
-
|
|
4034
|
+
.stamp-place__edge {
|
|
4035
|
+
position: absolute;
|
|
4036
|
+
inset: calc(var(--rule-w) / -2) auto auto calc(var(--rule-w) / -2);
|
|
4037
|
+
width: calc(100% + var(--rule-w));
|
|
4038
|
+
height: calc(100% + var(--rule-w));
|
|
4039
|
+
max-width: none; /* base.css caps every svg at its box */
|
|
4040
|
+
overflow: visible; /* the stroke is centred on the rect's own edge: half of it falls outside the svg */
|
|
4041
|
+
pointer-events: none;
|
|
4042
|
+
}
|
|
4043
|
+
.stamp-place__edge > rect {
|
|
4044
|
+
width: 100%;
|
|
4045
|
+
height: 100%;
|
|
4046
|
+
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 */
|
|
4047
|
+
fill: none;
|
|
4048
|
+
stroke: var(--_edge);
|
|
4049
|
+
stroke-width: var(--rule-w);
|
|
4050
|
+
stroke-dasharray: var(--stamp-place-dash, var(--sp-2)) var(--stamp-place-gap, var(--sp-3));
|
|
4051
|
+
}
|
|
4052
|
+
.card .stamp-place, .post-paper .stamp-place { --_edge: var(--stamp-place-border, var(--card-muted)); color: var(--stamp-place-ink, var(--card-muted)); }
|
|
4053
|
+
.stamp-place--faint { --_edge: var(--stamp-place-faint-border, var(--hairline)); }
|
|
4012
4054
|
|
|
4013
4055
|
/* ---- stamp book: the album page — three stamps across on an album ground, who each is from under it ----
|
|
4014
4056
|
<div class="stamp-book">
|
package/dist/cards/Card.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ComponentProps, type ReactNode } from 'react';
|
|
1
|
+
import { type ComponentProps, type MouseEvent, type ReactNode } from 'react';
|
|
2
2
|
import { type CardHeadProps } from './CardHead.js';
|
|
3
3
|
import { type StackPlace } from '../sheets/stackCard.js';
|
|
4
4
|
export type { StackPlace };
|
|
@@ -47,9 +47,11 @@ export interface CardProps extends ComponentProps<'article'> {
|
|
|
47
47
|
* (its CardFoot last); the children are the content between them (frameless rows, kv, a ring, a paragraph — never a second card
|
|
48
48
|
* boundary). The order is the card's, never the writer's. A head of its own shape: write `<CardHead>` as the first child instead.
|
|
49
49
|
* A page with one feature and one CTA may be a single card. Keep everything big.
|
|
50
|
+
* PRESSED WHOLE: a card whose ONLY control is its foot's button, or a row card's end button, is pressed anywhere — a touch off the
|
|
51
|
+
* button runs that button's own action once, and the card dims under the finger. The app writes nothing; any other card is only read.
|
|
50
52
|
*/
|
|
51
53
|
export declare function Card({ tone, paper, sm, row, cover, fold, centre, band, as, heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }: CardProps): import("react").DetailedReactHTMLElement<{
|
|
52
|
-
onClick:
|
|
54
|
+
onClick: (e: MouseEvent<HTMLElement>) => void;
|
|
53
55
|
"aria-busy"?: (boolean | "true" | "false") | undefined;
|
|
54
56
|
ref: (node: HTMLElement | null) => void;
|
|
55
57
|
className: string;
|
package/dist/cards/Card.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Children, createElement, isValidElement, useRef } from 'react';
|
|
2
|
+
import { Children, createElement, isValidElement, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../cx.js';
|
|
4
4
|
import { warnOnce } from '../state.js';
|
|
5
5
|
import { CardHead } from './CardHead.js';
|
|
6
6
|
import { CardFoot } from './CardFoot.js';
|
|
7
|
+
import { goControl } from './cardGo.js';
|
|
7
8
|
import { StackHeadCtx, StackPullCtx, stackPaint, useStackCard } from '../sheets/stackCard.js';
|
|
8
9
|
/**
|
|
9
10
|
* Card — ONE boundary: head · content · foot. Cards are content the system offers (the words, the thing to read).
|
|
@@ -11,6 +12,8 @@ import { StackHeadCtx, StackPullCtx, stackPaint, useStackCard } from '../sheets/
|
|
|
11
12
|
* (its CardFoot last); the children are the content between them (frameless rows, kv, a ring, a paragraph — never a second card
|
|
12
13
|
* boundary). The order is the card's, never the writer's. A head of its own shape: write `<CardHead>` as the first child instead.
|
|
13
14
|
* A page with one feature and one CTA may be a single card. Keep everything big.
|
|
15
|
+
* PRESSED WHOLE: a card whose ONLY control is its foot's button, or a row card's end button, is pressed anywhere — a touch off the
|
|
16
|
+
* button runs that button's own action once, and the card dims under the finger. The app writes nothing; any other card is only read.
|
|
14
17
|
*/
|
|
15
18
|
export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'article', heading, caption, end, level, foot, footLink, stack, className, style, children, onClick, ref, ...rest }) {
|
|
16
19
|
const head = heading != null || caption != null || end != null;
|
|
@@ -34,11 +37,30 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
|
|
|
34
37
|
else if (passed)
|
|
35
38
|
passed.current = node;
|
|
36
39
|
};
|
|
40
|
+
const [go, setGo] = useState(false);
|
|
41
|
+
useLayoutEffect(() => { setGo(own.current != null && goControl(own.current) != null); }); // every render: what the card holds is its children's
|
|
42
|
+
const pressing = useRef(false);
|
|
37
43
|
const click = (e) => {
|
|
44
|
+
if (pressing.current)
|
|
45
|
+
return; // the control's click, pressed from here, coming back up: the app's onClick has run for this press
|
|
38
46
|
onClick?.(e);
|
|
47
|
+
const target = e.target;
|
|
48
|
+
const card = own.current;
|
|
49
|
+
if (!e.defaultPrevented && card?.contains(target)) {
|
|
50
|
+
const control = goControl(card);
|
|
51
|
+
if (control && !control.contains(target)) {
|
|
52
|
+
pressing.current = true;
|
|
53
|
+
try {
|
|
54
|
+
control.click();
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
pressing.current = false;
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
39
62
|
if (!stack || e.defaultPrevented || swallow.current)
|
|
40
63
|
return;
|
|
41
|
-
const target = e.target;
|
|
42
64
|
if (target.closest('.sheet-stack__back')) {
|
|
43
65
|
stack.onClose();
|
|
44
66
|
return;
|
|
@@ -54,10 +76,10 @@ export function Card({ tone, paper, sm, row, cover, fold, centre, band, as = 'ar
|
|
|
54
76
|
const props = {
|
|
55
77
|
...rest,
|
|
56
78
|
ref: setRef,
|
|
57
|
-
className: cx('card', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', painted.className, className),
|
|
79
|
+
className: cx('card', tone && tone !== 1 && `card--${tone}`, paper && 'sheet--paper', sm && 'card--sm', row && 'card--row', cover && 'card--cover', fold && 'card--fold', centre && 'card--centre', band && 'card--band', go && 'card--go', painted.className, className),
|
|
58
80
|
style: painted.style || style ? { ...painted.style, ...style } : undefined,
|
|
59
81
|
...(stack ? { 'aria-busy': painted.busy } : {}),
|
|
60
|
-
onClick:
|
|
82
|
+
onClick: click,
|
|
61
83
|
};
|
|
62
84
|
if (!head && !tail)
|
|
63
85
|
return createElement(as, props, inner(children));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* THE RULE (Lh 2026-10-09): a card that holds exactly ONE control, standing where the card's own move sits (its foot, or the end of a
|
|
2
|
+
row card), is pressed whole — a touch anywhere on it is a press on that control. Asked by `Card` only, once for the class it
|
|
3
|
+
renders and once, fresh, for each click. */
|
|
4
|
+
/** everything a member presses, types in or drags. One more of these anywhere in the card (a nested card's too) and the card is only read */
|
|
5
|
+
const CONTROLS = [
|
|
6
|
+
'button', 'a[href]', 'input:not([type=hidden])', 'textarea', 'select', 'summary',
|
|
7
|
+
'[role=button]', '[role=link]', '[role=tab]', '[role=radio]', '[role=checkbox]', '[role=switch]', '[role=slider]', '[role=menuitem]',
|
|
8
|
+
'[tabindex]:not([tabindex="-1"])', '[contenteditable]',
|
|
9
|
+
'.stamp-on', // dragged
|
|
10
|
+
].join(', ');
|
|
11
|
+
/** a card that is pressed whole already (a link, a label, a button), or where a touch means something else: a sheet is where the member
|
|
12
|
+
* acts, a modal asks, a touch on a stack's sheet opens it (`stack-card` is what `Card` renders from its `stack` prop) */
|
|
13
|
+
const NOT_A_GO_CARD = 'a, label, button, .sheet, .modal, .stack-card';
|
|
14
|
+
/** the control this card presses when it is touched off that control, or null: the card is only read */
|
|
15
|
+
export function goControl(card) {
|
|
16
|
+
if (card.matches(NOT_A_GO_CARD))
|
|
17
|
+
return null;
|
|
18
|
+
const controls = card.querySelectorAll(CONTROLS);
|
|
19
|
+
if (controls.length !== 1)
|
|
20
|
+
return null;
|
|
21
|
+
const control = controls[0];
|
|
22
|
+
if (control.matches(':disabled, [aria-disabled="true"]'))
|
|
23
|
+
return null;
|
|
24
|
+
const inFoot = control.parentElement?.matches('.card__foot') && control.parentElement.parentElement === card;
|
|
25
|
+
const atRowEnd = card.matches('.card--row') && card.lastElementChild === control;
|
|
26
|
+
return inFoot || atRowEnd ? control : null;
|
|
27
|
+
}
|