@netlify/spark-ui 1.29.0 → 1.31.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.
Files changed (54) hide show
  1. package/README.md +4 -3
  2. package/dist/chunks/browser.Ca2rTNTu.js +769 -0
  3. package/dist/components/articleCopyActions.d.ts +9 -0
  4. package/dist/components/articleCopyActions.test.d.ts +1 -0
  5. package/dist/components/index.d.ts +2 -2
  6. package/dist/components/preact/DropZone/DropZone.d.ts +84 -15
  7. package/dist/components/preact/DropZone/DropZone.js +117 -85
  8. package/dist/components/preact/DropZone/drop-core/authedDropClient.d.ts +80 -0
  9. package/dist/components/preact/DropZone/drop-core/authedDropClient.js +99 -0
  10. package/dist/components/preact/DropZone/drop-core/buildClient.d.ts +98 -0
  11. package/dist/components/preact/DropZone/drop-core/buildClient.js +92 -0
  12. package/dist/components/preact/DropZone/drop-core/client.d.ts +4 -0
  13. package/dist/components/preact/DropZone/drop-core/client.js +2 -1
  14. package/dist/components/preact/DropZone/drop-core/deploy.d.ts +31 -0
  15. package/dist/components/preact/DropZone/drop-core/deploy.js +36 -18
  16. package/dist/components/preact/DropZone/drop-core/detectBuild.d.ts +26 -0
  17. package/dist/components/preact/DropZone/drop-core/detectBuild.js +80 -53
  18. package/dist/components/preact/DropZone/drop-core/errors.d.ts +38 -0
  19. package/dist/components/preact/DropZone/drop-core/errors.js +50 -5
  20. package/dist/components/preact/DropZone/drop-core/fileTypes.d.ts +17 -0
  21. package/dist/components/preact/DropZone/drop-core/fileTypes.js +11 -0
  22. package/dist/components/preact/DropZone/drop-core/index.d.ts +4 -0
  23. package/dist/components/preact/DropZone/drop-core/index.js +32 -15
  24. package/dist/components/preact/DropZone/drop-core/readFiles.js +1 -1
  25. package/dist/components/preact/DropZone/drop-core/types.d.ts +6 -1
  26. package/dist/components/preact/DropZone/drop-core/zip.d.ts +46 -0
  27. package/dist/components/preact/DropZone/drop-core/zip.js +43 -0
  28. package/dist/components/preact/DropZone/index.d.ts +3 -2
  29. package/dist/components/preact/DropZone/index.js +27 -9
  30. package/dist/components/preact/DropZone/useDropDeploy.d.ts +36 -5
  31. package/dist/components/preact/DropZone/useDropDeploy.js +140 -65
  32. package/dist/components/preact/index.d.ts +2 -1
  33. package/dist/components/preact/index.js +17 -12
  34. package/package.json +5 -2
  35. package/packages/components/ArticleCopyActions.astro +122 -0
  36. package/packages/components/articleCopyActions.test.ts +143 -0
  37. package/packages/components/articleCopyActions.ts +41 -0
  38. package/packages/components/index.ts +15 -0
  39. package/packages/components/preact/DropZone/DropZone.tsx +145 -24
  40. package/packages/components/preact/DropZone/drop-core/authedDropClient.ts +194 -0
  41. package/packages/components/preact/DropZone/drop-core/buildClient.ts +190 -0
  42. package/packages/components/preact/DropZone/drop-core/client.ts +1 -1
  43. package/packages/components/preact/DropZone/drop-core/deploy.ts +65 -0
  44. package/packages/components/preact/DropZone/drop-core/detectBuild.ts +125 -41
  45. package/packages/components/preact/DropZone/drop-core/errors.ts +84 -1
  46. package/packages/components/preact/DropZone/drop-core/fileTypes.ts +34 -0
  47. package/packages/components/preact/DropZone/drop-core/index.ts +4 -0
  48. package/packages/components/preact/DropZone/drop-core/readFiles.ts +1 -1
  49. package/packages/components/preact/DropZone/drop-core/types.ts +12 -1
  50. package/packages/components/preact/DropZone/drop-core/zip.ts +118 -0
  51. package/packages/components/preact/DropZone/index.tsx +24 -0
  52. package/packages/components/preact/DropZone/useDropDeploy.ts +219 -34
  53. package/packages/components/preact/index.ts +2 -1
  54. package/dist/chunks/browser.D58rdxmc.js +0 -444
