@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/README.md +49 -46
- package/cardds.js +47 -10
- package/css/actions.css +12 -27
- package/css/base.css +24 -10
- package/css/layover.css +67 -22
- package/css/sheet.css +10 -4
- package/css/step.css +17 -6
- package/css/tokens.css +1 -0
- package/dist/cardds.css +131 -69
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/scaffold/BaseContent.d.ts +14 -0
- package/dist/scaffold/BaseContent.js +15 -0
- package/dist/scaffold/EmptyState.d.ts +2 -2
- package/dist/scaffold/EmptyState.js +2 -2
- package/dist/sheets/Drawer.d.ts +7 -3
- package/dist/sheets/Drawer.js +8 -4
- package/dist/sheets/Sheet.d.ts +3 -3
- package/dist/sheets/SheetStage.d.ts +1 -1
- package/dist/sheets/Step.d.ts +2 -7
- package/dist/sheets/Step.js +2 -6
- package/package.json +3 -5
- package/dist/scaffold/Section.d.ts +0 -12
- package/dist/scaffold/Section.js +0 -13
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
|
-
<
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.
|
|
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
|
-
/* ----
|
|
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
|
-
.
|
|
440
|
-
.
|
|
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
|
|
459
|
-
.card .empty-state { color: var(--card-
|
|
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
|
-
|
|
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;
|
|
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
|
-
<
|
|
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
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
.
|
|
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
|
|
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
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
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.
|
|
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"
|
|
1750
|
-
<button class="btn btn--
|
|
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
|
-
/*
|
|
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
|
|
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
|
|
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
|
|
2845
|
-
|
|
2846
|
-
<div class="
|
|
2847
|
-
|
|
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
|
-
|
|
2851
|
-
|
|
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
|
-
.
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
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
|
-
.
|
|
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
|
-
.
|
|
2878
|
-
.
|
|
2879
|
-
.
|
|
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/
|
|
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/
|
|
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 })] }));
|
package/dist/sheets/Drawer.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
-
export interface
|
|
2
|
+
export interface ModalProps extends ComponentPropsWithoutRef<'article'> {
|
|
3
3
|
}
|
|
4
|
-
/**
|
|
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
|
|
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
|
}
|
package/dist/sheets/Drawer.js
CHANGED
|
@@ -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
|
-
/**
|
|
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("
|
|
10
|
+
return _jsx("div", { className: cx('modal__lift', className), ...rest });
|
|
7
11
|
}
|
|
8
|
-
/** Drawer — the dark
|
|
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('
|
|
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 }) {
|
package/dist/sheets/Sheet.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
package/dist/sheets/Step.d.ts
CHANGED
|
@@ -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
|
|
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 →
|
|
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;
|
package/dist/sheets/Step.js
CHANGED
|
@@ -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
|
|
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 →
|
|
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.
|
|
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)
|
|
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
|
|
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;
|