@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.
- package/README.md +562 -0
- package/cardds.css +18 -0
- package/cardds.js +473 -0
- package/css/actions.css +292 -0
- package/css/base.css +128 -0
- package/css/card.css +283 -0
- package/css/choice.css +297 -0
- package/css/fonts.css +37 -0
- package/css/forms.css +64 -0
- package/css/journey.css +61 -0
- package/css/layover.css +243 -0
- package/css/lists.css +142 -0
- package/css/media.css +232 -0
- package/css/numbers.css +258 -0
- package/css/palette.css +112 -0
- package/css/people.css +79 -0
- package/css/sheet.css +295 -0
- package/css/stack.css +262 -0
- package/css/step.css +71 -0
- package/css/tokens.css +177 -0
- package/dist/actions/Btn.d.ts +311 -0
- package/dist/actions/Btn.js +14 -0
- package/dist/actions/BtnRow.d.ts +5 -0
- package/dist/actions/BtnRow.js +6 -0
- package/dist/actions/Chip.d.ts +328 -0
- package/dist/actions/Chip.js +25 -0
- package/dist/actions/Dot.d.ts +7 -0
- package/dist/actions/Dot.js +6 -0
- package/dist/actions/Dropdown.d.ts +34 -0
- package/dist/actions/Dropdown.js +19 -0
- package/dist/actions/Fab.d.ts +9 -0
- package/dist/actions/Fab.js +7 -0
- package/dist/actions/IconBtn.d.ts +308 -0
- package/dist/actions/IconBtn.js +8 -0
- package/dist/actions/Link.d.ts +301 -0
- package/dist/actions/Link.js +8 -0
- package/dist/actions/Segment.d.ts +11 -0
- package/dist/actions/Segment.js +10 -0
- package/dist/cardds.css +3068 -0
- package/dist/cards/Card.d.ts +310 -0
- package/dist/cards/Card.js +14 -0
- package/dist/cards/CardFoot.d.ts +5 -0
- package/dist/cards/CardFoot.js +6 -0
- package/dist/cards/CardHead.d.ts +21 -0
- package/dist/cards/CardHead.js +11 -0
- package/dist/cards/CardParts.d.ts +25 -0
- package/dist/cards/CardParts.js +26 -0
- package/dist/cards/Placeholder.d.ts +9 -0
- package/dist/cards/Placeholder.js +6 -0
- package/dist/cards/Stat.d.ts +17 -0
- package/dist/cards/Stat.js +14 -0
- package/dist/choice/Calendar.d.ts +23 -0
- package/dist/choice/Calendar.js +7 -0
- package/dist/choice/Check.d.ts +14 -0
- package/dist/choice/Check.js +10 -0
- package/dist/choice/DayStrip.d.ts +16 -0
- package/dist/choice/DayStrip.js +10 -0
- package/dist/choice/Mood.d.ts +11 -0
- package/dist/choice/Mood.js +6 -0
- package/dist/choice/Slider.d.ts +11 -0
- package/dist/choice/Slider.js +14 -0
- package/dist/cx.d.ts +2 -0
- package/dist/cx.js +2 -0
- package/dist/forms/AddRow.d.ts +5 -0
- package/dist/forms/AddRow.js +7 -0
- package/dist/forms/Composer.d.ts +13 -0
- package/dist/forms/Composer.js +6 -0
- package/dist/forms/Field.d.ts +17 -0
- package/dist/forms/Field.js +10 -0
- package/dist/forms/FileBtn.d.ts +18 -0
- package/dist/forms/FileBtn.js +10 -0
- package/dist/forms/Pin.d.ts +19 -0
- package/dist/forms/Pin.js +27 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +75 -0
- package/dist/journey/NoteRow.d.ts +7 -0
- package/dist/journey/NoteRow.js +7 -0
- package/dist/journey/Route.d.ts +18 -0
- package/dist/journey/Route.js +8 -0
- package/dist/journey/TileBadge.d.ts +15 -0
- package/dist/journey/TileBadge.js +6 -0
- package/dist/layover/Banner.d.ts +9 -0
- package/dist/layover/Banner.js +6 -0
- package/dist/layover/Callout.d.ts +10 -0
- package/dist/layover/Callout.js +6 -0
- package/dist/layover/Deck.d.ts +9 -0
- package/dist/layover/Deck.js +10 -0
- package/dist/lists/Kv.d.ts +27 -0
- package/dist/lists/Kv.js +19 -0
- package/dist/lists/Legend.d.ts +11 -0
- package/dist/lists/Legend.js +11 -0
- package/dist/lists/Row.d.ts +319 -0
- package/dist/lists/Row.js +17 -0
- package/dist/lists/Timeline.d.ts +11 -0
- package/dist/lists/Timeline.js +10 -0
- package/dist/media/MapArea.d.ts +20 -0
- package/dist/media/MapArea.js +11 -0
- package/dist/media/Mosaic.d.ts +11 -0
- package/dist/media/Mosaic.js +10 -0
- package/dist/media/Postcard.d.ts +20 -0
- package/dist/media/Postcard.js +10 -0
- package/dist/media/Quote.d.ts +11 -0
- package/dist/media/Quote.js +6 -0
- package/dist/media/Tile.d.ts +320 -0
- package/dist/media/Tile.js +16 -0
- package/dist/numbers/Badge.d.ts +7 -0
- package/dist/numbers/Badge.js +6 -0
- package/dist/numbers/Band.d.ts +16 -0
- package/dist/numbers/Band.js +10 -0
- package/dist/numbers/Bars.d.ts +14 -0
- package/dist/numbers/Bars.js +10 -0
- package/dist/numbers/DotGrid.d.ts +12 -0
- package/dist/numbers/DotGrid.js +6 -0
- package/dist/numbers/Picker.d.ts +13 -0
- package/dist/numbers/Picker.js +18 -0
- package/dist/numbers/Ring.d.ts +15 -0
- package/dist/numbers/Ring.js +6 -0
- package/dist/numbers/Track.d.ts +12 -0
- package/dist/numbers/Track.js +10 -0
- package/dist/people/Avatar.d.ts +314 -0
- package/dist/people/Avatar.js +13 -0
- package/dist/people/AvatarPick.d.ts +18 -0
- package/dist/people/AvatarPick.js +10 -0
- package/dist/scaffold/ActionBar.d.ts +21 -0
- package/dist/scaffold/ActionBar.js +16 -0
- package/dist/scaffold/AppBar.d.ts +18 -0
- package/dist/scaffold/AppBar.js +10 -0
- package/dist/scaffold/Centre.d.ts +30 -0
- package/dist/scaffold/Centre.js +41 -0
- package/dist/scaffold/EmptyState.d.ts +14 -0
- package/dist/scaffold/EmptyState.js +8 -0
- package/dist/scaffold/FilterRow.d.ts +5 -0
- package/dist/scaffold/FilterRow.js +6 -0
- package/dist/scaffold/FloatBar.d.ts +310 -0
- package/dist/scaffold/FloatBar.js +15 -0
- package/dist/scaffold/PagerAt.d.ts +11 -0
- package/dist/scaffold/PagerAt.js +6 -0
- package/dist/scaffold/Screen.d.ts +299 -0
- package/dist/scaffold/Screen.js +11 -0
- package/dist/scaffold/Section.d.ts +12 -0
- package/dist/scaffold/Section.js +13 -0
- package/dist/scaffold/TopBar.d.ts +15 -0
- package/dist/scaffold/TopBar.js +9 -0
- package/dist/sheets/CardBack.d.ts +7 -0
- package/dist/sheets/CardBack.js +7 -0
- package/dist/sheets/Drawer.d.ts +17 -0
- package/dist/sheets/Drawer.js +16 -0
- package/dist/sheets/Sheet.d.ts +21 -0
- package/dist/sheets/Sheet.js +13 -0
- package/dist/sheets/SheetBody.d.ts +5 -0
- package/dist/sheets/SheetBody.js +6 -0
- package/dist/sheets/SheetHead.d.ts +13 -0
- package/dist/sheets/SheetHead.js +6 -0
- package/dist/sheets/SheetStack.d.ts +21 -0
- package/dist/sheets/SheetStack.js +14 -0
- package/dist/sheets/SheetStage.d.ts +20 -0
- package/dist/sheets/SheetStage.js +11 -0
- package/dist/sheets/Step.d.ts +15 -0
- package/dist/sheets/Step.js +15 -0
- package/dist/type/Icon.d.ts +16 -0
- package/dist/type/Icon.js +12 -0
- package/dist/type/Text.d.ts +302 -0
- package/dist/type/Text.js +16 -0
- package/dist/type/icons.d.ts +6 -0
- package/dist/type/icons.js +2 -0
- package/fonts/LICENSES.md +44 -0
- package/fonts/fc-pride-medium.otf +0 -0
- package/fonts/pk-nonthaburi-demo.ttf +0 -0
- package/icons.svg +61 -0
- 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 */
|