@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/README.md ADDED
@@ -0,0 +1,562 @@
1
+ # cardds
2
+
3
+ Card-first mobile design system, **React-first**: `src/` is the component
4
+ library — one thin component per pattern, emitting exactly the markup the CSS
5
+ documents — and `app/` is the Commons Time Bank PWA built from it (the twelve
6
+ screens of the Claude Design handoff, as real routes). The CSS (`css/*.css`)
7
+ stays the only truth: a component never styles anything, it only picks classes
8
+ from props. The CSS also works alone, with no build, for a host that wants
9
+ class names rather than components; `demo/` keeps the CSS spec pages the
10
+ acceptance tests drive.
11
+
12
+ Repo: **https://github.com/everysundays/cardds** (private — this is the
13
+ source of truth; every consuming project vendors a copy from here, never
14
+ the other way round).
15
+
16
+ **Wireframe by default** — grayscale with visible outlines. Color is opt-in,
17
+ lives only in one file, applied with one attribute. Two faces ship with it
18
+ (`fonts/`, `css/fonts.css`): PK Nonthaburi for text, FC Pride for headings
19
+ and big numbers — both Thai + Latin, both single-weight demo/non-commercial
20
+ licences (`fonts/LICENSES.md`).
21
+
22
+ ## Use
23
+
24
+ ```bash
25
+ npm install
26
+ npm run dev # the app (app/) on http://localhost:5174 — the twelve screens, /screens lists them
27
+ npm run build # the library: icons → src/type/icons.ts · tsc → dist/ (ESM + .d.ts) · dist/cardds.css (flattened)
28
+ npm run build:app # the PWA → app/dist (manifest, service worker, icons)
29
+ npm test # Playwright: the step/sheet/centre geometry on demo/, the app's screens and the four handoff upgrades
30
+ ```
31
+
32
+ ```tsx
33
+ import { Screen, TopBar, Step, StepCard, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from 'cardds';
34
+ import 'cardds/dist/cardds.css'; // or cardds/cardds.css with the css/ folder beside it
35
+ ```
36
+
37
+ `src/index.ts` imports `cardds.js` for its side effects, so a React host gets
38
+ the behaviours for free: the `.sheet` handle (tap, drag, keyboard) with the
39
+ focus and on-screen-keyboard raises, `.sheet-stack--tap` opening and closing,
40
+ the `.pin` auto-advance (a digit moves on, Backspace moves back, a paste fills
41
+ the code), a card heading that needs more than two lines squeezing its type
42
+ (see *Anatomy*), and the `.dropdown` writing its picked value back. `Icon`
43
+ inlines the 56 symbols of `icons.svg` at build time, so no sprite file ships.
44
+
45
+ **The app** (`app/`): Vite + React Router, `cardds` aliased to `src/` so the
46
+ app and the package share one truth with no build between them. Twelve
47
+ screens from `docs/handoff/commons-time-bank-SPEC.md` — welcome, install (the
48
+ real `beforeinstallprompt`), verify phone, home, browse, detail, matches,
49
+ empty, the two menus (sheet stacks), post, profile. `app/README.md` records
50
+ what the port changed against the mockups and why.
51
+
52
+ **CSS only** (no React, no build):
53
+
54
+ ```html
55
+ <link rel="stylesheet" href="cardds.css">
56
+ <script src="cardds.js" defer></script> <!-- optional: the behaviours above -->
57
+ <html data-palette="clay"> <!-- omit for b&w wireframe -->
58
+ ```
59
+
60
+ Everything renders without `cardds.js`: a dropdown still opens (a native
61
+ popover) but won't update its label, a stack still displays but won't open, a
62
+ sheet renders every state but only moves when a class changes, a pin is six
63
+ plain inputs.
64
+
65
+ `demo/index.html` is the CSS spec — every component in one page, palette
66
+ switcher top-right. `demo/sheet.html` — the single sheet at phone height: a step
67
+ that reads, a step that acts, the overlay stage; `demo/stack.html` — the sheet
68
+ stack: the menu and the display variant; `demo/elements.html` — in-card
69
+ elements and layovers grown from the timebanking board. Serve the repo root
70
+ (`python3 -m http.server 4174`) and open `/demo/`. These pages are the fixtures
71
+ of `tests/step.spec.js` and `tests/centre.spec.js`; `tests/app.spec.js` drives
72
+ the app. Nothing here is verified by eye.
73
+
74
+ ### In another project
75
+
76
+ **React (the main way, 2026-09-16):** the package is `@usableapp/cardds` on npm
77
+ (published from a `v*` tag by `.github/workflows/publish.yml`, trusted publishing).
78
+
79
+ ```bash
80
+ npm i @usableapp/cardds
81
+ ```
82
+
83
+ ```tsx
84
+ import { Card, CardHead, Btn } from '@usableapp/cardds';
85
+ import '@usableapp/cardds/dist/cardds.css'; // the CSS, fonts resolve from the package
86
+ ```
87
+
88
+ Browse what exists at `npm run dev` → `/gallery` in this repo (every component, every story, searchable).
89
+
90
+ **CSS only:** there's no build and no dependencies, so installing is cloning once, then
91
+ copying the static files into your project — never edit the clone, never
92
+ fork the CSS:
93
+
94
+ ```bash
95
+ # once, wherever you keep it (~/Sites/cardds is the convention on this machine)
96
+ git clone git@github.com:everysundays/cardds.git ~/Sites/cardds
97
+ # to update it later
98
+ git -C ~/Sites/cardds pull
99
+
100
+ # then vendor the static files into the consuming project
101
+ cp -R ~/Sites/cardds/css ~/Sites/cardds/fonts ~/Sites/cardds/cardds.css \
102
+ ~/Sites/cardds/cardds.js ~/Sites/cardds/icons.svg \
103
+ ./assets/
104
+ ```
105
+
106
+ `cardds.css` is just the import bundle for `css/*.css`, so keep the `css/`
107
+ folder next to it, and `fonts/` beside that (`css/fonts.css` reaches the faces
108
+ as `../fonts/`). `icons.svg` is referenced by path from your markup
109
+ (`<use href="icons.svg#bell">`), so put it where those references resolve.
110
+ Then follow the rules below — the short version is: tune `palette.css` and
111
+ nothing else, and never write a raw hex or px in a component.
112
+
113
+ The repo is private, so cloning needs an account with access (`everysundays`
114
+ on GitHub) — set that up as an SSH host alias if you already use a different
115
+ account for `git@github.com` day to day, the way this machine's `~/.ssh/config`
116
+ does it for other `everysundays` repos.
117
+
118
+ **Recording what you copied matters.** A vendored copy with no record of which
119
+ commit it came from can't be told apart from a fork. timebank's
120
+ `scripts/cardds-vendor.mjs` is the reference: `npm run cardds:sync` copies the
121
+ files above from a local checkout (`CARDDS_SRC`, default `~/Sites/cardds`) and
122
+ writes `VENDORED.json` — commit hash, date, a sha256 per file — so
123
+ `npm run check:cardds` can catch a hand edit or a stale copy. `git pull` that
124
+ checkout before syncing to pick up what's been pushed here.
125
+
126
+ Working with Claude in another project? The `/cardds` skill
127
+ (`~/.claude/skills/cardds/`) carries the install and the contract, and points
128
+ back here. It is deliberately thin — **this repo stays the source of truth**,
129
+ so new components and rule changes go here, and the skill only gets updated
130
+ if the contract itself changes.
131
+
132
+ ### React adapter — how a component is written
133
+
134
+ `src/<group>/<Name>.tsx`, one export per class family; sub-parts are their own
135
+ components (`CardHead`, `KvRow`, `TrackStep`, `SheetBody`). A component
136
+ chooses classes from props (`tone`, `state`, `primary`, `sm`…) and lays the
137
+ contract's children in order — nothing else. Behaviour lives in `cardds.js`
138
+ (delegated, class toggles) and a component only surfaces it: `Pin` listens
139
+ for `cardds:pin` and calls `onChange(code, complete)`; `Sheet` sets the state
140
+ class, `cardds.js` moves it. Never fix a look in a wrapper: fix the CSS, the
141
+ wrapper follows. **Claude Design** builds with these components through
142
+ `/design-sync` (`.design-sync/`: the sync config, the authored preview
143
+ stories, the conventions the design agent reads) — a new component needs a
144
+ preview story there.
145
+
146
+ ## Sheet stack markup
147
+
148
+ The sheet stack (`.sheet-stack`, `stack.css` — the menu, see *Anatomy*) is the
149
+ one component whose markup has rules, since a sheet's place in the stack comes
150
+ from its position among its siblings. A stacked sheet is written as a `.card`:
151
+ the card is the material, the sheet is what it is in the stack.
152
+
153
+ ```html
154
+ <div class="sheet-stack sheet-stack--tap"> <!-- drop --tap for a static stack -->
155
+ <article class="card card--2">
156
+ <div class="card__head">
157
+ <button class="icon-btn icon-btn--sm card__back" aria-label="Back">…</button>
158
+ <h2 class="t-h2 t-caps">Shopping</h2>
159
+ <span class="icon-row">…</span> <!-- optional trailing icons -->
160
+ </div>
161
+ …the sheet's own content…
162
+ </article>
163
+ <article class="card card--3">…</article>
164
+ <article class="card">…</article> <!-- last = the front sheet -->
165
+ </div>
166
+ ```
167
+
168
+ - **Sheets must be direct children** of `.sheet-stack`. Order is the stack
169
+ order: first = back, last = the front sheet that shows its full content.
170
+ - **`--closed`** (with `--tap`): nothing open on arrival — every sheet, the
171
+ front one too, peeks at the stack's bottom; the headroom above is the base's.
172
+ An optional `<div class="sheet-stack__base">` — written AFTER the sheets, so
173
+ it takes no sheet slot — is what the base shows there, behind every sheet.
174
+ A tap opens any sheet over it; back drops it home.
175
+ - **Five sheets max.** Slots are declared per `:nth-child` in `stack.css`; a
176
+ sixth would render at slot 0, on top of the first.
177
+ - **Every head needs its own `.card__back`** if the stack is `--tap`. It stays
178
+ hidden until that sheet is open, then slides into a reserved header slot.
179
+ - **No ids, no wiring.** `cardds.js` finds any `.sheet-stack--tap` on the page.
180
+ - **`.card-stack` is gone.** That was this component's first name and the wrong
181
+ one — the members are sheets and the stack is the menu, not a list of cards.
182
+ Never bring it back, not even as an alias.
183
+
184
+ ## Step markup
185
+
186
+ A step is one screen, one task (see *Anatomy*). It is the other component
187
+ whose markup has rules, because the sheet's box is the step itself:
188
+
189
+ ```html
190
+ <!-- reads: card only -->
191
+ <section class="step">
192
+ <article class="card step__card">
193
+ <div class="card__head"><div><span class="t-overline t-muted">ขั้นที่ 2 จาก 4</span><h2 class="t-h2">…</h2></div></div>
194
+ <p class="t-body">…</p>
195
+ </article>
196
+ </section>
197
+ <div class="action-bar action-bar--pager">…</div>
198
+
199
+ <!-- acts: card + sheet -->
200
+ <section class="step">
201
+ <article class="card step__card">…the words…</article>
202
+ <article class="card sheet sheet--half" data-sheet-states="peek half full">
203
+ <button class="sheet__handle" type="button" aria-label="ปรับความสูง"></button>
204
+ <div class="card__head"><h2 class="t-h2">…</h2><span class="chip">…</span></div>
205
+ <div class="sheet__body">
206
+ …cardds controls (.field / .pin / .composer / .rows + .check / .segment /
207
+ .chip-grid), or small content cards (.card--sm)…
208
+ </div>
209
+ </article>
210
+ </section>
211
+ <div class="action-bar">…one wide button…</div>
212
+ ```
213
+
214
+ - **`.step` needs a bounded column**: a direct child of `.screen--fill`, or
215
+ of an app section with a definite height. It is a flex child that fills the
216
+ column between the top bar's reserve and the bar's, and clips at its own
217
+ bottom edge — which is what makes the sheet stop above the bar. In a plain
218
+ scrolling `.screen` it has nothing to fill.
219
+ - **The sheet is full-width; the card keeps the inset.** The step bleeds to
220
+ the screen's edges (reading `--screen-pad`, as `.sheet-stack` does) and the
221
+ card takes the inset back as its side margins — so the card and the bar's
222
+ buttons stand on the inset line, and the sheet runs edge to edge like the
223
+ bar and like a sheet in a `.sheet-stage`.
224
+ - **`.step__card` first, `.sheet` last.** The card carries the step's words
225
+ and is a normal card: head · content · optional foot. The sheet is
226
+ **handle · head · body**: the handle, then a `.card__head` title row (that
227
+ row is what `peek` shows — heading left, the contextual thing right), then
228
+ `.sheet__body` with what the member acts on. The handle and the head are
229
+ pinned; the body is the one thing that scrolls, to the card's own edges. A
230
+ sheet with no head (the dim page sheet) is a body alone.
231
+ - **The step follows its top bar directly, and touches it.** `full` means
232
+ the sheet's top edge at the bar's bottom edge — nothing above it but the
233
+ bar. So the room the screen would have left between the bar and the step
234
+ moves inside the step, above the card: `.topbar + .step` cancels the
235
+ screen's gap (`--screen-gap`), `.appbar + .step` cancels the reserve's
236
+ breathing (`--header-gap`), and the card takes that room as its lead. A
237
+ step that follows anything else keeps the screen's gap, and the sheet
238
+ stops under that thing.
239
+ - **The bar is outside the step**, after it. `.step` reads the bar's room
240
+ from `--bar-reserve`; any parent that hosts the `.action-bar` counts
241
+ (`<body>`, an app shell, a demo frame).
242
+ - **A sheet inside a step never carries the step's action.** The bar is the
243
+ one move. A sheet gets its own buttons only on a screen that has no bar —
244
+ the map sheet in `sheet.html`, whose "Directions" is that screen's move.
245
+ - **Never write a sheet's height, and never a `.sheet-stage` inside a step.**
246
+ State is a class (`sheet--peek` / `--half` / `--full`); the sheet is always
247
+ the step's full height and only slides. `carddsSheetSet(sheet, state)` is
248
+ the one way to change it from script, so listeners hear `cardds:sheet`.
249
+
250
+ ## Centred card markup
251
+
252
+ One card at the human centre — the introduction screens, and any screen with
253
+ one thing to read and one move. The heading above, the bar below, the card
254
+ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
255
+
256
+ ```html
257
+ <div class="screen screen--fill">
258
+ <header class="topbar"><h1 class="t-h1">ธนาคารเวลายินดีต้อนรับ</h1></header>
259
+ <article class="card card--centre">
260
+ <div class="card__head"><div><span class="t-overline t-muted">ขั้นที่ 1 จาก 4</span><h2 class="t-h2">ปรับขนาดตัวหนังสือ</h2></div></div>
261
+ <p class="t-body">…</p>
262
+ </article>
263
+ </div>
264
+ <div class="action-bar action-bar--tiers">
265
+ <div class="action-bar__tier"><label class="slider"><input class="slider__range" type="range" min="100" max="160" step="15" aria-label="ขนาดตัวหนังสือ"></label></div>
266
+ <div class="action-bar__tier action-bar__tier--pager">
267
+ <button class="icon-btn" aria-label="ย้อนกลับ">…</button>
268
+ <button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
269
+ </div>
270
+ </div>
271
+ ```
272
+
273
+ - **The card floats in the fill screen** (`.screen--fill` is its box), hugs its
274
+ words, keeps the screen's inset, and is capped so it never reaches the title
275
+ bar above it nor the bar's room below — past that it scrolls inside. Never
276
+ give it a height; never move it with a margin. The cap is twice the room
277
+ above the centre (2 × (40% − the title bar)): a card taller than that is
278
+ not a centred card — it is a plain scrolling `.screen` (the detail view).
279
+ - **A small group at the same centre** — an empty state; a card with its quiet
280
+ second button and the pager dots under it; a deck with its two round
281
+ buttons — is `.screen__centre` (React: `Centre`), a block in the fill
282
+ screen after the topbar. Same placement and cap; it runs edge to edge with
283
+ the inset as padding, so a deck's fanned cards may overhang and clip at the
284
+ screen's edges (the fold visual, not a bug), and stacks its children at the
285
+ screen's gap. Never hand-lay `top: 40%` on a screen.
286
+ - **The bar is outside, as always.** Plain `.action-bar` for one move; with a
287
+ control that belongs to the screen (a slider), `.action-bar--tiers` stacks a
288
+ control tier above the buttons and publishes the taller reserve
289
+ (`--bar-reserve`) from whatever parent hosts the bar.
290
+ - **The round pager** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
291
+ holding two `.icon-btn`s and nothing else: they stand at the two edges. The
292
+ step that commits still goes back to one wide button (rule 3).
293
+
294
+ ## Files
295
+
296
+ | file | what | touch it? |
297
+ |---|---|---|
298
+ | `css/fonts.css` + `fonts/` | the two `@font-face`s: PK Nonthaburi (body, `--font-ui`), FC Pride (display, `--font-display`); the font files and their licences | when swapping a face |
299
+ | `css/tokens.css` | spacing, radius, type levels, motion (`--motion-sheet`), the human centre (`--screen-centre`), grayscale role defaults; registers the two sheet tokens (`--sheet-peek`, `--sheet-half`) so script can read them resolved | rarely |
300
+ | `css/palette.css` | **color settings — palettes as role-token overrides** | yes, this one |
301
+ | `css/base.css` | reset, surface, typography classes, `.section` | rarely |
302
+ | `css/card.css` | `.card` + its skeleton (`.card__head` / content / `.card__foot`, heading squeeze), `.card-list`, `.card--row`, `.icon-row`, stats, `.card--centre` (one card at the human centre of a fill screen) and `.screen__centre` (a small group there) | when adding variants |
303
+ | `css/stack.css` | **sheet stack** — `.sheet-stack`: the menu, sheets stacked (heads peek, up to 5; `--tap` tap-to-open, the back button dissolves in; `--closed` nothing open, heads at the bottom, the base above (`.sheet-stack__base`); `--display` the title is the peek, `--fanned` wider peeks); always edge to edge; a stacked sheet is a `.card` | rarely |
304
+ | `css/sheet.css` | **sheet single** — `.sheet`: a card as bottom sheet, full height always, a state is how far it slid: `--peek` / `--half` / `--full` (translate only); handle · head · `.sheet__body` (the head pinned, the body scrolls); `.sheet-stage` (overlay stage: map sheet, a `__hero` — one thing centred in the band a half sheet leaves open, `--dim`, `--raised`, ask drawer) | when adding states |
305
+ | `css/step.css` | `.step` — one screen, one task: `.step__card` (reads) + optional `.sheet` (acts), the bar's room, the keyboard's room | rarely |
306
+ | `css/forms.css` | `.field` (outlined input, label = placeholder), `.add-row` | when adding controls |
307
+ | `css/journey.css` | `.route`, `.tile-badge`, `.note-row` — trip/status primitives | when adding variants |
308
+ | `css/actions.css` | topbar, chips, buttons (`.btn--xl`), `.dropdown` (the standard select: pill + `.menu` popover), `.action-bar` (+ `--pager` for previous/next, as labels or as two round buttons; `--tiers` for a control row above the buttons), `.fab` | when adding actions |
309
+ | `css/numbers.css` | `.ring` gauge, `.track` steps, `.card--band` + `.band-stack`, `.dotgrid`, `.picker`, `.badge`, `.bars` | big numbers |
310
+ | `css/people.css` | `.avatar` (sm/lg/xl, outline, add, on, halo), `.avatar-stack`, `.avatar-pick` | people |
311
+ | `css/choice.css` | `.check`, `.toggle`, `.chip--toggle`, `.chip-grid` + `.chip--pick`, `.day-strip`, `.calendar`, `.mood`, `.pin`, `.composer`, `.slider` | choice controls |
312
+ | `css/lists.css` | `.row` / `.rows` (frameless rows in a card), `.kv`, `.kv-grid`, `.timeline`, `.legend`, `.link` | lists inside cards |
313
+ | `css/media.css` | `.card--cover`, `.quote`, `.tile-grid` + `.tile`, `.card--fold`, `.mosaic`, `.wave` | media & display |
314
+ | `css/layover.css` | `.drawer` (ask), `.menu` (popover), `.banner` (details), `.float-bar`, `.callout`, `.chip--bubble`, `.deck` | cards over content |
315
+ | `icons.svg` | Lucide sprite (ISC), 56 minimal stroke icons | add symbols as needed |
316
+ | `src/` → `dist/` | the React library: `src/<group>/<Name>.tsx`, one thin component per pattern, JSDoc = the rule; built by `npm run build` (tsc) into `dist/` with `.d.ts`; `src/type/icons.ts` is generated from `icons.svg` | when adding a component |
317
+ | `app/` | the Commons Time Bank PWA: `app/src/screens/*.tsx` (the twelve screens), `App.tsx` (the routes), `install.ts` (the install prompt), `public/` (manifest, `sw.js`, icons); `npm run dev` / `build:app` | the product |
318
+ | `demo/` | the CSS spec pages (`index`, `sheet`, `stack`, `elements`) — CSS only, and the fixtures of the geometry tests | when the CSS contract changes |
319
+ | `docs/handoff/` | the Claude Design handoff spec the app was ported from | reference |
320
+ | `scripts/` | `build-icons.mjs` (icons.svg → `src/type/icons.ts`), `flatten-css.mjs` (`cardds.css` + imports → `dist/cardds.css`) | rarely |
321
+ | `docs/guides/` | `building-with-cardds.md` — the anatomy, the step, the two sheet systems and the vocabulary in React terms; shipped to Claude Design as its guidelines | when the contract changes |
322
+ | `cardds.js` | the behaviours (delegated, class/property toggles only — everything renders without it): dropdown value sync, `.sheet-stack--tap` open/close (tolerates one wrapper per sheet; close reads the tapped sheet), `.pin` auto-advance / Backspace / paste + `cardds:pin`, card-heading squeeze (`--squeeze`), the sheet handle (tap / drag-snap / Enter / Space), the focus and keyboard raises (`--kb`), `cardds:sheet` | rarely |
323
+
324
+ ## Anatomy — base, menus, content, and the card skeleton
325
+
326
+ Every screen is the same four things, and every card the same three:
327
+
328
+ - A screen is **base, top menu, bottom menu, content**. The base is the
329
+ surface; the menus are the chrome above and below (`.topbar` / `.appbar`,
330
+ `.action-bar` / `.float-bar`); content is everything between.
331
+ - Content has three homes: **on the base**, **in a card**, **in a bottom
332
+ sheet**. Content normally comes in cards; static data (a blurb, a legal
333
+ note, help steps) may sit on the base directly.
334
+ - **A card is head · content · foot.**
335
+ - **Head** (`.card__head`): the heading on the left; hints, actions or a
336
+ more-menu — the contextual info — at the far right. Two lines is the
337
+ heading's budget; with nothing at the far right it runs all the way
338
+ across. Hard maximum three lines: when the words need more than two,
339
+ the type squeezes to fit (`cardds.js` sets `--squeeze`, `card.css`
340
+ multiplies the level's `--fs-self` by it, and clamps at three).
341
+ - **Content**: whatever the card frames — rows, key-values, a gauge, a
342
+ paragraph. Frameless, per rule 9.
343
+ - **Foot** (`.card__foot`): the card's action buttons on one line. One
344
+ button takes the width; up to four share it in even parts; from three
345
+ on they drop their side padding so the labels keep the room.
346
+ - A page with **one feature and one CTA** (an install page, a single
347
+ decision) may be contained in one card: head, the pitch, foot.
348
+ - **A bottom sheet holds what the member acts on** — controls, or small
349
+ content cards to pick from or read more — and it opens to the full height
350
+ of its box. **Cards are content the system offers**: the words, the
351
+ explanation, the thing to read. Do not put a form in a card when a sheet
352
+ would do, and do not put the step's explanation in a sheet.
353
+ - **Two sheet systems, one material.** *Sheet single* (`.sheet`, `sheet.css`)
354
+ is ONE sheet the member acts in, whatever it holds: a short entry in front
355
+ of a step's card, a place on a map, a payment on a dim page — in a `.step`
356
+ or a `.sheet-stage`, and its state is how far it slid (peek / half / full).
357
+ *Sheet stack* (`.sheet-stack`, `stack.css`) is MANY sheets, each a main
358
+ section of the app, stacked like paper at the middle of the home screen:
359
+ the menu. Heads peek, a tap brings one sheet to the front, back returns it;
360
+ each sheet is the metaphor for the section it opens. Both are cut from the
361
+ same material — a stacked sheet is written as a `.card`, and the single
362
+ sheet is `.card.sheet` — and both obey rule 8: rigid, full height, moved
363
+ by `translate` only. (The stack was first named "card stack": wrong, and
364
+ retired.) Demos: `sheet.html`, `stack.html`.
365
+ - **A step is one screen, one task: `.step`.** What the member reads is a
366
+ card (`.step__card`); what the member does, if the step asks for it, is a
367
+ `.sheet` in front of it; the screen's one move is `.action-bar`, outside
368
+ the step, below it. A step where the member only READS is the card alone.
369
+ A step where the member ACTS (types, picks, shoots a photo, records) adds
370
+ the sheet: peek shows its handle and title row, half shows half of it,
371
+ full slides up until its top edge touches the top bar — and at every one
372
+ of those it stops above the bottom bar, because the step is the box it
373
+ slides in: it starts where the top bar ends and ends where the bar's room
374
+ begins. Nobody lays a step out by hand and nobody writes a
375
+ sheet's height. In short: **member acts → card + sheet, member reads →
376
+ card.** Markup: *Step markup*. Demo: `sheet.html`, `elements.html` #ask,
377
+ `index.html` "step".
378
+
379
+ - **Where the centre is.** To the person holding a phone, the bottom fifth of
380
+ the screen is the thumb's — the bar, the controls — so what is meant to be
381
+ read sits at the middle of the four fifths above it: **the human centre is
382
+ 40% down the screen, not 50%** (`--screen-centre`, Lh 2026-09-10). A screen
383
+ with one card and nothing else to read puts it there (`.card--centre`, the
384
+ introduction screens); a splash puts its mark there.
385
+
386
+ Demo: the *skeleton* block at the top of `elements.html`.
387
+
388
+ ## Rules of the system
389
+
390
+ 1. **One card = one boundary.** A card frames one thing: a category, a stat
391
+ group, a list row. Tone slots: `.card` (card-1), `.card--2`, `.card--3`.
392
+ 2. **Sections are not cards.** Page sections = frameless on-surface content
393
+ (`.section`) + a lone/packed CTA (`.cta-pack`).
394
+ 3. **Surrounding UI is separate.** `.topbar`, `.filter-row`, `.chip-row`,
395
+ `.action-bar` live outside cards, above/below. The bottom bar has two
396
+ arrangements and they mean different things: the default pack (one wide
397
+ CTA, optional round icon) is **one action** on this screen, and
398
+ `.action-bar--pager` (two equal buttons at the edges, indicator between)
399
+ is **a sequence you can walk both ways**. A wide button next to a small
400
+ round one reads as an action plus an afterthought — wrong whenever back
401
+ and next are peers. The step that commits goes back to the default pack.
402
+ 4. **Components reference role tokens only** (`--surface`, `--card-1-bg`,
403
+ `--accent`…), never hex. Derived tints are tokens too (`--outline`,
404
+ `--hairline`, `--ink-tint`, `--ink-rail`, `--accent-soft`, `--scrim`), and
405
+ masks use `currentColor` as "opaque". So palettes swap freely.
406
+ 5. **`.screen` carries the horizontal inset and nothing vertical.** The inset
407
+ is `--screen-pad`; cards and on-surface UI sit flush inside it, with no
408
+ per-component side padding. Top and bottom are zero: vertical room only
409
+ exists as a reserve for something fixed parked over the screen, so each bar
410
+ adds its own — `.appbar` the top (`.screen:has(> .appbar)`), `.action-bar`
411
+ and `.float-bar` the bottom, each from its own height token. Whatever
412
+ parent hosts the bar counts (`:has(> .action-bar) .screen`): `<body>` or an
413
+ app shell alike. A page with no bars needs no override, and `.screen--fill`
414
+ is excluded from the bottom reserves — there a `.step` carries the bar's
415
+ room itself, as a bottom margin, reading the same `--bar-reserve`. The
416
+ reserves use the nominal control height, so a bar whose button wraps to two
417
+ lines can still overlap the last line of content.
418
+ **Padding is published, not copied.** `.screen` sets `--screen-pad` and
419
+ `--screen-gap`, its appbar reserve sets `--header-gap`, and `.card` sets
420
+ `--card-pad` and `--card-gap`; anything that has to cancel a container's
421
+ padding or gap reads the token rather than repeating its value. `.sheet-stack` uses it to
422
+ break out full-bleed when the column touches the viewport edges (≤27em /
423
+ 432px), and `.bleed` uses it for a scrolling track inside a card:
424
+
425
+ ```html
426
+ <div class="avatar-pick bleed">…</div>
427
+ ```
428
+
429
+ The track runs to the card's edges so it reads as scrollable, while the
430
+ first and last item stay on the same line as the heading. Works for any
431
+ horizontal strip — `.chip-row`, a tile grid — in any container that
432
+ publishes `--card-pad`.
433
+ 6. **The sheet stack is the menu, and a sheet is rigid material.**
434
+ `.sheet-stack` (`stack.css`) sheets are tall: heads peek (`--stack-step`,
435
+ up to 5 layers), each head = title + `.icon-row` functions (head text =
436
+ icon height, 28px). On a `.screen--fill` the stack stretches and bleeds
437
+ off the bottom edge — bottom corners square. Markup rules: see *Sheet
438
+ stack markup* above; the two sheet systems: *Anatomy*.
439
+
440
+ `.screen--fill` is exactly one phone height — a height, not a minimum, so
441
+ a `.step` inside it has a bound to fill and its card and sheet scroll inside
442
+ rather than growing the page — and it clips (`overflow: clip`, never
443
+ `hidden`): a clipped box is not a scroll container, so nothing, not even
444
+ the browser revealing a focused field, can shift the screen up.
445
+
446
+ In a `.sheet-stack--tap`, a sheet is rigid material — it moves in, moves
447
+ out, or is covered, but **never changes size**. Every sheet is the same
448
+ full height, set once; a peek is not a short sheet, it's a full sheet the
449
+ next one covers. So `translate` is the only animated property, and
450
+ **z-index is the resting `--level` order and is never touched** — because
451
+ whatever sits in front of the tapped card is exactly what has to leave.
452
+ Opening slides the tapped card up by its own `--slot` × `--stack-step`;
453
+ cards after it move out below (`.is-after`); cards before it tuck one
454
+ step down, staggered nearest-first via `--stagger` (`.is-before`), and
455
+ get covered. Closing reverses it in two phases: the after-pack returns
456
+ while the card drops into its slot, then the before-pack slides back up
457
+ from behind. Slots, offsets and paint order are all CSS `:nth-child`, so
458
+ a stack renders correctly with no script; `cardds.js` only toggles the
459
+ state classes. Don't reintroduce height, `display` or z-index switching
460
+ here — every past bug in this animation came from exactly that.
461
+ 7. **Everything shrinks to 320px.** `.screen > *`, `.card` and `.card > *`
462
+ carry `min-width: 0`, so no child's min-content (an input, a fixed row)
463
+ can widen a card's grid track past its padding; topbar titles ellipsize;
464
+ segment items share width. Never give a component a fixed width that can
465
+ beat the viewport — let it `flex: 1` up to a `max-width` token instead.
466
+ 8. **A single sheet is rigid material, like a sheet in the stack.** It is always the full
467
+ height of the box that holds it (`inset: 0`, set once, never written
468
+ again) and a state is nothing but how far that full card slid:
469
+ `sheet--full` slid nowhere, `sheet--half` slid down by `--sheet-half`
470
+ (50% of itself), `sheet--peek` slid down until only `--sheet-peek` shows —
471
+ the handle and the title row. `translate` is the only animated property
472
+ (`--motion-sheet`), and nothing toggles height, display or z-index for a
473
+ state — the stack's rule, inherited. The box clips whatever slid below its
474
+ bottom edge, so a sheet at any height is still a card: rounded top, a
475
+ straight cut at the box's edge — and its shape is handle · head · body:
476
+ the handle and the title row are pinned, `.sheet__body` scrolls beneath
477
+ them, and the body's `scroll-padding-bottom` is the slid-under part (in
478
+ the sheet's own container units, `--_cover`), so a focused control is
479
+ revealed above the cut, not under it. `--sheet-half` is a share of the
480
+ sheet's height (`0.5`), one number serving both the translate and that
481
+ padding. Two boxes hold sheets and the states
482
+ mean the same in both: `.step` (an app screen, the bar outside — Anatomy)
483
+ and `.sheet-stage` (a page-level overlay: a sheet over a map, the dim page
484
+ sheet `--dim` + `--raised`, the ask drawer `--ask` + `.lift` + `.drawer`).
485
+ A stage keeps the top bar's line free (`--_top`), so `--full` stops under
486
+ `--header-h` and keeps its corners. Both sheet tokens are published — a
487
+ sheet with a taller head retunes `--sheet-peek` — and registered in
488
+ `tokens.css`, which is how `cardds.js` reads them to snap a drag. Demo:
489
+ `sheet.html`, `elements.html`.
490
+ 9. **Elements inside a card are frameless.** Rows, key-values, pickers,
491
+ gauges and tiles never draw a second card boundary; hairlines
492
+ (`--hairline`) separate rows, `--outline` keeps controls legible on
493
+ colored palettes. Data goes in as unitless custom properties
494
+ (`--value`, `--done`, `--total`); the math lives in CSS.
495
+ 10. **The dark slot re-points the accent.** `.card--3` sets `--accent` /
496
+ `--on-accent` to its own ink and bg, so every accent consumer (buttons,
497
+ checks, toggles, rings, selected days) inverts without extra rules.
498
+
499
+ ## New palette
500
+
501
+ Copy any block in `palette.css`, rename, retune:
502
+
503
+ ```css
504
+ [data-palette="mine"] {
505
+ --surface: …; --on-surface: …;
506
+ --card-1-bg: …; --card-1-ink: …; /* + card-2, card-3 */
507
+ --accent: …; --on-accent: …;
508
+ --action-bg: …; --action-ink: …;
509
+ }
510
+ ```
511
+
512
+ ## Scale: everything is rem
513
+
514
+ Every length token is rem (`--sp-4: 1rem`, `--tap: 3rem`, `--r-card: 2rem`,
515
+ `--fs-h1: 1.75rem` …), so the whole system resizes from one number: the root
516
+ font size. `base.css` keeps it at `100%` (the user's text-size setting, 16px by
517
+ default) and steps narrow phones (≤360px) to `93.75%`, which keeps the 25rem
518
+ column, its 48px taps and its type in proportion on 320–360 screens. Only
519
+ `--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
520
+ An app with its own text-size setting publishes it as `--text-scale` on
521
+ `<html>` — a unitless number, `1` by default — and `base.css` multiplies it into
522
+ the root, so a member's "larger" composes with the narrow-phone step rather
523
+ than overwriting it, and the app never writes a length from script.
524
+ Components never write a raw length: spacing from `--sp-*`, control sizes from
525
+ `--tap`, `--tap-lg`, `--btn-h`, `--bar-h`, `--header-h`, `--fab`, `--badge`, container
526
+ padding from `--screen-pad` / `--card-pad`, radii from
527
+ `--r-*`, type from `--fs-*` / `--type-*`. Data (progress, counts) enters as
528
+ unitless custom properties. One measured length comes from script: `--kb`,
529
+ the height an on-screen keyboard covers, published on `:root` by `cardds.js`
530
+ from the visual viewport — as a px length, since only the browser knows it,
531
+ so no component ever writes `px` to consume it (`.action-bar` rides up by it,
532
+ `.step` grows its room by it). The narrow-phone step is an `em` breakpoint,
533
+ so it follows the text size too: at 131% a 390px phone counts as narrow.
534
+
535
+ ## Icons & touch targets
536
+
537
+ Icons come from `icons.svg` (Lucide, ISC — monochrome stroke, currentColor):
538
+
539
+ ```html
540
+ <svg class="icon" aria-hidden="true"><use href="icons.svg#bell"/></svg>
541
+ ```
542
+
543
+ `.icon` = `--icon` (1.75rem / 28px), `.icon--xs` = 1rem inside captions and chips,
544
+ `.icon--lg` = 2rem for empty states. Never raw emoji/unicode glyphs — they
545
+ render as colored emoji on phones. Add icons by appending `<symbol>`s
546
+ (grab from lucide.dev, paste path data).
547
+
548
+ Touch targets: `--tap` 3rem (`.icon-btn`), `--tap-sm` 2.75rem floor (`.icon-btn--sm`,
549
+ selects, add-row), `--tap-lg` 3.5rem for hero controls. `.btn` ≥ `--btn-h` (3.25rem).
550
+ Everything thumb-first.
551
+
552
+ The two bars across the top of a screen — `.topbar` (screen title) and
553
+ `.appbar` (app context) — share one rule and one floor, `--header-h` (3.75rem /
554
+ 60px), so a title-only bar stands as tall as one carrying an icon button, a
555
+ dropdown or a `.btn`. `.screen` reserves its top padding from the same token.
556
+
557
+ ## Typography levels
558
+
559
+ `t-display` 34–42/800 · `t-h1` 28 · `t-h2` 22 · `t-title` 17 · `t-body` 15 ·
560
+ `t-label` 13 · `t-caption` 12 · `t-overline` 11 mono caps · `t-stat` big tabular
561
+ numbers (`t-stat--mono`, `t-stat--xl`). Pixel values are at the default root;
562
+ each is an `--fs-*` rem token. Swap `--font-display` per app for character.
package/cardds.css ADDED
@@ -0,0 +1,18 @@
1
+ /* cardds — card-first mobile design system. One import. */
2
+ @import url("css/fonts.css"); /* the two faces: PK Nonthaburi (body), FC Pride (display) */
3
+ @import url("css/tokens.css"); /* structure + wireframe defaults */
4
+ @import url("css/palette.css"); /* color settings — the file you tune */
5
+ @import url("css/base.css"); /* reset, surface, type levels, section */
6
+ @import url("css/card.css"); /* card boundary + list variants */
7
+ @import url("css/stack.css"); /* sheet stack: .sheet-stack — the menu, sheets stacked, heads peek, tap opens */
8
+ @import url("css/sheet.css"); /* sheet single: .sheet — one card as a bottom sheet, peek / half / full */
9
+ @import url("css/step.css"); /* .step: one screen, one task — card, optional sheet, bar outside */
10
+ @import url("css/forms.css"); /* outlined fields, add-row */
11
+ @import url("css/journey.css"); /* route, tile badge, note row */
12
+ @import url("css/actions.css"); /* topbar, chips, buttons, CTA pack */
13
+ @import url("css/numbers.css"); /* ring, track, band, dot grid, picker, badge, bars */
14
+ @import url("css/people.css"); /* avatar, avatar stack, chooser row */
15
+ @import url("css/choice.css"); /* check, toggle, toggle chip, chip grid, days, calendar, mood, pin, composer */
16
+ @import url("css/lists.css"); /* row, rows, key-value, kv grid, timeline, legend, link */
17
+ @import url("css/media.css"); /* cover card, quote, tile grid, fold, mosaic, wave */
18
+ @import url("css/layover.css"); /* ask drawer, popover menu, banner, float bar, callout, deck */