@@ -0,0 +1,122 @@
1
+ ---
2
+ interface Props {
3
+ url: string;
4
+ content: string;
5
+ title?: string;
6
+ class?: string;
7
+ copyUrlLabel?: string;
8
+ copyContentLabel?: string;
9
+ copiedLabel?: string;
10
+ trackingProps?: Record<string, string>;
11
+ trackingSelectorClass?: string;
12
+ }
13
+
14
+ const {
15
+ url,
16
+ content,
17
+ title,
18
+ class: className,
19
+ copyUrlLabel = 'Copy URL',
20
+ copyContentLabel = 'Copy page',
21
+ copiedLabel = 'Copied!',
22
+ trackingProps = {},
23
+ trackingSelectorClass,
24
+ } = Astro.props;
25
+
26
+ const copyContent = title ? `# ${title}\n\n${content}` : content;
27
+ ---
28
+
29
+ <div class:list={['copy-actions', className]} data-copied-label={copiedLabel}>
30
+ <button
31
+ type="button"
32
+ class:list={['copy-actions-action', 'copy-actions-copy-url', trackingSelectorClass]}
33
+ data-copy="url"
34
+ data-value={url}
35
+ data-track-action="copy_url"
36
+ {...trackingProps}
37
+ >
38
+ <svg
39
+ class="copy-actions-icon"
40
+ viewBox="0 0 16 16"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ stroke-width="1.3"
44
+ stroke-linecap="round"
45
+ stroke-linejoin="round"
46
+ aria-hidden="true"
47
+ role="presentation"
48
+ >
49
+ <path d="M6.5 9.5a2.5 2.5 0 0 0 3.6.1l2-2a2.5 2.5 0 0 0-3.6-3.6l-1.1 1.1"></path>
50
+ <path d="M9.5 6.5a2.5 2.5 0 0 0-3.6-.1l-2 2a2.5 2.5 0 0 0 3.6 3.6l1.1-1.1"></path>
51
+ </svg>
52
+ <span class="copy-actions-label">{copyUrlLabel}</span>
53
+ </button>
54
+ <button
55
+ type="button"
56
+ class:list={['copy-actions-action', 'copy-actions-copy-content', trackingSelectorClass]}
57
+ data-copy="page"
58
+ data-track-action="copy_page"
59
+ {...trackingProps}
60
+ >
61
+ <svg
62
+ class="copy-actions-icon"
63
+ viewBox="0 0 16 16"
64
+ fill="currentColor"
65
+ aria-hidden="true"
66
+ role="presentation"
67
+ >
68
+ <path
69
+ d="M4.83333 1C3.82214 1 3 1.78477 3 2.75V13.25C3 14.2152 3.82214 15 4.83333 15H12.1667C13.1779 15 14 14.2152 14 13.25V5.375H10.3333C9.8263 5.375 9.41667 4.98398 9.41667 4.5V1H4.83333ZM10.3333 1V4.5H14L10.3333 1ZM6.20833 8H10.7917C11.0438 8 11.25 8.19687 11.25 8.4375C11.25 8.67813 11.0438 8.875 10.7917 8.875H6.20833C5.95625 8.875 5.75 8.67813 5.75 8.4375C5.75 8.19687 5.95625 8 6.20833 8ZM6.20833 9.75H10.7917C11.0438 9.75 11.25 9.94687 11.25 10.1875C11.25 10.4281 11.0438 10.625 10.7917 10.625H6.20833C5.95625 10.625 5.75 10.4281 5.75 10.1875C5.75 9.94687 5.95625 9.75 6.20833 9.75ZM6.20833 11.5H10.7917C11.0438 11.5 11.25 11.6969 11.25 11.9375C11.25 12.1781 11.0438 12.375 10.7917 12.375H6.20833C5.95625 12.375 5.75 12.1781 5.75 11.9375C5.75 11.6969 5.95625 11.5 6.20833 11.5Z"
70
+ ></path>
71
+ </svg>
72
+ <span class="copy-actions-label">{copyContentLabel}</span>
73
+ </button>
74
+ </div>
75
+
76
+ <script is:inline type="text/plain" data-copy-actions-source set:text={copyContent} />
77
+
78
+ <style>
79
+ .copy-actions {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: var(--ntl-space-2xs);
83
+ }
84
+
85
+ .copy-actions-action {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: var(--ntl-space-3xs);
89
+ background: none;
90
+ border: none;
91
+ padding: 0;
92
+ font: inherit;
93
+ color: var(--ntl-color-text-weak);
94
+ cursor: pointer;
95
+ }
96
+
97
+ .copy-actions-label {
98
+ font-size: var(--ntl-font-size-text-sm);
99
+ }
100
+
101
+ .copy-actions-icon {
102
+ width: var(--ntl-space-s);
103
+ height: var(--ntl-space-s);
104
+ }
105
+
106
+ .copy-actions-copy-url::after {
107
+ content: '•';
108
+ margin-inline-start: var(--ntl-space-2xs);
109
+ }
110
+
111
+ .copy-actions-action:hover {
112
+ color: var(--ntl-color-text);
113
+ }
114
+ </style>
115
+
116
+ <script>
117
+ import { initArticleCopyActions } from './articleCopyActions';
118
+
119
+ // `astro:page-load` fires on the initial load *and* after every client-side
120
+ // navigation (`<ClientRouter />`).
121
+ document.addEventListener('astro:page-load', () => initArticleCopyActions());
122
+ </script>
@@ -0,0 +1,143 @@
1
+ // @vitest-environment jsdom
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { initArticleCopyActions } from './articleCopyActions';
4
+
5
+ const ARTICLE_URL = 'https://www.netlify.com/blog/some-post/';
6
+ const MARKDOWN = '# Some post\n\nBody content.';
7
+
8
+ function renderCopyActions({ url = ARTICLE_URL, markdown = MARKDOWN, copiedLabel = 'Copied!' } = {}) {
9
+ document.body.innerHTML = `
10
+ <div class="copy-actions" data-copied-label="${copiedLabel}">
11
+ <button
12
+ type="button"
13
+ class="copy-actions-action copy-actions-copy-url"
14
+ data-copy="url"
15
+ data-value="${url}"
16
+ >
17
+ <span>Copy URL</span>
18
+ </button>
19
+ <button
20
+ type="button"
21
+ class="copy-actions-action copy-actions-copy-content"
22
+ data-copy="page"
23
+ >
24
+ <span>Copy page</span>
25
+ </button>
26
+ </div>
27
+ <script type="text/plain" data-copy-actions-source>${markdown}</script>
28
+ `;
29
+
30
+ return {
31
+ wrapper: document.querySelector<HTMLElement>('.copy-actions')!,
32
+ copyUrlBtn: document.querySelector<HTMLButtonElement>('.copy-actions-copy-url')!,
33
+ copyPageBtn: document.querySelector<HTMLButtonElement>('.copy-actions-copy-content')!,
34
+ urlLabel: document.querySelector<HTMLElement>('.copy-actions-copy-url span')!,
35
+ pageLabel: document.querySelector<HTMLElement>('.copy-actions-copy-content span')!,
36
+ };
37
+ }
38
+
39
+ describe('initArticleCopyActions', () => {
40
+ let writeText: ReturnType<typeof vi.fn>;
41
+
42
+ beforeEach(() => {
43
+ vi.useFakeTimers();
44
+ writeText = vi.fn().mockResolvedValue(undefined);
45
+ Object.defineProperty(navigator, 'clipboard', {
46
+ value: { writeText },
47
+ configurable: true,
48
+ });
49
+ });
50
+
51
+ afterEach(() => {
52
+ vi.runOnlyPendingTimers();
53
+ vi.useRealTimers();
54
+ document.body.innerHTML = '';
55
+ });
56
+
57
+ it('copies the article URL and shows a temporary confirmation', async () => {
58
+ const { copyUrlBtn, urlLabel } = renderCopyActions();
59
+ initArticleCopyActions();
60
+
61
+ copyUrlBtn.click();
62
+ await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith(ARTICLE_URL));
63
+ expect(urlLabel.textContent).toBe('Copied!');
64
+ });
65
+
66
+ it('copies the markdown source for the "Copy page" button', async () => {
67
+ const { copyPageBtn, pageLabel } = renderCopyActions();
68
+ initArticleCopyActions();
69
+
70
+ copyPageBtn.click();
71
+ await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith(MARKDOWN));
72
+ expect(pageLabel.textContent).toBe('Copied!');
73
+ });
74
+
75
+ it('resets the label back to its original text after the delay', async () => {
76
+ const { copyUrlBtn, urlLabel } = renderCopyActions();
77
+ initArticleCopyActions();
78
+
79
+ copyUrlBtn.click();
80
+ await vi.waitFor(() => expect(urlLabel.textContent).toBe('Copied!'));
81
+
82
+ vi.advanceTimersByTime(1500);
83
+ expect(urlLabel.textContent).toBe('Copy URL');
84
+ });
85
+
86
+ it('uses a custom copied label when data-copied-label is set', async () => {
87
+ const { copyUrlBtn, urlLabel } = renderCopyActions({ copiedLabel: 'Done!' });
88
+ initArticleCopyActions();
89
+
90
+ copyUrlBtn.click();
91
+ await vi.waitFor(() => expect(urlLabel.textContent).toBe('Done!'));
92
+ });
93
+
94
+ it('does not attach duplicate listeners when initialised twice on the same DOM', async () => {
95
+ const { copyUrlBtn } = renderCopyActions();
96
+ initArticleCopyActions();
97
+ initArticleCopyActions(); // e.g. a spurious re-fire
98
+
99
+ copyUrlBtn.click();
100
+ await vi.waitFor(() => expect(writeText).toHaveBeenCalledTimes(1));
101
+ });
102
+
103
+ it('re-binds after a client-side navigation swaps in fresh buttons', async () => {
104
+ // Simulate the ClientRouter bug: first page loads and works...
105
+ const first = renderCopyActions({ url: 'https://example.com/first/' });
106
+ initArticleCopyActions();
107
+ first.copyUrlBtn.click();
108
+ await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('https://example.com/first/'));
109
+
110
+ // ...then a navigation replaces the DOM with a new post's buttons.
111
+ const second = renderCopyActions({ url: 'https://example.com/second/' });
112
+ // Re-running on `astro:page-load` is what makes the new buttons work.
113
+ initArticleCopyActions();
114
+
115
+ second.copyUrlBtn.click();
116
+ await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('https://example.com/second/'));
117
+ });
118
+
119
+ it('scopes the markdown source lookup to each wrapper, not a global selector', async () => {
120
+ document.body.innerHTML = `
121
+ <div class="copy-actions" data-copied-label="Copied!">
122
+ <button type="button" class="copy-actions-action copy-actions-copy-content" data-copy="page">
123
+ <span>Copy page</span>
124
+ </button>
125
+ </div>
126
+ <script type="text/plain" data-copy-actions-source>First post markdown</script>
127
+ <div class="copy-actions" data-copied-label="Copied!">
128
+ <button type="button" class="copy-actions-action copy-actions-copy-content" data-copy="page">
129
+ <span>Copy page</span>
130
+ </button>
131
+ </div>
132
+ <script type="text/plain" data-copy-actions-source>Second post markdown</script>
133
+ `;
134
+ initArticleCopyActions();
135
+
136
+ const buttons = document.querySelectorAll<HTMLButtonElement>('.copy-actions-copy-content');
137
+ buttons[1].click();
138
+ await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('Second post markdown'));
139
+
140
+ buttons[0].click();
141
+ await vi.waitFor(() => expect(writeText).toHaveBeenCalledWith('First post markdown'));
142
+ });
143
+ });
@@ -0,0 +1,41 @@
1
+ const COPIED_LABEL_FALLBACK = 'Copied!';
2
+ const RESET_DELAY_MS = 1500;
3
+
4
+ /**
5
+ * Wire up the "Copy URL" / "Copy page" buttons rendered by ArticleCopyActions.astro.
6
+ *
7
+ * This runs on every `astro:page-load` (initial load *and* every client-side
8
+ * navigation via `<ClientRouter />`), so the buttons keep working after routing
9
+ * to another article/post. Each wrapper is guarded so re-initialising the same
10
+ * DOM node never attaches duplicate listeners.
11
+ */
12
+ export function initArticleCopyActions(root: ParentNode = document): void {
13
+ root.querySelectorAll<HTMLElement>('.copy-actions').forEach(wrapper => {
14
+ if (wrapper.dataset.copyBound === 'true') return;
15
+ wrapper.dataset.copyBound = 'true';
16
+
17
+ const source = wrapper.nextElementSibling as HTMLElement | null;
18
+ const copiedLabel = wrapper.dataset.copiedLabel ?? COPIED_LABEL_FALLBACK;
19
+
20
+ wrapper.querySelectorAll<HTMLButtonElement>('.copy-actions-action').forEach(btn => {
21
+ const label = btn.querySelector<HTMLElement>('span');
22
+ const originalText = label?.textContent;
23
+
24
+ btn.addEventListener('click', async () => {
25
+ const value =
26
+ btn.getAttribute('data-copy') === 'url'
27
+ ? btn.getAttribute('data-value')
28
+ : source?.textContent;
29
+
30
+ if (!value || !label) return;
31
+
32
+ await navigator.clipboard.writeText(value);
33
+
34
+ label.textContent = copiedLabel;
35
+ setTimeout(() => {
36
+ label.textContent = originalText ?? '';
37
+ }, RESET_DELAY_MS);
38
+ });
39
+ });
40
+ });
41
+ }
@@ -9,17 +9,32 @@ export { default as SearchModal } from './preact/SearchModal/components/SearchMo
9
9
  export {
10
10
  DropZone,
11
11
  AnonymousDropClient,
12
+ AuthenticatedBuildClient,
13
+ AuthenticatedDropClient,
14
+ deployBuild,
12
15
  deployFiles,
16
+ detectBuild,
17
+ inferBuildSettings,
13
18
  readDataTransfer,
14
19
  readFileList,
20
+ zipFiles,
15
21
  } from './preact/DropZone';
