@netlify/spark-ui 1.30.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 (51) 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 +76 -14
  7. package/dist/components/preact/DropZone/DropZone.js +111 -84
  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/index.d.ts +3 -0
  21. package/dist/components/preact/DropZone/drop-core/index.js +33 -18
  22. package/dist/components/preact/DropZone/drop-core/readFiles.js +1 -1
  23. package/dist/components/preact/DropZone/drop-core/types.d.ts +6 -1
  24. package/dist/components/preact/DropZone/drop-core/zip.d.ts +46 -0
  25. package/dist/components/preact/DropZone/drop-core/zip.js +43 -0
  26. package/dist/components/preact/DropZone/index.d.ts +3 -2
  27. package/dist/components/preact/DropZone/index.js +27 -11
  28. package/dist/components/preact/DropZone/useDropDeploy.d.ts +35 -4
  29. package/dist/components/preact/DropZone/useDropDeploy.js +139 -70
  30. package/dist/components/preact/index.d.ts +2 -1
  31. package/dist/components/preact/index.js +17 -12
  32. package/package.json +5 -2
  33. package/packages/components/ArticleCopyActions.astro +122 -0
  34. package/packages/components/articleCopyActions.test.ts +143 -0
  35. package/packages/components/articleCopyActions.ts +41 -0
  36. package/packages/components/index.ts +15 -0
  37. package/packages/components/preact/DropZone/DropZone.tsx +123 -20
  38. package/packages/components/preact/DropZone/drop-core/authedDropClient.ts +194 -0
  39. package/packages/components/preact/DropZone/drop-core/buildClient.ts +190 -0
  40. package/packages/components/preact/DropZone/drop-core/client.ts +1 -1
  41. package/packages/components/preact/DropZone/drop-core/deploy.ts +65 -0
  42. package/packages/components/preact/DropZone/drop-core/detectBuild.ts +125 -41
  43. package/packages/components/preact/DropZone/drop-core/errors.ts +84 -1
  44. package/packages/components/preact/DropZone/drop-core/index.ts +3 -0
  45. package/packages/components/preact/DropZone/drop-core/readFiles.ts +1 -1
  46. package/packages/components/preact/DropZone/drop-core/types.ts +12 -1
  47. package/packages/components/preact/DropZone/drop-core/zip.ts +118 -0
  48. package/packages/components/preact/DropZone/index.tsx +23 -0
  49. package/packages/components/preact/DropZone/useDropDeploy.ts +195 -30
  50. package/packages/components/preact/index.ts +2 -1
  51. 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';
@@ -4,11 +4,13 @@ import {
4
4
  getFileTypes,
5
5
  readDataTransfer,
6
6
  readFileList,
7
+ type BuildClient,
7
8
  type BuildDetection,
8
9
  type DropClient,
9
10
  } from './drop-core';
