@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/actions.css
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
cardds/actions.css — contextual commands around/inside cards:
|
|
3
|
+
topbar, chips, buttons, selects, bottom CTA pack
|
|
4
|
+
============================================================ */
|
|
5
|
+
|
|
6
|
+
/* ---- the bars across the top of a screen: one height, whatever they carry ----
|
|
7
|
+
.topbar (screen title) and .appbar (app context) are the same bar with
|
|
8
|
+
different contents, so the height is declared here once rather than left to
|
|
9
|
+
whichever control happens to be tallest. Measured before this rule existed:
|
|
10
|
+
a title-only topbar 46px, one with an .icon-btn--sm 60px, with a full-size
|
|
11
|
+
.icon-btn 64px, with a .btn 70px. --header-h is a floor, not a cap — a bar
|
|
12
|
+
given a deliberately taller control still grows instead of clipping it. */
|
|
13
|
+
.topbar, .appbar {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: var(--sp-3);
|
|
17
|
+
padding-block: var(--sp-2);
|
|
18
|
+
min-height: var(--header-h);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* ---- top bar: back + screen title ---- */
|
|
22
|
+
.topbar .t-h1 {
|
|
23
|
+
text-transform: uppercase;
|
|
24
|
+
flex: 1;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
text-overflow: ellipsis;
|
|
28
|
+
white-space: nowrap;
|
|
29
|
+
padding-block: 0.15em; /* Thai marks survive the clip — see card.css, the head rule */
|
|
30
|
+
margin-block: -0.15em;
|
|
31
|
+
}
|
|
32
|
+
.topbar__actions { margin-left: auto; display: flex; gap: var(--sp-2); }
|
|
33
|
+
|
|
34
|
+
/* a screen whose one big move sits at the top right (home's "ขอความช่วยเหลือ" — Lh 2026-09-09:
|
|
35
|
+
no bottom bar on the home screen) puts a primary .btn in the top bar's actions; it stands at
|
|
36
|
+
the hero height so it is the easiest thing on the screen to hit */
|
|
37
|
+
.topbar__actions > .btn--primary { min-height: var(--tap-lg); }
|
|
38
|
+
|
|
39
|
+
/* ---- appbar: app context/brand switcher (logo · two-line label · chevron) ---- */
|
|
40
|
+
.appbar {
|
|
41
|
+
position: fixed;
|
|
42
|
+
inset-inline: 0;
|
|
43
|
+
top: 0;
|
|
44
|
+
max-width: var(--screen-w);
|
|
45
|
+
margin-inline: auto;
|
|
46
|
+
padding-inline: var(--screen-pad); /* fixed + full-bleed: the screen's own inset, so the bar's contents line up with the title bar under it (Lh 2026-09-15) */
|
|
47
|
+
z-index: 40; /* topmost — card animations never cover it */
|
|
48
|
+
background: var(--surface); /* solid: sliding cards never show through */
|
|
49
|
+
}
|
|
50
|
+
/* fixed, so it covers the top of the screen it sits in — reserve its height
|
|
51
|
+
there, from the same token, the way the bottom bars reserve theirs. The
|
|
52
|
+
breathing under it is published: a .step cancels it to touch the bar. */
|
|
53
|
+
.screen:has(> .appbar) { --header-gap: var(--sp-2); padding-top: calc(var(--header-h) + var(--header-gap)); }
|
|
54
|
+
.appbar__brand { flex: 1; display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
|
|
55
|
+
.appbar__text { display: grid; min-width: 0; flex: 1; } /* pushes trailing button to the right edge */
|
|
56
|
+
.appbar__title, .appbar__sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.35; } /* clipped lines: room for Thai marks */
|
|
57
|
+
.appbar__title { font: var(--type-label); }
|
|
58
|
+
.appbar__sub { font: var(--type-caption); color: var(--on-surface-muted); }
|
|
59
|
+
/* the bar's trailing controls (one or two: a labelled switch chip, a wallet button — home,
|
|
60
|
+
Lh 2026-09-13) stand on the tap floor: a chip that is a switch grows to --tap here */
|
|
61
|
+
.appbar .chip--toggle { min-height: var(--tap); }
|
|
62
|
+
|
|
63
|
+
/* ---- icon button (round) ---- */
|
|
64
|
+
.icon-btn {
|
|
65
|
+
width: var(--tap); height: var(--tap);
|
|
66
|
+
flex: none;
|
|
67
|
+
border-radius: var(--r-chip);
|
|
68
|
+
border: var(--border-w) solid var(--outline); /* see .btn — same reason */
|
|
69
|
+
background: var(--action-bg);
|
|
70
|
+
color: var(--action-ink);
|
|
71
|
+
display: grid;
|
|
72
|
+
place-items: center;
|
|
73
|
+
font-size: var(--fs-lg);
|
|
74
|
+
line-height: 1;
|
|
75
|
+
}
|
|
76
|
+
.icon-btn--invert { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
77
|
+
.icon-btn--sm { width: var(--tap-sm); height: var(--tap-sm); font-size: var(--fs-title); }
|
|
78
|
+
/* fab: the square floating action, bottom-right of a stage or screen */
|
|
79
|
+
.fab { width: var(--fab); height: var(--fab); border-radius: var(--r-card); position: absolute; right: var(--sp-4); bottom: calc(var(--sp-5) + var(--bar-h) + var(--sp-3)); box-shadow: var(--shadow-float); z-index: 31; }
|
|
80
|
+
|
|
81
|
+
/* ---- buttons ---- */
|
|
82
|
+
.btn {
|
|
83
|
+
/* --outline, not --action-border: same reason as .chip — on a palette where
|
|
84
|
+
--card-1-bg equals --action-bg a transparent border makes a plain button
|
|
85
|
+
invisible on a white card. --primary and --quiet set their own edge. */
|
|
86
|
+
border: var(--border-w) solid var(--outline);
|
|
87
|
+
background: var(--action-bg);
|
|
88
|
+
color: var(--action-ink);
|
|
89
|
+
border-radius: var(--r-chip);
|
|
90
|
+
padding: var(--sp-4) var(--sp-6);
|
|
91
|
+
font: var(--type-label);
|
|
92
|
+
font-size: var(--fs-md);
|
|
93
|
+
display: inline-flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
gap: var(--sp-2);
|
|
97
|
+
min-height: var(--btn-h);
|
|
98
|
+
text-decoration: none; /* .btn works as <a> too */
|
|
99
|
+
}
|
|
100
|
+
.btn--primary { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
101
|
+
/* quiet keeps a faint outline so it still reads as a button on colored palettes;
|
|
102
|
+
it is ink on whatever it sits on — inherit, or on the dark slot (card--3) it
|
|
103
|
+
would take the light-card action ink and vanish */
|
|
104
|
+
.btn--quiet { background: transparent; color: inherit; border-color: color-mix(in srgb, currentColor 35%, transparent); }
|
|
105
|
+
.btn--block { width: 100%; }
|
|
106
|
+
/* xl: the one giant call to action in a tinted card ("Taxi!") */
|
|
107
|
+
.btn--xl { min-height: var(--btn-xl-h); font: 800 var(--fs-3xl)/1 var(--font-display); letter-spacing: var(--track-h1); border-radius: var(--r-card); padding-inline: var(--sp-8); }
|
|
108
|
+
/* disabled keeps the slot — a control that vanishes moves everything else,
|
|
109
|
+
and a pager whose "previous" disappeared on page one would shift the bar
|
|
110
|
+
under the thumb on every page */
|
|
111
|
+
:is(.btn, .icon-btn):disabled { opacity: 0.4; cursor: default; }
|
|
112
|
+
/* a row of buttons sharing the width (twin CTAs inside a card) */
|
|
113
|
+
.btn-row { display: flex; gap: var(--sp-2); }
|
|
114
|
+
.btn-row > .btn { flex: 1; min-width: 0; }
|
|
115
|
+
|
|
116
|
+
/* ---- chip: status/attribute tag, optional signal dot ---- */
|
|
117
|
+
.chip {
|
|
118
|
+
display: inline-flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: var(--sp-2);
|
|
121
|
+
border-radius: var(--r-chip);
|
|
122
|
+
/* --outline, not --action-border: a chip is a control, and on a palette
|
|
123
|
+
where --card-1-bg == --action-bg (field, roast, hydro) a transparent
|
|
124
|
+
border makes it invisible on a white card. Same edge fields, checks,
|
|
125
|
+
toggles, tiles and the dropdown already use. */
|
|
126
|
+
border: var(--border-w) solid var(--outline);
|
|
127
|
+
background: var(--action-bg);
|
|
128
|
+
color: var(--action-ink);
|
|
129
|
+
padding: var(--sp-2) var(--sp-3);
|
|
130
|
+
font: var(--type-label);
|
|
131
|
+
font-size: var(--fs-sm);
|
|
132
|
+
min-height: var(--chip-h);
|
|
133
|
+
text-decoration: none;
|
|
134
|
+
justify-self: start; /* as a grid child it hugs its text, never stretches */
|
|
135
|
+
}
|
|
136
|
+
.dot { width: var(--dot); height: var(--dot); flex: none; border-radius: 50%; background: var(--dot-a); } /* signal dot: chips, legends */
|
|
137
|
+
.dot--b { background: var(--dot-b); }
|
|
138
|
+
.chip--on { background: var(--accent); color: var(--on-accent); border-color: transparent; }
|
|
139
|
+
/* dark-slot inversion lives in card.css (.card--3 :is(...)) */
|
|
140
|
+
|
|
141
|
+
.chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
|
|
142
|
+
|
|
143
|
+
/* ---- dropdown: the standard for every select — a pill trigger + .menu popover ----
|
|
144
|
+
<div class="dropdown"><button class="dropdown__btn" popovertarget="f1"><span class="dropdown__label">this week</span><svg class="icon icon--xs"><use href="icons.svg#chevron-down"/></svg></button>
|
|
145
|
+
<div class="menu" id="f1" popover><button class="menu__item" aria-current="true" data-value="week">this week</button>…</div></div>
|
|
146
|
+
cardds.js copies the picked item into the label and fires "change" on .dropdown.
|
|
147
|
+
.menu (layover.css) draws the panel; anchor positioning pins it under the trigger. */
|
|
148
|
+
.dropdown { position: relative; min-width: 0; display: inline-grid; }
|
|
149
|
+
.dropdown__btn {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
justify-content: space-between;
|
|
153
|
+
gap: var(--sp-2);
|
|
154
|
+
width: 100%;
|
|
155
|
+
min-width: 0;
|
|
156
|
+
min-height: var(--tap-sm);
|
|
157
|
+
padding: var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-4);
|
|
158
|
+
border: var(--border-w) solid var(--outline); /* --outline: keeps a faint edge on colored palettes, like fields */
|
|
159
|
+
border-radius: var(--r-chip);
|
|
160
|
+
background: var(--action-bg);
|
|
161
|
+
color: var(--action-ink);
|
|
162
|
+
font: var(--type-label);
|
|
163
|
+
font-size: var(--fs-body);
|
|
164
|
+
text-align: left;
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
}
|
|
167
|
+
.dropdown__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
168
|
+
.dropdown__btn > .icon { transition: rotate 0.2s ease; }
|
|
169
|
+
.dropdown:has(.menu:popover-open) .dropdown__btn > .icon { rotate: 180deg; }
|
|
170
|
+
.dropdown--end .menu[popover] { position-area: block-end span-inline-start; } /* right-aligned trigger: panel grows leftwards */
|
|
171
|
+
.filter-row { display: flex; gap: var(--sp-2); }
|
|
172
|
+
.filter-row > .dropdown { flex: 1; min-width: 0; }
|
|
173
|
+
|
|
174
|
+
/* ---- segment (day picker / tab strip) — items share width, shrink on small screens ---- */
|
|
175
|
+
.segment { display: flex; gap: var(--sp-1); }
|
|
176
|
+
.segment__item {
|
|
177
|
+
border: 0;
|
|
178
|
+
background: transparent;
|
|
179
|
+
color: inherit;
|
|
180
|
+
opacity: 0.6;
|
|
181
|
+
border-radius: var(--r-chip);
|
|
182
|
+
padding: var(--sp-2) var(--sp-1);
|
|
183
|
+
font: var(--type-label);
|
|
184
|
+
font-size: var(--fs-sm);
|
|
185
|
+
min-height: calc(var(--tap-sm) - var(--sp-0));
|
|
186
|
+
flex: 1;
|
|
187
|
+
min-width: 0;
|
|
188
|
+
}
|
|
189
|
+
.segment__item[aria-selected="true"] {
|
|
190
|
+
background: var(--accent);
|
|
191
|
+
color: var(--on-accent);
|
|
192
|
+
opacity: 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ---- bottom CTA pack: fixed action zone under the content ----
|
|
196
|
+
--bar-reserve is the room the bar needs — its own height plus a breathing
|
|
197
|
+
gap above it — published here so anything else that must stop clear of it
|
|
198
|
+
reads this instead of recomputing it. .screen's own reserve below reads
|
|
199
|
+
it; so does .step (step.css), the box a sheet slides in — one number, so
|
|
200
|
+
the two can't drift apart.
|
|
201
|
+
--kb is the on-screen keyboard's height, published on :root by cardds.js
|
|
202
|
+
from the visual viewport (0 without one, and on a platform that shrinks
|
|
203
|
+
the layout viewport itself): the bar rides above the keyboard on iOS the
|
|
204
|
+
way it already does on Android, and .step grows its room by the same. */
|
|
205
|
+
:root { --bar-reserve: calc(var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
206
|
+
.action-bar {
|
|
207
|
+
position: fixed;
|
|
208
|
+
inset-inline: 0;
|
|
209
|
+
bottom: var(--kb, 0);
|
|
210
|
+
z-index: 30; /* above .sheet-stack sheets, which paint at --level 1-5 */
|
|
211
|
+
max-width: var(--screen-w);
|
|
212
|
+
margin-inline: auto;
|
|
213
|
+
padding: var(--sp-3) var(--sp-4) var(--sp-5);
|
|
214
|
+
display: flex;
|
|
215
|
+
gap: var(--sp-2);
|
|
216
|
+
}
|
|
217
|
+
.action-bar .btn { flex: 1; box-shadow: var(--shadow-float); }
|
|
218
|
+
.action-bar .icon-btn { width: var(--btn-h); height: var(--btn-h); box-shadow: var(--shadow-float); }
|
|
219
|
+
|
|
220
|
+
/* ---- pager: previous / next at the two edges of the bar ----
|
|
221
|
+
For a sequence you can walk BOTH ways — a three-card feature intro, a step
|
|
222
|
+
flow. Use it instead of the default pack whenever going back is as ordinary
|
|
223
|
+
as going on: a wide CTA beside a small round icon reads as one action plus
|
|
224
|
+
an afterthought, which is wrong when the two are peers.
|
|
225
|
+
|
|
226
|
+
Both buttons are the same shape and the same height, so neither wins by
|
|
227
|
+
being physically bigger; the fill alone leans forward. The bar carries the
|
|
228
|
+
buttons only: where you are is said by the .pager__at dots UNDER the card
|
|
229
|
+
that changes (a .screen__centre group, card.css), never in the bar — the
|
|
230
|
+
dots move with what moves (Lh, 2026-09-12). Between the buttons goes a
|
|
231
|
+
"skip" link or nothing. Keep the last step of a flow on the plain
|
|
232
|
+
.action-bar: a step that COMMITS deserves one wide button, not a pair.
|
|
233
|
+
|
|
234
|
+
<div class="action-bar action-bar--pager">
|
|
235
|
+
<button class="btn" disabled><svg class="icon icon--xs"><use href="icons.svg#arrow-left"/></svg>Back</button>
|
|
236
|
+
<button class="btn btn--primary">Next<svg class="icon icon--xs"><use href="icons.svg#arrow-right"/></svg></button>
|
|
237
|
+
</div> */
|
|
238
|
+
.action-bar--pager { align-items: center; }
|
|
239
|
+
/* equal share of the bar, and tighter than a lone CTA so two labels plus the
|
|
240
|
+
indicator still fit a 320px screen */
|
|
241
|
+
.action-bar--pager > .btn {
|
|
242
|
+
flex: 1 1 0;
|
|
243
|
+
min-width: 0;
|
|
244
|
+
padding-inline: var(--sp-3);
|
|
245
|
+
gap: var(--sp-1);
|
|
246
|
+
white-space: nowrap;
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
}
|
|
249
|
+
/* the page dots: under the card in a .screen__centre group; tight gaps */
|
|
250
|
+
.pager__at { flex: 0 1 auto; min-width: 0; overflow: hidden; display: flex; align-items: center; gap: var(--sp-1); }
|
|
251
|
+
.pager__at > * {
|
|
252
|
+
width: var(--dot);
|
|
253
|
+
height: var(--dot);
|
|
254
|
+
border-radius: 50%;
|
|
255
|
+
background: currentColor;
|
|
256
|
+
opacity: 0.3;
|
|
257
|
+
}
|
|
258
|
+
.pager__at > .is-at { background: var(--accent); opacity: 1; }
|
|
259
|
+
/* ---- tiers: a control row above the buttons ----
|
|
260
|
+
A screen whose one move comes with a control — the introduction's text-size
|
|
261
|
+
slider above its pager. Each tier is a row; the bar stacks them, and any
|
|
262
|
+
parent hosting the bar publishes the room for both, the way the plain
|
|
263
|
+
reserve is published above, so a .screen or a .card--centre stops clear.
|
|
264
|
+
<div class="action-bar action-bar--tiers">
|
|
265
|
+
<div class="action-bar__tier"><label class="slider">…</label></div>
|
|
266
|
+
<div class="action-bar__tier action-bar__tier--pager">…the round pager…</div>
|
|
267
|
+
</div> */
|
|
268
|
+
.action-bar--tiers { flex-direction: column; align-items: stretch; gap: var(--sp-3); }
|
|
269
|
+
/* the tiers bar is as transparent as every other bar: the content scrolls under
|
|
270
|
+
it and shows through — the rail and the round buttons carry their own float
|
|
271
|
+
shadow and read on their own (Lh, 2026-09-12; replaces the 09-11 fade) */
|
|
272
|
+
.action-bar__tier { display: flex; align-items: center; gap: var(--sp-2); min-height: var(--tap); }
|
|
273
|
+
.action-bar__tier > .btn { flex: 1; }
|
|
274
|
+
:has(> .action-bar--tiers) { --bar-reserve: calc(var(--tap) + var(--sp-3) + var(--btn-h) + var(--sp-3) + var(--sp-5) + var(--sp-6)); }
|
|
275
|
+
|
|
276
|
+
/* ---- round pager: previous / next as two round buttons at the two edges ----
|
|
277
|
+
The pager with nothing to say between its buttons — no dots, no label — as
|
|
278
|
+
two equal round icon buttons, one at each edge, in thumb reach from either
|
|
279
|
+
side. Rule 3 still holds: the step that commits goes back to one wide button.
|
|
280
|
+
<div class="action-bar action-bar--pager">
|
|
281
|
+
<button class="icon-btn" aria-label="ย้อนกลับ">…</button>
|
|
282
|
+
<button class="icon-btn icon-btn--invert" aria-label="ถัดไป">…</button>
|
|
283
|
+
</div> — or the same two in an .action-bar__tier--pager under a control */
|
|
284
|
+
:is(.action-bar--pager, .action-bar__tier--pager) > .icon-btn:first-child { margin-right: auto; }
|
|
285
|
+
|
|
286
|
+
/* .screen's default bottom padding is a constant; derive the reserve from the
|
|
287
|
+
bar instead, so a taller button (a longer label, a script with more leading)
|
|
288
|
+
can never end up underneath it. Any parent that hosts the bar counts —
|
|
289
|
+
<body>, or an app shell (#app > .screen + .action-bar) — since the bar is
|
|
290
|
+
fixed to the viewport wherever it sits. (.screen--fill reserves nothing:
|
|
291
|
+
its .step carries the bar's room itself, step.css.) */
|
|
292
|
+
:has(> .action-bar) .screen:not(.screen--fill) { padding-bottom: var(--bar-reserve); }
|
package/css/base.css
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
cardds/base.css — reset, surface, typography levels, section
|
|
3
|
+
============================================================ */
|
|
4
|
+
|
|
5
|
+
*, *::before, *::after { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
|
6
|
+
|
|
7
|
+
/* the scale: everything is rem, so this one number resizes the whole system.
|
|
8
|
+
100% = the user's text size (16px by default); narrow phones step to 15/16 so
|
|
9
|
+
the 25rem column and its 48px taps keep their proportions on 320–360 screens.
|
|
10
|
+
--text-scale is an app's OWN text-size setting — a unitless number the app
|
|
11
|
+
writes on <html> (1 = as the browser has it; 1.1875 = larger). It multiplies
|
|
12
|
+
in here so it composes with the narrow-phone step instead of overwriting the
|
|
13
|
+
root, and so the app never writes a length from script. */
|
|
14
|
+
html { font-size: calc(100% * var(--text-scale, 1)); scrollbar-width: none; }
|
|
15
|
+
@media (max-width: 22.5em) { html { font-size: calc(93.75% * var(--text-scale, 1)); } }
|
|
16
|
+
html::-webkit-scrollbar { display: none; } /* Chromium/Safari */
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
margin: 0;
|
|
20
|
+
background: var(--surface);
|
|
21
|
+
color: var(--on-surface);
|
|
22
|
+
font: var(--type-body);
|
|
23
|
+
-webkit-font-smoothing: antialiased;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* components here set display (grid/flex), which outranks the UA's
|
|
27
|
+
[hidden] { display: none } — so the attribute silently does nothing on a
|
|
28
|
+
.btn, .icon-btn, .chip or .row. Restore it once, for all of them. */
|
|
29
|
+
[hidden] { display: none !important; }
|
|
30
|
+
|
|
31
|
+
img, svg { display: block; max-width: 100%; }
|
|
32
|
+
button { font: inherit; cursor: pointer; }
|
|
33
|
+
|
|
34
|
+
/* icon from icons.svg sprite: <svg class="icon"><use href="icons.svg#bell"/></svg> */
|
|
35
|
+
.icon { width: var(--icon); height: var(--icon); flex: none; }
|
|
36
|
+
.icon--sm { width: var(--icon-sm); height: var(--icon-sm); }
|
|
37
|
+
.icon--xs { width: var(--icon-xs); height: var(--icon-xs); } /* inside captions, chips, links */
|
|
38
|
+
.icon--lg { width: var(--icon-lg); height: var(--icon-lg); } /* empty states */
|
|
39
|
+
|
|
40
|
+
/* mobile screen column — the standard horizontal inset, and nothing vertical.
|
|
41
|
+
Top and bottom are zero on purpose: vertical room is only ever a RESERVE for
|
|
42
|
+
something fixed parked over the screen, so each bar adds its own and a page
|
|
43
|
+
without bars needs no override. .appbar reserves the top (actions.css),
|
|
44
|
+
.action-bar (actions.css) and .float-bar (layover.css) the bottom. */
|
|
45
|
+
.screen {
|
|
46
|
+
--screen-gap: var(--sp-5); /* published: a .step cancels it to touch the bar it follows (step.css) */
|
|
47
|
+
max-width: var(--screen-w);
|
|
48
|
+
margin-inline: auto;
|
|
49
|
+
padding-inline: var(--screen-pad);
|
|
50
|
+
display: grid;
|
|
51
|
+
gap: var(--screen-gap);
|
|
52
|
+
}
|
|
53
|
+
.screen > * { min-width: 0; } /* content never widens the column past the viewport */
|
|
54
|
+
/* a screen that carries its own palette (data-palette on the element, not on <html>)
|
|
55
|
+
paints its own surface, so a design tool can show one screen per palette */
|
|
56
|
+
.screen[data-palette] { background: var(--surface); color: var(--on-surface); }
|
|
57
|
+
|
|
58
|
+
/* app screen: exactly one phone height — a height, not a minimum, so that a
|
|
59
|
+
.step (step.css) has a bound to fill and its card and sheet scroll INSIDE
|
|
60
|
+
it instead of growing the page; and clip, not hidden, because a clipped
|
|
61
|
+
box is not a scroll container: nothing — not even the browser revealing a
|
|
62
|
+
focused field — can shift the screen up. A trailing .sheet-stack (stack.css) stretches
|
|
63
|
+
and bleeds off the bottom edge. */
|
|
64
|
+
.screen--fill {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
height: 100dvh;
|
|
68
|
+
overflow: clip;
|
|
69
|
+
position: relative; /* the box a .card--centre or a .screen__centre floats in (card.css) */
|
|
70
|
+
}
|
|
71
|
+
/* min-height: 0 — a flex item's floor is its content, and the stack's content
|
|
72
|
+
is its own full-height sheets, so without it the stack grew to the whole
|
|
73
|
+
screen and its open sheet ran past the bottom edge by the bars' height,
|
|
74
|
+
unreachable by scrolling (timebank home, 2026-09-13). The stack IS the room
|
|
75
|
+
left under the bars; every sheet fills exactly that. */
|
|
76
|
+
.screen--fill > .sheet-stack { flex: 1; min-height: 0; }
|
|
77
|
+
|
|
78
|
+
/* ---- typography levels ---- */
|
|
79
|
+
/* --fs-self: each level publishes its own size, so a consumer that scales a
|
|
80
|
+
heading (card.css --squeeze) can multiply it without knowing the level */
|
|
81
|
+
.t-display { font: var(--type-display); letter-spacing: var(--track-display); margin: 0; text-wrap: balance; --fs-self: var(--fs-display); }
|
|
82
|
+
.t-h1 { font: var(--type-h1); letter-spacing: var(--track-h1); margin: 0; --fs-self: var(--fs-h1); }
|
|
83
|
+
.t-h2 { font: var(--type-h2); letter-spacing: var(--track-h2); margin: 0; --fs-self: var(--fs-h2); }
|
|
84
|
+
.t-title { font: var(--type-title); margin: 0; --fs-self: var(--fs-title); }
|
|
85
|
+
.t-body { font: var(--type-body); margin: 0; }
|
|
86
|
+
.t-label { font: var(--type-label); }
|
|
87
|
+
.t-caption { font: var(--type-caption); }
|
|
88
|
+
.t-overline { font: var(--type-overline); letter-spacing: var(--track-overline); text-transform: uppercase; }
|
|
89
|
+
.t-stat { font: var(--type-stat); letter-spacing: var(--track-display); margin: 0; font-variant-numeric: tabular-nums; }
|
|
90
|
+
.t-stat--mono { font-family: var(--font-mono); letter-spacing: 0; } /* clocks, ETAs, codes */
|
|
91
|
+
.t-stat--xl { font-size: var(--fs-hero); } /* the one number on a sheet */
|
|
92
|
+
|
|
93
|
+
.t-muted { color: var(--on-surface-muted); }
|
|
94
|
+
.card .t-muted { color: var(--card-muted); } /* muted follows the card's ink, not the surface's */
|
|
95
|
+
.card--3 .t-muted { color: inherit; opacity: 0.7; } /* card-muted is tuned for light cards; dim instead on the dark slot */
|
|
96
|
+
.t-caps { text-transform: uppercase; }
|
|
97
|
+
.grow { flex: 1; min-width: 0; } /* the child that takes the remaining row space */
|
|
98
|
+
|
|
99
|
+
/* ---- section: frameless on-surface content + CTA (never a card) ---- */
|
|
100
|
+
.section {
|
|
101
|
+
display: grid;
|
|
102
|
+
gap: var(--sp-3);
|
|
103
|
+
padding-block: var(--sp-4);
|
|
104
|
+
}
|
|
105
|
+
.section > .t-body { color: var(--on-surface-muted); max-width: 34ch; }
|
|
106
|
+
.section .cta-pack {
|
|
107
|
+
display: flex;
|
|
108
|
+
gap: var(--sp-2);
|
|
109
|
+
margin-top: var(--sp-2);
|
|
110
|
+
flex-wrap: wrap;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ---- empty state: on-surface, centered, optional CTA below ---- */
|
|
114
|
+
.empty-state {
|
|
115
|
+
display: grid;
|
|
116
|
+
justify-items: center;
|
|
117
|
+
text-align: center;
|
|
118
|
+
gap: var(--sp-2);
|
|
119
|
+
padding: var(--sp-12) var(--sp-6);
|
|
120
|
+
color: var(--on-surface-muted);
|
|
121
|
+
}
|
|
122
|
+
.empty-state > :is(h1, h2, h3) { color: var(--on-surface); } /* the heading in full ink, the words muted */
|
|
123
|
+
.empty-state__action { margin-top: var(--sp-2); }
|
|
124
|
+
/* inside a card (a section's "nothing yet"): the card's ink, the way .t-muted and .row__meta follow it */
|
|
125
|
+
.card .empty-state { color: var(--card-muted); padding-block: var(--sp-6); }
|
|
126
|
+
.card .empty-state > :is(h1, h2, h3) { color: var(--card-ink); }
|
|
127
|
+
.card--3 .empty-state { color: inherit; }
|
|
128
|
+
.card--3 .empty-state > :not(:is(h1, h2, h3)) { opacity: 0.7; }
|