@usableapp/cardds 0.4.0 → 0.4.1
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/README.md +3 -3
- package/css/layover.css +12 -15
- package/css/lists.css +9 -0
- package/css/media.css +67 -31
- package/css/numbers.css +16 -0
- package/css/sheet.css +8 -4
- package/css/stack.css +3 -3
- package/dist/cardds.css +115 -53
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lists/Row.d.ts +11 -283
- package/dist/lists/Row.js +10 -3
- package/dist/media/Postcard.d.ts +15 -5
- package/dist/media/Postcard.js +8 -5
- package/dist/numbers/Progress.d.ts +9 -0
- package/dist/numbers/Progress.js +7 -0
- package/dist/sheets/Drawer.d.ts +2 -2
- package/dist/sheets/Drawer.js +1 -1
- package/dist/sheets/Sheet.d.ts +1 -3
- package/dist/sheets/Sheet.js +2 -2
- package/dist/sheets/SheetStack.d.ts +3 -3
- package/dist/sheets/SheetStack.js +3 -3
- package/dist/sheets/SheetStage.d.ts +3 -5
- package/dist/sheets/SheetStage.js +3 -3
- package/dist/sheets/StackBack.d.ts +7 -0
- package/dist/sheets/StackBack.js +7 -0
- package/package.json +1 -1
- package/dist/sheets/CardBack.d.ts +0 -7
- package/dist/sheets/CardBack.js +0 -7
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ the card is the material, the sheet is what it is in the stack.
|
|
|
105
105
|
<div class="sheet-stack sheet-stack--tap"> <!-- drop --tap for a static stack -->
|
|
106
106
|
<article class="card card--2">
|
|
107
107
|
<div class="card__head">
|
|
108
|
-
<button class="icon-btn icon-btn--sm
|
|
108
|
+
<button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
|
|
109
109
|
<h2 class="t-h2 t-caps">Shopping</h2>
|
|
110
110
|
<span class="icon-row">…</span> <!-- optional trailing icons -->
|
|
111
111
|
</div>
|
|
@@ -125,7 +125,7 @@ the card is the material, the sheet is what it is in the stack.
|
|
|
125
125
|
A tap opens any sheet over it; back drops it home.
|
|
126
126
|
- **Five sheets max.** Slots are declared per `:nth-child` in `stack.css`; a
|
|
127
127
|
sixth would render at slot 0, on top of the first.
|
|
128
|
-
- **Every head needs its own `.
|
|
128
|
+
- **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
|
|
129
129
|
hidden until that sheet is open, then slides into a reserved header slot.
|
|
130
130
|
- **No ids, no wiring.** `cardds.js` finds any `.sheet-stack--tap` on the page.
|
|
131
131
|
- **`.card-stack` is gone.** That was this component's first name and the wrong
|
|
@@ -433,7 +433,7 @@ Demo: the *skeleton* block at the top of `elements.html`.
|
|
|
433
433
|
padding. Two boxes hold sheets and the states
|
|
434
434
|
mean the same in both: `.step` (an app screen, the bar outside — Anatomy)
|
|
435
435
|
and `.sheet-stage` (a page-level overlay: a sheet over a map, the dim page
|
|
436
|
-
|
|
436
|
+
stage `--dim` + `.modal` = `__lift` + `__drawer` — a sheet always stands on the floor, never raised).
|
|
437
437
|
A stage keeps the top bar's line free (`--_top`), so `--full` stops under
|
|
438
438
|
`--header-h` and keeps its corners. Both sheet tokens are published — a
|
|
439
439
|
sheet with a taller head retunes `--sheet-peek` — and registered in
|
package/css/layover.css
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
lives in sheet.css; these compose with .sheet-stage.
|
|
6
6
|
============================================================ */
|
|
7
7
|
|
|
8
|
-
/* ----
|
|
9
|
-
<div class="sheet-stage sheet-stage--
|
|
8
|
+
/* ---- the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
|
|
9
|
+
<div class="sheet-stage sheet-stage--dim"> (dim = the page under a scrim; a Modal on a clean ground is the stage without it — Lh 2026-09-22, `--ask` retired: the stage does the ground, the modal centres itself)
|
|
10
10
|
<article class="card modal">
|
|
11
11
|
<div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
|
|
12
12
|
<div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
|
|
@@ -17,30 +17,27 @@
|
|
|
17
17
|
.modal is the card — no border (the shadow is its edge on the scrim);
|
|
18
18
|
.modal__lift the light part that holds the thing being asked about (it
|
|
19
19
|
scrolls when tall); .modal__drawer the dark end that asks. The ground is
|
|
20
|
-
the
|
|
20
|
+
whatever the stage shows (dim it or not). Two beats (2 × --motion-sheet) when
|
|
21
21
|
the stage appears, and the same two backwards when it closes:
|
|
22
22
|
1. the card drops in from above the stage to the screen's centre — its
|
|
23
23
|
drawer still inside it, height 0: nothing of the ask shows yet
|
|
24
24
|
2. the drawer comes out of the card's bottom edge while the lift moves
|
|
25
25
|
up, both at once: the card grows from its centre, which never moves
|
|
26
|
-
(the
|
|
26
|
+
(the modal centres itself — absolute, margin auto; a growing card keeps its middle where it is)
|
|
27
27
|
Closing: a click on any button in the drawer plays the same animations in
|
|
28
28
|
reverse (cardds.js: Animation.reverse() on each — nothing duplicated), then
|
|
29
29
|
fires "cardds:modal" on the .sheet-stage (bubbles, detail.answer from the
|
|
30
30
|
button's data-answer) for the app to remove the stage.
|
|
31
31
|
Reduced motion: the finished state, no beats; closing fires at once. */
|
|
32
|
-
.sheet-stage--ask {
|
|
33
|
-
--drawer-h: 14rem;
|
|
34
|
-
display: grid;
|
|
35
|
-
align-content: center; /* the card at the screen's centre — and kept there as it grows */
|
|
36
|
-
padding-inline: var(--screen-pad);
|
|
37
|
-
}
|
|
38
|
-
.sheet-stage--ask::after { content: ""; position: absolute; inset: 0; background: var(--scrim); } /* the page dimmed */
|
|
39
|
-
.sheet-stage--ask > .sheet-stage__bg { filter: blur(var(--sp-2)); }
|
|
40
32
|
.modal {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
--drawer-h: 14rem;
|
|
34
|
+
position: absolute; /* at the stage's centre, and kept there as it grows: absolute + margin auto re-centres on every height */
|
|
35
|
+
inset: 0;
|
|
36
|
+
margin: auto;
|
|
37
|
+
height: fit-content; /* inset 0 alone would stretch it to the stage: its own height, then the margins share the rest */
|
|
38
|
+
width: calc(100% - 2 * var(--screen-pad));
|
|
39
|
+
z-index: 2; /* over the dim stage's scrim (z 1) */
|
|
40
|
+
max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
|
|
44
41
|
padding: 0; /* the parts pad themselves */
|
|
45
42
|
border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
|
|
46
43
|
display: grid;
|
package/css/lists.css
CHANGED
|
@@ -36,6 +36,15 @@
|
|
|
36
36
|
.row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
|
|
37
37
|
.row--lg { min-height: var(--row-lg-min-h, var(--fab)); } /* menu rows: a taller target, round arrow at the end; .rows gives them more air */
|
|
38
38
|
.row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
|
|
39
|
+
/* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
|
|
40
|
+
who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
|
|
41
|
+
<details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
|
|
42
|
+
.row-expand { display: block; }
|
|
43
|
+
.row-expand > summary { list-style: none; cursor: pointer; }
|
|
44
|
+
.row-expand > summary::-webkit-details-marker { display: none; }
|
|
45
|
+
.row-expand > summary > .icon:last-child { transition: rotate var(--motion-slide); }
|
|
46
|
+
.row-expand[open] > summary > .icon:last-child { rotate: 180deg; }
|
|
47
|
+
.row-expand__more { padding-top: var(--row-expand-gap, var(--sp-3)); }
|
|
39
48
|
/* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
|
|
40
49
|
the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
|
|
41
50
|
pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
|
package/css/media.css
CHANGED
|
@@ -152,59 +152,75 @@
|
|
|
152
152
|
.mosaic > .ph { opacity: 0.4; border-width: var(--border-w); }
|
|
153
153
|
.mosaic > img { width: 100%; height: 100%; object-fit: cover; }
|
|
154
154
|
|
|
155
|
-
/* ---- postcard: a member's introduction
|
|
156
|
-
|
|
155
|
+
/* ---- postcard: a member's introduction in the shape of a cover card — the photo fills it,
|
|
156
|
+
the band at the bottom names it; tap the photo to turn it over (Lh 2026-09-12; reshaped 2026-09-22) ----
|
|
157
157
|
<div class="postcard">
|
|
158
|
-
<
|
|
159
|
-
<img src="…" alt=""> — or <div class="ph"></div>
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
<div class="postcard__face postcard__front">
|
|
159
|
+
<button class="postcard__turn" type="button" aria-label="พลิกดูด้านหลัง"><img src="…" alt=""> — or <div class="ph"></div> —</button>
|
|
160
|
+
<div class="postcard__band">
|
|
161
|
+
<div class="postcard__words"><span class="t-h1">ป้าแจ๋ว</span><span class="t-body">บ้านสวนผัก</span></div>
|
|
162
|
+
<button class="icon-btn icon-btn--invert" aria-label="ถูกใจ" aria-pressed="false">♥</button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="postcard__face postcard__back">
|
|
166
|
+
<button class="postcard__turn" type="button" aria-label="พลิกดูด้านหน้า">
|
|
167
|
+
<div class="postcard__words">…</div><p class="postcard__caption">ชอบชงกาแฟดริปตอนเช้า</p>
|
|
168
|
+
</button>
|
|
169
|
+
<div class="postcard__voice"><div class="progress"></div><span class="t-caption">0:07 / 0:12</span><button class="icon-btn icon-btn--invert" aria-label="ฟัง">▶</button></div> (a voice note: no caption — the line, its time, the control under them, at the middle)
|
|
170
|
+
</div>
|
|
166
171
|
</div>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
The two faces share one box and turn (a Y rotation at --motion-flip, a plain
|
|
173
|
+
swap under prefers-reduced-motion). A face is a div: the TURN is the button
|
|
174
|
+
that fills it (the photo, or the caption), the band lies over its foot and
|
|
175
|
+
turns with it — words and the heart; the back sets the name a fifth of the way down, the words after it; a
|
|
176
|
+
button in the band is never inside the turn button. The size is the cover
|
|
177
|
+
card's (--postcard-min-h = the cover's min-height, the card radius). Who
|
|
178
|
+
liked it, a voice note, a reply live OUTSIDE the postcard, in a Row under it. */
|
|
173
179
|
.postcard {
|
|
174
180
|
position: relative;
|
|
175
|
-
|
|
181
|
+
min-height: var(--postcard-min-h, 22.5rem);
|
|
176
182
|
perspective: 60rem;
|
|
177
|
-
border-radius: var(--postcard-radius, var(--r-
|
|
183
|
+
border-radius: var(--postcard-radius, var(--r-card));
|
|
184
|
+
isolation: isolate;
|
|
178
185
|
}
|
|
179
186
|
.postcard__face {
|
|
180
187
|
position: absolute;
|
|
181
188
|
inset: 0;
|
|
182
189
|
display: grid;
|
|
183
190
|
align-content: end;
|
|
184
|
-
padding: var(--postcard-face-pad, var(--sp-4));
|
|
185
|
-
gap: var(--postcard-face-gap, var(--sp-2));
|
|
186
191
|
border: var(--border-w) solid var(--postcard-face-border, var(--card-border));
|
|
187
192
|
border-radius: inherit;
|
|
188
193
|
overflow: hidden;
|
|
189
194
|
isolation: isolate;
|
|
190
|
-
color: inherit;
|
|
191
|
-
text-align: left;
|
|
192
195
|
backface-visibility: hidden;
|
|
193
196
|
-webkit-backface-visibility: hidden;
|
|
194
197
|
transition: transform var(--motion-flip) ease-in-out;
|
|
195
198
|
box-shadow: var(--postcard-face-shadow, var(--shadow-float));
|
|
196
199
|
}
|
|
197
|
-
|
|
198
|
-
|
|
200
|
+
/* the turn: the button that fills the face, under the band */
|
|
201
|
+
.postcard__turn {
|
|
202
|
+
position: absolute;
|
|
203
|
+
inset: 0;
|
|
204
|
+
display: grid;
|
|
205
|
+
align-content: start;
|
|
206
|
+
padding: var(--postcard-face-pad, var(--card-pad-rest));
|
|
207
|
+
border: 0;
|
|
208
|
+
border-radius: inherit;
|
|
209
|
+
background: transparent;
|
|
210
|
+
color: inherit;
|
|
211
|
+
text-align: left;
|
|
212
|
+
cursor: pointer;
|
|
213
|
+
}
|
|
214
|
+
.postcard__turn:focus-visible { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
|
|
215
|
+
/* the front: the photo (or the wireframe box) fills it; the band reads on a scrim */
|
|
199
216
|
.postcard__front { background: var(--postcard-front-bg, var(--card-3-bg)); color: var(--postcard-front-ink, var(--card-3-ink)); }
|
|
200
|
-
.postcard__front > img,
|
|
201
|
-
.postcard__front > .ph {
|
|
217
|
+
.postcard__front .postcard__turn > img,
|
|
218
|
+
.postcard__front .postcard__turn > .ph {
|
|
202
219
|
position: absolute;
|
|
203
220
|
inset: 0;
|
|
204
221
|
width: 100%;
|
|
205
222
|
height: 100%;
|
|
206
223
|
object-fit: cover;
|
|
207
|
-
z-index: -2;
|
|
208
224
|
border: 0;
|
|
209
225
|
border-radius: 0;
|
|
210
226
|
min-height: 0;
|
|
@@ -213,20 +229,40 @@
|
|
|
213
229
|
content: "";
|
|
214
230
|
position: absolute;
|
|
215
231
|
inset: 0;
|
|
216
|
-
z-index:
|
|
232
|
+
z-index: 1;
|
|
233
|
+
pointer-events: none;
|
|
217
234
|
background: linear-gradient(to top, var(--scrim), transparent 45%);
|
|
218
235
|
}
|
|
219
|
-
/* the back: paper — the
|
|
236
|
+
/* the back: paper — the caption and the name packed at the middle, big enough to read at arm's length */
|
|
220
237
|
.postcard__back {
|
|
221
238
|
background: var(--postcard-back-bg, var(--card-2-bg));
|
|
222
239
|
color: var(--postcard-back-ink, var(--card-2-ink));
|
|
223
|
-
align-content: space-between;
|
|
224
240
|
transform: rotateY(180deg);
|
|
225
241
|
}
|
|
242
|
+
.postcard__back .postcard__turn { align-content: start; padding-top: var(--postcard-back-top, 20%); gap: var(--postcard-back-gap, var(--sp-5)); } /* the name a fifth down the paper, the words after it */
|
|
226
243
|
.postcard__caption { margin: 0; font: var(--postcard-caption-font, var(--type-title)); text-wrap: pretty; }
|
|
227
|
-
|
|
244
|
+
/* the band: the face's foot — name and line left, the heart right; in flow at the face's end, over the turn.
|
|
245
|
+
Taps between its words fall through to the turn; its buttons take their own. */
|
|
246
|
+
.postcard__band {
|
|
247
|
+
position: relative;
|
|
248
|
+
z-index: 2;
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: end;
|
|
251
|
+
gap: var(--postcard-band-gap, var(--sp-3));
|
|
252
|
+
padding: var(--postcard-face-pad, var(--card-pad-rest));
|
|
253
|
+
pointer-events: none;
|
|
254
|
+
}
|
|
255
|
+
.postcard__band > * { pointer-events: auto; }
|
|
256
|
+
.postcard__words { display: grid; gap: var(--postcard-words-gap, var(--sp-1)); flex: 1; min-width: 0; pointer-events: none; }
|
|
257
|
+
.postcard__front .postcard__band { --accent: var(--postcard-front-ink, var(--card-3-ink)); --on-accent: var(--postcard-front-bg, var(--card-3-bg)); }
|
|
258
|
+
.postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
|
|
228
259
|
.postcard.is-flipped > .postcard__front { transform: rotateY(-180deg); }
|
|
229
260
|
.postcard.is-flipped > .postcard__back { transform: rotateY(0); }
|
|
261
|
+
/* a voice note on the back: no words to read — its timeline (a Progress), its length and the control to hear it
|
|
262
|
+
under them, at the middle over the turn, on the face's inset; the app plays and keeps the time */
|
|
263
|
+
.postcard__voice { position: absolute; inset: 0; z-index: 2; display: grid; align-content: center; justify-items: center; gap: var(--postcard-voice-gap, var(--sp-3)); padding: var(--postcard-face-pad, var(--card-pad-rest)); pointer-events: none; }
|
|
264
|
+
.postcard__voice > * { pointer-events: auto; }
|
|
265
|
+
.postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
|
|
230
266
|
@media (prefers-reduced-motion: reduce) {
|
|
231
267
|
.postcard__face { transition: none; }
|
|
232
268
|
}
|
package/css/numbers.css
CHANGED
|
@@ -256,3 +256,19 @@
|
|
|
256
256
|
}
|
|
257
257
|
.bar--on::after { background: var(--accent); }
|
|
258
258
|
.bar--on > span { background: var(--accent); color: var(--on-accent); padding: var(--sp-0) var(--sp-2); border-radius: var(--r-chip); }
|
|
259
|
+
|
|
260
|
+
/* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
|
|
261
|
+
timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
|
|
262
|
+
--progress-at is DATA the component writes (0–1), not a knob (scripts/knobs.mjs lists it). The done part is a
|
|
263
|
+
gradient of the line's ink sized by the share; the rest is the background colour. */
|
|
264
|
+
.progress {
|
|
265
|
+
display: block;
|
|
266
|
+
width: 100%;
|
|
267
|
+
height: var(--progress-h, var(--sp-1));
|
|
268
|
+
border-radius: var(--progress-radius, var(--r-chip));
|
|
269
|
+
color: var(--progress-done-ink, inherit);
|
|
270
|
+
background-color: var(--progress-rest-bg, color-mix(in srgb, currentColor 25%, transparent));
|
|
271
|
+
background-image: linear-gradient(currentColor, currentColor);
|
|
272
|
+
background-size: calc(var(--progress-at, 0) * 100%) 100%;
|
|
273
|
+
background-repeat: no-repeat;
|
|
274
|
+
}
|
package/css/sheet.css
CHANGED
|
@@ -250,8 +250,13 @@
|
|
|
250
250
|
inset-inline: var(--screen-pad);
|
|
251
251
|
top: var(--_band-centre);
|
|
252
252
|
translate: 0 -50%;
|
|
253
|
-
/*
|
|
253
|
+
/* as tall as the band less the screen's inset at both ends (Lh 2026-09-22: the room under the hero = the room at its sides);
|
|
254
|
+
what it holds fills it (a postcard's min-height gives way) */
|
|
255
|
+
height: calc((100cqh - var(--_top, 0rem)) / 2 - 2 * var(--screen-pad));
|
|
256
|
+
display: grid;
|
|
254
257
|
}
|
|
258
|
+
.sheet-stage > .sheet-stage__hero > * { min-height: 0; } /* outranks a component's own min-height (the postcard's) */
|
|
259
|
+
/* no z-index on the hero: it follows the background in DOM order and the sheet, later, covers it at full */
|
|
255
260
|
|
|
256
261
|
/* ---- map: the box a map engine draws in, behind a stage sheet ----
|
|
257
262
|
<div class="sheet-stage__bg"><div class="map"><div>…the engine's canvas…</div>
|
|
@@ -327,10 +332,9 @@
|
|
|
327
332
|
/* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
|
|
328
333
|
foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
|
|
329
334
|
scrolls, as far as it needs to. In a .step the keyboard's room is the step's (--kb, step.css); on a stage
|
|
330
|
-
the sheet gives it up itself, as --_lift
|
|
335
|
+
the sheet gives it up itself, as --_lift. (A sheet ALWAYS stands on the floor — `.sheet--raised`, the one that
|
|
336
|
+
floated above a footnote strip on a dim stage, was retired 2026-09-22: that is a Modal's job.) */
|
|
331
337
|
.sheet-stage > .sheet { --_lift: var(--kb, 0rem); bottom: var(--_lift); }
|
|
332
|
-
.sheet-stage > .sheet--raised { --_lift: calc(var(--tap-lg) + var(--kb, 0rem)); }
|
|
333
|
-
.sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--sheet-raised-radius, var(--r-card)); border-bottom: var(--border-w) solid var(--card-border); }
|
|
334
338
|
.sheet-stage__foot {
|
|
335
339
|
position: absolute;
|
|
336
340
|
inset-inline: 0;
|
package/css/stack.css
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<div class="sheet-stack sheet-stack--tap"> (drop --tap for a static stack)
|
|
20
20
|
<article class="card card--2">
|
|
21
21
|
<div class="card__head">
|
|
22
|
-
<button class="icon-btn icon-btn--sm
|
|
22
|
+
<button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
|
|
23
23
|
<h2 class="t-h2 t-caps">Shopping</h2>
|
|
24
24
|
<span class="icon-row">…</span> (optional trailing icons)
|
|
25
25
|
</div>
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
.sheet-stack--tap > .card.is-open > .card__head {
|
|
244
244
|
padding-left: calc(var(--tap-sm) + var(--sp-3));
|
|
245
245
|
}
|
|
246
|
-
.sheet-stack--tap .
|
|
246
|
+
.sheet-stack--tap .sheet-stack__back {
|
|
247
247
|
position: absolute;
|
|
248
248
|
left: 0;
|
|
249
249
|
top: 50%;
|
|
@@ -252,7 +252,7 @@
|
|
|
252
252
|
visibility: hidden;
|
|
253
253
|
transition: opacity 0.35s ease, visibility 0.35s;
|
|
254
254
|
}
|
|
255
|
-
.sheet-stack--tap > .card.is-open .
|
|
255
|
+
.sheet-stack--tap > .card.is-open .sheet-stack__back {
|
|
256
256
|
opacity: 1;
|
|
257
257
|
visibility: visible;
|
|
258
258
|
}
|
package/dist/cardds.css
CHANGED
|
@@ -839,7 +839,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
839
839
|
<div class="sheet-stack sheet-stack--tap"> (drop --tap for a static stack)
|
|
840
840
|
<article class="card card--2">
|
|
841
841
|
<div class="card__head">
|
|
842
|
-
<button class="icon-btn icon-btn--sm
|
|
842
|
+
<button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
|
|
843
843
|
<h2 class="t-h2 t-caps">Shopping</h2>
|
|
844
844
|
<span class="icon-row">…</span> (optional trailing icons)
|
|
845
845
|
</div>
|
|
@@ -1063,7 +1063,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1063
1063
|
.sheet-stack--tap > .card.is-open > .card__head {
|
|
1064
1064
|
padding-left: calc(var(--tap-sm) + var(--sp-3));
|
|
1065
1065
|
}
|
|
1066
|
-
.sheet-stack--tap .
|
|
1066
|
+
.sheet-stack--tap .sheet-stack__back {
|
|
1067
1067
|
position: absolute;
|
|
1068
1068
|
left: 0;
|
|
1069
1069
|
top: 50%;
|
|
@@ -1072,7 +1072,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1072
1072
|
visibility: hidden;
|
|
1073
1073
|
transition: opacity 0.35s ease, visibility 0.35s;
|
|
1074
1074
|
}
|
|
1075
|
-
.sheet-stack--tap > .card.is-open .
|
|
1075
|
+
.sheet-stack--tap > .card.is-open .sheet-stack__back {
|
|
1076
1076
|
opacity: 1;
|
|
1077
1077
|
visibility: visible;
|
|
1078
1078
|
}
|
|
@@ -1330,8 +1330,13 @@ button { font: inherit; cursor: pointer; }
|
|
|
1330
1330
|
inset-inline: var(--screen-pad);
|
|
1331
1331
|
top: var(--_band-centre);
|
|
1332
1332
|
translate: 0 -50%;
|
|
1333
|
-
/*
|
|
1333
|
+
/* as tall as the band less the screen's inset at both ends (Lh 2026-09-22: the room under the hero = the room at its sides);
|
|
1334
|
+
what it holds fills it (a postcard's min-height gives way) */
|
|
1335
|
+
height: calc((100cqh - var(--_top, 0rem)) / 2 - 2 * var(--screen-pad));
|
|
1336
|
+
display: grid;
|
|
1334
1337
|
}
|
|
1338
|
+
.sheet-stage > .sheet-stage__hero > * { min-height: 0; } /* outranks a component's own min-height (the postcard's) */
|
|
1339
|
+
/* no z-index on the hero: it follows the background in DOM order and the sheet, later, covers it at full */
|
|
1335
1340
|
|
|
1336
1341
|
/* ---- map: the box a map engine draws in, behind a stage sheet ----
|
|
1337
1342
|
<div class="sheet-stage__bg"><div class="map"><div>…the engine's canvas…</div>
|
|
@@ -1407,10 +1412,9 @@ button { font: inherit; cursor: pointer; }
|
|
|
1407
1412
|
/* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
|
|
1408
1413
|
foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
|
|
1409
1414
|
scrolls, as far as it needs to. In a .step the keyboard's room is the step's (--kb, step.css); on a stage
|
|
1410
|
-
the sheet gives it up itself, as --_lift
|
|
1415
|
+
the sheet gives it up itself, as --_lift. (A sheet ALWAYS stands on the floor — `.sheet--raised`, the one that
|
|
1416
|
+
floated above a footnote strip on a dim stage, was retired 2026-09-22: that is a Modal's job.) */
|
|
1411
1417
|
.sheet-stage > .sheet { --_lift: var(--kb, 0rem); bottom: var(--_lift); }
|
|
1412
|
-
.sheet-stage > .sheet--raised { --_lift: calc(var(--tap-lg) + var(--kb, 0rem)); }
|
|
1413
|
-
.sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--sheet-raised-radius, var(--r-card)); border-bottom: var(--border-w) solid var(--card-border); }
|
|
1414
1418
|
.sheet-stage__foot {
|
|
1415
1419
|
position: absolute;
|
|
1416
1420
|
inset-inline: 0;
|
|
@@ -2211,6 +2215,22 @@ button { font: inherit; cursor: pointer; }
|
|
|
2211
2215
|
.bar--on::after { background: var(--accent); }
|
|
2212
2216
|
.bar--on > span { background: var(--accent); color: var(--on-accent); padding: var(--sp-0) var(--sp-2); border-radius: var(--r-chip); }
|
|
2213
2217
|
|
|
2218
|
+
/* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
|
|
2219
|
+
timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
|
|
2220
|
+
--progress-at is DATA the component writes (0–1), not a knob (scripts/knobs.mjs lists it). The done part is a
|
|
2221
|
+
gradient of the line's ink sized by the share; the rest is the background colour. */
|
|
2222
|
+
.progress {
|
|
2223
|
+
display: block;
|
|
2224
|
+
width: 100%;
|
|
2225
|
+
height: var(--progress-h, var(--sp-1));
|
|
2226
|
+
border-radius: var(--progress-radius, var(--r-chip));
|
|
2227
|
+
color: var(--progress-done-ink, inherit);
|
|
2228
|
+
background-color: var(--progress-rest-bg, color-mix(in srgb, currentColor 25%, transparent));
|
|
2229
|
+
background-image: linear-gradient(currentColor, currentColor);
|
|
2230
|
+
background-size: calc(var(--progress-at, 0) * 100%) 100%;
|
|
2231
|
+
background-repeat: no-repeat;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2214
2234
|
/* ---- css/people.css — avatar, avatar stack, chooser row ---- */
|
|
2215
2235
|
/* ============================================================
|
|
2216
2236
|
cardds/people.css — avatars: single, stacked, chooser row.
|
|
@@ -2625,6 +2645,15 @@ button { font: inherit; cursor: pointer; }
|
|
|
2625
2645
|
.row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
|
|
2626
2646
|
.row--lg { min-height: var(--row-lg-min-h, var(--fab)); } /* menu rows: a taller target, round arrow at the end; .rows gives them more air */
|
|
2627
2647
|
.row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
|
|
2648
|
+
/* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
|
|
2649
|
+
who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
|
|
2650
|
+
<details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
|
|
2651
|
+
.row-expand { display: block; }
|
|
2652
|
+
.row-expand > summary { list-style: none; cursor: pointer; }
|
|
2653
|
+
.row-expand > summary::-webkit-details-marker { display: none; }
|
|
2654
|
+
.row-expand > summary > .icon:last-child { transition: rotate var(--motion-slide); }
|
|
2655
|
+
.row-expand[open] > summary > .icon:last-child { rotate: 180deg; }
|
|
2656
|
+
.row-expand__more { padding-top: var(--row-expand-gap, var(--sp-3)); }
|
|
2628
2657
|
/* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
|
|
2629
2658
|
the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
|
|
2630
2659
|
pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
|
|
@@ -2945,59 +2974,75 @@ button { font: inherit; cursor: pointer; }
|
|
|
2945
2974
|
.mosaic > .ph { opacity: 0.4; border-width: var(--border-w); }
|
|
2946
2975
|
.mosaic > img { width: 100%; height: 100%; object-fit: cover; }
|
|
2947
2976
|
|
|
2948
|
-
/* ---- postcard: a member's introduction
|
|
2949
|
-
|
|
2977
|
+
/* ---- postcard: a member's introduction in the shape of a cover card — the photo fills it,
|
|
2978
|
+
the band at the bottom names it; tap the photo to turn it over (Lh 2026-09-12; reshaped 2026-09-22) ----
|
|
2950
2979
|
<div class="postcard">
|
|
2951
|
-
<
|
|
2952
|
-
<img src="…" alt=""> — or <div class="ph"></div>
|
|
2953
|
-
<
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2980
|
+
<div class="postcard__face postcard__front">
|
|
2981
|
+
<button class="postcard__turn" type="button" aria-label="พลิกดูด้านหลัง"><img src="…" alt=""> — or <div class="ph"></div> —</button>
|
|
2982
|
+
<div class="postcard__band">
|
|
2983
|
+
<div class="postcard__words"><span class="t-h1">ป้าแจ๋ว</span><span class="t-body">บ้านสวนผัก</span></div>
|
|
2984
|
+
<button class="icon-btn icon-btn--invert" aria-label="ถูกใจ" aria-pressed="false">♥</button>
|
|
2985
|
+
</div>
|
|
2986
|
+
</div>
|
|
2987
|
+
<div class="postcard__face postcard__back">
|
|
2988
|
+
<button class="postcard__turn" type="button" aria-label="พลิกดูด้านหน้า">
|
|
2989
|
+
<div class="postcard__words">…</div><p class="postcard__caption">ชอบชงกาแฟดริปตอนเช้า</p>
|
|
2990
|
+
</button>
|
|
2991
|
+
<div class="postcard__voice"><div class="progress"></div><span class="t-caption">0:07 / 0:12</span><button class="icon-btn icon-btn--invert" aria-label="ฟัง">▶</button></div> (a voice note: no caption — the line, its time, the control under them, at the middle)
|
|
2992
|
+
</div>
|
|
2959
2993
|
</div>
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2994
|
+
The two faces share one box and turn (a Y rotation at --motion-flip, a plain
|
|
2995
|
+
swap under prefers-reduced-motion). A face is a div: the TURN is the button
|
|
2996
|
+
that fills it (the photo, or the caption), the band lies over its foot and
|
|
2997
|
+
turns with it — words and the heart; the back sets the name a fifth of the way down, the words after it; a
|
|
2998
|
+
button in the band is never inside the turn button. The size is the cover
|
|
2999
|
+
card's (--postcard-min-h = the cover's min-height, the card radius). Who
|
|
3000
|
+
liked it, a voice note, a reply live OUTSIDE the postcard, in a Row under it. */
|
|
2966
3001
|
.postcard {
|
|
2967
3002
|
position: relative;
|
|
2968
|
-
|
|
3003
|
+
min-height: var(--postcard-min-h, 22.5rem);
|
|
2969
3004
|
perspective: 60rem;
|
|
2970
|
-
border-radius: var(--postcard-radius, var(--r-
|
|
3005
|
+
border-radius: var(--postcard-radius, var(--r-card));
|
|
3006
|
+
isolation: isolate;
|
|
2971
3007
|
}
|
|
2972
3008
|
.postcard__face {
|
|
2973
3009
|
position: absolute;
|
|
2974
3010
|
inset: 0;
|
|
2975
3011
|
display: grid;
|
|
2976
3012
|
align-content: end;
|
|
2977
|
-
padding: var(--postcard-face-pad, var(--sp-4));
|
|
2978
|
-
gap: var(--postcard-face-gap, var(--sp-2));
|
|
2979
3013
|
border: var(--border-w) solid var(--postcard-face-border, var(--card-border));
|
|
2980
3014
|
border-radius: inherit;
|
|
2981
3015
|
overflow: hidden;
|
|
2982
3016
|
isolation: isolate;
|
|
2983
|
-
color: inherit;
|
|
2984
|
-
text-align: left;
|
|
2985
3017
|
backface-visibility: hidden;
|
|
2986
3018
|
-webkit-backface-visibility: hidden;
|
|
2987
3019
|
transition: transform var(--motion-flip) ease-in-out;
|
|
2988
3020
|
box-shadow: var(--postcard-face-shadow, var(--shadow-float));
|
|
2989
3021
|
}
|
|
2990
|
-
|
|
2991
|
-
|
|
3022
|
+
/* the turn: the button that fills the face, under the band */
|
|
3023
|
+
.postcard__turn {
|
|
3024
|
+
position: absolute;
|
|
3025
|
+
inset: 0;
|
|
3026
|
+
display: grid;
|
|
3027
|
+
align-content: start;
|
|
3028
|
+
padding: var(--postcard-face-pad, var(--card-pad-rest));
|
|
3029
|
+
border: 0;
|
|
3030
|
+
border-radius: inherit;
|
|
3031
|
+
background: transparent;
|
|
3032
|
+
color: inherit;
|
|
3033
|
+
text-align: left;
|
|
3034
|
+
cursor: pointer;
|
|
3035
|
+
}
|
|
3036
|
+
.postcard__turn:focus-visible { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
|
|
3037
|
+
/* the front: the photo (or the wireframe box) fills it; the band reads on a scrim */
|
|
2992
3038
|
.postcard__front { background: var(--postcard-front-bg, var(--card-3-bg)); color: var(--postcard-front-ink, var(--card-3-ink)); }
|
|
2993
|
-
.postcard__front > img,
|
|
2994
|
-
.postcard__front > .ph {
|
|
3039
|
+
.postcard__front .postcard__turn > img,
|
|
3040
|
+
.postcard__front .postcard__turn > .ph {
|
|
2995
3041
|
position: absolute;
|
|
2996
3042
|
inset: 0;
|
|
2997
3043
|
width: 100%;
|
|
2998
3044
|
height: 100%;
|
|
2999
3045
|
object-fit: cover;
|
|
3000
|
-
z-index: -2;
|
|
3001
3046
|
border: 0;
|
|
3002
3047
|
border-radius: 0;
|
|
3003
3048
|
min-height: 0;
|
|
@@ -3006,20 +3051,40 @@ button { font: inherit; cursor: pointer; }
|
|
|
3006
3051
|
content: "";
|
|
3007
3052
|
position: absolute;
|
|
3008
3053
|
inset: 0;
|
|
3009
|
-
z-index:
|
|
3054
|
+
z-index: 1;
|
|
3055
|
+
pointer-events: none;
|
|
3010
3056
|
background: linear-gradient(to top, var(--scrim), transparent 45%);
|
|
3011
3057
|
}
|
|
3012
|
-
/* the back: paper — the
|
|
3058
|
+
/* the back: paper — the caption and the name packed at the middle, big enough to read at arm's length */
|
|
3013
3059
|
.postcard__back {
|
|
3014
3060
|
background: var(--postcard-back-bg, var(--card-2-bg));
|
|
3015
3061
|
color: var(--postcard-back-ink, var(--card-2-ink));
|
|
3016
|
-
align-content: space-between;
|
|
3017
3062
|
transform: rotateY(180deg);
|
|
3018
3063
|
}
|
|
3064
|
+
.postcard__back .postcard__turn { align-content: start; padding-top: var(--postcard-back-top, 20%); gap: var(--postcard-back-gap, var(--sp-5)); } /* the name a fifth down the paper, the words after it */
|
|
3019
3065
|
.postcard__caption { margin: 0; font: var(--postcard-caption-font, var(--type-title)); text-wrap: pretty; }
|
|
3020
|
-
|
|
3066
|
+
/* the band: the face's foot — name and line left, the heart right; in flow at the face's end, over the turn.
|
|
3067
|
+
Taps between its words fall through to the turn; its buttons take their own. */
|
|
3068
|
+
.postcard__band {
|
|
3069
|
+
position: relative;
|
|
3070
|
+
z-index: 2;
|
|
3071
|
+
display: flex;
|
|
3072
|
+
align-items: end;
|
|
3073
|
+
gap: var(--postcard-band-gap, var(--sp-3));
|
|
3074
|
+
padding: var(--postcard-face-pad, var(--card-pad-rest));
|
|
3075
|
+
pointer-events: none;
|
|
3076
|
+
}
|
|
3077
|
+
.postcard__band > * { pointer-events: auto; }
|
|
3078
|
+
.postcard__words { display: grid; gap: var(--postcard-words-gap, var(--sp-1)); flex: 1; min-width: 0; pointer-events: none; }
|
|
3079
|
+
.postcard__front .postcard__band { --accent: var(--postcard-front-ink, var(--card-3-ink)); --on-accent: var(--postcard-front-bg, var(--card-3-bg)); }
|
|
3080
|
+
.postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
|
|
3021
3081
|
.postcard.is-flipped > .postcard__front { transform: rotateY(-180deg); }
|
|
3022
3082
|
.postcard.is-flipped > .postcard__back { transform: rotateY(0); }
|
|
3083
|
+
/* a voice note on the back: no words to read — its timeline (a Progress), its length and the control to hear it
|
|
3084
|
+
under them, at the middle over the turn, on the face's inset; the app plays and keeps the time */
|
|
3085
|
+
.postcard__voice { position: absolute; inset: 0; z-index: 2; display: grid; align-content: center; justify-items: center; gap: var(--postcard-voice-gap, var(--sp-3)); padding: var(--postcard-face-pad, var(--card-pad-rest)); pointer-events: none; }
|
|
3086
|
+
.postcard__voice > * { pointer-events: auto; }
|
|
3087
|
+
.postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
|
|
3023
3088
|
@media (prefers-reduced-motion: reduce) {
|
|
3024
3089
|
.postcard__face { transition: none; }
|
|
3025
3090
|
}
|
|
@@ -3032,8 +3097,8 @@ button { font: inherit; cursor: pointer; }
|
|
|
3032
3097
|
lives in sheet.css; these compose with .sheet-stage.
|
|
3033
3098
|
============================================================ */
|
|
3034
3099
|
|
|
3035
|
-
/* ----
|
|
3036
|
-
<div class="sheet-stage sheet-stage--
|
|
3100
|
+
/* ---- the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
|
|
3101
|
+
<div class="sheet-stage sheet-stage--dim"> (dim = the page under a scrim; a Modal on a clean ground is the stage without it — Lh 2026-09-22, `--ask` retired: the stage does the ground, the modal centres itself)
|
|
3037
3102
|
<article class="card modal">
|
|
3038
3103
|
<div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
|
|
3039
3104
|
<div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
|
|
@@ -3044,30 +3109,27 @@ button { font: inherit; cursor: pointer; }
|
|
|
3044
3109
|
.modal is the card — no border (the shadow is its edge on the scrim);
|
|
3045
3110
|
.modal__lift the light part that holds the thing being asked about (it
|
|
3046
3111
|
scrolls when tall); .modal__drawer the dark end that asks. The ground is
|
|
3047
|
-
the
|
|
3112
|
+
whatever the stage shows (dim it or not). Two beats (2 × --motion-sheet) when
|
|
3048
3113
|
the stage appears, and the same two backwards when it closes:
|
|
3049
3114
|
1. the card drops in from above the stage to the screen's centre — its
|
|
3050
3115
|
drawer still inside it, height 0: nothing of the ask shows yet
|
|
3051
3116
|
2. the drawer comes out of the card's bottom edge while the lift moves
|
|
3052
3117
|
up, both at once: the card grows from its centre, which never moves
|
|
3053
|
-
(the
|
|
3118
|
+
(the modal centres itself — absolute, margin auto; a growing card keeps its middle where it is)
|
|
3054
3119
|
Closing: a click on any button in the drawer plays the same animations in
|
|
3055
3120
|
reverse (cardds.js: Animation.reverse() on each — nothing duplicated), then
|
|
3056
3121
|
fires "cardds:modal" on the .sheet-stage (bubbles, detail.answer from the
|
|
3057
3122
|
button's data-answer) for the app to remove the stage.
|
|
3058
3123
|
Reduced motion: the finished state, no beats; closing fires at once. */
|
|
3059
|
-
.sheet-stage--ask {
|
|
3060
|
-
--drawer-h: 14rem;
|
|
3061
|
-
display: grid;
|
|
3062
|
-
align-content: center; /* the card at the screen's centre — and kept there as it grows */
|
|
3063
|
-
padding-inline: var(--screen-pad);
|
|
3064
|
-
}
|
|
3065
|
-
.sheet-stage--ask::after { content: ""; position: absolute; inset: 0; background: var(--scrim); } /* the page dimmed */
|
|
3066
|
-
.sheet-stage--ask > .sheet-stage__bg { filter: blur(var(--sp-2)); }
|
|
3067
3124
|
.modal {
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3125
|
+
--drawer-h: 14rem;
|
|
3126
|
+
position: absolute; /* at the stage's centre, and kept there as it grows: absolute + margin auto re-centres on every height */
|
|
3127
|
+
inset: 0;
|
|
3128
|
+
margin: auto;
|
|
3129
|
+
height: fit-content; /* inset 0 alone would stretch it to the stage: its own height, then the margins share the rest */
|
|
3130
|
+
width: calc(100% - 2 * var(--screen-pad));
|
|
3131
|
+
z-index: 2; /* over the dim stage's scrim (z 1) */
|
|
3132
|
+
max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
|
|
3071
3133
|
padding: 0; /* the parts pad themselves */
|
|
3072
3134
|
border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
|
|
3073
3135
|
display: grid;
|