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