16
22
  export type {
23
+ AuthenticatedBuildClientConfig,
24
+ AuthenticatedDropClientConfig,
25
+ BuildClient,
26
+ BuildDeployResult,
27
+ BuildDetection,
28
+ BuildSite,
29
+ DeployedBuild,
30
+ DeployedSite,
17
31
  DropClient,
18
32
  DropResponse,
19
33
  Digest,
20
34
  DeployResult,
21
35
  DeployProgress,
22
36
  DeployPhase,
37
+ InferredBuildSettings,
23
38
  ProcessedFile,
24
39
  } from './preact/DropZone';
25
40
  export { type IconName } from './preact/Icon';
@@ -1,8 +1,16 @@
1
1
  import { useEffect, useRef, useState } from 'preact/hooks';
2
2
  import type { ComponentChildren } from 'preact';
3
- import { readDataTransfer, readFileList, type BuildDetection, type DropClient } from './drop-core';
3
+ import {
4
+ getFileTypes,
5
+ readDataTransfer,
6
+ readFileList,
7
+ type BuildClient,
8
+ type BuildDetection,
9
+ type DropClient,
10
+ } from './drop-core';
4
11
  import {
5
12
  useDropDeploy,
13
+ type DeployedBuild,
6
14
  type DeployedSite,
7
15
  type DeployError,
8
16
  type DeployStatus,
@@ -16,6 +24,13 @@ const DRAG_INACTIVE_MS = 120;
16
24
 
17
25
  const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}`;
18
26
 
27
+ /**
28
+ * Where an authenticated static deploy lands: the new site's project overview,
29
+ * the same place the app's own logged-in drop sends you.
30
+ */
31
+ const defaultSiteDashboardUrl = (siteName: string) =>
32
+ `https://app.netlify.com/projects/${siteName}`;
33
+
19
34
  /**
20
35
  * Where a build-required drop is sent by default. Mirrors the app's own Drop
21
36
  * page: signup with `next=/drop`, so after signup + email verification the user
@@ -23,6 +38,15 @@ const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${si
23
38
  */
