@netlify/spark-ui 1.25.0-alpha.0 → 1.26.0-alpha.1
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 -0
- package/dist/chunks/{AskNetlify.jaxhKBOK.js → AskNetlify.B3RnU2w-.js} +124 -126
- package/dist/chunks/_commonjsHelpers.DaMA6jEr.js +8 -0
- package/dist/chunks/jszip.min.DGG1sjYa.js +2327 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/preact/AskNetlify/AskNetlify.js +1 -1
- package/dist/components/preact/DropZone/DropZone.d.ts +19 -0
- package/dist/components/preact/DropZone/DropZone.js +207 -0
- package/dist/components/preact/DropZone/drop-core/client.d.ts +19 -0
- package/dist/components/preact/DropZone/drop-core/client.js +41 -0
- package/dist/components/preact/DropZone/drop-core/deploy.d.ts +6 -0
- package/dist/components/preact/DropZone/drop-core/deploy.js +30 -0
- package/dist/components/preact/DropZone/drop-core/digest.d.ts +3 -0
- package/dist/components/preact/DropZone/drop-core/digest.js +12 -0
- package/dist/components/preact/DropZone/drop-core/index.d.ts +5 -0
- package/dist/components/preact/DropZone/drop-core/index.js +16 -0
- package/dist/components/preact/DropZone/drop-core/readFiles.d.ts +15 -0
- package/dist/components/preact/DropZone/drop-core/readFiles.js +615 -0
- package/dist/components/preact/DropZone/drop-core/types.d.ts +30 -0
- package/dist/components/preact/DropZone/drop-core/types.js +1 -0
- package/dist/components/preact/SearchModal/components/SearchModal/SearchModal.js +1 -1
- package/dist/components/preact/index.js +1 -1
- package/package.json +4 -2
- package/packages/components/index.ts +16 -0
- package/packages/components/preact/DropZone/DropZone.css +340 -0
- package/packages/components/preact/DropZone/DropZone.tsx +295 -0
- package/packages/components/preact/DropZone/drop-core/client.ts +61 -0
- package/packages/components/preact/DropZone/drop-core/deploy.ts +55 -0
- package/packages/components/preact/DropZone/drop-core/digest.ts +11 -0
- package/packages/components/preact/DropZone/drop-core/index.ts +5 -0
- package/packages/components/preact/DropZone/drop-core/readFiles.ts +133 -0
- package/packages/components/preact/DropZone/drop-core/types.ts +35 -0
|
@@ -6,4 +6,20 @@ export { default as SearchLink } from './preact/SearchModal/components/SearchLin
|
|
|
6
6
|
export { default as SearchHeading } from './preact/SearchModal/components/SearchHeading/SearchHeading';
|
|
7
7
|
export { default as SearchToggle } from './preact/SearchModal/components/SearchToggle/SearchToggle';
|
|
8
8
|
export { default as SearchModal } from './preact/SearchModal/components/SearchModal/SearchModal';
|
|
9
|
+
export { default as DropZone } from './preact/DropZone/DropZone';
|
|
10
|
+
export {
|
|
11
|
+
AnonymousDropClient,
|
|
12
|
+
deployFiles,
|
|
13
|
+
readDataTransfer,
|
|
14
|
+
readFileList,
|
|
15
|
+
} from './preact/DropZone/drop-core';
|
|
16
|
+
export type {
|
|
17
|
+
DropClient,
|
|
18
|
+
Deploy,
|
|
19
|
+
Digest,
|
|
20
|
+
DeployResult,
|
|
21
|
+
DeployProgress,
|
|
22
|
+
DeployPhase,
|
|
23
|
+
ProcessedFile,
|
|
24
|
+
} from './preact/DropZone/drop-core';
|
|
9
25
|
export { type IconName } from './preact/Icon';
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
@import url('../../../styles/animations.css');
|
|
2
|
+
|
|
3
|
+
/* ----------------------------- */
|
|
4
|
+
/* Container */
|
|
5
|
+
/* ----------------------------- */
|
|
6
|
+
.n-dropzone {
|
|
7
|
+
--n-dropzone-accent: var(--color-brand-primary, #0e7fe1);
|
|
8
|
+
--n-dropzone-radius: var(--radius-2xl, 1rem);
|
|
9
|
+
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
position: relative;
|
|
12
|
+
display: flex;
|
|
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;
|
|
22
|
+
color: var(--color-text-default, #14161a);
|
|
23
|
+
background: var(--color-bg, #fff);
|
|
24
|
+
border: 2px dashed var(--color-border-default, rgba(0, 0, 0, 0.15));
|
|
25
|
+
border-radius: var(--n-dropzone-radius);
|
|
26
|
+
transition:
|
|
27
|
+
border-color 200ms ease-out,
|
|
28
|
+
background-color 200ms ease-out,
|
|
29
|
+
box-shadow 200ms ease-out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Drag-over: the target glows without resizing. */
|
|
33
|
+
.n-dropzone.is-active {
|
|
34
|
+
border-color: var(--n-dropzone-accent);
|
|
35
|
+
background: var(--color-bg-2, #f6f8fa);
|
|
36
|
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--n-dropzone-accent) 18%, transparent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.n-dropzone.is-busy {
|
|
40
|
+
border-style: solid;
|
|
41
|
+
border-color: var(--color-border-subtle, rgba(0, 0, 0, 0.08));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* A soft accent wash that fades in while dragging. */
|
|
45
|
+
.n-dropzone::before {
|
|
46
|
+
content: '';
|
|
47
|
+
position: absolute;
|
|
48
|
+
inset: 0;
|
|
49
|
+
border-radius: inherit;
|
|
50
|
+
background: radial-gradient(
|
|
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;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.n-dropzone.is-active::before {
|
|
61
|
+
opacity: 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ----------------------------- */
|
|
65
|
+
/* State panel (keyed → replays) */
|
|
66
|
+
/* ----------------------------- */
|
|
67
|
+
.n-dropzone-panel {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--space-xs, 0.75rem);
|
|
72
|
+
width: 100%;
|
|
73
|
+
animation: dropPanelIn 320ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@keyframes dropPanelIn {
|
|
77
|
+
from {
|
|
78
|
+
opacity: 0;
|
|
79
|
+
transform: translateY(8px) scale(0.98);
|
|
80
|
+
}
|
|
81
|
+
to {
|
|
82
|
+
opacity: 1;
|
|
83
|
+
transform: none;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.n-dropzone-title {
|
|
88
|
+
margin: 0;
|
|
89
|
+
font-size: 1.125rem;
|
|
90
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
91
|
+
line-height: var(--line-height-snug, 1.3);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.n-dropzone-title--error {
|
|
95
|
+
color: var(--color-invalid-text, #d00);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.n-dropzone-subtitle {
|
|
99
|
+
margin: 0;
|
|
100
|
+
max-width: 30rem;
|
|
101
|
+
font-size: 0.9375rem;
|
|
102
|
+
color: var(--color-text-weak, #5c6570);
|
|
103
|
+
line-height: var(--line-height-text, 1.5);
|
|
104
|
+
}
|
|
105
|
+
|
|
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
|
+
/* ----------------------------- */
|
|
142
|
+
/* Success glyph (draws in) */
|
|
143
|
+
/* ----------------------------- */
|
|
144
|
+
.n-dropzone-glyph--success {
|
|
145
|
+
color: var(--color-success-text, #108b46);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.n-dropzone-check-ring {
|
|
149
|
+
stroke-dasharray: 126;
|
|
150
|
+
stroke-dashoffset: 126;
|
|
151
|
+
animation: dropDraw 500ms ease-out 80ms forwards;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.n-dropzone-check-mark {
|
|
155
|
+
stroke-dasharray: 32;
|
|
156
|
+
stroke-dashoffset: 32;
|
|
157
|
+
animation: dropDraw 320ms ease-out 460ms forwards;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@keyframes dropDraw {
|
|
161
|
+
to {
|
|
162
|
+
stroke-dashoffset: 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ----------------------------- */
|
|
167
|
+
/* Loading / spinner */
|
|
168
|
+
/* ----------------------------- */
|
|
169
|
+
.n-dropzone-loading {
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: var(--space-s, 1rem);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.n-dropzone-spinner {
|
|
177
|
+
width: 2.25rem;
|
|
178
|
+
height: 2.25rem;
|
|
179
|
+
border-radius: var(--radius-pill, 999px);
|
|
180
|
+
border: 3px solid color-mix(in srgb, var(--n-dropzone-accent) 22%, transparent);
|
|
181
|
+
border-top-color: var(--n-dropzone-accent);
|
|
182
|
+
animation: dropSpin 720ms linear infinite;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@keyframes dropSpin {
|
|
186
|
+
to {
|
|
187
|
+
transform: rotate(360deg);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.n-dropzone-nudge {
|
|
192
|
+
display: block;
|
|
193
|
+
font-size: 0.8125rem;
|
|
194
|
+
color: var(--color-text-weaker, #8a929c);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* ----------------------------- */
|
|
198
|
+
/* Upload progress bar */
|
|
199
|
+
/* ----------------------------- */
|
|
200
|
+
.n-dropzone-progress {
|
|
201
|
+
width: min(22rem, 100%);
|
|
202
|
+
height: 0.5rem;
|
|
203
|
+
border-radius: var(--radius-pill, 999px);
|
|
204
|
+
background: var(--color-bg-3, rgba(0, 0, 0, 0.08));
|
|
205
|
+
overflow: hidden;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.n-dropzone-progress-bar {
|
|
209
|
+
height: 100%;
|
|
210
|
+
border-radius: inherit;
|
|
211
|
+
background: var(--n-dropzone-accent);
|
|
212
|
+
transition: width 300ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* ----------------------------- */
|
|
216
|
+
/* Done state links & actions */
|
|
217
|
+
/* ----------------------------- */
|
|
218
|
+
.n-dropzone-link {
|
|
219
|
+
display: inline-flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
gap: var(--space-3xs, 0.35rem);
|
|
222
|
+
font-size: 1rem;
|
|
223
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
224
|
+
color: var(--color-link, #0e7fe1);
|
|
225
|
+
text-decoration: none;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.n-dropzone-link:hover {
|
|
229
|
+
text-decoration: underline;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.n-dropzone-link-icon {
|
|
233
|
+
width: 0.9em;
|
|
234
|
+
height: 0.9em;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.n-dropzone-actions {
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-wrap: wrap;
|
|
240
|
+
align-items: center;
|
|
241
|
+
justify-content: center;
|
|
242
|
+
gap: var(--space-s, 1rem);
|
|
243
|
+
margin-block-start: var(--space-2xs, 0.5rem);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* ----------------------------- */
|
|
247
|
+
/* Buttons */
|
|
248
|
+
/* ----------------------------- */
|
|
249
|
+
.n-dropzone-browse {
|
|
250
|
+
display: inline-flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
justify-content: center;
|
|
253
|
+
padding: var(--space-2xs, 0.5rem) var(--space-m, 1.25rem);
|
|
254
|
+
border: none;
|
|
255
|
+
border-radius: var(--radius-pill, 999px);
|
|
256
|
+
background: var(--n-dropzone-accent);
|
|
257
|
+
color: var(--neutral-000, #fff);
|
|
258
|
+
font-size: 0.9375rem;
|
|
259
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
260
|
+
line-height: var(--line-height-none, 1);
|
|
261
|
+
text-decoration: none;
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
transition:
|
|
264
|
+
background-color 150ms ease-out,
|
|
265
|
+
transform 120ms ease-out;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.n-dropzone-browse:hover {
|
|
269
|
+
background: var(--color-brand-primary-hover, #0b6bc0);
|
|
270
|
+
transform: translateY(-1px);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.n-dropzone-browse:active {
|
|
274
|
+
transform: translateY(0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.n-dropzone-reset {
|
|
278
|
+
padding: var(--space-2xs, 0.5rem) var(--space-m, 1.25rem);
|
|
279
|
+
border: 1px solid var(--color-border-default, rgba(0, 0, 0, 0.15));
|
|
280
|
+
border-radius: var(--radius-pill, 999px);
|
|
281
|
+
background: transparent;
|
|
282
|
+
color: var(--color-text-default, #14161a);
|
|
283
|
+
font-size: 0.9375rem;
|
|
284
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
285
|
+
cursor: pointer;
|
|
286
|
+
transition: background-color 150ms ease-out;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.n-dropzone-reset:hover {
|
|
290
|
+
background: var(--color-bg-2, #f6f8fa);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* ----------------------------- */
|
|
294
|
+
/* Warning banner */
|
|
295
|
+
/* ----------------------------- */
|
|
296
|
+
.n-dropzone-warning {
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: center;
|
|
299
|
+
gap: var(--space-2xs, 0.5rem);
|
|
300
|
+
margin: 0;
|
|
301
|
+
margin-block-start: var(--space-xs, 0.75rem);
|
|
302
|
+
padding: var(--space-2xs, 0.5rem) var(--space-s, 1rem);
|
|
303
|
+
max-width: 32rem;
|
|
304
|
+
border-radius: var(--radius-m, 0.5rem);
|
|
305
|
+
background: var(--color-warning-bg, #fdf3d7);
|
|
306
|
+
color: var(--color-warning-text, #7a5b00);
|
|
307
|
+
font-size: 0.8125rem;
|
|
308
|
+
text-align: left;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.n-dropzone-warning-icon {
|
|
312
|
+
flex: none;
|
|
313
|
+
width: 1rem;
|
|
314
|
+
height: 1rem;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* ----------------------------- */
|
|
318
|
+
/* Reduced motion */
|
|
319
|
+
/* ----------------------------- */
|
|
320
|
+
@media (prefers-reduced-motion: reduce) {
|
|
321
|
+
.n-dropzone,
|
|
322
|
+
.n-dropzone-panel,
|
|
323
|
+
.n-dropzone-glyph-arrow,
|
|
324
|
+
.n-dropzone-spinner,
|
|
325
|
+
.n-dropzone-progress-bar,
|
|
326
|
+
.n-dropzone-check-ring,
|
|
327
|
+
.n-dropzone-check-mark {
|
|
328
|
+
animation: none !important;
|
|
329
|
+
transition: none !important;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.n-dropzone-check-ring,
|
|
333
|
+
.n-dropzone-check-mark {
|
|
334
|
+
stroke-dashoffset: 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.n-dropzone-spinner {
|
|
338
|
+
border-top-color: var(--n-dropzone-accent);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { useRef, useState } from 'preact/hooks';
|
|
2
|
+
import Icon from '../Icon';
|
|
3
|
+
import {
|
|
4
|
+
AnonymousDropClient,
|
|
5
|
+
deployFiles,
|
|
6
|
+
getSingleNonIndexHtmlFileName,
|
|
7
|
+
hasMultipleHtmlFilesWithoutIndex,
|
|
8
|
+
readDataTransfer,
|
|
9
|
+
readFileList,
|
|
10
|
+
type DeployProgress,
|
|
11
|
+
type DropClient,
|
|
12
|
+
type ProcessedFile,
|
|
13
|
+
} from './drop-core';
|
|
14
|
+
|
|
15
|
+
import './DropZone.css';
|
|
16
|
+
|
|
17
|
+
type Status =
|
|
18
|
+
| { kind: 'idle' }
|
|
19
|
+
| { kind: 'reading' }
|
|
20
|
+
| { kind: 'creating' }
|
|
21
|
+
| { kind: 'uploading'; uploaded: number; total: number }
|
|
22
|
+
| { kind: 'processing' }
|
|
23
|
+
| { kind: 'done'; url: string; token: string; siteName: string; siteId: string }
|
|
24
|
+
| { kind: 'error'; message: string };
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
/** Override the API base (e.g. for a staging environment). */
|
|
28
|
+
apiBase?: string;
|
|
29
|
+
/** Inject a custom client (e.g. for testing). Defaults to AnonymousDropClient. */
|
|
30
|
+
client?: DropClient;
|
|
31
|
+
/** Build the "claim this site" URL from the assigned site name (subdomain). */
|
|
32
|
+
claimUrl?: (siteName: string) => string;
|
|
33
|
+
/** Called once the deploy is live. */
|
|
34
|
+
onDeploy?: (result: { url: string; token: string; siteName: string; siteId: string }) => void;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}/claim`;
|
|
39
|
+
|
|
40
|
+
function progressPhaseToStatus(e: DeployProgress): Status {
|
|
41
|
+
switch (e.phase) {
|
|
42
|
+
case 'creating':
|
|
43
|
+
return { kind: 'creating' };
|
|
44
|
+
case 'uploading':
|
|
45
|
+
return { kind: 'uploading', uploaded: e.uploaded ?? 0, total: e.total ?? 0 };
|
|
46
|
+
case 'processing':
|
|
47
|
+
case 'ready':
|
|
48
|
+
return { kind: 'processing' };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function DropZone({
|
|
53
|
+
apiBase,
|
|
54
|
+
client,
|
|
55
|
+
claimUrl = defaultClaimUrl,
|
|
56
|
+
onDeploy,
|
|
57
|
+
className,
|
|
58
|
+
}: Props) {
|
|
59
|
+
const [status, setStatus] = useState<Status>({ kind: 'idle' });
|
|
60
|
+
const [warning, setWarning] = useState<string | null>(null);
|
|
61
|
+
const [active, setActive] = useState(false);
|
|
62
|
+
const dropClient = useRef(client ?? new AnonymousDropClient(apiBase)).current;
|
|
63
|
+
|
|
64
|
+
const busy = status.kind !== 'idle' && status.kind !== 'done' && status.kind !== 'error';
|
|
65
|
+
|
|
66
|
+
async function run(read: () => Promise<ProcessedFile[]>) {
|
|
67
|
+
setWarning(null);
|
|
68
|
+
setStatus({ kind: 'reading' });
|
|
69
|
+
try {
|
|
70
|
+
const files = await read();
|
|
71
|
+
if (!files.length) {
|
|
72
|
+
setStatus({ kind: 'error', message: 'No files found to deploy.' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Surface index.html warnings without blocking the deploy.
|
|
77
|
+
const singleNonIndex = getSingleNonIndexHtmlFileName(files);
|
|
78
|
+
if (singleNonIndex) {
|
|
79
|
+
setWarning(`No index.html found — "${singleNonIndex}" won’t be served at the site root.`);
|
|
80
|
+
} else if (hasMultipleHtmlFilesWithoutIndex(files)) {
|
|
81
|
+
setWarning('No index.html found — your site may not have a landing page.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { url, token, deploy } = await deployFiles(dropClient, files, {
|
|
85
|
+
onProgress: e => setStatus(progressPhaseToStatus(e)),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Persist for the claim step after signup (mirrors the dashboard).
|
|
89
|
+
try {
|
|
90
|
+
localStorage.setItem('drop-token', token);
|
|
91
|
+
localStorage.setItem('dropSiteName', deploy.subdomain);
|
|
92
|
+
} catch {
|
|
93
|
+
/* private mode / storage disabled — non-fatal */
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setStatus({ kind: 'done', url, token, siteName: deploy.subdomain, siteId: deploy.id });
|
|
97
|
+
onDeploy?.({ url, token, siteName: deploy.subdomain, siteId: deploy.id });
|
|
98
|
+
} catch (err) {
|
|
99
|
+
setStatus({ kind: 'error', message: err instanceof Error ? err.message : String(err) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function reset() {
|
|
104
|
+
setWarning(null);
|
|
105
|
+
setStatus({ kind: 'idle' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
class={`n-dropzone${active ? ' is-active' : ''}${busy ? ' is-busy' : ''}${className ? ` ${className}` : ''}`}
|
|
111
|
+
data-state={status.kind}
|
|
112
|
+
onDragOver={e => {
|
|
113
|
+
if (busy) return;
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
setActive(true);
|
|
116
|
+
}}
|
|
117
|
+
onDragLeave={e => {
|
|
118
|
+
// Ignore drag-leave bubbling from children.
|
|
119
|
+
if (e.currentTarget === e.target) setActive(false);
|
|
120
|
+
}}
|
|
121
|
+
onDrop={e => {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
setActive(false);
|
|
124
|
+
if (busy) return;
|
|
125
|
+
const dt = e.dataTransfer;
|
|
126
|
+
if (!dt) return;
|
|
127
|
+
// Capture entries synchronously — readDataTransfer reads them before
|
|
128
|
+
// its first await, so the DataTransfer isn't detached out from under us.
|
|
129
|
+
run(() => readDataTransfer(dt));
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{/* keyed so each state change remounts and replays its enter animation */}
|
|
133
|
+
<div class="n-dropzone-panel" key={status.kind}>
|
|
134
|
+
<StatusView status={status} claimUrl={claimUrl} onReset={reset} onFiles={run} />
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{warning && status.kind !== 'error' && (
|
|
138
|
+
<p class="n-dropzone-warning n-fadeIn" role="status">
|
|
139
|
+
<Icon name="lightbulb" className="n-dropzone-warning-icon" />
|
|
140
|
+
{warning}
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function StatusView({
|
|
148
|
+
status,
|
|
149
|
+
claimUrl,
|
|
150
|
+
onReset,
|
|
151
|
+
onFiles,
|
|
152
|
+
}: {
|
|
153
|
+
status: Status;
|
|
154
|
+
claimUrl: (siteName: string) => string;
|
|
155
|
+
onReset: () => void;
|
|
156
|
+
onFiles: (read: () => Promise<ProcessedFile[]>) => void;
|
|
157
|
+
}) {
|
|
158
|
+
switch (status.kind) {
|
|
159
|
+
case 'idle':
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
<UploadGlyph />
|
|
163
|
+
<p class="n-dropzone-title">Drag & drop your site folder or zip</p>
|
|
164
|
+
<p class="n-dropzone-subtitle">Deploys instantly to a live URL — no account needed.</p>
|
|
165
|
+
<label class="n-dropzone-browse">
|
|
166
|
+
Browse files
|
|
167
|
+
<input
|
|
168
|
+
type="file"
|
|
169
|
+
hidden
|
|
170
|
+
multiple
|
|
171
|
+
// @ts-expect-error non-standard directory-picker attributes
|
|
172
|
+
webkitdirectory=""
|
|
173
|
+
directory=""
|
|
174
|
+
onChange={e => {
|
|
175
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
176
|
+
if (input.files?.length) onFiles(() => readFileList(input.files!));
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
</label>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
case 'reading':
|
|
184
|
+
return <Spinner label="Reading files…" />;
|
|
185
|
+
case 'creating':
|
|
186
|
+
return <Spinner label="Creating deploy…" />;
|
|
187
|
+
case 'processing':
|
|
188
|
+
return <Spinner label="Processing — almost live…" />;
|
|
189
|
+
|
|
190
|
+
case 'uploading': {
|
|
191
|
+
const pct = status.total ? Math.round((status.uploaded / status.total) * 100) : 0;
|
|
192
|
+
return (
|
|
193
|
+
<>
|
|
194
|
+
<Spinner label={`Uploading ${status.uploaded}/${status.total} files…`} />
|
|
195
|
+
<div class="n-dropzone-progress" role="progressbar" aria-valuenow={pct}>
|
|
196
|
+
<div class="n-dropzone-progress-bar" style={{ width: `${pct}%` }} />
|
|
197
|
+
</div>
|
|
198
|
+
</>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'done':
|
|
203
|
+
return (
|
|
204
|
+
<>
|
|
205
|
+
<SuccessGlyph />
|
|
206
|
+
<p class="n-dropzone-title">Your site is live!</p>
|
|
207
|
+
<a class="n-dropzone-link" href={status.url} target="_blank" rel="noopener noreferrer">
|
|
208
|
+
{status.url.replace(/^https?:\/\//, '')}
|
|
209
|
+
<Icon name="link" className="n-dropzone-link-icon" />
|
|
210
|
+
</a>
|
|
211
|
+
<div class="n-dropzone-actions">
|
|
212
|
+
<a class="n-dropzone-browse" href={claimUrl(status.siteName)}>
|
|
213
|
+
Claim this site
|
|
214
|
+
</a>
|
|
215
|
+
<button type="button" class="n-dropzone-reset" onClick={onReset}>
|
|
216
|
+
Deploy another
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
</>
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
case 'error':
|
|
223
|
+
return (
|
|
224
|
+
<>
|
|
225
|
+
<p class="n-dropzone-title n-dropzone-title--error">Something went wrong</p>
|
|
226
|
+
<p class="n-dropzone-subtitle" role="alert">
|
|
227
|
+
{status.message}
|
|
228
|
+
</p>
|
|
229
|
+
<button type="button" class="n-dropzone-browse" onClick={onReset}>
|
|
230
|
+
Try again
|
|
231
|
+
</button>
|
|
232
|
+
</>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function Spinner({ label }: { label: string }) {
|
|
238
|
+
return (
|
|
239
|
+
<div class="n-dropzone-loading">
|
|
240
|
+
<span class="n-dropzone-spinner" aria-hidden="true" />
|
|
241
|
+
<p class="n-dropzone-subtitle" aria-live="polite">
|
|
242
|
+
{label} <span class="n-dropzone-nudge">Please don’t refresh.</span>
|
|
243
|
+
</p>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function UploadGlyph() {
|
|
249
|
+
return (
|
|
250
|
+
<span class="n-dropzone-glyph" aria-hidden="true">
|
|
251
|
+
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
252
|
+
<path
|
|
253
|
+
class="n-dropzone-glyph-tray"
|
|
254
|
+
d="M8 30v6a4 4 0 0 0 4 4h24a4 4 0 0 0 4-4v-6"
|
|
255
|
+
stroke="currentColor"
|
|
256
|
+
stroke-width="3"
|
|
257
|
+
stroke-linecap="round"
|
|
258
|
+
/>
|
|
259
|
+
<path
|
|
260
|
+
class="n-dropzone-glyph-arrow"
|
|
261
|
+
d="M24 32V10m0 0-8 8m8-8 8 8"
|
|
262
|
+
stroke="currentColor"
|
|
263
|
+
stroke-width="3"
|
|
264
|
+
stroke-linecap="round"
|
|
265
|
+
stroke-linejoin="round"
|
|
266
|
+
/>
|
|
267
|
+
</svg>
|
|
268
|
+
</span>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function SuccessGlyph() {
|
|
273
|
+
return (
|
|
274
|
+
<span class="n-dropzone-glyph n-dropzone-glyph--success" aria-hidden="true">
|
|
275
|
+
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
276
|
+
<circle
|
|
277
|
+
class="n-dropzone-check-ring"
|
|
278
|
+
cx="24"
|
|
279
|
+
cy="24"
|
|
280
|
+
r="20"
|
|
281
|
+
stroke="currentColor"
|
|
282
|
+
stroke-width="3"
|
|
283
|
+
/>
|
|
284
|
+
<path
|
|
285
|
+
class="n-dropzone-check-mark"
|
|
286
|
+
d="M15 24.5 21 30.5 33 18"
|
|
287
|
+
stroke="currentColor"
|
|
288
|
+
stroke-width="3"
|
|
289
|
+
stroke-linecap="round"
|
|
290
|
+
stroke-linejoin="round"
|
|
291
|
+
/>
|
|
292
|
+
</svg>
|
|
293
|
+
</span>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Deploy, Digest } from './types';
|
|
2
|
+
|
|
3
|
+
export interface DropClient {
|
|
4
|
+
getToken(): Promise<string>;
|
|
5
|
+
createDeploy(digest: Digest, token: string): Promise<Deploy>;
|
|
6
|
+
uploadFile(deployId: string, path: string, content: ArrayBuffer, token: string): Promise<void>;
|
|
7
|
+
waitUntilReady(deploy: Deploy, token: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const API = 'https://api.netlify.com/api/v1';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Unauthenticated drop client. Authorized server-side by the Referer header,
|
|
14
|
+
* so it only works from an allowlisted origin (see backend checklist).
|
|
15
|
+
*/
|
|
16
|
+
export class AnonymousDropClient implements DropClient {
|
|
17
|
+
constructor(private apiBase = API) {}
|
|
18
|
+
|
|
19
|
+
async getToken(): Promise<string> {
|
|
20
|
+
const res = await fetch(`${this.apiBase}/drop/token`, { method: 'POST' });
|
|
21
|
+
if (!res.ok) throw new Error(`drop/token failed: ${res.status}`);
|
|
22
|
+
return (await res.json()).token;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async createDeploy(files: Digest, token: string): Promise<Deploy> {
|
|
26
|
+
const res = await fetch(`${this.apiBase}/drop`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ files, token }),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) throw new Error(`drop failed: ${res.status}`);
|
|
32
|
+
return res.json();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async uploadFile(
|
|
36
|
+
deployId: string,
|
|
37
|
+
path: string,
|
|
38
|
+
content: ArrayBuffer,
|
|
39
|
+
token: string
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
// PUT (not POST) so paths with "&" etc. work.
|
|
42
|
+
const res = await fetch(`${this.apiBase}/deploys/${deployId}/files${path}`, {
|
|
43
|
+
method: 'PUT',
|
|
44
|
+
headers: { 'Content-Type': 'application/octet-stream', Authorization: `Bearer ${token}` },
|
|
45
|
+
body: content,
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) throw new Error(`upload ${path} failed: ${res.status}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async waitUntilReady(deploy: Deploy, token: string): Promise<void> {
|
|
51
|
+
// Poll the deploy with the drop token until ready.
|
|
52
|
+
for (let i = 0; i < 600; i++) {
|
|
53
|
+
const res = await fetch(`${this.apiBase}/deploys/${deploy.id}`, {
|
|
54
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
55
|
+
});
|
|
56
|
+
if (res.ok && (await res.json()).state === 'ready') return;
|
|
57
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
58
|
+
}
|
|
59
|
+
throw new Error('Deploy did not become ready in time');
|
|
60
|
+
}
|
|
61
|
+
}
|