@echodial/deck 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/LICENSE +21 -0
- package/README.md +1327 -0
- package/bin/deck.mjs +219 -0
- package/build.mjs +338 -0
- package/dist/api-buckets.json +10486 -0
- package/dist/api.json +57896 -0
- package/dist/brand/deck-apple-touch-icon.png +0 -0
- package/dist/brand/deck-logo-dark.svg +1 -0
- package/dist/brand/deck-logo-light.svg +1 -0
- package/dist/brand/deck-logo-stacked.svg +1 -0
- package/dist/brand/deck-logo.svg +1 -0
- package/dist/brand/deck-mark.svg +7 -0
- package/dist/brand/deck-og.png +0 -0
- package/dist/collisions.json +223 -0
- package/dist/deck-adapters.js +482 -0
- package/dist/deck-adapters.min.js +23 -0
- package/dist/deck-extras.js +823 -0
- package/dist/deck-extras.min.js +5 -0
- package/dist/deck-icons.svg +184 -0
- package/dist/deck.bundle.js +2638 -0
- package/dist/deck.bundle.min.js +86 -0
- package/dist/deck.css +7904 -0
- package/dist/deck.esm.js +2644 -0
- package/dist/deck.js +1331 -0
- package/dist/deck.min.css +32 -0
- package/dist/deck.min.js +60 -0
- package/dist/layers/anchor.css +192 -0
- package/dist/layers/buttons.css +210 -0
- package/dist/layers/charts.css +337 -0
- package/dist/layers/combobox.css +248 -0
- package/dist/layers/components.css +868 -0
- package/dist/layers/container.css +199 -0
- package/dist/layers/datagrid.css +335 -0
- package/dist/layers/datepicker.css +305 -0
- package/dist/layers/forms.css +401 -0
- package/dist/layers/gradients.css +302 -0
- package/dist/layers/inputs.css +522 -0
- package/dist/layers/layers.css +87 -0
- package/dist/layers/layout.css +235 -0
- package/dist/layers/logical.css +226 -0
- package/dist/layers/media.css +400 -0
- package/dist/layers/mobile.css +245 -0
- package/dist/layers/motion.css +379 -0
- package/dist/layers/nav.css +771 -0
- package/dist/layers/perf.css +234 -0
- package/dist/layers/print.css +198 -0
- package/dist/layers/reset.css +103 -0
- package/dist/layers/space3d.css +280 -0
- package/dist/layers/toasts.css +201 -0
- package/dist/layers/tokens.css +229 -0
- package/dist/layers/type.css +114 -0
- package/dist/layers/utilities.css +241 -0
- package/dist/sizes.json +52 -0
- package/dist/usage.json +6340 -0
- package/package.json +94 -0
- package/src/00-layers.css +87 -0
- package/src/01-tokens.css +229 -0
- package/src/02-reset.css +103 -0
- package/src/03-type.css +114 -0
- package/src/04-layout.css +235 -0
- package/src/05-buttons.css +210 -0
- package/src/06-forms.css +401 -0
- package/src/07-components.css +868 -0
- package/src/08-mobile.css +245 -0
- package/src/09-utilities.css +241 -0
- package/src/10-datepicker.css +305 -0
- package/src/11-combobox.css +248 -0
- package/src/12-datagrid.css +335 -0
- package/src/13-toasts.css +201 -0
- package/src/14-charts.css +337 -0
- package/src/16-motion.css +379 -0
- package/src/18-container.css +199 -0
- package/src/19-logical.css +226 -0
- package/src/20-gradients.css +302 -0
- package/src/21-space3d.css +280 -0
- package/src/22-nav.css +771 -0
- package/src/23-inputs.css +522 -0
- package/src/24-media.css +400 -0
- package/src/25-anchor.css +192 -0
- package/src/26-perf.css +234 -0
- package/src/99-print.css +198 -0
- package/src/brand/deck-apple-touch-icon.png +0 -0
- package/src/brand/deck-logo-dark.svg +1 -0
- package/src/brand/deck-logo-light.svg +1 -0
- package/src/brand/deck-logo-stacked.svg +1 -0
- package/src/brand/deck-logo.svg +1 -0
- package/src/brand/deck-mark.svg +7 -0
- package/src/brand/deck-og.png +0 -0
- package/src/brand/sources.json +7 -0
- package/src/deck-icons.svg +184 -0
- package/src/js/deck-adapters.js +482 -0
- package/src/js/deck-extras.js +823 -0
- package/src/js/deck.js +1331 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Deck — 04 Layout
|
|
3
|
+
Composable primitives. You lay out with about eight classes, not forty.
|
|
4
|
+
========================================================================== */
|
|
5
|
+
|
|
6
|
+
@layer deck.layout {
|
|
7
|
+
|
|
8
|
+
/* Centres content and caps it at --container, with the responsive page
|
|
9
|
+
gutter already applied. Nest one inside another and the inner one adds
|
|
10
|
+
a second gutter, so use .container-full instead. */
|
|
11
|
+
.container {
|
|
12
|
+
inline-size: 100%;
|
|
13
|
+
max-inline-size: var(--container, 76rem);
|
|
14
|
+
margin-inline: auto;
|
|
15
|
+
padding-inline: var(--space-gutter);
|
|
16
|
+
}
|
|
17
|
+
/* A single column of prose: about 70 characters at the body size,
|
|
18
|
+
which is inside the maximum line length WCAG recommends. */
|
|
19
|
+
.container-sm { --container: 40rem; }
|
|
20
|
+
/* An article with a rail, or a form. */
|
|
21
|
+
.container-md { --container: 56rem; }
|
|
22
|
+
/* The same 76rem as the bare .container, named. Use it when a page
|
|
23
|
+
should say which width it means rather than rely on the default. */
|
|
24
|
+
.container-lg { --container: 76rem; }
|
|
25
|
+
/* A wide dashboard or a data grid. */
|
|
26
|
+
.container-xl { --container: 90rem; }
|
|
27
|
+
/* Keeps the page gutter, drops the max width. This is what to nest
|
|
28
|
+
inside another container — a second .container would apply the
|
|
29
|
+
gutter twice. */
|
|
30
|
+
.container-full { --container: none; }
|
|
31
|
+
|
|
32
|
+
/* --- The spacing scale ----------------------------------------------------
|
|
33
|
+
Every spacing utility in Deck uses the same seven steps:
|
|
34
|
+
|
|
35
|
+
0 1 2 3 4 6 8
|
|
36
|
+
0 .25rem .5rem .75rem 1rem 1.5rem 2rem
|
|
37
|
+
|
|
38
|
+
.stack, .cluster, .gap-*, the padding utilities and the margin utilities
|
|
39
|
+
all implement exactly these, so a step that exists in one family exists in
|
|
40
|
+
all of them. There is deliberately no 5: at 1.25rem it is indistinguishable
|
|
41
|
+
from 4 and 6 in a real layout, and a half step between two neighbours is
|
|
42
|
+
the step people reach for when they are not sure, which is how .stack-5
|
|
43
|
+
came to be used in the demo when no such class existed.
|
|
44
|
+
|
|
45
|
+
The --space-* tokens keep their full range (0 through 24). Tokens are the
|
|
46
|
+
palette a component composes from; the utilities are the smaller set you
|
|
47
|
+
author markup with. Those are different jobs, so they are different sizes.
|
|
48
|
+
---------------------------------------------------------------------- */
|
|
49
|
+
|
|
50
|
+
/* Vertical rhythm without margin collapse surprises */
|
|
51
|
+
.stack {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
gap: var(--gap, var(--space-4));
|
|
55
|
+
}
|
|
56
|
+
/* No gap. For two elements that must touch. */
|
|
57
|
+
.stack-0 { --gap: 0; }
|
|
58
|
+
/* A hairline. A label and the control it names, when the two read as
|
|
59
|
+
one thing. */
|
|
60
|
+
.stack-1 { --gap: var(--space-1); }
|
|
61
|
+
/* Tight vertical rhythm, for a label and its control or a title and its
|
|
62
|
+
subtitle. */
|
|
63
|
+
.stack-2 { --gap: var(--space-2); }
|
|
64
|
+
/* The default rhythm for related blocks inside a card or a section. */
|
|
65
|
+
.stack-3 { --gap: var(--space-3); }
|
|
66
|
+
/* The default, named. Same as .stack with no step class. */
|
|
67
|
+
.stack-4 { --gap: var(--space-4); }
|
|
68
|
+
/* Loose vertical rhythm, for separating whole sections of a page. */
|
|
69
|
+
.stack-6 { --gap: var(--space-6); }
|
|
70
|
+
/* Very loose, for whole sections of a page where .section is too much. */
|
|
71
|
+
.stack-8 { --gap: var(--space-8); }
|
|
72
|
+
|
|
73
|
+
/* Horizontal group that wraps instead of overflowing on a phone */
|
|
74
|
+
.cluster {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-wrap: wrap;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: var(--gap, var(--space-3));
|
|
79
|
+
}
|
|
80
|
+
/* Kept as the readable name for step 2, which is what most clusters want */
|
|
81
|
+
.cluster-tight { --gap: var(--space-2); }
|
|
82
|
+
/* Packs the row against the end edge, which follows the writing
|
|
83
|
+
direction rather than always being the right. */
|
|
84
|
+
.cluster-end { justify-content: flex-end; }
|
|
85
|
+
/* Centres the row. */
|
|
86
|
+
.cluster-center { justify-content: center; }
|
|
87
|
+
/* First item to one end, last to the other. Beware of combining it
|
|
88
|
+
with wrapping: each line is justified separately, so a lone item on
|
|
89
|
+
the second line jumps to the far end and looks stranded. .bar with
|
|
90
|
+
.push is the predictable version. */
|
|
91
|
+
.cluster-between { justify-content: space-between; }
|
|
92
|
+
|
|
93
|
+
/* Push the last child away from the rest */
|
|
94
|
+
.bar {
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: var(--space-3);
|
|
98
|
+
}
|
|
99
|
+
.bar > .push { margin-inline-start: auto; }
|
|
100
|
+
|
|
101
|
+
/* Auto-fitting grid: no breakpoints needed, it responds to its own width */
|
|
102
|
+
.grid {
|
|
103
|
+
display: grid;
|
|
104
|
+
gap: var(--gap, var(--space-4));
|
|
105
|
+
grid-template-columns: repeat(auto-fit, minmax(min(var(--min, 17rem), 100%), 1fr));
|
|
106
|
+
}
|
|
107
|
+
/* Two-ish columns: an auto-fit grid with a 24rem floor. Identical in
|
|
108
|
+
effect to .grid-wide, but it writes grid-template-columns directly
|
|
109
|
+
rather than setting --min, so --min cannot adjust it afterwards.
|
|
110
|
+
Prefer .grid-wide. */
|
|
111
|
+
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(24rem, 100%), 1fr)); }
|
|
112
|
+
/* Narrows the auto-fit column floor to 12rem, so the grid packs more
|
|
113
|
+
columns in before it wraps. */
|
|
114
|
+
.grid-tight { --min: 12rem; }
|
|
115
|
+
/* Widens the auto-fit column floor to 24rem, so the grid packs fewer,
|
|
116
|
+
wider columns before it wraps. For cards with a paragraph in them. */
|
|
117
|
+
.grid-wide { --min: 24rem; }
|
|
118
|
+
/* Exactly two equal columns at every width, including a phone. Only
|
|
119
|
+
for a count that is part of the meaning — a 2x2 comparison. It does
|
|
120
|
+
not reflow, so it does not satisfy the reflow criterion on its own. */
|
|
121
|
+
.grid-fixed-2 { grid-template-columns: 1fr 1fr; }
|
|
122
|
+
/* Exactly three equal columns at every width. See .grid-fixed-2. */
|
|
123
|
+
.grid-fixed-3 { grid-template-columns: repeat(3, 1fr); }
|
|
124
|
+
/* Exactly four equal columns at every width. On a phone that is four
|
|
125
|
+
70px columns; reach for .grid with a --min instead. */
|
|
126
|
+
.grid-fixed-4 { grid-template-columns: repeat(4, 1fr); }
|
|
127
|
+
|
|
128
|
+
/* Content + rail. Collapses to one column below the width it needs. */
|
|
129
|
+
.split {
|
|
130
|
+
display: grid;
|
|
131
|
+
gap: var(--gap, var(--space-6));
|
|
132
|
+
grid-template-columns: 1fr;
|
|
133
|
+
}
|
|
134
|
+
/* A grid item's min-inline-size is auto, so a child that cannot shrink — a
|
|
135
|
+
form row, a wide table, a long code block — makes the whole column wider
|
|
136
|
+
than its track and pushes the page sideways. This is the single most
|
|
137
|
+
common grid bug there is; the two-column form in the demo overflowed a
|
|
138
|
+
375px viewport by 16px until this line existed. */
|
|
139
|
+
.split > * { min-inline-size: 0; }
|
|
140
|
+
@media (min-width: 64rem) {
|
|
141
|
+
.split { grid-template-columns: minmax(0, 1fr) var(--rail, 20rem); }
|
|
142
|
+
/* Puts the rail on the starting edge without touching markup order, so
|
|
143
|
+
the main column stays first for a screen reader and for the keyboard.
|
|
144
|
+
Use it for supplementary navigation; not for a rail holding the first
|
|
145
|
+
thing a reader needs. */
|
|
146
|
+
.split-rail-start { grid-template-columns: var(--rail, 20rem) minmax(0, 1fr); }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Center a thing with a max width and no side effects */
|
|
150
|
+
.center {
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
align-items: center;
|
|
154
|
+
text-align: center;
|
|
155
|
+
gap: var(--gap, var(--space-4));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Vertical page rhythm: a block of --space-section, which is a clamp that
|
|
159
|
+
grows with the viewport. For spacing inside a component reach for
|
|
160
|
+
.stack instead. */
|
|
161
|
+
.section { padding-block: var(--space-section); }
|
|
162
|
+
|
|
163
|
+
/* Everything stays on one line and scrolls sideways — cards, chips, dates */
|
|
164
|
+
.scroller {
|
|
165
|
+
display: flex;
|
|
166
|
+
gap: var(--gap, var(--space-3));
|
|
167
|
+
overflow-x: auto;
|
|
168
|
+
overscroll-behavior-inline: contain;
|
|
169
|
+
scroll-snap-type: x proximity;
|
|
170
|
+
scrollbar-width: none;
|
|
171
|
+
padding-block-end: var(--space-2);
|
|
172
|
+
margin-inline: calc(var(--space-gutter) * -1);
|
|
173
|
+
padding-inline: var(--space-gutter);
|
|
174
|
+
}
|
|
175
|
+
.scroller::-webkit-scrollbar { display: none; }
|
|
176
|
+
.scroller > * { scroll-snap-align: start; flex: 0 0 auto; }
|
|
177
|
+
|
|
178
|
+
/* A sticky header. Where scroll-state container queries are supported
|
|
179
|
+
the blur and shadow live on an ::after that is transparent until the
|
|
180
|
+
header actually sticks, so in flow it looks like part of the page.
|
|
181
|
+
The treatment rides on a pseudo-element because a scroll-state
|
|
182
|
+
container can style its descendants but not itself. */
|
|
183
|
+
.sticky-top {
|
|
184
|
+
position: sticky;
|
|
185
|
+
inset-block-start: 0;
|
|
186
|
+
z-index: var(--z-sticky);
|
|
187
|
+
background: color-mix(in oklab, var(--bg) 82%, transparent);
|
|
188
|
+
backdrop-filter: saturate(1.6) blur(12px);
|
|
189
|
+
-webkit-backdrop-filter: saturate(1.6) blur(12px);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Sitting in flow it needs no treatment at all; it earns the blur and the
|
|
193
|
+
shadow at the moment it actually sticks. A scroll-state container cannot
|
|
194
|
+
style itself, so the treatment rides on ::after, which is inside it. */
|
|
195
|
+
@supports (container-type: scroll-state) {
|
|
196
|
+
.sticky-top {
|
|
197
|
+
container-type: scroll-state;
|
|
198
|
+
background: none;
|
|
199
|
+
backdrop-filter: none;
|
|
200
|
+
-webkit-backdrop-filter: none;
|
|
201
|
+
}
|
|
202
|
+
.sticky-top::after {
|
|
203
|
+
content: "";
|
|
204
|
+
position: absolute;
|
|
205
|
+
inset: 0;
|
|
206
|
+
z-index: -1;
|
|
207
|
+
opacity: 0;
|
|
208
|
+
background: color-mix(in oklab, var(--bg) 82%, transparent);
|
|
209
|
+
backdrop-filter: saturate(1.6) blur(12px);
|
|
210
|
+
-webkit-backdrop-filter: saturate(1.6) blur(12px);
|
|
211
|
+
transition: opacity var(--dur-2) var(--ease-out);
|
|
212
|
+
}
|
|
213
|
+
@container scroll-state(stuck: block-start) {
|
|
214
|
+
.sticky-top::after { opacity: 1; box-shadow: var(--shadow-2); }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Header, content, footer in a 100dvh grid whose middle row takes the
|
|
219
|
+
remaining height — the sticky footer, without the hacks. dvh rather
|
|
220
|
+
than vh so it tracks a phone browser hiding its chrome. It expects
|
|
221
|
+
exactly three children. */
|
|
222
|
+
.app-shell {
|
|
223
|
+
display: grid;
|
|
224
|
+
min-block-size: 100dvh;
|
|
225
|
+
grid-template-rows: auto 1fr auto;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Aspect boxes for media */
|
|
229
|
+
.ratio-16x9 { aspect-ratio: 16 / 9; object-fit: cover; inline-size: 100%; }
|
|
230
|
+
.ratio-4x3 { aspect-ratio: 4 / 3; object-fit: cover; inline-size: 100%; }
|
|
231
|
+
.ratio-1x1 { aspect-ratio: 1; object-fit: cover; inline-size: 100%; }
|
|
232
|
+
|
|
233
|
+
/* Container queries: components respond to their slot, not the viewport */
|
|
234
|
+
.cq { container-type: inline-size; }
|
|
235
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Deck — 05 Buttons
|
|
3
|
+
========================================================================== */
|
|
4
|
+
|
|
5
|
+
@layer deck.components {
|
|
6
|
+
|
|
7
|
+
.btn {
|
|
8
|
+
--btn-bg: var(--surface);
|
|
9
|
+
--btn-text: var(--text);
|
|
10
|
+
--btn-border: var(--line-strong);
|
|
11
|
+
--btn-shadow: var(--shadow-1);
|
|
12
|
+
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
gap: var(--space-2);
|
|
17
|
+
block-size: var(--control-h);
|
|
18
|
+
min-inline-size: var(--control-h);
|
|
19
|
+
padding-inline: var(--space-4);
|
|
20
|
+
border: 1px solid var(--btn-border);
|
|
21
|
+
border-radius: var(--r-sm);
|
|
22
|
+
background: var(--btn-bg);
|
|
23
|
+
color: var(--btn-text);
|
|
24
|
+
box-shadow: var(--btn-shadow);
|
|
25
|
+
font-size: var(--text-base);
|
|
26
|
+
font-weight: 560;
|
|
27
|
+
line-height: 1;
|
|
28
|
+
letter-spacing: -.008em;
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
text-decoration: none;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
user-select: none;
|
|
33
|
+
-webkit-tap-highlight-color: transparent;
|
|
34
|
+
touch-action: manipulation;
|
|
35
|
+
transition: background-color var(--dur-1) var(--ease-out),
|
|
36
|
+
border-color var(--dur-1) var(--ease-out),
|
|
37
|
+
color var(--dur-1) var(--ease-out),
|
|
38
|
+
box-shadow var(--dur-1) var(--ease-out),
|
|
39
|
+
translate var(--dur-1) var(--ease-out);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Every button clears the 44px touch target even when it looks smaller */
|
|
43
|
+
.btn { position: relative; }
|
|
44
|
+
.btn::before {
|
|
45
|
+
content: "";
|
|
46
|
+
position: absolute;
|
|
47
|
+
inset-block-start: 50%;
|
|
48
|
+
inset-inline-start: 50%;
|
|
49
|
+
translate: -50% -50%;
|
|
50
|
+
inline-size: 100%;
|
|
51
|
+
block-size: 100%;
|
|
52
|
+
min-inline-size: var(--tap);
|
|
53
|
+
min-block-size: var(--tap);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.btn:hover { background: var(--surface-hover); border-color: var(--ink-400); }
|
|
57
|
+
.btn:active { translate: 0 1px; box-shadow: none; }
|
|
58
|
+
.btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
59
|
+
|
|
60
|
+
/* Styles a link or non-form element as disabled. It cannot stop a click
|
|
61
|
+
the way the disabled attribute does, so add aria-disabled="true" and
|
|
62
|
+
remove the href if it must not be followed. */
|
|
63
|
+
.btn[disabled], .btn[aria-disabled="true"], .btn.is-disabled {
|
|
64
|
+
opacity: .5;
|
|
65
|
+
cursor: not-allowed;
|
|
66
|
+
pointer-events: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* --- Variants ----------------------------------------------------------- */
|
|
70
|
+
/* The one important action in a view. Solid brand fill; use exactly one
|
|
71
|
+
per screen so it keeps meaning something. */
|
|
72
|
+
.btn-primary {
|
|
73
|
+
--btn-bg: var(--brand-600);
|
|
74
|
+
--btn-text: var(--text-on-brand);
|
|
75
|
+
--btn-border: transparent;
|
|
76
|
+
--btn-shadow: var(--shadow-2), var(--shadow-inset);
|
|
77
|
+
}
|
|
78
|
+
.btn-primary:hover { --btn-bg: var(--brand-700); border-color: transparent; background: var(--brand-700); }
|
|
79
|
+
|
|
80
|
+
/* A secondary emphasis colour, for upsells and highlights. */
|
|
81
|
+
.btn-accent {
|
|
82
|
+
--btn-bg: var(--accent-500);
|
|
83
|
+
--btn-text: var(--text-on-accent);
|
|
84
|
+
--btn-border: transparent;
|
|
85
|
+
--btn-shadow: var(--shadow-2), var(--shadow-inset);
|
|
86
|
+
}
|
|
87
|
+
.btn-accent:hover { background: oklch(from var(--accent-500) calc(l - .05) c h); border-color: transparent; }
|
|
88
|
+
|
|
89
|
+
/* Destructive and irreversible. Pair it with a confirmation. */
|
|
90
|
+
.btn-danger {
|
|
91
|
+
--btn-bg: var(--bad-600);
|
|
92
|
+
--btn-text: var(--text-on-bad);
|
|
93
|
+
--btn-border: transparent;
|
|
94
|
+
--btn-shadow: var(--shadow-2), var(--shadow-inset);
|
|
95
|
+
}
|
|
96
|
+
.btn-danger:hover { background: var(--bad-700); border-color: transparent; }
|
|
97
|
+
|
|
98
|
+
/* Tinted, low weight. Good for an action repeated down a list. */
|
|
99
|
+
.btn-soft {
|
|
100
|
+
--btn-bg: var(--brand-soft);
|
|
101
|
+
--btn-text: var(--brand-soft-text);
|
|
102
|
+
--btn-border: transparent;
|
|
103
|
+
--btn-shadow: none;
|
|
104
|
+
}
|
|
105
|
+
.btn-soft:hover { background: color-mix(in oklab, var(--brand-soft) 80%, var(--brand-300)); border-color: transparent; }
|
|
106
|
+
|
|
107
|
+
/* No border or background until hover. For toolbars and dense rows where
|
|
108
|
+
seven bordered buttons would be noise. */
|
|
109
|
+
.btn-ghost {
|
|
110
|
+
--btn-bg: transparent;
|
|
111
|
+
--btn-border: transparent;
|
|
112
|
+
--btn-shadow: none;
|
|
113
|
+
}
|
|
114
|
+
.btn-ghost:hover { background: var(--surface-hover); border-color: transparent; }
|
|
115
|
+
|
|
116
|
+
/* Brand-coloured border, transparent fill. */
|
|
117
|
+
.btn-outline {
|
|
118
|
+
--btn-bg: transparent;
|
|
119
|
+
--btn-text: var(--brand);
|
|
120
|
+
--btn-border: color-mix(in oklab, var(--brand) 45%, transparent);
|
|
121
|
+
--btn-shadow: none;
|
|
122
|
+
}
|
|
123
|
+
.btn-outline:hover { background: var(--brand-soft); border-color: var(--brand); }
|
|
124
|
+
|
|
125
|
+
/* Looks like a link, behaves like a button. Only for a real <button>
|
|
126
|
+
that has to sit in a row of links — on an anchor it adds a touch
|
|
127
|
+
target, a ring offset and nowrap to something that wanted none of them. */
|
|
128
|
+
.btn-link {
|
|
129
|
+
--btn-bg: transparent;
|
|
130
|
+
--btn-border: transparent;
|
|
131
|
+
--btn-shadow: none;
|
|
132
|
+
--btn-text: var(--brand);
|
|
133
|
+
padding-inline: var(--space-1);
|
|
134
|
+
block-size: auto;
|
|
135
|
+
}
|
|
136
|
+
.btn-link:hover { background: none; border-color: transparent; text-decoration: underline; }
|
|
137
|
+
|
|
138
|
+
/* --- Sizes -------------------------------------------------------------- */
|
|
139
|
+
/* The 34px size. Still clears the 44px touch target, because the target
|
|
140
|
+
is a pseudo-element rather than the box. */
|
|
141
|
+
.btn-sm {
|
|
142
|
+
block-size: var(--control-h-sm);
|
|
143
|
+
padding-inline: var(--space-3);
|
|
144
|
+
font-size: var(--text-sm);
|
|
145
|
+
border-radius: var(--r-xs);
|
|
146
|
+
gap: var(--space-1);
|
|
147
|
+
}
|
|
148
|
+
/* The large size, from --control-h-lg. */
|
|
149
|
+
.btn-lg {
|
|
150
|
+
block-size: var(--control-h-lg);
|
|
151
|
+
padding-inline: var(--space-6);
|
|
152
|
+
font-size: var(--text-md);
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
}
|
|
155
|
+
/* Full width. For a phone, or the primary action at the end of a form. */
|
|
156
|
+
.btn-block { inline-size: 100%; }
|
|
157
|
+
|
|
158
|
+
/* Square, icon-only button. It has no text, so it needs an aria-label. */
|
|
159
|
+
.btn-icon { padding-inline: 0; inline-size: var(--control-h); }
|
|
160
|
+
.btn-icon.btn-sm { inline-size: var(--control-h-sm); }
|
|
161
|
+
.btn-icon.btn-lg { inline-size: var(--control-h-lg); }
|
|
162
|
+
/* A fully rounded button, usually paired with .btn-icon. */
|
|
163
|
+
.btn-round { border-radius: var(--r-full); }
|
|
164
|
+
|
|
165
|
+
/* --- Group -------------------------------------------------------------- */
|
|
166
|
+
/* Buttons joined into one control, with the inner radii and the doubled
|
|
167
|
+
border between them removed. Logical radii, so it mirrors under RTL. */
|
|
168
|
+
.btn-group { display: inline-flex; }
|
|
169
|
+
.btn-group > .btn { border-radius: 0; box-shadow: none; }
|
|
170
|
+
.btn-group > .btn:first-child { border-start-start-radius: var(--r-sm); border-end-start-radius: var(--r-sm); }
|
|
171
|
+
.btn-group > .btn:last-child { border-start-end-radius: var(--r-sm); border-end-end-radius: var(--r-sm); }
|
|
172
|
+
.btn-group > .btn + .btn { margin-inline-start: -1px; }
|
|
173
|
+
.btn-group > .btn:hover, .btn-group > .btn:focus-visible { z-index: 1; }
|
|
174
|
+
|
|
175
|
+
/* --- Loading ------------------------------------------------------------ */
|
|
176
|
+
.btn.is-loading { color: transparent; pointer-events: none; }
|
|
177
|
+
.btn.is-loading::after {
|
|
178
|
+
content: "";
|
|
179
|
+
position: absolute;
|
|
180
|
+
inline-size: 1em;
|
|
181
|
+
block-size: 1em;
|
|
182
|
+
border: 2px solid color-mix(in oklab, var(--btn-text) 30%, transparent);
|
|
183
|
+
border-block-start-color: var(--btn-text);
|
|
184
|
+
border-radius: var(--r-full);
|
|
185
|
+
animation: dk-spin .62s linear infinite;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@keyframes dk-spin { to { rotate: 360deg; } }
|
|
189
|
+
|
|
190
|
+
/* Floating action button, parked above the tab bar and the notch */
|
|
191
|
+
.fab {
|
|
192
|
+
position: fixed;
|
|
193
|
+
inset-block-end: calc(var(--space-5) + env(safe-area-inset-bottom));
|
|
194
|
+
inset-inline-end: var(--space-5);
|
|
195
|
+
z-index: var(--z-nav);
|
|
196
|
+
inline-size: 56px;
|
|
197
|
+
block-size: 56px;
|
|
198
|
+
border-radius: var(--r-full);
|
|
199
|
+
background: var(--brand-600);
|
|
200
|
+
color: var(--text-on-brand);
|
|
201
|
+
box-shadow: var(--shadow-4);
|
|
202
|
+
display: grid;
|
|
203
|
+
place-items: center;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
transition: scale var(--dur-2) var(--ease-spring), background-color var(--dur-1) var(--ease-out);
|
|
206
|
+
}
|
|
207
|
+
.fab:hover { background: var(--brand-700); scale: 1.04; }
|
|
208
|
+
.fab:active { scale: .96; }
|
|
209
|
+
body:has(.tabbar) .fab { inset-block-end: calc(var(--space-4) + var(--tap) + var(--space-3) + env(safe-area-inset-bottom)); }
|
|
210
|
+
}
|