@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,305 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Deck — 10 Date picker
|
|
3
|
+
Popover calendar on desktop, bottom sheet on a phone. Single date, range,
|
|
4
|
+
and month/year jump. Behaviour lives in deck.js; this file is the skin.
|
|
5
|
+
========================================================================== */
|
|
6
|
+
|
|
7
|
+
@layer deck.components {
|
|
8
|
+
|
|
9
|
+
/* --- Trigger field ------------------------------------------------------ */
|
|
10
|
+
/* The trigger for a date picker: an input with a calendar icon inside its
|
|
11
|
+
starting edge and a clear button at the end. The input is readonly and
|
|
12
|
+
cursor: pointer, because the value comes from the calendar — which also
|
|
13
|
+
means a reader who knows the date cannot type it. */
|
|
14
|
+
.datefield { position: relative; display: flex; align-items: center; }
|
|
15
|
+
.datefield > .icon {
|
|
16
|
+
position: absolute;
|
|
17
|
+
inset-inline-start: var(--space-3);
|
|
18
|
+
color: var(--text-faint);
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
}
|
|
21
|
+
.datefield > .input { padding-inline-start: var(--space-10); cursor: pointer; }
|
|
22
|
+
/* Clears the field. Hidden until .has-value is set by deck.js. */
|
|
23
|
+
.datefield > .datefield-clear {
|
|
24
|
+
position: absolute;
|
|
25
|
+
inset-inline-end: var(--space-2);
|
|
26
|
+
inline-size: 28px;
|
|
27
|
+
block-size: 28px;
|
|
28
|
+
display: grid;
|
|
29
|
+
place-items: center;
|
|
30
|
+
border-radius: var(--r-full);
|
|
31
|
+
color: var(--text-faint);
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
}
|
|
34
|
+
.datefield > .datefield-clear:hover { background: var(--surface-hover); color: var(--text); }
|
|
35
|
+
.datefield:not(.has-value) .datefield-clear { display: none; }
|
|
36
|
+
|
|
37
|
+
/* --- Panel -------------------------------------------------------------- */
|
|
38
|
+
/* The calendar panel: a native popover, anchored to its field by
|
|
39
|
+
25-anchor.css with the same fallback chain as a menu. Where there is no
|
|
40
|
+
room below it opens upward, and position-try-order: most-block-size picks
|
|
41
|
+
the direction with the most room rather than the first that fits — which
|
|
42
|
+
matters more for a calendar than for a menu. */
|
|
43
|
+
.datepicker {
|
|
44
|
+
position: absolute;
|
|
45
|
+
margin: 0;
|
|
46
|
+
inset: auto;
|
|
47
|
+
padding: 0;
|
|
48
|
+
inline-size: max-content;
|
|
49
|
+
max-inline-size: calc(100vw - var(--space-6));
|
|
50
|
+
background: var(--surface);
|
|
51
|
+
border: 1px solid var(--line);
|
|
52
|
+
border-radius: var(--r-md);
|
|
53
|
+
box-shadow: var(--shadow-4);
|
|
54
|
+
color: var(--text);
|
|
55
|
+
opacity: 0;
|
|
56
|
+
translate: 0 -6px;
|
|
57
|
+
transition: opacity var(--dur-2) var(--ease-out),
|
|
58
|
+
translate var(--dur-2) var(--ease-out),
|
|
59
|
+
overlay var(--dur-2) allow-discrete,
|
|
60
|
+
display var(--dur-2) allow-discrete;
|
|
61
|
+
}
|
|
62
|
+
.datepicker:popover-open { opacity: 1; translate: 0; }
|
|
63
|
+
@starting-style { .datepicker:popover-open { opacity: 0; translate: 0 -6px; } }
|
|
64
|
+
.datepicker::backdrop { background: transparent; }
|
|
65
|
+
|
|
66
|
+
.datepicker-layout { display: flex; flex-direction: column; }
|
|
67
|
+
@media (min-width: 40rem) {
|
|
68
|
+
.datepicker-layout:has(.datepicker-presets) { flex-direction: row; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* --- Preset rail --------------------------------------------------------- */
|
|
72
|
+
/* The shortcut rail — "Last 7 days", "This month". A horizontal scroller
|
|
73
|
+
below 40rem and a column beside the calendar above it. */
|
|
74
|
+
.datepicker-presets {
|
|
75
|
+
display: flex;
|
|
76
|
+
gap: var(--space-1);
|
|
77
|
+
padding: var(--space-2);
|
|
78
|
+
overflow-x: auto;
|
|
79
|
+
scrollbar-width: none;
|
|
80
|
+
border-block-end: 1px solid var(--line);
|
|
81
|
+
}
|
|
82
|
+
.datepicker-presets::-webkit-scrollbar { display: none; }
|
|
83
|
+
@media (min-width: 40rem) {
|
|
84
|
+
.datepicker-presets {
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
inline-size: 10.5rem;
|
|
87
|
+
overflow-x: visible;
|
|
88
|
+
border-block-end: 0;
|
|
89
|
+
border-inline-end: 1px solid var(--line);
|
|
90
|
+
background: var(--surface-2);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
.datepicker-preset {
|
|
94
|
+
padding: var(--space-2) var(--space-3);
|
|
95
|
+
min-block-size: 36px;
|
|
96
|
+
border-radius: var(--r-xs);
|
|
97
|
+
font-size: var(--text-sm);
|
|
98
|
+
font-weight: 520;
|
|
99
|
+
color: var(--text-muted);
|
|
100
|
+
text-align: start;
|
|
101
|
+
white-space: nowrap;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
|
|
104
|
+
}
|
|
105
|
+
.datepicker-preset:hover { background: var(--surface-hover); color: var(--text); }
|
|
106
|
+
.datepicker-preset[aria-pressed="true"] { background: var(--brand-soft); color: var(--brand-soft-text); font-weight: 600; }
|
|
107
|
+
|
|
108
|
+
/* --- Calendar ----------------------------------------------------------- */
|
|
109
|
+
.datepicker-main { padding: var(--space-3); }
|
|
110
|
+
|
|
111
|
+
.datepicker-head {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: var(--space-1);
|
|
115
|
+
padding-block-end: var(--space-2);
|
|
116
|
+
}
|
|
117
|
+
.datepicker-title {
|
|
118
|
+
flex: 1 1 auto;
|
|
119
|
+
display: inline-flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
gap: var(--space-2);
|
|
123
|
+
min-block-size: 36px;
|
|
124
|
+
padding-inline: var(--space-3);
|
|
125
|
+
border-radius: var(--r-sm);
|
|
126
|
+
font-weight: 620;
|
|
127
|
+
font-size: var(--text-sm);
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
letter-spacing: -.01em;
|
|
130
|
+
}
|
|
131
|
+
.datepicker-title:hover { background: var(--surface-hover); }
|
|
132
|
+
.datepicker-nav {
|
|
133
|
+
inline-size: 36px;
|
|
134
|
+
block-size: 36px;
|
|
135
|
+
display: grid;
|
|
136
|
+
place-items: center;
|
|
137
|
+
border-radius: var(--r-sm);
|
|
138
|
+
color: var(--text-muted);
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
}
|
|
141
|
+
.datepicker-nav:hover { background: var(--surface-hover); color: var(--text); }
|
|
142
|
+
.datepicker-nav[disabled] { opacity: .35; pointer-events: none; }
|
|
143
|
+
|
|
144
|
+
.datepicker-months { display: flex; gap: var(--space-5); }
|
|
145
|
+
.datepicker-month { display: flex; flex-direction: column; }
|
|
146
|
+
|
|
147
|
+
.datepicker-grid {
|
|
148
|
+
display: grid;
|
|
149
|
+
grid-template-columns: repeat(7, 1fr);
|
|
150
|
+
gap: 1px;
|
|
151
|
+
}
|
|
152
|
+
.datepicker-dow {
|
|
153
|
+
text-align: center;
|
|
154
|
+
font-size: var(--text-2xs);
|
|
155
|
+
font-weight: 620;
|
|
156
|
+
color: var(--text-faint);
|
|
157
|
+
padding-block: var(--space-2);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* One day cell. 38px, raised to 42px on a coarse pointer so it approaches
|
|
161
|
+
the touch target. Eight is- states compose on it, and the range band is
|
|
162
|
+
built from is-in-range plus logical corner radii on the two ends. */
|
|
163
|
+
.datepicker-day {
|
|
164
|
+
position: relative;
|
|
165
|
+
inline-size: 38px;
|
|
166
|
+
block-size: 38px;
|
|
167
|
+
display: grid;
|
|
168
|
+
place-items: center;
|
|
169
|
+
border-radius: var(--r-sm);
|
|
170
|
+
font-size: var(--text-sm);
|
|
171
|
+
font-variant-numeric: tabular-nums;
|
|
172
|
+
color: var(--text);
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
-webkit-tap-highlight-color: transparent;
|
|
175
|
+
transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
|
|
176
|
+
}
|
|
177
|
+
@media (pointer: coarse) { .datepicker-day { inline-size: 42px; block-size: 42px; } }
|
|
178
|
+
|
|
179
|
+
.datepicker-day:hover { background: var(--surface-hover); }
|
|
180
|
+
.datepicker-day.is-outside { color: var(--text-faint); opacity: .55; }
|
|
181
|
+
.datepicker-day[disabled], .datepicker-day.is-blocked {
|
|
182
|
+
color: var(--text-faint);
|
|
183
|
+
opacity: .4;
|
|
184
|
+
pointer-events: none;
|
|
185
|
+
text-decoration: line-through;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Today gets a marker, not a fill, so it never competes with the selection */
|
|
189
|
+
.datepicker-day.is-today::after {
|
|
190
|
+
content: "";
|
|
191
|
+
position: absolute;
|
|
192
|
+
inset-block-end: 5px;
|
|
193
|
+
inline-size: 4px;
|
|
194
|
+
block-size: 4px;
|
|
195
|
+
border-radius: var(--r-full);
|
|
196
|
+
background: var(--brand);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.datepicker-day.is-selected {
|
|
200
|
+
background: var(--brand-600);
|
|
201
|
+
color: var(--text-on-brand);
|
|
202
|
+
font-weight: 620;
|
|
203
|
+
}
|
|
204
|
+
.datepicker-day.is-selected::after { background: var(--text-on-brand); }
|
|
205
|
+
|
|
206
|
+
/* Range: the middle days get a continuous band, the ends get the caps */
|
|
207
|
+
.datepicker-day.is-in-range {
|
|
208
|
+
background: var(--brand-soft);
|
|
209
|
+
color: var(--brand-soft-text);
|
|
210
|
+
border-radius: 0;
|
|
211
|
+
}
|
|
212
|
+
.datepicker-day.is-range-start {
|
|
213
|
+
border-start-end-radius: 0;
|
|
214
|
+
border-end-end-radius: 0;
|
|
215
|
+
}
|
|
216
|
+
.datepicker-day.is-range-end {
|
|
217
|
+
border-start-start-radius: 0;
|
|
218
|
+
border-end-start-radius: 0;
|
|
219
|
+
}
|
|
220
|
+
.datepicker-day.is-range-start.is-range-end { border-radius: var(--r-sm); }
|
|
221
|
+
.datepicker-grid:has(.is-in-range) .datepicker-day.is-in-range:first-child {
|
|
222
|
+
border-start-start-radius: var(--r-sm); border-end-start-radius: var(--r-sm);
|
|
223
|
+
}
|
|
224
|
+
.datepicker-grid:has(.is-in-range) .datepicker-day.is-in-range:nth-child(7n) {
|
|
225
|
+
border-start-end-radius: var(--r-sm); border-end-end-radius: var(--r-sm);
|
|
226
|
+
}
|
|
227
|
+
.datepicker-day.is-preview { background: color-mix(in oklab, var(--brand-soft) 60%, transparent); }
|
|
228
|
+
.datepicker-day:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; z-index: 1; }
|
|
229
|
+
|
|
230
|
+
/* --- Month and year jump grid ------------------------------------------- */
|
|
231
|
+
/* Opens the month and year grid. A picker without one makes a reader press
|
|
232
|
+
the next-month arrow twenty-four times to reach a date two years out. */
|
|
233
|
+
.datepicker-jump {
|
|
234
|
+
display: grid;
|
|
235
|
+
grid-template-columns: repeat(3, 1fr);
|
|
236
|
+
gap: var(--space-1);
|
|
237
|
+
padding-block: var(--space-2);
|
|
238
|
+
}
|
|
239
|
+
.datepicker-jump > button {
|
|
240
|
+
min-block-size: 40px;
|
|
241
|
+
padding-inline: var(--space-2);
|
|
242
|
+
border-radius: var(--r-sm);
|
|
243
|
+
font-size: var(--text-sm);
|
|
244
|
+
font-variant-numeric: tabular-nums;
|
|
245
|
+
color: var(--text-muted);
|
|
246
|
+
cursor: pointer;
|
|
247
|
+
}
|
|
248
|
+
.datepicker-jump > button:hover { background: var(--surface-hover); color: var(--text); }
|
|
249
|
+
.datepicker-jump > button[aria-selected="true"] {
|
|
250
|
+
background: var(--brand-600);
|
|
251
|
+
color: var(--text-on-brand);
|
|
252
|
+
font-weight: 620;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* --- Time row and footer ------------------------------------------------- */
|
|
256
|
+
.datepicker-time {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: var(--space-2);
|
|
260
|
+
padding: var(--space-3);
|
|
261
|
+
border-block-start: 1px solid var(--line);
|
|
262
|
+
}
|
|
263
|
+
.datepicker-time .input { inline-size: auto; min-inline-size: 7.5rem; }
|
|
264
|
+
|
|
265
|
+
.datepicker-foot {
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: var(--space-2);
|
|
269
|
+
padding: var(--space-3);
|
|
270
|
+
border-block-start: 1px solid var(--line);
|
|
271
|
+
background: var(--surface-2);
|
|
272
|
+
}
|
|
273
|
+
.datepicker-readout {
|
|
274
|
+
flex: 1 1 auto;
|
|
275
|
+
font-size: var(--text-sm);
|
|
276
|
+
color: var(--text-muted);
|
|
277
|
+
font-variant-numeric: tabular-nums;
|
|
278
|
+
min-inline-size: 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* --- Phone: the picker becomes a sheet ---------------------------------- */
|
|
282
|
+
@media (max-width: 30rem) {
|
|
283
|
+
.datepicker {
|
|
284
|
+
position: fixed;
|
|
285
|
+
inset: auto 0 0 0;
|
|
286
|
+
inline-size: 100%;
|
|
287
|
+
max-inline-size: 100%;
|
|
288
|
+
border-radius: var(--r-lg) var(--r-lg) 0 0;
|
|
289
|
+
border-inline: 0;
|
|
290
|
+
border-block-end: 0;
|
|
291
|
+
translate: 0 100%;
|
|
292
|
+
padding-block-end: env(safe-area-inset-bottom);
|
|
293
|
+
transition: translate var(--dur-3) var(--ease-out),
|
|
294
|
+
overlay var(--dur-3) allow-discrete,
|
|
295
|
+
display var(--dur-3) allow-discrete;
|
|
296
|
+
opacity: 1;
|
|
297
|
+
}
|
|
298
|
+
.datepicker:popover-open { translate: 0; }
|
|
299
|
+
@starting-style { .datepicker:popover-open { translate: 0 100%; } }
|
|
300
|
+
.datepicker::backdrop { background: oklch(18% .02 var(--hue-neutral) / .45); }
|
|
301
|
+
.datepicker-months { flex-direction: column; }
|
|
302
|
+
.datepicker-day { inline-size: 100%; }
|
|
303
|
+
.datepicker-main { padding-inline: var(--space-4); }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Deck — 06 Forms
|
|
3
|
+
Inputs are 16px on touch devices so iOS never zooms on focus.
|
|
4
|
+
========================================================================== */
|
|
5
|
+
|
|
6
|
+
@layer deck.components {
|
|
7
|
+
|
|
8
|
+
/* One form control and everything attached to it: label, input, help
|
|
9
|
+
text, error. The vertical gap between them comes from here, so a field
|
|
10
|
+
never needs margins. */
|
|
11
|
+
.field {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: var(--space-2);
|
|
15
|
+
min-inline-size: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* The label for a control. A flex row, so a "required" or "optional"
|
|
19
|
+
marker sits beside the text without extra markup. */
|
|
20
|
+
.label {
|
|
21
|
+
font-size: var(--text-sm);
|
|
22
|
+
font-weight: 580;
|
|
23
|
+
color: var(--text);
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: var(--space-2);
|
|
27
|
+
}
|
|
28
|
+
/* The marker for an optional field. Where most fields are required this
|
|
29
|
+
reads better than an asterisk on nearly every label. */
|
|
30
|
+
.label .optional { color: var(--text-faint); font-weight: 400; }
|
|
31
|
+
/* The marker beside a required field's label. Visual only — a screen
|
|
32
|
+
reader announces the required attribute, not the asterisk. */
|
|
33
|
+
.label .required { color: var(--bad-500); }
|
|
34
|
+
|
|
35
|
+
/* A hint below a control. Needs aria-describedby on the control to reach
|
|
36
|
+
a screen reader — sitting next to it is not an association. */
|
|
37
|
+
.help { font-size: var(--text-sm); color: var(--text-muted); }
|
|
38
|
+
/* A validation message below a control. Lightened for dark mode with
|
|
39
|
+
light-dark(), and aligned to flex-start so an icon stays on the first
|
|
40
|
+
line of a two-line message. */
|
|
41
|
+
.error {
|
|
42
|
+
font-size: var(--text-sm);
|
|
43
|
+
color: light-dark(var(--bad-700), oklch(76% .13 var(--hue-bad)));
|
|
44
|
+
display: flex;
|
|
45
|
+
gap: var(--space-2);
|
|
46
|
+
align-items: flex-start;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.input, .textarea, .select {
|
|
50
|
+
inline-size: 100%;
|
|
51
|
+
min-block-size: var(--control-h);
|
|
52
|
+
padding: var(--space-2) var(--space-3);
|
|
53
|
+
background: var(--surface);
|
|
54
|
+
color: var(--text);
|
|
55
|
+
border: 1px solid var(--line-strong);
|
|
56
|
+
border-radius: var(--r-sm);
|
|
57
|
+
box-shadow: var(--shadow-1);
|
|
58
|
+
font-size: var(--text-base);
|
|
59
|
+
line-height: 1.45;
|
|
60
|
+
transition: border-color var(--dur-1) var(--ease-out),
|
|
61
|
+
box-shadow var(--dur-1) var(--ease-out),
|
|
62
|
+
background-color var(--dur-1) var(--ease-out);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@media (pointer: coarse) {
|
|
66
|
+
.input, .textarea, .select { font-size: 16px; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.input::placeholder, .textarea::placeholder { color: var(--text-faint); }
|
|
70
|
+
|
|
71
|
+
.input:hover, .textarea:hover, .select:hover { border-color: var(--ink-400); }
|
|
72
|
+
|
|
73
|
+
.input:focus, .textarea:focus, .select:focus {
|
|
74
|
+
outline: none;
|
|
75
|
+
border-color: var(--focus);
|
|
76
|
+
box-shadow: var(--ring);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.input[disabled], .textarea[disabled], .select[disabled] {
|
|
80
|
+
background: var(--bg-sunken);
|
|
81
|
+
color: var(--text-faint);
|
|
82
|
+
cursor: not-allowed;
|
|
83
|
+
box-shadow: none;
|
|
84
|
+
}
|
|
85
|
+
.input[readonly] { background: var(--surface-2); }
|
|
86
|
+
|
|
87
|
+
/* Grows with its content instead of scrolling in a 4-row box */
|
|
88
|
+
.textarea {
|
|
89
|
+
min-block-size: calc(var(--control-h) * 2);
|
|
90
|
+
resize: vertical;
|
|
91
|
+
field-sizing: content;
|
|
92
|
+
max-block-size: 22rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.select {
|
|
96
|
+
appearance: none;
|
|
97
|
+
padding-inline-end: var(--space-10);
|
|
98
|
+
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
|
|
99
|
+
linear-gradient(135deg, currentColor 50%, transparent 50%);
|
|
100
|
+
background-position: calc(100% - 19px) calc(50% + 1px), calc(100% - 14px) calc(50% + 1px);
|
|
101
|
+
background-size: 5px 5px, 5px 5px;
|
|
102
|
+
background-repeat: no-repeat;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Validation only after the user has actually interacted */
|
|
107
|
+
/* Marks a field invalid from your own validation, producing the same
|
|
108
|
+
border and ring as :user-invalid without needing the browser to agree.
|
|
109
|
+
One of the few states an author writes by hand. */
|
|
110
|
+
.input:user-invalid, .textarea:user-invalid, .select:user-invalid,
|
|
111
|
+
.field.is-invalid :is(.input, .textarea, .select) {
|
|
112
|
+
border-color: var(--bad-500);
|
|
113
|
+
}
|
|
114
|
+
.input:user-invalid:focus, .field.is-invalid :is(.input, .textarea, .select):focus {
|
|
115
|
+
box-shadow: 0 0 0 3px color-mix(in oklab, var(--bad-500) 26%, transparent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* --- Input group: prefix / suffix / attached button --------------------- */
|
|
119
|
+
/* A prefix, suffix or attached button joined to a control. The group
|
|
120
|
+
takes over the border, radius and shadow, and the input inside strips
|
|
121
|
+
its own, so the seam does not double up. :focus-within puts the ring
|
|
122
|
+
around the whole assembly. */
|
|
123
|
+
.input-group {
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: stretch;
|
|
126
|
+
inline-size: 100%;
|
|
127
|
+
border: 1px solid var(--line-strong);
|
|
128
|
+
border-radius: var(--r-sm);
|
|
129
|
+
background: var(--surface);
|
|
130
|
+
box-shadow: var(--shadow-1);
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
}
|
|
133
|
+
.input-group:focus-within { border-color: var(--focus); box-shadow: var(--ring); }
|
|
134
|
+
.input-group > .input {
|
|
135
|
+
border: 0;
|
|
136
|
+
border-radius: 0;
|
|
137
|
+
box-shadow: none;
|
|
138
|
+
background: transparent;
|
|
139
|
+
}
|
|
140
|
+
.input-group > .input:focus { box-shadow: none; }
|
|
141
|
+
/* A prefix or suffix attached to a control inside .input-group. Its
|
|
142
|
+
divider is border-inline-end normally and border-inline-start on
|
|
143
|
+
:last-child, so a leading and a trailing addon each get their rule on
|
|
144
|
+
the correct side in both writing directions. */
|
|
145
|
+
.input-group > .addon {
|
|
146
|
+
display: inline-flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: var(--space-2);
|
|
149
|
+
padding-inline: var(--space-3);
|
|
150
|
+
background: var(--surface-2);
|
|
151
|
+
color: var(--text-muted);
|
|
152
|
+
font-size: var(--text-sm);
|
|
153
|
+
white-space: nowrap;
|
|
154
|
+
border-inline-end: 1px solid var(--line);
|
|
155
|
+
}
|
|
156
|
+
.input-group > .addon:last-child { border-inline-start: 1px solid var(--line); border-inline-end: 0; }
|
|
157
|
+
.input-group > .btn { border-radius: 0; border-block: 0; border-inline-end: 0; box-shadow: none; }
|
|
158
|
+
|
|
159
|
+
/* --- Search field ------------------------------------------------------- */
|
|
160
|
+
/* A control with an icon inside its starting edge. The icon is
|
|
161
|
+
pointer-events: none, so clicking it focuses the field underneath
|
|
162
|
+
rather than doing nothing. */
|
|
163
|
+
.search { position: relative; display: flex; align-items: center; }
|
|
164
|
+
.search > .icon {
|
|
165
|
+
position: absolute;
|
|
166
|
+
inset-inline-start: var(--space-3);
|
|
167
|
+
color: var(--text-faint);
|
|
168
|
+
pointer-events: none;
|
|
169
|
+
}
|
|
170
|
+
.search > .input { padding-inline-start: var(--space-10); border-radius: var(--r-full); }
|
|
171
|
+
|
|
172
|
+
/* --- Checkbox and radio ------------------------------------------------- */
|
|
173
|
+
/* A checkbox or radio and its text, laid out as a row that clears the
|
|
174
|
+
44px touch target. Wrap the text in .check-text to add a second line. */
|
|
175
|
+
.check {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: flex-start;
|
|
178
|
+
gap: var(--space-3);
|
|
179
|
+
min-block-size: var(--tap);
|
|
180
|
+
padding-block: var(--space-2);
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
line-height: 1.4;
|
|
183
|
+
}
|
|
184
|
+
.check input[type="checkbox"], .check input[type="radio"] {
|
|
185
|
+
appearance: none;
|
|
186
|
+
flex: 0 0 auto;
|
|
187
|
+
inline-size: 20px;
|
|
188
|
+
block-size: 20px;
|
|
189
|
+
margin-block-start: 1px;
|
|
190
|
+
border: 1.5px solid var(--line-strong);
|
|
191
|
+
background: var(--surface);
|
|
192
|
+
border-radius: var(--r-xs);
|
|
193
|
+
display: grid;
|
|
194
|
+
place-content: center;
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
transition: background-color var(--dur-1) var(--ease-out),
|
|
197
|
+
border-color var(--dur-1) var(--ease-out);
|
|
198
|
+
}
|
|
199
|
+
.check input[type="radio"] { border-radius: var(--r-full); }
|
|
200
|
+
.check input:hover { border-color: var(--brand-400); }
|
|
201
|
+
.check input:checked { background: var(--brand-600); border-color: var(--brand-600); }
|
|
202
|
+
.check input[type="checkbox"]::before {
|
|
203
|
+
content: "";
|
|
204
|
+
inline-size: 11px;
|
|
205
|
+
block-size: 11px;
|
|
206
|
+
scale: 0;
|
|
207
|
+
transition: scale var(--dur-2) var(--ease-spring);
|
|
208
|
+
box-shadow: inset 1em 1em var(--text-on-brand);
|
|
209
|
+
clip-path: polygon(14% 44%, 0 65%, 41% 100%, 100% 16%, 84% 0, 39% 68%);
|
|
210
|
+
}
|
|
211
|
+
.check input[type="radio"]::before {
|
|
212
|
+
content: "";
|
|
213
|
+
inline-size: 9px;
|
|
214
|
+
block-size: 9px;
|
|
215
|
+
border-radius: var(--r-full);
|
|
216
|
+
background: var(--text-on-brand);
|
|
217
|
+
scale: 0;
|
|
218
|
+
transition: scale var(--dur-2) var(--ease-spring);
|
|
219
|
+
}
|
|
220
|
+
.check input:checked::before { scale: 1; }
|
|
221
|
+
.check input:indeterminate { background: var(--brand-600); border-color: var(--brand-600); }
|
|
222
|
+
.check input:indeterminate::before {
|
|
223
|
+
scale: 1;
|
|
224
|
+
clip-path: polygon(10% 42%, 90% 42%, 90% 58%, 10% 58%);
|
|
225
|
+
}
|
|
226
|
+
.check input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
227
|
+
/* The text beside a checkbox or radio. A column, so a .check-note can sit
|
|
228
|
+
under the label as a second line. */
|
|
229
|
+
/* The text beside a checkbox, as a column, so a .check-note can sit
|
|
230
|
+
beneath the label as a second line. */
|
|
231
|
+
.check .check-text { display: flex; flex-direction: column; gap: 2px; }
|
|
232
|
+
/* A muted second line inside .check-text. It is inside the label, so it
|
|
233
|
+
becomes part of the accessible name — move it out and use
|
|
234
|
+
aria-describedby if that name gets too long. */
|
|
235
|
+
.check .check-note { font-size: var(--text-sm); color: var(--text-muted); }
|
|
236
|
+
|
|
237
|
+
/* Card-style choice, good for pricing and plan pickers */
|
|
238
|
+
.check-card {
|
|
239
|
+
border: 1px solid var(--line);
|
|
240
|
+
border-radius: var(--r-md);
|
|
241
|
+
padding: var(--space-4);
|
|
242
|
+
background: var(--surface);
|
|
243
|
+
transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
|
|
244
|
+
}
|
|
245
|
+
.check-card:has(input:checked) {
|
|
246
|
+
border-color: var(--brand-500);
|
|
247
|
+
background: var(--brand-soft);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* --- Switch ------------------------------------------------------------- */
|
|
251
|
+
/* An on/off control that takes effect immediately, drawn over a real
|
|
252
|
+
checkbox. If the reader has to press Save afterwards, use .check
|
|
253
|
+
instead. Add role="switch" when "on"/"off" reads better than
|
|
254
|
+
"checked"/"unchecked". */
|
|
255
|
+
.switch { display: flex; align-items: center; gap: var(--space-3); min-block-size: var(--tap); cursor: pointer; }
|
|
256
|
+
.switch input {
|
|
257
|
+
appearance: none;
|
|
258
|
+
flex: 0 0 auto;
|
|
259
|
+
inline-size: 46px;
|
|
260
|
+
block-size: 28px;
|
|
261
|
+
border-radius: var(--r-full);
|
|
262
|
+
background: var(--ink-300);
|
|
263
|
+
position: relative;
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
transition: background-color var(--dur-2) var(--ease-out);
|
|
266
|
+
}
|
|
267
|
+
.switch input::after {
|
|
268
|
+
content: "";
|
|
269
|
+
position: absolute;
|
|
270
|
+
inset-block-start: 3px;
|
|
271
|
+
inset-inline-start: 3px;
|
|
272
|
+
inline-size: 22px;
|
|
273
|
+
block-size: 22px;
|
|
274
|
+
border-radius: var(--r-full);
|
|
275
|
+
background: var(--ink-0);
|
|
276
|
+
box-shadow: var(--shadow-2);
|
|
277
|
+
transition: translate var(--dur-2) var(--ease-spring);
|
|
278
|
+
}
|
|
279
|
+
.switch input:checked { background: var(--brand-600); }
|
|
280
|
+
.switch input:checked::after { translate: 18px 0; }
|
|
281
|
+
.switch input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
282
|
+
|
|
283
|
+
/* --- Range -------------------------------------------------------------- */
|
|
284
|
+
/* A slider. The track and thumb are vendor pseudo-elements with
|
|
285
|
+
different names and different box models per engine, so every
|
|
286
|
+
declaration is written twice — WebKit needs a negative block-start
|
|
287
|
+
margin to centre the thumb and Firefox does not. */
|
|
288
|
+
.range {
|
|
289
|
+
inline-size: 100%;
|
|
290
|
+
appearance: none;
|
|
291
|
+
background: transparent;
|
|
292
|
+
block-size: var(--tap);
|
|
293
|
+
}
|
|
294
|
+
.range::-webkit-slider-runnable-track {
|
|
295
|
+
block-size: 6px;
|
|
296
|
+
border-radius: var(--r-full);
|
|
297
|
+
background: var(--ink-200);
|
|
298
|
+
}
|
|
299
|
+
.range::-moz-range-track {
|
|
300
|
+
block-size: 6px;
|
|
301
|
+
border-radius: var(--r-full);
|
|
302
|
+
background: var(--ink-200);
|
|
303
|
+
}
|
|
304
|
+
.range::-webkit-slider-thumb {
|
|
305
|
+
appearance: none;
|
|
306
|
+
margin-block-start: -8px;
|
|
307
|
+
inline-size: 22px;
|
|
308
|
+
block-size: 22px;
|
|
309
|
+
border-radius: var(--r-full);
|
|
310
|
+
background: var(--brand-600);
|
|
311
|
+
border: 3px solid var(--ink-0);
|
|
312
|
+
box-shadow: var(--shadow-2);
|
|
313
|
+
cursor: grab;
|
|
314
|
+
}
|
|
315
|
+
.range::-moz-range-thumb {
|
|
316
|
+
inline-size: 22px;
|
|
317
|
+
block-size: 22px;
|
|
318
|
+
border-radius: var(--r-full);
|
|
319
|
+
background: var(--brand-600);
|
|
320
|
+
border: 3px solid var(--ink-0);
|
|
321
|
+
box-shadow: var(--shadow-2);
|
|
322
|
+
cursor: grab;
|
|
323
|
+
}
|
|
324
|
+
.range:active::-webkit-slider-thumb { cursor: grabbing; scale: 1.08; }
|
|
325
|
+
|
|
326
|
+
/* --- File --------------------------------------------------------------- */
|
|
327
|
+
/* A dashed drop zone wrapping a real file input. The input is 1px and
|
|
328
|
+
transparent rather than display: none, so it keeps its place in the
|
|
329
|
+
tab order, and :focus-within is what makes that focus visible. Deck
|
|
330
|
+
styles the zone; dropping needs a handler of your own. */
|
|
331
|
+
.file {
|
|
332
|
+
display: grid;
|
|
333
|
+
place-items: center;
|
|
334
|
+
gap: var(--space-2);
|
|
335
|
+
padding: var(--space-6);
|
|
336
|
+
border: 1.5px dashed var(--line-strong);
|
|
337
|
+
border-radius: var(--r-md);
|
|
338
|
+
background: var(--surface-2);
|
|
339
|
+
text-align: center;
|
|
340
|
+
color: var(--text-muted);
|
|
341
|
+
cursor: pointer;
|
|
342
|
+
transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
|
|
343
|
+
}
|
|
344
|
+
.file:hover, .file:focus-within { border-color: var(--brand-500); background: var(--brand-soft); }
|
|
345
|
+
.file input[type="file"] { position: absolute; opacity: 0; inline-size: 1px; block-size: 1px; }
|
|
346
|
+
|
|
347
|
+
/* --- Fieldset ----------------------------------------------------------- */
|
|
348
|
+
/* A bordered group of controls that answer one question. Its legend is
|
|
349
|
+
the group's accessible name, which is what a radio group needs and
|
|
350
|
+
what a .label above one does not provide. */
|
|
351
|
+
.fieldset {
|
|
352
|
+
border: 1px solid var(--line);
|
|
353
|
+
border-radius: var(--r-md);
|
|
354
|
+
padding: var(--space-5);
|
|
355
|
+
display: flex;
|
|
356
|
+
flex-direction: column;
|
|
357
|
+
gap: var(--space-4);
|
|
358
|
+
min-inline-size: 0;
|
|
359
|
+
}
|
|
360
|
+
.fieldset > legend {
|
|
361
|
+
padding-inline: var(--space-2);
|
|
362
|
+
font-size: var(--text-sm);
|
|
363
|
+
font-weight: 600;
|
|
364
|
+
color: var(--text-muted);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* Two fields side by side on anything wider than a phone */
|
|
368
|
+
.field-row {
|
|
369
|
+
display: grid;
|
|
370
|
+
gap: var(--space-4);
|
|
371
|
+
grid-template-columns: 1fr;
|
|
372
|
+
}
|
|
373
|
+
@media (min-width: 40rem) {
|
|
374
|
+
.field-row { grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* Sticky save bar for long forms on mobile */
|
|
378
|
+
.form-actions {
|
|
379
|
+
display: flex;
|
|
380
|
+
gap: var(--space-3);
|
|
381
|
+
flex-wrap: wrap;
|
|
382
|
+
}
|
|
383
|
+
/* Pins the action row to the bottom of the viewport with a blur behind
|
|
384
|
+
it and a safe-area inset for the home indicator. Add matching bottom
|
|
385
|
+
padding to the form, or the last field sits underneath it. */
|
|
386
|
+
.form-actions-sticky {
|
|
387
|
+
position: sticky;
|
|
388
|
+
inset-block-end: 0;
|
|
389
|
+
z-index: var(--z-sticky);
|
|
390
|
+
background: color-mix(in oklab, var(--bg) 88%, transparent);
|
|
391
|
+
backdrop-filter: blur(12px);
|
|
392
|
+
-webkit-backdrop-filter: blur(12px);
|
|
393
|
+
border-block-start: 1px solid var(--line);
|
|
394
|
+
padding-block: var(--space-3);
|
|
395
|
+
padding-block-end: calc(var(--space-3) + env(safe-area-inset-bottom));
|
|
396
|
+
}
|
|
397
|
+
.form-actions-sticky > .btn { flex: 1 1 auto; }
|
|
398
|
+
@media (min-width: 48rem) {
|
|
399
|
+
.form-actions-sticky > .btn { flex: 0 0 auto; }
|
|
400
|
+
}
|
|
401
|
+
}
|