@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,335 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Deck — 12 Data grid
|
|
3
|
+
A real table with a frozen header and pinned columns. Add .dg-pin-start to
|
|
4
|
+
the cells of a column you want frozen on the left, .dg-pin-end for the
|
|
5
|
+
right. Set --dg-pin-start to the pinned column's width.
|
|
6
|
+
========================================================================== */
|
|
7
|
+
|
|
8
|
+
@layer deck.components {
|
|
9
|
+
|
|
10
|
+
.dg-wrap {
|
|
11
|
+
--dg-pin-start: 0px;
|
|
12
|
+
--dg-pin-end: 0px;
|
|
13
|
+
--dg-row-h: 44px;
|
|
14
|
+
position: relative;
|
|
15
|
+
inline-size: 100%;
|
|
16
|
+
max-block-size: var(--dg-height, 70dvh);
|
|
17
|
+
overflow: auto;
|
|
18
|
+
overscroll-behavior: contain;
|
|
19
|
+
-webkit-overflow-scrolling: touch;
|
|
20
|
+
background: var(--surface);
|
|
21
|
+
border: 1px solid var(--line);
|
|
22
|
+
border-radius: var(--r-md);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* A real table with a frozen header and pinned columns. Use it when the
|
|
26
|
+
reader operates on data — sorts, selects, scans — rather than reads it;
|
|
27
|
+
.table is lighter for presentation. Must sit inside .dg-wrap, which
|
|
28
|
+
carries the scroll container and every custom property these rules read. */
|
|
29
|
+
.dg {
|
|
30
|
+
inline-size: 100%;
|
|
31
|
+
border-collapse: separate;
|
|
32
|
+
border-spacing: 0;
|
|
33
|
+
font-size: var(--text-sm);
|
|
34
|
+
font-variant-numeric: tabular-nums;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.dg :is(th, td) {
|
|
38
|
+
padding: 0 var(--space-4);
|
|
39
|
+
block-size: var(--dg-row-h);
|
|
40
|
+
text-align: start;
|
|
41
|
+
white-space: nowrap;
|
|
42
|
+
background: var(--surface);
|
|
43
|
+
border-block-end: 1px solid var(--line);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* --- Frozen header ------------------------------------------------------- */
|
|
47
|
+
.dg thead th {
|
|
48
|
+
position: sticky;
|
|
49
|
+
inset-block-start: 0;
|
|
50
|
+
z-index: 3;
|
|
51
|
+
background: var(--surface-2);
|
|
52
|
+
font-size: var(--text-xs);
|
|
53
|
+
font-weight: 620;
|
|
54
|
+
color: var(--text-muted);
|
|
55
|
+
border-block-end: 1px solid var(--line-strong);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* --- Pinned columns ------------------------------------------------------ */
|
|
59
|
+
.dg :is(th, td).dg-pin-start {
|
|
60
|
+
position: sticky;
|
|
61
|
+
inset-inline-start: 0;
|
|
62
|
+
z-index: 2;
|
|
63
|
+
}
|
|
64
|
+
.dg thead th.dg-pin-start { z-index: 4; }
|
|
65
|
+
|
|
66
|
+
.dg :is(th, td).dg-pin-start-2 {
|
|
67
|
+
position: sticky;
|
|
68
|
+
inset-inline-start: var(--dg-pin-start);
|
|
69
|
+
z-index: 2;
|
|
70
|
+
}
|
|
71
|
+
.dg thead th.dg-pin-start-2 { z-index: 4; }
|
|
72
|
+
|
|
73
|
+
.dg :is(th, td).dg-pin-end {
|
|
74
|
+
position: sticky;
|
|
75
|
+
inset-inline-end: 0;
|
|
76
|
+
z-index: 2;
|
|
77
|
+
}
|
|
78
|
+
.dg thead th.dg-pin-end { z-index: 4; }
|
|
79
|
+
|
|
80
|
+
/* The shadow only appears once the grid is actually scrolled sideways.
|
|
81
|
+
A box-shadow offset is physical, so the geometry lives here and 19-logical
|
|
82
|
+
mirrors it for RTL once, rather than once per branch below. */
|
|
83
|
+
.dg-wrap {
|
|
84
|
+
--dg-pin-shadow-start: 6px 0 8px -6px oklch(20% .03 var(--hue-neutral) / .28);
|
|
85
|
+
--dg-pin-shadow-end: -6px 0 8px -6px oklch(20% .03 var(--hue-neutral) / .28);
|
|
86
|
+
--dg-pin-clip-start: inset(0 -12px 0 0);
|
|
87
|
+
--dg-pin-clip-end: inset(0 0 0 -12px);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Ask the scroll container directly. No scroll handler, no class to keep in
|
|
91
|
+
sync, and the query is already logical so RTL needs nothing here. */
|
|
92
|
+
@supports (container-type: scroll-state) {
|
|
93
|
+
.dg-wrap { container-type: scroll-state; }
|
|
94
|
+
@container scroll-state(scrollable: inline-start) {
|
|
95
|
+
.dg :is(th, td).dg-pin-start:last-of-type,
|
|
96
|
+
.dg :is(th, td).dg-pin-start-2,
|
|
97
|
+
.dg :is(th, td).dg-pin-start:not(:has(~ .dg-pin-start-2)) {
|
|
98
|
+
box-shadow: var(--dg-pin-shadow-start);
|
|
99
|
+
clip-path: var(--dg-pin-clip-start);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
@container scroll-state(scrollable: inline-end) {
|
|
103
|
+
.dg :is(th, td).dg-pin-end {
|
|
104
|
+
box-shadow: var(--dg-pin-shadow-end);
|
|
105
|
+
clip-path: var(--dg-pin-clip-end);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Where it is missing, Grid.shadows() in deck.js toggles these two classes. */
|
|
111
|
+
@supports not (container-type: scroll-state) {
|
|
112
|
+
.dg-wrap.is-scrolled-x .dg :is(th, td).dg-pin-start:last-of-type,
|
|
113
|
+
.dg-wrap.is-scrolled-x .dg :is(th, td).dg-pin-start-2,
|
|
114
|
+
.dg-wrap.is-scrolled-x .dg :is(th, td).dg-pin-start:not(:has(~ .dg-pin-start-2)) {
|
|
115
|
+
box-shadow: var(--dg-pin-shadow-start);
|
|
116
|
+
clip-path: var(--dg-pin-clip-start);
|
|
117
|
+
}
|
|
118
|
+
.dg-wrap.is-scrolled-end .dg :is(th, td).dg-pin-end {
|
|
119
|
+
box-shadow: var(--dg-pin-shadow-end);
|
|
120
|
+
clip-path: var(--dg-pin-clip-end);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* --- Rows ---------------------------------------------------------------- */
|
|
125
|
+
.dg tbody tr:hover :is(td) { background: var(--surface-hover); }
|
|
126
|
+
.dg tbody tr:last-child td { border-block-end: 0; }
|
|
127
|
+
|
|
128
|
+
.dg tbody tr[aria-selected="true"] td {
|
|
129
|
+
background: var(--brand-soft);
|
|
130
|
+
color: var(--brand-soft-text);
|
|
131
|
+
}
|
|
132
|
+
.dg tbody tr[aria-selected="true"]:hover td {
|
|
133
|
+
background: color-mix(in oklab, var(--brand-soft) 80%, var(--brand-200));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Zebra is opt-in, and it never fights the pinned column background */
|
|
137
|
+
.dg-zebra tbody tr:nth-child(even) td { background: var(--surface-2); }
|
|
138
|
+
.dg-zebra tbody tr:nth-child(even):hover td { background: var(--surface-hover); }
|
|
139
|
+
|
|
140
|
+
.dg tbody tr.is-new td { animation: dg-flash 1.4s var(--ease-out); }
|
|
141
|
+
@keyframes dg-flash { from { background: var(--good-100); } }
|
|
142
|
+
|
|
143
|
+
/* --- Cell variants ------------------------------------------------------- */
|
|
144
|
+
/* A numeric column in the data grid: aligned to the end edge, and sorted
|
|
145
|
+
numerically when the header carries data-sort="num". */
|
|
146
|
+
/* A numeric column: aligned to the end edge, and sorted numerically when
|
|
147
|
+
the header carries data-sort="num". */
|
|
148
|
+
.dg .dg-num { text-align: end; }
|
|
149
|
+
/* A centred column. */
|
|
150
|
+
.dg .dg-center { text-align: center; }
|
|
151
|
+
.dg .dg-wrap-text { white-space: normal; min-inline-size: 16rem; }
|
|
152
|
+
.dg .dg-tight { padding-inline: var(--space-2); }
|
|
153
|
+
/* The selection column. 44px wide, and the one place Deck strips .check of
|
|
154
|
+
its min-block-size — a dense grid of 34px rows cannot also have 44px
|
|
155
|
+
checkboxes. The checkbox still needs its own accessible name. */
|
|
156
|
+
.dg .dg-check { inline-size: 44px; padding-inline: 0; text-align: center; }
|
|
157
|
+
.dg .dg-check .check { justify-content: center; min-block-size: auto; padding: 0; }
|
|
158
|
+
/* A column shrunk to its content, for a row menu or a pair of buttons. */
|
|
159
|
+
.dg .dg-actions { inline-size: 1%; padding-inline: var(--space-2); }
|
|
160
|
+
|
|
161
|
+
/* --- Density ------------------------------------------------------------- */
|
|
162
|
+
/* 34px rows and smaller text, for scanning many rows at once. */
|
|
163
|
+
.dg-compact { --dg-row-h: 34px; font-size: var(--text-xs); }
|
|
164
|
+
.dg-compact :is(th, td) { padding-inline: var(--space-3); }
|
|
165
|
+
/* 56px rows, for a grid with two lines of content per cell. */
|
|
166
|
+
.dg-comfy { --dg-row-h: 56px; }
|
|
167
|
+
|
|
168
|
+
/* --- Sortable headers ---------------------------------------------------- */
|
|
169
|
+
/* The button deck.js builds inside a th[data-sort]. The styling keys off
|
|
170
|
+
th[aria-sort], so the arrow direction and the announced sort state are
|
|
171
|
+
the same value and cannot drift apart. */
|
|
172
|
+
.dg-sort {
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: var(--space-2);
|
|
176
|
+
inline-size: 100%;
|
|
177
|
+
block-size: 100%;
|
|
178
|
+
color: inherit;
|
|
179
|
+
font: inherit;
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
user-select: none;
|
|
182
|
+
}
|
|
183
|
+
.dg-num .dg-sort { justify-content: flex-end; }
|
|
184
|
+
.dg-sort:hover { color: var(--text); }
|
|
185
|
+
.dg-sort .dg-sort-icon {
|
|
186
|
+
inline-size: 14px;
|
|
187
|
+
block-size: 14px;
|
|
188
|
+
opacity: 0;
|
|
189
|
+
flex: 0 0 auto;
|
|
190
|
+
transition: opacity var(--dur-1) var(--ease-out), rotate var(--dur-2) var(--ease-out);
|
|
191
|
+
}
|
|
192
|
+
.dg-sort:hover .dg-sort-icon { opacity: .4; }
|
|
193
|
+
th[aria-sort] .dg-sort { color: var(--brand); }
|
|
194
|
+
th[aria-sort] .dg-sort-icon { opacity: 1; }
|
|
195
|
+
th[aria-sort="descending"] .dg-sort-icon { rotate: 180deg; }
|
|
196
|
+
|
|
197
|
+
/* Column resize grip. deck.js writes the width to the th. */
|
|
198
|
+
.dg-resize {
|
|
199
|
+
position: absolute;
|
|
200
|
+
inset-block: 0;
|
|
201
|
+
inset-inline-end: -3px;
|
|
202
|
+
inline-size: 7px;
|
|
203
|
+
cursor: col-resize;
|
|
204
|
+
z-index: 1;
|
|
205
|
+
touch-action: none;
|
|
206
|
+
}
|
|
207
|
+
.dg-resize::after {
|
|
208
|
+
content: "";
|
|
209
|
+
position: absolute;
|
|
210
|
+
inset-block: 8px;
|
|
211
|
+
inset-inline-start: 3px;
|
|
212
|
+
inline-size: 1px;
|
|
213
|
+
background: var(--line-strong);
|
|
214
|
+
opacity: 0;
|
|
215
|
+
transition: opacity var(--dur-1) var(--ease-out);
|
|
216
|
+
}
|
|
217
|
+
.dg-resize:hover::after, .dg-wrap.is-resizing .dg-resize::after { opacity: 1; background: var(--brand); }
|
|
218
|
+
.dg thead th { position: sticky; }
|
|
219
|
+
.dg thead th:has(.dg-resize) { position: sticky; }
|
|
220
|
+
|
|
221
|
+
/* --- Footer totals ------------------------------------------------------- */
|
|
222
|
+
.dg tfoot td {
|
|
223
|
+
position: sticky;
|
|
224
|
+
inset-block-end: 0;
|
|
225
|
+
z-index: 3;
|
|
226
|
+
background: var(--surface-2);
|
|
227
|
+
border-block-start: 1px solid var(--line-strong);
|
|
228
|
+
border-block-end: 0;
|
|
229
|
+
font-weight: 640;
|
|
230
|
+
color: var(--text);
|
|
231
|
+
}
|
|
232
|
+
.dg tfoot td.dg-pin-start { z-index: 4; }
|
|
233
|
+
|
|
234
|
+
/* --- Toolbar and status bar ---------------------------------------------- */
|
|
235
|
+
.dg-toolbar {
|
|
236
|
+
display: flex;
|
|
237
|
+
align-items: center;
|
|
238
|
+
gap: var(--space-2);
|
|
239
|
+
flex-wrap: wrap;
|
|
240
|
+
padding-block-end: var(--space-3);
|
|
241
|
+
}
|
|
242
|
+
.dg-toolbar .search { flex: 1 1 14rem; }
|
|
243
|
+
|
|
244
|
+
.dg-statusbar {
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: center;
|
|
247
|
+
gap: var(--space-3);
|
|
248
|
+
flex-wrap: wrap;
|
|
249
|
+
padding-block-start: var(--space-3);
|
|
250
|
+
font-size: var(--text-sm);
|
|
251
|
+
color: var(--text-muted);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* Selection bar that slides in once rows are checked */
|
|
255
|
+
.dg-selection {
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: var(--space-3);
|
|
259
|
+
padding: var(--space-2) var(--space-3);
|
|
260
|
+
border-radius: var(--r-sm);
|
|
261
|
+
background: var(--brand-soft);
|
|
262
|
+
color: var(--brand-soft-text);
|
|
263
|
+
font-size: var(--text-sm);
|
|
264
|
+
font-weight: 560;
|
|
265
|
+
animation: dk-combo-in var(--dur-2) var(--ease-out);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* --- Loading and empty --------------------------------------------------- */
|
|
269
|
+
.dg-wrap.is-loading { pointer-events: none; }
|
|
270
|
+
.dg-wrap.is-loading .dg tbody { opacity: .45; }
|
|
271
|
+
.dg-wrap.is-loading::after {
|
|
272
|
+
content: "";
|
|
273
|
+
position: absolute;
|
|
274
|
+
inset-block-start: 0;
|
|
275
|
+
inset-inline: 0;
|
|
276
|
+
block-size: 2px;
|
|
277
|
+
background: linear-gradient(90deg, transparent, var(--brand-500), transparent);
|
|
278
|
+
background-size: 40% 100%;
|
|
279
|
+
background-repeat: no-repeat;
|
|
280
|
+
animation: dg-bar 1.1s var(--ease-in-out) infinite;
|
|
281
|
+
z-index: 5;
|
|
282
|
+
}
|
|
283
|
+
@keyframes dg-bar {
|
|
284
|
+
from { background-position: -40% 0; }
|
|
285
|
+
to { background-position: 140% 0; }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* The row that holds an empty state, with its height and padding released
|
|
289
|
+
so the state can be any size. */
|
|
290
|
+
.dg-empty td { block-size: auto; padding: 0; }
|
|
291
|
+
|
|
292
|
+
/* --- Phone: swap to cards ------------------------------------------------
|
|
293
|
+
Add .dg-cards and put the column name in data-label on each td.
|
|
294
|
+
---------------------------------------------------------------------- */
|
|
295
|
+
@media (max-width: 44rem) {
|
|
296
|
+
.dg-cards-wrap { max-block-size: none; overflow: visible; border: 0; background: none; }
|
|
297
|
+
.dg-cards thead, .dg-cards tfoot { display: none; }
|
|
298
|
+
/* Below a breakpoint, restacks each row into a labelled card, taking the
|
|
299
|
+
column name from data-label on the cell. Costs the column alignment that
|
|
300
|
+
makes a grid comparable; prefer .dg-cq, which asks the container rather
|
|
301
|
+
than the viewport. */
|
|
302
|
+
.dg-cards, .dg-cards tbody, .dg-cards tr, .dg-cards td { display: block; inline-size: 100%; }
|
|
303
|
+
.dg-cards tr {
|
|
304
|
+
background: var(--surface);
|
|
305
|
+
border: 1px solid var(--line);
|
|
306
|
+
border-radius: var(--r-md);
|
|
307
|
+
margin-block-end: var(--space-3);
|
|
308
|
+
overflow: hidden;
|
|
309
|
+
}
|
|
310
|
+
.dg-cards td {
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: space-between;
|
|
314
|
+
gap: var(--space-4);
|
|
315
|
+
block-size: auto;
|
|
316
|
+
min-block-size: var(--tap);
|
|
317
|
+
padding: var(--space-2) var(--space-4);
|
|
318
|
+
white-space: normal;
|
|
319
|
+
text-align: end;
|
|
320
|
+
position: static !important;
|
|
321
|
+
box-shadow: none !important;
|
|
322
|
+
clip-path: none !important;
|
|
323
|
+
}
|
|
324
|
+
.dg-cards td::before {
|
|
325
|
+
content: attr(data-label);
|
|
326
|
+
font-size: var(--text-xs);
|
|
327
|
+
font-weight: 620;
|
|
328
|
+
color: var(--text-muted);
|
|
329
|
+
text-align: start;
|
|
330
|
+
flex: 0 0 auto;
|
|
331
|
+
}
|
|
332
|
+
.dg-cards td:empty { display: none; }
|
|
333
|
+
.dg-cards td.dg-num { text-align: end; }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
Deck — 13 Toasts
|
|
3
|
+
A stack, not a list. Collapsed toasts sit behind the newest one and fan out
|
|
4
|
+
on hover or focus. Queue logic is in deck.js (Deck.toast).
|
|
5
|
+
========================================================================== */
|
|
6
|
+
|
|
7
|
+
@layer deck.components {
|
|
8
|
+
|
|
9
|
+
/* The fixed container. pointer-events: none on the region and auto on each
|
|
10
|
+
toast, so the empty area does not swallow clicks on the page beneath. Sits
|
|
11
|
+
above a .tabbar when the page has one, and clears the home indicator. */
|
|
12
|
+
.toast-region {
|
|
13
|
+
position: fixed;
|
|
14
|
+
z-index: var(--z-toast);
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: var(--space-2);
|
|
18
|
+
inline-size: min(23rem, calc(100vw - var(--space-6)));
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
/* the stack grows upward from the bottom by default */
|
|
21
|
+
inset-block-end: calc(var(--space-4) + env(safe-area-inset-bottom));
|
|
22
|
+
inset-inline-end: var(--space-4);
|
|
23
|
+
}
|
|
24
|
+
body:has(.tabbar) .toast-region {
|
|
25
|
+
inset-block-end: calc(var(--space-4) + var(--tap) + env(safe-area-inset-bottom));
|
|
26
|
+
}
|
|
27
|
+
@media (min-width: 48rem) {
|
|
28
|
+
body:has(.tabbar) .toast-region { inset-block-end: calc(var(--space-4) + env(safe-area-inset-bottom)); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.toast-region-start { inset-inline: var(--space-4) auto; }
|
|
32
|
+
.toast-region-center { inset-inline: 0; margin-inline: auto; }
|
|
33
|
+
.toast-region-top {
|
|
34
|
+
inset-block: calc(var(--space-4) + env(safe-area-inset-top)) auto;
|
|
35
|
+
flex-direction: column-reverse;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* On a phone the stack spans the width and sits above the thumb */
|
|
39
|
+
@media (max-width: 30rem) {
|
|
40
|
+
.toast-region { inset-inline: var(--space-3); inline-size: auto; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* --- The toast ----------------------------------------------------------- */
|
|
44
|
+
/* One notification. The accent colour is a single custom property,
|
|
45
|
+
--toast-accent, which paints the edge, the icon and the timer together, so
|
|
46
|
+
a kind is one declaration. Built by Deck.toast() rather than written by
|
|
47
|
+
hand; touch-action: pan-y lets a horizontal swipe dismiss it while a
|
|
48
|
+
vertical drag still scrolls the page. */
|
|
49
|
+
.toast {
|
|
50
|
+
--toast-accent: var(--ink-400);
|
|
51
|
+
position: relative;
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: flex-start;
|
|
54
|
+
gap: var(--space-3);
|
|
55
|
+
padding: var(--space-3) var(--space-4);
|
|
56
|
+
background: var(--surface);
|
|
57
|
+
color: var(--text);
|
|
58
|
+
border: 1px solid var(--line);
|
|
59
|
+
border-radius: var(--r-md);
|
|
60
|
+
box-shadow: var(--shadow-4);
|
|
61
|
+
font-size: var(--text-sm);
|
|
62
|
+
pointer-events: auto;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
touch-action: pan-y;
|
|
65
|
+
transform-origin: bottom center;
|
|
66
|
+
transition: translate var(--dur-3) var(--ease-out),
|
|
67
|
+
scale var(--dur-3) var(--ease-out),
|
|
68
|
+
opacity var(--dur-2) var(--ease-out),
|
|
69
|
+
margin var(--dur-3) var(--ease-out);
|
|
70
|
+
}
|
|
71
|
+
.toast::before {
|
|
72
|
+
content: "";
|
|
73
|
+
position: absolute;
|
|
74
|
+
inset-block: 0;
|
|
75
|
+
inset-inline-start: 0;
|
|
76
|
+
inline-size: 3px;
|
|
77
|
+
background: var(--toast-accent);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* The leading glyph, coloured by --toast-accent so it matches the kind
|
|
81
|
+
without a rule of its own. */
|
|
82
|
+
.toast-icon { color: var(--toast-accent); margin-block-start: 1px; flex: 0 0 auto; }
|
|
83
|
+
/* The text column of a toast. min-inline-size: 0 so a long unbroken string
|
|
84
|
+
cannot push the close button off the end. */
|
|
85
|
+
.toast-main { flex: 1 1 auto; min-inline-size: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
86
|
+
.toast-title { font-weight: 620; }
|
|
87
|
+
.toast-text { color: var(--text-muted); }
|
|
88
|
+
.toast-close {
|
|
89
|
+
flex: 0 0 auto;
|
|
90
|
+
inline-size: 26px;
|
|
91
|
+
block-size: 26px;
|
|
92
|
+
display: grid;
|
|
93
|
+
place-items: center;
|
|
94
|
+
border-radius: var(--r-full);
|
|
95
|
+
color: var(--text-faint);
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
margin-block-start: -2px;
|
|
98
|
+
margin-inline-end: -4px;
|
|
99
|
+
}
|
|
100
|
+
.toast-close:hover { background: var(--surface-hover); color: var(--text); }
|
|
101
|
+
.toast-actions { display: flex; gap: var(--space-2); margin-block-start: var(--space-2); }
|
|
102
|
+
.toast-action {
|
|
103
|
+
font-size: var(--text-sm);
|
|
104
|
+
font-weight: 620;
|
|
105
|
+
color: var(--brand);
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
padding: var(--space-1) 0;
|
|
108
|
+
}
|
|
109
|
+
.toast-action:hover { text-decoration: underline; }
|
|
110
|
+
|
|
111
|
+
/* --- Kinds --------------------------------------------------------------- */
|
|
112
|
+
.toast-good { --toast-accent: var(--good-500); }
|
|
113
|
+
.toast-warn { --toast-accent: var(--warn-500); }
|
|
114
|
+
.toast-bad { --toast-accent: var(--bad-500); }
|
|
115
|
+
.toast-info { --toast-accent: var(--brand-500); }
|
|
116
|
+
.toast-loading { --toast-accent: var(--brand-500); }
|
|
117
|
+
|
|
118
|
+
/* --- Auto-dismiss timer -------------------------------------------------- */
|
|
119
|
+
/* The auto-dismiss bar. A CSS animation rather than a JavaScript countdown,
|
|
120
|
+
which is what allows hovering or focusing the region to pause every timer
|
|
121
|
+
in it with one declaration — so a reader moving toward an Undo button does
|
|
122
|
+
not have it vanish as they arrive. */
|
|
123
|
+
.toast-timer {
|
|
124
|
+
position: absolute;
|
|
125
|
+
inset-block-end: 0;
|
|
126
|
+
inset-inline-start: 0;
|
|
127
|
+
block-size: 2px;
|
|
128
|
+
inline-size: 100%;
|
|
129
|
+
background: var(--toast-accent);
|
|
130
|
+
opacity: .5;
|
|
131
|
+
transform-origin: left center;
|
|
132
|
+
animation: dk-toast-timer linear forwards;
|
|
133
|
+
animation-duration: var(--toast-duration, 5s);
|
|
134
|
+
}
|
|
135
|
+
@keyframes dk-toast-timer { to { scale: 0 1; } }
|
|
136
|
+
/* Hovering the stack pauses every timer in it */
|
|
137
|
+
.toast-region:is(:hover, :focus-within) .toast-timer { animation-play-state: paused; }
|
|
138
|
+
|
|
139
|
+
/* --- Enter and exit ------------------------------------------------------ */
|
|
140
|
+
.toast { animation: dk-toast-in var(--dur-3) var(--ease-spring); }
|
|
141
|
+
@keyframes dk-toast-in {
|
|
142
|
+
from { opacity: 0; translate: 0 16px; scale: .94; }
|
|
143
|
+
}
|
|
144
|
+
.toast-region-top .toast { animation-name: dk-toast-in-top; }
|
|
145
|
+
@keyframes dk-toast-in-top {
|
|
146
|
+
from { opacity: 0; translate: 0 -16px; scale: .94; }
|
|
147
|
+
}
|
|
148
|
+
.toast.is-leaving {
|
|
149
|
+
opacity: 0;
|
|
150
|
+
scale: .94;
|
|
151
|
+
translate: var(--toast-exit-x, 0) 8px;
|
|
152
|
+
margin-block-end: calc((var(--toast-h, 60px) + var(--space-2)) * -1);
|
|
153
|
+
pointer-events: none;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* --- Collapsed stack -----------------------------------------------------
|
|
157
|
+
--i counts from the newest, which is the first child because push()
|
|
158
|
+
prepends. Beyond the third toast they hide behind.
|
|
159
|
+
---------------------------------------------------------------------- */
|
|
160
|
+
@supports (animation-delay: calc(sibling-index() * 1ms)) {
|
|
161
|
+
.toast-region-stacked .toast { --i: calc(sibling-index() - 1); }
|
|
162
|
+
}
|
|
163
|
+
@supports not (animation-delay: calc(sibling-index() * 1ms)) {
|
|
164
|
+
.toast-region-stacked .toast:nth-child(1) { --i: 0; }
|
|
165
|
+
.toast-region-stacked .toast:nth-child(2) { --i: 1; }
|
|
166
|
+
.toast-region-stacked .toast:nth-child(3) { --i: 2; }
|
|
167
|
+
.toast-region-stacked .toast:nth-child(4) { --i: 3; }
|
|
168
|
+
.toast-region-stacked .toast:nth-child(5) { --i: 4; }
|
|
169
|
+
.toast-region-stacked .toast:nth-child(6) { --i: 5; }
|
|
170
|
+
.toast-region-stacked .toast:nth-child(7) { --i: 6; }
|
|
171
|
+
.toast-region-stacked .toast:nth-child(8) { --i: 7; }
|
|
172
|
+
}
|
|
173
|
+
.toast-region-stacked .toast {
|
|
174
|
+
position: absolute;
|
|
175
|
+
inset-inline: 0;
|
|
176
|
+
inset-block-end: 0;
|
|
177
|
+
translate: 0 calc(var(--i) * -8px);
|
|
178
|
+
scale: calc(1 - var(--i) * .04);
|
|
179
|
+
opacity: calc(1 - var(--i) * .18);
|
|
180
|
+
z-index: calc(100 - var(--i));
|
|
181
|
+
}
|
|
182
|
+
.toast-region-stacked .toast:nth-child(n+4) { opacity: 0; pointer-events: none; }
|
|
183
|
+
|
|
184
|
+
.toast-region-stacked:is(:hover, :focus-within) .toast {
|
|
185
|
+
translate: 0 calc(var(--i) * (var(--toast-h, 68px) + var(--space-2)) * -1);
|
|
186
|
+
scale: 1;
|
|
187
|
+
opacity: 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* --- Swipe to dismiss ---------------------------------------------------- */
|
|
191
|
+
.toast.is-dragging { transition: none; cursor: grabbing; user-select: none; }
|
|
192
|
+
|
|
193
|
+
/* --- Undo pattern -------------------------------------------------------- */
|
|
194
|
+
.toast-undo { --toast-accent: var(--ink-500); }
|
|
195
|
+
.toast-undo .toast-action { color: var(--text); }
|
|
196
|
+
|
|
197
|
+
@media (prefers-reduced-motion: reduce) {
|
|
198
|
+
.toast, .toast-region-stacked .toast { animation: none; transition: opacity var(--dur-1) linear; }
|
|
199
|
+
.toast-timer { animation: none; display: none; }
|
|
200
|
+
}
|
|
201
|
+
}
|