@netlify/spark-ui 1.31.0-alpha.5 → 1.31.0-alpha.7

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 (42) hide show
  1. package/README.md +3 -4
  2. package/dist/components/preact/DropZone/DropZone.d.ts +23 -97
  3. package/dist/components/preact/DropZone/useDropDeploy.js +110 -105
  4. package/dist/drop-core/analytics.d.ts +4 -15
  5. package/dist/drop-core/authedApi.d.ts +9 -22
  6. package/dist/drop-core/authedDropClient.d.ts +13 -55
  7. package/dist/drop-core/authedDropClient.js +5 -18
  8. package/dist/drop-core/buildClient.d.ts +19 -53
  9. package/dist/drop-core/buildClient.js +27 -40
  10. package/dist/drop-core/buildStash.d.ts +6 -20
  11. package/dist/drop-core/buildStash.js +36 -24
  12. package/dist/drop-core/client.d.ts +1 -1
  13. package/dist/drop-core/constants.d.ts +1 -21
  14. package/dist/drop-core/deploy.d.ts +5 -17
  15. package/dist/drop-core/detectBuild.d.ts +8 -26
  16. package/dist/drop-core/errors.d.ts +8 -27
  17. package/dist/drop-core/fileTypes.d.ts +4 -14
  18. package/dist/drop-core/readFiles.d.ts +5 -11
  19. package/dist/drop-core/types.d.ts +2 -4
  20. package/dist/drop-core/zip.d.ts +7 -30
  21. package/package.json +1 -1
  22. package/packages/components/preact/DropZone/DropZone.tsx +27 -121
  23. package/packages/components/preact/DropZone/useDropDeploy.test.ts +12 -0
  24. package/packages/components/preact/DropZone/useDropDeploy.ts +39 -60
  25. package/packages/drop-core/README.md +311 -39
  26. package/packages/drop-core/analytics.ts +4 -15
  27. package/packages/drop-core/authedApi.ts +9 -22
  28. package/packages/drop-core/authedDropClient.ts +13 -55
  29. package/packages/drop-core/buildClient.test.ts +19 -4
  30. package/packages/drop-core/buildClient.ts +21 -63
  31. package/packages/drop-core/buildStash.test.ts +19 -0
  32. package/packages/drop-core/buildStash.ts +38 -29
  33. package/packages/drop-core/client.ts +6 -10
  34. package/packages/drop-core/constants.ts +8 -22
  35. package/packages/drop-core/deploy.ts +6 -22
  36. package/packages/drop-core/detectBuild.ts +15 -50
  37. package/packages/drop-core/errors.test.ts +3 -1
  38. package/packages/drop-core/errors.ts +9 -30
  39. package/packages/drop-core/fileTypes.ts +5 -19
  40. package/packages/drop-core/readFiles.ts +5 -12
  41. package/packages/drop-core/types.ts +2 -4
  42. package/packages/drop-core/zip.ts +7 -30
@@ -2,23 +2,15 @@ import { ProcessedFile } from './types';
2
2
  /** Why a drop was judged to need a build — surfaced to analytics and `onBuildRequired`. */
3
3
  export type BuildReason = 'netlify-toml-build' | 'package-json-build-script' | 'framework-dependency' | 'functions-directory';
4
4
  export interface BuildDetection {
5
- /** True when the dropped project needs a build step and can't be served as static files. */
5
+ /** The drop can't be served as static files. */
6
6
  buildRequired: boolean;
7
7
  /** The signal that decided it (first match wins), or null for a plain static drop. */
8
8
  reason: BuildReason | null;
9
9
  }
10
10
  /**
11
- * Decide whether a dropped project needs a build. Runs on the already-read,
12
- * root-normalized `ProcessedFile[]` (a dropped `.zip` is expanded and the common
13
- * root folder stripped upstream in `readFiles`), so build config sits at the
14
- * deploy root, e.g. `/netlify.toml`, `/package.json`.
15
- *
16
- * The anonymous drop API only performs static deploys, so a build-required
17
- * project can't go through it — the consumer redirects such drops to the
18
- * authenticated Drop page instead. This heuristic is deliberately lightweight
19
- * (no framework-detection dependency): it reads the two config files and looks
20
- * for the zero-config functions directories. A plain static site (HTML/CSS/JS,
21
- * no build config) returns `{ buildRequired: false }`.
11
+ * Does this drop need a build? Deliberately lightweight (two config files +
12
+ * the functions dirs; no framework-detection dependency). Input is
13
+ * root-normalized by `readFiles`, so config sits at `/netlify.toml` etc.
22
14
  */
