@netlify/spark-ui 1.26.0-alpha.6 → 1.26.0-alpha.8
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/dist/assets/DropZone.css +1 -1
- package/dist/components/FilterableGrid/filter-types.d.ts +40 -0
- package/dist/components/FilterableGrid/index.d.ts +2 -0
- package/dist/components/preact/DropZone/DropZone.d.ts +29 -7
- package/dist/components/preact/DropZone/DropZone.js +101 -128
- package/dist/components/preact/DropZone/useDropDeploy.d.ts +50 -0
- package/dist/components/preact/DropZone/useDropDeploy.js +64 -0
- package/package.json +4 -1
- package/packages/components/Button.astro +3 -0
- package/packages/components/FilterableGrid/FilterableGrid.astro +403 -0
- package/packages/components/FilterableGrid/filter-types.ts +48 -0
- package/packages/components/FilterableGrid/index.ts +2 -0
- package/packages/components/preact/DropZone/DropZone.css +52 -91
- package/packages/components/preact/DropZone/DropZone.tsx +153 -159
- package/packages/components/preact/DropZone/useDropDeploy.ts +149 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { FilterableGridProps } from './filter-types';
|
|
3
|
+
import Button from '../Button.astro';
|
|
4
|
+
|
|
5
|
+
const { filters, results, totalCount, matchMode, id } = Astro.props as FilterableGridProps;
|
|
6
|
+
|
|
7
|
+
const filterModeAttrs = Object.fromEntries(
|
|
8
|
+
filters
|
|
9
|
+
.filter(category => category.matchMode)
|
|
10
|
+
.map(category => [`filter-mode-${category.key}`, category.matchMode])
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const initialResultsLabel = `${totalCount} ${totalCount === 1 ? results.singular : results.plural}`;
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<div id={id} data-filter-root>
|
|
17
|
+
{
|
|
18
|
+
filters.length > 0 && (
|
|
19
|
+
<filter-container oninit filter-mode={matchMode} {...filterModeAttrs}>
|
|
20
|
+
<form class="n-filter-form" data-filter-form>
|
|
21
|
+
<details-utils
|
|
22
|
+
class="n-filter-dropdowns"
|
|
23
|
+
force-close="(min-width: 37.5em)"
|
|
24
|
+
force-restore
|
|
25
|
+
close-click-outside="(min-width: 37.5em)"
|
|
26
|
+
animate
|
|
27
|
+
>
|
|
28
|
+
{filters.map(category => (
|
|
29
|
+
<details class="n-filter-facet">
|
|
30
|
+
<summary>
|
|
31
|
+
<span class="n-filter-label">
|
|
32
|
+
{category.label}
|
|
33
|
+
<span class="n-filter-count" data-filter-count-badge data-filter-category={category.key}>
|
|
34
|
+
0
|
|
35
|
+
</span>
|
|
36
|
+
</span>
|
|
37
|
+
<svg
|
|
38
|
+
class="n-filter-chevron-icon"
|
|
39
|
+
viewBox="0 0 512 512"
|
|
40
|
+
fill="none"
|
|
41
|
+
role="presentation"
|
|
42
|
+
>
|
|
43
|
+
<path
|
|
44
|
+
d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"
|
|
45
|
+
fill="currentcolor"
|
|
46
|
+
/>
|
|
47
|
+
</svg>
|
|
48
|
+
</summary>
|
|
49
|
+
<fieldset class="n-filter-menu">
|
|
50
|
+
<legend class="n-filter-legend">{category.legend ?? `${category.label}:`}</legend>
|
|
51
|
+
{category.control === 'radio' && category.anyOption && (
|
|
52
|
+
<label>
|
|
53
|
+
<input
|
|
54
|
+
type="radio"
|
|
55
|
+
name={category.key}
|
|
56
|
+
value=""
|
|
57
|
+
data-filter-key={category.key}
|
|
58
|
+
checked
|
|
59
|
+
/>
|
|
60
|
+
{category.anyOption.label}
|
|
61
|
+
</label>
|
|
62
|
+
)}
|
|
63
|
+
{category.options.map(option => (
|
|
64
|
+
<label>
|
|
65
|
+
<input
|
|
66
|
+
type={category.control === 'radio' ? 'radio' : 'checkbox'}
|
|
67
|
+
name={category.key}
|
|
68
|
+
value={option.value}
|
|
69
|
+
data-filter-key={category.key}
|
|
70
|
+
/>
|
|
71
|
+
{option.label}
|
|
72
|
+
</label>
|
|
73
|
+
))}
|
|
74
|
+
</fieldset>
|
|
75
|
+
</details>
|
|
76
|
+
))}
|
|
77
|
+
</details-utils>
|
|
78
|
+
</form>
|
|
79
|
+
|
|
80
|
+
<div class="n-filter-meta">
|
|
81
|
+
<p
|
|
82
|
+
class="n-filter-results"
|
|
83
|
+
data-filter-results={`${results.singular}/${results.plural}`}
|
|
84
|
+
aria-live="polite"
|
|
85
|
+
>
|
|
86
|
+
{initialResultsLabel}
|
|
87
|
+
</p>
|
|
88
|
+
<ul class="n-filter-tags" role="list" data-filter-tags>
|
|
89
|
+
{filters.flatMap(category =>
|
|
90
|
+
category.options.map(option => (
|
|
91
|
+
<li
|
|
92
|
+
class="n-filter-tag"
|
|
93
|
+
data-tag-key={category.key}
|
|
94
|
+
data-tag-value={option.value}
|
|
95
|
+
hidden
|
|
96
|
+
>
|
|
97
|
+
<Button
|
|
98
|
+
type="button"
|
|
99
|
+
variant="outline"
|
|
100
|
+
data-tag-key={category.key}
|
|
101
|
+
data-tag-value={option.value}
|
|
102
|
+
>
|
|
103
|
+
{option.label}
|
|
104
|
+
<svg class="n-close-icon" viewBox="0 0 512 512" fill="none" role="presentation">
|
|
105
|
+
<path
|
|
106
|
+
d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3l105.4 105.3c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256z"
|
|
107
|
+
fill="currentcolor"
|
|
108
|
+
/>
|
|
109
|
+
</svg>
|
|
110
|
+
</Button>
|
|
111
|
+
</li>
|
|
112
|
+
))
|
|
113
|
+
)}
|
|
114
|
+
</ul>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<slot />
|
|
118
|
+
</filter-container>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
{filters.length === 0 && <slot />}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<script src="@zachleat/filter-container/filter-container.js"></script>
|
|
125
|
+
<script src="@zachleat/details-utils/details-utils.js"></script>
|
|
126
|
+
<script>
|
|
127
|
+
function initFilterableGrid(root: HTMLElement): void {
|
|
128
|
+
const form = root.querySelector<HTMLFormElement>('[data-filter-form]');
|
|
129
|
+
if (!form) return;
|
|
130
|
+
|
|
131
|
+
const tagsList = root.querySelector<HTMLUListElement>('[data-filter-tags]');
|
|
132
|
+
|
|
133
|
+
const updateCount = (key: string) => {
|
|
134
|
+
const count = Array.from(
|
|
135
|
+
form.querySelectorAll<HTMLInputElement>(`[data-filter-key="${CSS.escape(key)}"]:checked`)
|
|
136
|
+
).filter(entry => !!entry.value).length;
|
|
137
|
+
const counter = root.querySelector<HTMLElement>(
|
|
138
|
+
`[data-filter-count-badge][data-filter-category="${CSS.escape(key)}"]`
|
|
139
|
+
);
|
|
140
|
+
if (counter) {
|
|
141
|
+
counter.textContent = String(count);
|
|
142
|
+
counter.classList.toggle('is-visible', count > 0);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const showChip = (chipLi: HTMLLIElement) => {
|
|
147
|
+
if (!tagsList) return;
|
|
148
|
+
chipLi.hidden = false;
|
|
149
|
+
tagsList.appendChild(chipLi);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const hideChip = (chipLi: HTMLLIElement) => {
|
|
153
|
+
chipLi.hidden = true;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const findChip = (key: string, value: string): HTMLLIElement | null =>
|
|
157
|
+
tagsList?.querySelector<HTMLLIElement>(
|
|
158
|
+
`li[data-tag-key="${CSS.escape(key)}"][data-tag-value="${CSS.escape(value)}"]`
|
|
159
|
+
) ?? null;
|
|
160
|
+
|
|
161
|
+
const onCheckboxInput = (key: string, value: string, checked: boolean) => {
|
|
162
|
+
const chip = findChip(key, value);
|
|
163
|
+
if (!chip) return;
|
|
164
|
+
if (checked) showChip(chip);
|
|
165
|
+
else hideChip(chip);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const onRadioInput = (key: string, value: string) => {
|
|
169
|
+
if (!tagsList) return;
|
|
170
|
+
tagsList
|
|
171
|
+
.querySelectorAll<HTMLLIElement>(`li[data-tag-key="${CSS.escape(key)}"]`)
|
|
172
|
+
.forEach(chip => {
|
|
173
|
+
if (value && chip.dataset.tagValue === value) showChip(chip);
|
|
174
|
+
else hideChip(chip);
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const onTagInput = (input: HTMLInputElement) => {
|
|
179
|
+
if (!tagsList) return;
|
|
180
|
+
const key = input.dataset.filterKey;
|
|
181
|
+
if (!key) return;
|
|
182
|
+
if (input.type === 'radio') onRadioInput(key, input.value);
|
|
183
|
+
else onCheckboxInput(key, input.value, input.checked);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
form.addEventListener('input', event => {
|
|
187
|
+
if (!(event.target instanceof HTMLInputElement)) return;
|
|
188
|
+
const key = event.target.dataset.filterKey;
|
|
189
|
+
if (key) updateCount(key);
|
|
190
|
+
if (tagsList) onTagInput(event.target);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
tagsList?.addEventListener('click', event => {
|
|
194
|
+
const button = (event.target as HTMLElement).closest('button');
|
|
195
|
+
if (!button || !tagsList.contains(button)) return;
|
|
196
|
+
const key = button.dataset.tagKey;
|
|
197
|
+
const value = button.dataset.tagValue;
|
|
198
|
+
if (!key || value === undefined) return;
|
|
199
|
+
const input = form.querySelector<HTMLInputElement>(
|
|
200
|
+
`input[name="${CSS.escape(key)}"][value="${CSS.escape(value)}"]`
|
|
201
|
+
);
|
|
202
|
+
if (!input) return;
|
|
203
|
+
if (input.type === 'radio') {
|
|
204
|
+
const resetRadio = form.querySelector<HTMLInputElement>(
|
|
205
|
+
`input[type="radio"][name="${CSS.escape(key)}"][value=""]`
|
|
206
|
+
);
|
|
207
|
+
resetRadio?.dispatchEvent(
|
|
208
|
+
new MouseEvent('click', { view: window, bubbles: true, cancelable: true })
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
input.dispatchEvent(
|
|
212
|
+
new MouseEvent('click', { view: window, bubbles: true, cancelable: true })
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Seed counts/chips from state restored by filter-container's `oninit` (e.g. from URL params).
|
|
218
|
+
window.addEventListener('load', () => {
|
|
219
|
+
const checked = form.querySelectorAll<HTMLInputElement>('[data-filter-key]:checked');
|
|
220
|
+
checked.forEach(input => {
|
|
221
|
+
const key = input.dataset.filterKey;
|
|
222
|
+
if (key) updateCount(key);
|
|
223
|
+
if (tagsList) onTagInput(input);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const filterRoot = document.querySelector<HTMLElement>('[data-filter-root]');
|
|
229
|
+
if (filterRoot) initFilterableGrid(filterRoot);
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<style>
|
|
233
|
+
.n-filter-form {
|
|
234
|
+
display: flex;
|
|
235
|
+
justify-content: center;
|
|
236
|
+
margin-block-end: var(--ntl-space-s);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.n-filter-dropdowns {
|
|
240
|
+
display: flex;
|
|
241
|
+
flex-wrap: wrap;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
width: 100%;
|
|
244
|
+
border-radius: var(--ntl-radius-m);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@media (min-width: 37.5em) {
|
|
248
|
+
.n-filter-dropdowns {
|
|
249
|
+
display: inline-flex;
|
|
250
|
+
width: auto;
|
|
251
|
+
gap: var(--ntl-space-s);
|
|
252
|
+
padding: var(--ntl-space-s);
|
|
253
|
+
background-color: var(--ntl-color-bg-2);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.n-filter-facet {
|
|
258
|
+
width: 100%;
|
|
259
|
+
border-top: 1px solid var(--ntl-color-border-strong);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.n-filter-facet:last-child {
|
|
263
|
+
border-bottom: 1px solid var(--ntl-color-border-strong);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@media (min-width: 37.5em) {
|
|
267
|
+
.n-filter-facet,
|
|
268
|
+
.n-filter-facet:last-child {
|
|
269
|
+
width: auto;
|
|
270
|
+
border: none;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.n-filter-facet > summary {
|
|
275
|
+
position: relative;
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
justify-content: space-between;
|
|
279
|
+
gap: var(--ntl-space-2xs);
|
|
280
|
+
padding-block: var(--ntl-space-2xs);
|
|
281
|
+
color: var(--ntl-heading-text-color);
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
list-style: none;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.n-filter-facet > summary::-webkit-details-marker {
|
|
287
|
+
display: none;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@media (min-width: 37.5em) {
|
|
291
|
+
.n-filter-facet > summary {
|
|
292
|
+
padding: var(--ntl-space-2xs) var(--ntl-space-s);
|
|
293
|
+
border: 1px solid var(--ntl-color-border-strong);
|
|
294
|
+
border-radius: var(--ntl-radius-m);
|
|
295
|
+
background-color: var(--ntl-color-bg);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.n-filter-facet[open] > summary {
|
|
299
|
+
border-color: var(--ntl-blue-700);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.n-filter-label {
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
gap: var(--ntl-space-3xs);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.n-filter-chevron-icon {
|
|
310
|
+
width: 0.7em;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.n-filter-count {
|
|
314
|
+
display: none;
|
|
315
|
+
align-items: center;
|
|
316
|
+
justify-content: center;
|
|
317
|
+
width: 1.5em;
|
|
318
|
+
height: 1.5em;
|
|
319
|
+
border-radius: 50%;
|
|
320
|
+
background: var(--ntl-blue-700);
|
|
321
|
+
color: var(--ntl-neutral-000);
|
|
322
|
+
font-size: 0.75em;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.n-filter-facet[open] .n-filter-count,
|
|
326
|
+
.n-filter-facet:not([open]) .n-filter-count.is-visible {
|
|
327
|
+
display: inline-flex;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@media (min-width: 37.5em) {
|
|
331
|
+
.n-filter-count {
|
|
332
|
+
position: absolute;
|
|
333
|
+
top: -0.5em;
|
|
334
|
+
right: -0.75em;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.n-filter-menu {
|
|
339
|
+
display: flex;
|
|
340
|
+
flex-direction: column;
|
|
341
|
+
padding-block: var(--ntl-space-2xs) var(--ntl-space-s);
|
|
342
|
+
border: none;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.n-filter-menu label {
|
|
346
|
+
display: flex;
|
|
347
|
+
align-items: center;
|
|
348
|
+
white-space: nowrap;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.n-filter-menu label:not(:last-child) {
|
|
352
|
+
margin-block-end: var(--ntl-space-2xs);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.n-filter-menu input[type='checkbox'],
|
|
356
|
+
.n-filter-menu input[type='radio'] {
|
|
357
|
+
margin-inline-end: var(--ntl-space-2xs);
|
|
358
|
+
accent-color: var(--ntl-blue-700);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.n-filter-legend {
|
|
362
|
+
position: absolute;
|
|
363
|
+
width: 1px;
|
|
364
|
+
height: 1px;
|
|
365
|
+
padding: 0;
|
|
366
|
+
margin: -1px;
|
|
367
|
+
overflow: hidden;
|
|
368
|
+
clip: rect(0, 0, 0, 0);
|
|
369
|
+
white-space: nowrap;
|
|
370
|
+
border: 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@media (min-width: 37.5em) {
|
|
374
|
+
.n-filter-menu {
|
|
375
|
+
position: absolute;
|
|
376
|
+
z-index: 1;
|
|
377
|
+
padding: var(--ntl-space-s);
|
|
378
|
+
border-radius: var(--ntl-radius-m);
|
|
379
|
+
background-color: var(--ntl-color-bg);
|
|
380
|
+
box-shadow: var(--ntl-shadow-deep, 0 0.5rem 1.5rem rgba(0, 0, 0, 0.2));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.n-filter-meta {
|
|
385
|
+
display: flex;
|
|
386
|
+
flex-direction: column;
|
|
387
|
+
align-items: center;
|
|
388
|
+
gap: var(--ntl-space-s);
|
|
389
|
+
margin-block-end: var(--ntl-space-l);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.n-filter-results {
|
|
393
|
+
color: var(--ntl-color-text-weak);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.n-filter-tags {
|
|
397
|
+
display: flex;
|
|
398
|
+
flex-wrap: wrap;
|
|
399
|
+
justify-content: center;
|
|
400
|
+
gap: var(--ntl-space-s);
|
|
401
|
+
list-style: none;
|
|
402
|
+
}
|
|
403
|
+
</style>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface FilterOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface FilterCategory {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
legend?: string;
|
|
10
|
+
control?: 'checkbox' | 'radio';
|
|
11
|
+
/** Radio-only: prepend a checked `value=""` reset row. */
|
|
12
|
+
anyOption?: { label: string };
|
|
13
|
+
matchMode?: 'any' | 'all';
|
|
14
|
+
options: FilterOption[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FilterableGridProps {
|
|
18
|
+
filters: FilterCategory[];
|
|
19
|
+
results: { singular: string; plural: string };
|
|
20
|
+
/** Server-rendered initial count, shown before any client JS runs. */
|
|
21
|
+
totalCount: number;
|
|
22
|
+
/** Global match mode; per-category `matchMode` overrides it for that category. */
|
|
23
|
+
matchMode: 'any' | 'all';
|
|
24
|
+
id?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds the `data-filter-{key}` attributes a slotted grid item needs so
|
|
29
|
+
* `<filter-container>` can match it against the active facets. Spread the
|
|
30
|
+
* result onto whatever bespoke element you render inside <FilterableGrid>.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* <article {...getFilterAttrs({ topic: ['react', 'astro'], level: 'beginner' })}>
|
|
34
|
+
* ...
|
|
35
|
+
* </article>
|
|
36
|
+
* // -> data-filter-topic="react,astro" data-filter-level="beginner"
|
|
37
|
+
*/
|
|
38
|
+
export function getFilterAttrs(
|
|
39
|
+
values: Record<string, string | string[]>,
|
|
40
|
+
delimiter = ','
|
|
41
|
+
): Record<`data-filter-${string}`, string> {
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.entries(values).map(([key, value]) => [
|
|
44
|
+
`data-filter-${key}`,
|
|
45
|
+
Array.isArray(value) ? value.join(delimiter) : value,
|
|
46
|
+
])
|
|
47
|
+
) as Record<`data-filter-${string}`, string>;
|
|
48
|
+
}
|
|
@@ -1,87 +1,95 @@
|
|
|
1
1
|
@import url('../../../styles/animations.css');
|
|
2
2
|
|
|
3
3
|
/* ----------------------------- */
|
|
4
|
-
/* Container
|
|
4
|
+
/* Container (invisible layer) */
|
|
5
5
|
/* ----------------------------- */
|
|
6
|
+
/* The zone wraps arbitrary content and stays out of the way until a drag
|
|
7
|
+
enters it. Only a subtle dashed border hints that it's a drop target. */
|
|
6
8
|
.n-dropzone {
|
|
7
9
|
--n-dropzone-accent: var(--color-brand-primary, #0e7fe1);
|
|
8
10
|
--n-dropzone-radius: var(--radius-2xl, 1rem);
|
|
9
11
|
|
|
10
12
|
box-sizing: border-box;
|
|
11
13
|
position: relative;
|
|
12
|
-
|
|
13
|
-
flex-direction: column;
|
|
14
|
-
align-items: center;
|
|
15
|
-
justify-content: center;
|
|
16
|
-
gap: var(--space-s);
|
|
17
|
-
width: 100%;
|
|
18
|
-
height: 100%;
|
|
19
|
-
min-height: 18rem;
|
|
20
|
-
padding: var(--space-2xl, 2.5rem) var(--space-l, 1.5rem);
|
|
21
|
-
text-align: center;
|
|
14
|
+
min-height: 4rem;
|
|
22
15
|
color: var(--color-text-default, #14161a);
|
|
23
|
-
background: var(--color-bg, #fff);
|
|
24
16
|
border: 2px dashed var(--color-border-default, rgba(0, 0, 0, 0.15));
|
|
25
17
|
border-radius: var(--n-dropzone-radius);
|
|
18
|
+
outline: none;
|
|
26
19
|
transition:
|
|
27
20
|
border-color 200ms ease-out,
|
|
28
21
|
background-color 200ms ease-out,
|
|
29
22
|
box-shadow 200ms ease-out;
|
|
30
23
|
}
|
|
31
24
|
|
|
25
|
+
.n-dropzone:focus-visible {
|
|
26
|
+
border-color: var(--n-dropzone-accent);
|
|
27
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--n-dropzone-accent) 40%, transparent);
|
|
28
|
+
}
|
|
29
|
+
|
|
32
30
|
/* Drag-over: the target glows without resizing. */
|
|
33
31
|
.n-dropzone.is-active {
|
|
34
32
|
border-color: var(--n-dropzone-accent);
|
|
35
|
-
background: var(--color-bg-2, #f6f8fa);
|
|
36
33
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--n-dropzone-accent) 18%, transparent);
|
|
37
34
|
}
|
|
38
35
|
|
|
36
|
+
/* While deploying, the dashes settle into a solid, quieter frame. */
|
|
39
37
|
.n-dropzone.is-busy {
|
|
40
38
|
border-style: solid;
|
|
41
39
|
border-color: var(--color-border-subtle, rgba(0, 0, 0, 0.08));
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
/*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
120% 120% at 50% 0%,
|
|
52
|
-
color-mix(in srgb, var(--n-dropzone-accent) 8%, transparent),
|
|
53
|
-
transparent 60%
|
|
54
|
-
);
|
|
55
|
-
opacity: 0;
|
|
56
|
-
transition: opacity 200ms ease-out;
|
|
57
|
-
pointer-events: none;
|
|
42
|
+
/* ----------------------------- */
|
|
43
|
+
/* Wrapped content */
|
|
44
|
+
/* ----------------------------- */
|
|
45
|
+
/* Kept in flow (so the zone doesn't collapse) but faded out of the way once a
|
|
46
|
+
drag enters or a deploy starts, letting the overlay take over. */
|
|
47
|
+
.n-dropzone-content {
|
|
48
|
+
transition: opacity 150ms ease-out;
|
|
58
49
|
}
|
|
59
50
|
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
/* `.is-bare` (showStatus={false}) keeps content visible so a custom animation
|
|
52
|
+
can play in place of the built-in overlay. */
|
|
53
|
+
.n-dropzone.is-active:not(.is-bare) .n-dropzone-content,
|
|
54
|
+
.n-dropzone.is-busy:not(.is-bare) .n-dropzone-content {
|
|
55
|
+
opacity: 0;
|
|
56
|
+
pointer-events: none;
|
|
62
57
|
}
|
|
63
58
|
|
|
64
59
|
/* ----------------------------- */
|
|
65
|
-
/*
|
|
60
|
+
/* Overlay (drag tint / status) */
|
|
66
61
|
/* ----------------------------- */
|
|
67
|
-
.n-dropzone-
|
|
62
|
+
.n-dropzone-overlay {
|
|
63
|
+
position: absolute;
|
|
64
|
+
inset: 0;
|
|
65
|
+
z-index: 1;
|
|
68
66
|
display: flex;
|
|
69
67
|
flex-direction: column;
|
|
70
68
|
align-items: center;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
justify-content: center;
|
|
70
|
+
gap: var(--space-s, 1rem);
|
|
71
|
+
padding: var(--space-l, 1.5rem);
|
|
72
|
+
text-align: center;
|
|
73
|
+
border-radius: inherit;
|
|
74
|
+
/* During a hover-only drag the overlay is a translucent wash and lets the
|
|
75
|
+
drop event fall through to the container beneath it. */
|
|
76
|
+
pointer-events: none;
|
|
77
|
+
background: color-mix(in srgb, var(--n-dropzone-accent) 10%, transparent);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
/* Deploying / error: opaque scrim that captures interaction. */
|
|
81
|
+
.n-dropzone.is-busy .n-dropzone-overlay,
|
|
82
|
+
.n-dropzone[data-state='error'] .n-dropzone-overlay {
|
|
83
|
+
pointer-events: auto;
|
|
84
|
+
background: color-mix(in srgb, var(--color-bg, #fff) 88%, transparent);
|
|
85
|
+
backdrop-filter: blur(2px);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.n-dropzone-hint {
|
|
89
|
+
margin: 0;
|
|
90
|
+
font-size: 1.0625rem;
|
|
91
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
92
|
+
color: var(--n-dropzone-accent);
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
.n-dropzone-title {
|
|
@@ -103,41 +111,6 @@
|
|
|
103
111
|
line-height: var(--line-height-text, 1.5);
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
/* ----------------------------- */
|
|
107
|
-
/* Idle upload glyph */
|
|
108
|
-
/* ----------------------------- */
|
|
109
|
-
.n-dropzone-glyph {
|
|
110
|
-
display: grid;
|
|
111
|
-
place-items: center;
|
|
112
|
-
width: 3.5rem;
|
|
113
|
-
height: 3.5rem;
|
|
114
|
-
color: var(--n-dropzone-accent);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.n-dropzone-glyph svg {
|
|
118
|
-
width: 100%;
|
|
119
|
-
height: 100%;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/* The arrow gently bobs to invite a drop; the tray stays put. */
|
|
123
|
-
.n-dropzone-glyph-arrow {
|
|
124
|
-
animation: dropArrowBob 2.4s ease-in-out infinite;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.n-dropzone.is-active .n-dropzone-glyph-arrow {
|
|
128
|
-
animation-duration: 0.9s;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
@keyframes dropArrowBob {
|
|
132
|
-
0%,
|
|
133
|
-
100% {
|
|
134
|
-
transform: translateY(0);
|
|
135
|
-
}
|
|
136
|
-
50% {
|
|
137
|
-
transform: translateY(-3px);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
114
|
/* ----------------------------- */
|
|
142
115
|
/* Loading / spinner */
|
|
143
116
|
/* ----------------------------- */
|
|
@@ -222,24 +195,13 @@
|
|
|
222
195
|
/* Warning banner */
|
|
223
196
|
/* ----------------------------- */
|
|
224
197
|
.n-dropzone-warning {
|
|
225
|
-
display: flex;
|
|
226
|
-
align-items: center;
|
|
227
|
-
gap: var(--space-2xs, 0.5rem);
|
|
228
198
|
margin: 0;
|
|
229
|
-
margin-block-start: var(--space-xs, 0.75rem);
|
|
230
199
|
padding: var(--space-2xs, 0.5rem) var(--space-s, 1rem);
|
|
231
200
|
max-width: 32rem;
|
|
232
201
|
border-radius: var(--radius-m, 0.5rem);
|
|
233
202
|
background: var(--color-warning-bg, #fdf3d7);
|
|
234
203
|
color: var(--color-warning-text, #7a5b00);
|
|
235
204
|
font-size: 0.8125rem;
|
|
236
|
-
text-align: left;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
.n-dropzone-warning-icon {
|
|
240
|
-
flex: none;
|
|
241
|
-
width: 1rem;
|
|
242
|
-
height: 1rem;
|
|
243
205
|
}
|
|
244
206
|
|
|
245
207
|
/* ----------------------------- */
|
|
@@ -247,8 +209,7 @@
|
|
|
247
209
|
/* ----------------------------- */
|
|
248
210
|
@media (prefers-reduced-motion: reduce) {
|
|
249
211
|
.n-dropzone,
|
|
250
|
-
.n-dropzone-
|
|
251
|
-
.n-dropzone-glyph-arrow,
|
|
212
|
+
.n-dropzone-overlay,
|
|
252
213
|
.n-dropzone-spinner,
|
|
253
214
|
.n-dropzone-progress-bar {
|
|
254
215
|
animation: none !important;
|