@cliff-studio/sanity-plugin-bunny-input 2.0.0 → 2.0.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/index.js CHANGED
@@ -1,5 +1,860 @@
1
- import "sanity";
2
- import { bunnyInput, getMp4Url, getPlaybackUrl, getThumbnailUrl } from "./_chunks-es/index.js";
1
+ import { set, unset, defineType, defineField, definePlugin } from "sanity";
2
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
+ import { useState, useRef, useCallback, useEffect } from "react";
4
+ import { Dialog, Box, TextInput, Card, Text, Flex, Spinner, Grid, Stack, Button } from "@sanity/ui";
5
+ import { UploadIcon } from "@sanity/icons/Upload";
6
+ import { SearchIcon } from "@sanity/icons/Search";
7
+ import { VideoIcon } from "@sanity/icons/Video";
8
+ const BUNNY_API_BASE = "https://video.bunnycdn.com";
9
+ function mapBunnyStatus(status) {
10
+ switch (status) {
11
+ case 0:
12
+ // created
13
+ case 1:
14
+ return "uploading";
15
+ case 2:
16
+ // processing
17
+ case 3:
18
+ return "processing";
19
+ case 4:
20
+ return "ready";
21
+ case 5:
22
+ // error
23
+ default:
24
+ return "error";
25
+ }
26
+ }
27
+ function getThumbnailUrl(cdnHostname, videoId) {
28
+ return `https://${cdnHostname}/${videoId}/thumbnail.jpg`;
29
+ }
30
+ function getPlaybackUrl(cdnHostname, videoId) {
31
+ return `https://${cdnHostname}/${videoId}/playlist.m3u8`;
32
+ }
33
+ function getMp4Url(cdnHostname, videoId, availableResolutions = "") {
34
+ const parsedResolutions = (availableResolutions || "").split(",").map((s) => Number.parseInt(String(s.trim()).replace(/p$/i, ""), 10)).filter((n) => Number.isFinite(n)), bestResolution = parsedResolutions.length ? Math.max(...parsedResolutions) : 720;
35
+ return `https://${cdnHostname}/${videoId}/play_${bestResolution}p.mp4`;
36
+ }
37
+ function createHeaders(config, includeJsonContentType = !1, useDirectApi = !1) {
38
+ const headers = {
39
+ Accept: "application/json"
40
+ };
41
+ return includeJsonContentType && (headers["Content-Type"] = "application/json"), config.apiKey && (useDirectApi || !config.proxyEndpoint) && (headers.AccessKey = config.apiKey), headers;
42
+ }
43
+ function getCollectionListUrl(config, searchTerm) {
44
+ return config.proxyEndpoint && !config.apiKey ? `${config.proxyEndpoint}/collections?search=${encodeURIComponent(searchTerm)}` : `${BUNNY_API_BASE}/library/${config.libraryId}/collections?search=${encodeURIComponent(
45
+ searchTerm
46
+ )}&page=1&itemsPerPage=100`;
47
+ }
48
+ function getCreateCollectionUrl(config) {
49
+ return config.proxyEndpoint && !config.apiKey ? `${config.proxyEndpoint}/collections` : `${BUNNY_API_BASE}/library/${config.libraryId}/collections`;
50
+ }
51
+ async function getCollectionList(config, searchTerm) {
52
+ const url = getCollectionListUrl(config, searchTerm), response = await fetch(url, {
53
+ headers: createHeaders(config, !1, !config.proxyEndpoint || !!config.apiKey)
54
+ });
55
+ if (!response.ok) {
56
+ const error = await response.text();
57
+ throw new Error(`Failed to list collections: ${error}`);
58
+ }
59
+ return (await response.json()).items || [];
60
+ }
61
+ async function getOrCreateCollection(config, collectionName) {
62
+ const normalizedCollectionName = collectionName?.trim();
63
+ if (!normalizedCollectionName)
64
+ throw new Error("Collection name cannot be empty");
65
+ const existingCollection = (await getCollectionList(config, normalizedCollectionName)).find(
66
+ (collection) => collection.name?.toLowerCase() === normalizedCollectionName.toLowerCase()
67
+ );
68
+ if (existingCollection)
69
+ return existingCollection;
70
+ const response = await fetch(getCreateCollectionUrl(config), {
71
+ method: "POST",
72
+ headers: createHeaders(config, !0, !config.proxyEndpoint || !!config.apiKey),
73
+ body: JSON.stringify({ name: normalizedCollectionName })
74
+ });
75
+ if (!response.ok) {
76
+ const error = await response.text();
77
+ throw new Error(`Failed to create collection: ${error}`);
78
+ }
79
+ return response.json();
80
+ }
81
+ async function createVideo(config, title, collectionId) {
82
+ const url = config.proxyEndpoint ? `${config.proxyEndpoint}/create` : `${BUNNY_API_BASE}/library/${config.libraryId}/videos`, headers = createHeaders(config, !0), response = await fetch(url, {
83
+ method: "POST",
84
+ headers,
85
+ body: JSON.stringify({
86
+ title,
87
+ ...collectionId ? { collectionId } : {}
88
+ })
89
+ });
90
+ if (!response.ok) {
91
+ const error = await response.text();
92
+ throw new Error(`Failed to create video: ${error}`);
93
+ }
94
+ return response.json();
95
+ }
96
+ async function uploadVideo(config, videoId, file, onProgress) {
97
+ const url = config.proxyEndpoint ? `${config.proxyEndpoint}/upload/${videoId}` : `${BUNNY_API_BASE}/library/${config.libraryId}/videos/${videoId}`;
98
+ return new Promise((resolve, reject) => {
99
+ const xhr = new XMLHttpRequest();
100
+ xhr.upload.addEventListener("progress", (event) => {
101
+ if (event.lengthComputable && onProgress) {
102
+ const progress = Math.round(event.loaded / event.total * 100);
103
+ onProgress(progress);
104
+ }
105
+ }), xhr.addEventListener("load", () => {
106
+ if (xhr.status >= 200 && xhr.status < 300)
107
+ try {
108
+ const response = JSON.parse(xhr.responseText);
109
+ resolve(response);
110
+ } catch {
111
+ resolve({ success: !0, message: "OK" });
112
+ }
113
+ else
114
+ reject(new Error(`Upload failed: ${xhr.statusText}`));
115
+ }), xhr.addEventListener("error", () => {
116
+ reject(new Error("Upload failed: Network error"));
117
+ }), xhr.open("PUT", url), xhr.setRequestHeader("Accept", "application/json"), !config.proxyEndpoint && config.apiKey && xhr.setRequestHeader("AccessKey", config.apiKey), xhr.send(file);
118
+ });
119
+ }
120
+ async function getVideo(config, videoId) {
121
+ const url = config.proxyEndpoint ? `${config.proxyEndpoint}/video/${videoId}` : `${BUNNY_API_BASE}/library/${config.libraryId}/videos/${videoId}`, headers = createHeaders(config), response = await fetch(url, { headers });
122
+ if (!response.ok) {
123
+ const error = await response.text();
124
+ throw new Error(`Failed to get video: ${error}`);
125
+ }
126
+ return response.json();
127
+ }
128
+ async function listVideos(config, { collectionId, search = "", page = 1, itemsPerPage = 50 } = {}) {
129
+ const params = new URLSearchParams({
130
+ page: String(page),
131
+ itemsPerPage: String(itemsPerPage),
132
+ orderBy: "date"
133
+ });
134
+ collectionId && params.set("collection", collectionId), search && params.set("search", search);
135
+ const url = config.proxyEndpoint ? `${config.proxyEndpoint}/videos?${params}` : `${BUNNY_API_BASE}/library/${config.libraryId}/videos?${params}`, response = await fetch(url, {
136
+ headers: createHeaders(config, !1, !config.proxyEndpoint || !!config.apiKey)
137
+ });
138
+ if (!response.ok) {
139
+ const error = await response.text();
140
+ throw new Error(`Failed to list videos: ${error}`);
141
+ }
142
+ const payload = await response.json();
143
+ return {
144
+ items: payload.items || [],
145
+ totalItems: payload.totalItems ?? 0,
146
+ currentPage: payload.currentPage ?? page,
147
+ itemsPerPage: payload.itemsPerPage ?? itemsPerPage
148
+ };
149
+ }
150
+ async function deleteVideo(config, videoId) {
151
+ const url = config.proxyEndpoint ? `${config.proxyEndpoint}/video/${videoId}` : `${BUNNY_API_BASE}/library/${config.libraryId}/videos/${videoId}`, headers = createHeaders(config), response = await fetch(url, {
152
+ method: "DELETE",
153
+ headers
154
+ });
155
+ if (!response.ok) {
156
+ const error = await response.text();
157
+ throw new Error(`Failed to delete video: ${error}`);
158
+ }
159
+ return response.json();
160
+ }
161
+ const ITEMS_PER_PAGE = 48;
162
+ function BunnyBrowserModal({ config, collectionId: collectionIdProp, onSelect, onClose }) {
163
+ const [videos, setVideos] = useState([]), [search, setSearch] = useState(""), [page, setPage] = useState(1), [totalItems, setTotalItems] = useState(0), [isLoading, setIsLoading] = useState(!1), [error, setError] = useState(null), [resolvedCollectionId, setResolvedCollectionId] = useState(collectionIdProp || null), searchTimeoutRef = useRef(null), fetchVideos = useCallback(
164
+ async (searchTerm, pageNum, collId) => {
165
+ setIsLoading(!0), setError(null);
166
+ try {
167
+ const result = await listVideos(config, {
168
+ collectionId: collId,
169
+ search: searchTerm,
170
+ page: pageNum,
171
+ itemsPerPage: ITEMS_PER_PAGE
172
+ });
173
+ setVideos(result.items), setTotalItems(result.totalItems);
174
+ } catch (err) {
175
+ setError(err instanceof Error ? err.message : "Failed to load videos");
176
+ } finally {
177
+ setIsLoading(!1);
178
+ }
179
+ },
180
+ [config]
181
+ );
182
+ useEffect(() => {
183
+ async function init() {
184
+ let collId = collectionIdProp || null;
185
+ if (!collId && config.collectionName)
186
+ try {
187
+ collId = (await getOrCreateCollection(config, config.collectionName)).guid, setResolvedCollectionId(collId);
188
+ } catch (err) {
189
+ console.warn("Could not resolve collection, browsing full library:", err);
190
+ }
191
+ fetchVideos("", 1, collId);
192
+ }
193
+ init();
194
+ }, []);
195
+ const handleSearchChange = useCallback(
196
+ (e) => {
197
+ const value = e.target.value;
198
+ setSearch(value), setPage(1), searchTimeoutRef.current && clearTimeout(searchTimeoutRef.current), searchTimeoutRef.current = setTimeout(() => {
199
+ fetchVideos(value, 1, resolvedCollectionId);
200
+ }, 400);
201
+ },
202
+ [fetchVideos, resolvedCollectionId]
203
+ ), handlePageChange = useCallback(
204
+ (newPage) => {
205
+ setPage(newPage), fetchVideos(search, newPage, resolvedCollectionId);
206
+ },
207
+ [fetchVideos, search, resolvedCollectionId]
208
+ ), handleSelect = useCallback(
209
+ (video) => {
210
+ if (mapBunnyStatus(video.status) !== "ready") return;
211
+ const width = Number(video.width), height = Number(video.height), hasDims = Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0, ratio = hasDims ? Number((width / height).toFixed(6)) : null, orientation = hasDims ? width > height ? "landscape" : width < height ? "portrait" : "square" : null;
212
+ onSelect({
213
+ _type: "bunnyVideo",
214
+ videoId: video.guid,
215
+ libraryId: String(video.videoLibraryId),
216
+ title: video.title,
217
+ status: "ready",
218
+ duration: video.length,
219
+ width: hasDims ? width : null,
220
+ height: hasDims ? height : null,
221
+ ratio,
222
+ orientation,
223
+ thumbnailUrl: getThumbnailUrl(config.cdnHostname, video.guid),
224
+ playbackUrl: getPlaybackUrl(config.cdnHostname, video.guid),
225
+ mp4Url: getMp4Url(config.cdnHostname, video.guid, video.availableResolutions),
226
+ ...resolvedCollectionId ? { collectionId: resolvedCollectionId } : {},
227
+ ...video.collectionId ? { collectionId: video.collectionId } : {}
228
+ });
229
+ },
230
+ [config, resolvedCollectionId, onSelect]
231
+ ), totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);
232
+ return /* @__PURE__ */ jsx(
233
+ Dialog,
234
+ {
235
+ header: "Browse videos",
236
+ id: "bunny-browser-modal",
237
+ onClose,
238
+ width: 3,
239
+ children: /* @__PURE__ */ jsxs(Box, { padding: 4, children: [
240
+ /* @__PURE__ */ jsx(Box, { marginBottom: 4, children: /* @__PURE__ */ jsx(
241
+ TextInput,
242
+ {
243
+ icon: SearchIcon,
244
+ placeholder: "Search videos\u2026",
245
+ value: search,
246
+ onChange: handleSearchChange
247
+ }
248
+ ) }),
249
+ error && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", marginBottom: 4, children: /* @__PURE__ */ jsx(Text, { size: 1, children: error }) }),
250
+ isLoading ? /* @__PURE__ */ jsx(Flex, { align: "center", justify: "center", padding: 6, children: /* @__PURE__ */ jsx(Spinner, {}) }) : videos.length === 0 ? /* @__PURE__ */ jsx(Flex, { align: "center", justify: "center", padding: 6, children: /* @__PURE__ */ jsx(Text, { muted: !0, size: 1, children: search ? "No videos found matching your search" : "No videos in this collection" }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
251
+ /* @__PURE__ */ jsx(Grid, { columns: [2, 2, 3, 4], gap: 3, children: videos.map((video) => {
252
+ const status = mapBunnyStatus(video.status), isReady = status === "ready", thumbUrl = getThumbnailUrl(config.cdnHostname, video.guid);
253
+ return /* @__PURE__ */ jsxs(
254
+ Card,
255
+ {
256
+ radius: 2,
257
+ shadow: 1,
258
+ style: {
259
+ cursor: isReady ? "pointer" : "not-allowed",
260
+ opacity: isReady ? 1 : 0.5
261
+ },
262
+ onClick: () => isReady && handleSelect(video),
263
+ children: [
264
+ /* @__PURE__ */ jsxs(
265
+ Box,
266
+ {
267
+ style: {
268
+ position: "relative",
269
+ paddingBottom: "56.25%",
270
+ backgroundColor: "#111",
271
+ borderRadius: "2px",
272
+ overflow: "hidden"
273
+ },
274
+ children: [
275
+ /* @__PURE__ */ jsx(
276
+ "img",
277
+ {
278
+ src: thumbUrl,
279
+ alt: video.title || "Video thumbnail",
280
+ style: {
281
+ position: "absolute",
282
+ top: 0,
283
+ left: 0,
284
+ width: "100%",
285
+ height: "100%",
286
+ objectFit: "cover"
287
+ }
288
+ }
289
+ ),
290
+ !isReady && /* @__PURE__ */ jsx(
291
+ Flex,
292
+ {
293
+ align: "center",
294
+ justify: "center",
295
+ style: {
296
+ position: "absolute",
297
+ inset: 0,
298
+ backgroundColor: "rgba(0,0,0,0.5)"
299
+ },
300
+ children: /* @__PURE__ */ jsx(Text, { size: 0, style: { color: "#fff" }, children: status === "processing" ? "Processing\u2026" : status })
301
+ }
302
+ )
303
+ ]
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsx(Box, { padding: 3, children: /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
307
+ /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: video.title || "Untitled" }),
308
+ video.length > 0 && /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
309
+ Math.floor(video.length / 60),
310
+ ":",
311
+ String(Math.floor(video.length % 60)).padStart(2, "0")
312
+ ] })
313
+ ] }) })
314
+ ]
315
+ },
316
+ video.guid
317
+ );
318
+ }) }),
319
+ totalPages > 1 && /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "center", gap: 2, marginTop: 4, children: [
320
+ /* @__PURE__ */ jsx(
321
+ Button,
322
+ {
323
+ text: "Previous",
324
+ mode: "ghost",
325
+ disabled: page <= 1,
326
+ onClick: () => handlePageChange(page - 1)
327
+ }
328
+ ),
329
+ /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
330
+ "Page ",
331
+ page,
332
+ " of ",
333
+ totalPages
334
+ ] }),
335
+ /* @__PURE__ */ jsx(
336
+ Button,
337
+ {
338
+ text: "Next",
339
+ mode: "ghost",
340
+ disabled: page >= totalPages,
341
+ onClick: () => handlePageChange(page + 1)
342
+ }
343
+ )
344
+ ] })
345
+ ] })
346
+ ] })
347
+ }
348
+ );
349
+ }
350
+ function BunnyInput(props) {
351
+ const { value, onChange, config } = props, [uploadProgress, setUploadProgress] = useState(0), [isUploading, setIsUploading] = useState(!1), [error, setError] = useState(null), [isBrowserOpen, setIsBrowserOpen] = useState(!1), fileInputRef = useRef(null), pollIntervalRef = useRef(null);
352
+ useEffect(() => (value?.videoId && value?.status === "processing" && (pollIntervalRef.current = window.setInterval(async () => {
353
+ try {
354
+ const videoData = await getVideo(config, value.videoId), status = mapBunnyStatus(videoData.status);
355
+ if (status === "ready" || status === "error") {
356
+ pollIntervalRef.current && (clearInterval(pollIntervalRef.current), pollIntervalRef.current = null);
357
+ const width = Number(videoData.width), height = Number(videoData.height), hasDims = Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0, ratio = hasDims ? Number((width / height).toFixed(6)) : null, orientation = hasDims ? width > height ? "landscape" : width < height ? "portrait" : "square" : null;
358
+ onChange(
359
+ set({
360
+ ...value,
361
+ status,
362
+ duration: videoData.length,
363
+ height: hasDims ? height : null,
364
+ width: hasDims ? width : null,
365
+ ratio,
366
+ orientation,
367
+ thumbnailUrl: getThumbnailUrl(config.cdnHostname, value.videoId),
368
+ playbackUrl: getPlaybackUrl(config.cdnHostname, value.videoId),
369
+ mp4Url: getMp4Url(
370
+ config.cdnHostname,
371
+ value.videoId,
372
+ videoData.availableResolutions
373
+ )
374
+ })
375
+ );
376
+ }
377
+ } catch (err) {
378
+ console.error("Error polling video status:", err);
379
+ }
380
+ }, 5e3)), () => {
381
+ pollIntervalRef.current && clearInterval(pollIntervalRef.current);
382
+ }), [value?.videoId, value?.status, config, onChange, value]);
383
+ const handleFileSelect = useCallback(
384
+ async (file) => {
385
+ if (!file.type.startsWith("video/")) {
386
+ setError("Please select a video file");
387
+ return;
388
+ }
389
+ setError(null), setIsUploading(!0), setUploadProgress(0);
390
+ try {
391
+ const title = file.name.replace(/\.[^/.]+$/, "");
392
+ let collectionId = config.collectionId, collectionName = config.collectionName;
393
+ if (!collectionId && collectionName) {
394
+ const collection = await getOrCreateCollection(config, collectionName);
395
+ collectionId = collection.guid, collectionName = collection.name;
396
+ }
397
+ const created = await createVideo(config, title, collectionId);
398
+ onChange(
399
+ set({
400
+ _type: "bunnyVideo",
401
+ videoId: created.guid,
402
+ libraryId: String(created.videoLibraryId),
403
+ title,
404
+ status: "uploading",
405
+ ...collectionId ? { collectionId } : {},
406
+ ...collectionName ? { collectionName } : {}
407
+ })
408
+ ), await uploadVideo(config, created.guid, file, setUploadProgress), onChange(
409
+ set({
410
+ _type: "bunnyVideo",
411
+ videoId: created.guid,
412
+ libraryId: String(created.videoLibraryId),
413
+ title,
414
+ status: "processing",
415
+ ...collectionId ? { collectionId } : {},
416
+ ...collectionName ? { collectionName } : {}
417
+ })
418
+ );
419
+ } catch (err) {
420
+ console.error("Upload error:", err), setError(err instanceof Error ? err.message : "Upload failed"), onChange(
421
+ set({
422
+ ...value,
423
+ status: "error",
424
+ errorMessage: err instanceof Error ? err.message : "Upload failed"
425
+ })
426
+ );
427
+ } finally {
428
+ setIsUploading(!1), setUploadProgress(0);
429
+ }
430
+ },
431
+ [config, onChange, value]
432
+ ), handleInputChange = useCallback(
433
+ (e) => {
434
+ const file = e.target.files?.[0];
435
+ file && handleFileSelect(file);
436
+ },
437
+ [handleFileSelect]
438
+ ), handleBrowseSelect = useCallback(
439
+ (videoData) => {
440
+ setIsBrowserOpen(!1), onChange(set(videoData));
441
+ },
442
+ [onChange]
443
+ ), handleRemove = useCallback(async () => {
444
+ if (value?.videoId)
445
+ try {
446
+ await deleteVideo(config, value.videoId);
447
+ } catch (err) {
448
+ console.error("Error deleting from Bunny:", err);
449
+ }
450
+ onChange(unset());
451
+ }, [config, value, onChange]);
452
+ return value?.videoId && value?.status === "ready" ? /* @__PURE__ */ jsxs(Fragment, { children: [
453
+ /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, shadow: 1, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
454
+ /* @__PURE__ */ jsx(
455
+ Box,
456
+ {
457
+ style: {
458
+ position: "relative",
459
+ paddingBottom: "56.25%",
460
+ backgroundColor: "#000",
461
+ borderRadius: "4px",
462
+ overflow: "hidden"
463
+ },
464
+ children: value.thumbnailUrl && /* @__PURE__ */ jsx(
465
+ "img",
466
+ {
467
+ src: value.thumbnailUrl,
468
+ alt: value.title || "Video thumbnail",
469
+ style: {
470
+ position: "absolute",
471
+ top: 0,
472
+ left: 0,
473
+ width: "100%",
474
+ height: "100%",
475
+ objectFit: "contain"
476
+ }
477
+ }
478
+ )
479
+ }
480
+ ),
481
+ /* @__PURE__ */ jsxs(Flex, { justify: "space-between", align: "center", children: [
482
+ /* @__PURE__ */ jsxs(Stack, { space: 2, children: [
483
+ /* @__PURE__ */ jsx(Text, { size: 1, weight: "semibold", children: value.title }),
484
+ value.duration && /* @__PURE__ */ jsxs(Text, { size: 1, muted: !0, children: [
485
+ Math.floor(value.duration / 60),
486
+ ":",
487
+ String(Math.floor(value.duration % 60)).padStart(2, "0")
488
+ ] })
489
+ ] }),
490
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
491
+ /* @__PURE__ */ jsx(
492
+ Button,
493
+ {
494
+ text: "Browse",
495
+ mode: "ghost",
496
+ onClick: () => setIsBrowserOpen(!0)
497
+ }
498
+ ),
499
+ /* @__PURE__ */ jsx(Button, { text: "Remove", tone: "critical", mode: "ghost", onClick: handleRemove })
500
+ ] })
501
+ ] })
502
+ ] }) }),
503
+ isBrowserOpen && /* @__PURE__ */ jsx(
504
+ BunnyBrowserModal,
505
+ {
506
+ config,
507
+ collectionId: value.collectionId || config.collectionId,
508
+ onSelect: handleBrowseSelect,
509
+ onClose: () => setIsBrowserOpen(!1)
510
+ }
511
+ )
512
+ ] }) : value?.status === "processing" ? /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, shadow: 1, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", align: "center", justify: "center", gap: 3, children: [
513
+ /* @__PURE__ */ jsx(Spinner, {}),
514
+ /* @__PURE__ */ jsx(Text, { size: 1, children: "Processing video..." }),
515
+ /* @__PURE__ */ jsx(Text, { size: 0, muted: !0, children: "This may take a few minutes" })
516
+ ] }) }) : isUploading ? /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, shadow: 1, children: /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
517
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", align: "center", justify: "center", gap: 2, children: [
518
+ /* @__PURE__ */ jsx(Spinner, {}),
519
+ /* @__PURE__ */ jsxs(Text, { size: 1, children: [
520
+ "Uploading... ",
521
+ uploadProgress,
522
+ "%"
523
+ ] })
524
+ ] }),
525
+ /* @__PURE__ */ jsx(
526
+ Box,
527
+ {
528
+ style: {
529
+ height: "4px",
530
+ backgroundColor: "#e5e5e5",
531
+ borderRadius: "2px",
532
+ overflow: "hidden"
533
+ },
534
+ children: /* @__PURE__ */ jsx(
535
+ Box,
536
+ {
537
+ style: {
538
+ height: "100%",
539
+ width: `${uploadProgress}%`,
540
+ backgroundColor: "#2563eb",
541
+ transition: "width 0.3s ease"
542
+ }
543
+ }
544
+ )
545
+ }
546
+ )
547
+ ] }) }) : error || value?.status === "error" ? /* @__PURE__ */ jsxs(Stack, { space: 3, children: [
548
+ /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "critical", children: /* @__PURE__ */ jsxs(Text, { size: 1, children: [
549
+ "Error: ",
550
+ error || value?.errorMessage
551
+ ] }) }),
552
+ /* @__PURE__ */ jsx(
553
+ "input",
554
+ {
555
+ ref: fileInputRef,
556
+ type: "file",
557
+ accept: "video/*",
558
+ onChange: handleInputChange
559
+ }
560
+ )
561
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
562
+ /* @__PURE__ */ jsx("div", { style: { padding: 1 }, children: /* @__PURE__ */ jsx(Card, { tone: "inherit", border: !0, paddingX: 3, paddingY: 2, radius: 2, children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 4, justify: "space-between", children: [
563
+ /* @__PURE__ */ jsxs(Flex, { flex: 1, align: "center", gap: 3, children: [
564
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: /* @__PURE__ */ jsx(VideoIcon, {}) }),
565
+ /* @__PURE__ */ jsx(Text, { size: 1, muted: !0, children: "Drag or paste video here" })
566
+ ] }),
567
+ /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 1, children: [
568
+ /* @__PURE__ */ jsxs("label", { style: { display: "contents" }, children: [
569
+ /* @__PURE__ */ jsx(
570
+ Button,
571
+ {
572
+ as: "span",
573
+ icon: UploadIcon,
574
+ text: "Upload",
575
+ mode: "bleed",
576
+ padding: 2,
577
+ style: { cursor: "pointer" }
578
+ }
579
+ ),
580
+ /* @__PURE__ */ jsx(
581
+ "input",
582
+ {
583
+ ref: fileInputRef,
584
+ type: "file",
585
+ accept: "video/*",
586
+ onChange: handleInputChange,
587
+ style: {
588
+ position: "absolute",
589
+ width: "1px",
590
+ height: "1px",
591
+ overflow: "hidden",
592
+ opacity: 0
593
+ }
594
+ }
595
+ )
596
+ ] }),
597
+ /* @__PURE__ */ jsx(
598
+ Button,
599
+ {
600
+ icon: SearchIcon,
601
+ text: "Select",
602
+ mode: "bleed",
603
+ padding: 2,
604
+ onClick: () => setIsBrowserOpen(!0)
605
+ }
606
+ )
607
+ ] })
608
+ ] }) }) }),
609
+ isBrowserOpen && /* @__PURE__ */ jsx(
610
+ BunnyBrowserModal,
611
+ {
612
+ config,
613
+ collectionId: config.collectionId,
614
+ onSelect: handleBrowseSelect,
615
+ onClose: () => setIsBrowserOpen(!1)
616
+ }
617
+ )
618
+ ] });
619
+ }
620
+ function BunnyPreview(props) {
621
+ const { title, status, thumbnailUrl } = props, statusColors = {
622
+ uploading: "#f59e0b",
623
+ processing: "#3b82f6",
624
+ ready: "#22c55e",
625
+ error: "#ef4444"
626
+ }, statusLabels = {
627
+ uploading: "Uploading",
628
+ processing: "Processing",
629
+ ready: "Ready",
630
+ error: "Error"
631
+ };
632
+ return /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, padding: 2, children: [
633
+ thumbnailUrl ? /* @__PURE__ */ jsx(
634
+ Box,
635
+ {
636
+ style: {
637
+ width: 80,
638
+ height: 45,
639
+ borderRadius: 4,
640
+ overflow: "hidden",
641
+ backgroundColor: "#000",
642
+ flexShrink: 0
643
+ },
644
+ children: /* @__PURE__ */ jsx(
645
+ "img",
646
+ {
647
+ src: thumbnailUrl,
648
+ alt: title || "Video thumbnail",
649
+ style: {
650
+ width: "100%",
651
+ height: "100%",
652
+ objectFit: "cover"
653
+ }
654
+ }
655
+ )
656
+ }
657
+ ) : /* @__PURE__ */ jsx(
658
+ Box,
659
+ {
660
+ style: {
661
+ width: 80,
662
+ height: 45,
663
+ borderRadius: 4,
664
+ backgroundColor: "#e5e5e5",
665
+ display: "flex",
666
+ alignItems: "center",
667
+ justifyContent: "center",
668
+ flexShrink: 0
669
+ },
670
+ children: /* @__PURE__ */ jsx(
671
+ "svg",
672
+ {
673
+ width: "24",
674
+ height: "24",
675
+ viewBox: "0 0 24 24",
676
+ fill: "none",
677
+ stroke: "currentColor",
678
+ strokeWidth: "1.5",
679
+ strokeLinecap: "round",
680
+ strokeLinejoin: "round",
681
+ style: { opacity: 0.4 },
682
+ children: /* @__PURE__ */ jsx("polygon", { points: "5 3 19 12 5 21 5 3" })
683
+ }
684
+ )
685
+ }
686
+ ),
687
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 1, style: { minWidth: 0 }, children: [
688
+ /* @__PURE__ */ jsx(
689
+ Text,
690
+ {
691
+ size: 1,
692
+ weight: "semibold",
693
+ style: {
694
+ overflow: "hidden",
695
+ textOverflow: "ellipsis",
696
+ whiteSpace: "nowrap"
697
+ },
698
+ children: title || "Untitled video"
699
+ }
700
+ ),
701
+ status && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, children: [
702
+ /* @__PURE__ */ jsx(
703
+ Box,
704
+ {
705
+ style: {
706
+ width: 8,
707
+ height: 8,
708
+ borderRadius: "50%",
709
+ backgroundColor: statusColors[status] || "#9ca3af"
710
+ }
711
+ }
712
+ ),
713
+ /* @__PURE__ */ jsx(Text, { size: 0, muted: !0, children: statusLabels[status] || status })
714
+ ] })
715
+ ] })
716
+ ] });
717
+ }
718
+ const createBunnyVideoSchema = (config) => defineType({
719
+ type: "object",
720
+ title: "Bunny Video",
721
+ name: "bunnyVideo",
722
+ fields: [
723
+ defineField({
724
+ type: "string",
725
+ title: "Video ID",
726
+ name: "videoId",
727
+ readOnly: !0
728
+ }),
729
+ defineField({
730
+ type: "string",
731
+ title: "Library ID",
732
+ name: "libraryId",
733
+ readOnly: !0
734
+ }),
735
+ defineField({
736
+ type: "string",
737
+ title: "Collection ID",
738
+ name: "collectionId",
739
+ readOnly: !0
740
+ }),
741
+ defineField({
742
+ type: "string",
743
+ title: "Collection name",
744
+ name: "collectionName",
745
+ readOnly: !0
746
+ }),
747
+ defineField({
748
+ type: "string",
749
+ title: "Title",
750
+ name: "title"
751
+ }),
752
+ defineField({
753
+ type: "string",
754
+ title: "Status",
755
+ name: "status",
756
+ options: {
757
+ list: [
758
+ { title: "Uploading", value: "uploading" },
759
+ { title: "Processing", value: "processing" },
760
+ { title: "Ready", value: "ready" },
761
+ { title: "Error", value: "error" }
762
+ ]
763
+ },
764
+ readOnly: !0
765
+ }),
766
+ defineField({
767
+ type: "number",
768
+ title: "Video width",
769
+ name: "width",
770
+ readOnly: !0
771
+ }),
772
+ defineField({
773
+ type: "number",
774
+ title: "Video height",
775
+ name: "height",
776
+ readOnly: !0
777
+ }),
778
+ defineField({
779
+ type: "number",
780
+ title: "Video ratio",
781
+ name: "ratio",
782
+ readOnly: !0
783
+ }),
784
+ defineField({
785
+ type: "string",
786
+ title: "Orientation",
787
+ name: "orientation",
788
+ options: {
789
+ list: [
790
+ { title: "Landscape", value: "landscape" },
791
+ { title: "Portrait", value: "portrait" },
792
+ { title: "Square", value: "square" }
793
+ ]
794
+ },
795
+ readOnly: !0
796
+ }),
797
+ defineField({
798
+ type: "string",
799
+ title: "Thumbnail URL",
800
+ name: "thumbnailUrl",
801
+ readOnly: !0
802
+ }),
803
+ defineField({
804
+ type: "string",
805
+ title: "Playback URL",
806
+ name: "playbackUrl",
807
+ readOnly: !0
808
+ }),
809
+ defineField({
810
+ type: "string",
811
+ title: "MP4 URL",
812
+ name: "mp4Url",
813
+ readOnly: !0
814
+ }),
815
+ defineField({
816
+ type: "number",
817
+ title: "Duration (seconds)",
818
+ name: "duration",
819
+ readOnly: !0
820
+ }),
821
+ defineField({
822
+ type: "string",
823
+ title: "Error Message",
824
+ name: "errorMessage",
825
+ readOnly: !0,
826
+ hidden: !0
827
+ })
828
+ ],
829
+ components: {
830
+ input: (props) => BunnyInput({ ...props, config }),
831
+ preview: BunnyPreview
832
+ },
833
+ preview: {
834
+ select: {
835
+ title: "title",
836
+ status: "status",
837
+ thumbnailUrl: "thumbnailUrl"
838
+ }
839
+ }
840
+ }), bunnyInput = definePlugin((config) => {
841
+ if (!config.libraryId)
842
+ throw new Error("sanity-plugin-bunny-input: libraryId is required");
843
+ if (!config.cdnHostname)
844
+ throw new Error("sanity-plugin-bunny-input: cdnHostname is required (e.g., vz-abc123-xyz.b-cdn.net)");
845
+ if (!config.apiKey && !config.proxyEndpoint)
846
+ throw new Error("sanity-plugin-bunny-input: Either apiKey or proxyEndpoint is required");
847
+ const normalizedCollectionName = typeof config.collectionName == "string" ? config.collectionName.trim() : void 0, pluginConfig = {
848
+ ...config,
849
+ ...normalizedCollectionName ? { collectionName: normalizedCollectionName } : {}
850
+ };
851
+ return {
852
+ name: "sanity-plugin-bunny-input",
853
+ schema: {
854
+ types: [createBunnyVideoSchema(pluginConfig)]
855
+ }
856
+ };
857
+ });
3
858
  export {
4
859
  bunnyInput,
5
860
  getMp4Url,