@usableapp/cardds 0.7.96 → 0.7.97
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +3 -1131
- package/bin/cardds.mjs +1 -1
- package/css/actions.css +2 -2
- package/css/numbers.css +1 -1
- package/css/palette.css +1 -1
- package/css/stack.css +1 -1
- package/css/theme.template.css +2 -2
- package/css/tokens.css +2 -2
- package/dist/actions/Chip.d.ts +1 -1
- package/dist/actions/Dropdown.d.ts +1 -1
- package/dist/cardds.css +7 -7
- package/dist/cards/inkGrounds.d.ts +1 -1
- package/dist/cards/inkGrounds.js +1 -1
- package/dist/catalog.json +1 -1
- package/docs/reference.md +1137 -0
- package/lib/guide.mjs +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,1135 +1,7 @@
|
|
|
1
1
|
# cardds
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> content into the UI — tokens (`theme.css`), props, slots, composition — without touching cardds. When a project
|
|
5
|
-
> cannot get what it needs from outside, that is a gap in the framework: report it; it is closed here as a
|
|
6
|
-
> GENERAL extension point, never as that project's special case. A consumer project never edits or releases cardds.
|
|
7
|
-
>
|
|
8
|
-
> **For:** people and AI alike — this is the reference, the one place a rule of the system is written.
|
|
9
|
-
> The other docs only point here: `CLAUDE.md` (AI working IN this repo — decisions and gotchas),
|
|
10
|
-
> `docs/guides/building-with-cardds.md` (AI composing screens in React; shipped to Claude Design),
|
|
11
|
-
> the `/cardds` skill (AI in another project — install + contract). To LOOK at the system: `npm run dev`.
|
|
12
|
-
>
|
|
13
|
-
> **Accessibility:** [Ink follows its ground](#ink-follows-its-ground). Two ink sets, onlight and ondark; every ground declares
|
|
14
|
-
> only its side, and every word and glyph on it follows. `inkGrounds()` checks your theme.
|
|
3
|
+
A card-first UI framework for mobile web apps, in React. It is a set of components for phone-width screens: cards, bottom sheets, sheet stacks, steps, sticky notes, stamps and postcards. A project installs it as `@usableapp/cardds` and gives it its own look with a `theme.css`, without changing cardds itself. With no theme, it draws a grayscale wireframe.
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
library — one thin component per pattern, emitting exactly the markup the CSS
|
|
18
|
-
documents. The CSS (`css/*.css`)
|
|
19
|
-
stays the only truth: a component never styles anything, it only picks classes
|
|
20
|
-
from props. The demo is the gallery (`npm run dev`):
|
|
21
|
-
every component's stories, live, searchable.
|
|
5
|
+
The rules of the system are in [`docs/reference.md`](docs/reference.md). The gallery shows every component (`npm run dev`).
|
|
22
6
|
|
|
23
|
-
|
|
24
|
-
source of truth; every consuming project installs the npm package
|
|
25
|
-
`@usableapp/cardds`, never the other way round).
|
|
26
|
-
|
|
27
|
-
**Wireframe by default** — grayscale with visible outlines. Color is opt-in,
|
|
28
|
-
lives only in one file, applied with one attribute. Two faces ship with it
|
|
29
|
-
(`fonts/`, `css/fonts.css`): PK Nonthaburi for text, FC Pride for headings
|
|
30
|
-
and big numbers — both Thai + Latin, both single-weight demo/non-commercial
|
|
31
|
-
licences (`fonts/LICENSES.md`).
|
|
32
|
-
|
|
33
|
-
## Use
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
npm install
|
|
37
|
-
npm run dev # the gallery on http://localhost:5174 — every story, searchable
|
|
38
|
-
npm run build # the library: icons → src/type/icons.ts · tsc → dist/ (ESM + .d.ts) · dist/cardds.css (flattened)
|
|
39
|
-
npm test # Playwright: the tokens and the step/sheet/centre geometry on tests/fixtures/, the gallery's stories and behaviours
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
```tsx
|
|
43
|
-
import { Screen, TopBar, Step, Page, Sheet, SheetBody, CardHead, Field, Pin, ActionBar, Btn } from '@usableapp/cardds';
|
|
44
|
-
import '@usableapp/cardds/dist/cardds.css';
|
|
45
|
-
import './theme.css'; // the project's own look, after the system's — see "A project's theme"
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
**The viewport meta cardds expects** (one way for every app):
|
|
49
|
-
|
|
50
|
-
```html
|
|
51
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
`viewport-fit=cover` = the app runs edge to edge under the phone's own bars; cardds publishes their room as
|
|
55
|
-
`--safe-top` / `--safe-bottom` (from `env(safe-area-inset-*)`, 0 without cover) and keeps what the member reads or taps
|
|
56
|
-
clear of them — the bottom bar's buttons, a sheet's peeked head, its foot and the end of its body, the float bar, the FAB,
|
|
57
|
-
a stage's foot; a fill `Screen` starts under the status bar — while every ground still runs to the edge. A
|
|
58
|
-
component of the app's own pinned to an edge reads the same two tokens. `interactive-widget=resizes-content` =
|
|
59
|
-
Android shrinks the page for the keyboard (`--kb` stays 0 there; iOS publishes it).
|
|
60
|
-
|
|
61
|
-
Every behaviour is the component's own React code — the `Sheet`'s handle (tap, drag,
|
|
62
|
-
keyboard) with the focus and on-screen-keyboard raises, the `SheetStack` opening and
|
|
63
|
-
closing, the `Pin` auto-advance, the `Modal`'s leaving — each controlled like an input
|
|
64
|
-
(`x` + `onXChange`, or `defaultX`). `src/index.ts` imports `cardds.js` for the environment
|
|
65
|
-
only: the keyboard's height (`--kb`) and `.bleed`'s scrollbar. `Icon` inlines the 61
|
|
66
|
-
symbols of `icons.svg` at build time, so no sprite file ships.
|
|
67
|
-
|
|
68
|
-
**The gallery** (`gallery/`, `npm run dev` → http://localhost:5174): Vite + React Router, `cardds`
|
|
69
|
-
aliased to `src/`, so the gallery and the package share one truth with no build between them.
|
|
70
|
-
`/` every story of every component, `/:Name` one component — each `.design-sync/previews/<Name>.tsx`
|
|
71
|
-
story live in a 375×812 phone cell, grouped by `src/<group>/`, searchable; switches for the palette
|
|
72
|
-
(`own` = a colour well per colour base), inspect (box outlines + dimensions), a mock keyboard (focus a field:
|
|
73
|
-
it comes up over the phone cell and publishes `--kb` there — to SEE what a keyboard leaves of the screen; it
|
|
74
|
-
types nothing), text size, and a slider per size base. Nothing is hand-listed: add a preview file or a story and it shows.
|
|
75
|
-
|
|
76
|
-
**On a real phone** — the mock keyboard shows the room a keyboard takes; the REAL one (its kind by `inputMode`: `numeric` /
|
|
77
|
-
`tel` = digits, `email`, `text`; its language is the member's own setting) only shows on a device. The gallery's server is
|
|
78
|
-
open on the Wi-Fi (`server.host: true`): `npm run dev` prints a `Network:` address — open it on a phone on the same
|
|
79
|
-
Wi-Fi. A project using cardds does the same in its own `vite.config` (`server: { host: true }`, or `vite --host`).
|
|
80
|
-
The iOS Simulator's Safari shows the iOS keyboard too (⌘K if it hides); desktop device modes show none. Installing as a
|
|
81
|
-
PWA needs https — a tunnel (`cloudflared tunnel --url http://localhost:<port>`), then Add to Home Screen. `host: true`
|
|
82
|
-
lets anyone on the same network open the dev server: use it on a network you trust.
|
|
83
|
-
|
|
84
|
-
`tests/fixtures/*.html` are the pages the geometry tests measure — not a demo. Nothing here is verified by eye.
|
|
85
|
-
|
|
86
|
-
### In another project
|
|
87
|
-
|
|
88
|
-
**React (the main way, 2026-09-16):** the package is `@usableapp/cardds` on npm
|
|
89
|
-
(published from this machine: `npm run release` sends every `v*` tag the registry lacks, oldest first).
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
npm i @usableapp/cardds
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
```tsx
|
|
96
|
-
import { Card, CardHead, Btn } from '@usableapp/cardds';
|
|
97
|
-
import '@usableapp/cardds/dist/cardds.css'; // the CSS, fonts resolve from the package
|
|
98
|
-
import './theme.css'; // the project's look, after cardds.css
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
**One way in, one step** (2026-09-29): `npx cardds init` — writes the project's `theme.css` (a copy of the
|
|
102
|
-
template), adds `npm run cardds:check`, and says what to import. Then a screen is a template (`Walkthrough` · `Home` ·
|
|
103
|
-
`CallSheet` · `Splash`) or components composed — IMPORTED, never their markup copied: the behaviour lives in the
|
|
104
|
-
component. `npx cardds check [dir]` runs the rules (`lint/rules.mjs`): a hand-written cardds class is an error; a
|
|
105
|
-
`Sheet` that cannot move (`state` without `onStateChange`) or leaves by vanishing (on a condition without
|
|
106
|
-
`onSettled`) is a warning; a bar after `</Screen>` or pinned by hand is an error. The same rules in ESLint (flat
|
|
107
|
-
config): `import cardds from '@usableapp/cardds/eslint'` → `cardds.configs.recommended`. A deliberate exception:
|
|
108
|
-
`// cardds-ignore-next-line`. Adjust from outside only: `theme.css` → knobs → props / slots.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
**เบราว์เซอร์** (เช็ก 2026-09-24): iPhone ตั้งแต่ **iOS 16.4** (มี.ค. 2023) ขึ้นไป
|
|
112
|
-
|
|
113
|
-
| เบราว์เซอร์ | ขั้นต่ำ | ออกเมื่อ | เครื่องที่ใช้ได้ |
|
|
114
|
-
|---|---|---|---|
|
|
115
|
-
| Safari / iOS | 16.4 | มี.ค. 2023 | iPhone 8 / X (2017) ขึ้นไป ที่อัปเดตแล้ว |
|
|
116
|
-
| Chrome | 114 | พ.ค. 2023 | Android ส่วนใหญ่ที่ Chrome อัปเดตอยู่ |
|
|
117
|
-
| Firefox | 128 | ก.ค. 2024 | — |
|
|
118
|
-
|
|
119
|
-
iOS 16.4–16.x ไม่มี popover ในตัว — cardds ใส่ `@oddbird/popover-polyfill` มาให้ (ทำงานเฉพาะเครื่องที่ไม่มี)
|
|
120
|
-
เก่ากว่า 16.4: ระยะของ sheet และสีเพี้ยน (`@property`, `color-mix()`) ส่วนเบราว์เซอร์ที่ผ่านขั้นต่ำแต่ยังไม่ใช่รุ่นล่าสุด
|
|
121
|
-
เมนูจะขึ้นกลางจอแทนที่จะอยู่ใต้ปุ่ม
|
|
122
|
-
|
|
123
|
-
สำหรับโปรเจกต์ที่อยากเช็กเบราว์เซอร์ก่อนเริ่ม (เช่นหน้าติดตั้ง): เช็กเฉพาะฟีเจอร์ที่**จำเป็น**
|
|
124
|
-
ตัวอื่น cardds มีตัวสำรองไว้แล้ว ไม่ต้องเช็ก
|
|
125
|
-
|
|
126
|
-
| จำเป็น: ไม่มีแล้วหน้าจอพัง | วิธีเช็ก |
|
|
127
|
-
|---|---|
|
|
128
|
-
| `@property` | `'registerProperty' in CSS` |
|
|
129
|
-
| `color-mix()` | `CSS.supports('color', 'color-mix(in srgb, red, blue)')` |
|
|
130
|
-
| container queries | `CSS.supports('container-type: size') && CSS.supports('height: 1cqh')` |
|
|
131
|
-
| `:has()` | `CSS.supports('selector(:has(a))')` |
|
|
132
|
-
|
|
133
|
-
มีตัวสำรองแล้ว ไม่ต้องเช็ก: popover (polyfill), anchor positioning, view transitions, `@starting-style`, `text-wrap`
|
|
134
|
-
iOS 16.4–18.2 เช็กผ่าน แต่ยังรองรับไม่เต็ม เมนูขึ้นกลางจอแทนใต้ปุ่ม
|
|
135
|
-
|
|
136
|
-
Working with Claude in another project? The `/cardds` skill
|
|
137
|
-
(`~/.claude/skills/cardds/`) carries the install and the contract, and points
|
|
138
|
-
back here. It is deliberately thin — **this repo stays the source of truth**,
|
|
139
|
-
so new components and rule changes go here, and the skill only gets updated
|
|
140
|
-
if the contract itself changes.
|
|
141
|
-
|
|
142
|
-
### For an AI session: the catalog
|
|
143
|
-
|
|
144
|
-
A session asked for a prototype need not read `src/`: the package ships a catalog (`dist/catalog.json`, built by
|
|
145
|
-
`npm run build`) of every component — props, slots — and every story with its code and its rendered HTML. Ask it first:
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
npx cardds find [words…] [--represents person] [--shape group] [--interact pick|none]
|
|
149
|
-
# by the AXES first (docs/roles.md: what it stands for · one or a group · what the member can do), words second;
|
|
150
|
-
# the screens already drawn, then components, then what is used with them (nothing → a hint, not a list)
|
|
151
|
-
npx cardds scaffold <id> # a whole file from a screen: tsx (the code) or --html (a static document linking cardds.css)
|
|
152
|
-
npx cardds get <Name> [--html] # one component: its role, props (a ReactNode prop is a slot), what it composes, its knobs + variations with code
|
|
153
|
-
npx cardds list # every name by what it is for: one · group-brief · group-detail, the show-only first, then by what the member can do
|
|
154
|
-
npx cardds check <dir> # the rules, before a screen is called done
|
|
155
|
-
npx cardds guide # the few rules no checker sees, in one short text
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
`get` lists the component's **knobs** — each `--name` with its default, what it sets and the selector that reads it, joined by the classes the
|
|
159
|
-
component emits, so a knob named for an old name (`--action-note-*`) is found under the component that has it now (`BarNote`). It says what the
|
|
160
|
-
component **composes** (the catalog names its own source renders or calls: `Fab` → `IconBtn`, `Icon`) and what it is **composed by** (the inverse).
|
|
161
|
-
`find` matches the role's note and what a component stands in for as well as its name and description, so a question by intention ("a setting
|
|
162
|
-
turned on or off", "a code digit by digit") finds the piece (`Toggle`, `Pin`).
|
|
163
|
-
|
|
164
|
-
`list` · `get` · `find` · `scaffold` take `--json` (the function's result) and `--root <path>` (a cardds checkout: rebuilt first, so never stale). The same
|
|
165
|
-
five answers over MCP — `list_components` · `get_component` · `find_components` · `scaffold_screen` · `check_source`:
|
|
166
|
-
|
|
167
|
-
```json
|
|
168
|
-
{ "mcpServers": { "cardds": { "command": "npx", "args": ["cardds", "mcp"] } } }
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
The MCP server hands a session the same text as `npx cardds guide` as its instructions, when it starts.
|
|
172
|
-
|
|
173
|
-
A scaffolded tsx may import `./_story` or `./_*Art`: story glue, not in the package — replace or drop it.
|
|
174
|
-
|
|
175
|
-
### React adapter — how a component is written
|
|
176
|
-
|
|
177
|
-
`src/<group>/<Name>.tsx`, one export per class family; sub-parts are their own
|
|
178
|
-
components (`CardHead`, `KvRow`, `TrackStep`, `SheetBody`). A component
|
|
179
|
-
chooses classes from props (`tone`, `state`, `primary`, `sm`…) and lays the
|
|
180
|
-
contract's children in order. A component that has STATE is controlled like an input
|
|
181
|
-
(`state` / `open` / `value` / `flipped` / `selected` + its `on…`, or a `default…` twin), keeps its own behaviour
|
|
182
|
-
inside itself and only ever asks through that callback; it never knows another component — the app wires them
|
|
183
|
-
(2026-09-21; `cardds.js` is down to `--kb`). Never fix a look in a wrapper: fix the CSS, the
|
|
184
|
-
wrapper follows. **Claude Design** builds with these components through
|
|
185
|
-
`/design-sync` (`.design-sync/`: the sync config, the authored preview
|
|
186
|
-
stories, the conventions the design agent reads) — a new component needs a
|
|
187
|
-
preview story there.
|
|
188
|
-
|
|
189
|
-
## Sheet stack markup
|
|
190
|
-
|
|
191
|
-
The sheet stack (`.sheet-stack`, `stack.css` — the menu, see *Anatomy*) is the
|
|
192
|
-
one component whose markup has rules, since a sheet's place in the stack comes
|
|
193
|
-
from its position among its siblings. A stacked sheet is written as a `.card`:
|
|
194
|
-
the card is the material, the sheet is what it is in the stack.
|
|
195
|
-
|
|
196
|
-
```html
|
|
197
|
-
<div class="sheet-stack sheet-stack--tap"> <!-- drop --tap for a static stack -->
|
|
198
|
-
<article class="card card--2">
|
|
199
|
-
<div class="card__head">
|
|
200
|
-
<button class="icon-btn icon-btn--sm sheet-stack__back" aria-label="Back">…</button>
|
|
201
|
-
<h2 class="t-h2 t-caps">Shopping</h2>
|
|
202
|
-
<span class="icon-row">…</span> <!-- optional trailing icons -->
|
|
203
|
-
</div>
|
|
204
|
-
…the sheet's own content…
|
|
205
|
-
</article>
|
|
206
|
-
<article class="card card--3">…</article>
|
|
207
|
-
<article class="card">…</article> <!-- last = the front sheet -->
|
|
208
|
-
</div>
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
- **Sheets must be direct children** of `.sheet-stack`. Order is the stack
|
|
212
|
-
order: first = back, last = the front sheet that shows its full content.
|
|
213
|
-
- **`--closed`** (with `--tap`): nothing open on arrival — every sheet, the
|
|
214
|
-
front one too, peeks at the stack's bottom; the headroom above is the base's.
|
|
215
|
-
An optional `<div class="sheet-stack__base">` — written AFTER the sheets, so
|
|
216
|
-
it takes no sheet slot — is what the base shows there, behind every sheet.
|
|
217
|
-
Loose things in it rest against the heads, with the same room under the
|
|
218
|
-
last one as between two of them (`--stack-base-gap`); one `Page` reads from
|
|
219
|
-
the top, one `Centre` stands in the middle. A card standing there is flat:
|
|
220
|
-
the base clips at its edges, so a shadow would end in a straight line.
|
|
221
|
-
A room too short for the loose things in it squeezes nothing: the upper
|
|
222
|
-
thing shows whole from the room's top, the rest lies under the heads, and
|
|
223
|
-
the room scrolls up until the last thing stands clear of the heads by
|
|
224
|
-
`--stack-base-gap`. `SheetStack` measures this and renders `is-long` on the
|
|
225
|
-
base; only then does the base scroll and run under the first sheet's round
|
|
226
|
-
corners. A room that holds its content does not scroll, and a finger on it
|
|
227
|
-
moves nothing.
|
|
228
|
-
A tap, or a pull up, opens any sheet over it. The open title or its back
|
|
229
|
-
button, tapped or pulled down, drops it home. A pull down in the open
|
|
230
|
-
content, already at the top, asks `onRefresh` — omit it and that pull only
|
|
231
|
-
scrolls. A tab in the head stays a click. Every sheet wears a grip — four
|
|
232
|
-
small dots at the middle of its top edge, a mark that it slides, not a
|
|
233
|
-
control. `mark="line"` on the `SheetStack` (or `Home`) draws one line
|
|
234
|
-
there instead, as a single `Sheet`'s; while a sheet is `refreshing`, either
|
|
235
|
-
mark changes colour the same way. Right after a fold, while the
|
|
236
|
-
pile is still coming back, a tap is answered by where each head RESTS: the
|
|
237
|
-
head that will stand under the finger opens, whatever is passing there.
|
|
238
|
-
An open sheet stops under the top bar. `rise="flood"` on the `SheetStack` (or
|
|
239
|
-
`Home`) — `sheet-stack--flood` in markup — lets it rise over the bar and fill
|
|
240
|
-
the whole screen instead: the stack is then the whole screen, the bar a layer
|
|
241
|
-
under the sheets (hidden while one is open), nothing resizes. At the screen's
|
|
242
|
-
top the open sheet's top corners are square; folded, they are round again. It needs
|
|
243
|
-
`--closed` in a `Screen fill`.
|
|
244
|
-
- **`Home` takes a sheet's tab from the app.** Each of `Home sheets` keeps its
|
|
245
|
-
own tab (`defaultTab`, else the first) unless the app gives it `tab` — then
|
|
246
|
-
that tab shows, and a tapped tab asks `onTabChange(tab)`. Something outside
|
|
247
|
-
the stack (a card in the base) brings a sheet up on a named tab by setting
|
|
248
|
-
that sheet's `tab` and the Home's `open` together. Story `Home · Feed`.
|
|
249
|
-
- **Five sheets max.** Slots are declared per `:nth-child` in `stack.css`; a
|
|
250
|
-
sixth would render at slot 0, on top of the first.
|
|
251
|
-
- **Every head needs its own `.sheet-stack__back`** if the stack is `--tap`. It stays
|
|
252
|
-
hidden until that sheet is open, then slides into a reserved header slot.
|
|
253
|
-
- **No ids, no wiring.** `SheetStack` holds which sheet is open (`open` + `onOpenChange`, or `defaultOpen`). A pull down in the open content asks `onRefresh(index)`. The stack does not fetch: the app reloads (that sheet alone, or more — its call) and says so with `refreshing` (that index, then `null` when the data is in) — that sheet's grip dots dissolve white to black meanwhile, and a pull there does not ask again.
|
|
254
|
-
- **`.card-stack` is gone.** That was this component's first name and the wrong
|
|
255
|
-
one — the members are sheets and the stack is the menu, not a list of cards.
|
|
256
|
-
Never bring it back, not even as an alias.
|
|
257
|
-
|
|
258
|
-
## Screen: top · content · bottom
|
|
259
|
-
|
|
260
|
-
**Every screen is three parts in ONE flex column** (Lh 2026-09-23) — the words used to ask for a screen, and the props
|
|
261
|
-
of `Screen`:
|
|
262
|
-
|
|
263
|
-
| Part | Prop | What fills it |
|
|
264
|
-
|---|---|---|
|
|
265
|
-
| **top** | `top` | `TopBar` · `AppBar` · `"lead"` (no bar: the content starts `--screen-lead`, 15% of the screen, down) |
|
|
266
|
-
| **content** | children | a `Step` (`Pages` · `Page` · `Page` + `Sheet`) · `Centre` · `SheetStack` … |
|
|
267
|
-
| **bottom** | `bottom` | `ActionBar` (rows: `controls` · the one `cta` row — the pager bar ← → or one wide button · a `ctaLink`) · `FloatBar` · nothing |
|
|
268
|
-
|
|
269
|
-
```tsx
|
|
270
|
-
<Screen fill top="lead" bottom={<ActionBar variant="pager">…</ActionBar>}>
|
|
271
|
-
<Step><Pages>…</Pages><Pager at={1} of={3} /></Step>
|
|
272
|
-
</Screen>
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
- **Top and content are one flex column; the bottom is a LAYER** (Lh 2026-09-23). In `Screen fill` the content runs to the
|
|
276
|
-
screen's bottom edge; the bottom (`ActionBar`, with the walk's `Pager` as its first row) lies over it at the foot and
|
|
277
|
-
covers what is under it — on purpose, nothing is added to the content to make room. The keyboard lifts the layer (`--kb`).
|
|
278
|
-
- **The bar is INSIDE the screen** — `bottom`, never a sibling after `</Screen>`, never pinned with a hand-set `position`.
|
|
279
|
-
A `FloatBar` in `bottom` floats the same way.
|
|
280
|
-
- **A screen with a `Sheet` has no bottom.** The sheet finishes on its own `CardFoot`; `Screen` warns once when both
|
|
281
|
-
are there.
|
|
282
|
-
- **Checked:** `validateScreen` (`scripts/validate.mjs`, run on every story and template by
|
|
283
|
-
`tests/composition.spec.js`) fails a bar after `</Screen>` or a pinned bar, warns on a Sheet + ActionBar.
|
|
284
|
-
- **Templates** are `Screen`s with the three parts filled (`Walkthrough` = top `lead` · content `Pages` · bottom
|
|
285
|
-
`ActionBar`); the gallery draws those parts on a template's page. The gallery's nav groups a screen's
|
|
286
|
-
*top* and *bottom*, then each frame with the pieces that fill it: *card* (Card, its head, body and foot),
|
|
287
|
-
*sheet*, *stack*, *step*. A piece that also fits another frame says so on its page.
|
|
288
|
-
|
|
289
|
-
## Step markup
|
|
290
|
-
|
|
291
|
-
A step is one screen, one task (see *Anatomy*). It is the other component
|
|
292
|
-
whose markup has rules, because the sheet's box is the step itself:
|
|
293
|
-
|
|
294
|
-
```html
|
|
295
|
-
<!-- reads: the words only -->
|
|
296
|
-
<section class="step">
|
|
297
|
-
<section class="page">
|
|
298
|
-
<div class="card__head"><h2 class="t-h2">…</h2></div>
|
|
299
|
-
<p class="t-body">…</p>
|
|
300
|
-
</section>
|
|
301
|
-
</section>
|
|
302
|
-
<div class="action-bar action-bar--pager">…</div> <!-- the screen's bottom: inside .screen, after the step -->
|
|
303
|
-
|
|
304
|
-
<!-- acts: the words + a sheet -->
|
|
305
|
-
<section class="step">
|
|
306
|
-
<section class="page">…the words…</section>
|
|
307
|
-
<article class="card sheet sheet--half">
|
|
308
|
-
<button class="sheet__handle" type="button" aria-label="ปรับความสูง"></button>
|
|
309
|
-
<div class="card__head"><h2 class="t-h2">…</h2><span class="chip">…</span></div>
|
|
310
|
-
<div class="sheet__body">
|
|
311
|
-
…cardds controls (.field / .pin / .composer / .rows + .check / .segment /
|
|
312
|
-
.chip-grid), or small content cards (.card--sm)…
|
|
313
|
-
</div>
|
|
314
|
-
<div class="card__foot"><button class="btn btn--primary btn--block">…the one button that finishes the sheet…</button></div>
|
|
315
|
-
</article>
|
|
316
|
-
</section>
|
|
317
|
-
<!-- no .action-bar: a bar is the base's, never in front of a sheet -->
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
- **`.step` needs a bounded column**: a direct child of `.screen--fill`, or
|
|
321
|
-
of an app section with a definite height. It is a flex child that fills the
|
|
322
|
-
column between the top bar's reserve and the bar's, and clips at its own
|
|
323
|
-
bottom edge — which is what makes the sheet stop above the bar. In a plain
|
|
324
|
-
scrolling `.screen` it has nothing to fill.
|
|
325
|
-
- **The sheet is full-width; the card keeps the inset.** The step bleeds to
|
|
326
|
-
the screen's edges (reading `--screen-pad`, as `.sheet-stack` does) and the
|
|
327
|
-
card takes the inset back as its side margins — so the card and the bar's
|
|
328
|
-
buttons stand on the inset line, and the sheet runs edge to edge like the
|
|
329
|
-
bar and like a sheet in a `.sheet-stage`.
|
|
330
|
-
- **`.page` first, `.sheet` last.** The block carries the step's words
|
|
331
|
-
ON THE BASE — no frame, no area colour (it was a `.card` until 2026-09-16;
|
|
332
|
-
Lh: a step reads like a page, not a card in a page). It keeps a card's
|
|
333
|
-
rhythm: a `.card__head`, `--card-gap` between things. The sheet is
|
|
334
|
-
**handle · head · body**: the handle, then a `.card__head` title row (that
|
|
335
|
-
row is what `peek` shows — heading left, the contextual thing right), then
|
|
336
|
-
`.sheet__body` with what the member acts on. The handle and the head are
|
|
337
|
-
pinned; the body is the one thing that scrolls, to the card's own edges. The
|
|
338
|
-
handle is a line, or four dots (`mark="dots"`): both sit in the card's top
|
|
339
|
-
padding, the same distance from the edge, so the title stays put. `refreshing`
|
|
340
|
-
(off unless the app sets it) changes the mark's colour while the app reloads
|
|
341
|
-
what the sheet shows, as a stack sheet's does; the sheet does not fetch. `paper` makes
|
|
342
|
-
the content white while the head keeps the sheet's colour (`tone`, as a card).
|
|
343
|
-
The same prop on a `StackSheet`, or on a `Card` in a stack: the head keeps the
|
|
344
|
-
tone, the body and the foot are white, and the gap under the head stays the tone.
|
|
345
|
-
While that sheet is closed, the corners the next sheet does not cover show the
|
|
346
|
-
card's colour. That colour fades in as the sheet returns, and is done when it stops.
|
|
347
|
-
A sheet with no head (the dim page sheet) is a body alone.
|
|
348
|
-
- **The step follows its top bar directly, and touches it.** `full` means
|
|
349
|
-
the sheet's top edge at the bar's bottom edge — nothing above it but the
|
|
350
|
-
bar. So the room the screen would have left between the bar and the step
|
|
351
|
-
moves inside the step, above the card: `.topbar + .step` cancels the
|
|
352
|
-
screen's gap (`--screen-gap`), `.appbar + .step` cancels the reserve's
|
|
353
|
-
breathing (`--header-gap`), and the card takes that room as its lead. A
|
|
354
|
-
step that follows anything else keeps the screen's gap, and the sheet
|
|
355
|
-
stops under that thing.
|
|
356
|
-
- **The bar is after the step, inside the screen** (the screen's bottom — *Screen: top · content · bottom*). In a
|
|
357
|
-
`.screen--fill` the bar is a layer over the content's foot. On a SCROLLING screen (not fill, 0.7.0) the screen is a
|
|
358
|
-
column at least one phone tall and the bar its last row, `position: sticky` at the viewport's foot (above `--kb`):
|
|
359
|
-
it stays in reach while the words scroll and sits under them when they end. Either way the bar is written INSIDE
|
|
360
|
-
the Screen (`bottom`) — a bar after `</Screen>` is an error (`validateScreen`).
|
|
361
|
-
- **Every sheet has one shape** (Lh 2026-09-21): the handle and the head stay at the top and never scroll · the
|
|
362
|
-
foot's button stands at the bottom — above the keyboard when there is one (`--kb`: a step gives the keyboard
|
|
363
|
-
its room, a stage sheet gives it up itself as `--_lift`) · everything between them scrolls, as far as it
|
|
364
|
-
needs to: the body's bottom padding is the part that slid under (`--_cover`), so the last row comes up to
|
|
365
|
-
the button at half and 3q as it does at full.
|
|
366
|
-
- **Layers: the base, the sheet over it, the top bar** (Lh 2026-09-20). The `.action-bar` belongs to the
|
|
367
|
-
BASE — the move of a page of words (a pager bar, or one wide button). It is never in front of a sheet. A
|
|
368
|
-
screen with a sheet finishes ON the sheet: a `.card__foot`, the sheet's last child — ONE row, the page's
|
|
369
|
-
conclusion. The foot stands on the screen's bottom edge at half, 3q and full alike (a layer as tall as
|
|
370
|
-
the sheet, slid up by the share the sheet slid down — `sheet.css`), and is gone at peek. Nothing inside
|
|
371
|
-
the sheet's body commits on its own (a `Composer` there takes no `send`). This is how cardds's own screens are
|
|
372
|
-
composed — guidance; the framework does not enforce it: a component never changes because another one is there.
|
|
373
|
-
- **Never write a sheet's height, and never a `.sheet-stage` inside a step.**
|
|
374
|
-
State is a class (`sheet--peek` / `--half` / `--full`); the sheet is always
|
|
375
|
-
the step's full height and only slides. In React the APP owns it: `<Sheet state={s}
|
|
376
|
-
onStateChange={setS}>` (controlled) or `<Sheet defaultState="half">` (it keeps its own). The sheet's own behaviour —
|
|
377
|
-
the handle's tap and drag, the focus raise, the climb when the keyboard moves the cut — lives in the component and
|
|
378
|
-
only ever ASKS through `onStateChange(state, reason)`; nothing outside the sheet is touched.
|
|
379
|
-
A sheet always ARRIVES and LEAVES by sliding: mounted already up, it starts `away` for one frame and slides in. To
|
|
380
|
-
take it off the screen set `state="away"` and unmount it in `onSettled` (`s === 'away'`) — unmounting it while it is up
|
|
381
|
-
only makes it vanish (dev warning). Story: `Sheet · MountedOnDemand`.
|
|
382
|
-
- **A part that becomes active has its field waiting with the cursor** (Lh 2026-09-29) — a `Sheet` come up, a `SheetStep`
|
|
383
|
-
opened, a `SheetStack` sheet opened, a `Modal` arrived. Once it has settled: the field marked `data-autofocus`, else its
|
|
384
|
-
first field, else its heading (a step's title, a modal's question) or, for a sheet come onto the screen, its handle. A field
|
|
385
|
-
that already has the cursor keeps it. One mechanic for all of them (`focusWhenActive`, src/state.ts); an app writes no focus code.
|
|
386
|
-
Another field than the first: `<Field data-autofocus … />`. Off: `data-standby="off"` on any box around the part — no field
|
|
387
|
-
is given the cursor (the heading or the handle still is). The gallery sets it on its stories; its Cursor button (C) turns it on.
|
|
388
|
-
- **The signal set — four colours, four meanings, everywhere** (Lh 2026-10-02): `--signal-ok` green (it went right) ·
|
|
389
|
-
`--signal-warn` yellow (look out) · `--signal-error` red (it went wrong) · `--signal-info` blue (for your information).
|
|
390
|
-
The wireframe has them too. A theme may retune a shade, never a meaning. Each is dark enough for words and edges;
|
|
391
|
-
`--signal-*-soft` is its light ground (the signal washed 14% into the surface). Never the colour alone: an icon or
|
|
392
|
-
words say it too. Read by: `Field`/`Pin`/`FormNote` error, `Dot tone` / `Chip dot="ok|warn|error|info"`, `Snackbar` (ok).
|
|
393
|
-
- **Good news is a `Snackbar`, a warning or a question is a `Modal`** (Lh 2026-10-02): a `Snackbar` is a quick line over the
|
|
394
|
-
screen — something went well, said in passing. A child of the `Screen`; it slides in from the top (or `bottom`, above the
|
|
395
|
-
bottom layer), stays `duration` (4 s), slides out, never takes the cursor, never stops the member. `open` +
|
|
396
|
-
`onOpenChange(open, reason)` or `defaultOpen`; unmount it in `onClosed`. One at a time: the queue and the notification
|
|
397
|
-
list are the app's — the app shows its newest notification in it, `onTap` opens that notification.
|
|
398
|
-
- **Mount a `Modal` only when it is to show.** A `Modal` first mounted with `open={false}` is drawn in place, then leaves,
|
|
399
|
-
and calls `onClosed` after about 700ms. Known fault, not fixed: under React StrictMode (any dev build) `onClosed` is
|
|
400
|
-
never called for a `Modal` mounted that way. Keep the app's own `mounted` flag: set it when the question is asked,
|
|
401
|
-
clear it in `onClosed`.
|
|
402
|
-
- **cardds names its own controls, in the page's language** (Lh 2026-09-29): a handle, a back button, a Pin's cells… read
|
|
403
|
-
their name from `src/words.ts` by `<html lang>` (en, th ship; `setWords(lang, {…})` adds one). An app passes no label that
|
|
404
|
-
only repeats these words; a prop still wins.
|
|
405
|
-
|
|
406
|
-
## Centred card markup
|
|
407
|
-
|
|
408
|
-
One card at the human centre — the introduction screens, and any screen with
|
|
409
|
-
one thing to read and one move. The heading above, the bar below, the card
|
|
410
|
-
between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
411
|
-
|
|
412
|
-
```html
|
|
413
|
-
<div class="screen screen--fill">
|
|
414
|
-
<header class="topbar"><h1 class="t-h1">ธนาคารเวลายินดีต้อนรับ</h1></header>
|
|
415
|
-
<article class="card card--centre">
|
|
416
|
-
<div class="card__head"><div><h2 class="t-h2">ปรับขนาดตัวหนังสือ</h2></div></div>
|
|
417
|
-
<p class="t-body">…</p>
|
|
418
|
-
</article>
|
|
419
|
-
</div>
|
|
420
|
-
<div class="action-bar action-bar--tiers">
|
|
421
|
-
<div class="action-bar__tier"><label class="slider"><input class="slider__range" type="range" min="100" max="160" step="15" aria-label="ขนาดตัวหนังสือ"></label></div>
|
|
422
|
-
<div class="action-bar__tier action-bar__tier--pager">
|
|
423
|
-
<button class="icon-btn" aria-label="ย้อนกลับ">…</button>
|
|
424
|
-
<button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
|
|
425
|
-
</div>
|
|
426
|
-
</div>
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
- **The card floats in the fill screen** (`.screen--fill` is its box), hugs its
|
|
430
|
-
words, keeps the screen's inset, and is capped so it never reaches the title
|
|
431
|
-
bar above it nor the bar's room below — past that it scrolls inside. Never
|
|
432
|
-
give it a height; never move it with a margin. The cap is twice the room
|
|
433
|
-
above the centre (2 × (40% − the title bar)): a card taller than that is
|
|
434
|
-
not a centred card — it is a plain scrolling `.screen` (the detail view).
|
|
435
|
-
- **A small group at the same centre** — an empty state; a card with its quiet
|
|
436
|
-
second button and the `Pager` dots under it — is `.screen__centre` (React: `Centre`), a block in the fill
|
|
437
|
-
screen after the topbar. Same placement and cap; it runs edge to edge with
|
|
438
|
-
the inset as padding, and stacks its children at the
|
|
439
|
-
screen's gap. Never hand-lay `top: 40%` on a screen.
|
|
440
|
-
- **The bar is the screen's bottom.** Plain `.action-bar` for one move; with a
|
|
441
|
-
control that belongs to the screen (a slider), `.action-bar--tiers` stacks a
|
|
442
|
-
control tier above the buttons (it publishes the taller `--bar-reserve` the centred card still reads for its cap).
|
|
443
|
-
- **The pager bar** is `.action-bar--pager` (or an `.action-bar__tier--pager`)
|
|
444
|
-
holding two peers at the two edges, a skip link between or nothing: two round
|
|
445
|
-
`.icon-btn`s, or two `.btn`s that name the step each leads to with the arrow at the
|
|
446
|
-
round pager's size ("← คำขอ" · "วันเวลา →", Lh 2026-10-17; the 2026-09-16 "never words" is lifted).
|
|
447
|
-
Worded, each hugs its edge at its own size with open ground between: left and right, never two halves. The names are steps; the step that
|
|
448
|
-
commits ("Done") still goes back to one wide button (rule 3).
|
|
449
|
-
- **Page that moves** (Lh 2026-09-20) — two ways, never both on one screen:
|
|
450
|
-
- *Pages of words:* `.step > .pages > .page` (one `aria-current="step"`, the rest `inert`) — every page
|
|
451
|
-
one height, the round pager bar turns them (`slideTo()`), the words slide like a page. No sheet there.
|
|
452
|
-
ONE SCREEN TO THE NEXT (a route change) slides the WHOLE screen the same way: `slideScreen(() => navigate(to), 'next' | 'back')`,
|
|
453
|
-
and `installScreenSlide()` once at app start for the back button (story `Step · WalksOn`). `data-slide="off"` on a Screen = no slide.
|
|
454
|
-
ONE OUTCOME, DRAWN ONCE (Lh 2026-10-01): a screen that can come out more than one way (signed in or not, a list or
|
|
455
|
-
its empty state, a redirect) is NOT drawn until the app knows which — `<Screen ready={known}>`. Not ready = nothing of it
|
|
456
|
-
is drawn; arrived at through `slideScreen` / the back button, the screen before stays until it is ready (at most 3 s), then
|
|
457
|
-
the right one slides in. Never one outcome first and another after (story `Step · ArrivesReady`). On a first load there is
|
|
458
|
-
no screen before: hold the `Splash` (`done`) until the first screen is known.
|
|
459
|
-
THE BACK STACK (Lh 2026-10-01): back takes ONE thing off — a screen (a route), or a LAYER: what lies over a screen, a whole
|
|
460
|
-
bottom sheet or a modal. A layer is one history entry, kept in the entry's STATE (the address stays the page's own, so the
|
|
461
|
-
screen behind does not slide): `const way = useLayer('way')` → `way.value` (null = not up) · `way.open(value)` (one entry;
|
|
462
|
-
already up = the same entry shows the new value) · `way.close()` (the part's own close: the handle, the scrim, a button —
|
|
463
|
-
back one entry, or, when it came with the page, the entry without it) · `way.arrive(value)` (a link straight to an open
|
|
464
|
-
sheet: read `?way=` once, no entry added, the query dropped). `useLayers().closeAll(then)` takes every layer off, then moves
|
|
465
|
-
(signing out). NOTHING INSIDE A LAYER TAKES AN ENTRY — a step, typed text, a sheet's rest, a tab: back at any step takes the
|
|
466
|
-
whole sheet down, and opened again it starts anew (a new `key` per opening). ONE LAYER PER TICK: two layers opened in
|
|
467
|
-
the same handler lose the first (`a.open(1); b.open(2)` gives `a.value` null and `b.value` 2). Open the second in a
|
|
468
|
-
later tick. The project hands its router over once:
|
|
469
|
-
`<LayerHistory state push replace back>` around its screens — react-router: `location = useLocation()`, `state = location.state`,
|
|
470
|
-
`push = (s, search) => navigate({ pathname: location.pathname, search: search === 'drop' ? '' : location.search }, { state: s })`, `replace = (s, search) => navigate({ pathname: location.pathname, search: search === 'drop' ? '' : location.search }, { replace: true, state: s })`,
|
|
471
|
-
`back = (n) => navigate(-n)`. cardds never calls the History API itself. Story `LayerHistory · Back`.
|
|
472
|
-
A FINISHED PROCESS IS NOT GONE BACK INTO: history cannot be deleted, so (a) inside a process every move on is a `replace`,
|
|
473
|
-
never a push — when it ends, back skips all of it; (b) a page that can no longer be returned to guards itself: it checks on
|
|
474
|
-
arrival (`Screen ready={false}` meanwhile) and, when its work is already done, replaces itself with the right screen — that
|
|
475
|
-
covers forward, an old link, and entries left ahead after `closeAll`.
|
|
476
|
-
- *Sheets the app calls:* the base offers WAYS IN — sign in · new phone · join — as buttons (`.cta-pack`); each
|
|
477
|
-
way has a sheet that holds its WHOLE form and its own submit on its foot, starting `.sheet--away` (not on the
|
|
478
|
-
screen: slid out, hidden once gone, `inert`). **The wiring is the app's, not cardds's**: a `useState` of which
|
|
479
|
-
sheet is on and where, each button sets it, each `<Sheet state onStateChange>` reads it — one at a time, the same
|
|
480
|
-
button raises a peeked sheet, the handle stops at peek (`states="peek full"`). Story `Step · Calls` is those few
|
|
481
|
-
lines; it could as well be a route, which is what makes the phone's back button close a sheet. CSS's only part:
|
|
482
|
-
behind a peeked sheet the words keep `--sheet-peek` of room (the box declares the token), so the buttons under
|
|
483
|
-
it scroll clear.
|
|
484
|
-
**Never a form taken apart** — one sheet per field, a "send" left on the base: a form lives whole in ONE sheet.
|
|
485
|
-
Fixture `tests/fixtures/calls.html`, tests `tests/calls.spec.js`, stories `Step · Walks` / `Step · Calls`.
|
|
486
|
-
- **A pager bar turns pages of words, never a screen with a sheet** (Lh 2026-09-20). A
|
|
487
|
-
sheet means this screen has work that cannot be walked past: no ← →, no bar at all — the sheet's
|
|
488
|
-
own foot finishes it. (Guidance — not enforced.)
|
|
489
|
-
- A screen is **top · content · bottom** on the base (*Screen: top · content · bottom*). The base is the
|
|
490
|
-
surface; top and bottom are the chrome above and below (`.topbar` / `.appbar`,
|
|
491
|
-
`.action-bar` / `.float-bar`); content is everything between.
|
|
492
|
-
- Content has three homes: **on the base**, **in a card**, **in a bottom
|
|
493
|
-
sheet**. Content normally comes in cards; static data (a blurb, a legal
|
|
494
|
-
note, help steps) may sit on the base directly.
|
|
495
|
-
- **A card is head · content · foot.**
|
|
496
|
-
- **Head** (`.card__head`): the heading on the left; hints, actions or a
|
|
497
|
-
more-menu — the contextual info — at the far right. With nothing at
|
|
498
|
-
the far right it runs all the way across. The heading wraps freely —
|
|
499
|
-
no line budget, no squeeze, no clamp (Lh 2026-09-16): a height rule on
|
|
500
|
-
words only distorts them. The head is FLAT markup — heading, then an
|
|
501
|
-
optional caption as a span, then the far-right thing; no
|
|
502
|
-
wrapper element, no `<p>`: the head wraps, the caption takes the row
|
|
503
|
-
below (`order`), the gap tokens do the rest (Lh 2026-09-16).
|
|
504
|
-
- **Content**: whatever the card frames — rows, key-values, a gauge, a
|
|
505
|
-
paragraph. Frameless, per rule 9.
|
|
506
|
-
- **Foot** (`.card__foot`): the card's action buttons on one line. One
|
|
507
|
-
button takes the width; up to four share it in even parts; from three
|
|
508
|
-
on they drop their side padding so the labels keep the room. A panel of
|
|
509
|
-
up to four icon buttons (`IconBtnGroup`) takes the foot the same way; with
|
|
510
|
-
`form` it is as wide as its buttons (one tap each) and stands in the middle,
|
|
511
|
-
as `Btn form` does — in a foot, a `Page`, a `Centre` or a `CtaPack` — and
|
|
512
|
-
with `form labelled` its `label` is SHOWN as words at its left, the two
|
|
513
|
-
centred as one line (Lh 2026-10-10).
|
|
514
|
-
- In React the head and the foot are the card's **slots**:
|
|
515
|
-
`<Card heading caption end foot footLink>` with the content as children —
|
|
516
|
-
the card writes the order (head first, foot last). A head of its own shape
|
|
517
|
-
is a `<CardHead>` child instead; never both (Lh 2026-09-27).
|
|
518
|
-
- **A card that goes on is pressed whole** (Lh 2026-10-09). A card whose ONLY
|
|
519
|
-
control is its foot's button, or the end button of a row card, is pressed
|
|
520
|
-
anywhere: a touch off the button runs that button's own action once, and the
|
|
521
|
-
whole card dims under the finger (`.card--go`, the card-wide press answer of
|
|
522
|
-
a row or a card link). `Card` does it by itself — the app wires the button
|
|
523
|
-
and nothing else, and there is no prop to turn it off. The keyboard and a
|
|
524
|
-
screen reader meet the button only. Every other card is only read: two or
|
|
525
|
-
more controls anywhere in it (a field, a choice, a nested card's button),
|
|
526
|
-
the one button in the head or in the content, a disabled button, a sheet, a
|
|
527
|
-
stack's sheet, a modal. A card that is itself a link or a label was pressed
|
|
528
|
-
whole already. Story `Card · PressedWhole`.
|
|
529
|
-
- **A feed is a queue of cards, all in sight** (Lh 2026-10-09; the whole
|
|
530
|
-
queue 2026-10-15). `CardFeed` takes the WHOLE queue as `FeedCard`
|
|
531
|
-
children, in the app's order, and draws every one: ONE is BIG (heading,
|
|
532
|
-
content, its button) and the rest are SMALL (the same card cut down to its
|
|
533
|
-
head — no content, no button). A touch anywhere on a small one makes it
|
|
534
|
-
the big one and the big one small; none changes place. The small card is
|
|
535
|
-
itself the button (named by its heading, Enter and Space) and what it does
|
|
536
|
-
not show cannot be pressed, focused or read; the big card is a plain card,
|
|
537
|
-
pressed whole by the rule above. The queue is the app's, and so is its
|
|
538
|
-
length (5, then 10…): near the end the feed asks for more with
|
|
539
|
-
`onMore()`, once per queue, when the card with `ahead` (default 2) cards
|
|
540
|
-
under it comes into view; the app appends, and the feed asks again at the
|
|
541
|
-
new queue's card. When the app says the queue is complete it passes `end`
|
|
542
|
-
(the words drawn after the last card) and the feed asks no more. When a
|
|
543
|
-
card LEAVES or the order changes, what is left slides up and the UPPER card
|
|
544
|
-
is big, whichever was big before; cards appended at the end fade in and ask
|
|
545
|
-
nothing, so a member who scrolled down stays there. One card alone is big;
|
|
546
|
-
no card, nothing is drawn. Which card is big is controlled like an input:
|
|
547
|
-
`big` (a card's `id`) + `onBigChange(id, 'tap' | 'queue')`, or
|
|
548
|
-
`defaultBig`. A `FeedCard` is `id` · `heading` · `caption` · `icon` (its
|
|
549
|
-
thumb, in both sizes) or `lead` (anything at the head's start instead: a
|
|
550
|
-
`Stamp` is the small stamp, 48 × 60, an `Avatar`; it wins over `icon`) ·
|
|
551
|
-
`tone` · `foot` (one `Btn`: the big card is pressed whole; two or more:
|
|
552
|
-
only the buttons act) · children (the content); its size is the feed's,
|
|
553
|
-
never a prop. Knobs `--card-feed-gap`, `--feed-card-head-h`. Reduced
|
|
554
|
-
motion: nothing slides or fades. Stories `CardFeed · Default`, `More`,
|
|
555
|
-
`StampLead`, `Home · Feed`.
|
|
556
|
-
- A page with **one feature and one CTA** (an install page, a single
|
|
557
|
-
decision) may be contained in one card: head, the pitch, foot.
|
|
558
|
-
- **A bottom sheet holds what the member acts on** — controls, or small
|
|
559
|
-
content cards to pick from or read more — and it opens to the full height
|
|
560
|
-
of its box. **Cards are content the system offers**: the words, the
|
|
561
|
-
explanation, the thing to read. Do not put the step's explanation in a
|
|
562
|
-
sheet.
|
|
563
|
-
- **The sheet is the paper the member fills in** (Lh 2026-10-18). Every
|
|
564
|
-
control the member fills in or picks stands on a sheet: a `Sheet`'s
|
|
565
|
-
`SheetBody`, a `SheetStep`, or a stack sheet. That is `Field`, `Composer`
|
|
566
|
-
and `ComposerArea`, `Pin`, `Dropdown`, `Slider`, `Toggle`, `Check`,
|
|
567
|
-
`ChipPick`, `Calendar`, `DayStrip` and `Day`, `Picker`, `Mood`,
|
|
568
|
-
`AvatarPick`, `FileBtn`, `AddRow`, `MapPick`, a `Tile pick` and a
|
|
569
|
-
`Tile upload`. A card is for reading and pressing buttons only; never put
|
|
570
|
-
a form, or one control of it, in a card. `cardds check` rule
|
|
571
|
-
`field-in-card` (error) flags a control written inside a `<Card>`. A `Card`
|
|
572
|
-
that is a sheet of a `SheetStack` is that stack's sheet, so it may hold
|
|
573
|
-
them. Seen in timebank's help-ask prototype: a Composer, a Dropdown and a
|
|
574
|
-
map on white cards inside the help sheet, the cards the colour of the
|
|
575
|
-
ground.
|
|
576
|
-
- **Two sheet systems, one material.** *Sheet single* (`.sheet`, `sheet.css`)
|
|
577
|
-
is ONE sheet the member acts in, whatever it holds: a short entry in front
|
|
578
|
-
of a step's card, a place on a map, a payment on a dim page — in a `.step`
|
|
579
|
-
or a `.sheet-stage`, and its state is how far it slid (peek / half / 3q / full).
|
|
580
|
-
*Sheet stack* (`.sheet-stack`, `stack.css`) is MANY sheets, each a main
|
|
581
|
-
section of the app, stacked like paper at the middle of the home screen:
|
|
582
|
-
the menu. Heads peek, a tap brings one sheet to the front, back returns it;
|
|
583
|
-
each sheet is the metaphor for the section it opens. Both are cut from the
|
|
584
|
-
same material — a stacked sheet is written as a `.card`, and the single
|
|
585
|
-
sheet is `.card.sheet` — and both obey rule 8: rigid, full height, moved
|
|
586
|
-
by `translate` only. (The stack was first named "card stack": wrong, and
|
|
587
|
-
retired.) See them live in the gallery: `Sheet`, `SheetStack`.
|
|
588
|
-
- **A step is one screen, one task: `.step`.** What the member reads is
|
|
589
|
-
words on the base (`.page` — the same block as anywhere on the base; it was `.step__card` until 2026-09-16); what the member does, if the step asks for it, is a
|
|
590
|
-
`.sheet` in front of it; the screen's one move is `.action-bar`, the screen's
|
|
591
|
-
bottom, after the step. A step where the member only READS is the card alone.
|
|
592
|
-
A step where the member ACTS (types, picks, shoots a photo, records) adds
|
|
593
|
-
the sheet: peek shows its handle and title row, half shows half of it,
|
|
594
|
-
full slides up until its top edge touches the top bar — and at every one
|
|
595
|
-
of those it stops above the bottom bar, because the step is the box it
|
|
596
|
-
slides in: it starts where the top bar ends and ends where the bar's room
|
|
597
|
-
begins. Nobody lays a step out by hand and nobody writes a
|
|
598
|
-
sheet's height. In short: **member acts → card + sheet, member reads →
|
|
599
|
-
card.** Markup: *Step markup*. Live: the gallery's `Step` page.
|
|
600
|
-
|
|
601
|
-
- **Where the centre is.** To the person holding a phone, the bottom fifth of
|
|
602
|
-
the screen is the thumb's — the bar, the controls — so what is meant to be
|
|
603
|
-
read sits at the middle of the four fifths above it: **the human centre is
|
|
604
|
-
40% down the screen, not 50%** (`--screen-centre`, Lh 2026-09-10). A screen
|
|
605
|
-
with one card and nothing else to read puts it there (`.card--centre`, the
|
|
606
|
-
introduction screens); a splash puts its mark there.
|
|
607
|
-
|
|
608
|
-
Live: every screen in the gallery (`npm run dev`).
|
|
609
|
-
|
|
610
|
-
## Sticky notes
|
|
611
|
-
|
|
612
|
-
A sticky note is a square of paper with a few words on it. The family is in `src/sticky/`, its CSS in `css/sticky.css`.
|
|
613
|
-
|
|
614
|
-
- **One note, one size.** Every `Sticky` is one square, 168 by default, with square corners. It never grows with its
|
|
615
|
-
words. Words it cannot hold at its size are written one size smaller (`--sticky-small-font`, the label size), and
|
|
616
|
-
the note then holds six lines instead of four and a half. Words longer than that are cut where their room ends, with
|
|
617
|
-
no ellipsis. A `StickyField` stops at that room: the note refuses the input that would not fit, and the app's
|
|
618
|
-
`onChange` is not called for it. One knob sets the side, `--sticky-size`; set it on an element to resize the notes
|
|
619
|
-
inside that element only.
|
|
620
|
-
- **The fold says unread.** A folded bottom-right corner means the note has not been read. `flat` means it has. A
|
|
621
|
-
folded note also says so to a screen reader after its words: cardds's own word by the page's language, or
|
|
622
|
-
`unreadLabel`.
|
|
623
|
-
- **`Sticky`** — `tone` is one of the nine papers in `STICKY_TONES`. `head`, `date`, `foot` are small rows above and
|
|
624
|
-
under the words. `onClick` makes the whole note a button. `stuck` holds what is stuck on the paper (`StampOn`).
|
|
625
|
-
`tilt` turns the one note a start screen hands over. A `Chip sm` in the `foot` is a name label on the paper: it
|
|
626
|
-
stands at the start, what follows it (a time) at the end, on one line.
|
|
627
|
-
- **`StickyBoard`** — notes stuck on a page. Rows wrap by the width: two on a phone, three from 504 of room, a screen about 540 wide.
|
|
628
|
-
The notes stand edge to edge and each is turned by one of four angles in turn (`--sticky-board-turn-1` to `-4`), so
|
|
629
|
-
no two notes that touch lean alike. Later is on top, always: the right note over the left, the lower over the
|
|
630
|
-
upper. `--sticky-board-lap` (0) is how far each lies over the one before. Where two notes do not fit side by side,
|
|
631
|
-
each lies over the one before by what is missing, up to half the fold; past that there is one note in a row. A
|
|
632
|
-
note's `foot` is laid over the note's foot and takes no room from the words, so a note reads the same on the page
|
|
633
|
-
as where it is opened.
|
|
634
|
-
- **`StickyThread`** — notes as a conversation, in two ways. With head rows: each `StickyPost` shows `who`, `name`
|
|
635
|
-
and `time` above its notes, and `rail` joins the avatars for a feed. `lapped`: a chat with no head rows. Theirs
|
|
636
|
-
stand at the left and mine (`side="right"`) at the right, each turn lying over a corner of the one before
|
|
637
|
-
(`--sticky-thread-lap` down, `--sticky-thread-lap-x` across), the time close under its note, the name only heard.
|
|
638
|
-
Two turns from one side stand `--sticky-thread-same-gap` apart.
|
|
639
|
-
- **`StickyHeading`** — what a chat told in notes is about, for as long as that lasts: a `title` and a short `caption`
|
|
640
|
-
(when) written on what it stands on, a dotted rule under it; information only. It stands in two places and no other
|
|
641
|
-
component has a prop for it. In a `Sticky`'s `stuck` (the list) it lies across the top of that chat's note, and the
|
|
642
|
-
note's words start under it and show two lines at most. As the first child of a `StickyThread` (the room, with
|
|
643
|
-
`as="h2"`: the sub-heading under the screen's h1) it stays at the top while the notes run under it. The app stops
|
|
644
|
-
rendering it when the subject ends. Knobs: `--sticky-heading-title-font`, `-caption-font`, `-gap`, `-room`.
|
|
645
|
-
- **`StickyAnswer`** — the note you answer on. Its child is a `Sticky flat` holding a `StickyField`. At rest it waits
|
|
646
|
-
at the screen's foot near the right, three quarters of it in sight (`--sticky-answer-show`). A touch on it puts the
|
|
647
|
-
cursor in the field and the holder asks to write: `writing` + `onWritingChange(writing, reason)`, or
|
|
648
|
-
`defaultWriting`. While writing, the note stands at the top, the screen behind is washed pale and takes no touch,
|
|
649
|
-
and `tools` (the paper chooser, the send button) and `tray` (the writer's stamp) stand under it. A touch outside or
|
|
650
|
-
Escape asks to stop. The words, the paper and the stamps on the note are the app's. It is a child of the `Screen`.
|
|
651
|
-
- **`StickyPile`** — one to six notes lying one over another, as the newest notes of a feed do. It is a feed item of
|
|
652
|
-
its own, with no card frame. The first child is on top, so pass the newest first. Each note under it lies a step
|
|
653
|
-
right and down (`--sticky-pile-step-x`, 24, and `--sticky-pile-step-y`, 8), turned by the board's four angles by
|
|
654
|
-
its place, so the top note turns the same whatever the count. Only the top note draws its `foot`. At 375 a pile of
|
|
655
|
-
one is 184 × 184, of three 232 × 200, of six 304 × 224 (`--sticky-pile-pad`, 8, is the room round the notes for
|
|
656
|
-
their turn). The pile stands in the middle of its column and is never wider than it: a narrow column shortens the
|
|
657
|
-
steps. A seventh note is not drawn, and the console says so once. With `onClick` the whole pile is one button, and
|
|
658
|
-
`label` is its name (required with `onClick`): one Tab stop, a ring round the pile. Where the press goes is the
|
|
659
|
-
app's. A `StampOn` on the top note keeps its own touch; the stamps on the notes under it are inert. A pile's notes
|
|
660
|
-
all wear their stamp at one size.
|
|
661
|
-
- **`StickyField`** is the bare textarea on a note. **`StickyPick`** is the paper chooser: `value` + `onChange`,
|
|
662
|
-
`tones` for the ones on offer.
|
|
663
|
-
|
|
664
|
-
Whose colour a note wears is the app's rule. The gallery's `StickyScreens` shows one: a neighbour's note wears the
|
|
665
|
-
ground of that neighbour's stamp, and my note wears the paper I picked while writing, from a writing set plus the
|
|
666
|
-
ground of my own stamp.
|
|
667
|
-
|
|
668
|
-
## Stamps
|
|
669
|
-
|
|
670
|
-
A stamp is a small paper picture a member keeps and sticks on a note or a postcard for a neighbour. Sending never uses
|
|
671
|
-
it up. cardds draws the paper; the pictures, their words and which stamp a member owns are the app's.
|
|
672
|
-
|
|
673
|
-
- **`Stamp`** — a perforated 4:5 stamp. `pictures` is one to four nodes, the app's own art (an svg, an img), in placement
|
|
674
|
-
order: the stamp places, sizes and turns them by how many there are. `tone` is the ground, one of `STICKY_TONES` (the
|
|
675
|
-
same list and the same `--sticky-<tone>-bg` knobs as a note's paper). `name` is the big line on the picture's foot,
|
|
676
|
-
`words` the small line above it. `tilt` turns it, `onClick` makes the whole stamp a button. Every inner length follows
|
|
677
|
-
the stamp's width.
|
|
678
|
-
- **A stamp is one size wherever it stands** (Lh 2026-10-11). A `Stamp` and a `StampPlace` are `--stamp-size` wide (two
|
|
679
|
-
taps: 96 × 120) in a `Centre`, a sheet, a page, a `StampBook` and a card's head. What holds a stamp only places it. A
|
|
680
|
-
holder narrower than a stamp shrinks it, and the stamp is still shown whole. Set `--stamp-size` on `:root` for every
|
|
681
|
-
stamp, or on an element for the stamps inside it. There is one other size, the small one: a stamp in a `StampOn` and a
|
|
682
|
-
stamp in an `AvatarStack` are `--stamp-sm-size` wide (one tap: 48 × 60), on any surface and in any row. The stamps in
|
|
683
|
-
a `PostDesk`'s tray are the standard size. One exception, by the owner's decision: the stamp at a postcard's corner is
|
|
684
|
-
the postcard's own size, 72 × 90 (`--post-corner-stamp-w`), in neither. `words` is not drawn on a stamp 96 wide or
|
|
685
|
-
narrower, so at the standard size only `name` shows, and it is larger.
|
|
686
|
-
- **`StampPlace`** — an empty place the size of a stamp, its edge a ring of round dots: a word inside, `onClick` for "make
|
|
687
|
-
yours", `faint` for a gap nobody is asked to fill. The edge is a drawn stroke, so the dots are yours: `--stamp-place-dot`
|
|
688
|
-
is how wide each is (default `--rule-w`, 2px), `--stamp-place-gap` the distance from one dot's centre to the next
|
|
689
|
-
(default 6px), and a `--stamp-place-dash` above 0 stretches each dot into a round-ended dash. Its corner is
|
|
690
|
-
`--stamp-place-radius`. The dots' outer edge is the box's edge whatever their width.
|
|
691
|
-
- **`StampBook`** + **`StampCell`** — the album page: cells three across on an album ground, a `label` under each stamp,
|
|
692
|
-
the free places drawn up to `places`. A cell with `onClick` is a button; `on` marks it pressed. Each stamp stands in
|
|
693
|
-
the middle of its third. `bare` is the page with no ground, no padding and no round. `strip` is one line of stamps
|
|
694
|
-
that scrolls sideways, the tray a member picks from. `fill` is the stamp's third size: each stamp as wide as
|
|
695
|
-
its third, 4:5, the gaps one `--screen-pad` both ways (`--stamp-book-fill-gap`), nothing at the row's sides; with `bare`
|
|
696
|
-
at 375 a stamp is 103.67 × 129.58. A `false` or `null` child holds no place.
|
|
697
|
-
- A stamp stands for a person in three more places. In a `Centre` — a `Stamp` or a `StampPlace` — it stands in the
|
|
698
|
-
middle and the words and link under it centre. In a card's head — `<Card heading={name} caption={greeting}
|
|
699
|
-
end={<Stamp bare …/>} foot={<Btn primary block>…</Btn>} />` — the words stand at the left and the stamp at the right,
|
|
700
|
-
beside both lines; the card's one button presses it whole. In an `AvatarStack` it is a head, the small size
|
|
701
|
-
(`more="+9"` for the count).
|
|
702
|
-
- **`StampOn`** — a stamp stuck on a surface. `at` is the stamp's middle as a share of the surface (`{ x, y }`, 0 to 1),
|
|
703
|
-
`turn` its angle in degrees. With `onAtChange` the member drags it (the arrow keys move it too) and it asks with the
|
|
704
|
-
new place when the finger lets go; without it the stamp is still. The surface is the nearest positioned box:
|
|
705
|
-
`Sticky stuck={…}` is one. The stamp is the small size, 48 × 60 whatever the surface's size (`--stamp-sm-size`), and
|
|
706
|
-
on a note it may lie over the words. With `arrive` the stamp sticks itself: as it comes onto the screen it is struck
|
|
707
|
-
onto its place, and `onArrived` tells the app when it has landed (at once where motion is off), so the app does what
|
|
708
|
-
follows. A `Card` and a `Sheet` are positioned boxes, so a `StampOn` written inside one lies on it.
|
|
709
|
-
`--stamp-on-arrive-time` sets how long the strike takes.
|
|
710
|
-
`main` makes it the stamp's main size, 96 × 120 (`--stamp-size`; 90 × 112.5 on a screen 360 wide), to cover what
|
|
711
|
-
it lies on. With `onRemove` the stamp can be taken off: when a drag ends or an arrow key moves it, a cross stands on
|
|
712
|
-
its top right corner for `removeFor` ms (default 3000). A press on the cross, or Delete / Backspace on the focused
|
|
713
|
-
stamp, asks, and the app takes the `StampOn` away. A stamp with `onRemove` and no `onAtChange` shows the cross on a
|
|
714
|
-
press. The cross is a 24 disc (`--stamp-on-remove-size`, `--stamp-on-remove-bg` #141414, `--stamp-on-remove-ink`
|
|
715
|
-
#ffffff) in a 44 target, centred on the corner and upright whatever the stamp's `turn`. Its name is cardds's word
|
|
716
|
-
("Remove" / "เอาออก"), or `removeLabel`. A press or a drag on a removable stamp never reaches what lies under it.
|
|
717
|
-
A stamp that is still arriving cannot be taken off yet: its cross waits until it has landed.
|
|
718
|
-
|
|
719
|
-
What an app stores for a stamp: the picture ids (1 to 4, in order), the ground tone, the name, the small line. Making
|
|
720
|
-
one is the app's screen: `ChipPick` checkboxes that each show a picture, the app refuses a fifth, and the app draws
|
|
721
|
-
ONE of the picked chips at random for the ground and keeps it, so the stamp renders the same every time. The gallery's
|
|
722
|
-
`StampScreens` page has the four screens: the first stamp on a home, one from a neighbour, make your own, a note with
|
|
723
|
-
stamps.
|
|
724
|
-
|
|
725
|
-
**`Mark`** is a highlighter stroke over a word inside running text: `<Text level="body"><Mark>ธนาคารเวลา</Mark> ส่ง…</Text>`.
|
|
726
|
-
Green (`--signal-ok`) unless `tone` names another signal; `--mark-bg` / `--mark-ink` retune it. It is information, never
|
|
727
|
-
a control, and never a chip or a line of its own.
|
|
728
|
-
|
|
729
|
-
## Postcards
|
|
730
|
-
|
|
731
|
-
A postcard is the one piece of mail with no envelope: the written side is the card, and the corner (a stamp, the postmark
|
|
732
|
-
struck over its edge) proves the trip. Three components, one for each thing a member does with one. None of them flips;
|
|
733
|
-
the older `Postcard` (a member's cover card that turns over) is another component and shares nothing with these.
|
|
734
|
-
|
|
735
|
-
- **`PostLetter`**: reading one that arrived. Two upright 2:3 sheets, one in front. The picture side is `picture` (the
|
|
736
|
-
app's img or svg) in a white margin with `caption` under it; the written side is the address (`kicker`, `to` lines) over
|
|
737
|
-
the message (`children`, set large, `sign` at its end), with `stamp` and `postmark` on the corner. The tab over the
|
|
738
|
-
card, or a tap on the card, turns it: the sheet in front swings to its edge and the other swings in. `face` +
|
|
739
|
-
`onFaceChange`, or `defaultFace`. `voice` is a voice note's time as the app words it; with it the picture side has a
|
|
740
|
-
play control (`playing` + `onPlayingChange`). cardds never plays sound: the control asks, the app plays and passes the
|
|
741
|
-
time. `journey` is one line under the card.
|
|
742
|
-
- **`PostDesk`**: writing one. The paper is landscape and twice the screen, and the phone looks at one half (`half` +
|
|
743
|
-
`onHalfChange`): the message on ruled lines, then the address (`address`, a few `PostDeskLine`s) and the stamp box.
|
|
744
|
-
Typing stops when the half is full; `onLinesChange(left, fits)` and `onFull` let the app word its `note`. `stamps` is
|
|
745
|
-
the sender's own stamps (`{ id, stamp, label }`), shown under the card; `stamp` + `onStampChange` is the one in the
|
|
746
|
-
box. Sending is the screen's button, never the desk's: the app sets `state="struck"`, the `postmark` lands, the desk
|
|
747
|
-
asks for `sent`, the card leaves upward and `sent` stands in its place.
|
|
748
|
-
- **`PostBox`** + **`PostBoxCard`**: keeping many. Cards whole and landscape, one behind another, each showing its top
|
|
749
|
-
edge (`from`, `when`, `stamp`, `postmark`). The last child stands in front, nearest the thumb. Pressing an edge parts
|
|
750
|
-
the box at that card (`open` is its `id`, `null` is the front card; `onOpenChange`).
|
|
751
|
-
- **`Postmark`**: the round mark, `place` and `date`. Information, nothing to press.
|
|
752
|
-
|
|
753
|
-
The stamp is always a `Stamp` the app hands over. On a card's corner it is the postcard's own size, 72 × 90
|
|
754
|
-
(`--post-corner-stamp-w`; it is not one of the stamp's two sizes), and shows its pictures only (pass `bare`; the corner
|
|
755
|
-
drops the words by itself too); the postmark is struck over it by 0.4 of its own size. The empty stamp box in a
|
|
756
|
-
`PostDesk` is the same size and shows its word. In the desk's tray a stamp is the standard size, 96 × 120
|
|
757
|
-
(`--stamp-size`), and keeps its words; the tray is one line that scrolls sideways when the stamps are wider than the
|
|
758
|
-
screen (three fit a 343 column). `--post-desk-stamps-gap` is the space between them. `PostLetter` and `PostDesk` size
|
|
759
|
-
themselves to the
|
|
760
|
-
screen they are in, so they go in a `Step` > `Page bleed`. **`Page bleed`** runs the page to the screen's edges and keeps
|
|
761
|
-
the inset as its own padding, so a card that leans, slides or casts a shadow is cut at the glass, not at the inset.
|
|
762
|
-
The gallery's `PostScreens` page has the three finished screens (read, write, keep) to start from.
|
|
763
|
-
|
|
764
|
-
## Rules of the system
|
|
765
|
-
|
|
766
|
-
1. **One card = one boundary.** A card frames one thing: a category, a stat
|
|
767
|
-
group, a list row. Tone slots: `.card` (card-1), `.card--2`, `.card--3`.
|
|
768
|
-
2. **Sections are not cards.** Page sections = frameless on-surface content
|
|
769
|
-
(`.page`) + a lone/packed CTA (`.cta-pack`).
|
|
770
|
-
3. **Surrounding UI is separate.** `.topbar`, `.filter-row`, `.chip-row`,
|
|
771
|
-
`.action-bar` live outside cards, above/below. The bottom bar has two
|
|
772
|
-
arrangements and they mean different things: the default pack (one wide
|
|
773
|
-
CTA, optional round icon) is **one action** on this screen, and
|
|
774
|
-
`.action-bar--pager` (two equal buttons at the edges, indicator between)
|
|
775
|
-
is **a sequence you can walk both ways**. A wide button next to a small
|
|
776
|
-
round one reads as an action plus an afterthought — wrong whenever back
|
|
777
|
-
and next are peers. The step that commits goes back to the default pack.
|
|
778
|
-
4. **Components reference role tokens only** (`--surface`, `--card-1-bg`,
|
|
779
|
-
`--accent`…), never hex. Derived tints are tokens too (`--outline`,
|
|
780
|
-
`--hairline`, `--ink-tint`, `--ink-rail`, `--accent-soft`, `--scrim`), and
|
|
781
|
-
masks use `currentColor` as "opaque". So palettes swap freely.
|
|
782
|
-
5. **`.screen` carries the horizontal inset and nothing vertical.** The inset
|
|
783
|
-
is `--screen-pad`; cards and on-surface UI sit flush inside it, with no
|
|
784
|
-
per-component side padding. Top and bottom are zero: vertical room only
|
|
785
|
-
is taken by what is in the column. The bottom bar is always the screen's last child (0.7.1 — the bar fixed to
|
|
786
|
-
the viewport from outside and its reserves are gone): in a `.screen--fill` a layer over the content's foot, on a
|
|
787
|
-
scrolling screen a `sticky` last row. `--bar-reserve` stays published only for a `.card--centre`'s cap.
|
|
788
|
-
**Padding is published, not copied.** `.screen` sets `--screen-pad` and
|
|
789
|
-
`--screen-gap`, its appbar reserve sets `--header-gap`, and `.card` sets
|
|
790
|
-
`--card-pad` and `--card-gap`; anything that has to cancel a container's
|
|
791
|
-
padding or gap reads the token rather than repeating its value. `.sheet-stack` uses it to
|
|
792
|
-
break out full-bleed when the column touches the viewport edges (≤27em /
|
|
793
|
-
432px), and `.bleed` uses it for a scrolling track inside a card:
|
|
794
|
-
|
|
795
|
-
```html
|
|
796
|
-
<div class="avatar-pick bleed">…</div>
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
The track runs to the card's edges so it reads as scrollable, while the
|
|
800
|
-
first and last item stay on the same line as the heading. Works for any
|
|
801
|
-
horizontal strip — `.chip-row`, a tile grid — in any container that
|
|
802
|
-
publishes `--card-pad`.
|
|
803
|
-
6. **The sheet stack is the menu, and a sheet is rigid material.**
|
|
804
|
-
`.sheet-stack` (`stack.css`) sheets are tall: heads peek (`--stack-step`,
|
|
805
|
-
up to 5 layers), each head = title + `.icon-row` functions (head text =
|
|
806
|
-
icon height, 28px). On a `.screen--fill` the stack stretches and bleeds
|
|
807
|
-
off the bottom edge — bottom corners square. Markup rules: see *Sheet
|
|
808
|
-
stack markup* above; the two sheet systems: *Anatomy*.
|
|
809
|
-
|
|
810
|
-
`.screen--fill` is exactly one phone height — a height, not a minimum, so
|
|
811
|
-
a `.step` inside it has a bound to fill and its card and sheet scroll inside
|
|
812
|
-
rather than growing the page — and it clips (`overflow: clip`, never
|
|
813
|
-
`hidden`): a clipped box is not a scroll container, so nothing, not even
|
|
814
|
-
the browser revealing a focused field, can shift the screen up.
|
|
815
|
-
|
|
816
|
-
In a `.sheet-stack--tap`, a sheet is rigid material — it moves in, moves
|
|
817
|
-
out, or is covered, but **never changes size**. Every sheet is the same
|
|
818
|
-
full height, set once; a peek is not a short sheet, it's a full sheet the
|
|
819
|
-
next one covers. So `translate` is the only animated property, and
|
|
820
|
-
**z-index is the resting `--level` order and is never touched** — because
|
|
821
|
-
whatever sits in front of the tapped card is exactly what has to leave.
|
|
822
|
-
Opening slides the tapped card up by its own `--slot` × `--stack-step`;
|
|
823
|
-
cards after it move out below (`.is-after`); cards before it tuck one
|
|
824
|
-
step down, staggered nearest-first via `--stagger` (`.is-before`), and
|
|
825
|
-
get covered. Closing reverses it in two phases: the after-pack returns
|
|
826
|
-
while the card drops into its slot, then the before-pack slides back up
|
|
827
|
-
from behind. Slots, offsets and paint order are all CSS `:nth-child`, so
|
|
828
|
-
a stack renders correctly with no script. The app owns which index is
|
|
829
|
-
open and whether that sheet's data is in. The card renders what those
|
|
830
|
-
mean (`is-open`, `is-before`, `is-after`, `is-loading`, the grip, the
|
|
831
|
-
pull) from the `stack` prop `SheetStack` passes it. Don't reintroduce
|
|
832
|
-
height, `display` or z-index switching here — every past bug in this
|
|
833
|
-
animation came from exactly that. Don't stamp those classes onto the
|
|
834
|
-
card after it has rendered.
|
|
835
|
-
7. **Everything shrinks to 320px.** `.screen > *`, `.card` and `.card > *`
|
|
836
|
-
carry `min-width: 0`, so no child's min-content (an input, a fixed row)
|
|
837
|
-
can widen a card's grid track past its padding; topbar titles ellipsize;
|
|
838
|
-
segment items share width. Never give a component a fixed width that can
|
|
839
|
-
beat the viewport — let it `flex: 1` up to a `max-width` token instead.
|
|
840
|
-
8. **A single sheet is rigid material, like a sheet in the stack.** It is always the full
|
|
841
|
-
height of the box that holds it (`inset: 0`, set once, never written
|
|
842
|
-
again) and a state is nothing but how far that full card slid:
|
|
843
|
-
`sheet--full` slid nowhere, `sheet--3q` slid down by `--sheet-3q` (a
|
|
844
|
-
quarter of itself — three quarters show), `sheet--half` by `--sheet-half`
|
|
845
|
-
(50%), `sheet--peek` slid down until only `--sheet-peek` shows — the
|
|
846
|
-
handle and the title row. A tap on the handle toggles between the sheet's
|
|
847
|
-
own state and peek (never a step-by-step climb, Lh 2026-09-16); a drag
|
|
848
|
-
snaps to any listed state, and where it lands is what a tap returns to. `translate` is the only animated property
|
|
849
|
-
(`--motion-sheet`), and nothing toggles height, display or z-index for a
|
|
850
|
-
state — the stack's rule, inherited. The box clips whatever slid below its
|
|
851
|
-
bottom edge, so a sheet at any height is still a card: rounded top, a
|
|
852
|
-
straight cut at the box's edge — and its shape is handle · head · body:
|
|
853
|
-
the handle and the title row are pinned, `.sheet__body` scrolls beneath
|
|
854
|
-
them, and the body's `scroll-padding-bottom` is the slid-under part (in
|
|
855
|
-
the sheet's own container units, `--_cover`), so a focused control is
|
|
856
|
-
revealed above the cut, not under it. `--sheet-half` is a share of the
|
|
857
|
-
sheet's height (`0.5`), one number serving both the translate and that
|
|
858
|
-
padding. Two boxes hold sheets and the states
|
|
859
|
-
mean the same in both: `.step` (an app screen — Anatomy)
|
|
860
|
-
and `.sheet-stage` (a page-level overlay: a sheet over a map, the dim page
|
|
861
|
-
stage `--dim` + `.modal` = `__lift` + `__drawer` — a sheet always stands on the floor, never raised).
|
|
862
|
-
A stage keeps the top bar's line free (`--_top`), so `--full` stops under
|
|
863
|
-
`--header-h` and keeps its corners. Both sheet tokens are published — a
|
|
864
|
-
sheet with a taller head retunes `--sheet-peek` — and registered in
|
|
865
|
-
`tokens.css`, which is how `Sheet` reads them to snap a drag. Live: the
|
|
866
|
-
gallery's `Sheet` page.
|
|
867
|
-
9. **Elements inside a card are frameless.** Rows, key-values, pickers,
|
|
868
|
-
gauges and tiles never draw a second card boundary; hairlines
|
|
869
|
-
(`--hairline`) separate rows, `--outline` keeps controls legible on
|
|
870
|
-
colored palettes. Data goes in as unitless custom properties
|
|
871
|
-
(`--value`, `--done`, `--total`); the math lives in CSS.
|
|
872
|
-
10. **The dark slot re-points the accent.** `.card--3` sets `--accent` /
|
|
873
|
-
`--on-accent` to its own ink and bg, so every accent consumer (buttons,
|
|
874
|
-
checks, toggles, rings, selected days) inverts without extra rules.
|
|
875
|
-
|
|
876
|
-
**A screen is scanned, not read** (Lh 2026-09-24) — a member arrives with one
|
|
877
|
-
question and hunts for its answer. Rules 11–13 serve that:
|
|
878
|
-
|
|
879
|
-
11. **Everything holds to edges.** Every element touches at least two edges —
|
|
880
|
-
the card's, the column's, or a line made by the thing above it (a row, the
|
|
881
|
-
avatar + name line). Words to READ are aligned start, values end (the
|
|
882
|
-
receipt: items left, prices right; `.card__head`, `.kv`, `.row`); centred
|
|
883
|
-
only what is not read — dots, a lone number, an icon, a mark. Empty room is
|
|
884
|
-
not filled with more content; a new edge is made instead (line the buttons
|
|
885
|
-
up with the avatar). One exception: a small control with its own words
|
|
886
|
-
(`IconBtnGroup form labelled`) is one centred line — the words name the
|
|
887
|
-
control, they are not content to be read.
|
|
888
|
-
12. **Show, don't explain.** Where a known picture carries it — an avatar for a
|
|
889
|
-
person, an icon, a `Ring`/`Track`/`NumberBand`, a `Chip` for a category — use it
|
|
890
|
-
instead of words. Never add a label, a caption line, a comparison or a
|
|
891
|
-
tooltip to explain what the picture already says (the retired overline was
|
|
892
|
-
that mistake). Long lists are differentiated, not spaced out: group them
|
|
893
|
-
(`Rows heading` per group), put the avatar where a person is named.
|
|
894
|
-
13. **Emphasis is the difference from the neighbours.** An untouched default
|
|
895
|
-
is quiet (grey, thin border, no fill — `Chip muted`, `Dropdown muted`);
|
|
896
|
-
only what the member changed, or the one thing asked of them, takes the
|
|
897
|
-
accent. To make one thing stand out, calm what surrounds it — never
|
|
898
|
-
bolder, never a second colour.
|
|
899
|
-
14. **A frame says "tap me".** Inside a card, only a control draws its own
|
|
900
|
-
edge (a chip, a field, a pick tile, a small `Card sm` to pick). What is
|
|
901
|
-
only READ never gets a frame — no card in a card: a stat tile stands on
|
|
902
|
-
the card's ground, a note is a `Banner` (a light band, a rule above and
|
|
903
|
-
below, edge to edge), a count is words (`Text caption muted`), never a
|
|
904
|
-
chip. A chip or tile that looks pressable and is not lies.
|
|
905
|
-
|
|
906
|
-
## A project's theme
|
|
907
|
-
|
|
908
|
-
A project retunes cardds from ONE file of its own, and nothing else. Copy
|
|
909
|
-
`css/theme.template.css` into the project as `theme.css`, load it **after**
|
|
910
|
-
`cardds.css`, change values:
|
|
911
|
-
|
|
912
|
-
```css
|
|
913
|
-
:root {
|
|
914
|
-
--sp-base: 0.3rem; /* roomier */
|
|
915
|
-
--r-base: 0.5rem; /* tighter corners */
|
|
916
|
-
--color-surface: #faf6ee; --color-ink: #1f1b16; --color-paper: #fff;
|
|
917
|
-
--color-brand: #c2410c; --color-on-brand: #fff;
|
|
918
|
-
}
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
```html
|
|
922
|
-
<html data-palette="own"> <!-- colours of your own = coloured mode; any value no stock palette uses -->
|
|
923
|
-
```
|
|
924
|
-
|
|
925
|
-
That file is where a project's look lives. Bases and roles:
|
|
926
|
-
|
|
927
|
-
| what | tokens | moves |
|
|
928
|
-
|---|---|---|
|
|
929
|
-
| room **between** things | `--sp-base` (0.25rem = 4) | every `--sp-N` = base × N → paddings, gaps, the screen's inset, the bar's reserve, the sheet's peek |
|
|
930
|
-
| type | `--fs-base` (1.375rem = 22 — the body) | every `--fs-*`, `--badge`, `--icon-xs` |
|
|
931
|
-
| corners | `--r-base` (1.3rem = 20.8) | every `--r-*`; `0` = square. `--r-chip` stays fully round. One card radius (`--r-card`) for every card, tile, menu, sheet, fab, but a small card keeps the big card's PROPORTION, not its size: a row card is `--r-card-row` (13) |
|
|
932
|
-
| the size **of** things | `--tap-base` (3rem = 48) | `--tap*`, `--btn-h`, `--chip-h`, `--bar-h`, `--header-h`, `--fab`, `--icon*`, `--avatar*`, `--dot*`, `--ring*`, `--tile-*`. `--tap*` never under `--tap-floor` (44px) |
|
|
933
|
-
| faces | `--font-ui`, `--font-display` | |
|
|
934
|
-
| colour | `--color-surface`, `--color-ink`, `--color-paper`, `--color-brand`, `--color-on-brand` — no default: unset = the wireframe | every role: surface / words, the three card tones (paper · the brand washed 14% into the surface · the brand), the muted inks (the ink washed into their ground), accent, neutral actions, `--dot-a` |
|
|
935
|
-
| colour roles (the escape hatch) | the ink sets `--ink-{onlight,ondark}(-muted)`; the sides `--surface-dark`, `--card-{1,2,3,4,5}-dark`, `--accent-dark`, `--action-dark`, `--neg-dark`; `--surface`, `--card-{1,2,3,4,5}-bg`, `--card-border`, `--accent`, `--action-{bg,border}`, `--pos`, `--neg`, `--dot-{a,b}`, `--signal-{ok,warn,error,info}-{onlight,ondark}` | a role set directly wins over its derivation; the tints derive from the roles |
|
|
936
|
-
|
|
937
|
-
Rules:
|
|
938
|
-
|
|
939
|
-
- **Three levels, widest first — use the widest that says what you mean.**
|
|
940
|
-
1. a **base** moves a whole family (`--sp-base`, `--fs-base`, `--r-base`, `--tap-base`, the five `--color-*`): the system keeps its proportions.
|
|
941
|
-
2. a **role / root token** moves one meaning everywhere (`--accent`, `--card-1-bg`, `--btn-h`, `--chip-h`, `--header-h`, `--screen-w`, `--motion-sheet`…).
|
|
942
|
-
3. a **knob** moves ONE component (`--btn-pad-x`, `--chip-bg`, `--icon-btn-size`, `--appbar-pad-x`…) — on `:root` for everywhere, or on any element for
|
|
943
|
-
the components inside it. A knob is declared nowhere; its default is the fallback in the rule. The full list is generated from the CSS:
|
|
944
|
-
[`docs/knobs.md`](docs/knobs.md) (`npm run knobs`). Missing the knob you need? That is a gap in cardds — report it; it is added as a knob, never as your special case.
|
|
945
|
-
- **Never a multiplier, never a step of the scale** (`--sp-4`, `--fs-h1`, `--r-card` — retune the base, or the component's knob), never a private
|
|
946
|
-
(`--_*`) or a published value (`--card-pad`, `--bar-reserve`, `--sheet-*`: other rules do arithmetic with them).
|
|
947
|
-
- **On `:root` only.** The derived tokens are computed where they are declared
|
|
948
|
-
(`:root`), so a base set on a subtree does not reach them.
|
|
949
|
-
- **After `cardds.css`.** A stock palette's block and a project's `:root` weigh
|
|
950
|
-
the same; the later file wins.
|
|
951
|
-
- **Coloured mode is the attribute.** `data-palette`, whatever its value, is what
|
|
952
|
-
trades the wireframe's borders for the card's shadow and the controls' soft
|
|
953
|
-
edge (`palette.css`, `forms.css`). Colours of your own → `<html data-palette="own">`.
|
|
954
|
-
Without it the colours still apply, on wireframe borders.
|
|
955
|
-
- **cardds does not pick the colours; it measures two things about them.** A theme's
|
|
956
|
-
values are the project's call and the derivations are only starting points; any
|
|
957
|
-
role can be set outright. What cardds checks is that the member can see each card
|
|
958
|
-
([`cardGrounds()`](#can-the-member-see-the-card) names a tone whose fill and edge both
|
|
959
|
-
sit under 3:1 on its ground) and read what stands on it
|
|
960
|
-
([`inkGrounds()`](#ink-follows-its-ground) names a word or icon under 4.5:1 or 3:1).
|
|
961
|
-
Both name the tokens to move.
|
|
962
|
-
- The member's text-size setting is the same `--fs-base`, written on `<html>`
|
|
963
|
-
at run time by `setTextScale(percent)` (a share of the theme's base; 100 = the
|
|
964
|
-
theme's own): the words grow, and the things grow HALF as much (`--tap-base`; `controlsFor(percent)` — 130% text → 115% controls, 160% → 130%), so a larger text still fits what holds it. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
|
|
965
|
-
|
|
966
|
-
Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
|
|
967
|
-
palette `own` shows a colour well per colour base.
|
|
968
|
-
`tests/tokens.spec.js` holds the defaults to the px they always were and the
|
|
969
|
-
template to the defaults; the step / centre geometry runs at two extreme
|
|
970
|
-
themes (`tight`, `loose`).
|
|
971
|
-
|
|
972
|
-
## Ink follows its ground
|
|
973
|
-
|
|
974
|
-
The accessibility rule for colour (Lh 2026-10-18): "there are text colours ONLIGHT and ONDARK. When the ground is light, use
|
|
975
|
-
onlight; when the ground is dark, use ondark." It holds for words and for glyphs: icons, marks and dots that carry meaning, a
|
|
976
|
-
check's tick, a switch's knob, the pager's dots.
|
|
977
|
-
|
|
978
|
-
1. **Two ink sets.** A palette (or your theme) sets `--ink-onlight` and `--ink-onlight-muted`, `--ink-ondark` and
|
|
979
|
-
`--ink-ondark-muted`. Each holds 4.5:1 on every ground of its side.
|
|
980
|
-
2. **Every ground says only its side.** `--surface-dark`, `--card-1-dark` … `--card-5-dark`, `--accent-dark` (the brand fill),
|
|
981
|
-
`--action-dark` (the paper of buttons, chips and fields), `--neg-dark`: `1` is dark, `0` light. A surface turns its side into
|
|
982
|
-
the inks every piece on it reads: `--card-ink`, `--card-muted`, `--on-accent`, `--accent-ink` and the four signals.
|
|
983
|
-
3. **A piece reads its ink only from the ground it stands on** (`currentColor`, `--card-ink`, `--card-muted`). It never names a
|
|
984
|
-
fixed colour, and a muted word or glyph is a shade, never an opacity.
|
|
985
|
-
4. **4.5:1 for words, 3:1 for large words (24px, or 18.66px bold), icons and glyphs** (WCAG 2.2 SC 1.4.3, 1.4.11), on the ground
|
|
986
|
-
the piece is really drawn on. A tint that carries words (a banner, a step bar, today's day) is a ground of its card's side, so
|
|
987
|
-
a dark fill holds its ondark ink on its 12% tint too.
|
|
988
|
-
5. **Signals** have an onlight and an ondark shade (`--signal-ok-onlight`, `--signal-ok-ondark`, …): no one shade of a hue holds
|
|
989
|
-
4.5:1 on both. The ondark one is the ondark ink unless a palette gives its own. The icon and the words carry the meaning.
|
|
990
|
-
|
|
991
|
-
**Dark mode** is the sides turned to `1` with their fills darkened, and no rule per piece:
|
|
992
|
-
|
|
993
|
-
```css
|
|
994
|
-
:root[data-mode="dark"] {
|
|
995
|
-
--surface-dark: 1; --card-1-dark: 1; --card-2-dark: 1; --card-4-dark: 1; --action-dark: 1;
|
|
996
|
-
--surface: #121212; --color-paper: #1e1e1e; --card-1-bg: #1e1e1e; --card-2-bg: #2a2a2a; --card-4-bg: #333333; --action-bg: #262626;
|
|
997
|
-
}
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
`tests/ink.spec.js` holds the gallery to exactly that block: every story, every tone, 0 pieces short.
|
|
1001
|
-
|
|
1002
|
-
Check your own theme where it is loaded. `inkGrounds()` walks every visible word, field value, icon and glyph inside a root and
|
|
1003
|
-
returns one row per piece, with the side its ground declares and the ink set its colour is:
|
|
1004
|
-
|
|
1005
|
-
```ts
|
|
1006
|
-
import { inkGrounds } from '@usableapp/cardds';
|
|
1007
|
-
console.table(inkGrounds().filter((r) => !r.pass)); // a dev page
|
|
1008
|
-
|
|
1009
|
-
// a Playwright test on your app (Playwright sends the function to the page)
|
|
1010
|
-
const short = (await page.evaluate(inkGrounds)).filter((r) => !r.pass).map((r) => r.problem);
|
|
1011
|
-
expect(short).toEqual([]);
|
|
1012
|
-
// '.t-caption.t-muted "since May" on .card.card--3 (light side): #575757 (--card-muted = --ink-onlight-muted) on #141414 (--card-bg) is 2.55:1, words need 4.5:1'
|
|
1013
|
-
```
|
|
1014
|
-
|
|
1015
|
-
A side and an ink set that disagree with the ground (`light side` on a dark fill) say the ground declared the wrong side. It
|
|
1016
|
-
reports and changes nothing. It does not measure a piece on a picture (a `url()` ground; a gradient counts as decoration unless it
|
|
1017
|
-
is one colour), a disabled control, a hidden one, words drawn by CSS `content`, decoration colours (`--dot-a`, `--dot-b`, the
|
|
1018
|
-
highlights), or what lies under a positioned element that is not its ancestor (a scrim drawn by `::after`). cardds holds itself to
|
|
1019
|
-
the same function: `tests/ink.spec.js` renders every gallery story in every stock palette and in the dark mode above, as written
|
|
1020
|
-
and with every card turned to each tone 1–5, about 27,000 pieces a pass, in `npm run ci`.
|
|
1021
|
-
|
|
1022
|
-
## Can the member see the card?
|
|
1023
|
-
|
|
1024
|
-
A card is one boundary, and a boundary the member cannot see is no boundary (WCAG 2.2 SC 1.4.11, non-text contrast: 3:1 against what is
|
|
1025
|
-
next to it). A card's boundary is its fill against the ground it stands on, or its edge (`--card-border`, or a finish's line).
|
|
1026
|
-
In coloured mode the edge is `--on-surface-muted`, the ground's muted words, which a palette holds to AA on its surface; the wireframe's
|
|
1027
|
-
is its black line. A light card on a light ground cannot reach 3:1 by its fill (a tone-1 card in a tone-1 sheet is 1:1), so the edge
|
|
1028
|
-
carries it.
|
|
1029
|
-
|
|
1030
|
-
Your theme decides the colours, so run the check on your theme. `cardGrounds()` builds a card of each tone (1–5) on each ground a card
|
|
1031
|
-
stands on (the screen, a sheet's body, a `paper` sheet's body) where your theme is loaded, reads what the browser draws and returns one
|
|
1032
|
-
row per pair:
|
|
1033
|
-
|
|
1034
|
-
```ts
|
|
1035
|
-
import { cardGrounds } from '@usableapp/cardds';
|
|
1036
|
-
console.table(cardGrounds()); // a dev page: ground · tone · fill · edge · pass · problem
|
|
1037
|
-
|
|
1038
|
-
// a Playwright test on your app (Playwright sends the function to the page)
|
|
1039
|
-
const rows = await page.evaluate(cardGrounds);
|
|
1040
|
-
expect(rows.filter((r) => !r.pass).map((r) => r.problem)).toEqual([]);
|
|
1041
|
-
```
|
|
1042
|
-
|
|
1043
|
-
A failing row names the tokens and the shortfall, for example
|
|
1044
|
-
`tone 1 card on the screen: fill 1:1 (--card-1-bg #ffffff on --surface #ffffff), edge 1:1 (--card-border); one of them needs 3:1 — the fill is short by 2, or give --card-border a colour of 3:1 on #ffffff`.
|
|
1045
|
-
It reports and changes nothing: which knob you move is your call. It runs in a browser on purpose: your theme's `var()` aliases,
|
|
1046
|
-
`[data-palette]` blocks, finish and `color-mix()` are resolved by the same engine that draws the screen. A stock palette is held to the
|
|
1047
|
-
same check in every finish (`tests/card-grounds.spec.js`).
|
|
1048
|
-
|
|
1049
|
-
## New palette
|
|
1050
|
-
|
|
1051
|
-
Copy any block in `palette.css`, rename, retune:
|
|
1052
|
-
|
|
1053
|
-
```css
|
|
1054
|
-
[data-palette="mine"] {
|
|
1055
|
-
--surface: …;
|
|
1056
|
-
--ink-onlight: …; --ink-onlight-muted: …; /* words and glyphs on a light ground */
|
|
1057
|
-
--ink-ondark: …; --ink-ondark-muted: …; /* on a dark ground */
|
|
1058
|
-
--card-1-bg: …; /* + card-2 … card-5; a tone whose side is not the default's says so: --card-5-dark: 0; */
|
|
1059
|
-
--accent: …;
|
|
1060
|
-
--action-bg: …;
|
|
1061
|
-
}
|
|
1062
|
-
```
|
|
1063
|
-
|
|
1064
|
-
## Finish
|
|
1065
|
-
|
|
1066
|
-
The finish of a card's paper — a style apart from colour, beside any palette or none:
|
|
1067
|
-
|
|
1068
|
-
```html
|
|
1069
|
-
<html data-palette="orchard" data-finish="print"> <!-- or on any ancestor; no attribute = the flat look -->
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
Four values, one attribute (`css/finish.css`; the header names each and its knobs):
|
|
1073
|
-
|
|
1074
|
-
| `data-finish` | the paper | knobs (set in `theme.css`, as any knob) |
|
|
1075
|
-
|---|---|---|
|
|
1076
|
-
| `lit` | lit from above: a lit top edge, a hairline ring, a layered shadow that grows with height; filled buttons stand up and go down under the finger; fields sunk | the shadows every component reads (`--shadow-card`, `--shadow-float`, `--shadow-sheet`) |
|
|
1077
|
-
| `print` | printed paper: an ink line in the card's own ink, a hard misregistered offset, grain over the card, a dotted rule under a lone card's head | `--print-off` (the offset, 3px) · `--print-grain` (the grain's opacity, 0.32) |
|
|
1078
|
-
| `sticker` | a die-cut sticker: a white cut line, a short hard drop, a lone card leans (never one in a stack, a list or a feed), buttons with a bottom edge that press down | `--sticker-cut` (5px) · `--sticker-tilt` (0.8deg; `0deg` = straight) |
|
|
1079
|
-
| `ink` | a poster / comic print: a thick line in the surface's ink round every boundary and control, a hard offset in the same ink | `--ink-w` (2.5px) · `--ink-off` (5px) |
|
|
1080
|
-
|
|
1081
|
-
Under every finish the controls' wireframe border steps back (`--action-border`; ink puts its own line there) and the card keeps its edge (`--card-border`: lit's ring and sticker's cut do not reach 3:1 on a light ground, see [Can the member see the card?](#can-the-member-see-the-card)), a card in a closed stack's base stays flat, and the information inside a card stays flat — only a container or a control gets an edge. Colours are mixes of the roles, so a finish sits in any palette. The gallery's nav has a finish switch (key `f`); `Card · Finishes` shows the four side by side, `Home · Finish` a home under `print`.
|
|
1082
|
-
|
|
1083
|
-
## Scale: everything is rem
|
|
1084
|
-
|
|
1085
|
-
Every length token resolves to rem — a multiplier × one of four rem bases
|
|
1086
|
-
(`--sp-4` = `--sp-base` × 4 = 1rem, `--tap` = `--tap-base` = 3rem, `--r-card` =
|
|
1087
|
-
`--r-base` × 2, `--fs-h1` = `--fs-base` × 1.2; see "A project's theme") — so the
|
|
1088
|
-
whole system scales with the root font size. `base.css` keeps it at `100%` (the browser's setting, 16px by
|
|
1089
|
-
default) and steps narrow phones (≤360px) to `93.75%`, which keeps the
|
|
1090
|
-
column, its 48px taps and its type in proportion on 320–360 screens. Only
|
|
1091
|
-
`--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
|
|
1092
|
-
An app with its own text-size setting calls `setTextScale(percent)`, which writes
|
|
1093
|
-
`--fs-base` on `<html>` from the theme's base (100% = the theme's; 160% = 2.2rem by default): the type and its holders (the count
|
|
1094
|
-
badge, the inline icon) grow, the controls grow half as much (`--tap-base`), spacing and radii keep their size.
|
|
1095
|
-
Components never write a raw length: spacing from `--sp-*`, control sizes from
|
|
1096
|
-
`--tap`, `--tap-lg`, `--btn-h`, `--bar-h`, `--header-h`, `--fab`, `--badge`, container
|
|
1097
|
-
padding from `--screen-pad` / `--card-pad`, radii from
|
|
1098
|
-
`--r-*`, type from `--fs-*` / `--type-*`. Data (progress, counts) enters as
|
|
1099
|
-
unitless custom properties. One measured length comes from script: `--kb`,
|
|
1100
|
-
the height an on-screen keyboard covers, published on `:root` by `cardds.js`
|
|
1101
|
-
from the visual viewport — as a px length, since only the browser knows it,
|
|
1102
|
-
so no component ever writes `px` to consume it (`.action-bar` rides up by it,
|
|
1103
|
-
`.step` grows its room by it). The narrow-phone step is an `em` breakpoint,
|
|
1104
|
-
so it follows the text size too: at 131% a 390px phone counts as narrow.
|
|
1105
|
-
|
|
1106
|
-
## Icons & touch targets
|
|
1107
|
-
|
|
1108
|
-
Icons come from `icons.svg` (Lucide, ISC — monochrome stroke, currentColor):
|
|
1109
|
-
|
|
1110
|
-
```html
|
|
1111
|
-
<svg class="icon" aria-hidden="true"><use href="icons.svg#bell"/></svg>
|
|
1112
|
-
```
|
|
1113
|
-
|
|
1114
|
-
`.icon` = `--icon` (1.75rem / 28px), `.icon--xs` = 1rem inside captions and chips,
|
|
1115
|
-
`.icon--lg` = 2rem for empty states. Never raw emoji/unicode glyphs — they
|
|
1116
|
-
render as colored emoji on phones. Add icons by appending `<symbol>`s
|
|
1117
|
-
(grab from lucide.dev, paste path data).
|
|
1118
|
-
|
|
1119
|
-
Touch targets: `--tap` 3rem (`.icon-btn`), `--tap-sm` 2.75rem floor (`.icon-btn--sm`,
|
|
1120
|
-
selects, add-row), `--tap-lg` 3.5rem for hero controls. `.btn` ≥ `--btn-h` (3.25rem).
|
|
1121
|
-
Everything thumb-first.
|
|
1122
|
-
|
|
1123
|
-
The two bars across the top of a screen — `.topbar` (screen title) and
|
|
1124
|
-
`.appbar` (app context) — share one rule and one floor, `--header-h` (3.75rem /
|
|
1125
|
-
60px), so a title-only bar stands as tall as one carrying an icon button, a
|
|
1126
|
-
dropdown or a `.btn`. `.screen` reserves its top padding from the same token.
|
|
1127
|
-
`.topbar__actions` takes at most TWO controls (Lh 2026-09-22): a third and beyond
|
|
1128
|
-
is not drawn (`TopBar` warns once) — more moves go in a `Menu` behind one button.
|
|
1129
|
-
|
|
1130
|
-
## Typography levels
|
|
1131
|
-
|
|
1132
|
-
`t-display` 34–42/800 · `t-h1` 28 · `t-h2` 22 · `t-title` 17 · `t-body` 15 ·
|
|
1133
|
-
`t-label` 13 · `t-caption` 12 · `t-stat` big tabular
|
|
1134
|
-
numbers (`t-stat--mono`, `t-stat--xl`). Pixel values are at the default root;
|
|
1135
|
-
each is an `--fs-*` rem token. Swap `--font-display` per app for character.
|
|
7
|
+
Licence: none granted. The source is public to read, not to use. See [`LICENSE`](LICENSE).
|