23
15
  export declare function detectBuild(files: ProcessedFile[]): BuildDetection;
24
16
  /** Build settings confident enough to write into a `netlify.toml` for buildbot. */
@@ -31,19 +23,9 @@ export interface InferredBuildSettings {
31
23
  framework: string;
32
24
  }
33
25
  /**
34
- * Infer the build command and publish directory for a build-required drop, so
35
- * `zipFiles` can write them into a `netlify.toml` that buildbot reads.
36
- *
37
- * Deliberately conservative: it returns settings **only** when both halves are
38
- * known — a `build` script (which gives the command) and a recognised framework
39
- * with a fixed default publish directory (which gives the output). Anything else
40
- * returns `null`, leaving the zip without a synthesised `[build]` table so
41
- * buildbot's own framework detection resolves the settings instead. That matters
42
- * because a `[build]` table with a command but no `publish` makes buildbot
43
- * publish the deploy root — shipping the project's *source* rather than its
44
- * built output, which is worse than not guessing at all.
45
- *
46
- * Returns `null` too when the drop already declares a `[build]` table: the
47
- * author's own config always wins.
26
+ * Build settings for the synthesised netlify.toml — only when BOTH halves are
27
+ * known (build script → command, known framework → publish). Guessing publish
28
+ * wrong ships source instead of output, so `null` beats a bad guess. An
29
+ * author-declared [build] table always wins (also `null`).
48
30
  */
49
31
  export declare function inferBuildSettings(files: ProcessedFile[]): InferredBuildSettings | null;
@@ -1,33 +1,22 @@
1
1
  /**
2
- * Thrown when the user drops or picks a single loose file that Drop can't turn
3
- * into a site on its own — anything that isn't a `.zip` (a site archive) or an
4
- * HTML page (served as index.html). A folder, multiple files, a zip, or a lone
5
- * HTML file are all fine; any other single file is rejected rather than
6
- * deployed as a one-asset site (matches react-ui).
2
+ * A single loose file that can't stand up a site — only a zip, folder,
3
+ * multiple files, or a lone HTML page may deploy (matches react-ui).
7
4
  */
8
5
  export declare class UnsupportedDropError extends Error {
9
6
  readonly code = "unsupported-drop";
10
7
  constructor();
11
8
  }
12
9
  /**
13
- * Thrown when an authenticated path (static or build) turns out to have no
14
- * usable session — the short-lived upload token came back empty, or the
15
- * session-cookie site create was refused, so nothing was created. For static
16
- * drops the flow falls back to the anonymous drop + claim; builds have no
17
- * anonymous equivalent, so the consumer's only recourse is to send the visitor
18
- * through login/signup and have them drop again.
10
+ * The pre-mutation "no session" signal: nothing was created yet, so static
11
+ * drops may fall back to the anonymous flow and build drops stash + hand off.
19
12
  */
20
13
  export declare class NotAuthenticatedError extends Error {
21
14
  readonly code = "not-authenticated";
22
15
  constructor(message?: string);
23
16
  }
24
17
  /**
25
- * Whether an error is the pre-mutation "no session" signal, however it
26
- * traveled. This package ships as both raw TypeScript (`./components`) and
27
- * compiled dist (`./preact`, `./drop-core`), so two copies of the class can
28
- * coexist in one app and defeat `instanceof` — the `code` brand is the
29
- * copy-proof fallback. Control flow that decides whether an anonymous retry
30
- * is safe must use this, never a bare `instanceof`.
18
+ * Copy-proof check for the "no session" error — `instanceof` misses class
19
+ * copies from the package's other entrypoint. Fallback gates must use this.
31
20
  */
32
21
  export declare function isNotAuthenticatedError(err: unknown): err is NotAuthenticatedError;