24
39
  const defaultBuildSignupUrl = 'https://app.netlify.com/signup?next=/drop&utm_campaign=drop';
25
40
 
41
+ /**
42
+ * Where an authenticated build drop lands: the new site's project overview —
43
+ * the same destination as a static deploy, and where the app's own Drop page
44
+ * sends build drops. The overview surfaces the in-progress deploy; consumers
45
+ * who want the deploy-log page instead can override with `build.deployId`.
46
+ */
47
+ const defaultBuildDeployUrl = ({ siteName }: DeployedBuild) =>
48
+ `https://app.netlify.com/projects/${siteName}`;
49
+
26
50
  interface Props {
27
51
  /** Content the drop layer sits on top of. The zone is invisible until you drag over it. */
28
52
  children?: ComponentChildren;
@@ -32,23 +56,81 @@ interface Props {
32
56
  client?: DropClient;
33
57
  /** Build the "claim this site" URL from the assigned site name (subdomain). */
34
58
  claimUrl?: (siteName: string) => string;
59
+ /**
60
+ * Where an authenticated static deploy sends the visitor — the site already
61
+ * lives in their account, so there is no claim step. Defaults to the new
62
+ * site's project overview on app.netlify.com.
63
+ */
64
+ siteDashboardUrl?: (siteName: string) => string;
35
65
  /**
36
66
  * Where to send a build-required drop (framework source, build command,
37
- * functions) when `onBuildRequired` isn't set. Such projects can't deploy
38
- * through the anonymous static drop API, so by default the user is redirected
39
- * here — the app signup page with `next=/drop` — to build on the
40
- * authenticated Drop page. Override to point at your own destination.
67
+ * functions) that can't be built here — no `getUploadToken`/`buildClient`, or a
68
+ * visitor who isn't logged in. Such projects can't deploy through the anonymous
69
+ * static drop API, so by default the user is redirected here — the app signup
70
+ * page with `next=/drop` — to build on the authenticated Drop page. Override to
71
+ * point at your own destination.
41
72
  */
42
73
  buildSignupUrl?: string;
43
74
  /**
44
- * Called when a build-required drop is detected, instead of the default
45
- * `buildSignupUrl` redirect. Use it to own the hand-off yourself (custom
46
- * routing, persisting the drop, gating on auth). When set, the component does
47
- * not redirect. The static drop path is unaffected either way.
75
+ * Mint a short-lived bearer token for direct api.netlify.com uploads — pass it
76
+ * to switch on the whole authenticated path for logged-in visitors: static
77
+ * drops deploy straight into their account (then `siteDashboardUrl`), and
78
+ * build-required drops build in place instead of being handed off to the app.
79
+ * On netlify.com this reads `GET /access-control/generate-access-control-token`.
80
+ * Resolving `null` means "no session": static drops fall back to the anonymous
81
+ * claim flow, build drops to `buildSignupUrl`.
82
+ * See [Logged-in visitors](#logged-in-visitors).
83
+ */
84
+ getUploadToken?: () => Promise<string | null>;
85
+ /**
86
+ * Inject a custom `BuildClient` (tests, staging, a different account flow).
87
+ * Takes precedence over `getUploadToken` for the build path.
88
+ */
89
+ buildClient?: BuildClient;
90
+ /**
91
+ * Inject a custom authenticated `DropClient` for static drops (tests, staging,
92
+ * a different account flow). Takes precedence over `getUploadToken` for the
93
+ * static path. Distinct from `client`, which is the anonymous fallback.
94
+ */
95
+ authenticatedDropClient?: DropClient;
96
+ /**
97
+ * API root for the authenticated paths' JSON calls, which go through your
98
+ * access-control rewrite so the session cookie authorizes them. Defaults to
99
+ * `/access-control/bb-api/api/v1`. (Uploads always go direct to
100
+ * `api.netlify.com` — the proxy can't carry bodies that large.)
101
+ */
102
+ proxyBase?: string;
103
+ /** Create the visitor's new project in this account. Omit for their default account. */
104
+ accountSlug?: string;
105
+ /**
106
+ * Where an enqueued build sends the visitor. Defaults to the new site's
107
+ * project overview, same as a static deploy; `build.deployId` is available for
108
+ * consumers who want the deploy-log page instead.
109
+ */
110
+ buildDeployUrl?: (build: DeployedBuild) => string;
111
+ /**
112
+ * Poll until the build finishes before handing off, instead of redirecting as
113
+ * soon as it's enqueued. The trade-off: waiting means a failing build surfaces
114
+ * its own error message here (via `onError` and the overlay) rather than the
115
+ * visitor discovering it on the deploy page — at the cost of holding them on a
116
+ * spinner for the length of the build, which is minutes. Off by default.
117
+ */
118
+ waitForBuild?: boolean;
119
+ /**
120
+ * Called when a build-required drop is detected, instead of building it or
121
+ * redirecting. Use it to own the hand-off yourself (custom routing, persisting
122
+ * the drop, gating on auth). When set, the component neither builds nor
123
+ * redirects. The static drop path is unaffected either way.
48
124
  */
49
125
  onBuildRequired?: (info: BuildDetection) => void;
50
126
  /** Called once the deploy is live. */
51
127
  onDeploy?: (result: DeployedSite) => void;
128
+ /**
129
+ * Called once a build-required drop has been zipped, uploaded and enqueued —
130
+ * before the redirect to `buildDeployUrl`. The site exists at this point but
131
+ * won't serve content until the build finishes.
132
+ */
133
+ onBuildDeploy?: (build: DeployedBuild) => void;
52
134
  /**
53
135
  * Called when a deploy fails, with a humanized `message` (ready to show a
54
136
  * user — the same copy the built-in overlay uses) and the raw `reason`. Pair
@@ -68,11 +150,22 @@ interface Props {
68
150
  * properties. Stays provider-agnostic — wire it to Segment/Amplitude/GA in the
69
151
  * consuming app, e.g. `onTrack={(name, props) => new Analytics().track(name, props)}`.
70
152
  * Emitted events (all include `dropzone_id` when `analyticsId` is set):
71
- * - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count })
153
+ * - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count, file_types })
72
154
  * - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
73
- * - `dropzone_build_required` — drop needs a build; handed off to signup ({ method, reason, file_count })
74
- * - `dropzone_deploy_succeeded` — deploy went live ({ method, site_name, file_count })
75
- * - `dropzone_deploy_failed` — deploy errored out ({ method, reason, file_count? })
155
+ * - `dropzone_build_required` — drop needs a build (built here, or handed off) ({ method, reason, file_count, file_types })
156
+ * - `dropzone_deploy_succeeded` — static deploy went live ({ method, site_name, file_count, authenticated, auth_fallback?, file_types })
157
+ * - `dropzone_build_deploy_succeeded` — build enqueued for a logged-in visitor ({ method, reason, site_name, file_count, framework?, file_types })
158
+ * - `dropzone_deploy_failed` — deploy or build errored out ({ method, reason, file_count?, file_types })
159
+ *
160
+ * `authenticated` says whether the deploy landed in the visitor's account;
161
+ * `auth_fallback: true` is sent only when an authenticated attempt bounced
162
+ * (stale auth hint) and the drop completed anonymously — omitted otherwise.
163
+ * `file_types` is the sorted, de-duplicated list of MIME types in the
164
+ * selection, with `application/octet-stream` standing in wherever the browser
165
+ * reports none (a dragged folder, an `.exe`). It is omitted when the browser
166
+ * exposes no files at all. Note `dropzone_files_dropped` only fires for drag —
167
+ * a file-picker selection reports its types on the outcome events, as
168
+ * `dropzone_browse_opened` fires before any file exists.
76
169
  */
77
170
  onTrack?: TrackFn;
78
171
  /**
@@ -89,9 +182,18 @@ export default function DropZone({
89
182
  apiBase,
90
183
  client,
91
184
  claimUrl = defaultClaimUrl,
185
+ siteDashboardUrl = defaultSiteDashboardUrl,
92
186
  buildSignupUrl = defaultBuildSignupUrl,
187
+ getUploadToken,
188
+ buildClient,
189
+ authenticatedDropClient,
190
+ proxyBase,
191
+ accountSlug,
192
+ buildDeployUrl = defaultBuildDeployUrl,
193
+ waitForBuild,
93
194
  onBuildRequired,
94
195
  onDeploy,
196
+ onBuildDeploy,
95
197
  onError,
96
198
  showStatus = true,
97
199
  onTrack,
@@ -107,9 +209,18 @@ export default function DropZone({
107
209
  apiBase,
108
210
  client,
109
211
  claimUrl,
212
+ siteDashboardUrl,
110
213
  buildSignupUrl,
214
+ getUploadToken,
215
+ buildClient,
216
+ authenticatedDropClient,
217
+ proxyBase,
218
+ accountSlug,
219
+ buildDeployUrl,
220
+ waitForBuild,
111
221
  onBuildRequired,
112
222
  onDeploy,
223
+ onBuildDeploy,
113
224
  onError,
114
225
  track,
115
226
  });
@@ -156,7 +267,8 @@ export default function DropZone({
156
267
  if (!dt) return;
157
268
  // Capture entries synchronously — readDataTransfer reads them before
158
269
  // its first await, so the DataTransfer isn't detached out from under us.
159
- deploy(() => readDataTransfer(dt), 'drag');
270
+ // Same reason the MIME types are read here and not inside `deploy`.
271
+ deploy(() => readDataTransfer(dt), 'drag', getFileTypes(dt.files));
160
272
  }}
161
273
  onClick={e => {
162
274
  // Only the zone's own empty area opens the picker; clicks that land on
@@ -184,7 +296,11 @@ export default function DropZone({
184
296
  directory=""
185
297
  onChange={e => {
186
298
  const input = e.currentTarget as HTMLInputElement;
187
- if (input.files?.length) deploy(() => readFileList(input.files!), 'browse');
299
+ // Read the types before `input.value = ''` below clears the FileList.
300
+ if (input.files?.length) {
301
+ const files = input.files;
302
+ deploy(() => readFileList(files), 'browse', getFileTypes(files));
303
+ }
188
304
  input.value = '';
189
305
  }}
190
306
  />
@@ -249,20 +365,25 @@ function StatusView({
249
365
 
250
366
  case 'reading':
251
367
  return <Spinner label="Reading files…" />;
368
+ case 'zipping':
369
+ return <Spinner label="Packaging your project…" />;
252
370
  case 'creating':
253
371
  return <Spinner label="Creating deploy…" />;
254
372
  case 'processing':
255
373
  return <Spinner label="Processing — almost live…" />;
374
+ case 'building':
375
+ return <Spinner label="Building your project…" />;
256
376
  case 'redirecting':
257
- return (
258
- <Spinner
259
- label={
260
- status.target === 'signup'
261
- ? 'This project needs a build — taking you to Netlify…'
262
- : 'Site’s live — taking you to claim it…'
263
- }
264
- />
265
- );
377
+ switch (status.target) {
378
+ case 'signup':
379
+ return <Spinner label="This project needs a build — taking you to Netlify…" />;
380
+ case 'build':
381
+ return <Spinner label="Build started — taking you to your new project…" />;
382
+ case 'claim':
383
+ return <Spinner label="Site’s live — taking you to claim it…" />;
384
+ case 'dashboard':
385
+ return <Spinner label="Site’s live — taking you to your new project…" />;
386
+ }
266
387
 
267
388
  case 'uploading': {
268
389
  const pct = status.total ? Math.round((status.uploaded / status.total) * 100) : 0;