@usableapp/cardds 0.4.0 → 0.4.2

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/dist/cardds.css CHANGED
@@ -492,6 +492,11 @@ button { font: inherit; cursor: pointer; }
492
492
  padding-block: var(--base-content-pad-y, var(--sp-4));
493
493
  }
494
494
  .base-content > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
495
+ /* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
496
+ the "where am I" dots under the words, centred — the same dots the centred card has under it */
497
+ .base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; max-width: 100%; object-fit: contain; justify-self: start; }
498
+ .base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
499
+ .base-content > .pager__at { justify-self: center; }
495
500
  .base-content .cta-pack {
496
501
  display: flex;
497
502
  align-items: start; /* buttons keep their own height, never the row's */
@@ -839,7 +844,7 @@ button { font: inherit; cursor: pointer; }
839
844
  <div class="sheet-stack sheet-stack--tap"> (drop --tap for a static stack)
840
845
  <article class="card card--2">
841
846
  <div class="card__head">
842
- <button class="icon-btn icon-btn--sm card__back" aria-label="Back">…</button>
847
+ <button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
843
848
  <h2 class="t-h2 t-caps">Shopping</h2>
844
849
  <span class="icon-row">…</span> (optional trailing icons)
845
850
  </div>
@@ -1063,7 +1068,7 @@ button { font: inherit; cursor: pointer; }
1063
1068
  .sheet-stack--tap > .card.is-open > .card__head {
1064
1069
  padding-left: calc(var(--tap-sm) + var(--sp-3));
1065
1070
  }
1066
- .sheet-stack--tap .card__back {
1071
+ .sheet-stack--tap .sheet-stack__back {
1067
1072
  position: absolute;
1068
1073
  left: 0;
1069
1074
  top: 50%;
@@ -1072,7 +1077,7 @@ button { font: inherit; cursor: pointer; }
1072
1077
  visibility: hidden;
1073
1078
  transition: opacity 0.35s ease, visibility 0.35s;
1074
1079
  }
1075
- .sheet-stack--tap > .card.is-open .card__back {
1080
+ .sheet-stack--tap > .card.is-open .sheet-stack__back {
1076
1081
  opacity: 1;
1077
1082
  visibility: visible;
1078
1083
  }
@@ -1330,8 +1335,13 @@ button { font: inherit; cursor: pointer; }
1330
1335
  inset-inline: var(--screen-pad);
1331
1336
  top: var(--_band-centre);
1332
1337
  translate: 0 -50%;
1333
- /* no z-index: it follows the background in DOM order and the sheet, later, covers it at full */
1338
+ /* 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);
1339
+ what it holds fills it (a postcard's min-height gives way) */
1340
+ height: calc((100cqh - var(--_top, 0rem)) / 2 - 2 * var(--screen-pad));
1341
+ display: grid;
1334
1342
  }
1343
+ .sheet-stage > .sheet-stage__hero > * { min-height: 0; } /* outranks a component's own min-height (the postcard's) */
1344
+ /* no z-index on the hero: it follows the background in DOM order and the sheet, later, covers it at full */
1335
1345
 
1336
1346
  /* ---- map: the box a map engine draws in, behind a stage sheet ----
1337
1347
  <div class="sheet-stage__bg"><div class="map"><div>…the engine's canvas…</div>
@@ -1407,10 +1417,9 @@ button { font: inherit; cursor: pointer; }
1407
1417
  /* EVERY sheet has one shape (Lh, 2026-09-21): the handle and the head stay at the top and never scroll · the
1408
1418
  foot's button stands at the bottom, and ABOVE the keyboard when there is one · everything between them
1409
1419
  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 — the same token a raised sheet floats on. */
1420
+ the sheet gives it up itself, as --_lift. (A sheet ALWAYS stands on the floor — `.sheet--raised`, the one that
1421
+ floated above a footnote strip on a dim stage, was retired 2026-09-22: that is a Modal's job.) */
1411
1422
  .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
1423
  .sheet-stage__foot {
1415
1424
  position: absolute;
1416
1425
  inset-inline: 0;
@@ -2211,6 +2220,22 @@ button { font: inherit; cursor: pointer; }
2211
2220
  .bar--on::after { background: var(--accent); }
2212
2221
  .bar--on > span { background: var(--accent); color: var(--on-accent); padding: var(--sp-0) var(--sp-2); border-radius: var(--r-chip); }
2213
2222
 
2223
+ /* ---- progress: one straight line, the done share solid, the rest faint (Lh 2026-09-22) — a voice note's
2224
+ timeline, a download, a step count as a line. <div class="progress" role="progressbar" style="--progress-at: 0.4"></div>
2225
+ --progress-at is DATA the component writes (0–1), not a knob (scripts/knobs.mjs lists it). The done part is a
2226
+ gradient of the line's ink sized by the share; the rest is the background colour. */
2227
+ .progress {
2228
+ display: block;
2229
+ width: 100%;
2230
+ height: var(--progress-h, var(--sp-1));
2231
+ border-radius: var(--progress-radius, var(--r-chip));
2232
+ color: var(--progress-done-ink, inherit);
2233
+ background-color: var(--progress-rest-bg, color-mix(in srgb, currentColor 25%, transparent));
2234
+ background-image: linear-gradient(currentColor, currentColor);
2235
+ background-size: calc(var(--progress-at, 0) * 100%) 100%;
2236
+ background-repeat: no-repeat;
2237
+ }
2238
+
2214
2239
  /* ---- css/people.css — avatar, avatar stack, chooser row ---- */
2215
2240
  /* ============================================================
2216
2241
  cardds/people.css — avatars: single, stacked, chooser row.
@@ -2625,6 +2650,15 @@ button { font: inherit; cursor: pointer; }
2625
2650
  .row__end { margin-left: auto; text-align: right; display: grid; gap: var(--row-end-gap, var(--sp-0)); justify-items: end; flex: none; }
2626
2651
  .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
2652
  .row--lg .row__title { font: 700 var(--fs-lg)/1.2 var(--font-display); }
2653
+ /* a row that EXPANDS (Lh 2026-09-22): the row is a <details>' summary, what it shows comes out under it —
2654
+ who liked a postcard, the rest of a list. An end chevron turns when open. The app owns the list.
2655
+ <details class="row-expand"><summary class="row">…lead · body · end…</summary><div class="row-expand__more">…</div></details> */
2656
+ .row-expand { display: block; }
2657
+ .row-expand > summary { list-style: none; cursor: pointer; }
2658
+ .row-expand > summary::-webkit-details-marker { display: none; }
2659
+ .row-expand > summary > .icon:last-child { transition: rotate var(--motion-slide); }
2660
+ .row-expand[open] > summary > .icon:last-child { rotate: 180deg; }
2661
+ .row-expand__more { padding-top: var(--row-expand-gap, var(--sp-3)); }
2628
2662
  /* a finished item in a checklist (a to-do list, an onboarding "complete your introduction" list):
2629
2663
  the words go muted, a filled tick sits at the end, and the row is no longer a tap target —
2630
2664
  pointer-events off here, aria-disabled from the markup. The tick is the .check's checked look
@@ -2945,59 +2979,75 @@ button { font: inherit; cursor: pointer; }
2945
2979
  .mosaic > .ph { opacity: 0.4; border-width: var(--border-w); }
2946
2980
  .mosaic > img { width: 100%; height: 100%; object-fit: cover; }
2947
2981
 
2948
- /* ---- postcard: a member's introduction, a card in the hand — photo in front,
2949
- words on the back; tap to turn it over (Lh, 2026-09-12; card #63) ----
2982
+ /* ---- postcard: a member's introduction in the shape of a cover card — the photo fills it,
2983
+ the band at the bottom names it; tap the photo to turn it over (Lh 2026-09-12; reshaped 2026-09-22) ----
2950
2984
  <div class="postcard">
2951
- <button class="postcard__face postcard__front" type="button" aria-label="พลิกดูด้านหลัง">
2952
- <img src="…" alt=""> — or <div class="ph"></div> —
2953
- <span class="postcard__label">ป้าแจ๋ว</span>
2954
- </button>
2955
- <button class="postcard__face postcard__back" type="button" aria-label="พลิกดูด้านหน้า">
2956
- <p class="postcard__caption">ชอบชงกาแฟดริปตอนเช้า</p>
2957
- <span class="postcard__label">ป้าแจ๋ว</span>
2958
- </button>
2985
+ <div class="postcard__face postcard__front">
2986
+ <button class="postcard__turn" type="button" aria-label="พลิกดูด้านหลัง"><img src="…" alt=""> — or <div class="ph"></div> —</button>
2987
+ <div class="postcard__band">
2988
+ <div class="postcard__words"><span class="t-h1">ป้าแจ๋ว</span><span class="t-body">บ้านสวนผัก</span></div>
2989
+ <button class="icon-btn icon-btn--invert" aria-label="ถูกใจ" aria-pressed="false">♥</button>
2990
+ </div>
2991
+ </div>
2992
+ <div class="postcard__face postcard__back">
2993
+ <button class="postcard__turn" type="button" aria-label="พลิกดูด้านหน้า">
2994
+ <div class="postcard__words">…</div><p class="postcard__caption">ชอบชงกาแฟดริปตอนเช้า</p>
2995
+ </button>
2996
+ <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)
2997
+ </div>
2959
2998
  </div>
2960
- Each face is the whole tap target; cardds.js toggles .is-flipped on the
2961
- postcard (either face, Enter/Space too). A voice note, a like, a reply live
2962
- OUTSIDE the postcard (a Row under it): a face is a button, and a button
2963
- holds no button. The faces share one box (a 3:2 landscape card, the media
2964
- radius); the turn is a Y rotation at --motion-flip, a plain swap under
2965
- prefers-reduced-motion. Reads ink from the card slot it sits in. */
2999
+ The two faces share one box and turn (a Y rotation at --motion-flip, a plain
3000
+ swap under prefers-reduced-motion). A face is a div: the TURN is the button
3001
+ that fills it (the photo, or the caption), the band lies over its foot and
3002
+ turns with it — words and the heart; the back sets the name a fifth of the way down, the words after it; a
3003
+ button in the band is never inside the turn button. The size is the cover
3004
+ card's (--postcard-min-h = the cover's min-height, the card radius). Who
3005
+ liked it, a voice note, a reply live OUTSIDE the postcard, in a Row under it. */
2966
3006
  .postcard {
2967
3007
  position: relative;
2968
- aspect-ratio: 3 / 2;
3008
+ min-height: var(--postcard-min-h, 22.5rem);
2969
3009
  perspective: 60rem;
2970
- border-radius: var(--postcard-radius, var(--r-media));
3010
+ border-radius: var(--postcard-radius, var(--r-card));
3011
+ isolation: isolate;
2971
3012
  }
2972
3013
  .postcard__face {
2973
3014
  position: absolute;
2974
3015
  inset: 0;
2975
3016
  display: grid;
2976
3017
  align-content: end;
2977
- padding: var(--postcard-face-pad, var(--sp-4));
2978
- gap: var(--postcard-face-gap, var(--sp-2));
2979
3018
  border: var(--border-w) solid var(--postcard-face-border, var(--card-border));
2980
3019
  border-radius: inherit;
2981
3020
  overflow: hidden;
2982
3021
  isolation: isolate;
2983
- color: inherit;
2984
- text-align: left;
2985
3022
  backface-visibility: hidden;
2986
3023
  -webkit-backface-visibility: hidden;
2987
3024
  transition: transform var(--motion-flip) ease-in-out;
2988
3025
  box-shadow: var(--postcard-face-shadow, var(--shadow-float));
2989
3026
  }
2990
- .postcard__face:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
2991
- /* the front: the photo (or the wireframe box) fills it, the name reads on a scrim */
3027
+ /* the turn: the button that fills the face, under the band */
3028
+ .postcard__turn {
3029
+ position: absolute;
3030
+ inset: 0;
3031
+ display: grid;
3032
+ align-content: start;
3033
+ padding: var(--postcard-face-pad, var(--card-pad-rest));
3034
+ border: 0;
3035
+ border-radius: inherit;
3036
+ background: transparent;
3037
+ color: inherit;
3038
+ text-align: left;
3039
+ cursor: pointer;
3040
+ }
3041
+ .postcard__turn:focus-visible { outline: var(--focus); outline-offset: calc(-1 * var(--sp-1)); }
3042
+ /* the front: the photo (or the wireframe box) fills it; the band reads on a scrim */
2992
3043
  .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 {
3044
+ .postcard__front .postcard__turn > img,
3045
+ .postcard__front .postcard__turn > .ph {
2995
3046
  position: absolute;
2996
3047
  inset: 0;
2997
3048
  width: 100%;
2998
3049
  height: 100%;
2999
3050
  object-fit: cover;
3000
- z-index: -2;
3001
3051
  border: 0;
3002
3052
  border-radius: 0;
3003
3053
  min-height: 0;
@@ -3006,20 +3056,40 @@ button { font: inherit; cursor: pointer; }
3006
3056
  content: "";
3007
3057
  position: absolute;
3008
3058
  inset: 0;
3009
- z-index: -1;
3059
+ z-index: 1;
3060
+ pointer-events: none;
3010
3061
  background: linear-gradient(to top, var(--scrim), transparent 45%);
3011
3062
  }
3012
- /* the back: paper — the words, big enough to read at arm's length */
3063
+ /* the back: paper — the caption and the name packed at the middle, big enough to read at arm's length */
3013
3064
  .postcard__back {
3014
3065
  background: var(--postcard-back-bg, var(--card-2-bg));
3015
3066
  color: var(--postcard-back-ink, var(--card-2-ink));
3016
- align-content: space-between;
3017
3067
  transform: rotateY(180deg);
3018
3068
  }
3069
+ .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
3070
  .postcard__caption { margin: 0; font: var(--postcard-caption-font, var(--type-title)); text-wrap: pretty; }
3020
- .postcard__label { font: var(--postcard-label-font, var(--type-overline)); letter-spacing: var(--track-overline); text-transform: uppercase; opacity: 0.85; }
3071
+ /* the band: the face's foot — name and line left, the heart right; in flow at the face's end, over the turn.
3072
+ Taps between its words fall through to the turn; its buttons take their own. */
3073
+ .postcard__band {
3074
+ position: relative;
3075
+ z-index: 2;
3076
+ display: flex;
3077
+ align-items: end;
3078
+ gap: var(--postcard-band-gap, var(--sp-3));
3079
+ padding: var(--postcard-face-pad, var(--card-pad-rest));
3080
+ pointer-events: none;
3081
+ }
3082
+ .postcard__band > * { pointer-events: auto; }
3083
+ .postcard__words { display: grid; gap: var(--postcard-words-gap, var(--sp-1)); flex: 1; min-width: 0; pointer-events: none; }
3084
+ .postcard__front .postcard__band { --accent: var(--postcard-front-ink, var(--card-3-ink)); --on-accent: var(--postcard-front-bg, var(--card-3-bg)); }
3085
+ .postcard__band .icon-btn[aria-pressed="true"] .icon { fill: currentColor; }
3021
3086
  .postcard.is-flipped > .postcard__front { transform: rotateY(-180deg); }
3022
3087
  .postcard.is-flipped > .postcard__back { transform: rotateY(0); }
3088
+ /* a voice note on the back: no words to read — its timeline (a Progress), its length and the control to hear it
3089
+ under them, at the middle over the turn, on the face's inset; the app plays and keeps the time */
3090
+ .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; }
3091
+ .postcard__voice > * { pointer-events: auto; }
3092
+ .postcard__voice > .progress { justify-self: stretch; pointer-events: none; }
3023
3093
  @media (prefers-reduced-motion: reduce) {
3024
3094
  .postcard__face { transition: none; }
3025
3095
  }
@@ -3032,8 +3102,8 @@ button { font: inherit; cursor: pointer; }
3032
3102
  lives in sheet.css; these compose with .sheet-stage.
3033
3103
  ============================================================ */
3034
3104
 
3035
- /* ---- ask: the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
3036
- <div class="sheet-stage sheet-stage--ask">
3105
+ /* ---- the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
3106
+ <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
3107
  <article class="card modal">
3038
3108
  <div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
3039
3109
  <div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
@@ -3044,30 +3114,27 @@ button { font: inherit; cursor: pointer; }
3044
3114
  .modal is the card — no border (the shadow is its edge on the scrim);
3045
3115
  .modal__lift the light part that holds the thing being asked about (it
3046
3116
  scrolls when tall); .modal__drawer the dark end that asks. The ground is
3047
- the page under a scrim (like --dim). Two beats (2 × --motion-sheet) when
3117
+ whatever the stage shows (dim it or not). Two beats (2 × --motion-sheet) when
3048
3118
  the stage appears, and the same two backwards when it closes:
3049
3119
  1. the card drops in from above the stage to the screen's centre — its
3050
3120
  drawer still inside it, height 0: nothing of the ask shows yet
3051
3121
  2. the drawer comes out of the card's bottom edge while the lift moves
3052
3122
  up, both at once: the card grows from its centre, which never moves
3053
- (the stage centres it; a growing card keeps its middle where it is)
3123
+ (the modal centres itself — absolute, margin auto; a growing card keeps its middle where it is)
3054
3124
  Closing: a click on any button in the drawer plays the same animations in
3055
3125
  reverse (cardds.js: Animation.reverse() on each — nothing duplicated), then
3056
3126
  fires "cardds:modal" on the .sheet-stage (bubbles, detail.answer from the
3057
3127
  button's data-answer) for the app to remove the stage.
3058
3128
  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
3129
  .modal {
3068
- position: relative;
3069
- z-index: 1; /* over the scrim */
3070
- max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) — a % here would be of the grid area, which is the card itself */
3130
+ --drawer-h: 14rem;
3131
+ position: absolute; /* at the stage's centre, and kept there as it grows: absolute + margin auto re-centres on every height */
3132
+ inset: 0;
3133
+ margin: auto;
3134
+ height: fit-content; /* inset 0 alone would stretch it to the stage: its own height, then the margins share the rest */
3135
+ width: calc(100% - 2 * var(--screen-pad));
3136
+ z-index: 2; /* over the dim stage's scrim (z 1) */
3137
+ max-height: calc(100cqh - 2 * var(--sp-5)); /* cqh: the stage (a size container) */
3071
3138
  padding: 0; /* the parts pad themselves */
3072
3139
  border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
3073
3140
  display: grid;
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ export * from './cards/CardParts.js';
20
20
  export * from './cards/Stat.js';
21
21
  export * from './cards/Placeholder.js';
22
22
  export * from './sheets/SheetStack.js';
23
- export * from './sheets/CardBack.js';
23
+ export * from './sheets/StackBack.js';
24
24
  export * from './sheets/Sheet.js';
25
25
  export * from './sheets/SheetHead.js';
26
26
  export * from './sheets/SheetBody.js';
@@ -47,6 +47,7 @@ export * from './choice/Calendar.js';
47
47
  export * from './choice/Mood.js';
48
48
  export * from './choice/Slider.js';
49
49
  export * from './numbers/Ring.js';
50
+ export * from './numbers/Progress.js';
50
51
  export * from './numbers/Track.js';
51
52
  export * from './numbers/Band.js';
52
53
  export * from './numbers/DotGrid.js';
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ export * from './cards/CardParts.js';
23
23
  export * from './cards/Stat.js';
24
24
  export * from './cards/Placeholder.js';
25
25
  export * from './sheets/SheetStack.js';
26
- export * from './sheets/CardBack.js';
26
+ export * from './sheets/StackBack.js';
27
27
  export * from './sheets/Sheet.js';
28
28
  export * from './sheets/SheetHead.js';
29
29
  export * from './sheets/SheetBody.js';
@@ -50,6 +50,7 @@ export * from './choice/Calendar.js';
50
50
  export * from './choice/Mood.js';
51
51
  export * from './choice/Slider.js';
52
52
  export * from './numbers/Ring.js';
53
+ export * from './numbers/Progress.js';
53
54
  export * from './numbers/Track.js';
54
55
  export * from './numbers/Band.js';
55
56
  export * from './numbers/DotGrid.js';