33
22
  /** `POST …/sites` failed, so there is nothing to build into. Carries the HTTP status. */
@@ -45,12 +34,7 @@ export declare class BuildCreateError extends Error {
45
34
  status?: number;
46
35
  constructor(status?: number);
47
36
  }
48
- /**
49
- * The build ran and ended in `error`/`rejected`. `reason` is the API's
50
- * `error_message` when there is one — it's the only part of a build failure that
51
- * says anything actionable (a failing build command, a missing dependency), so
52
- * it is surfaced to the user rather than swallowed.
53
- */
37
+ /** The build ran and broke. `reason` carries the API's `error_message` — the only actionable part. */
54
38
  export declare class BuildFailedError extends Error {
55
39
  reason?: string | undefined;
56
40
  readonly code = "build-failed";
@@ -61,10 +45,7 @@ export declare class BuildTimeoutError extends Error {
61
45
  readonly code = "build-timeout";
62
46
  constructor();
63
47
  }
64
- /**
65
- * A static deploy ran and ended in `error`/`rejected`. Like a build failure,
66
- * the API's `error_message` is the actionable part, so it travels on the error.
67
- */
48
+ /** A static deploy ran and ended in `error`/`rejected`; `reason` carries the API's `error_message`. */
68
49
  export declare class DeployFailedError extends Error {
69
50
  reason?: string | undefined;
70
51
  readonly code = "deploy-failed";
@@ -1,17 +1,7 @@
1
1
  /**
2
- * Distinct MIME types across a drop or file-picker selection, sorted so the same
3
- * selection always yields the same array.
4
- *
5
- * Read from the raw `File.type` at the drop boundary rather than from
6
- * `ProcessedFile`s: the MIME type only exists on the original `File`, and reading
7
- * can throw before any `ProcessedFile` does (an unsupported single file) — the
8
- * very case the file type is most worth knowing about.
9
- *
10
- * Browsers leave `File.type` empty for anything they don't recognise — a dragged
11
- * folder, an `.exe`, an extensionless file — so those collapse to
12
- * `application/octet-stream` and the property never carries an empty string.
13
- *
14
- * Call this synchronously inside the drop handler: a `DataTransfer` detaches once
15
- * the event turn ends.
2
+ * Distinct, sorted MIME types of a selection. Read at the drop boundary — the
3
+ * type only exists on the original File, and reading can throw before any
4
+ * ProcessedFile exists. Call synchronously in the drop handler: a DataTransfer
5
+ * detaches after the event turn.
16
6
  */
17
7
  export declare function getFileTypes(files: FileList | null | undefined): string[];
@@ -1,21 +1,15 @@
1
1
  import { ProcessedFile } from './types';
2
2
  /**
3
- * Read a DataTransfer from a drop event into ProcessedFiles.
4
- *
5
- * NOTE: `webkitGetAsEntry()` must be called synchronously inside the drop
6
- * handler — the DataTransfer is detached once the event turn ends. The
7
- * component captures entries first (see `readDroppedEntries`) then calls the
8
- * async readers, so this function is safe to `await`.
3
+ * Read a drop's DataTransfer into ProcessedFiles. `webkitGetAsEntry()` runs
4
+ * synchronously before the first await — the DataTransfer detaches once the
5
+ * event turn ends.
9
6
  */
10
7
  export declare function readDataTransfer(dt: DataTransfer): Promise<ProcessedFile[]>;
11
8
  export declare function readFileList(files: FileList): Promise<ProcessedFile[]>;
12
9
  export declare function getSingleNonIndexHtmlFileName(files: ProcessedFile[]): string | null;
13
10
  export declare function hasMultipleHtmlFilesWithoutIndex(files: ProcessedFile[]): boolean;
14
11
  /**
15
- * When the set has no index.html but exactly one HTML file (a lone page
16
- * alongside its assets, or a single dropped file), rename that file to
17
- * index.html — keeping its directory — so it is served at the site root
18
- * instead of 404ing. Content and sha are untouched. Any other set (already has
19
- * an index, multiple HTML files, or no HTML) is returned unchanged.
12
+ * A lone non-index HTML page is renamed to index.html (same directory,
13
+ * content/sha untouched) so it serves instead of 404ing. Other sets unchanged.
20
14
  */
21
15
  export declare function renameSingleNonIndexHtmlToIndex(files: ProcessedFile[]): ProcessedFile[];
@@ -9,10 +9,8 @@ export interface ProcessedFile {
9
9
  }
10
10
  export type Digest = Record<string, string>;
11
11
  /**
12
- * Response body of `POST /api/v1/drop`. Not a canonical Deploy resource —
13
- * it's a mixed shape carrying both the anonymous site and the initial deploy
14
- * so the client has everything it needs to upload, poll, and hand off to the
15
- * user in one round trip.
12
+ * Response of `POST /api/v1/drop` — a mixed site+deploy shape, everything the
13
+ * client needs to upload, poll, and hand off in one round trip.
16
14
  */
17
15
  export interface DropResponse {
18
16
  /** site id (UUID) — surfaced to callers so they can build claim links */
@@ -1,46 +1,23 @@
1
1
  import { InferredBuildSettings } from './detectBuild';
2
2
  import { ProcessedFile } from './types';
3
- /**
4
- * Render inferred settings as a `[build]` table. Only `command` and `publish`
5
- * are written: everything else (functions directory, plugins, node version)
6
- * is zero-config on Netlify's side and guessing at it would override detection
7
- * that is better informed than we are.
8
- */
3
+ /** Renders a [build] table — command and publish only; everything else is zero-config server-side. */
9
4
  export declare function generateNetlifyToml({ command, publish }: InferredBuildSettings): string;
10
5
  export interface ZipOptions {
11
6
  /** Filename given to the produced `File`. Shows up as the build's source archive. */
12
7
  name?: string;
13
- /**
14
- * Synthesise a `netlify.toml` `[build]` table from `inferBuildSettings` when the
15
- * drop doesn't declare one (default `true`). Set `false` to upload the files
16
- * exactly as dropped and let buildbot resolve every setting itself.
17
- */
8
+ /** Synthesise a [build] table when the drop lacks one (default true); false uploads as-dropped. */
18
9
  synthesizeNetlifyToml?: boolean;
19
10
  }
20
11
  export interface ZipResult {
21
12
  /** The archive, ready to hand to `createBuildFromZip`. */
22
13
  file: File;
23
- /**
24
- * The settings written into `netlify.toml`, or `null` when the archive is a
25
- * faithful copy of the drop — either because it already declared a `[build]`
26
- * table, or because nothing could be inferred with confidence.
27
- */
14
+ /** Settings written into netlify.toml, or null when the archive is a faithful copy. */
28
15
  buildSettings: InferredBuildSettings | null;
29
16
  }
30
17
  /**
31
- * Zip an already-read file set into the source archive that
32
- * `POST /sites/{id}/builds` expects.
33
- *
34
- * The input is the same root-normalized `ProcessedFile[]` the static deploy path
35
- * uses (see `readFiles`), so junk — dotfiles, `node_modules`, macOS metadata — is
36
- * already gone and paths sit at the deploy root. Output directories (`dist/`,
37
- * `build/`) are deliberately *not* stripped: buildbot regenerates them when
38
- * there's a build command, and a drop whose `netlify.toml` publishes a committed
39
- * directory without a command would break if we removed it.
40
- *
41
- * When the drop needs a build but declares no `[build]` table, an inferred one is
42
- * written in (appended to an existing `netlify.toml`, or created if there is
43
- * none) so buildbot doesn't publish the project's source. See
44
- * `inferBuildSettings` for how conservative that inference is.
18
+ * Zip a read file set into the archive `POST /sites/{id}/builds` expects.
19
+ * Junk is already gone (readFiles); dist/build are deliberately kept — a drop
20
+ * publishing a committed directory without a command would break without them.
21
+ * A [build] table is synthesised when missing (appended, so redirects survive).
45
22
  */
46
23
  export declare function zipFiles(files: ProcessedFile[], { name, synthesizeNetlifyToml }?: ZipOptions): Promise<ZipResult>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@netlify/spark-ui",
3
3
  "description": "Assets, design tokens, components, and utilities",
4
- "version": "1.31.0-alpha.5",
4
+ "version": "1.31.0-alpha.7",
5
5
  "type": "module",
6
6
  "main": "dist/components/preact/index.js",
7
7
  "types": "dist/components/preact/index.d.ts",
@@ -25,26 +25,13 @@ const DRAG_INACTIVE_MS = 120;
25
25
 
26
26
  const defaultClaimUrl = (siteName: string) => `https://app.netlify.com/drop/${siteName}`;
27
27
 
28
- /**
29
- * Where an authenticated static deploy lands: the new site's project overview,
30
- * the same place the app's own logged-in drop sends you.
31
- */
28
+ /** The project overview — where the app's own logged-in drop lands too. */
32
29
  const defaultSiteDashboardUrl = (siteName: string) =>
33
30
  `https://app.netlify.com/projects/${siteName}`;
34
31
 
35
- /**
36
- * Where a build-required drop is sent by default. Mirrors the app's own Drop
37
- * page: signup with `next=/drop`, so after signup + email verification the user
38
- * lands back on the authenticated Drop page and can build there.
39
- */
32
+ // Mirrors the app's own Drop page hand-off.
40
33
  const defaultBuildSignupUrl = 'https://app.netlify.com/signup?next=/drop&utm_campaign=drop';
41
34
 
42
- /**
43
- * Where an authenticated build drop lands: the new site's project overview —
44
- * the same destination as a static deploy, and where the app's own Drop page
45
- * sends build drops. The overview surfaces the in-progress deploy; consumers
46
- * who want the deploy-log page instead can override with `build.deployId`.
47
- */
48
35
  const defaultBuildDeployUrl = ({ siteName }: DeployedBuild) =>
49
36
  `https://app.netlify.com/projects/${siteName}`;
50
37
 
@@ -57,29 +44,17 @@ interface Props {
57
44
  client?: DropClient;
58
45
  /** Build the "claim this site" URL from the assigned site name (subdomain). */
59
46
  claimUrl?: (siteName: string) => string;
60
- /**
61
- * Where an authenticated static deploy sends the visitor — the site already
62
- * lives in their account, so there is no claim step. Defaults to the new
63
- * site's project overview on app.netlify.com.
64
- */
47
+ /** Destination after an authenticated static deploy (no claim step). Default: the project overview. */
65
48
  siteDashboardUrl?: (siteName: string) => string;
66
49
  /**
67
- * The signup hand-off for a build-required drop that can't be built here (no
68
- * session, or no build wiring). The drop is stashed on this origin first, so
69
- * a flow that returns the visitor to this page deploys it automatically —
70
- * see `resumeStashedBuilds`. Defaults to the app signup page with
71
- * `next=/drop`.
50
+ * Signup hand-off for a build drop with no session. The drop is stashed on
51
+ * this origin first — return the visitor here and it deploys automatically.
72
52
  */
73
53
  buildSignupUrl?: string;
74
54
  /**
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 are stashed and handed to `buildSignupUrl`.
82
- * See [Logged-in visitors](#logged-in-visitors).
55
+ * Mint the upload bearer — passing this switches on the whole authenticated
56
+ * path. `null` means "no session": static drops fall back to the anonymous
57
+ * claim flow; build drops stash and hand off. See the docs' Logged-in visitors.
83
58
  */
84
59
  getUploadToken?: () => Promise<string | null>;
85
60
  /**
@@ -87,108 +62,46 @@ interface Props {
87
62
  * Takes precedence over `getUploadToken` for the build path.
88
63
  */
89
64
  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
- */
65
+ /** Custom authenticated static-drop client; wins over `getUploadToken`. (`client` is the anonymous fallback.) */
95
66
  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
- */
67
+ /** Root for the session-cookie JSON calls, via your access-control rewrite. Uploads always go direct. */
102
68
  proxyBase?: string;
103
69
  /** Create the visitor's new project in this account. Omit for their default account. */
104
70
  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
- */
71
+ /** Destination after a build is enqueued. Default: the project overview (`build.deployId` for the logs page). */
110
72
  buildDeployUrl?: (build: DeployedBuild) => string;
111
73
  /**
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.
74
+ * Hold the visitor until the build finishes (off by default — builds take
75
+ * minutes). On is the only way a failing build's own message reaches `onError`.
117
76
  */
118
77
  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.
124
- */
78
+ /** Take over build-required drops entirely — when set, the component neither builds nor redirects. */
125
79
  onBuildRequired?: (info: BuildDetection) => void;
126
80
  /**
127
- * Called instead of the `buildSignupUrl` redirect when a build-required drop
128
- * can't be built here (no session). By this point the drop has been stashed
129
- * on this origin (`stashed` says whether that succeeded) — own the hand-off
130
- * however fits your auth flow, e.g. open a signup popup that keeps the
131
- * visitor on the page. Once they're back authenticated, a remount of the
132
- * zone deploys the stash automatically.
81
+ * Own the signup hand-off instead of the `buildSignupUrl` redirect (e.g. an
82
+ * auth popup). The drop is already stashed; a remount while authenticated
83
+ * deploys it.
133
84
  */
134
85
  onBuildSignupHandoff?: (info: { stashed: boolean; detection: BuildDetection }) => void;
135
- /**
136
- * Deploy a previously stashed build drop automatically when the zone mounts
137
- * with an authenticated visitor (default `true`). The stash is written when a
138
- * logged-out build drop is handed to signup; see
139
- * [Signup hand-off and auto-resume](#signup-hand-off-and-auto-resume).
140
- */
86
+ /** Auto-deploy a stashed build drop on mount when authenticated (default true). */
141
87
  resumeStashedBuilds?: boolean;
142
88
  /** Called once the deploy is live. */
143
89
  onDeploy?: (result: DeployedSite) => void;
144
- /**
145
- * Called once a build-required drop has been zipped, uploaded and enqueued —
146
- * before the redirect to `buildDeployUrl`. The site exists at this point but
147
- * won't serve content until the build finishes.
148
- */
90
+ /** Called once a build is enqueued, before the `buildDeployUrl` redirect. */
149
91
  onBuildDeploy?: (build: DeployedBuild) => void;
150
- /**
151
- * Called when a deploy fails, with a humanized `message` (ready to show a
152
- * user — the same copy the built-in overlay uses) and the raw `reason`. Pair
153
- * this with `showStatus={false}` to render your own error UI.
154
- */
92
+ /** Deploy failures: humanized `message` (the overlay's copy) + raw `reason`. Pair with showStatus={false}. */
155
93
  onError?: (error: DeployError) => void;
156
94
  /**
157
- * Render the built-in status overlay (drag tint, spinner, progress bar, error).
158
- * Set to `false` to suppress it entirely and keep your children visible, then
159
- * drive your own feedback from the `data-state` attribute on the root element.
160
- * Note: in this mode you're responsible for surfacing errors yourself — use
161
- * `onError` to get the message.
95
+ * Render the built-in overlay. `false` keeps children visible — drive your
96
+ * own feedback from `data-state`, and surface errors yourself via `onError`.
162
97
  */
163
98
  showStatus?: boolean;
164
99
  /**
165
- * Analytics hook. Called at each tracked moment with an event name and
166
- * properties. Stays provider-agnostic — wire it to Segment/Amplitude/GA in the
167
- * consuming app, e.g. `onTrack={(name, props) => new Analytics().track(name, props)}`.
168
- * Emitted events (all include `dropzone_id` when `analyticsId` is set):
169
- * - `dropzone_files_dropped` — files dropped via drag ({ method: 'drag', file_count, file_types })
170
- * - `dropzone_browse_opened` — file picker opened ({ method: 'keyboard' | 'click' })
171
- * - `dropzone_build_required` — drop needs a build (built here, or handed off) ({ method, reason, file_count, file_types })
172
- * - `dropzone_deploy_succeeded` — static deploy went live ({ method, site_name, file_count, authenticated, auth_fallback?, file_types })
173
- * - `dropzone_build_deploy_succeeded` — build enqueued for a logged-in visitor ({ method, reason, site_name, file_count, framework?, file_types })
174
- * - `dropzone_deploy_failed` — deploy or build errored out ({ method, reason, file_count?, file_types })
175
- *
176
- * `authenticated` says whether the deploy landed in the visitor's account;
177
- * `auth_fallback: true` is sent only when an authenticated attempt bounced
178
- * (stale auth hint) and the drop completed anonymously — omitted otherwise.
179
- * `file_types` is the sorted, de-duplicated list of MIME types in the
180
- * selection, with `application/octet-stream` standing in wherever the browser
181
- * reports none (a dragged folder, an `.exe`). It is omitted when the browser
182
- * exposes no files at all. Note `dropzone_files_dropped` only fires for drag —
183
- * a file-picker selection reports its types on the outcome events, as
184
- * `dropzone_browse_opened` fires before any file exists.
100
+ * Provider-agnostic analytics hook. Event names and payload shapes live in
101
+ * DROPZONE_EVENTS / DropZoneEventProperties (drop-core/analytics.ts).
185
102
  */
186
103
  onTrack?: TrackFn;
187
- /**
188
- * Stable identifier for this instance, attached to every tracked event as
189
- * `dropzone_id`. Set it when a page renders more than one Drop Zone so the
190
- * events can be told apart in Amplitude.
191
- */
104
+ /** Stamped on every event as `dropzone_id` — set when a page has more than one zone. */
192
105
  analyticsId?: string;
193
106
  className?: string;
194
107
  }
@@ -218,8 +131,6 @@ export default function DropZone({
218
131
  analyticsId,
219
132
  className,
220
133
  }: Props) {
221
- // Tag every event with this instance's id so multiple zones on a page can be
222
- // told apart, then hand it off to the consumer's analytics.
223
134
  const track: TrackFn = (event, properties = {}) =>
224
135
  onTrack?.(event, analyticsId ? { dropzone_id: analyticsId, ...properties } : properties);
225
136
 
@@ -253,8 +164,6 @@ export default function DropZone({
253
164
  inputRef.current?.click();
254
165
  };
255
166
 
256
- // The overlay only paints when there's something to show — dragging, deploying,
257
- // or an error. With `showStatus` off, the consumer drives their own feedback.
258
167
  const showOverlay = showStatus && (active || busy || status.kind === 'error');
259
168
  const rootClass = [
260
169
  'n-dropzone',
@@ -343,11 +252,8 @@ export default function DropZone({
343
252
  }
344
253
 
345
254
  /**
346
- * Track whether a drag is currently over the zone. A drag has no reliable
347
- * "cancelled" event — pressing Escape or dropping elsewhere just stops the
348
- * stream of `dragover` events — so we stay active only while they keep arriving
349
- * and clear shortly after they stop. This also handles drags over children,
350
- * where `dragleave` targets a child rather than the zone.
255
+ * Drag-over tracking by dragover decay: drags have no reliable cancel event,
256
+ * and dragleave targets children — so stay active while events keep arriving.
351
257
  */
352
258
  function useDragActive() {
353
259
  const [active, setActive] = useState(false);
@@ -287,6 +287,18 @@ describe('build-required drops', () => {
287
287
 
288
288
  expect(saveDropStash).not.toHaveBeenCalled();
289
289
  expect(harness.assigned).toEqual(['https://app.netlify.com/signup?next=/drop']);
290
+ // Nothing was stashed, so the drop is lost rather than deferred — reported
291
+ // with the same reason react-ui uses for its own build bounce.
292
+ const failed = harness.events.find(e => e.event === DROPZONE_EVENTS.DEPLOY_FAILED);
293
+ expect(failed?.props).toMatchObject({ reason: 'anonymous_build_not_supported' });
294
+ });
295
+
296
+ it('does not report a loss when the archive was stashed', async () => {
297
+ const harness = mountHook({ buildClient: makeBuildClient('no-auth'), getUploadToken: async () => null });
298
+ await harness.api.deploy(async () => buildFiles(), 'drag', []);
299
+ await settle();
300
+
301
+ expect(harness.events.map(e => e.event)).not.toContain(DROPZONE_EVENTS.DEPLOY_FAILED);
290
302
  });
291
303
 
292
304
  it('cedes everything to onBuildRequired when set', async () => {