@usableapp/cardds 0.1.4 → 0.1.6

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/css/step.css CHANGED
@@ -1,11 +1,11 @@
1
1
  /* ============================================================
2
2
  cardds/step.css — .step: one screen, one task.
3
- What the member reads (a card), what the member does when the
3
+ What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
4
4
  step asks for it (a sheet, sheet.css), and the screen's one
5
5
  move (.action-bar, actions.css) OUTSIDE the step, below it.
6
6
 
7
7
  <section class="step">
8
- <article class="card step__card">…the words…</article> ← first
8
+ <section class="base-content">…the words…</section> ← first
9
9
  <article class="card sheet sheet--half">…the controls…</article> ← last, optional
10
10
  </section>
11
11
  <div class="action-bar">…</div>
@@ -59,10 +59,21 @@
59
59
  frame — the bar is fixed to the viewport wherever it sits. */
60
60
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
61
61
 
62
- /* the reading card: a plain card with a flex rule — it hugs its content,
63
- and when the column runs out (a short phone, large text) it shrinks and
64
- scrolls inside itself instead of pushing the bar or the sheet */
65
- .step__card {
62
+ /* the reading block: a .base-content (base.css) — the same words-on-the-base
63
+ block as anywhere else (it was .step__card, and a .card before 2026-09-16;
64
+ Lh: a step reads like a page, not a card in a page — so it IS the page's
65
+ block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
66
+ between things, the tokens .card publishes for the head and .bleed) and
67
+ gets the flex rule: it hugs its content, and when the column runs out (a
68
+ short phone, large text) it shrinks and scrolls inside itself instead of
69
+ pushing the bar or the sheet */
70
+ .step > .base-content {
71
+ --card-pad: 0rem; /* published for .bleed: nothing to cancel */
72
+ --card-gap: var(--sp-4);
73
+ gap: var(--card-gap);
74
+ padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
75
+ align-content: start;
76
+ color: var(--on-surface);
66
77
  flex: 0 1 auto;
67
78
  min-height: 0;
68
79
  margin: var(--_lead, 0) var(--screen-pad) 0; /* the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
package/css/tokens.css CHANGED
@@ -176,3 +176,4 @@
176
176
  ============================================================ */
177
177
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
178
178
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
179
+ @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
package/dist/cardds.css CHANGED
@@ -217,6 +217,7 @@
217
217
  ============================================================ */
218
218
  @property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
219
219
  @property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
220
+ @property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
220
221
 
221
222
  /* ---- css/palette.css — color settings — the file you tune ---- */
222
223
  /* ============================================================
@@ -338,6 +339,17 @@
338
339
  ============================================================ */
339
340
 
340
341
  *, *::before, *::after { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
342
+ /* a control is a thing you press, not a thing you read: a long press never
343
+ raises the text-selection bar on it (Lh 2026-09-16). CONTROLS ONLY — words
344
+ in cards, rows and bodies stay selectable (an address, a code, a number
345
+ the member wants to copy), and inputs are untouched. Native buttons and
346
+ links are matched by element; the rest by the class that makes them one */
347
+ button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn, a.icon-btn, a.chip, label.chip, .chip--toggle, .chip--pick,
348
+ .segment__item, .day, .calendar__day, .mood__opt, .pin__cell, .slider, .picker__item, .float-bar__item, .menu__item, .sheet__handle, .dropdown__trigger, .track__step, .pager__at {
349
+ user-select: none;
350
+ -webkit-user-select: none;
351
+ -webkit-touch-callout: none;
352
+ }
341
353
 
342
354
  /* the scale: everything is rem, so this one number resizes the whole system.
343
355
  100% = the user's text size (16px by default); narrow phones step to 15/16 so
@@ -384,6 +396,7 @@ button { font: inherit; cursor: pointer; }
384
396
  padding-inline: var(--screen-pad);
385
397
  display: grid;
386
398
  gap: var(--screen-gap);
399
+ align-content: start; /* a screen taller than its content (a fixed phone) never spreads its rows — the top bar stays at the top, a row keeps its height */
387
400
  }
388
401
  .screen > * { min-width: 0; } /* content never widens the column past the viewport */
389
402
  /* a screen that carries its own palette (data-palette on the element, not on <html>)
@@ -430,15 +443,20 @@ button { font: inherit; cursor: pointer; }
430
443
  .t-caps { text-transform: uppercase; }
431
444
  .grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
432
445
 
433
- /* ---- section: frameless on-surface content + CTA (never a card) ---- */
434
- .section {
446
+ /* ---- base content: words straight on the base, no card around them — an
447
+ overline, a heading, a paragraph, maybe a .cta-pack. Was .section until
448
+ 2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
449
+ In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
450
+ .base-content {
435
451
  display: grid;
436
452
  gap: var(--sp-3);
453
+ align-content: start; /* a tall column never stretches the rows — the words hug the top */
437
454
  padding-block: var(--sp-4);
438
455
  }
439
- .section > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
440
- .section .cta-pack {
456
+ .base-content > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
457
+ .base-content .cta-pack {
441
458
  display: flex;
459
+ align-items: start; /* buttons keep their own height, never the row's */
442
460
  gap: var(--sp-2);
443
461
  margin-top: var(--sp-2);
444
462
  flex-wrap: wrap;
@@ -451,15 +469,12 @@ button { font: inherit; cursor: pointer; }
451
469
  text-align: center;
452
470
  gap: var(--sp-2);
453
471
  padding: var(--sp-12) var(--sp-6);
454
- color: var(--on-surface-muted);
472
+ color: var(--on-surface); /* full ink, icon and words alike: muted read as "disabled" at the middle of an empty screen (Lh 2026-09-16) — muted is for meta, not a sentence */
455
473
  }
456
- .empty-state > :is(h1, h2, h3) { color: var(--on-surface); } /* the heading in full ink, the words muted */
457
474
  .empty-state__action { margin-top: var(--sp-2); }
458
- /* inside a card (a section's "nothing yet"): the card's ink, the way .t-muted and .row__meta follow it */
459
- .card .empty-state { color: var(--card-muted); padding-block: var(--sp-6); }
460
- .card .empty-state > :is(h1, h2, h3) { color: var(--card-ink); }
475
+ /* inside a card (a section's "nothing yet"): the card's ink */
476
+ .card .empty-state { color: var(--card-ink); padding-block: var(--sp-6); }
461
477
  .card--3 .empty-state { color: inherit; }
462
- .card--3 .empty-state > :not(:is(h1, h2, h3)) { opacity: 0.7; }
463
478
 
464
479
  /* ---- css/card.css — card boundary + list variants ---- */
465
480
  /* ============================================================
@@ -1020,15 +1035,15 @@ button { font: inherit; cursor: pointer; }
1020
1035
  /* ---- css/sheet.css — sheet single: .sheet — one card as a bottom sheet, peek / half / full ---- */
1021
1036
  /* ============================================================
1022
1037
  cardds/sheet.css — sheet single: one card as a bottom sheet.
1023
- Three states: .sheet--peek · .sheet--half · .sheet--full
1038
+ Four states: .sheet--peek · .sheet--half · .sheet--3q · .sheet--full
1024
1039
 
1025
1040
  A sheet is rigid material, exactly like a sheet in .sheet-stack--tap
1026
1041
  (stack.css): it is ALWAYS the full height of the box that holds it —
1027
1042
  set once by inset: 0, never written again, never transitioned — and
1028
1043
  a state is nothing but how far that full card has slid up. Peek is
1029
1044
  a full sheet that slid down until only its handle and title show;
1030
- half is one that slid down by half of itself; full is one that has
1031
- not slid at all. The box clips whatever slid below its bottom edge,
1045
+ half is one that slid down by half of itself; 3q (three quarters,
1046
+ Lh 2026-09-16) by a quarter; full is one that has not slid at all. The box clips whatever slid below its bottom edge,
1032
1047
  so a sheet at any height is still a card: rounded top, a straight
1033
1048
  cut at the box's edge, its content scrolling inside.
1034
1049
 
@@ -1048,7 +1063,11 @@ button { font: inherit; cursor: pointer; }
1048
1063
  page. Many sheets stacked as the app's menu are the other system,
1049
1064
  .sheet-stack (stack.css): the same material, its own class and file.
1050
1065
 
1051
- <article class="card sheet sheet--half" data-sheet-states="peek half full">
1066
+ <article class="card sheet sheet--half" data-sheet-states="peek half 3q full">
1067
+ A tap on the handle TOGGLES the sheet between its own state and peek
1068
+ (Lh 2026-09-16) — never a step-by-step climb; a drag snaps to any of
1069
+ the listed states, and the state it lands on becomes the one a tap
1070
+ returns to.
1052
1071
  <button class="sheet__handle" type="button" aria-label="…"></button>
1053
1072
  <div class="card__head"><h2 class="t-h2">…</h2>…</div>
1054
1073
  <div class="sheet__body">…controls, or small content cards…</div>
@@ -1071,6 +1090,7 @@ button { font: inherit; cursor: pointer; }
1071
1090
  retune it: .my-sheet { --sheet-peek: … } */
1072
1091
  --sheet-peek: calc(var(--card-pad) + var(--_handle) + 2 * var(--card-gap) + var(--tap));
1073
1092
  --sheet-half: 0.5; /* a share of the sheet's own height */
1093
+ --sheet-3q: 0.25; /* three quarters showing: slid by a quarter */
1074
1094
  /* the sheet's own height, seen from inside it: the box it slides in is the
1075
1095
  size container (.step, .sheet-stage), so 100cqh is that box, less what
1076
1096
  the sheet gives up at its top (--_top) and bottom (--_lift). The sheet
@@ -1101,6 +1121,7 @@ button { font: inherit; cursor: pointer; }
1101
1121
  }
1102
1122
  .sheet--full { --_y: 0%; --_cover: 0rem; }
1103
1123
  .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
1124
+ .sheet--3q { --_y: calc(var(--sheet-3q) * 100%); --_cover: calc(var(--sheet-3q) * var(--_box)); }
1104
1125
  .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
1105
1126
  .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
1106
1127
 
@@ -1317,12 +1338,12 @@ button { font: inherit; cursor: pointer; }
1317
1338
  /* ---- css/step.css — .step: one screen, one task — card, optional sheet, bar outside ---- */
1318
1339
  /* ============================================================
1319
1340
  cardds/step.css — .step: one screen, one task.
1320
- What the member reads (a card), what the member does when the
1341
+ What the member reads (words on the base — not a card, Lh 2026-09-16), what the member does when the
1321
1342
  step asks for it (a sheet, sheet.css), and the screen's one
1322
1343
  move (.action-bar, actions.css) OUTSIDE the step, below it.
1323
1344
 
1324
1345
  <section class="step">
1325
- <article class="card step__card">…the words…</article> ← first
1346
+ <section class="base-content">…the words…</section> ← first
1326
1347
  <article class="card sheet sheet--half">…the controls…</article> ← last, optional
1327
1348
  </section>
1328
1349
  <div class="action-bar">…</div>
@@ -1376,10 +1397,21 @@ button { font: inherit; cursor: pointer; }
1376
1397
  frame — the bar is fixed to the viewport wherever it sits. */
1377
1398
  :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
1378
1399
 
1379
- /* the reading card: a plain card with a flex rule — it hugs its content,
1380
- and when the column runs out (a short phone, large text) it shrinks and
1381
- scrolls inside itself instead of pushing the bar or the sheet */
1382
- .step__card {
1400
+ /* the reading block: a .base-content (base.css) — the same words-on-the-base
1401
+ block as anywhere else (it was .step__card, and a .card before 2026-09-16;
1402
+ Lh: a step reads like a page, not a card in a page — so it IS the page's
1403
+ block, one name). Here it keeps a card's inner rhythm (a head, --card-gap
1404
+ between things, the tokens .card publishes for the head and .bleed) and
1405
+ gets the flex rule: it hugs its content, and when the column runs out (a
1406
+ short phone, large text) it shrinks and scrolls inside itself instead of
1407
+ pushing the bar or the sheet */
1408
+ .step > .base-content {
1409
+ --card-pad: 0rem; /* published for .bleed: nothing to cancel */
1410
+ --card-gap: var(--sp-4);
1411
+ gap: var(--card-gap);
1412
+ padding-block: 0; /* --_lead carries the top; the bar's room is the step's */
1413
+ align-content: start;
1414
+ color: var(--on-surface);
1383
1415
  flex: 0 1 auto;
1384
1416
  min-height: 0;
1385
1417
  margin: var(--_lead, 0) var(--screen-pad) 0; /* the screen's gap, moved in here from above the step; the inset, taken back from the bleed */
@@ -1731,35 +1763,27 @@ button { font: inherit; cursor: pointer; }
1731
1763
  .action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
1732
1764
  .action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
1733
1765
 
1734
- /* ---- pager: previous / next at the two edges of the bar ----
1766
+ /* ---- pager: previous / next as two round icon buttons at the two edges ----
1735
1767
  For a sequence you can walk BOTH ways — a three-card feature intro, a step
1736
1768
  flow. Use it instead of the default pack whenever going back is as ordinary
1737
1769
  as going on: a wide CTA beside a small round icon reads as one action plus
1738
1770
  an afterthought, which is wrong when the two are peers.
1739
1771
 
1740
- Both buttons are the same shape and the same height, so neither wins by
1741
- being physically bigger; the fill alone leans forward. The bar carries the
1742
- buttons only: where you are is said by the .pager__at dots UNDER the card
1772
+ The two are the same round shape and size (.icon-btn), one at each edge in
1773
+ thumb reach from either side; the fill (--invert) alone leans forward. They
1774
+ NEVER carry words (Lh 2026-09-16, for good): an arrow is the whole meaning,
1775
+ and a word that isn't a direction ("Get started", "Done") is not a pager
1776
+ button — that step COMMITS and goes back to the plain .action-bar with one
1777
+ wide button. Where you are is said by the .pager__at dots UNDER the card
1743
1778
  that changes (a .screen__centre group, card.css), never in the bar — the
1744
1779
  dots move with what moves (Lh, 2026-09-12). Between the buttons goes a
1745
- "skip" link or nothing. Keep the last step of a flow on the plain
1746
- .action-bar: a step that COMMITS deserves one wide button, not a pair.
1780
+ "skip" link or nothing.
1747
1781
 
1748
1782
  <div class="action-bar action-bar--pager">
1749
- <button class="btn" disabled><svg class="icon icon--xs"><use href="icons.svg#arrow-left"/></svg>Back</button>
1750
- <button class="btn btn--primary">Next<svg class="icon icon--xs"><use href="icons.svg#arrow-right"/></svg></button>
1751
- </div> */
1783
+ <button class="icon-btn" aria-label="ย้อนกลับ" disabled>…</button>
1784
+ <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1785
+ </div> — or the same two in an .action-bar__tier--pager under a control */
1752
1786
  .action-bar--pager { align-items: center; }
1753
- /* equal share of the bar, and tighter than a lone CTA so two labels plus the
1754
- indicator still fit a 320px screen */
1755
- .action-bar--pager > .btn {
1756
- flex: 1 1 0;
1757
- min-width: 0;
1758
- padding-inline: var(--sp-3);
1759
- gap: var(--sp-1);
1760
- white-space: nowrap;
1761
- overflow: hidden;
1762
- }
1763
1787
  /* the page dots: under the card in a .screen__centre group; tight gaps */
1764
1788
  .pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
1765
1789
  .pager__at > * {
@@ -1787,14 +1811,7 @@ button { font: inherit; cursor: pointer; }
1787
1811
  .action-bar__tier > .btn { flex: 1; }
1788
1812
  :has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
1789
1813
 
1790
- /* ---- round pager: previous / next as two round buttons at the two edges ----
1791
- The pager with nothing to say between its buttons — no dots, no label — as
1792
- two equal round icon buttons, one at each edge, in thumb reach from either
1793
- side. Rule 3 still holds: the step that commits goes back to one wide button.
1794
- <div class="action-bar action-bar--pager">
1795
- <button class="icon-btn" aria-label="ย้อนกลับ">…</button>
1796
- <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
1797
- </div> — or the same two in an .action-bar__tier--pager under a control */
1814
+ /* the first round button sits at the left edge, the last at the right; a skip link between them stays centred */
1798
1815
  :is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
1799
1816
 
1800
1817
  /* .screen's default bottom padding is a constant; derive the reserve from the
@@ -2834,49 +2851,94 @@ button { font: inherit; cursor: pointer; }
2834
2851
  /* ---- css/layover.css — ask drawer, popover menu, banner, float bar, callout, deck ---- */
2835
2852
  /* ============================================================
2836
2853
  cardds/layover.css — cards that lay OVER other content:
2837
- ask drawer · popover menu · expanding banner · float bar ·
2854
+ Modal (ask) · popover menu · expanding banner · float bar ·
2838
2855
  callout · swipe deck. The sheet (peek / half / full / page)
2839
2856
  lives in sheet.css; these compose with .sheet-stage.
2840
2857
  ============================================================ */
2841
2858
 
2842
- /* ---- ask drawer: the content card lifts, a dark drawer asks ----
2859
+ /* ---- ask: the Modal — the case drops in, its drawer opens below (Lh 2026-09-16: ONE card, two parts)
2843
2860
  <div class="sheet-stage sheet-stage--ask">
2844
- <article class="card lift">…page content…</article>
2845
- <div class="drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
2846
- <div class="btn-trio"><button class="icon-btn">×</button><span class="btn-trio__dots"></span><button class="icon-btn icon-btn--invert">?</button><span class="btn-trio__dots"></span><button class="icon-btn">✓</button></div>
2847
- </div></div> */
2861
+ <article class="card modal">
2862
+ <div class="modal__lift">…the card's content (a head, rows, a timeline)…</div>
2863
+ <div class="modal__drawer"><h2 class="t-h2">Take the case?</h2><p class="t-caption">This cannot be undone.</p>
2864
+ <div class="btn-trio">…</div>
2865
+ </div>
2866
+ </article>
2867
+ </div>
2868
+ .modal is the card — no border (the shadow is its edge on the scrim);
2869
+ .modal__lift the light part that holds the thing being asked about (it
2870
+ scrolls when tall); .modal__drawer the dark end that asks. The ground is
2871
+ the page under a scrim (like --dim). Two beats (2 × --motion-sheet) when
2872
+ the stage appears, and the same two backwards when it closes:
2873
+ 1. the card drops in from above the stage to the screen's centre — its
2874
+ drawer still inside it, height 0: nothing of the ask shows yet
2875
+ 2. the drawer comes out of the card's bottom edge while the lift moves
2876
+ up, both at once: the card grows from its centre, which never moves
2877
+ (the stage centres it; a growing card keeps its middle where it is)
2878
+ Closing: a click on any button in the drawer plays the same animations in
2879
+ reverse (cardds.js: Animation.reverse() on each — nothing duplicated), then
2880
+ fires "cardds:modal" on the .sheet-stage (bubbles, detail.answer from the
2881
+ button's data-answer) for the app to remove the stage.
2882
+ Reduced motion: the finished state, no beats; closing fires at once. */
2848
2883
  .sheet-stage--ask {
2849
2884
  --drawer-h: 14rem;
2850
- background: var(--card-3-bg);
2851
- color: var(--card-3-ink);
2885
+ display: grid;
2886
+ align-content: center; /* the card at the screen's centre — and kept there as it grows */
2887
+ padding-inline: var(--screen-pad);
2852
2888
  }
2853
- .lift {
2854
- position: absolute;
2855
- inset: 0 0 var(--drawer-h) 0;
2856
- border: 0;
2857
- border-radius: 0 0 var(--r-card) var(--r-card);
2858
- overflow: hidden;
2859
- overscroll-behavior: contain;
2889
+ .sheet-stage--ask::after { content: ""; position: absolute; inset: 0; background: var(--scrim); } /* the page dimmed */
2890
+ .sheet-stage--ask > .sheet-stage__bg { filter: blur(var(--sp-2)); }
2891
+ .modal {
2892
+ position: relative;
2893
+ z-index: 1; /* over the scrim */
2894
+ 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 */
2895
+ padding: 0; /* the parts pad themselves */
2896
+ border: 0; /* no frame on the modal (Lh 2026-09-16) — its edge is the shadow */
2897
+ display: grid;
2898
+ grid-template-rows: minmax(0, 1fr) auto;
2899
+ overflow: clip;
2860
2900
  box-shadow: var(--shadow-sheet);
2901
+ }
2902
+ .modal__lift {
2903
+ padding: var(--card-pad);
2904
+ display: grid;
2905
+ gap: var(--card-gap);
2861
2906
  align-content: start;
2907
+ min-height: 0;
2908
+ overflow-y: auto; /* tall content scrolls inside; the drawer stays */
2909
+ overscroll-behavior: contain;
2862
2910
  }
2863
- .drawer {
2864
- position: absolute;
2865
- inset-inline: 0;
2866
- bottom: 0;
2911
+ .modal__drawer {
2867
2912
  height: var(--drawer-h);
2913
+ box-sizing: border-box;
2868
2914
  padding: var(--sp-5);
2915
+ overflow: clip;
2869
2916
  display: grid;
2870
2917
  align-content: center;
2871
2918
  justify-items: center;
2872
2919
  gap: var(--sp-1);
2873
2920
  text-align: center;
2921
+ background: var(--card-3-bg);
2922
+ color: var(--card-3-ink);
2874
2923
  --accent: var(--card-3-ink); /* the one solid button inverts on the dark drawer */
2875
2924
  --on-accent: var(--card-3-bg);
2876
2925
  }
2877
- .drawer .t-caption { opacity: 0.7; }
2878
- .drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
2879
- .drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
2926
+ .modal__drawer .t-caption { opacity: 0.7; }
2927
+ .modal__drawer .icon-btn { background: transparent; color: inherit; border-color: var(--ink-rail); }
2928
+ .modal__drawer .icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; width: var(--tap-lg); height: var(--tap-lg); }
2929
+ @media (prefers-reduced-motion: no-preference) {
2930
+ .modal { animation: modal-drop calc(2 * var(--motion-sheet)) ease both; }
2931
+ .modal__drawer { animation: modal-open calc(2 * var(--motion-sheet)) ease both; }
2932
+ @keyframes modal-drop {
2933
+ 0% { translate: 0 -100cqh; } /* above the stage */
2934
+ 50%, 100% { translate: 0 0; } /* 1. at the centre — and it stays: beat 2 is the drawer's */
2935
+ }
2936
+ @keyframes modal-open {
2937
+ 0%, 50% { height: 0; padding-block: 0; } /* inside the card */
2938
+ 100% { height: var(--drawer-h); padding-block: var(--sp-5); } /* 2. out below, the lift up, together */
2939
+ }
2940
+ }
2941
+
2880
2942
  .btn-trio { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-3); }
2881
2943
  .btn-trio__dots {
2882
2944
  width: var(--sp-5);
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export * from './scaffold/ActionBar.js';
10
10
  export * from './scaffold/PagerAt.js';
11
11
  export * from './scaffold/FloatBar.js';
12
12
  export * from './scaffold/FilterRow.js';
13
- export * from './scaffold/Section.js';
13
+ export * from './scaffold/BaseContent.js';
14
14
  export * from './scaffold/EmptyState.js';
15
15
  export * from './scaffold/Centre.js';
16
16
  export * from './cards/Card.js';
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ export * from './scaffold/ActionBar.js';
13
13
  export * from './scaffold/PagerAt.js';
14
14
  export * from './scaffold/FloatBar.js';
15
15
  export * from './scaffold/FilterRow.js';
16
- export * from './scaffold/Section.js';
16
+ export * from './scaffold/BaseContent.js';
17
17
  export * from './scaffold/EmptyState.js';
18
18
  export * from './scaffold/Centre.js';
19
19
  export * from './cards/Card.js';
@@ -0,0 +1,14 @@
1
+ import type { ComponentPropsWithoutRef } from 'react';
2
+ export interface BaseContentProps extends ComponentPropsWithoutRef<'section'> {
3
+ }
4
+ /**
5
+ * BaseContent — words straight on the base, no card around them: an overline, a heading, a paragraph,
6
+ * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
7
+ * Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
8
+ * In a Step it is the reading block (first child; the buttons go to the ActionBar, not a CtaPack).
9
+ */
10
+ export declare function BaseContent({ className, ...rest }: BaseContentProps): import("react").JSX.Element;
11
+ export interface CtaPackProps extends ComponentPropsWithoutRef<'div'> {
12
+ }
13
+ /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. Outside a Step only: a Step's move is its ActionBar. */
14
+ export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from '../cx.js';
3
+ /**
4
+ * BaseContent — words straight on the base, no card around them: an overline, a heading, a paragraph,
5
+ * maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
6
+ * Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
7
+ * In a Step it is the reading block (first child; the buttons go to the ActionBar, not a CtaPack).
8
+ */
9
+ export function BaseContent({ className, ...rest }) {
10
+ return _jsx("section", { className: cx('base-content', className), ...rest });
11
+ }
12
+ /** CtaPack — the buttons under a BaseContent's words, wrapping on one line. Outside a Step only: a Step's move is its ActionBar. */
13
+ export function CtaPack({ className, ...rest }) {
14
+ return _jsx("div", { className: cx('cta-pack', className), ...rest });
15
+ }
@@ -9,6 +9,6 @@ export interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<'div'>, '
9
9
  /** the heading's level: h2 on a surface (default) · h3 inside a card's section, under its h3 heading */
10
10
  level?: 'h2' | 'h3';
11
11
  }
12
- /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. On the surface (in a `Centre`)
13
- * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in. */
12
+ /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. On the surface (always in a `Centre`)
13
+ * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16). */
14
14
  export declare function EmptyState({ icon, title, action, level, className, children, ...rest }: EmptyStateProps): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
- /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. On the surface (in a `Centre`)
4
- * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in. */
3
+ /** EmptyState — centred: a large icon, a heading, a line of body text, an optional CTA. On the surface (always in a `Centre`)
4
+ * or inside a card's section ("nothing here yet" under a heading) — the ink follows the card it sits in; full ink throughout, never muted (Lh 2026-09-16). */
5
5
  export function EmptyState({ icon, title, action, level = 'h2', className, children, ...rest }) {
6
6
  const H = level;
7
7
  return (_jsxs("div", { className: cx('empty-state', className), ...rest, children: [icon, title != null && _jsx(H, { className: "t-h2", children: title }), typeof children === 'string' ? _jsx("p", { className: "t-body", children: children }) : children, action != null && _jsx("div", { className: "empty-state__action", children: action })] }));
@@ -1,7 +1,11 @@
1
1
  import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
- export interface LiftProps extends ComponentPropsWithoutRef<'article'> {
2
+ export interface ModalProps extends ComponentPropsWithoutRef<'article'> {
3
3
  }
4
- /** Lift — the page content card lifted above a Drawer on an `SheetStage ask`. */
4
+ /** Modal — the ask: ONE card at the centre of a `SheetStage ask`, two parts — a `Lift` (the thing being asked about, light, scrolls when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above to the centre · the drawer comes out below while the lift moves up. A click on any button in the Drawer plays them backwards and fires `cardds:modal` on the stage (`detail.answer` = the button's `data-answer`) for the app to remove it. */
5
+ export declare function Modal({ className, ...rest }: ModalProps): import("react").JSX.Element;
6
+ export interface LiftProps extends ComponentPropsWithoutRef<'div'> {
7
+ }
8
+ /** Lift — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
5
9
  export declare function Lift({ className, ...rest }: LiftProps): import("react").JSX.Element;
6
10
  export interface DrawerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
7
11
  /** the question */
@@ -9,7 +13,7 @@ export interface DrawerProps extends Omit<ComponentPropsWithoutRef<'div'>, 'titl
9
13
  /** the fine print under it */
10
14
  caption?: ReactNode;
11
15
  }
12
- /** Drawer — the dark strip at the bottom of an ask stage: a question, a caption, a BtnTrio (no · ask · yes). */
16
+ /** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons); give each button a `data-answer` — any of them closes the modal. */
13
17
  export declare function Drawer({ title, caption, className, children, ...rest }: DrawerProps): import("react").JSX.Element;
14
18
  export interface BtnTrioProps extends ComponentPropsWithoutRef<'div'> {
15
19
  }
@@ -1,13 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, Fragment } from 'react';
3
3
  import { cx } from '../cx.js';
4
- /** Lift — the page content card lifted above a Drawer on an `SheetStage ask`. */
4
+ /** Modal — the ask: ONE card at the centre of a `SheetStage ask`, two parts — a `Lift` (the thing being asked about, light, scrolls when tall) and a `Drawer` (the dark end that asks). No border. Two beats on appearing: drops in from above to the centre · the drawer comes out below while the lift moves up. A click on any button in the Drawer plays them backwards and fires `cardds:modal` on the stage (`detail.answer` = the button's `data-answer`) for the app to remove it. */
5
+ export function Modal({ className, ...rest }) {
6
+ return _jsx("article", { className: cx('card modal', className), ...rest });
7
+ }
8
+ /** Lift — the light part of a `Modal`: the case, request or item the question is about (a CardHead, rows, a timeline); scrolls inside when tall. */
5
9
  export function Lift({ className, ...rest }) {
6
- return _jsx("article", { className: cx('card lift', className), ...rest });
10
+ return _jsx("div", { className: cx('modal__lift', className), ...rest });
7
11
  }
8
- /** Drawer — the dark strip at the bottom of an ask stage: a question, a caption, a BtnTrio (no · ask · yes). */
12
+ /** Drawer — the dark end of a `Modal`: the question, its fine print, and a `BtnTrio` (or two buttons); give each button a `data-answer` — any of them closes the modal. */
9
13
  export function Drawer({ title, caption, className, children, ...rest }) {
10
- return (_jsxs("div", { className: cx('drawer', className), ...rest, children: [_jsx("h2", { className: "t-h2", children: title }), caption != null && _jsx("p", { className: "t-caption", children: caption }), children] }));
14
+ return (_jsxs("div", { className: cx('modal__drawer', className), ...rest, children: [_jsx("h2", { className: "t-h2", children: title }), caption != null && _jsx("p", { className: "t-caption", children: caption }), children] }));
11
15
  }
12
16
  /** BtnTrio — three round buttons with dotted connectors between them (× · ? · ✓); the middle one inverted. */
13
17
  export function BtnTrio({ className, children, ...rest }) {
@@ -1,13 +1,13 @@
1
1
  import type { ComponentPropsWithoutRef } from 'react';
2
- export type SheetState = 'peek' | 'half' | 'full';
2
+ export type SheetState = 'peek' | 'half' | '3q' | 'full';
3
3
  export interface SheetProps extends ComponentPropsWithoutRef<'article'> {
4
- /** how far the sheet slid: `peek` (handle + title row) · `half` (default) · `full` (its top edge at the top bar) — a class, never a height */
4
+ /** how far the sheet slid: `peek` (handle + title row) · `half` (default) · `3q` (three quarters showing) · `full` (its top edge at the top bar) — a class, never a height. A tap on the handle toggles between this state and peek; a drag snaps to any of `states` */
5
5
  state?: SheetState;
6
6
  /** floats above a footnote strip on a dim stage (bottom corners and border back) */
7
7
  raised?: boolean;
8
8
  /** the grab handle (tap / drag / keyboard via cardds.js); `false` for a sheet with no handle, a string sets its accessible name */
9
9
  handle?: boolean | string;
10
- /** which states the handle cycles through, e.g. "peek half full" (default all three) */
10
+ /** which states a drag snaps to, e.g. "peek half full" (default all four: peek half 3q full) */
11
11
  states?: string;
12
12
  }
13
13
  /**
@@ -6,7 +6,7 @@ export interface SheetStageProps extends ComponentPropsWithoutRef<'div'> {
6
6
  hero?: ReactNode;
7
7
  /** blur and darken the background (the dim page sheet) */
8
8
  dim?: boolean;
9
- /** the ask drawer stage: a Lift card over a dark Drawer */
9
+ /** the ask stage: the page under a scrim, one Modal (Lift + Drawer) at the centre */
10
10
  ask?: boolean;
11
11
  /** a footnote strip on the scrim under a raised sheet */
12
12
  foot?: ReactNode;
@@ -2,14 +2,9 @@ import type { ComponentPropsWithoutRef } from 'react';
2
2
  export interface StepProps extends ComponentPropsWithoutRef<'section'> {
3
3
  }
4
4
  /**
5
- * Step — one screen, one task. Children: a StepCard first (what the member READS), an optional Sheet last
5
+ * Step — one screen, one task. Children: a BaseContent first (what the member READS — words on the base, not a card), an optional Sheet last
6
6
  * (what the member DOES); the ActionBar goes OUTSIDE, after the Step. It fills a `Screen fill` between the top bar
7
7
  * and the bar's room, bleeds to the screen's edges (the sheet is full-width), and follows its TopBar/AppBar directly.
8
- * Reads → StepCard only. Acts → StepCard + Sheet. Never hand-lay a step, never write a sheet height.
8
+ * Reads → BaseContent only. Acts → BaseContent + Sheet. Never hand-lay a step, never write a sheet height.
9
9
  */
10
10
  export declare function Step({ className, ...rest }: StepProps): import("react").JSX.Element;
11
- export interface StepCardProps extends ComponentPropsWithoutRef<'article'> {
12
- tone?: 1 | 2 | 3;
13
- }
14
- /** StepCard — the reading card of a Step: a normal Card (CardHead · content) that hugs its words and scrolls inside itself when the column runs out. */
15
- export declare function StepCard({ tone, className, ...rest }: StepCardProps): import("react").JSX.Element;
@@ -1,15 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cx } from '../cx.js';
3
3
  /**
4
- * Step — one screen, one task. Children: a StepCard first (what the member READS), an optional Sheet last
4
+ * Step — one screen, one task. Children: a BaseContent first (what the member READS — words on the base, not a card), an optional Sheet last
5
5
  * (what the member DOES); the ActionBar goes OUTSIDE, after the Step. It fills a `Screen fill` between the top bar
6
6
  * and the bar's room, bleeds to the screen's edges (the sheet is full-width), and follows its TopBar/AppBar directly.
7
- * Reads → StepCard only. Acts → StepCard + Sheet. Never hand-lay a step, never write a sheet height.
7
+ * Reads → BaseContent only. Acts → BaseContent + Sheet. Never hand-lay a step, never write a sheet height.
8
8
  */
9
9
  export function Step({ className, ...rest }) {
10
10
  return _jsx("section", { className: cx('step', className), ...rest });
11
11
  }
12
- /** StepCard — the reading card of a Step: a normal Card (CardHead · content) that hugs its words and scrolls inside itself when the column runs out. */
13
- export function StepCard({ tone, className, ...rest }) {
14
- return _jsx("article", { className: cx('card step__card', tone && tone !== 1 && `card--${tone}`, className), ...rest });
15
- }
package/package.json CHANGED
@@ -1,18 +1,16 @@
1
1
  {
2
2
  "name": "@usableapp/cardds",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
- "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth), app/ is the Commons Time Bank PWA built from them, demo/ the CSS spec pages the acceptance tests (tests/) drive.",
6
+ "description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+ssh://git@github.com/everysundays/cardds.git"
10
10
  },
11
11
  "scripts": {
12
- "dev": "vite --config app/vite.config.ts",
12
+ "dev": "vite --config gallery/vite.config.ts",
13
13
  "build": "node scripts/build-icons.mjs && tsc -p tsconfig.json && node scripts/flatten-css.mjs",
14
- "build:app": "node scripts/build-icons.mjs && vite build --config app/vite.config.ts",
15
- "preview:app": "vite preview --config app/vite.config.ts",
16
14
  "test": "playwright test",
17
15
  "validate:inbox": "node scripts/validate-inbox.mjs",
18
16
  "mcp:verify": "cd mcp-server && npm run verify && npm run verify:http",
@@ -1,12 +0,0 @@
1
- import type { ComponentPropsWithoutRef } from 'react';
2
- export interface SectionProps extends ComponentPropsWithoutRef<'section'> {
3
- }
4
- /**
5
- * Section — frameless on-surface content + a CTA: an overline, a display heading, a paragraph, a CtaPack.
6
- * NEVER a card: sections are what the base says; cards are content the system offers.
7
- */
8
- export declare function Section({ className, ...rest }: SectionProps): import("react").JSX.Element;
9
- export interface CtaPackProps extends ComponentPropsWithoutRef<'div'> {
10
- }
11
- /** CtaPack — the buttons under a Section's words, wrapping on one line. */
12
- export declare function CtaPack({ className, ...rest }: CtaPackProps): import("react").JSX.Element;