@usableapp/cardds 0.1.0

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.
Files changed (170) hide show
  1. package/README.md +562 -0
  2. package/cardds.css +18 -0
  3. package/cardds.js +473 -0
  4. package/css/actions.css +292 -0
  5. package/css/base.css +128 -0
  6. package/css/card.css +283 -0
  7. package/css/choice.css +297 -0
  8. package/css/fonts.css +37 -0
  9. package/css/forms.css +64 -0
  10. package/css/journey.css +61 -0
  11. package/css/layover.css +243 -0
  12. package/css/lists.css +142 -0
  13. package/css/media.css +232 -0
  14. package/css/numbers.css +258 -0
  15. package/css/palette.css +112 -0
  16. package/css/people.css +79 -0
  17. package/css/sheet.css +295 -0
  18. package/css/stack.css +262 -0
  19. package/css/step.css +71 -0
  20. package/css/tokens.css +177 -0
  21. package/dist/actions/Btn.d.ts +311 -0
  22. package/dist/actions/Btn.js +14 -0
  23. package/dist/actions/BtnRow.d.ts +5 -0
  24. package/dist/actions/BtnRow.js +6 -0
  25. package/dist/actions/Chip.d.ts +328 -0
  26. package/dist/actions/Chip.js +25 -0
  27. package/dist/actions/Dot.d.ts +7 -0
  28. package/dist/actions/Dot.js +6 -0
  29. package/dist/actions/Dropdown.d.ts +34 -0
  30. package/dist/actions/Dropdown.js +19 -0
  31. package/dist/actions/Fab.d.ts +9 -0
  32. package/dist/actions/Fab.js +7 -0
  33. package/dist/actions/IconBtn.d.ts +308 -0
  34. package/dist/actions/IconBtn.js +8 -0
  35. package/dist/actions/Link.d.ts +301 -0
  36. package/dist/actions/Link.js +8 -0
  37. package/dist/actions/Segment.d.ts +11 -0
  38. package/dist/actions/Segment.js +10 -0
  39. package/dist/cardds.css +3068 -0
  40. package/dist/cards/Card.d.ts +310 -0
  41. package/dist/cards/Card.js +14 -0
  42. package/dist/cards/CardFoot.d.ts +5 -0
  43. package/dist/cards/CardFoot.js +6 -0
  44. package/dist/cards/CardHead.d.ts +21 -0
  45. package/dist/cards/CardHead.js +11 -0
  46. package/dist/cards/CardParts.d.ts +25 -0
  47. package/dist/cards/CardParts.js +26 -0
  48. package/dist/cards/Placeholder.d.ts +9 -0
  49. package/dist/cards/Placeholder.js +6 -0
  50. package/dist/cards/Stat.d.ts +17 -0
  51. package/dist/cards/Stat.js +14 -0
  52. package/dist/choice/Calendar.d.ts +23 -0
  53. package/dist/choice/Calendar.js +7 -0
  54. package/dist/choice/Check.d.ts +14 -0
  55. package/dist/choice/Check.js +10 -0
  56. package/dist/choice/DayStrip.d.ts +16 -0
  57. package/dist/choice/DayStrip.js +10 -0
  58. package/dist/choice/Mood.d.ts +11 -0
  59. package/dist/choice/Mood.js +6 -0
  60. package/dist/choice/Slider.d.ts +11 -0
  61. package/dist/choice/Slider.js +14 -0
  62. package/dist/cx.d.ts +2 -0
  63. package/dist/cx.js +2 -0
  64. package/dist/forms/AddRow.d.ts +5 -0
  65. package/dist/forms/AddRow.js +7 -0
  66. package/dist/forms/Composer.d.ts +13 -0
  67. package/dist/forms/Composer.js +6 -0
  68. package/dist/forms/Field.d.ts +17 -0
  69. package/dist/forms/Field.js +10 -0
  70. package/dist/forms/FileBtn.d.ts +18 -0
  71. package/dist/forms/FileBtn.js +10 -0
  72. package/dist/forms/Pin.d.ts +19 -0
  73. package/dist/forms/Pin.js +27 -0
  74. package/dist/index.d.ts +72 -0
  75. package/dist/index.js +75 -0
  76. package/dist/journey/NoteRow.d.ts +7 -0
  77. package/dist/journey/NoteRow.js +7 -0
  78. package/dist/journey/Route.d.ts +18 -0
  79. package/dist/journey/Route.js +8 -0
  80. package/dist/journey/TileBadge.d.ts +15 -0
  81. package/dist/journey/TileBadge.js +6 -0
  82. package/dist/layover/Banner.d.ts +9 -0
  83. package/dist/layover/Banner.js +6 -0
  84. package/dist/layover/Callout.d.ts +10 -0
  85. package/dist/layover/Callout.js +6 -0
  86. package/dist/layover/Deck.d.ts +9 -0
  87. package/dist/layover/Deck.js +10 -0
  88. package/dist/lists/Kv.d.ts +27 -0
  89. package/dist/lists/Kv.js +19 -0
  90. package/dist/lists/Legend.d.ts +11 -0
  91. package/dist/lists/Legend.js +11 -0
  92. package/dist/lists/Row.d.ts +319 -0
  93. package/dist/lists/Row.js +17 -0
  94. package/dist/lists/Timeline.d.ts +11 -0
  95. package/dist/lists/Timeline.js +10 -0
  96. package/dist/media/MapArea.d.ts +20 -0
  97. package/dist/media/MapArea.js +11 -0
  98. package/dist/media/Mosaic.d.ts +11 -0
  99. package/dist/media/Mosaic.js +10 -0
  100. package/dist/media/Postcard.d.ts +20 -0
  101. package/dist/media/Postcard.js +10 -0
  102. package/dist/media/Quote.d.ts +11 -0
  103. package/dist/media/Quote.js +6 -0
  104. package/dist/media/Tile.d.ts +320 -0
  105. package/dist/media/Tile.js +16 -0
  106. package/dist/numbers/Badge.d.ts +7 -0
  107. package/dist/numbers/Badge.js +6 -0
  108. package/dist/numbers/Band.d.ts +16 -0
  109. package/dist/numbers/Band.js +10 -0
  110. package/dist/numbers/Bars.d.ts +14 -0
  111. package/dist/numbers/Bars.js +10 -0
  112. package/dist/numbers/DotGrid.d.ts +12 -0
  113. package/dist/numbers/DotGrid.js +6 -0
  114. package/dist/numbers/Picker.d.ts +13 -0
  115. package/dist/numbers/Picker.js +18 -0
  116. package/dist/numbers/Ring.d.ts +15 -0
  117. package/dist/numbers/Ring.js +6 -0
  118. package/dist/numbers/Track.d.ts +12 -0
  119. package/dist/numbers/Track.js +10 -0
  120. package/dist/people/Avatar.d.ts +314 -0
  121. package/dist/people/Avatar.js +13 -0
  122. package/dist/people/AvatarPick.d.ts +18 -0
  123. package/dist/people/AvatarPick.js +10 -0
  124. package/dist/scaffold/ActionBar.d.ts +21 -0
  125. package/dist/scaffold/ActionBar.js +16 -0
  126. package/dist/scaffold/AppBar.d.ts +18 -0
  127. package/dist/scaffold/AppBar.js +10 -0
  128. package/dist/scaffold/Centre.d.ts +30 -0
  129. package/dist/scaffold/Centre.js +41 -0
  130. package/dist/scaffold/EmptyState.d.ts +14 -0
  131. package/dist/scaffold/EmptyState.js +8 -0
  132. package/dist/scaffold/FilterRow.d.ts +5 -0
  133. package/dist/scaffold/FilterRow.js +6 -0
  134. package/dist/scaffold/FloatBar.d.ts +310 -0
  135. package/dist/scaffold/FloatBar.js +15 -0
  136. package/dist/scaffold/PagerAt.d.ts +11 -0
  137. package/dist/scaffold/PagerAt.js +6 -0
  138. package/dist/scaffold/Screen.d.ts +299 -0
  139. package/dist/scaffold/Screen.js +11 -0
  140. package/dist/scaffold/Section.d.ts +12 -0
  141. package/dist/scaffold/Section.js +13 -0
  142. package/dist/scaffold/TopBar.d.ts +15 -0
  143. package/dist/scaffold/TopBar.js +9 -0
  144. package/dist/sheets/CardBack.d.ts +7 -0
  145. package/dist/sheets/CardBack.js +7 -0
  146. package/dist/sheets/Drawer.d.ts +17 -0
  147. package/dist/sheets/Drawer.js +16 -0
  148. package/dist/sheets/Sheet.d.ts +21 -0
  149. package/dist/sheets/Sheet.js +13 -0
  150. package/dist/sheets/SheetBody.d.ts +5 -0
  151. package/dist/sheets/SheetBody.js +6 -0
  152. package/dist/sheets/SheetHead.d.ts +13 -0
  153. package/dist/sheets/SheetHead.js +6 -0
  154. package/dist/sheets/SheetStack.d.ts +21 -0
  155. package/dist/sheets/SheetStack.js +14 -0
  156. package/dist/sheets/SheetStage.d.ts +20 -0
  157. package/dist/sheets/SheetStage.js +11 -0
  158. package/dist/sheets/Step.d.ts +15 -0
  159. package/dist/sheets/Step.js +15 -0
  160. package/dist/type/Icon.d.ts +16 -0
  161. package/dist/type/Icon.js +12 -0
  162. package/dist/type/Text.d.ts +302 -0
  163. package/dist/type/Text.js +16 -0
  164. package/dist/type/icons.d.ts +6 -0
  165. package/dist/type/icons.js +2 -0
  166. package/fonts/LICENSES.md +44 -0
  167. package/fonts/fc-pride-medium.otf +0 -0
  168. package/fonts/pk-nonthaburi-demo.ttf +0 -0
  169. package/icons.svg +61 -0
  170. package/package.json +62 -0
