@usableapp/cardds 0.4.2 → 0.5.1
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 +47 -0
- package/README.md +14 -13
- package/css/actions.css +6 -1
- package/css/base.css +9 -11
- package/css/card.css +7 -8
- package/css/choice.css +17 -4
- package/css/forms.css +7 -4
- package/css/journey.css +1 -1
- package/css/lists.css +2 -4
- package/css/media.css +3 -3
- package/css/numbers.css +1 -1
- package/css/sheet.css +5 -2
- package/css/theme.template.css +2 -2
- package/css/tokens.css +23 -21
- package/dist/cardds.css +81 -60
- package/dist/cards/CardHead.d.ts +3 -4
- package/dist/cards/CardHead.js +4 -4
- package/dist/cards/Stat.d.ts +1 -1
- package/dist/cards/Stat.js +1 -1
- package/dist/media/Quote.d.ts +1 -1
- package/dist/media/Quote.js +1 -1
- package/dist/scaffold/BaseContent.d.ts +1 -1
- package/dist/scaffold/BaseContent.js +1 -1
- package/dist/scaffold/TopBar.d.ts +3 -2
- package/dist/scaffold/TopBar.js +12 -1
- package/dist/sheets/Sheet.js +20 -0
- package/dist/type/Text.d.ts +2 -2
- package/dist/type/Text.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
> For: a project that consumes `@usableapp/cardds` and its AI — what changed between versions, written as before → after, so a screen can be brought up to date without reading the source. Newest first. A consumer never edits cardds; if something here leaves you without a way to do what you did before, report the gap.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
### Field type 26px, 56px tall (Lh 2026-09-22)
|
|
8
|
+
`--field-min-h` default is `--tap-lg` (56, was `--tap` 48) — every field stands as tall as the action field already did.
|
|
9
|
+
`--field-font` default is `400 var(--fs-lg) / 1.4 var(--font-ui)` (was `--type-body`, 22): the words being typed read a size up, level with the Composer. The field keeps its 48px height. A theme that set `--field-font` is unaffected.
|
|
10
|
+
|
|
11
|
+
### Field: no browser focus ring; the field's border goes full ink while the cursor is in it (Lh 2026-09-22)
|
|
12
|
+
The inner input drew the browser's own ring as a second box inside the field. Gone. Knob `--field-active-border` (default `currentColor`); coloured palettes keep the 25% border at rest only.
|
|
13
|
+
|
|
14
|
+
### Pin: the waiting cell shows a caret inside the pill, no focus ring (Lh 2026-09-22)
|
|
15
|
+
The `:focus-visible` outline sat outside the pill and was cropped by the sheet's edge. Gone. The cell the cursor waits in now has a full-ink border and a blinking bar drawn inside it (the native caret is hidden). Knobs `--pin-cell-active-border`, `--pin-caret-w`, `--pin-caret-h`.
|
|
16
|
+
|
|
17
|
+
## 0.5.0 — 2026-09-22
|
|
18
|
+
|
|
19
|
+
### Words are full ink; muted means inactive (Lh 2026-09-22)
|
|
20
|
+
`.base-content > .t-body` and `.kv__key` read `--on-surface` / `--card-ink` (were the muted grey). Knobs `--base-content-body-ink`, `--kv-key-ink` if a project wants them back. Muted stays on captions, `t-muted`, and inactive states (`.row--done`).
|
|
21
|
+
|
|
22
|
+
### Overline retired (Lh 2026-09-22) — BREAKING
|
|
23
|
+
A small caps line above a heading is a second message nobody asked for. Gone: `Text level="overline"` (TypeScript rejects it), `CardHead overline` (drop the prop — a walk's progress is a `Track` in the body, or nothing), `.t-overline`, `--type-overline`, `--track-overline`. The small name over a control ("รหัส OTP") is `Text level="label" muted`. Ring xs, tile-badge label, calendar weekday, timeline mark and quote attribution now read `--type-caption` with no tracking or uppercase (their font knobs are unchanged).
|
|
24
|
+
|
|
25
|
+
### h1 and h2 one size down (Lh 2026-09-22)
|
|
26
|
+
`--fs-h1` ×1.45 → ×1.2 (32 → 26: the screen title, so more of it fits the top bar) · `--fs-h2` ×1.2 → ×1 (26 → 22: a card's heading, told apart by the display face + weight). `--fs-3xl` (the xl button) stays 32; `--fs-lg`/`--fs-2xl` stay 26. Token names unchanged; a theme that sets `--fs-base` follows.
|
|
27
|
+
|
|
28
|
+
### TopBar: at most two trailing actions (Lh 2026-09-22)
|
|
29
|
+
A third control in `actions` is no longer drawn (`.topbar__actions > :nth-child(n+3)`), and `TopBar` warns once in the console. A bar that showed three icon buttons now shows two — move the rest into a `Menu` behind one `IconBtn`, or onto the base. Story `TopBar · TooManyActions`.
|
|
30
|
+
|
|
31
|
+
### `--text-scale` retired — the text-size setting is `--fs-base`
|
|
32
|
+
`--text-scale` multiplied the root font size, so a member's "larger text" zoomed every button, gap and icon too. Gone. An app's text-size setting now writes the type base on `<html>`: `document.documentElement.style.setProperty('--fs-base', '2.2rem')` (1.375rem = 100%, 2.2rem = 160%) — only the words and their holders (count badge, inline icon) grow. Remove `--text-scale` from your app; if a screen relied on the UI growing with the text, that is a gap to report.
|
|
33
|
+
|
|
34
|
+
### The type scale, rethought (Lh 2026-09-22) — BREAKING to the eye
|
|
35
|
+
The base IS the body: `--fs-base` 1.375rem (22px). Nothing that reads is under 17px. Sizes are a job's fit, not rungs — jobs share a size. Multipliers only, no clamp/vw. 16 tokens → 7 sizes:
|
|
36
|
+
| × | px | tokens |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| 0.775 | 17 | `--fs-xs` `--fs-caption` `--fs-label` `--fs-sm` (was 11 · 12 · 13 · 14) |
|
|
39
|
+
| 1 | 22 | `--fs-body` `--fs-md` `--fs-title` (was 15 · 16 · 17) |
|
|
40
|
+
| 1.2 | 26 | `--fs-h2` `--fs-lg` `--fs-2xl` (was 22 · 20 · 24) |
|
|
41
|
+
| 1.45 | 32 | `--fs-h1` `--fs-3xl` (was 28 · 32) |
|
|
42
|
+
| 1.8 | 40 | `--fs-display` `--fs-stat` (was clamp 34–42 · 30–38) |
|
|
43
|
+
| 2.5 | 55 | `--fs-hero` (was 56) |
|
|
44
|
+
| 2.9 | 64 | `--fs-giant` (was clamp 48–64) |
|
|
45
|
+
`--badge` = base × 1.2 (26), `--icon-xs` = base × 0.775 (17). A project's `theme.css` that set `--fs-base: 1rem` now gets 16px body — set it to `1.375rem` (the template's new default), or leave it unset. Every token name is unchanged.
|
|
46
|
+
- The sheet's peek strip follows the head's floor `--sheet-head-h` = max(a tap, an h2 line); when a title still wraps (a narrow phone at a large text size) the `Sheet` measures its head and publishes the real strip on its box, so the whole title row stays above the cut.
|
|
47
|
+
- The type page (`Text · Scale`) shows the 7 sizes with both faces and every job that shares each; the `/type` page is gone.
|
|
48
|
+
|
|
49
|
+
- `--field-font` (the words typed into a `Field`, default `--type-body`) and `--composer-input-font` (the Composer's textarea) are knobs now — a project that wants bigger type in its inputs sets them in `theme.css`. Every input already follows `--text-scale` on `<html>` (rem); if yours does not, look for a `px` `font-size` on `input` in your own CSS (the iOS no-zoom rule is the usual one).
|
|
50
|
+
- `.base-content > img` keeps its ratio inside `--base-content-media-h` (`height: auto`).
|
|
51
|
+
|
|
5
52
|
## 0.4.2 — 2026-09-22
|
|
6
53
|
|
|
7
54
|
### Words on the base: a picture and the dots
|
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ whose markup has rules, because the sheet's box is the step itself:
|
|
|
141
141
|
<!-- reads: the words only -->
|
|
142
142
|
<section class="step">
|
|
143
143
|
<section class="base-content">
|
|
144
|
-
<div class="card__head"><
|
|
144
|
+
<div class="card__head"><h2 class="t-h2">…</h2></div>
|
|
145
145
|
<p class="t-body">…</p>
|
|
146
146
|
</section>
|
|
147
147
|
</section>
|
|
@@ -222,7 +222,7 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
222
222
|
<div class="screen screen--fill">
|
|
223
223
|
<header class="topbar"><h1 class="t-h1">ธนาคารเวลายินดีต้อนรับ</h1></header>
|
|
224
224
|
<article class="card card--centre">
|
|
225
|
-
<div class="card__head"><div><
|
|
225
|
+
<div class="card__head"><div><h2 class="t-h2">ปรับขนาดตัวหนังสือ</h2></div></div>
|
|
226
226
|
<p class="t-body">…</p>
|
|
227
227
|
</article>
|
|
228
228
|
</div>
|
|
@@ -283,10 +283,9 @@ between at 40% down the screen (see *Anatomy*, "Where the centre is"):
|
|
|
283
283
|
the far right it runs all the way across. The heading wraps freely —
|
|
284
284
|
no line budget, no squeeze, no clamp (Lh 2026-09-16): a height rule on
|
|
285
285
|
words only distorts them. The head is FLAT markup — heading, then an
|
|
286
|
-
optional
|
|
287
|
-
wrapper element, no `<p>`: the head wraps, the
|
|
288
|
-
|
|
289
|
-
rest (Lh 2026-09-16).
|
|
286
|
+
optional caption as a span, then the far-right thing; no
|
|
287
|
+
wrapper element, no `<p>`: the head wraps, the caption takes the row
|
|
288
|
+
below (`order`), the gap tokens do the rest (Lh 2026-09-16).
|
|
290
289
|
- **Content**: whatever the card frames — rows, key-values, a gauge, a
|
|
291
290
|
paragraph. Frameless, per rule 9.
|
|
292
291
|
- **Foot** (`.card__foot`): the card's action buttons on one line. One
|
|
@@ -500,8 +499,9 @@ Rules:
|
|
|
500
499
|
- **cardds does not judge the colours.** No contrast check, no warning — a
|
|
501
500
|
theme's values are the project's call. The derivations are only starting points;
|
|
502
501
|
any role can be set outright.
|
|
503
|
-
-
|
|
504
|
-
|
|
502
|
+
- The member's text-size setting is the same `--fs-base`, written on `<html>`
|
|
503
|
+
by the app at run time (2.2rem = 160%): the words grow, the things keep their
|
|
504
|
+
size. (`--text-scale`, which multiplied the root and zoomed the UI too, is gone.)
|
|
505
505
|
|
|
506
506
|
Try it live: the gallery's nav (`npm run dev`) has a slider per size base, and
|
|
507
507
|
palette `own` shows a colour well per colour base.
|
|
@@ -531,10 +531,9 @@ whole system still resizes from one number: the root font size. `base.css` keeps
|
|
|
531
531
|
default) and steps narrow phones (≤360px) to `93.75%`, which keeps the 25rem
|
|
532
532
|
column, its 48px taps and its type in proportion on 320–360 screens. Only
|
|
533
533
|
`--border-w`, `--rule-w` and the focus ring stay in px, so edges stay crisp.
|
|
534
|
-
An app with its own text-size setting
|
|
535
|
-
|
|
536
|
-
the
|
|
537
|
-
than overwriting it, and the app never writes a length from script.
|
|
534
|
+
An app with its own text-size setting writes `--fs-base` on `<html>` (the type
|
|
535
|
+
base, 1.375rem = 100%; 2.2rem = 160%): only the type and its holders (the count
|
|
536
|
+
badge, the inline icon) grow; taps, spacing and radii keep their size.
|
|
538
537
|
Components never write a raw length: spacing from `--sp-*`, control sizes from
|
|
539
538
|
`--tap`, `--tap-lg`, `--btn-h`, `--bar-h`, `--header-h`, `--fab`, `--badge`, container
|
|
540
539
|
padding from `--screen-pad` / `--card-pad`, radii from
|
|
@@ -567,10 +566,12 @@ The two bars across the top of a screen — `.topbar` (screen title) and
|
|
|
567
566
|
`.appbar` (app context) — share one rule and one floor, `--header-h` (3.75rem /
|
|
568
567
|
60px), so a title-only bar stands as tall as one carrying an icon button, a
|
|
569
568
|
dropdown or a `.btn`. `.screen` reserves its top padding from the same token.
|
|
569
|
+
`.topbar__actions` takes at most TWO controls (Lh 2026-09-22): a third and beyond
|
|
570
|
+
is not drawn (`TopBar` warns once) — more moves go in a `Menu` behind one button.
|
|
570
571
|
|
|
571
572
|
## Typography levels
|
|
572
573
|
|
|
573
574
|
`t-display` 34–42/800 · `t-h1` 28 · `t-h2` 22 · `t-title` 17 · `t-body` 15 ·
|
|
574
|
-
`t-label` 13 · `t-caption` 12 · `t-
|
|
575
|
+
`t-label` 13 · `t-caption` 12 · `t-stat` big tabular
|
|
575
576
|
numbers (`t-stat--mono`, `t-stat--xl`). Pixel values are at the default root;
|
|
576
577
|
each is an `--fs-*` rem token. Swap `--font-display` per app for character.
|
package/css/actions.css
CHANGED
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
margin-block: -0.15em;
|
|
39
39
|
}
|
|
40
40
|
.topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
|
|
41
|
+
/* at most TWO trailing controls (Lh 2026-09-22): the title must keep its line; a third and
|
|
42
|
+
beyond is not drawn — more moves belong in a Menu behind one button, or on the base */
|
|
43
|
+
.topbar__actions > :nth-child(n + 3) { display: none; }
|
|
41
44
|
|
|
42
45
|
/* a screen whose one big move sits at the top right (home's "ขอความช่วยเหลือ" — Lh 2026-09-09:
|
|
43
46
|
no bottom bar on the home screen) puts a primary .btn in the top bar's actions; it stands at
|
|
@@ -141,7 +144,9 @@
|
|
|
141
144
|
text-decoration: none;
|
|
142
145
|
justify-self: start; /* as a grid child it hugs its text, never stretches */
|
|
143
146
|
}
|
|
144
|
-
.dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); } /* signal dot: chips, legends
|
|
147
|
+
.dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); translate: 0 var(--dot-drop, 0.09em); } /* signal dot: chips, legends.
|
|
148
|
+
The drop: a line box is centred on its ascender room (Thai marks), the letters beside the dot sit lower — the dot rides down to the
|
|
149
|
+
letters' own middle, in em so it follows the text size (Lh 2026-09-22: "ตัวหนังสือกับไฟเสมอกัน") */
|
|
145
150
|
.dot--b { background: var(--dot-b); }
|
|
146
151
|
.chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
|
|
147
152
|
/* dark-slot inversion lives in card.css (.card--3 :is(...)) */
|
package/css/base.css
CHANGED
|
@@ -16,14 +16,13 @@ button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/* the scale: everything is rem, so this one number resizes the whole system.
|
|
19
|
-
100% = the
|
|
19
|
+
100% = the browser's text size (16px by default); narrow phones step to 15/16 so
|
|
20
20
|
the 25rem column and its 48px taps keep their proportions on 320–360 screens.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@media (max-width: 22.5em) { html { font-size: calc(93.75% * var(--text-scale, 1)); } }
|
|
21
|
+
An app's OWN text-size setting is NOT here (Lh 2026-09-22: --text-scale, which
|
|
22
|
+
multiplied the root, zoomed the UI too — retired): it is --fs-base on <html>,
|
|
23
|
+
the type base, so the words grow and the things keep their size (tokens.css). */
|
|
24
|
+
html { font-size: 100%; scrollbar-width: none; }
|
|
25
|
+
@media (max-width: 22.5em) { html { font-size: 93.75%; } }
|
|
27
26
|
html::-webkit-scrollbar { display: none; } /* Chromium/Safari */
|
|
28
27
|
|
|
29
28
|
body {
|
|
@@ -96,7 +95,6 @@ button { font: inherit; cursor: pointer; }
|
|
|
96
95
|
.t-body { font: var(--type-body); margin: 0; }
|
|
97
96
|
.t-label { font: var(--type-label); }
|
|
98
97
|
.t-caption { font: var(--type-caption); }
|
|
99
|
-
.t-overline { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; }
|
|
100
98
|
.t-stat { font: var(--type-stat); letter-spacing: var(--track-display); margin: 0; font-variant-numeric: tabular-nums; }
|
|
101
99
|
.t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
|
|
102
100
|
.t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
|
|
@@ -108,7 +106,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
108
106
|
.grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
|
|
109
107
|
|
|
110
108
|
/* ---- base content: words straight on the base, no card around them — an
|
|
111
|
-
|
|
109
|
+
a heading, a paragraph, maybe a .cta-pack. Was .section until
|
|
112
110
|
2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
|
|
113
111
|
In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
|
|
114
112
|
.base-content {
|
|
@@ -117,10 +115,10 @@ button { font: inherit; cursor: pointer; }
|
|
|
117
115
|
align-content: start; /* a tall column never stretches the rows — the words hug the top */
|
|
118
116
|
padding-block: var(--base-content-pad-y, var(--sp-4));
|
|
119
117
|
}
|
|
120
|
-
.base-content > .t-body { color: var(--on-surface
|
|
118
|
+
.base-content > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
|
|
121
119
|
/* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
|
|
122
120
|
the "where am I" dots under the words, centred — the same dots the centred card has under it */
|
|
123
|
-
.base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; max-width: 100%; object-fit: contain; justify-self: start; }
|
|
121
|
+
.base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
|
|
124
122
|
.base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
|
|
125
123
|
.base-content > .pager__at { justify-self: center; }
|
|
126
124
|
.base-content .cta-pack {
|
package/css/card.css
CHANGED
|
@@ -39,19 +39,18 @@
|
|
|
39
39
|
multi-line title can opt back out with .card__head--top. */
|
|
40
40
|
.card__head {
|
|
41
41
|
display: flex;
|
|
42
|
-
flex-wrap: wrap; /* the caption
|
|
42
|
+
flex-wrap: wrap; /* the caption takes a line of its own — no wrapper div */
|
|
43
43
|
align-items: center;
|
|
44
44
|
justify-content: space-between;
|
|
45
45
|
column-gap: var(--card-head-col-gap, var(--sp-3));
|
|
46
46
|
row-gap: var(--card-head-row-gap, var(--sp-0));
|
|
47
47
|
}
|
|
48
48
|
/* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
|
|
49
|
-
<div class="card__head"><
|
|
50
|
-
The heading and the far-right thing share the first row;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.card__head > :is(.t-
|
|
54
|
-
.card__head > .t-overline { order: -1; }
|
|
49
|
+
<div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
|
|
50
|
+
The heading and the far-right thing share the first row; a caption sits on
|
|
51
|
+
a row below (order 1), whatever the DOM order — so an author writes the
|
|
52
|
+
words in reading order. */
|
|
53
|
+
.card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
|
|
55
54
|
.card__head--top { align-items: flex-start; }
|
|
56
55
|
|
|
57
56
|
.card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
|
|
@@ -73,7 +72,7 @@
|
|
|
73
72
|
/* Skeleton head — the heading in a card's OWN head, and only there.
|
|
74
73
|
The heading wraps freely: no line budget, no squeeze, no clamp (Lh
|
|
75
74
|
2026-09-16 — a height rule on words only distorts them; content that needs
|
|
76
|
-
more rows takes more rows). The group that holds
|
|
75
|
+
more rows takes more rows). The group that holds heading ·
|
|
77
76
|
caption is the head's own wrapping row (above) — no wrapper element, no
|
|
78
77
|
height, only the row gap. */
|
|
79
78
|
.card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
|
package/css/choice.css
CHANGED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
.calendar { display: grid; gap: var(--calendar-gap, var(--sp-3)); }
|
|
139
139
|
.calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
|
|
140
140
|
.calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
|
|
141
|
-
.calendar__wd { font: var(--calendar-wd-font, var(--type-
|
|
141
|
+
.calendar__wd { font: var(--calendar-wd-font, var(--type-caption)); color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
|
|
142
142
|
.card--3 .calendar__wd { color: inherit; opacity: 0.7; }
|
|
143
143
|
.calendar__day {
|
|
144
144
|
width: 100%;
|
|
@@ -204,10 +204,23 @@
|
|
|
204
204
|
text-align: center;
|
|
205
205
|
font: var(--pin-cell-font, var(--type-stat));
|
|
206
206
|
font-variant-numeric: tabular-nums;
|
|
207
|
-
caret-color: currentColor;
|
|
208
207
|
}
|
|
209
208
|
.pin__cell:not(:placeholder-shown) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
210
|
-
|
|
209
|
+
/* the cell the cursor waits in (Lh 2026-09-22): NO outline ring — it sat outside the pill and got cropped by the sheet's
|
|
210
|
+
edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
|
|
211
|
+
is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
|
|
212
|
+
.pin__cell { caret-color: transparent; }
|
|
213
|
+
.pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
|
|
214
|
+
.pin__cell:focus:placeholder-shown {
|
|
215
|
+
background-image: linear-gradient(currentColor, currentColor);
|
|
216
|
+
background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
|
|
217
|
+
background-position: center;
|
|
218
|
+
background-repeat: no-repeat;
|
|
219
|
+
}
|
|
220
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
221
|
+
.pin__cell:focus:placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
|
|
222
|
+
@keyframes pin-caret { 50% { background-size: 0 0; } }
|
|
223
|
+
}
|
|
211
224
|
|
|
212
225
|
/* ---- composer: big text area with a bottom bar (icon · hint · send) ----
|
|
213
226
|
<div class="composer"><textarea class="composer__input" placeholder="What do you need?"></textarea>
|
|
@@ -221,7 +234,7 @@
|
|
|
221
234
|
padding: 0;
|
|
222
235
|
background: transparent;
|
|
223
236
|
color: inherit;
|
|
224
|
-
font: 700 var(--fs-2xl)/1.25 var(--font-display);
|
|
237
|
+
font: var(--composer-input-font, 700 var(--fs-2xl)/1.25 var(--font-display));
|
|
225
238
|
letter-spacing: var(--track-h2);
|
|
226
239
|
outline: none;
|
|
227
240
|
}
|
package/css/forms.css
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
background: var(--field-bg, var(--action-bg));
|
|
14
14
|
color: var(--field-ink, var(--action-ink));
|
|
15
15
|
padding-inline: var(--field-pad-x, var(--sp-4));
|
|
16
|
-
min-height: var(--field-min-h, var(--tap));
|
|
16
|
+
min-height: var(--field-min-h, var(--tap-lg)); /* 56, the action field's height, for every field (Lh 2026-09-22: "ใหญ่ดี") */
|
|
17
17
|
min-width: 0;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -22,19 +22,22 @@
|
|
|
22
22
|
border: 0;
|
|
23
23
|
background: transparent;
|
|
24
24
|
color: inherit;
|
|
25
|
-
font: var(--
|
|
25
|
+
font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does */
|
|
26
26
|
width: 100%;
|
|
27
27
|
min-width: 0;
|
|
28
|
-
outline
|
|
28
|
+
outline: none; /* the browser's ring drew a second box inside the field (Lh 2026-09-22) — the field itself tells focus */
|
|
29
|
+
caret-color: currentColor;
|
|
29
30
|
}
|
|
30
31
|
.field input::placeholder { color: inherit; opacity: 0.55; }
|
|
32
|
+
/* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
|
|
33
|
+
.field:has(:focus) { border-color: var(--field-active-border, currentColor); }
|
|
31
34
|
|
|
32
35
|
/* field with its action inside: date pick, send, go — the button sits flush right */
|
|
33
36
|
.field--action { padding-right: var(--sp-1); min-height: var(--field-action-min-h, var(--tap-lg)); }
|
|
34
37
|
.field--action > .icon-btn { margin-left: auto; }
|
|
35
38
|
|
|
36
39
|
/* colored palettes drop the visible outline elsewhere — fields keep one */
|
|
37
|
-
[data-palette] .field { border-color: color-mix(in srgb, currentColor 25%, transparent); }
|
|
40
|
+
[data-palette] .field:not(:has(:focus)) { border-color: color-mix(in srgb, currentColor 25%, transparent); }
|
|
38
41
|
|
|
39
42
|
/* ---- add-row: inline "+ add another" action ---- */
|
|
40
43
|
.add-row {
|
package/css/journey.css
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
background: var(--tile-badge-bg, var(--card-3-bg));
|
|
35
35
|
color: var(--tile-badge-ink, var(--card-3-ink));
|
|
36
36
|
}
|
|
37
|
-
.tile-badge__label { font: var(--tile-badge-label-font, var(--type-
|
|
37
|
+
.tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: 0.7; }
|
|
38
38
|
.tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
|
|
39
39
|
|
|
40
40
|
/* plain: no fill — a date box beside a title (Jun / 18) */
|
package/css/lists.css
CHANGED
|
@@ -82,8 +82,7 @@
|
|
|
82
82
|
.kv { margin: 0; display: grid; }
|
|
83
83
|
.kv__row { display: flex; align-items: center; justify-content: space-between; gap: var(--kv-row-gap, var(--sp-3)); min-height: var(--kv-row-min-h, var(--tap)); }
|
|
84
84
|
.kv--lined > .kv__row + .kv__row { border-top: var(--border-w) solid var(--hairline); }
|
|
85
|
-
.kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-
|
|
86
|
-
.card--3 .kv__key { color: inherit; opacity: 0.7; }
|
|
85
|
+
.kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-ink)); } /* full ink (Lh 2026-09-22): a key is read like the value; muted = inactive, not "secondary" */
|
|
87
86
|
.kv__val { margin: 0; font: var(--kv-val-font, var(--type-title)); display: inline-flex; align-items: center; gap: var(--kv-val-gap, var(--sp-2)); text-align: right; min-width: 0; }
|
|
88
87
|
|
|
89
88
|
/* ---- kv grid: value over label, cells split by a rule (Sex | Weight) ----
|
|
@@ -177,8 +176,7 @@
|
|
|
177
176
|
}
|
|
178
177
|
.timeline__mark {
|
|
179
178
|
justify-self: start;
|
|
180
|
-
font: var(--timeline-mark-font, var(--type-
|
|
181
|
-
letter-spacing: var(--track-overline);
|
|
179
|
+
font: var(--timeline-mark-font, var(--type-caption));
|
|
182
180
|
text-transform: uppercase;
|
|
183
181
|
padding: var(--timeline-mark-pad-y, var(--sp-1)) var(--timeline-mark-pad-x, var(--sp-2));
|
|
184
182
|
border-radius: var(--timeline-mark-radius, var(--r-chip));
|
package/css/media.css
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
/* ---- cover: the image IS the card; text over a scrim ----
|
|
7
7
|
<article class="card card--cover"><div class="ph" role="img"></div>
|
|
8
|
-
<div class="card__head"><
|
|
8
|
+
<div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
|
|
9
9
|
<div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
|
|
10
10
|
<div class="row">…meta…</div></article> */
|
|
11
11
|
.card--cover {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
border-radius: 0;
|
|
31
31
|
min-height: 0;
|
|
32
32
|
}
|
|
33
|
-
/* scrim: dark at both ends so
|
|
33
|
+
/* scrim: dark at both ends so the meta stays readable */
|
|
34
34
|
.card--cover::before {
|
|
35
35
|
content: "";
|
|
36
36
|
position: absolute;
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
}
|
|
53
53
|
.quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
|
|
54
54
|
.quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
|
|
55
|
-
.quote__by { font: var(--quote-by-font, var(--type-
|
|
55
|
+
.quote__by { font: var(--quote-by-font, var(--type-caption)); color: var(--quote-by-ink, var(--card-muted)); }
|
|
56
56
|
.card--3 .quote__by { color: inherit; opacity: 0.7; }
|
|
57
57
|
|
|
58
58
|
/* ---- tile grid: big square tiles — icon top, label bottom ----
|
package/css/numbers.css
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
.ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
|
|
38
38
|
.ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
|
|
39
39
|
.ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
|
|
40
|
-
.ring--xs .ring__value { font: var(--type-
|
|
40
|
+
.ring--xs .ring__value { font: var(--type-caption); }
|
|
41
41
|
|
|
42
42
|
/* ---- track: step progress in a pill — done · done · todo ----
|
|
43
43
|
<ol class="track"><li class="track__step is-done">icon</li> …</ol> */
|
package/css/sheet.css
CHANGED
|
@@ -89,7 +89,10 @@
|
|
|
89
89
|
/* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
|
|
90
90
|
have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
|
|
91
91
|
inherits it. A sheet whose head is taller retunes it on the box. */
|
|
92
|
-
:is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--
|
|
92
|
+
:is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h)); }
|
|
93
|
+
/* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
|
|
94
|
+
setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
|
|
95
|
+
:root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
|
|
93
96
|
|
|
94
97
|
/* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
|
|
95
98
|
.sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
|
|
@@ -201,7 +204,7 @@
|
|
|
201
204
|
- a label .row with its .check / .toggle may be the head when the sheet's
|
|
202
205
|
whole ask is that switch: peek shows it (a row is --tap tall plus its own
|
|
203
206
|
padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
|
|
204
|
-
.sheet > .card__head { min-height: var(--
|
|
207
|
+
.sheet > .card__head { min-height: var(--sheet-head-h); }
|
|
205
208
|
.sheet__head {
|
|
206
209
|
display: grid;
|
|
207
210
|
grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
|
package/css/theme.template.css
CHANGED
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
:root {
|
|
24
24
|
/* ---- size: four bases, the multipliers stay the system's ---- */
|
|
25
25
|
--sp-base: 0.25rem; /* 4 the room BETWEEN things: every --sp-N = base × N, so paddings and gaps */
|
|
26
|
-
--fs-base:
|
|
26
|
+
--fs-base: 1.375rem; /* 22 the type — the body size; every --fs-* is a multiplier of it (17 · 22 · 26 · 32 · 40 · 55 · 64), the count badge, the inline icon */
|
|
27
27
|
--r-base: 1rem; /* 16 the corners: every --r-* (0 = square; chips and buttons stay fully round) */
|
|
28
28
|
--tap-base: 3rem; /* 48 the size of THINGS: touch targets, control heights, icons, avatars, ring, tiles
|
|
29
29
|
(a touch target never goes under 44px, whatever this says) */
|
|
30
|
-
/*
|
|
30
|
+
/* a member's text-size setting is this same --fs-base, written on <html> by the app at run time (1.375rem = 100%, 2.2rem = 160%) — the words grow, the things keep their size */
|
|
31
31
|
|
|
32
32
|
/* ---- faces ---- */
|
|
33
33
|
--font-ui: "PK Nonthaburi", -apple-system, system-ui, sans-serif;
|
package/css/tokens.css
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
and never a multiplier, so the proportions of the system hold at any size.
|
|
21
21
|
The px in the comments are at these defaults (1rem = 16px). */
|
|
22
22
|
--sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
|
|
23
|
-
--fs-base:
|
|
23
|
+
--fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.775 (17).
|
|
24
|
+
Also the member's text-size setting: the app writes it on <html> at run time (2.2rem = 160%) — words grow, things don't */
|
|
24
25
|
--r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
|
|
25
26
|
--tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
|
|
26
27
|
--header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
--screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
|
|
55
56
|
--map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
|
|
56
57
|
--fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
|
|
57
|
-
--badge: calc(var(--fs-base) * 1.
|
|
58
|
+
--badge: calc(var(--fs-base) * 1.2); /* 26 count badge — holds a 17px number */
|
|
58
59
|
--dot: calc(var(--tap-base) * 5 / 24); /* 10 signal dot */
|
|
59
60
|
--dot-xs: calc(var(--tap-base) / 8); /* 6 marker dot */
|
|
60
61
|
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
--icon: calc(var(--tap-base) * 7 / 12); /* 28 default */
|
|
63
64
|
--icon-sm: calc(var(--tap-base) * 7 / 12); /* one size everywhere; retune if small contexts appear */
|
|
64
65
|
--icon-lg: calc(var(--tap-base) * 2 / 3); /* 32 empty states */
|
|
65
|
-
--icon-xs: var(--fs-base);
|
|
66
|
+
--icon-xs: calc(var(--fs-base) * 0.775); /* 17 inline with captions/chips only (row meta, link arrows) — the caption's size */
|
|
66
67
|
|
|
67
68
|
/* ---- people & big elements ---- */
|
|
68
69
|
--avatar-sm: calc(var(--tap-base) * 5 / 6); /* 40 row lead */
|
|
@@ -100,22 +101,24 @@
|
|
|
100
101
|
--font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
|
|
101
102
|
--font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
--fs-
|
|
106
|
-
--fs-
|
|
107
|
-
--fs-
|
|
108
|
-
--fs-
|
|
109
|
-
--fs-
|
|
110
|
-
--fs-
|
|
111
|
-
--fs-
|
|
112
|
-
--fs-
|
|
113
|
-
--fs-
|
|
114
|
-
--fs-
|
|
115
|
-
--fs-
|
|
116
|
-
--fs-
|
|
117
|
-
--fs-
|
|
118
|
-
--fs-
|
|
104
|
+
/* THE SCALE (Lh 2026-09-22): 7 sizes for 16 jobs — a size is a job's fit, not a rung; jobs share sizes.
|
|
105
|
+
Nothing that reads is under 17 (the smallest ui: chips). No clamp/vw: a size is one number. */
|
|
106
|
+
--fs-xs: calc(var(--fs-base) * 0.775); /* 17 the smallest ui words: ring xs, a tile-badge label */
|
|
107
|
+
--fs-caption: calc(var(--fs-base) * 0.775); /* 17 */
|
|
108
|
+
--fs-label: calc(var(--fs-base) * 0.775); /* 17 chips, buttons' small words */
|
|
109
|
+
--fs-sm: calc(var(--fs-base) * 0.775); /* 17 chips, segments — the floor */
|
|
110
|
+
--fs-body: var(--fs-base); /* 22 body, the words in a field */
|
|
111
|
+
--fs-md: var(--fs-base); /* 22 buttons, fields */
|
|
112
|
+
--fs-title: var(--fs-base); /* 22 row title (weight tells it apart) */
|
|
113
|
+
--fs-h2: var(--fs-base); /* 22 a card's heading (Lh 2026-09-22: one size down — the display face + weight tell it) */
|
|
114
|
+
--fs-lg: calc(var(--fs-base) * 1.2); /* 26 day numbers, tile values */
|
|
115
|
+
--fs-2xl: calc(var(--fs-base) * 1.2); /* 26 composer, small ring */
|
|
116
|
+
--fs-h1: calc(var(--fs-base) * 1.2); /* 26 screen title (Lh 2026-09-22: was 1.45 — a smaller title = more of it fits the bar) */
|
|
117
|
+
--fs-3xl: calc(var(--fs-base) * 1.45); /* 32 xl button */
|
|
118
|
+
--fs-display: calc(var(--fs-base) * 1.8); /* 40 hero words */
|
|
119
|
+
--fs-stat: calc(var(--fs-base) * 1.8); /* 40 big numbers */
|
|
120
|
+
--fs-hero: calc(var(--fs-base) * 2.5); /* 55 the one number on a sheet */
|
|
121
|
+
--fs-giant: calc(var(--fs-base) * 2.9); /* 64 data bands */
|
|
119
122
|
|
|
120
123
|
/* level weight size / line-height family */
|
|
121
124
|
--type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
|
|
@@ -128,8 +131,6 @@
|
|
|
128
131
|
--type-body: 400 var(--fs-body) / 1.5 var(--font-ui);
|
|
129
132
|
--type-label: 600 var(--fs-label) / 1.2 var(--font-ui); /* chips, buttons */
|
|
130
133
|
--type-caption: 500 var(--fs-caption) / 1.35 var(--font-ui);
|
|
131
|
-
--type-overline:700 var(--fs-xs) / 1.2 var(--font-mono); /* uppercase micro-labels */
|
|
132
|
-
--track-overline: 0.08em;
|
|
133
134
|
--type-stat: 800 var(--fs-stat) / 1.05 var(--font-display); /* big numbers */
|
|
134
135
|
|
|
135
136
|
/* ============================================================
|
|
@@ -208,5 +209,6 @@
|
|
|
208
209
|
falls back to when its own declaration cannot resolve.
|
|
209
210
|
============================================================ */
|
|
210
211
|
@property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
212
|
+
@property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
|
|
211
213
|
@property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
|
|
212
214
|
@property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
|
package/dist/cardds.css
CHANGED
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
and never a multiplier, so the proportions of the system hold at any size.
|
|
62
62
|
The px in the comments are at these defaults (1rem = 16px). */
|
|
63
63
|
--sp-base: 0.25rem; /* 4 spacing: --sp-N = base × N */
|
|
64
|
-
--fs-base:
|
|
64
|
+
--fs-base: 1.375rem; /* 22 type: the BODY is the base (Lh 2026-09-22) — every --fs-* is a multiplier of it; nothing under 0.775 (17).
|
|
65
|
+
Also the member's text-size setting: the app writes it on <html> at run time (2.2rem = 160%) — words grow, things don't */
|
|
65
66
|
--r-base: 1rem; /* 16 radius: --r-* (--r-chip stays fully round) */
|
|
66
67
|
--tap-base: 3rem; /* 48 the size of THINGS: touch targets and control heights (--tap*, --btn-h, --chip-h, --bar-h,
|
|
67
68
|
--header-h, --fab), icons, avatars, dots, the ring and the tiles. --sp-base is the room BETWEEN things.
|
|
@@ -95,7 +96,7 @@
|
|
|
95
96
|
--screen-pad: var(--sp-4); /* 16 the screen's inset — .sheet-stack cancels it */
|
|
96
97
|
--map-ring: calc(var(--tap-base) * 10 / 3); /* the rough-area ring on a map (sheet.css .map__ring) — a neighbourhood, not a house */
|
|
97
98
|
--fab: calc(var(--tap-base) * 3 / 2); /* 72 square floating action */
|
|
98
|
-
--badge: calc(var(--fs-base) * 1.
|
|
99
|
+
--badge: calc(var(--fs-base) * 1.2); /* 26 count badge — holds a 17px number */
|
|
99
100
|
--dot: calc(var(--tap-base) * 5 / 24); /* 10 signal dot */
|
|
100
101
|
--dot-xs: calc(var(--tap-base) / 8); /* 6 marker dot */
|
|
101
102
|
|
|
@@ -103,7 +104,7 @@
|
|
|
103
104
|
--icon: calc(var(--tap-base) * 7 / 12); /* 28 default */
|
|
104
105
|
--icon-sm: calc(var(--tap-base) * 7 / 12); /* one size everywhere; retune if small contexts appear */
|
|
105
106
|
--icon-lg: calc(var(--tap-base) * 2 / 3); /* 32 empty states */
|
|
106
|
-
--icon-xs: var(--fs-base);
|
|
107
|
+
--icon-xs: calc(var(--fs-base) * 0.775); /* 17 inline with captions/chips only (row meta, link arrows) — the caption's size */
|
|
107
108
|
|
|
108
109
|
/* ---- people & big elements ---- */
|
|
109
110
|
--avatar-sm: calc(var(--tap-base) * 5 / 6); /* 40 row lead */
|
|
@@ -141,22 +142,24 @@
|
|
|
141
142
|
--font-display: "FC Pride", var(--font-ui); /* the character face for headings and big numbers (fonts.css) */
|
|
142
143
|
--font-mono: "SF Mono", "JetBrains Mono", ui-monospace, monospace;
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
--fs-
|
|
147
|
-
--fs-
|
|
148
|
-
--fs-
|
|
149
|
-
--fs-
|
|
150
|
-
--fs-
|
|
151
|
-
--fs-
|
|
152
|
-
--fs-
|
|
153
|
-
--fs-
|
|
154
|
-
--fs-
|
|
155
|
-
--fs-
|
|
156
|
-
--fs-
|
|
157
|
-
--fs-
|
|
158
|
-
--fs-
|
|
159
|
-
--fs-
|
|
145
|
+
/* THE SCALE (Lh 2026-09-22): 7 sizes for 16 jobs — a size is a job's fit, not a rung; jobs share sizes.
|
|
146
|
+
Nothing that reads is under 17 (the smallest ui: chips). No clamp/vw: a size is one number. */
|
|
147
|
+
--fs-xs: calc(var(--fs-base) * 0.775); /* 17 the smallest ui words: ring xs, a tile-badge label */
|
|
148
|
+
--fs-caption: calc(var(--fs-base) * 0.775); /* 17 */
|
|
149
|
+
--fs-label: calc(var(--fs-base) * 0.775); /* 17 chips, buttons' small words */
|
|
150
|
+
--fs-sm: calc(var(--fs-base) * 0.775); /* 17 chips, segments — the floor */
|
|
151
|
+
--fs-body: var(--fs-base); /* 22 body, the words in a field */
|
|
152
|
+
--fs-md: var(--fs-base); /* 22 buttons, fields */
|
|
153
|
+
--fs-title: var(--fs-base); /* 22 row title (weight tells it apart) */
|
|
154
|
+
--fs-h2: var(--fs-base); /* 22 a card's heading (Lh 2026-09-22: one size down — the display face + weight tell it) */
|
|
155
|
+
--fs-lg: calc(var(--fs-base) * 1.2); /* 26 day numbers, tile values */
|
|
156
|
+
--fs-2xl: calc(var(--fs-base) * 1.2); /* 26 composer, small ring */
|
|
157
|
+
--fs-h1: calc(var(--fs-base) * 1.2); /* 26 screen title (Lh 2026-09-22: was 1.45 — a smaller title = more of it fits the bar) */
|
|
158
|
+
--fs-3xl: calc(var(--fs-base) * 1.45); /* 32 xl button */
|
|
159
|
+
--fs-display: calc(var(--fs-base) * 1.8); /* 40 hero words */
|
|
160
|
+
--fs-stat: calc(var(--fs-base) * 1.8); /* 40 big numbers */
|
|
161
|
+
--fs-hero: calc(var(--fs-base) * 2.5); /* 55 the one number on a sheet */
|
|
162
|
+
--fs-giant: calc(var(--fs-base) * 2.9); /* 64 data bands */
|
|
160
163
|
|
|
161
164
|
/* level weight size / line-height family */
|
|
162
165
|
--type-display: 800 var(--fs-display) / 1.15 var(--font-display); /* leading: Thai marks stack to 1.1em up and 0.3em down in FC Pride — 1.02 clipped them (2026-09-10) */
|
|
@@ -169,8 +172,6 @@
|
|
|
169
172
|
--type-body: 400 var(--fs-body) / 1.5 var(--font-ui);
|
|
170
173
|
--type-label: 600 var(--fs-label) / 1.2 var(--font-ui); /* chips, buttons */
|
|
171
174
|
--type-caption: 500 var(--fs-caption) / 1.35 var(--font-ui);
|
|
172
|
-
--type-overline:700 var(--fs-xs) / 1.2 var(--font-mono); /* uppercase micro-labels */
|
|
173
|
-
--track-overline: 0.08em;
|
|
174
175
|
--type-stat: 800 var(--fs-stat) / 1.05 var(--font-display); /* big numbers */
|
|
175
176
|
|
|
176
177
|
/* ============================================================
|
|
@@ -249,6 +250,7 @@
|
|
|
249
250
|
falls back to when its own declaration cannot resolve.
|
|
250
251
|
============================================================ */
|
|
251
252
|
@property --sheet-peek { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
253
|
+
@property --sheet-head-h { syntax: "<length>"; inherits: true; initial-value: 48px; } /* the sheet head's floor (sheet.css) — registered so the Sheet reads it resolved */
|
|
252
254
|
@property --sheet-half { syntax: "<number>"; inherits: true; initial-value: 0.5; }
|
|
253
255
|
@property --sheet-3q { syntax: "<number>"; inherits: true; initial-value: 0.25; }
|
|
254
256
|
|
|
@@ -390,14 +392,13 @@ button, [role="button"], [role="tab"], [role="option"], [role="menuitem"], a.btn
|
|
|
390
392
|
}
|
|
391
393
|
|
|
392
394
|
/* the scale: everything is rem, so this one number resizes the whole system.
|
|
393
|
-
100% = the
|
|
395
|
+
100% = the browser's text size (16px by default); narrow phones step to 15/16 so
|
|
394
396
|
the 25rem column and its 48px taps keep their proportions on 320–360 screens.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
@media (max-width: 22.5em) { html { font-size: calc(93.75% * var(--text-scale, 1)); } }
|
|
397
|
+
An app's OWN text-size setting is NOT here (Lh 2026-09-22: --text-scale, which
|
|
398
|
+
multiplied the root, zoomed the UI too — retired): it is --fs-base on <html>,
|
|
399
|
+
the type base, so the words grow and the things keep their size (tokens.css). */
|
|
400
|
+
html { font-size: 100%; scrollbar-width: none; }
|
|
401
|
+
@media (max-width: 22.5em) { html { font-size: 93.75%; } }
|
|
401
402
|
html::-webkit-scrollbar { display: none; } /* Chromium/Safari */
|
|
402
403
|
|
|
403
404
|
body {
|
|
@@ -470,7 +471,6 @@ button { font: inherit; cursor: pointer; }
|
|
|
470
471
|
.t-body { font: var(--type-body); margin: 0; }
|
|
471
472
|
.t-label { font: var(--type-label); }
|
|
472
473
|
.t-caption { font: var(--type-caption); }
|
|
473
|
-
.t-overline { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; }
|
|
474
474
|
.t-stat { font: var(--type-stat); letter-spacing: var(--track-display); margin: 0; font-variant-numeric: tabular-nums; }
|
|
475
475
|
.t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
|
|
476
476
|
.t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
|
|
@@ -482,7 +482,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
482
482
|
.grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
|
|
483
483
|
|
|
484
484
|
/* ---- base content: words straight on the base, no card around them — an
|
|
485
|
-
|
|
485
|
+
a heading, a paragraph, maybe a .cta-pack. Was .section until
|
|
486
486
|
2026-09-16 (Lh: the name said nothing about WHERE it lives; this one does).
|
|
487
487
|
In a .step it is the reading block and step.css adds the flex/scroll rule ---- */
|
|
488
488
|
.base-content {
|
|
@@ -491,10 +491,10 @@ button { font: inherit; cursor: pointer; }
|
|
|
491
491
|
align-content: start; /* a tall column never stretches the rows — the words hug the top */
|
|
492
492
|
padding-block: var(--base-content-pad-y, var(--sp-4));
|
|
493
493
|
}
|
|
494
|
-
.base-content > .t-body { color: var(--on-surface
|
|
494
|
+
.base-content > .t-body { color: var(--base-content-body-ink, var(--on-surface)); max-width: 34ch; } /* full ink: a sentence is read, muted is for meta (Lh 2026-09-22 — the words on the base were grey and hard to read) */
|
|
495
495
|
/* an illustration among the words (Lh 2026-09-22): capped, so a tall picture never pushes the words under the bar;
|
|
496
496
|
the "where am I" dots under the words, centred — the same dots the centred card has under it */
|
|
497
|
-
.base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; max-width: 100%; object-fit: contain; justify-self: start; }
|
|
497
|
+
.base-content > :is(img, .ph) { max-height: var(--base-content-media-h, 14rem); width: auto; height: auto; max-width: 100%; object-fit: contain; justify-self: start; } /* height auto: an <img> with width/height attributes keeps its ratio inside the cap — no band under the picture */
|
|
498
498
|
.base-content > .ph { width: 100%; height: var(--base-content-media-h, 14rem); }
|
|
499
499
|
.base-content > .pager__at { justify-self: center; }
|
|
500
500
|
.base-content .cta-pack {
|
|
@@ -561,19 +561,18 @@ button { font: inherit; cursor: pointer; }
|
|
|
561
561
|
multi-line title can opt back out with .card__head--top. */
|
|
562
562
|
.card__head {
|
|
563
563
|
display: flex;
|
|
564
|
-
flex-wrap: wrap; /* the caption
|
|
564
|
+
flex-wrap: wrap; /* the caption takes a line of its own — no wrapper div */
|
|
565
565
|
align-items: center;
|
|
566
566
|
justify-content: space-between;
|
|
567
567
|
column-gap: var(--card-head-col-gap, var(--sp-3));
|
|
568
568
|
row-gap: var(--card-head-row-gap, var(--sp-0));
|
|
569
569
|
}
|
|
570
570
|
/* flat head markup (Lh 2026-09-16 — no <p>, no wrapper, only padding and gap tokens):
|
|
571
|
-
<div class="card__head"><
|
|
572
|
-
The heading and the far-right thing share the first row;
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
.card__head > :is(.t-
|
|
576
|
-
.card__head > .t-overline { order: -1; }
|
|
571
|
+
<div class="card__head"><h2 class="t-h2">…</h2><span class="t-caption t-muted">…</span><button class="icon-btn icon-btn--sm">…</button></div>
|
|
572
|
+
The heading and the far-right thing share the first row; a caption sits on
|
|
573
|
+
a row below (order 1), whatever the DOM order — so an author writes the
|
|
574
|
+
words in reading order. */
|
|
575
|
+
.card__head > :is(.t-caption, .t-body) { flex: 1 1 100%; order: 1; margin: 0; }
|
|
577
576
|
.card__head--top { align-items: flex-start; }
|
|
578
577
|
|
|
579
578
|
.card__media { border-radius: var(--card-media-radius, var(--r-media)); overflow: hidden; position: relative; } /* relative: corner badges */
|
|
@@ -595,7 +594,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
595
594
|
/* Skeleton head — the heading in a card's OWN head, and only there.
|
|
596
595
|
The heading wraps freely: no line budget, no squeeze, no clamp (Lh
|
|
597
596
|
2026-09-16 — a height rule on words only distorts them; content that needs
|
|
598
|
-
more rows takes more rows). The group that holds
|
|
597
|
+
more rows takes more rows). The group that holds heading ·
|
|
599
598
|
caption is the head's own wrapping row (above) — no wrapper element, no
|
|
600
599
|
height, only the row gap. */
|
|
601
600
|
.card__head :is(h1, h2, h3) { overflow-wrap: anywhere; }
|
|
@@ -1174,7 +1173,10 @@ button { font: inherit; cursor: pointer; }
|
|
|
1174
1173
|
/* --sheet-peek is declared on the BOX the sheet slides in, not on the sheet: the words behind a peeked sheet
|
|
1175
1174
|
have to read it too (step.css keeps that much room under them), and a token only flows down. The sheet
|
|
1176
1175
|
inherits it. A sheet whose head is taller retunes it on the box. */
|
|
1177
|
-
:is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--
|
|
1176
|
+
:is(.step, .sheet-stage) { --_handle: var(--sp-1); --sheet-peek: calc(var(--card-pad-rest) + var(--_handle) + 2 * var(--card-gap-rest) + var(--sheet-head-h)); }
|
|
1177
|
+
/* the head's floor: a tap, or an h2 line with its Thai-mark padding when the type is bigger than that (a large text-size
|
|
1178
|
+
setting on a narrow phone) — the strip and the head read the same number, so the title row is always inside the strip */
|
|
1179
|
+
:root { --sheet-head-h: max(var(--tap), calc(var(--fs-h2) * 1.25 + var(--sp-2))); }
|
|
1178
1180
|
|
|
1179
1181
|
/* ---- called sheets (Lh, 2026-09-20): a .step whose .base-content has buttons that each CALL a sheet ----
|
|
1180
1182
|
.sheet--away = not on the screen: slid all the way out (translate, like every state — the material never
|
|
@@ -1286,7 +1288,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1286
1288
|
- a label .row with its .check / .toggle may be the head when the sheet's
|
|
1287
1289
|
whole ask is that switch: peek shows it (a row is --tap tall plus its own
|
|
1288
1290
|
padding; the strip cuts in that padding) — card #67's "ไม่อยู่บ้าน" */
|
|
1289
|
-
.sheet > .card__head { min-height: var(--
|
|
1291
|
+
.sheet > .card__head { min-height: var(--sheet-head-h); }
|
|
1290
1292
|
.sheet__head {
|
|
1291
1293
|
display: grid;
|
|
1292
1294
|
grid-template-columns: var(--tap-sm) 1fr var(--tap-sm);
|
|
@@ -1559,7 +1561,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1559
1561
|
background: var(--field-bg, var(--action-bg));
|
|
1560
1562
|
color: var(--field-ink, var(--action-ink));
|
|
1561
1563
|
padding-inline: var(--field-pad-x, var(--sp-4));
|
|
1562
|
-
min-height: var(--field-min-h, var(--tap));
|
|
1564
|
+
min-height: var(--field-min-h, var(--tap-lg)); /* 56, the action field's height, for every field (Lh 2026-09-22: "ใหญ่ดี") */
|
|
1563
1565
|
min-width: 0;
|
|
1564
1566
|
}
|
|
1565
1567
|
|
|
@@ -1568,19 +1570,22 @@ button { font: inherit; cursor: pointer; }
|
|
|
1568
1570
|
border: 0;
|
|
1569
1571
|
background: transparent;
|
|
1570
1572
|
color: inherit;
|
|
1571
|
-
font: var(--
|
|
1573
|
+
font: var(--field-font, 400 var(--fs-lg) / 1.4 var(--font-ui)); /* 26: the words being typed read a size up from body (Lh 2026-09-22); the Composer already does */
|
|
1572
1574
|
width: 100%;
|
|
1573
1575
|
min-width: 0;
|
|
1574
|
-
outline
|
|
1576
|
+
outline: none; /* the browser's ring drew a second box inside the field (Lh 2026-09-22) — the field itself tells focus */
|
|
1577
|
+
caret-color: currentColor;
|
|
1575
1578
|
}
|
|
1576
1579
|
.field input::placeholder { color: inherit; opacity: 0.55; }
|
|
1580
|
+
/* the field the cursor is in: its own border goes full ink, in every palette (the coloured ones fade it to 25% at rest) */
|
|
1581
|
+
.field:has(:focus) { border-color: var(--field-active-border, currentColor); }
|
|
1577
1582
|
|
|
1578
1583
|
/* field with its action inside: date pick, send, go — the button sits flush right */
|
|
1579
1584
|
.field--action { padding-right: var(--sp-1); min-height: var(--field-action-min-h, var(--tap-lg)); }
|
|
1580
1585
|
.field--action > .icon-btn { margin-left: auto; }
|
|
1581
1586
|
|
|
1582
1587
|
/* colored palettes drop the visible outline elsewhere — fields keep one */
|
|
1583
|
-
[data-palette] .field { border-color: color-mix(in srgb, currentColor 25%, transparent); }
|
|
1588
|
+
[data-palette] .field:not(:has(:focus)) { border-color: color-mix(in srgb, currentColor 25%, transparent); }
|
|
1584
1589
|
|
|
1585
1590
|
/* ---- add-row: inline "+ add another" action ---- */
|
|
1586
1591
|
.add-row {
|
|
@@ -1646,7 +1651,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
1646
1651
|
background: var(--tile-badge-bg, var(--card-3-bg));
|
|
1647
1652
|
color: var(--tile-badge-ink, var(--card-3-ink));
|
|
1648
1653
|
}
|
|
1649
|
-
.tile-badge__label { font: var(--tile-badge-label-font, var(--type-
|
|
1654
|
+
.tile-badge__label { font: var(--tile-badge-label-font, var(--type-caption)); opacity: 0.7; }
|
|
1650
1655
|
.tile-badge__value { font: 800 var(--fs-lg)/1.1 var(--font-display); font-variant-numeric: tabular-nums; }
|
|
1651
1656
|
|
|
1652
1657
|
/* plain: no fill — a date box beside a title (Jun / 18) */
|
|
@@ -1707,6 +1712,9 @@ button { font: inherit; cursor: pointer; }
|
|
|
1707
1712
|
margin-block: -0.15em;
|
|
1708
1713
|
}
|
|
1709
1714
|
.topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
|
|
1715
|
+
/* at most TWO trailing controls (Lh 2026-09-22): the title must keep its line; a third and
|
|
1716
|
+
beyond is not drawn — more moves belong in a Menu behind one button, or on the base */
|
|
1717
|
+
.topbar__actions > :nth-child(n + 3) { display: none; }
|
|
1710
1718
|
|
|
1711
1719
|
/* a screen whose one big move sits at the top right (home's "ขอความช่วยเหลือ" — Lh 2026-09-09:
|
|
1712
1720
|
no bottom bar on the home screen) puts a primary .btn in the top bar's actions; it stands at
|
|
@@ -1810,7 +1818,9 @@ button { font: inherit; cursor: pointer; }
|
|
|
1810
1818
|
text-decoration: none;
|
|
1811
1819
|
justify-self: start; /* as a grid child it hugs its text, never stretches */
|
|
1812
1820
|
}
|
|
1813
|
-
.dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); } /* signal dot: chips, legends
|
|
1821
|
+
.dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); translate: 0 var(--dot-drop, 0.09em); } /* signal dot: chips, legends.
|
|
1822
|
+
The drop: a line box is centred on its ascender room (Thai marks), the letters beside the dot sit lower — the dot rides down to the
|
|
1823
|
+
letters' own middle, in em so it follows the text size (Lh 2026-09-22: "ตัวหนังสือกับไฟเสมอกัน") */
|
|
1814
1824
|
.dot--b { background: var(--dot-b); }
|
|
1815
1825
|
.chip--on { background: var(--chip-on-bg, var(--accent)); color: var(--chip-on-ink, var(--on-accent)); border-color: transparent; }
|
|
1816
1826
|
/* dark-slot inversion lives in card.css (.card--3 :is(...)) */
|
|
@@ -2000,7 +2010,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2000
2010
|
.ring--sm { --_size: var(--avatar-xl); --_w: var(--sp-3); }
|
|
2001
2011
|
.ring--sm .ring__value { font: 800 var(--fs-2xl)/1 var(--font-display); }
|
|
2002
2012
|
.ring--xs { --_size: var(--tap-sm); --_w: var(--dot-xs); } /* inline counter (step 3 of 5) */
|
|
2003
|
-
.ring--xs .ring__value { font: var(--type-
|
|
2013
|
+
.ring--xs .ring__value { font: var(--type-caption); }
|
|
2004
2014
|
|
|
2005
2015
|
/* ---- track: step progress in a pill — done · done · todo ----
|
|
2006
2016
|
<ol class="track"><li class="track__step is-done">icon</li> …</ol> */
|
|
@@ -2458,7 +2468,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2458
2468
|
.calendar { display: grid; gap: var(--calendar-gap, var(--sp-3)); }
|
|
2459
2469
|
.calendar__head { display: flex; align-items: center; justify-content: space-between; gap: var(--calendar-head-gap, var(--sp-2)); }
|
|
2460
2470
|
.calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: var(--calendar-grid-gap, var(--sp-1)); justify-items: center; }
|
|
2461
|
-
.calendar__wd { font: var(--calendar-wd-font, var(--type-
|
|
2471
|
+
.calendar__wd { font: var(--calendar-wd-font, var(--type-caption)); color: var(--calendar-wd-ink, var(--card-muted)); padding-block: var(--calendar-wd-pad-y, var(--sp-2)); }
|
|
2462
2472
|
.card--3 .calendar__wd { color: inherit; opacity: 0.7; }
|
|
2463
2473
|
.calendar__day {
|
|
2464
2474
|
width: 100%;
|
|
@@ -2524,10 +2534,23 @@ button { font: inherit; cursor: pointer; }
|
|
|
2524
2534
|
text-align: center;
|
|
2525
2535
|
font: var(--pin-cell-font, var(--type-stat));
|
|
2526
2536
|
font-variant-numeric: tabular-nums;
|
|
2527
|
-
caret-color: currentColor;
|
|
2528
2537
|
}
|
|
2529
2538
|
.pin__cell:not(:placeholder-shown) { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
2530
|
-
|
|
2539
|
+
/* the cell the cursor waits in (Lh 2026-09-22): NO outline ring — it sat outside the pill and got cropped by the sheet's
|
|
2540
|
+
edge. The sign is INSIDE the pill: a full-ink border and a blinking bar drawn where the digit will land (the native caret
|
|
2541
|
+
is hidden — its height and place are the browser's; the pill's bar is one rule). A filled cell selects its digit instead. */
|
|
2542
|
+
.pin__cell { caret-color: transparent; }
|
|
2543
|
+
.pin__cell:focus { outline: none; border-color: var(--pin-cell-active-border, currentColor); }
|
|
2544
|
+
.pin__cell:focus:placeholder-shown {
|
|
2545
|
+
background-image: linear-gradient(currentColor, currentColor);
|
|
2546
|
+
background-size: var(--pin-caret-w, 2px) var(--pin-caret-h, 1em);
|
|
2547
|
+
background-position: center;
|
|
2548
|
+
background-repeat: no-repeat;
|
|
2549
|
+
}
|
|
2550
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2551
|
+
.pin__cell:focus:placeholder-shown { animation: pin-caret 1.1s steps(1) infinite; }
|
|
2552
|
+
@keyframes pin-caret { 50% { background-size: 0 0; } }
|
|
2553
|
+
}
|
|
2531
2554
|
|
|
2532
2555
|
/* ---- composer: big text area with a bottom bar (icon · hint · send) ----
|
|
2533
2556
|
<div class="composer"><textarea class="composer__input" placeholder="What do you need?"></textarea>
|
|
@@ -2541,7 +2564,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2541
2564
|
padding: 0;
|
|
2542
2565
|
background: transparent;
|
|
2543
2566
|
color: inherit;
|
|
2544
|
-
font: 700 var(--fs-2xl)/1.25 var(--font-display);
|
|
2567
|
+
font: var(--composer-input-font, 700 var(--fs-2xl)/1.25 var(--font-display));
|
|
2545
2568
|
letter-spacing: var(--track-h2);
|
|
2546
2569
|
outline: none;
|
|
2547
2570
|
}
|
|
@@ -2696,8 +2719,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2696
2719
|
.kv { margin: 0; display: grid; }
|
|
2697
2720
|
.kv__row { display: flex; align-items: center; justify-content: space-between; gap: var(--kv-row-gap, var(--sp-3)); min-height: var(--kv-row-min-h, var(--tap)); }
|
|
2698
2721
|
.kv--lined > .kv__row + .kv__row { border-top: var(--border-w) solid var(--hairline); }
|
|
2699
|
-
.kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-
|
|
2700
|
-
.card--3 .kv__key { color: inherit; opacity: 0.7; }
|
|
2722
|
+
.kv__key { font: var(--kv-key-font, var(--type-body)); color: var(--kv-key-ink, var(--card-ink)); } /* full ink (Lh 2026-09-22): a key is read like the value; muted = inactive, not "secondary" */
|
|
2701
2723
|
.kv__val { margin: 0; font: var(--kv-val-font, var(--type-title)); display: inline-flex; align-items: center; gap: var(--kv-val-gap, var(--sp-2)); text-align: right; min-width: 0; }
|
|
2702
2724
|
|
|
2703
2725
|
/* ---- kv grid: value over label, cells split by a rule (Sex | Weight) ----
|
|
@@ -2791,8 +2813,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2791
2813
|
}
|
|
2792
2814
|
.timeline__mark {
|
|
2793
2815
|
justify-self: start;
|
|
2794
|
-
font: var(--timeline-mark-font, var(--type-
|
|
2795
|
-
letter-spacing: var(--track-overline);
|
|
2816
|
+
font: var(--timeline-mark-font, var(--type-caption));
|
|
2796
2817
|
text-transform: uppercase;
|
|
2797
2818
|
padding: var(--timeline-mark-pad-y, var(--sp-1)) var(--timeline-mark-pad-x, var(--sp-2));
|
|
2798
2819
|
border-radius: var(--timeline-mark-radius, var(--r-chip));
|
|
@@ -2832,7 +2853,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2832
2853
|
|
|
2833
2854
|
/* ---- cover: the image IS the card; text over a scrim ----
|
|
2834
2855
|
<article class="card card--cover"><div class="ph" role="img"></div>
|
|
2835
|
-
<div class="card__head"><
|
|
2856
|
+
<div class="card__head"><button class="icon-btn icon-btn--sm">♥</button></div>
|
|
2836
2857
|
<div><h2 class="t-h1">Art Institute</h2><p class="t-body">111 South Michigan Ave</p></div>
|
|
2837
2858
|
<div class="row">…meta…</div></article> */
|
|
2838
2859
|
.card--cover {
|
|
@@ -2857,7 +2878,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2857
2878
|
border-radius: 0;
|
|
2858
2879
|
min-height: 0;
|
|
2859
2880
|
}
|
|
2860
|
-
/* scrim: dark at both ends so
|
|
2881
|
+
/* scrim: dark at both ends so the meta stays readable */
|
|
2861
2882
|
.card--cover::before {
|
|
2862
2883
|
content: "";
|
|
2863
2884
|
position: absolute;
|
|
@@ -2879,7 +2900,7 @@ button { font: inherit; cursor: pointer; }
|
|
|
2879
2900
|
}
|
|
2880
2901
|
.quote__text { margin: 0; font: var(--quote-text-font, var(--type-h1)); letter-spacing: var(--track-h1); text-wrap: balance; }
|
|
2881
2902
|
.quote--sm .quote__text { font: var(--type-h2); letter-spacing: var(--track-h2); }
|
|
2882
|
-
.quote__by { font: var(--quote-by-font, var(--type-
|
|
2903
|
+
.quote__by { font: var(--quote-by-font, var(--type-caption)); color: var(--quote-by-ink, var(--card-muted)); }
|
|
2883
2904
|
.card--3 .quote__by { color: inherit; opacity: 0.7; }
|
|
2884
2905
|
|
|
2885
2906
|
/* ---- tile grid: big square tiles — icon top, label bottom ----
|
package/dist/cards/CardHead.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ export interface CardHeadProps extends ComponentProps<'div'> {
|
|
|
3
3
|
/** the heading (a string becomes an h2 at level `t-h2`) — wraps freely, as many lines as the words need */
|
|
4
4
|
heading?: ReactNode;
|
|
5
5
|
/** small caps above the heading ("Step 1 of 4", "Time bank") */
|
|
6
|
-
overline?: ReactNode;
|
|
7
6
|
/** the muted line under the heading (a date, a subtitle) */
|
|
8
7
|
caption?: ReactNode;
|
|
9
8
|
/** the contextual thing at the far right: a chip, an IconBtn (more), a Link, a small Ring, an Avatar, a Badge (a count in a stack head) */
|
|
@@ -15,7 +14,7 @@ export interface CardHeadProps extends ComponentProps<'div'> {
|
|
|
15
14
|
}
|
|
16
15
|
/**
|
|
17
16
|
* CardHead — the card's head: the heading on the left takes the room, the contextual info hugs the far right.
|
|
18
|
-
* Use `heading` (+ `
|
|
19
|
-
* (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the
|
|
17
|
+
* Use `heading` (+ `caption`) and `end` for the standard head, or pass children for a custom one
|
|
18
|
+
* (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the caption is a span on a row of its own (card.css). Never put buttons here — they go in the CardFoot.
|
|
20
19
|
*/
|
|
21
|
-
export declare function CardHead({ heading,
|
|
20
|
+
export declare function CardHead({ heading, caption, end, top, level, className, children, ...rest }: CardHeadProps): import("react").JSX.Element;
|
package/dist/cards/CardHead.js
CHANGED
|
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
/**
|
|
4
4
|
* CardHead — the card's head: the heading on the left takes the room, the contextual info hugs the far right.
|
|
5
|
-
* Use `heading` (+ `
|
|
6
|
-
* (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the
|
|
5
|
+
* Use `heading` (+ `caption`) and `end` for the standard head, or pass children for a custom one
|
|
6
|
+
* (the FIRST child takes the room). Flat markup: no wrapper element, no <p> — the caption is a span on a row of its own (card.css). Never put buttons here — they go in the CardFoot.
|
|
7
7
|
*/
|
|
8
|
-
export function CardHead({ heading,
|
|
8
|
+
export function CardHead({ heading, caption, end, top, level = 'h2', className, children, ...rest }) {
|
|
9
9
|
const H = level === 'h1' ? 'h1' : 'h2';
|
|
10
|
-
return (_jsxs("div", { className: cx('card__head', top && 'card__head--top', className), ...rest, children: [
|
|
10
|
+
return (_jsxs("div", { className: cx('card__head', top && 'card__head--top', className), ...rest, children: [heading != null && _jsx(H, { className: level === 'display' ? 't-display' : `t-${level}`, children: heading }), caption != null && _jsx("span", { className: "t-caption t-muted", children: caption }), children, end] }));
|
|
11
11
|
}
|
package/dist/cards/Stat.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface StatProps extends ComponentProps<'div'> {
|
|
|
5
5
|
/** centred: the one number of a sheet or a stage, under a centred Avatar */
|
|
6
6
|
centre?: boolean;
|
|
7
7
|
}
|
|
8
|
-
/** Stat — an inline stat block:
|
|
8
|
+
/** Stat — an inline stat block: a label, a big `Text level="stat"` number, a caption; StatRows put a Delta beside the label. */
|
|
9
9
|
export declare function Stat({ grow, centre, className, ...rest }: StatProps): import("react").JSX.Element;
|
|
10
10
|
export interface StatRowProps extends ComponentProps<'div'> {
|
|
11
11
|
}
|
package/dist/cards/Stat.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
-
/** Stat — an inline stat block:
|
|
3
|
+
/** Stat — an inline stat block: a label, a big `Text level="stat"` number, a caption; StatRows put a Delta beside the label. */
|
|
4
4
|
export function Stat({ grow, centre, className, ...rest }) {
|
|
5
5
|
return _jsx("div", { className: cx('stat', grow && 'grow', centre && 'stat--centre', className), ...rest });
|
|
6
6
|
}
|
package/dist/media/Quote.d.ts
CHANGED
|
@@ -7,5 +7,5 @@ export interface QuoteProps extends ComponentProps<'blockquote'> {
|
|
|
7
7
|
/** h2-size text instead of h1 */
|
|
8
8
|
sm?: boolean;
|
|
9
9
|
}
|
|
10
|
-
/** Quote — a giant opening mark, big text,
|
|
10
|
+
/** Quote — a giant opening mark, big text, a small attribution. */
|
|
11
11
|
export declare function Quote({ text, by, sm, className, ...rest }: QuoteProps): import("react").JSX.Element;
|
package/dist/media/Quote.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
|
-
/** Quote — a giant opening mark, big text,
|
|
3
|
+
/** Quote — a giant opening mark, big text, a small attribution. */
|
|
4
4
|
export function Quote({ text, by, sm, className, ...rest }) {
|
|
5
5
|
return (_jsxs("blockquote", { className: cx('quote', sm && 'quote--sm', className), ...rest, children: [_jsx("p", { className: "quote__text", children: text }), by != null && _jsx("footer", { className: "quote__by", children: by })] }));
|
|
6
6
|
}
|
|
@@ -2,7 +2,7 @@ import type { ComponentProps } from 'react';
|
|
|
2
2
|
export interface BaseContentProps extends ComponentProps<'section'> {
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
|
-
* BaseContent — words straight on the base, no card around them:
|
|
5
|
+
* BaseContent — words straight on the base, no card around them: a heading, a paragraph,
|
|
6
6
|
* maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
|
|
7
7
|
* Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
|
|
8
8
|
* In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
/**
|
|
4
|
-
* BaseContent — words straight on the base, no card around them:
|
|
4
|
+
* BaseContent — words straight on the base, no card around them: a heading, a paragraph,
|
|
5
5
|
* maybe a CtaPack. Content has three homes (Card · Sheet · the base); this is the third one, named.
|
|
6
6
|
* Was Section (and, inside a Step, StepCard) until 2026-09-16 — one block, one name.
|
|
7
7
|
* In a Step it is the reading block (first child; the step's MOVE goes to the ActionBar, never here). It can move two ways
|
|
@@ -4,8 +4,9 @@ export interface TopBarProps extends Omit<ComponentProps<'header'>, 'title'> {
|
|
|
4
4
|
title: ReactNode;
|
|
5
5
|
/** a leading control — usually `<IconBtn sm icon="arrow-left" label="Back" />` */
|
|
6
6
|
back?: ReactNode;
|
|
7
|
-
/** trailing controls, right-aligned
|
|
8
|
-
*
|
|
7
|
+
/** trailing controls, right-aligned, AT MOST TWO (Lh 2026-09-22 — a third is not drawn and warns once): chips,
|
|
8
|
+
* small icon buttons, a Dropdown — or the screen's one big move as a `Btn primary` (home's ขอความช่วยเหลือ,
|
|
9
|
+
* --tap-lg tall there; Lh 2026-09-09: no bottom bar on home). More moves go in a Menu behind one button. */
|
|
9
10
|
actions?: ReactNode;
|
|
10
11
|
}
|
|
11
12
|
/**
|
package/dist/scaffold/TopBar.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
2
3
|
import { cx } from '../cx.js';
|
|
3
4
|
/**
|
|
4
5
|
* TopBar — the screen's title bar: back · TITLE · actions. Same 60px floor as the AppBar.
|
|
5
6
|
* One per screen, first child of the Screen; a Step that follows it touches it.
|
|
6
7
|
*/
|
|
7
8
|
export function TopBar({ title, back, actions, className, children, ...rest }) {
|
|
8
|
-
|
|
9
|
+
const acts = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const n = acts.current?.childElementCount ?? 0;
|
|
12
|
+
if (n > MAX_ACTIONS && !warned) {
|
|
13
|
+
warned = true;
|
|
14
|
+
console.warn(`cardds TopBar: ${n} actions — at most ${MAX_ACTIONS} are drawn; put the rest in a Menu behind one button`);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return (_jsxs("header", { className: cx('topbar', className), ...rest, children: [back, _jsx("h1", { className: "t-h1", children: title }), children, actions != null && _jsx("div", { className: "topbar__actions", ref: acts, children: actions })] }));
|
|
9
18
|
}
|
|
19
|
+
const MAX_ACTIONS = 2;
|
|
20
|
+
let warned = false;
|
package/dist/sheets/Sheet.js
CHANGED
|
@@ -105,6 +105,26 @@ export function Sheet({ state, defaultState = 'half', onStateChange, states, han
|
|
|
105
105
|
ro.observe(el.parentElement);
|
|
106
106
|
return () => ro.disconnect();
|
|
107
107
|
}, [reveal]);
|
|
108
|
+
/* the peek strip holds the handle and the WHOLE title row. CSS gives the box the strip for a one-line head (--sheet-peek in
|
|
109
|
+
sheet.css); when the head is taller than that floor — a title wrapped on a narrow phone at a large text size — the sheet
|
|
110
|
+
measures its head and publishes the real strip on its box, so nothing of the title is under the cut (2026-09-22, with the
|
|
111
|
+
22-base type scale). Removed when the head fits the floor again, so the CSS number is the one in force. */
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const el = ref.current, box = el?.parentElement, head = el?.querySelector(':scope > .card__head, :scope > .sheet__head');
|
|
114
|
+
if (!el || !box || !head || typeof ResizeObserver === 'undefined')
|
|
115
|
+
return;
|
|
116
|
+
const publish = () => {
|
|
117
|
+
box.style.removeProperty('--sheet-peek'); // read the CSS strip, not our own
|
|
118
|
+
const cs = getComputedStyle(el);
|
|
119
|
+
const strip = parseFloat(cs.getPropertyValue('--sheet-peek')) || 0, floor = parseFloat(cs.getPropertyValue('--sheet-head-h')) || 0;
|
|
120
|
+
const over = head.getBoundingClientRect().height - floor;
|
|
121
|
+
if (over > 0.5)
|
|
122
|
+
box.style.setProperty('--sheet-peek', `${strip + over}px`);
|
|
123
|
+
};
|
|
124
|
+
const ro = new ResizeObserver(publish);
|
|
125
|
+
ro.observe(head);
|
|
126
|
+
return () => { ro.disconnect(); box.style.removeProperty('--sheet-peek'); };
|
|
127
|
+
}, []);
|
|
108
128
|
const focus = (e) => {
|
|
109
129
|
onFocus?.(e);
|
|
110
130
|
if (current !== 'peek' || e.target.closest('.sheet__handle'))
|
package/dist/type/Text.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export type TextLevel = 'display' | 'h1' | 'h2' | 'title' | 'body' | 'label' | 'caption' | '
|
|
1
|
+
export type TextLevel = 'display' | 'h1' | 'h2' | 'title' | 'body' | 'label' | 'caption' | 'stat';
|
|
2
2
|
export interface TextProps extends React.HTMLAttributes<HTMLElement> {
|
|
3
3
|
/** the element it renders (React 19 passes `ref` as a prop) */
|
|
4
4
|
ref?: React.Ref<HTMLElement>;
|
|
5
|
-
/** the type level: display (hero words) · h1 · h2 (a card's heading) · title · body · label · caption ·
|
|
5
|
+
/** the type level: display (hero words) · h1 · h2 (a card's heading) · title · body · label · caption · stat (a big number) */
|
|
6
6
|
level: TextLevel;
|
|
7
7
|
/** stat only: the one giant number on a sheet (`.t-stat--xl`) */
|
|
8
8
|
xl?: boolean;
|
package/dist/type/Text.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createElement } from 'react';
|
|
2
2
|
import { cx } from '../cx.js';
|
|
3
3
|
const DEFAULT_AS = {
|
|
4
|
-
display: 'h2', h1: 'h2', h2: 'h2', title: 'p', body: 'p', label: 'span', caption: 'span',
|
|
4
|
+
display: 'h2', h1: 'h2', h2: 'h2', title: 'p', body: 'p', label: 'span', caption: 'span', stat: 'p',
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
7
|
* Text — one of cardds's type levels (`.t-display` … `.t-stat`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usableapp/cardds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "card-first mobile design system, React-first: the components in src/ are thin wrappers over the CSS contract (css/*.css stays the only truth); gallery/ shows every story live (npm run dev), tests/ measures the geometry.",
|