10
11
  import {
11
12
  useDropDeploy,
13
+ type DeployedBuild,
12
14
  type DeployedSite,
13
15
  type DeployError,
14
16
  type DeployStatus,
@@ -22,6 +24,13 @@ const DRAG_INACTIVE_MS = 120;
22
24
 
23
25
  const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}`;
24
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
+
25
34
  /**
26
35
  * Where a build-required drop is sent by default. Mirrors the app's own Drop
27
36
  * page: signup with `next=/drop`, so after signup + email verification the user
@@ -29,6 +38,15 @@ const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${si
29
38
  */
30
39
  const defaultBuildSignupUrl = 'https://app.netlify.com/signup?next=/drop&utm_campaign=drop';
31
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
+
32
50
  interface Props {
33
51
  /** Content the drop layer sits on top of. The zone is invisible until you drag over it. */
34
52
  children?: ComponentChildren;
@@ -38,23 +56,81 @@ interface Props {
38
56
  client?: DropClient;
39
57
  /** Build the "claim this site" URL from the assigned site name (subdomain). */
40
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;
41
65
  /**
42
66
  * Where to send a build-required drop (framework source, build command,
43
- * functions) when `onBuildRequired` isn't set. Such projects can't deploy
44
- * through the anonymous static drop API, so by default the user is redirected
45
- * here — the app signup page with `next=/drop` — to build on the
46
- * 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.
47
72
  */
48
73
  buildSignupUrl?: string;
49
74
  /**
50
- * Called when a build-required drop is detected, instead of the default
51
- * `buildSignupUrl` redirect. Use it to own the hand-off yourself (custom
52
- * routing, persisting the drop, gating on auth). When set, the component does
53
- * 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.
54
124
  */
55
125
  onBuildRequired?: (info: BuildDetection) => void;
56
126
  /** Called once the deploy is live. */
57
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;
58
134
  /**
59
135
  * Called when a deploy fails, with a humanized `message` (ready to show a
60
136
  * user — the same copy the built-in overlay uses) and the raw `reason`. Pair
@@ -76,10 +152,14 @@ interface Props {
76
152
  * Emitted events (all include `dropzone_id` when `analyticsId` is set):
77
153
  * - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count, file_types })
78
154
  * - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
79
- * - `dropzone_build_required` — drop needs a build; handed off to signup ({ method, reason, file_count, file_types })
80
- * - `dropzone_deploy_succeeded` — deploy went live ({ method, site_name, file_count, file_types })
81
- * - `dropzone_deploy_failed` — deploy errored out ({ method, reason, file_count?, file_types })
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 })
82
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.
83
163
  * `file_types` is the sorted, de-duplicated list of MIME types in the
84
164
  * selection, with `application/octet-stream` standing in wherever the browser
85
165
  * reports none (a dragged folder, an `.exe`). It is omitted when the browser
@@ -102,9 +182,18 @@ export default function DropZone({
102
182
  apiBase,
103
183
  client,
104
184
  claimUrl = defaultClaimUrl,
185
+ siteDashboardUrl = defaultSiteDashboardUrl,
105
186
  buildSignupUrl = defaultBuildSignupUrl,
187
+ getUploadToken,
188
+ buildClient,
189
+ authenticatedDropClient,
190
+ proxyBase,
191
+ accountSlug,
192
+ buildDeployUrl = defaultBuildDeployUrl,
193
+ waitForBuild,
106
194
  onBuildRequired,
107
195
  onDeploy,
196
+ onBuildDeploy,
108
197
  onError,
109
198
  showStatus = true,
110
199
  onTrack,
@@ -120,9 +209,18 @@ export default function DropZone({
120
209
  apiBase,
121
210
  client,
122
211
  claimUrl,
212
+ siteDashboardUrl,
123
213
  buildSignupUrl,
214
+ getUploadToken,
215
+ buildClient,
216
+ authenticatedDropClient,
217
+ proxyBase,
218
+ accountSlug,
219
+ buildDeployUrl,
220
+ waitForBuild,
124
221
  onBuildRequired,
125
222
  onDeploy,
223
+ onBuildDeploy,
126
224
  onError,
127
225
  track,
128
226
  });
@@ -267,20 +365,25 @@ function StatusView({
267
365
 
268
366
  case 'reading':
269
367
  return <Spinner label="Reading files…" />;
368
+ case 'zipping':
369
+ return <Spinner label="Packaging your project…" />;
270
370
  case 'creating':
271
371
  return <Spinner label="Creating deploy…" />;
272
372
  case 'processing':
273
373
  return <Spinner label="Processing — almost live…" />;
374
+ case 'building':
375
+ return <Spinner label="Building your project…" />;
274
376
  case 'redirecting':
275
- return (
276
- <Spinner
277
- label={
278
- status.target === 'signup'
279
- ? 'This project needs a build — taking you to Netlify…'
280
- : 'Site’s live — taking you to claim it…'
281
- }
282
- />
283
- );
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
+ }
284
387
 
285
388
  case 'uploading': {
286
389
  const pct = status.total ? Math.round((status.uploaded / status.total) * 100) : 0;