package/css/sheet.css ADDED
@@ -0,0 +1,295 @@
1
+ /* ============================================================
2
+ cardds/sheet.css — sheet single: one card as a bottom sheet.
3
+ Three states: .sheet--peek · .sheet--half · .sheet--full
4
+
5
+ A sheet is rigid material, exactly like a sheet in .sheet-stack--tap
6
+ (stack.css): it is ALWAYS the full height of the box that holds it —
7
+ set once by inset: 0, never written again, never transitioned — and
8
+ a state is nothing but how far that full card has slid up. Peek is
9
+ a full sheet that slid down until only its handle and title show;
10
+ half is one that slid down by half of itself; full is one that has
11
+ not slid at all. The box clips whatever slid below its bottom edge,
12
+ so a sheet at any height is still a card: rounded top, a straight
13
+ cut at the box's edge, its content scrolling inside.
14
+
15
+ So translate is the only animated property. Nothing here toggles
16
+ height, display or z-index for a state — every past bug in the
17
+ stack's animation came from exactly that, and the sheet inherits
18
+ the rule.
19
+
20
+ Two boxes hold sheets, and the state classes mean the same in both:
21
+ - .step (step.css) — an app screen where the member acts: the
22
+ reading card behind, the sheet in front, .action-bar outside.
23
+ - .sheet-stage (below) — a page-level overlay stage: a sheet over
24
+ a map, the dim page sheet, the ask drawer (layover.css).
25
+
26
+ This is the SINGLE sheet: one sheet the member acts in, whatever it
27
+ holds — a short entry in a step, a place on a map, a payment on a dim
28
+ page. Many sheets stacked as the app's menu are the other system,
29
+ .sheet-stack (stack.css): the same material, its own class and file.
30
+
31
+ <article class="card sheet sheet--half" data-sheet-states="peek half full">
32
+ <button class="sheet__handle" type="button" aria-label="…"></button>
33
+ <div class="card__head"><h2 class="t-h2">…</h2>…</div>
34
+ <div class="sheet__body">…controls, or small content cards…</div>
35
+ </article>
36
+ Written top-down: the handle, the title row (what peek shows), then
37
+ the body. The handle and the head stay where they are at every
38
+ state; the body is what scrolls — a sheet is a card whose head is
39
+ pinned, not a scrolling column with a title somewhere in it.
40
+ cardds.js adds the handle's click / drag / keyboard and the focus
41
+ raise; without it every state still renders.
42
+ ============================================================ */
43
+
44
+ .sheet {
45
+ --_handle: var(--sp-1);
46
+ /* the strip a peeked sheet shows: the card's own top padding, the handle,
47
+ the gap, one title row (--tap — the head rule below makes every head at
48
+ least that tall) and the gap under it, so the cut lands exactly where
49
+ the first control starts and nothing of it leaks through. Published
50
+ (and registered in tokens.css) so a sheet whose head is taller can
51
+ retune it: .my-sheet { --sheet-peek: … } */
52
+ --sheet-peek: calc(var(--card-pad) + var(--_handle) + 2 * var(--card-gap) + var(--tap));
53
+ --sheet-half: 0.5; /* a share of the sheet's own height */
54
+ /* the sheet's own height, seen from inside it: the box it slides in is the
55
+ size container (.step, .sheet-stage), so 100cqh is that box, less what
56
+ the sheet gives up at its top (--_top) and bottom (--_lift). The sheet
57
+ cannot be its own container — cq units measure a content box, and the
58
+ sheet's is its border box less the card's padding. */
59
+ --_box: calc(100cqh - var(--_top, 0rem) - var(--_lift, 0rem));
60
+ /* the resting offset of the current state, said twice: as a share of the
61
+ sheet's own height for its translate (--_y), and as a length for the
62
+ body's scroll-padding (--_cover) — a percentage down there would be of
63
+ the body, not of the sheet. Half by default. */
64
+ --_y: calc(var(--sheet-half) * 100%);
65
+ --_cover: calc(var(--sheet-half) * var(--_box));
66
+
67
+ position: absolute;
68
+ inset: 0;
69
+ top: var(--_top, 0); /* a stage keeps its top bar's line free — see .sheet-stage */
70
+ translate: 0 calc(var(--_y) + var(--_drag, 0rem)); /* --_drag: cardds.js, only while the handle is held */
71
+ transition: translate var(--motion-sheet) ease;
72
+
73
+ /* handle · head · body: a column, the body takes what is left and scrolls
74
+ (below) — so the handle and the head never move, whatever the content */
75
+ display: flex;
76
+ flex-direction: column;
77
+ overflow: clip;
78
+ border-radius: var(--r-card) var(--r-card) 0 0;
79
+ border-bottom: 0;
80
+ box-shadow: var(--shadow-sheet);
81
+ }
82
+ .sheet--full { --_y: 0%; --_cover: 0rem; }
83
+ .sheet--half { --_y: calc(var(--sheet-half) * 100%); --_cover: calc(var(--sheet-half) * var(--_box)); }
84
+ .sheet--peek { --_y: calc(100% - var(--sheet-peek)); --_cover: calc(var(--_box) - var(--sheet-peek)); }
85
+ .sheet.is-dragging { transition: none; } /* follows the finger; the snap gets the ease back */
86
+
87
+ /* the body — everything under the head, the one thing in a sheet that
88
+ scrolls. Its scrollport runs to the card's side and bottom edges (reading
89
+ --card-pad, the way .bleed does) so content scrolls clear of the edge
90
+ rather than stopping inside the padding, and a .bleed strip inside it
91
+ still reaches the edges. The card's own grid rhythm continues inside. */
92
+ .sheet__body {
93
+ flex: 1;
94
+ min-height: 0;
95
+ display: grid;
96
+ gap: var(--card-gap);
97
+ align-content: start;
98
+ margin-inline: calc(-1 * var(--card-pad));
99
+ padding-inline: var(--card-pad);
100
+ overflow-y: auto;
101
+ overscroll-behavior: contain;
102
+ /* the part of the sheet that slid under the box's bottom edge cannot be
103
+ seen, and the scroller has to know that: scroll-padding is exactly "the
104
+ obscured part of my scrollport", so a focused field, scrollIntoView and
105
+ anchor navigation all land a control in the visible part, never under
106
+ the cut. A length from the box's container units (--_box), since the
107
+ cut is a share of the SHEET's height, not of the body's. */
108
+ scroll-padding-bottom: var(--_cover);
109
+ }
110
+ .sheet__body > * { min-width: 0; } /* as .card > *: children shrink to the track, never widen it */
111
+ .sheet__body:last-child { margin-bottom: calc(-1 * var(--card-pad)); padding-bottom: var(--card-pad); }
112
+
113
+ /* grab handle — first child of the sheet. Drawn as a small bar, but it is
114
+ the thumb's grab area for the whole strip above the title: a pseudo widens
115
+ its hit box to --tap tall and wider than the bar without changing the
116
+ picture. touch-action: none hands a touch drag to cardds.js instead of to
117
+ the scroller behind it. */
118
+ .sheet__handle {
119
+ position: relative;
120
+ border: 0;
121
+ color: inherit; /* a <button> has its own UA colour; the bar is the card's ink */
122
+ background: currentColor;
123
+ opacity: 0.3;
124
+ width: var(--tap-sm);
125
+ height: var(--_handle);
126
+ border-radius: var(--r-chip);
127
+ margin: 0 auto;
128
+ padding: 0;
129
+ touch-action: none;
130
+ cursor: grab;
131
+ }
132
+ .sheet__handle::before {
133
+ content: "";
134
+ position: absolute;
135
+ inset: calc((var(--_handle) - var(--tap)) / 2) calc(-1 * var(--sp-8));
136
+ }
137
+ .sheet__handle:focus-visible { outline: var(--focus); outline-offset: var(--sp-2); }
138
+ .sheet.is-dragging .sheet__handle { cursor: grabbing; }
139
+
140
+ /* the head — a title row at least --tap tall, either kind, so the peek strip
141
+ (--sheet-peek) is the same for every sheet:
142
+ - in a step it is a plain card head (card.css): the heading left, the
143
+ contextual thing at the far right, the two-line budget and the squeeze
144
+ - .sheet__head is the stage sheet's three-column head: a round button,
145
+ the centred title and sub, a round button
146
+ - a label .row with its .check / .toggle may be the head when the sheet's
147
+ whole ask is that switch: peek shows it (a row is --tap tall plus its own
148
+ padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
149
+ .sheet > .card__head { min-height: var(--tap); }
150
+ .sheet__head {
151
+ display: grid;
152
+ grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
153
+ align-items: center;
154
+ gap: var(--sp-3);
155
+ min-height: var(--tap);
156
+ text-align: center;
157
+ }
158
+ .sheet__title { font: var(--type-h2); letter-spacing: var(--track-h2); margin: 0; }
159
+ .sheet__sub { font: var(--type-caption); color: var(--card-muted); }
160
+
161
+ /* ============================================================
162
+ .sheet-stage — the overlay stage: a page-height box the sheet
163
+ slides in, over a map, a page, previous cards. It is its own
164
+ 100dvh page, never a section of a scrolling screen — an app
165
+ screen where the member acts is a .step (step.css).
166
+ ============================================================ */
167
+ .sheet-stage {
168
+ /* the centre of the band a half sheet leaves open — from the bar's line to the sheet's
169
+ top edge at half ((100% + --_top) / 2): where a hero or a map's ring sits */
170
+ --_band-centre: calc((100% + 3 * var(--_top, 0rem)) / 4);
171
+ --_top: var(--header-h); /* full stops UNDER the line a top bar takes rather than
172
+ covering it — the same reserve .screen makes for a fixed .appbar — and
173
+ keeps its corners; the sheet reads it as its top edge */
174
+ position: relative;
175
+ width: 100%; /* also as a grid/flex child: children are absolute, so it must size itself */
176
+ height: 100dvh;
177
+ max-width: var(--screen-w);
178
+ margin-inline: auto;
179
+ overflow: clip; /* not hidden: a clipped box is not a scroll container, so revealing a focused field never shifts the stage */
180
+ container-type: size; /* the sheet's body reads this box's height as 100cqh (--_box above) */
181
+ background: var(--surface);
182
+ }
183
+ /* on an app screen the stage is the page: it takes the screen's inset back (edge to
184
+ edge, like a step) and, after a fixed .appbar, the room the screen reserved for
185
+ that bar — the stage keeps the bar's line free itself (--_top), so the reserve
186
+ would be paid twice and the page would scroll by it */
187
+ .screen > .sheet-stage { margin-inline: calc(-1 * var(--screen-pad)); width: auto; } /* auto: 100% would keep the inset width */
188
+ .appbar + .sheet-stage { margin-top: calc(-1 * (var(--header-h) + var(--header-gap, 0rem))); }
189
+ .sheet-stage__bg { position: absolute; inset: 0; } /* map / previous cards behind */
190
+ /* the hero: ONE thing shown in the band a half sheet leaves open (a postcard being made,
191
+ a photo being chosen) — centred in that band like a map's ring, on the screen's inset;
192
+ the sheet slides over it at full */
193
+ .sheet-stage__hero {
194
+ position: absolute;
195
+ inset-inline: var(--screen-pad);
196
+ top: var(--_band-centre);
197
+ translate: 0 -50%;
198
+ /* no z-index: it follows the background in DOM order and the sheet, later, covers it at full */
199
+ }
200
+
201
+ /* ---- map: the box a map engine draws in, behind a stage sheet ----
202
+ <div class="sheet-stage__bg"><div class="map"><div>…the engine's canvas…</div>
203
+ <span class="map__ring" role="img" aria-label="ย่านของคุณโดยประมาณ"></span>
204
+ <span class="map__hint">ลากแผนที่ให้วงครอบย่านที่คุณอยู่</span>
205
+ <span class="map__credit">© OpenStreetMap</span></div></div>
206
+ The first child is the engine's mount (the app hands it to Leaflet or the
207
+ like); it fills the box. Before tiles arrive — or offline — the box shows the
208
+ wireframe's street pattern, so a map is never a hole. The ring marks the
209
+ rough area the member is choosing: fixed at the box's centre, so dragging
210
+ the map underneath is what moves it (the centre IS the choice), never a
211
+ pin to place. The hint rides at the top, under the bar's line. */
212
+ .map {
213
+ position: absolute;
214
+ inset: 0;
215
+ isolation: isolate; /* the ring and the hint stack inside the map only — a sheet over the map covers them */
216
+ color: var(--on-surface);
217
+ background:
218
+ repeating-linear-gradient(75deg, transparent 0 5.5rem, var(--hairline) 5.5rem 6.25rem),
219
+ repeating-linear-gradient(-18deg, transparent 0 8.75rem, var(--ink-tint) 8.75rem 9.25rem),
220
+ var(--card-2-bg);
221
+ }
222
+ .map > :first-child { position: absolute; inset: 0; z-index: 0; isolation: isolate; } /* its own stacking context: an engine's layered panes stay under the ring */
223
+ .sheet-stage__bg .map > :first-child { background: transparent; } /* an engine's own grey ground would hide the pattern while tiles load */
224
+ .map__ring {
225
+ position: absolute;
226
+ left: 50%;
227
+ /* centred in the band a half sheet leaves open (the stage's --_band-centre) — the
228
+ member sees the whole ring while the sheet asks; the engine reads the point under it */
229
+ top: var(--_band-centre);
230
+ width: var(--map-ring);
231
+ height: var(--map-ring);
232
+ translate: -50% -50%;
233
+ border-radius: 50%;
234
+ border: var(--border-w) solid var(--accent);
235
+ background: color-mix(in srgb, var(--accent) 22%, transparent);
236
+ box-shadow: var(--shadow-float);
237
+ pointer-events: none;
238
+ z-index: 1; /* above the engine's panes (Leaflet's tiles sit at 200, but inside their own stacking context) */
239
+ }
240
+ .map__credit { /* the tile provider's credit — required by OSM; a caption, not the engine's own control */
241
+ position: absolute;
242
+ right: var(--sp-2);
243
+ bottom: var(--sp-2);
244
+ padding: var(--sp-0) var(--sp-2);
245
+ border-radius: var(--r-chip);
246
+ background: color-mix(in srgb, var(--surface) 80%, transparent);
247
+ color: var(--on-surface-muted);
248
+ font: var(--type-caption);
249
+ pointer-events: none;
250
+ z-index: 1;
251
+ }
252
+ .map__hint {
253
+ position: absolute;
254
+ top: calc(var(--_top, 0rem) + var(--sp-3));
255
+ left: 50%;
256
+ translate: -50% 0;
257
+ max-width: calc(100% - 2 * var(--sp-4));
258
+ padding: var(--sp-2) var(--sp-4);
259
+ border-radius: var(--r-chip);
260
+ background: var(--card-3-bg);
261
+ color: var(--card-3-ink);
262
+ font: var(--type-label);
263
+ text-align: center;
264
+ box-shadow: var(--shadow-float);
265
+ pointer-events: none;
266
+ z-index: 1;
267
+ }
268
+
269
+ /* raised: the sheet floats above a footnote strip on the scrim. Its bottom
270
+ edge shows, so it gets its corners and border back; full still stops at
271
+ the top bar's line, because the top edge never moved. */
272
+ .sheet--raised { --_lift: var(--tap-lg); bottom: var(--_lift); border-radius: var(--r-card); border-bottom: var(--border-w) solid var(--card-border); }
273
+ .sheet-stage__foot {
274
+ position: absolute;
275
+ inset-inline: 0;
276
+ bottom: 0;
277
+ height: var(--tap-lg);
278
+ margin: 0;
279
+ display: grid;
280
+ place-items: center;
281
+ font: var(--type-caption);
282
+ color: var(--card-3-ink);
283
+ z-index: 2;
284
+ }
285
+
286
+ /* dim stage: blur + darken whatever sits behind the sheet */
287
+ .sheet-stage--dim .sheet-stage__bg { filter: blur(var(--sp-2)); }
288
+ .sheet-stage--dim::after {
289
+ content: "";
290
+ position: absolute;
291
+ inset: 0;
292
+ background: var(--scrim);
293
+ z-index: 1;
294
+ }
295
+ .sheet-stage--dim .sheet { z-index: 2; }
package/css/stack.css ADDED
@@ -0,0 +1,262 @@
1
+ /* ============================================================
2
+ cardds/stack.css — .sheet-stack: the menu.
3
+ Sheets stacked in one column like paper, every one a main section
4
+ of the app, their heads peeking, the front one open. Tap a peeked
5
+ head and that sheet comes to the front; its back button returns it.
6
+
7
+ cardds has two sheet systems and one material:
8
+ - sheet single (sheet.css): ONE sheet the member acts in — a short
9
+ entry in front of a .step's card, or an overlay in a .sheet-stage.
10
+ Its state is how far it slid: peek / half / full.
11
+ - sheet stack (this file): MANY sheets, each a section, stacked at
12
+ the middle of the home screen. A peek is the strip the next sheet
13
+ does not cover; open is one sheet brought to the front.
14
+ A stacked sheet is written as a .card — the material — directly
15
+ inside .sheet-stack; .sheet stays the single sheet's class. This
16
+ component was first named "card stack": wrong, and retired — the
17
+ members are sheets and the stack is the menu, not a list of cards.
18
+
19
+ <div class="sheet-stack sheet-stack--tap"> (drop --tap for a static stack)
20
+ <article class="card card--2">
21
+ <div class="card__head">
22
+ <button class="icon-btn icon-btn--sm card__back" aria-label="Back">…</button>
23
+ <h2 class="t-h2 t-caps">Shopping</h2>
24
+ <span class="icon-row">…</span> (optional trailing icons)
25
+ </div>
26
+ …the sheet's own content…
27
+ </article>
28
+ <article class="card">…</article> (last = the front sheet)
29
+ <div class="sheet-stack__base">…</div> (--closed only, optional: what the base shows above the peeks)
30
+ </div>
31
+ --closed (with --tap): nothing open on arrival — every sheet peeks at the
32
+ bottom, the headroom above is the base's (block below).
33
+ Direct children only, five at most (slots are :nth-child below):
34
+ first = the back of the stack, last = the front. README "Sheet stack
35
+ markup". cardds.js opens and closes a --tap stack; without it the
36
+ stack still renders, it just won't open.
37
+ ============================================================ */
38
+
39
+ /* ---- the stack: cascading peek sheets; heads peek, the set bleeds to
40
+ the screen's bottom edge, so bottom corners are square ---- */
41
+ /* step = card padding (20) + head (48) + 20 breathing below the title,
42
+ so every peeked strip shows the title with even space above and below */
43
+ /* one track, exactly the stack's own height: an auto track would size itself
44
+ to the tallest sheet's CONTENT (a sheet's `height: 100%` is cyclic while the
45
+ track is being sized), so a long sheet grew the track past the screen and
46
+ every sheet's 100% then resolved against that — the bottom of every open
47
+ sheet sat below the screen, unreachable (timebank home, 2026-09-13). */
48
+ .sheet-stack { display: grid; grid-template-rows: minmax(0, 1fr); --stack-step: 5.5rem; }
49
+
50
+ /* the stack breaks out of the screen inset, edge to edge, always: the peek
51
+ strips and the opened sheet span the screen's full width whatever the
52
+ viewport (it used to bleed only under a 27em media query — a stack in a
53
+ phone frame on a desktop kept the inset, and read as a narrower card;
54
+ Lh, 2026-09-11) */
55
+ .sheet-stack { margin-inline: calc(-1 * var(--screen-pad)); }
56
+ .sheet-stack > .card {
57
+ grid-area: 1 / 1; /* every card shares the stack's cell; later ones offset down */
58
+ border-bottom-left-radius: 0;
59
+ border-bottom-right-radius: 0;
60
+ border-bottom: 0;
61
+ align-content: start;
62
+ align-self: start; /* own natural height (padding + head) by default */
63
+ --card-gap: var(--sp-5); /* head→content gap = the strip's 20px rhythm */
64
+ z-index: var(--level, 1); /* explicit paint order, 1 = back of the stack */
65
+ }
66
+ /* THE FRONT SHEET is the last .card, not the last child: a --closed stack
67
+ may carry a .sheet-stack__base after its sheets (below). Written once here
68
+ as "a card no card follows" so every rule about the front sheet agrees. */
69
+ .sheet-stack > .card:not(:has(~ .card)) {
70
+ align-self: stretch; /* the front card fills the remaining stack height */
71
+ }
72
+ /* --_n: how many sheets the stack holds, read from the DOM by :has so the
73
+ closed geometry needs no script and no per-stack declaration */
74
+ .sheet-stack { --_n: 1; }
75
+ .sheet-stack:has(> .card:nth-child(2)) { --_n: 2; }
76
+ .sheet-stack:has(> .card:nth-child(3)) { --_n: 3; }
77
+ .sheet-stack:has(> .card:nth-child(4)) { --_n: 4; }
78
+ .sheet-stack:has(> .card:nth-child(5)) { --_n: 5; }
79
+ /* a card's place in the deck, declared here rather than measured in JS, so
80
+ a stack renders correctly — right offsets, right paint order — with no
81
+ script at all. --slot is that place in steps; the tap animation reads it
82
+ to know how far up to travel. Five is the practical peek depth. */
83
+ .sheet-stack > .card:nth-child(1) { --slot: 0; --level: 1; }
84
+ .sheet-stack > .card:nth-child(2) { --slot: 1; --level: 2; margin-top: var(--stack-step); }
85
+ .sheet-stack > .card:nth-child(3) { --slot: 2; --level: 3; margin-top: calc(2 * var(--stack-step)); }
86
+ .sheet-stack > .card:nth-child(4) { --slot: 3; --level: 4; margin-top: calc(3 * var(--stack-step)); }
87
+ .sheet-stack > .card:nth-child(5) { --slot: 4; --level: 5; margin-top: calc(4 * var(--stack-step)); }
88
+
89
+ /* stack headers: uniform — [back?] title left, icons right, one height,
90
+ same gap to content on every card */
91
+ .sheet-stack > .card > .card__head {
92
+ align-items: center;
93
+ justify-content: flex-start;
94
+ min-height: var(--tap);
95
+ }
96
+ /* NOT a skeleton head: a peek is one line at its own size, and the display
97
+ stack deliberately runs a second line under the next card. So it opts out of
98
+ the whole skeleton-head treatment — budget, squeeze and clamp. cardds.js
99
+ skips the same cards. */
100
+ .sheet-stack > .card > .card__head :is(h1, h2, h3) {
101
+ font-size: var(--fs-h1);
102
+ line-height: 1;
103
+ letter-spacing: -0.01em;
104
+ margin-right: auto; /* pins the icon cluster right */
105
+ display: block;
106
+ -webkit-line-clamp: none;
107
+ overflow: visible;
108
+ overflow-wrap: normal;
109
+ }
110
+
111
+ /* display stack: the giant title IS the peek — line one shows, line two is
112
+ clipped by the next card (recents / library lists) */
113
+ .sheet-stack--display { --stack-step: 6.5rem; }
114
+ .sheet-stack--display > .card > .card__head { align-items: start; min-height: 0; }
115
+ .sheet-stack--display > .card > .card__head :is(h1, h2, h3) {
116
+ font: var(--type-display);
117
+ letter-spacing: var(--track-display);
118
+ /* the FIRST line sits on the centre of the peek strip (--stack-step tall,
119
+ measured from the card's top edge); line two runs under the next card */
120
+ margin-top: calc((var(--stack-step) - 1lh) / 2 - var(--sp-5));
121
+ }
122
+ /* fanned stack: a toggle opens the stack wider so every peek shows more */
123
+ .sheet-stack--fanned { --stack-step: 9.5rem; }
124
+
125
+ .sheet-stack > .card:has(~ .card) { cursor: pointer; }
126
+ /* a peeked card in a plain stack hides its own body. A --tap stack does
127
+ NOT: there, every card is full height and full content always, and a
128
+ peek is simply the strip of it the next card doesn't cover — the way a
129
+ card in a real deck is never a shorter card, just a mostly hidden one. */
130
+ .sheet-stack:not(.sheet-stack--tap) > .card:has(~ .card):not(.is-open) > :not(.card__head) { display: none; }
131
+
132
+ /* ============================================================
133
+ .sheet-stack--tap — tap a peeked sheet open; a back button returns it.
134
+
135
+ A sheet (a .card — the material, sheet.css's single sheet is the same
136
+ stuff) is rigid: it can move in, move out, or be covered — it can
137
+ never change shape. So every sheet here is the SAME full height,
138
+ always, set once and never touched again by anything. A "peek" is
139
+ not a short sheet; it's a full sheet the next one happens to cover,
140
+ showing only the strip above it, exactly like a sheet of paper part
141
+ way out of a real stack.
142
+
143
+ Because of that, the whole animation is one property: translate.
144
+ Nothing resizes, nothing re-stacks — z-index is the plain resting
145
+ --level order and is never touched, since the order already says
146
+ what has to happen. Cards after the tapped one sit IN FRONT of it,
147
+ so they're what's covering it, so they're what must leave. Cards
148
+ before it are already BEHIND it, so they need do nothing at all
149
+ except let themselves be covered as it rises over them.
150
+
151
+ .is-open — the tapped card, moved up to the top of the stack. Its
152
+ own resting offset is margin-top (below); opening just
153
+ translates that offset back off, landing it flush at
154
+ the stack's top with its full height exactly filling
155
+ to the bottom.
156
+ .is-before — an earlier card, nudged one step down so it slips
157
+ under the next card's resting strip and is covered
158
+ there. Each carries its own --stagger (set in JS: 0
159
+ for the one nearest the tapped card, counting up
160
+ further back) so they tuck away, and later un-tuck,
161
+ one at a time.
162
+ .is-after — a later card, moved out below, together, no stagger.
163
+ ============================================================ */
164
+ .sheet-stack--tap > .card {
165
+ height: 100%; /* every card identical and full length, always — the one
166
+ size in this whole component, never toggled, never transitioned. At a
167
+ resting margin-top of n steps it simply overhangs the stack's bottom
168
+ by n steps (clipped by .screen--fill), so translating it up by exactly
169
+ that much lands it filling the stack precisely. */
170
+ overflow: clip;
171
+ transition: translate 0.35s ease; /* the only animated property here */
172
+ transition-delay: calc(var(--stagger, 0) * 70ms); /* 0 for everything
173
+ except a staggered is-before card, set in JS as it tucks or un-tucks */
174
+ }
175
+ .sheet-stack--tap > .card.is-open {
176
+ /* up by its own resting offset — pure movement, nothing resizes */
177
+ translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step));
178
+ cursor: default;
179
+ overflow-y: auto;
180
+ overflow-x: clip;
181
+ overscroll-behavior: contain;
182
+ }
183
+ /* the front sheet — the one open on arrival — never travels up (it is not
184
+ tappable), so its resting offset would leave its last n steps below the
185
+ screen forever, unreachable by any scroll (timebank home, 2026-09-13). It
186
+ alone is cut to the room it actually has, and it scrolls like an opened
187
+ sheet does. Every other sheet keeps the one full height. */
188
+ .sheet-stack--tap:not(.sheet-stack--closed) > .card:not(:has(~ .card)) {
189
+ height: calc(100% - var(--slot, 0) * var(--stack-step));
190
+ overflow-y: auto;
191
+ overflow-x: clip;
192
+ overscroll-behavior: contain;
193
+ }
194
+ /* a scrolling sheet shows no scrollbar (a desktop with a mouse would paint one
195
+ down the card's edge — Lh 2026-09-15); it still scrolls, like the page itself */
196
+ .sheet-stack--tap > .card { scrollbar-width: none; }
197
+ .sheet-stack--tap > .card::-webkit-scrollbar { display: none; }
198
+ .sheet-stack--tap > .card.is-before { translate: 0 var(--stack-step); }
199
+ .sheet-stack--tap > .card.is-after { translate: 0 100vh; }
200
+
201
+ /* ============================================================
202
+ .sheet-stack--closed — nothing open on arrival. Every sheet, the front
203
+ one too, is a peek at the BOTTOM of the stack; the room above the peeks
204
+ is the screen's base, free for whatever the screen wants there (a
205
+ picture, a greeting — timebank home, Lh 2026-09-15). Tap any head and
206
+ that sheet rises over the base to fill the stack; back drops it home.
207
+
208
+ Same rigid material, same one animated property. The sheets keep their
209
+ full height and their margin-top slots; what changes is where they REST:
210
+ translated down by the headroom, 100% − n × step — a card's 100% is the
211
+ stack's height, so the last peek strip ends exactly at the stack's
212
+ bottom edge. Open is translate to −slot × step, as before: the sheet
213
+ lands flush at the top. is-before tucks one step below its resting place.
214
+ ============================================================ */
215
+ .sheet-stack--closed { --_headroom: calc(100% - var(--_n) * var(--stack-step)); }
216
+ .sheet-stack--closed > .card { translate: 0 var(--_headroom); cursor: pointer; }
217
+ .sheet-stack--closed > .card.is-before { translate: 0 calc(var(--_headroom) + var(--stack-step)); }
218
+ .sheet-stack--closed > .card.is-open { translate: 0 calc(-1 * var(--slot, 0) * var(--stack-step)); }
219
+ .sheet-stack--closed > .card.is-after { translate: 0 100vh; }
220
+
221
+ /* .sheet-stack__base — the base's content, in the headroom of a closed stack:
222
+ behind every sheet (level 0), the stack's inset put back (the stack bleeds),
223
+ exactly the headroom tall so it can lay its content out against the
224
+ peeks (align-content: end puts it right above them). Written LAST, after
225
+ the sheets, so the sheets' :nth-child slots stay what they are. */
226
+ .sheet-stack > .sheet-stack__base {
227
+ grid-area: 1 / 1;
228
+ align-self: start;
229
+ z-index: 0;
230
+ height: var(--_headroom, 0px);
231
+ padding-inline: var(--screen-pad);
232
+ display: grid;
233
+ align-content: end;
234
+ gap: var(--card-gap, var(--sp-5));
235
+ overflow: clip;
236
+ }
237
+
238
+ /* back button: lives in a slot the header reserves once open, and DISSOLVES
239
+ into it — an opacity fade in place, never a slide in from the left (the
240
+ slide read as a second thing moving while the sheet itself moved; Lh,
241
+ 2026-09-11). Only the sheet travels; the button appears where it is. */
242
+ .sheet-stack--tap .card__head {
243
+ position: relative;
244
+ padding-left: 0;
245
+ transition: padding-left 0.35s ease;
246
+ }
247
+ .sheet-stack--tap > .card.is-open > .card__head {
248
+ padding-left: calc(var(--tap-sm) + var(--sp-3));
249
+ }
250
+ .sheet-stack--tap .card__back {
251
+ position: absolute;
252
+ left: 0;
253
+ top: 50%;
254
+ translate: 0 -50%;
255
+ opacity: 0;
256
+ visibility: hidden;
257
+ transition: opacity 0.35s ease, visibility 0.35s;
258
+ }
259
+ .sheet-stack--tap > .card.is-open .card__back {
260
+ opacity: 1;
261
+ visibility: visible;
262
+ }
package/css/step.css ADDED
@@ -0,0 +1,71 @@
1
+ /* ============================================================
2
+ cardds/step.css — .step: one screen, one task.
3
+ What the member reads (a card), what the member does when the
4
+ step asks for it (a sheet, sheet.css), and the screen's one
5
+ move (.action-bar, actions.css) OUTSIDE the step, below it.
6
+
7
+ <section class="step">
8
+ <article class="card step__card">…the words…</article> ← first
9
+ <article class="card sheet sheet--half">…the controls…</article> ← last, optional
10
+ </section>
11
+ <div class="action-bar">…</div>
12
+
13
+ The step is the box the sheet slides in. It fills the screen
14
+ column between the top bar's reserve (.screen already pads for
15
+ .appbar) and the bar's, and clips at its own bottom edge — so the
16
+ sheet, at every state, stops above the bar and nothing ever sits
17
+ behind it. The card hugs its words and scrolls inside itself when
18
+ the column runs out. The sheet is full-width, like the bar: the
19
+ step bleeds to the screen's edges and the card takes the inset
20
+ back, so the card and the bar's buttons stand on the inset line
21
+ while the sheet runs edge to edge.
22
+
23
+ It needs a bounded column to fill: .screen--fill (base.css), or
24
+ an app section of a definite height. Reads → card only. Acts →
25
+ card + sheet. Never a hand-laid step, never a sheet height.
26
+ ============================================================ */
27
+ .step {
28
+ position: relative;
29
+ display: flex;
30
+ flex-direction: column;
31
+ flex: 1;
32
+ min-height: 0;
33
+ overflow: clip;
34
+ margin-inline: calc(-1 * var(--screen-pad)); /* edge to edge, reading the inset the screen publishes (as .sheet-stack does) */
35
+ container-type: size; /* the sheet's body reads the step's height as 100cqh — sheet.css, --_box.
36
+ (size containment also makes the step the containing block for fixed
37
+ descendants: the bar is outside the step, and a popover is in the top layer) */
38
+ /* --kb: the on-screen keyboard's height, published on :root by cardds.js
39
+ from the visual viewport — 0 without one, and on a platform that shrinks
40
+ the layout viewport itself. It grows the room below the step the same
41
+ way it lifts the bar (actions.css), so the control being typed into and
42
+ the bar both stay above the keyboard. */
43
+ margin-bottom: var(--kb, 0);
44
+ }
45
+ /* the step touches the top bar it follows, so that the sheet at full reaches
46
+ the bar's bottom edge and nothing else: the room the screen would have
47
+ left between them moves INSIDE the step, above the card (--_lead), where
48
+ the sheet can rise over it. After an in-flow .topbar that room is the
49
+ screen's gap; after a fixed .appbar it is the reserve's breathing — both
50
+ read from the tokens the screen publishes, never recomputed. A step that
51
+ follows anything else keeps the screen's gap and the sheet stops under
52
+ that thing. */
53
+ .topbar + .step { margin-top: calc(-1 * var(--screen-gap)); --_lead: var(--screen-gap); }
54
+ .appbar + .step { margin-top: calc(-1 * var(--header-gap)); --_lead: var(--screen-gap); }
55
+
56
+ /* the bar's room — read from actions.css, never recomputed. Margin, not
57
+ padding, so the clip cuts at the box edge and nothing shows behind the
58
+ bar. Any parent that hosts the bar counts — <body>, an app shell, a demo
59
+ frame — the bar is fixed to the viewport wherever it sits. */
60
+ :has(> .action-bar) .step { margin-bottom: calc(var(--bar-reserve) + var(--kb, 0rem)); }
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 {
66
+ flex: 0 1 auto;
67
+ min-height: 0;
68
+ 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 */
69
+ overflow-y: auto;
70
+ overscroll-behavior: contain;
71
+ }