@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/card.css ADDED
@@ -0,0 +1,283 @@
1
+ /* ============================================================
2
+ cardds/card.css — the card boundary + list variants
3
+ One card = one boundary. Tone via .card--1/2/3 (role slots).
4
+ ============================================================ */
5
+
6
+ .card {
7
+ --card-bg: var(--card-1-bg); /* the slot, readable by children (avatar rings, fold) */
8
+ --card-ink: var(--card-1-ink);
9
+ --card-pad: var(--sp-5); /* published so .bleed can cancel it exactly */
10
+ --card-gap: var(--sp-4); /* published so a sheet's peek strip (sheet.css) is exact */
11
+ background: var(--card-bg);
12
+ color: var(--card-ink);
13
+ border: var(--border-w) solid var(--card-border);
14
+ border-radius: var(--r-card);
15
+ padding: var(--card-pad);
16
+ display: grid;
17
+ gap: var(--card-gap);
18
+ position: relative;
19
+ min-width: 0;
20
+ text-decoration: none; /* .card works as <a> too */
21
+ }
22
+ /* the card is a grid: a child with a wide min-content (inputs, fixed-width
23
+ rows) would otherwise widen the track past the card's padding on narrow
24
+ screens. Children shrink to the track; they never size the card. */
25
+ .card > * { min-width: 0; }
26
+
27
+ .card--2 { --card-bg: var(--card-2-bg); --card-ink: var(--card-2-ink); --card-muted: var(--card-2-muted); }
28
+ .card--3 { --card-bg: var(--card-3-bg); --card-ink: var(--card-3-ink); }
29
+
30
+ /* dark/accent slot: accent-toned controls would vanish — the slot re-points
31
+ the accent tokens, so every accent consumer (btn--primary, chip--on,
32
+ check, toggle, ring, day…) inverts without per-component rules */
33
+ .card--3 { --accent: var(--card-3-ink); --on-accent: var(--card-3-bg); }
34
+
35
+ /* header row: title left, floating action right — centered, since the
36
+ trailing control (chip/badge/icon-btn/link) is almost always taller than
37
+ a single line of title text; top-aligning them left the control visually
38
+ low against the title's optical center. A head with a genuinely tall,
39
+ multi-line title can opt back out with .card__head--top. */
40
+ .card__head {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ gap: var(--sp-3);
45
+ }
46
+ .card__head--top { align-items: flex-start; }
47
+
48
+ .card__media { border-radius: var(--r-media); overflow: hidden; position: relative; } /* relative: corner badges */
49
+ .card__end { text-align: right; display: grid; gap: var(--sp-0); justify-items: end; } /* trailing value + caption */
50
+ .card__media > img { width: 100%; height: auto; object-fit: cover; }
51
+
52
+ /* ---- the card skeleton: head · content · foot ----
53
+ Every card is a head (the heading left, hints / actions / a more-menu at
54
+ the far right), its content, and an optional foot of action buttons.
55
+ The head's leading child — the heading, or the div that wraps heading and
56
+ caption — takes the room, so a heading with nothing at the far right runs
57
+ all the way across; whatever trails it hugs its own width. Only the
58
+ heading (or the div that wraps it) is that leading child: a control that
59
+ leads a custom head — a chip, an avatar, a tile badge — keeps its own
60
+ width, else the rule stretched a chip into a bar and an avatar into an
61
+ ellipse (two agents and the wallet demo caught it, 2026-09-10). */
62
+ .card__head > :is(h1, h2, h3, div, p):first-child { flex: 1; min-width: 0; }
63
+
64
+ /* Skeleton head — the heading in a card's OWN head, and only there.
65
+ Two lines are the budget: cardds.js measures and steps --squeeze down so the
66
+ type shrinks to fit; the clamp holds a heading that still overflows to three
67
+ lines, never more. The level's own size comes through --fs-self (base.css),
68
+ so the multiplier is level-agnostic and a heading without --squeeze renders
69
+ exactly as before.
70
+ A STACK head is not a skeleton head — it is a one-line peek at its own size,
71
+ and "stack headers" below opts it out of the budget, the squeeze and the
72
+ clamp. cardds.js skips those same cards, so CSS and JS agree on what counts
73
+ as a skeleton head. Change one, change the other. */
74
+ .card__head :is(h1, h2, h3) {
75
+ font-size: calc(var(--fs-self, var(--fs-h2)) * var(--squeeze, 1));
76
+ overflow-wrap: anywhere;
77
+ display: -webkit-box;
78
+ -webkit-box-orient: vertical;
79
+ -webkit-line-clamp: 3;
80
+ overflow: hidden;
81
+ /* the clip would take the hair the tight leading leaves outside the line
82
+ box — a stacked tone mark on top, สระอู below (fonts.css). Padding is
83
+ inside the clip, the negative margin gives the room back, so the head
84
+ measures exactly as before. */
85
+ padding-block: 0.15em;
86
+ margin-block: -0.15em;
87
+ }
88
+
89
+ /* ---- bleed: a scrolling track that runs to the container's edges ----
90
+ For a horizontal strip inside a padded container — .avatar-pick, .chip-row,
91
+ a tile grid: the TRACK reaches the edges so it reads as scrollable, while
92
+ the first and last item stay on the same line as everything else.
93
+ Reads --card-pad, so it is exact by construction and follows any container
94
+ that publishes one (.card today, a sheet or a screen the same way).
95
+ <div class="avatar-pick bleed">…</div>
96
+ Padding, not item margins: padding is inside the scrollable area, so the
97
+ last item scrolls fully clear of the edge instead of stopping against it.
98
+ scroll-padding keeps snap points on the content line, not the border edge. */
99
+ .bleed {
100
+ margin-inline: calc(-1 * var(--card-pad, 0px));
101
+ padding-inline: var(--card-pad, 0px);
102
+ scroll-padding-inline: var(--card-pad, 0px);
103
+ }
104
+ /* a bleeding child sits over the container's rounded corner, so a strip that
105
+ is genuinely first or last needs the corner clipped for it */
106
+ .card:has(> .bleed:first-child), .card:has(> .bleed:last-child) { overflow: clip; }
107
+
108
+ /* foot: the card's action buttons on one line. One button takes the width,
109
+ up to four share it in even parts — so the gaps read even too — and from
110
+ three on they drop their side padding so the labels keep the room. */
111
+ .card__foot { display: flex; gap: var(--sp-2); align-items: center; }
112
+ .card__foot > .btn { flex: 1 1 0; min-width: 0; }
113
+ .card__foot:has(> :nth-child(3)) > .btn { padding-inline: var(--sp-2); }
114
+
115
+ /* media placeholder for wireframes — currentColor so it survives any palette */
116
+ .ph {
117
+ background:
118
+ linear-gradient(to top right, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px)),
119
+ linear-gradient(to top left, transparent calc(50% - 1px), currentColor calc(50% - 1px), currentColor calc(50% + 1px), transparent calc(50% + 1px));
120
+ border: var(--border-w) solid currentColor;
121
+ border-radius: var(--r-media);
122
+ opacity: 0.4;
123
+ min-height: 10rem;
124
+ }
125
+ .ph--sm { min-height: 7.5rem; }
126
+ .ph--md { min-height: 9rem; }
127
+ /* ---- centred card: one thing to read, at the human centre of a fill screen ----
128
+ (.screen__centre below is the same placement for a small GROUP of things)
129
+ Where the centre of a phone IS, to the person holding it: the bottom fifth is
130
+ the thumb's — the bar, the controls — so what is meant to be read sits at the
131
+ middle of the four fifths above it, --screen-centre (40%) down the screen, not
132
+ at 50%. (Lh, 2026-09-10.) Any screen with one
133
+ card and nothing else to read may.
134
+ <div class="screen screen--fill"><header class="topbar">…</header><article class="card card--centre">…</article></div>
135
+ <div class="action-bar">…</div>
136
+ The card hugs its words and keeps the screen's inset. It is capped so that it
137
+ never reaches the title bar above it nor the bar's room below it — past that it
138
+ scrolls inside — from the same tokens those bars reserve their room with. */
139
+ .card--centre,
140
+ .screen--fill > .screen__centre {
141
+ position: absolute;
142
+ top: var(--screen-centre);
143
+ translate: 0 -50%;
144
+ /* the cap: twice the room above the centre (the title bar's height, when
145
+ one is there, and the screen's gap), or twice the room below it (the
146
+ bar's reserve) — whichever is less. --_lead is the title bar's height
147
+ only on a screen that HAS one: without a .topbar/.appbar before it the
148
+ block keeps that room (a deck and its two buttons, a card and its pager
149
+ under it, need it on a short phone; Lh, 2026-09-11). */
150
+ --_lead: 0rem;
151
+ max-height: calc(2 * min(var(--screen-centre) - var(--_lead) - var(--screen-gap), 100% - var(--screen-centre) - var(--bar-reserve)));
152
+ overflow-y: auto;
153
+ overscroll-behavior: contain;
154
+ }
155
+ :is(.topbar, .appbar) ~ :is(.card--centre, .screen__centre) { --_lead: var(--header-h); }
156
+ .card--centre { inset-inline: var(--screen-pad); }
157
+
158
+ /* ---- centred block: the same place, for one thing to read that is not a
159
+ lone card — an empty state, a card with its quiet second button and the
160
+ pager under it, a deck with its two round buttons. The block runs edge to
161
+ edge and keeps the screen's inset as padding, so a deck's fanned cards can
162
+ overhang and clip at the screen's edges (correct — the fold visual), and
163
+ its children stack at the screen's own rhythm (Lh, 2026-09-11):
164
+ <div class="screen screen--fill"><header class="topbar">…</header>
165
+ <div class="screen__centre"><article class="card">…</article><button class="btn btn--quiet btn--block">…</button></div>
166
+ </div> */
167
+ .screen__centre {
168
+ inset-inline: 0;
169
+ padding-inline: var(--screen-pad);
170
+ display: grid;
171
+ gap: var(--screen-gap);
172
+ align-content: start;
173
+ }
174
+ .screen__centre > * { min-width: 0; }
175
+
176
+ /* ---- the same group on a plain .screen: centred when short, as long as it
177
+ likes when not (Lh, 2026-09-12) ----
178
+ The member sees one content area — whatever is between the top bar and the
179
+ bar — and reads it as a whole page: a short card sits at its middle, a long
180
+ one simply makes the page longer, runs under the bar (the bar's reserve is
181
+ already the screen's bottom padding, actions.css) and scrolls up like any
182
+ page — nothing is capped, nothing shrinks, no scroll box inside a box.
183
+ So on a .screen that is NOT --fill the group is in flow: the column is at
184
+ least one phone tall, the group takes the room after the top bar and
185
+ centres its children in it. The screen's own inset stays the inset.
186
+ <div class="screen"><header class="topbar">…</header>
187
+ <div class="screen__centre"><article class="card">…</article><div class="pager__at">…</div></div>
188
+ </div><div class="action-bar">…</div> */
189
+ .screen:not(.screen--fill):has(> .screen__centre) { min-height: 100dvh; display: flex; flex-direction: column; }
190
+ .screen:not(.screen--fill) > .screen__centre { flex: 1; padding-inline: 0; align-content: center; }
191
+
192
+ /* ---- the card that changes slides: out one side, the next in from the other ----
193
+ A .screen__centre group that walks pages (data-slide="next" | "back" on the
194
+ root while a step changes) turns its card like a page: the old card slides
195
+ out, the new one slides in from the side the move came from — quick, the
196
+ heading and the dots stay still (Lh, 2026-09-12). Built on view transitions:
197
+ the React adapter's slideTo() sets data-slide on :root, starts the transition
198
+ and updates inside it; a browser without them (the floor: Chrome 105 / iOS 16)
199
+ simply switches. Only the card is named; the rest of the page must not
200
+ cross-fade. */
201
+ .screen__centre > .card, .pages > .card[aria-current] { view-transition-name: centre-card; }
202
+
203
+ /* ---- pages: the cards of a walk, all one height ----
204
+ A sequence the member walks (an introduction) reads steadier when every card
205
+ is the same height (Lh, 2026-09-12): so all its cards are on the page at
206
+ once, stacked in one grid cell, and the cell is as tall as the tallest — at
207
+ any text size. The current card carries aria-current="step"; every other
208
+ one is inert (out of the tab order and the accessibility tree) and hidden
209
+ in place, keeping its height. A page turn is the same slideTo() as a lone card.
210
+ <div class="screen__centre"><div class="pages">
211
+ <article class="card" aria-current="step">…</article>
212
+ <article class="card" inert>…</article>
213
+ </div><div class="pager__at">…</div></div> */
214
+ .pages { display: grid; }
215
+ .pages > .card { grid-area: 1 / 1; align-content: start; } /* stretched to the cell, the words stay at the top */
216
+ .pages > .card[inert] { visibility: hidden; }
217
+ ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
218
+ ::view-transition-old(centre-card), ::view-transition-new(centre-card) {
219
+ animation-duration: var(--motion-slide);
220
+ animation-timing-function: ease-out;
221
+ mix-blend-mode: normal;
222
+ }
223
+ :root[data-slide="next"]::view-transition-old(centre-card) { animation-name: centre-card-out-left; }
224
+ :root[data-slide="next"]::view-transition-new(centre-card) { animation-name: centre-card-in-right; }
225
+ :root[data-slide="back"]::view-transition-old(centre-card) { animation-name: centre-card-out-right; }
226
+ :root[data-slide="back"]::view-transition-new(centre-card) { animation-name: centre-card-in-left; }
227
+ @keyframes centre-card-out-left { to { translate: -100% 0; opacity: 0; } }
228
+ @keyframes centre-card-out-right { to { translate: 100% 0; opacity: 0; } }
229
+ @keyframes centre-card-in-right { from { translate: 100% 0; opacity: 0; } }
230
+ @keyframes centre-card-in-left { from { translate: -100% 0; opacity: 0; } }
231
+ @media (prefers-reduced-motion: reduce) {
232
+ ::view-transition-old(centre-card), ::view-transition-new(centre-card) { animation: none; }
233
+ }
234
+ /* the page dots under the card sit centred under it — the indicator belongs
235
+ with the page that changes, never in the bar (Lh, 2026-09-12) */
236
+ .screen__centre > .pager__at { justify-self: center; }
237
+
238
+ /* compact card inside a card (nested note, promo) */
239
+ .card--sm { padding: var(--sp-4); --card-gap: var(--sp-2); border-radius: var(--r-card); }
240
+
241
+ /* the sheet stack (.sheet-stack) — sheets stacked, heads peeking — lives in
242
+ stack.css; a stacked sheet is a plain .card, the material */
243
+
244
+ /* trailing icon cluster in a card header */
245
+ .icon-row { display: flex; align-items: center; gap: var(--sp-4); opacity: 0.85; }
246
+
247
+ /* ---- card list: rows, each row its own boundary ---- */
248
+ .card-list { display: grid; gap: var(--sp-3); }
249
+
250
+ .card--row {
251
+ border-radius: var(--r-card);
252
+ padding: var(--sp-4);
253
+ display: flex;
254
+ align-items: center;
255
+ gap: var(--sp-3);
256
+ }
257
+ .card--row .card__body { flex: 1; display: grid; gap: var(--sp-0); }
258
+
259
+ /* leading thumb/icon in a row */
260
+ .card__thumb {
261
+ width: var(--tap-sm); height: var(--tap-sm);
262
+ flex: none;
263
+ border-radius: var(--r-control);
264
+ border: var(--border-w) solid currentColor;
265
+ display: grid;
266
+ place-items: center;
267
+ font: var(--type-label);
268
+ opacity: 0.85;
269
+ }
270
+
271
+ /* ---- inline stat block (revenue-style) ---- */
272
+ .stat { display: grid; gap: var(--sp-1); }
273
+ .stat__row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
274
+
275
+ .delta {
276
+ font: var(--type-caption);
277
+ font-weight: 700;
278
+ padding: var(--sp-1) var(--sp-2);
279
+ border-radius: var(--r-chip);
280
+ background: var(--card-3-bg);
281
+ color: var(--card-3-ink);
282
+ }
283
+ .delta--neg { background: var(--neg); color: var(--card-3-ink); } /* light ink on the signal color in every slot */
package/css/choice.css ADDED
@@ -0,0 +1,297 @@
1
+ /* ============================================================
2
+ cardds/choice.css — thumb-size choice controls inside cards:
3
+ check circle · toggle · toggle chip · chip grid · day strip ·
4
+ calendar · mood picker · pin field · composer · upload tile
5
+ Native inputs carry the state; CSS reads :checked. No JS.
6
+ ============================================================ */
7
+
8
+ /* the data-URI icons below are masks: only their alpha is read, so the
9
+ "black" stroke inside them is shape, not color — the fill is the token. */
10
+
11
+ /* ---- check: round check circle at a row's end ----
12
+ <input type="checkbox" class="check" aria-label="Done"> */
13
+ .check {
14
+ appearance: none;
15
+ width: var(--tap);
16
+ height: var(--tap);
17
+ flex: none;
18
+ margin: 0;
19
+ border-radius: 50%;
20
+ border: var(--border-w) solid var(--outline);
21
+ background: var(--action-bg);
22
+ display: grid;
23
+ place-items: center;
24
+ cursor: pointer;
25
+ }
26
+ .check::after {
27
+ content: "";
28
+ width: calc(var(--tap) / 2);
29
+ height: calc(var(--tap) / 2);
30
+ background: var(--on-accent);
31
+ mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>') center / contain no-repeat;
32
+ -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>') center / contain no-repeat;
33
+ opacity: 0;
34
+ }
35
+ .check:checked { background: var(--accent); border-color: transparent; }
36
+ .check:checked::after { opacity: 1; }
37
+ .check:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
38
+ .check--sm { width: var(--tap-sm); height: var(--tap-sm); }
39
+
40
+ /* ---- toggle: big switch ----
41
+ <input type="checkbox" role="switch" class="toggle" aria-label="Private"> */
42
+ .toggle {
43
+ --_w: var(--bar-h); /* 64 × 36, knob = height − 2 × pad */
44
+ --_h: 2.25rem;
45
+ --_pad: 0.1875rem;
46
+ appearance: none;
47
+ width: var(--_w);
48
+ height: var(--_h);
49
+ flex: none;
50
+ margin: 0;
51
+ border-radius: var(--r-chip);
52
+ border: var(--border-w) solid var(--outline);
53
+ background: var(--action-bg);
54
+ position: relative;
55
+ cursor: pointer;
56
+ transition: background 0.2s ease;
57
+ }
58
+ .toggle::after {
59
+ content: "";
60
+ position: absolute;
61
+ top: var(--_pad);
62
+ left: var(--_pad);
63
+ width: calc(var(--_h) - 2 * var(--_pad) - 2 * var(--border-w));
64
+ height: calc(var(--_h) - 2 * var(--_pad) - 2 * var(--border-w));
65
+ border-radius: 50%;
66
+ background: currentColor;
67
+ opacity: 0.45;
68
+ transition: translate 0.2s ease, opacity 0.2s ease;
69
+ }
70
+ .toggle:checked { background: var(--accent); border-color: transparent; }
71
+ .toggle:checked::after { translate: calc(var(--_w) - var(--_h)) 0; background: var(--on-accent); opacity: 1; }
72
+ .toggle:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
73
+
74
+ /* ---- toggle chip: interest / filter picker — shows + off, × on ----
75
+ <label class="chip chip--toggle"><input type="checkbox">Nature</label> */
76
+ .chip--toggle { cursor: pointer; padding-right: var(--sp-2); }
77
+ .chip--toggle > input { appearance: none; position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
78
+ .chip--toggle::after {
79
+ content: "";
80
+ width: var(--icon-xs);
81
+ height: var(--icon-xs);
82
+ flex: none;
83
+ background: currentColor;
84
+ mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
85
+ -webkit-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>') center / contain no-repeat;
86
+ }
87
+ .chip--toggle:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
88
+ .chip--toggle:has(:checked)::after {
89
+ mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>');
90
+ -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>');
91
+ }
92
+ .chip--toggle:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
93
+ .chip--toggle:has(:checked) { font-style: italic; } /* the interest-picker flourish; drop if unwanted */
94
+
95
+ /* ---- chip grid: pick one slot / many options in a grid ----
96
+ <div class="chip-grid"><label class="chip chip--pick"><input type="radio" name="t">9:00</label>…</div> */
97
+ .chip-grid {
98
+ display: grid;
99
+ grid-template-columns: repeat(auto-fill, minmax(min(5.5rem, 100%), 1fr));
100
+ gap: var(--sp-2);
101
+ }
102
+ /* .chip hugs its text as a loose grid child (justify-self: start); inside a
103
+ chip grid the whole point is equal, full-width pick targets, so stretch.
104
+ Matters most in scripts without word spaces (Thai): min-content is the
105
+ whole string, tracks get wide, and hugging chips leave a ragged grid. */
106
+ .chip-grid > .chip { justify-self: stretch; justify-content: center; min-height: var(--btn-h); padding-inline: var(--sp-2); position: relative; }
107
+ .chip--pick { cursor: pointer; }
108
+ .chip--pick > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
109
+ .chip--pick:has(:checked) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
110
+ .chip--pick:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-1); }
111
+
112
+ /* ---- day strip: a week, one day selected ----
113
+ <div class="day-strip" role="tablist"><button class="day" role="tab" aria-selected="true"><span class="day__name">Thu</span><span class="day__num">16</span></button>…</div> */
114
+ .day-strip { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--sp-1); }
115
+ .day {
116
+ display: grid;
117
+ /* three rows, always: name · number · dot. The dot's row is reserved even on
118
+ a day that has no dot, so every number sits on the same line and every dot
119
+ sits on the same line — otherwise a marked day's rows share the height
120
+ differently and its number rides up. */
121
+ grid-template-rows: auto auto var(--dot-xs);
122
+ align-content: center; /* the group sits centred in the cell, not spread out */
123
+ justify-items: center;
124
+ gap: var(--sp-0);
125
+ min-width: 0;
126
+ min-height: var(--bar-h);
127
+ padding-block: var(--sp-3); /* equal air top and bottom, so the selected pill
128
+ is the same shape on every day */
129
+ padding-inline: 0;
130
+ border: 0;
131
+ border-radius: var(--r-chip);
132
+ background: transparent;
133
+ color: inherit;
134
+ opacity: 0.6;
135
+ }
136
+ .day__name { font: var(--type-caption); }
137
+ .day__num { font: 700 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
138
+ .day[aria-selected="true"] { background: var(--accent); color: var(--on-accent); opacity: 1; }
139
+ .day__dot { grid-row: 3; align-self: center; width: var(--dot-xs); height: var(--dot-xs); border-radius: 50%; background: currentColor; }
140
+
141
+ /* ---- calendar: month grid, 44px cells, one accent day, optional marks ----
142
+ <div class="calendar"><div class="calendar__head">…</div><div class="calendar__grid"><span class="calendar__wd">Mo</span>×7 <button class="calendar__day" style="--start: 3">1</button>…</div></div> */
143
+ .calendar { display: grid; gap: var(--sp-3); }
144
+ .calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
145
+ .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--sp-1); justify-items: center; }
146
+ .calendar__wd { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; color: var(--card-muted); padding-block: var(--sp-2); }
147
+ .card--3 .calendar__wd { color: inherit; opacity: 0.7; }
148
+ .calendar__day {
149
+ width: 100%;
150
+ max-width: var(--tap);
151
+ aspect-ratio: 1;
152
+ display: grid;
153
+ place-items: center;
154
+ border: 0;
155
+ border-radius: 50%;
156
+ background: transparent;
157
+ color: inherit;
158
+ font: 500 var(--fs-title)/1 var(--font-ui);
159
+ font-variant-numeric: tabular-nums;
160
+ grid-column-start: var(--start, auto); /* --start on the 1st: weekday column 1–7 */
161
+ }
162
+ .calendar__day--mark { background: var(--accent-soft); }
163
+ .calendar__day--mark2 { background: var(--accent-mid); color: var(--on-accent); }
164
+ .calendar__day[aria-selected="true"] { background: var(--accent); color: var(--on-accent); font-weight: 700; }
165
+ .calendar__day:disabled { opacity: 0.3; }
166
+
167
+ /* ---- mood: five dots in a pill, the picked one grows ----
168
+ <fieldset class="mood"><label class="mood__opt"><input type="radio" name="mood"><span class="mood__dot"></span></label>×5</fieldset> */
169
+ .mood {
170
+ margin: 0;
171
+ min-inline-size: 0; /* fieldset UA default is min-content — it must shrink with the card */
172
+ padding: var(--sp-1);
173
+ border: var(--border-w) solid var(--outline);
174
+ border-radius: var(--r-chip);
175
+ background: var(--action-bg);
176
+ color: var(--action-ink);
177
+ display: flex;
178
+ justify-content: space-between;
179
+ }
180
+ .mood__opt { flex: 1 1 0; max-width: var(--tap); min-width: 0; height: var(--tap); display: grid; place-items: center; position: relative; cursor: pointer; }
181
+ .mood__opt > input { appearance: none; position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
182
+ .mood__dot {
183
+ width: var(--icon-xs);
184
+ height: var(--icon-xs);
185
+ border-radius: 50%;
186
+ background: var(--_mood, currentColor);
187
+ transition: scale 0.2s ease, box-shadow 0.2s ease;
188
+ }
189
+ .mood__opt:nth-child(1) { --_mood: var(--neg); }
190
+ .mood__opt:nth-child(2) { --_mood: color-mix(in srgb, var(--neg) 50%, var(--dot-b)); }
191
+ .mood__opt:nth-child(3) { --_mood: var(--dot-b); }
192
+ .mood__opt:nth-child(4) { --_mood: color-mix(in srgb, var(--pos) 50%, var(--dot-b)); }
193
+ .mood__opt:nth-child(5) { --_mood: var(--pos); }
194
+ .mood__opt:has(:checked) > .mood__dot { scale: 1.7; box-shadow: 0 0 0 var(--sp-2) color-mix(in srgb, var(--_mood, currentColor) 25%, transparent); }
195
+ .mood__opt:has(:focus-visible) { outline: var(--focus); outline-offset: var(--sp-0); border-radius: 50%; }
196
+
197
+ /* ---- pin: big code entry, one digit per pill ----
198
+ <div class="pin"><input class="pin__cell" inputmode="numeric" maxlength="1" placeholder=" ">×4</div> */
199
+ .pin { display: flex; gap: var(--sp-3); }
200
+ .pin__cell {
201
+ flex: 1;
202
+ min-width: 0;
203
+ max-width: var(--fab);
204
+ aspect-ratio: 2 / 3;
205
+ border: var(--border-w) solid var(--outline);
206
+ border-radius: var(--r-chip);
207
+ background: var(--action-bg);
208
+ color: var(--action-ink);
209
+ text-align: center;
210
+ font: var(--type-stat);
211
+ font-variant-numeric: tabular-nums;
212
+ caret-color: currentColor;
213
+ }
214
+ .pin__cell:not(:placeholder-shown) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
215
+ .pin__cell:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
216
+
217
+ /* ---- composer: big text area with a bottom bar (icon · hint · send) ----
218
+ <div class="composer"><textarea class="composer__input" placeholder="What do you need?"></textarea>
219
+ <div class="composer__bar"><button class="icon-btn">…</button><span class="composer__hint">Your idea is born</span><button class="icon-btn icon-btn--invert">↑</button></div></div> */
220
+ .composer { display: grid; gap: var(--sp-3); }
221
+ .composer__input {
222
+ width: 100%;
223
+ min-height: 11rem;
224
+ resize: none;
225
+ border: 0;
226
+ padding: 0;
227
+ background: transparent;
228
+ color: inherit;
229
+ font: 700 var(--fs-2xl)/1.25 var(--font-display);
230
+ letter-spacing: var(--track-h2);
231
+ outline: none;
232
+ }
233
+ .composer__input::placeholder { color: inherit; opacity: 0.4; }
234
+ .composer__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
235
+ .composer__hint { font: var(--type-caption); color: var(--card-muted); }
236
+ .card--3 .composer__hint { color: inherit; opacity: 0.7; }
237
+
238
+ /* ---- slider: one value along a track, a thumb-size knob ----
239
+ <label class="slider"><span class="slider__label">ขนาดตัวหนังสือ</span><input class="slider__range" type="range" min="100" max="160" step="15" aria-label="ขนาดตัวหนังสือ"></label>
240
+ The label row is optional: in a bar under a card that already says what the
241
+ slider does, the input's aria-label is enough. The track is a hairline rail;
242
+ the knob is a full --tap, and the whole range is --tap tall, so the thumb has
243
+ a real target anywhere along it. The value is the input's own. */
244
+ .slider { display: grid; gap: var(--sp-1); flex: 1; min-width: 0; position: relative; } /* relative: the glyph rides the knob */
245
+ /* ---- the sample glyph: one letter IN the knob, on-accent, at the type size
246
+ the slider sets — so a text-size slider shows the size before anything is
247
+ read (Lh, 2026-09-12). cardds.js publishes the knob's place on the .slider
248
+ as --slider-at (0..1) from the input's value; the glyph sits over the knob
249
+ and never takes the pointer. <span class="slider__glyph" aria-hidden="true">ก</span>
250
+ before the input. */
251
+ .slider__glyph {
252
+ position: absolute;
253
+ bottom: 0;
254
+ left: calc(var(--tap) / 2 + (100% - var(--tap)) * var(--slider-at, 0));
255
+ width: var(--tap);
256
+ height: var(--tap);
257
+ /* the eye centres the INK, not the line box: a letter that stands on the
258
+ baseline (ก) has no descender, while the face's box keeps 0.30em under it
259
+ (fonts.css) — so the ink lands low when the box is centred; lifted 0.15em
260
+ (set by eye with Lh on FC Pride at h2, 2026-09-12) */
261
+ translate: -50% -0.15em;
262
+ display: grid;
263
+ place-items: center;
264
+ font: var(--type-h2);
265
+ color: var(--on-accent);
266
+ pointer-events: none;
267
+ }
268
+ .slider__label { font: var(--type-label); }
269
+ .slider__range {
270
+ appearance: none;
271
+ width: 100%;
272
+ height: var(--tap);
273
+ margin: 0;
274
+ background: transparent;
275
+ cursor: pointer;
276
+ }
277
+ .slider__range::-webkit-slider-runnable-track { height: var(--sp-1); border-radius: var(--r-chip); background: var(--ink-rail); }
278
+ .slider__range::-webkit-slider-thumb {
279
+ appearance: none;
280
+ width: var(--tap);
281
+ height: var(--tap);
282
+ margin-top: calc((var(--sp-1) - var(--tap)) / 2);
283
+ border-radius: 50%;
284
+ background: var(--accent);
285
+ border: var(--border-w) solid var(--on-accent);
286
+ box-shadow: var(--shadow-float);
287
+ }
288
+ .slider__range::-moz-range-track { height: var(--sp-1); border-radius: var(--r-chip); background: var(--ink-rail); }
289
+ .slider__range::-moz-range-thumb {
290
+ width: var(--tap);
291
+ height: var(--tap);
292
+ border-radius: 50%;
293
+ background: var(--accent);
294
+ border: var(--border-w) solid var(--on-accent);
295
+ box-shadow: var(--shadow-float);
296
+ }
297
+ .slider__range:focus-visible { outline: var(--focus); outline-offset: var(--sp-1); }
package/css/fonts.css ADDED
@@ -0,0 +1,37 @@
1
+ /* ============================================================
2
+ cardds/fonts.css — the two faces the system ships (Lh, 2026-09-10).
3
+ Body: PK Nonthaburi (Pocket Fonts, demo weight — one face; the browser
4
+ synthesises bold for labels and titles, so the 400/700 contrast holds).
5
+ Display: FC Pride (Fontcraft Studio, Medium — non-commercial licence);
6
+ declared across every weight so headings render the real Medium, never
7
+ a smeared faux-bold. Both cover Thai + Latin + digits. Licences: fonts/LICENSES.md.
8
+ tokens.css points --font-ui and --font-display at these; a consumer who
9
+ vendors the CSS copies fonts/ beside it (the url()s are ../fonts/).
10
+ ============================================================ */
11
+ @font-face {
12
+ font-family: "PK Nonthaburi";
13
+ src: url("../fonts/pk-nonthaburi-demo.ttf") format("truetype");
14
+ font-weight: 400 500;
15
+ font-style: normal;
16
+ font-display: swap;
17
+ /* the box the line-height centres = the ink: tone marks reach 1.02em up,
18
+ lower vowels 0.30em down (measured; the face declares 0.92 / 0.30 + gap) */
19
+ ascent-override: 102%;
20
+ descent-override: 30%;
21
+ line-gap-override: 0%;
22
+ }
23
+ @font-face {
24
+ font-family: "FC Pride";
25
+ src: url("../fonts/fc-pride-medium.otf") format("opentype");
26
+ font-weight: 100 900;
27
+ font-style: normal;
28
+ font-display: swap;
29
+ /* the face declares 1.20 up / 0.18 down, but a stacked vowel + tone mark
30
+ reaches 1.10em and สระอู 0.29em — override so the line box centres the
31
+ ink and a clipped heading loses the same hair top and bottom, not the
32
+ whole lower vowel. With the leading in tokens.css this is 0.075em a side
33
+ at h2; the clipped containers pad that back (card.css, actions.css) */
34
+ ascent-override: 110%;
35
+ descent-override: 30%;
36
+ line-gap-override: 0%;
37
+ }