@devultur/convex 1.5.0 → 1.5.2

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/react.js CHANGED
@@ -1,4 +1,9 @@
1
1
  "use client";
2
+ import {
3
+ useMediaUrl,
4
+ useRequestTokenWhen,
5
+ useVideoProps
6
+ } from "./chunk-AUAWZ5BD.js";
2
7
  import {
3
8
  mediaFromRecord
4
9
  } from "./chunk-KAIVI7RB.js";
@@ -24,57 +29,32 @@ import {
24
29
  import {
25
30
  describeDevulturError
26
31
  } from "./chunk-YYOZ3PDH.js";
32
+ import {
33
+ DevulturApiContext,
34
+ DevulturCapabilityError,
35
+ DevulturContext,
36
+ DevulturProgressContext,
37
+ useCanDeliver,
38
+ useDevultur,
39
+ useDevulturApi,
40
+ useDevulturOptional,
41
+ useDevulturProgress
42
+ } from "./chunk-FOBFHC4A.js";
27
43
 
28
44
  // src/react.tsx
29
- import React24 from "react";
45
+ import React26 from "react";
46
+ import { mediaPath as mediaPath5 } from "@devultur/core";
47
+ import { uploadFileWithProgress as uploadFileWithProgress2 } from "@devultur/react";
48
+ import { useAction as useAction3, useConvex, useMutation } from "convex/react";
49
+
50
+ // src/media-session.ts
51
+ import React2 from "react";
30
52
  import {
31
- buildMediaUrl as buildMediaUrl2,
32
- CaptionLocaleLabel,
33
- MediaStatus,
34
- mediaCaptionPath as mediaCaptionPath2,
35
- mediaPath as mediaPath3,
36
- mediaUrl as mediaUrl4,
37
- visibilityFromKey
53
+ buildMediaUrl,
54
+ mediaCaptionPath,
55
+ mediaUrl,
56
+ SESSION_HEADER
38
57
  } from "@devultur/core";
39
- import { UploadHttpError, uploadFileWithProgress } from "@devultur/react";
40
- import { useAction as useAction2, useConvex, useMutation, useQuery } from "convex/react";
41
-
42
- // src/devultur-context.ts
43
- import React from "react";
44
-
45
- // src/capability-error.ts
46
- var DevulturCapabilityError = class extends Error {
47
- code = "capability_unavailable";
48
- capability;
49
- constructor(capability) {
50
- super(
51
- `Devultur: \`${capability}\` isn't available under <DevulturViewerProvider>. Use <DevulturProvider api> with the actions it needs.`
52
- );
53
- this.name = "DevulturCapabilityError";
54
- this.capability = capability;
55
- }
56
- };
57
-
58
- // src/devultur-context.ts
59
- var DevulturContext = /* @__PURE__ */ React.createContext(null);
60
- var DevulturApiContext = /* @__PURE__ */ React.createContext(null);
61
- function useDevultur() {
62
- const context = React.use(DevulturContext);
63
- if (!context) throw new Error("useDevultur must be used within <DevulturProvider> or <DevulturViewerProvider>");
64
- return context;
65
- }
66
- function useDevulturOptional() {
67
- return React.use(DevulturContext);
68
- }
69
- function useDevulturApi() {
70
- const api = React.use(DevulturApiContext);
71
- if (api) return api;
72
- if (React.use(DevulturContext)) throw new DevulturCapabilityError("api");
73
- throw new Error("useDevulturApi must be used within <DevulturProvider>");
74
- }
75
-
76
- // src/media-fetch.ts
77
- import { SESSION_HEADER } from "@devultur/core";
78
58
 
79
59
  // src/image-recovery.ts
80
60
  async function isSessionMismatch(response) {
@@ -86,105 +66,8 @@ async function isSessionMismatch(response) {
86
66
  }
87
67
  }
88
68
 
89
- // src/media-fetch.ts
90
- var MediaFetchError = class extends Error {
91
- code;
92
- /** HTTP status, for `http_error` when the delivery answered. */
93
- status;
94
- /** The code's extra fields, e.g. `page_cap`'s `{ shown, total }`. */
95
- details;
96
- constructor(code, message, status, options) {
97
- super(message, options);
98
- this.name = "MediaFetchError";
99
- this.code = code;
100
- this.status = status;
101
- this.details = options?.details;
102
- }
103
- };
104
- function networkError(cause) {
105
- return new MediaFetchError("http_error", "Media fetch failed: no response", void 0, { cause });
106
- }
107
- function pageCapError(shown, total) {
108
- const message = `Showing the first ${shown} of ${total} pages`;
109
- return new MediaFetchError("page_cap", message, void 0, { details: { shown, total } });
110
- }
111
- function renderFailedError(cause) {
112
- return new MediaFetchError("render_failed", "The media couldn't be rendered", void 0, { cause });
113
- }
114
- function tokenUnavailableError(cause) {
115
- return new MediaFetchError("token_unavailable", "No viewer token: its issuance failed", void 0, { cause });
116
- }
117
- async function fetchMedia(request) {
118
- const { buildUrl, token, sessionSecret, reissueToken, signal, isPrivate = true, canReissue = true } = request;
119
- const headers = isPrivate && sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0;
120
- const response = await fetch(buildUrl(isPrivate ? token ?? void 0 : void 0), { headers, signal });
121
- if (!isPrivate || !canReissue || response.status !== 401) return response;
122
- if (!await isSessionMismatch(response)) return response;
123
- request.onReissue?.();
124
- const freshToken = await reissueToken();
125
- if (!freshToken) return response;
126
- return fetch(buildUrl(freshToken), { headers, signal });
127
- }
128
- async function loadMedia(request) {
129
- const response = await fetchMedia(request);
130
- if (!response.ok) {
131
- throw new MediaFetchError("http_error", `Media fetch failed: ${response.status}`, response.status);
132
- }
133
- const trusted = trustedContentType(request.contentType, response.headers.get("content-type"));
134
- if (!trusted.ok) {
135
- await response.body?.cancel();
136
- throw new MediaFetchError(trusted.reason, `Refusing to preview: ${trusted.reason}`);
137
- }
138
- const bytes = await readMediaBody(response, request.maxBytes);
139
- return { bytes, contentType: trusted.contentType };
140
- }
141
- function tooLarge(maxBytes) {
142
- return new MediaFetchError("too_large", `Media is larger than ${maxBytes} bytes`);
143
- }
144
- async function readMediaBody(response, maxBytes) {
145
- if (maxBytes === void 0) return new Uint8Array(await response.arrayBuffer());
146
- const declaredBytes = Number(response.headers.get("content-length") ?? Number.NaN);
147
- if (declaredBytes > maxBytes) {
148
- await response.body?.cancel();
149
- throw tooLarge(maxBytes);
150
- }
151
- if (!response.body) return new Uint8Array(0);
152
- return readCapped(response.body, maxBytes);
153
- }
154
- async function readCapped(body, maxBytes) {
155
- const reader = body.getReader();
156
- const chunks = [];
157
- let byteCount = 0;
158
- for (; ; ) {
159
- const { done, value } = await reader.read();
160
- if (done) break;
161
- byteCount += value.byteLength;
162
- if (byteCount > maxBytes) {
163
- await reader.cancel();
164
- throw tooLarge(maxBytes);
165
- }
166
- chunks.push(value);
167
- }
168
- const bytes = new Uint8Array(byteCount);
169
- let offset = 0;
170
- for (const chunk of chunks) {
171
- bytes.set(chunk, offset);
172
- offset += chunk.byteLength;
173
- }
174
- return bytes;
175
- }
176
-
177
- // src/media-session.ts
178
- import React3 from "react";
179
- import {
180
- buildMediaUrl,
181
- mediaCaptionPath,
182
- mediaUrl,
183
- SESSION_HEADER as SESSION_HEADER2
184
- } from "@devultur/core";
185
-
186
69
  // src/viewer-token.ts
187
- import React2 from "react";
70
+ import React from "react";
188
71
  import { DEFAULT_TOKEN_EXPIRY_SECONDS } from "@devultur/core";
189
72
  import { convexToJson } from "convex/values";
190
73
  var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
@@ -212,27 +95,27 @@ function tokenArgsKey(tokenArgs) {
212
95
  }
213
96
  function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs, isLazy = false }) {
214
97
  const argsKey = tokenArgsKey(tokenArgs);
215
- const stableArgs = React2.useMemo(() => tokenArgs ?? {}, [argsKey]);
216
- const [issued, setIssued] = React2.useState(null);
217
- const [attempt, setAttempt] = React2.useState(null);
218
- const [isRequested, setIsRequested] = React2.useState(!isLazy);
98
+ const stableArgs = React.useMemo(() => tokenArgs ?? {}, [argsKey]);
99
+ const [issued, setIssued] = React.useState(null);
100
+ const [attempt, setAttempt] = React.useState(null);
101
+ const [isRequested, setIsRequested] = React.useState(!isLazy);
219
102
  const token = issued?.argsKey === argsKey ? issued.token : null;
220
103
  const currentAttempt = attempt?.argsKey === argsKey ? attempt : null;
221
- const currentArgsKeyRef = React2.useRef(argsKey);
222
- const refreshTimerRef = React2.useRef(null);
223
- const inFlightRef = React2.useRef(null);
224
- const isMountedRef = React2.useRef(true);
225
- React2.useEffect(() => {
104
+ const currentArgsKeyRef = React.useRef(argsKey);
105
+ const refreshTimerRef = React.useRef(null);
106
+ const inFlightRef = React.useRef(null);
107
+ const isMountedRef = React.useRef(true);
108
+ React.useEffect(() => {
226
109
  currentArgsKeyRef.current = argsKey;
227
110
  }, [argsKey]);
228
- React2.useEffect(() => {
111
+ React.useEffect(() => {
229
112
  isMountedRef.current = true;
230
113
  return () => {
231
114
  isMountedRef.current = false;
232
115
  if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
233
116
  };
234
117
  }, []);
235
- const request = React2.useCallback(async () => {
118
+ const request = React.useCallback(async () => {
236
119
  const renew = () => {
237
120
  request().catch((error) => console.warn("Devultur: could not renew the viewer token", error));
238
121
  };
@@ -245,7 +128,7 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs, isLazy = f
245
128
  refreshTimerRef.current = setTimeout(renew, Math.max(ttlMs - REFRESH_BUFFER_MS, MIN_REFRESH_MS));
246
129
  return tokenValue;
247
130
  }, [issueViewerToken, stableArgs, argsKey, sessionSecret]);
248
- const issueOnce = React2.useCallback(() => {
131
+ const issueOnce = React.useCallback(() => {
249
132
  const inFlight = inFlightRef.current;
250
133
  if (inFlight?.argsKey === argsKey) return inFlight.promise;
251
134
  setAttempt({ argsKey, status: "loading" });
@@ -261,20 +144,20 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs, isLazy = f
261
144
  return promise;
262
145
  }, [request, argsKey]);
263
146
  const hasAttempt = currentAttempt !== null;
264
- React2.useEffect(() => {
147
+ React.useEffect(() => {
265
148
  if (!isRequested || token || hasAttempt) return;
266
149
  issueOnce().catch(() => {
267
150
  });
268
151
  }, [isRequested, token, hasAttempt, issueOnce]);
269
- const requestToken = React2.useCallback(() => setIsRequested(true), []);
270
- const reissueToken = React2.useCallback(
152
+ const requestToken = React.useCallback(() => setIsRequested(true), []);
153
+ const reissueToken = React.useCallback(
271
154
  () => issueOnce().catch(() => {
272
155
  setIssued((current) => current?.argsKey === argsKey ? null : current);
273
156
  return null;
274
157
  }),
275
158
  [issueOnce, argsKey]
276
159
  );
277
- const retryToken = React2.useCallback(() => {
160
+ const retryToken = React.useCallback(() => {
278
161
  setIsRequested(true);
279
162
  return issueOnce().catch(() => null);
280
163
  }, [issueOnce]);
@@ -285,20 +168,20 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs, isLazy = f
285
168
 
286
169
  // src/media-session.ts
287
170
  function useMediaSession({ issueViewerToken, tokenArgs, baseUrl, isLazy }) {
288
- const [sessionSecret] = React3.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
171
+ const [sessionSecret] = React2.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
289
172
  const { token, tokenStatus, tokenError, reissueToken, retryToken, requestToken } = useViewerToken({
290
173
  issueViewerToken,
291
174
  sessionSecret,
292
175
  tokenArgs,
293
176
  isLazy
294
177
  });
295
- const hasRequestedRef = React3.useRef(!isLazy);
296
- const requestTokenAfterRender = React3.useCallback(() => {
178
+ const hasRequestedRef = React2.useRef(!isLazy);
179
+ const requestTokenAfterRender = React2.useCallback(() => {
297
180
  if (hasRequestedRef.current) return;
298
181
  hasRequestedRef.current = true;
299
182
  queueMicrotask(requestToken);
300
183
  }, [requestToken]);
301
- const url = React3.useCallback(
184
+ const url = React2.useCallback(
302
185
  (media, options) => {
303
186
  if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
304
187
  if (media.visibility === "private" && !token) requestTokenAfterRender();
@@ -306,14 +189,14 @@ function useMediaSession({ issueViewerToken, tokenArgs, baseUrl, isLazy }) {
306
189
  },
307
190
  [baseUrl, token, requestTokenAfterRender]
308
191
  );
309
- const getCaptionsText = React3.useCallback(
192
+ const getCaptionsText = React2.useCallback(
310
193
  async (media, locale) => {
311
194
  if (!baseUrl) throw new Error("Devultur `getCaptionsText()` requires `baseUrl` on <DevulturProvider>");
312
195
  const path = mediaCaptionPath(media, locale);
313
196
  if (!path) throw new Error("getCaptionsText: the reference is not a video");
314
197
  const isPrivate = media.visibility === "private";
315
198
  const fetchWith = (viewerToken2) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken2 }), {
316
- headers: sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0
199
+ headers: sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0
317
200
  });
318
201
  const viewerToken = isPrivate ? token ?? await retryToken() : null;
319
202
  let response = await fetchWith(viewerToken ?? void 0);
@@ -326,7 +209,7 @@ function useMediaSession({ issueViewerToken, tokenArgs, baseUrl, isLazy }) {
326
209
  },
327
210
  [baseUrl, token, reissueToken, retryToken, sessionSecret]
328
211
  );
329
- return React3.useMemo(
212
+ return React2.useMemo(
330
213
  () => ({
331
214
  token,
332
215
  tokenStatus,
@@ -353,59 +236,327 @@ function useMediaSession({ issueViewerToken, tokenArgs, baseUrl, isLazy }) {
353
236
  ]
354
237
  );
355
238
  }
356
- function useRequestTokenWhen(session, isNeeded) {
357
- const { requestToken, token } = session;
358
- const hasToken = token !== null;
359
- React3.useEffect(() => {
360
- if (isNeeded && !hasToken) requestToken();
361
- }, [isNeeded, hasToken, requestToken]);
239
+
240
+ // src/upload-hints.ts
241
+ async function confirmUploadBestEffort(confirmUpload, key, file, hints) {
242
+ try {
243
+ await confirmUpload({ key, filename: file.name, ...hints });
244
+ } catch (error) {
245
+ console.warn("Devultur: could not record the upload; it is recorded later", error);
246
+ }
247
+ }
248
+ async function readImageDimensions(file) {
249
+ if (typeof createImageBitmap !== "function" || !file.type.startsWith("image/")) return {};
250
+ try {
251
+ const bitmap = await createImageBitmap(file);
252
+ const dimensions = { width: bitmap.width, height: bitmap.height };
253
+ bitmap.close();
254
+ return dimensions;
255
+ } catch {
256
+ return {};
257
+ }
362
258
  }
363
259
 
364
- // src/media-upload-derive.ts
365
- function deriveUploadState(local, videoState, retry) {
366
- const { phase, percent, file, failure, media, processRequested } = local;
367
- if (phase === "uploading") return { phase: "uploading", percent, file };
368
- if (phase === "upload-failed" && failure) {
260
+ // src/crop-upload.tsx
261
+ import React5 from "react";
262
+ import { ImageCropper } from "@devultur/react";
263
+
264
+ // src/use-media-upload.ts
265
+ import React4 from "react";
266
+ import { mediaPath as mediaPath2 } from "@devultur/core";
267
+ import { UploadHttpError, uploadFileWithProgress } from "@devultur/react";
268
+ import { useAction } from "convex/react";
269
+
270
+ // src/media-hooks.ts
271
+ import React3 from "react";
272
+ import {
273
+ buildMediaUrl as buildMediaUrl2,
274
+ CaptionLocaleLabel,
275
+ MediaStatus,
276
+ mediaCaptionPath as mediaCaptionPath2,
277
+ mediaPath
278
+ } from "@devultur/core";
279
+ import { useQuery } from "convex/react";
280
+ function useDevulturMedia(key) {
281
+ const apiCtx = React3.use(DevulturApiContext);
282
+ if (!apiCtx?.getMedia) {
283
+ throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
284
+ }
285
+ const record = useQuery(apiCtx.getMedia, key ? { key } : "skip");
286
+ if (!record) {
369
287
  return {
370
- phase: "failed",
371
- stage: "upload",
372
- reason: failure.reason,
373
- retriable: failure.retriable,
374
- message: failure.message,
375
- retry
288
+ status: MediaStatus.IDLE,
289
+ progress: null,
290
+ error: null,
291
+ failureReason: null,
292
+ retriable: null,
293
+ isQueued: false,
294
+ isTranscoding: false,
295
+ isCaptioning: false,
296
+ isReady: false,
297
+ isFailed: false,
298
+ playlistUrl: null,
299
+ thumbnailKey: null,
300
+ captionLocales: [],
301
+ duration: null,
302
+ isLoading: record === void 0
376
303
  };
377
304
  }
378
- if (phase === "idle" || !media) return { phase: "idle" };
379
- if (media.kind !== "video" || !processRequested) return { phase: "ready", media };
380
- switch (videoState.phase) {
381
- case "processing":
382
- return { phase: "processing", media, step: videoState.step, percent: videoState.percent };
383
- case "ready":
384
- return {
385
- phase: "ready",
386
- media,
387
- playlist: videoState.playlist,
388
- duration: videoState.duration,
389
- captions: videoState.captions,
390
- playerProps: videoState.playerProps
391
- };
392
- case "failed":
305
+ const status = record.transcodeStatus ?? MediaStatus.IDLE;
306
+ const isReady = status === MediaStatus.READY;
307
+ const isFailed = status === MediaStatus.FAILED;
308
+ const progress = record.transcodeProgress ? {
309
+ current: record.transcodeProgress.current,
310
+ total: record.transcodeProgress.total,
311
+ percent: record.transcodeProgress.total > 0 ? Math.round(
312
+ record.transcodeProgress.current / record.transcodeProgress.total * 100
313
+ ) : 0,
314
+ phase: record.transcodeProgress.phase
315
+ } : null;
316
+ return {
317
+ status,
318
+ progress,
319
+ error: record.transcodeError ?? null,
320
+ failureReason: isFailed ? record.failureReason ?? "internal" : null,
321
+ retriable: isFailed ? record.retriable ?? true : null,
322
+ isQueued: status === MediaStatus.QUEUED,
323
+ isTranscoding: status === MediaStatus.TRANSCODING,
324
+ isCaptioning: status === MediaStatus.CAPTIONING,
325
+ isReady,
326
+ isFailed,
327
+ playlistUrl: record.playlistUrl ?? null,
328
+ thumbnailKey: record.thumbnailKey ?? null,
329
+ captionLocales: record.captionLocales ?? [],
330
+ duration: record.duration ?? null,
331
+ isLoading: false
332
+ };
333
+ }
334
+ function useVideoState(media, options) {
335
+ const devultur = useDevultur();
336
+ const { token, baseUrl, sessionSecret } = devultur;
337
+ useRequestTokenWhen(devultur, media?.kind === "video" && media.visibility === "private");
338
+ const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
339
+ const locales = options?.locales;
340
+ return React3.useMemo(() => {
341
+ if (media?.kind !== "video") return { phase: "idle" };
342
+ if (status.isFailed) {
393
343
  return {
394
344
  phase: "failed",
395
- stage: "transcode",
396
- reason: videoState.reason,
397
- retriable: videoState.retriable,
398
- message: videoState.message,
399
- retry
345
+ reason: status.failureReason ?? "internal",
346
+ message: status.error ?? "Video processing failed",
347
+ retriable: status.retriable ?? true
400
348
  };
401
- default:
402
- return { phase: "queued", media };
403
- }
349
+ }
350
+ if (status.isReady) {
351
+ if (!baseUrl) throw new Error("useVideoState requires `baseUrl` on <DevulturProvider>");
352
+ const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
353
+ const playlist = buildMediaUrl2(baseUrl, mediaPath(media), { token: viewerToken });
354
+ const captions = (locales ?? status.captionLocales).flatMap((locale) => {
355
+ const path = mediaCaptionPath2(media, locale);
356
+ if (!path) return [];
357
+ const label = CaptionLocaleLabel[locale] ?? locale;
358
+ return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
359
+ });
360
+ return {
361
+ phase: "ready",
362
+ media,
363
+ playlist,
364
+ duration: status.duration ?? 0,
365
+ captions,
366
+ playerProps: {
367
+ src: playlist,
368
+ token: viewerToken,
369
+ captions,
370
+ sessionSecret: media.visibility === "private" ? sessionSecret : void 0
371
+ }
372
+ };
373
+ }
374
+ if (status.isQueued) return { phase: "queued" };
375
+ if (status.isTranscoding || status.isCaptioning) {
376
+ const step = status.progress?.phase ?? (status.isCaptioning ? "captioning" : "encoding");
377
+ return { phase: "processing", step, percent: status.progress?.percent ?? 0 };
378
+ }
379
+ return { phase: "idle" };
380
+ }, [media, status, baseUrl, token, locales, sessionSecret]);
381
+ }
382
+
383
+ // src/media-upload-derive.ts
384
+ function deriveUploadState(local, videoState, retry) {
385
+ const { phase, percent, file, failure, media, processRequested } = local;
386
+ if (phase === "uploading") return { phase: "uploading", percent, file };
387
+ if (phase === "upload-failed" && failure) {
388
+ return {
389
+ phase: "failed",
390
+ stage: "upload",
391
+ reason: failure.reason,
392
+ retriable: failure.retriable,
393
+ message: failure.message,
394
+ retry
395
+ };
396
+ }
397
+ if (phase === "idle" || !media) return { phase: "idle" };
398
+ if (media.kind !== "video" || !processRequested) return { phase: "ready", media };
399
+ switch (videoState.phase) {
400
+ case "processing":
401
+ return { phase: "processing", media, step: videoState.step, percent: videoState.percent };
402
+ case "ready":
403
+ return {
404
+ phase: "ready",
405
+ media,
406
+ playlist: videoState.playlist,
407
+ duration: videoState.duration,
408
+ captions: videoState.captions,
409
+ playerProps: videoState.playerProps
410
+ };
411
+ case "failed":
412
+ return {
413
+ phase: "failed",
414
+ stage: "transcode",
415
+ reason: videoState.reason,
416
+ retriable: videoState.retriable,
417
+ message: videoState.message,
418
+ retry
419
+ };
420
+ default:
421
+ return { phase: "queued", media };
422
+ }
423
+ }
424
+
425
+ // src/use-media-upload.ts
426
+ function useMediaUpload(options) {
427
+ const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
428
+ const api = useDevulturApi();
429
+ const createUploadUrlAction = useAction(api.createUploadUrl);
430
+ const deleteMediaAction = useAction(api.deleteMedia);
431
+ const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
432
+ const processVideoAction = useAction(api.processVideo ?? api.createUploadUrl);
433
+ const [phase, setPhase] = React4.useState("idle");
434
+ const [percent, setPercent] = React4.useState(0);
435
+ const [currentFile, setCurrentFile] = React4.useState(null);
436
+ const [media, setMedia] = React4.useState(null);
437
+ const [uploadFailure, setUploadFailure] = React4.useState(null);
438
+ const [processRequested, setProcessRequested] = React4.useState(false);
439
+ const abortRef = React4.useRef(null);
440
+ const lastFileRef = React4.useRef(null);
441
+ const lastKeyRef = React4.useRef(null);
442
+ const videoState = useVideoState(media, { locales });
443
+ const triggerProcess = React4.useCallback(
444
+ (key) => {
445
+ if (!api.processVideo) {
446
+ throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
447
+ }
448
+ lastKeyRef.current = key;
449
+ setProcessRequested(true);
450
+ processVideoAction({ key }).catch((err) => {
451
+ console.error("useMediaUpload: could not request the transcode", err);
452
+ });
453
+ },
454
+ [api.processVideo, processVideoAction]
455
+ );
456
+ const runUpload = React4.useCallback(
457
+ async (file) => {
458
+ const controller = new AbortController();
459
+ abortRef.current = controller;
460
+ lastFileRef.current = file;
461
+ setUploadFailure(null);
462
+ setMedia(null);
463
+ setProcessRequested(false);
464
+ setCurrentFile(file);
465
+ setPercent(0);
466
+ setPhase("uploading");
467
+ const fail = (reason, message) => {
468
+ const failure = { stage: "upload", reason, retriable: true, message };
469
+ setUploadFailure(failure);
470
+ setPhase("upload-failed");
471
+ onError?.(failure);
472
+ return null;
473
+ };
474
+ let result;
475
+ try {
476
+ result = await createUploadUrlAction({
477
+ filename: file.name,
478
+ contentType: file.type,
479
+ visibility
480
+ });
481
+ } catch (err) {
482
+ return fail("rejected", err instanceof Error ? err.message : "Could not start the upload");
483
+ }
484
+ const dimensions = await readImageDimensions(file);
485
+ try {
486
+ await uploadFileWithProgress(result.url, file, { onProgress: setPercent, signal: controller.signal });
487
+ } catch (err) {
488
+ if (err instanceof DOMException && err.name === "AbortError") {
489
+ setPhase("idle");
490
+ setCurrentFile(null);
491
+ setPercent(0);
492
+ return null;
493
+ }
494
+ const rejected = err instanceof UploadHttpError && (err.status === 413 || err.status === 415);
495
+ return fail(rejected ? "rejected" : "network", err instanceof Error ? err.message : "The upload failed");
496
+ }
497
+ if (api.confirmUpload) await confirmUploadBestEffort(confirmUploadAction, result.key, file, dimensions);
498
+ const uploaded = {
499
+ id: result.id,
500
+ kind: result.kind,
501
+ visibility: result.visibility,
502
+ filename: file.name,
503
+ key: result.key,
504
+ ...dimensions
505
+ };
506
+ setMedia(uploaded);
507
+ setPhase("uploaded");
508
+ if (replaces) deleteMediaAction({ key: mediaPath2(replaces) }).catch(() => {
509
+ });
510
+ onComplete?.(uploaded);
511
+ if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
512
+ return uploaded;
513
+ },
514
+ [
515
+ createUploadUrlAction,
516
+ confirmUploadAction,
517
+ api.confirmUpload,
518
+ deleteMediaAction,
519
+ triggerProcess,
520
+ visibility,
521
+ replaces,
522
+ process,
523
+ onComplete,
524
+ onError
525
+ ]
526
+ );
527
+ const cancel = React4.useCallback(() => {
528
+ abortRef.current?.abort();
529
+ }, []);
530
+ const reset = React4.useCallback(() => {
531
+ abortRef.current?.abort();
532
+ setPhase("idle");
533
+ setPercent(0);
534
+ setCurrentFile(null);
535
+ setMedia(null);
536
+ setUploadFailure(null);
537
+ setProcessRequested(false);
538
+ }, []);
539
+ const retry = React4.useCallback(() => {
540
+ if (phase === "upload-failed" && lastFileRef.current) {
541
+ runUpload(lastFileRef.current);
542
+ return;
543
+ }
544
+ if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
545
+ }, [phase, runUpload, triggerProcess]);
546
+ React4.useEffect(() => () => abortRef.current?.abort(), []);
547
+ const state = React4.useMemo(
548
+ () => deriveUploadState(
549
+ { phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
550
+ videoState,
551
+ retry
552
+ ),
553
+ [phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
554
+ );
555
+ const upload = React4.useCallback((file) => runUpload(file), [runUpload]);
556
+ return { upload, state, cancel, reset };
404
557
  }
405
558
 
406
559
  // src/crop-upload.tsx
407
- import React4 from "react";
408
- import { ImageCropper } from "@devultur/react";
409
560
  import { jsx, jsxs } from "react/jsx-runtime";
410
561
  var DEFAULT_LABELS = {
411
562
  zoom: "Zoom",
@@ -428,18 +579,18 @@ function CropUpload({
428
579
  children
429
580
  }) {
430
581
  const text = { ...DEFAULT_LABELS, ...labels };
431
- const [pendingFile, setPendingFile] = React4.useState(null);
432
- const inputRef = React4.useRef(null);
582
+ const [pendingFile, setPendingFile] = React5.useState(null);
583
+ const inputRef = React5.useRef(null);
433
584
  const { upload, state, cancel, reset } = useMediaUpload({ visibility, replaces, onComplete });
434
- const pickFile = React4.useCallback(() => inputRef.current?.click(), []);
435
- const handleCrop = React4.useCallback(
585
+ const pickFile = React5.useCallback(() => inputRef.current?.click(), []);
586
+ const handleCrop = React5.useCallback(
436
587
  (baked) => {
437
588
  setPendingFile(null);
438
589
  upload(baked);
439
590
  },
440
591
  [upload]
441
592
  );
442
- const resetAll = React4.useCallback(() => {
593
+ const resetAll = React5.useCallback(() => {
443
594
  setPendingFile(null);
444
595
  reset();
445
596
  }, [reset]);
@@ -492,7 +643,7 @@ function CropUpload({
492
643
  }
493
644
 
494
645
  // src/document-renderers.tsx
495
- import React5 from "react";
646
+ import React7 from "react";
496
647
 
497
648
  // src/attachment-icons.tsx
498
649
  import { jsx as jsx2 } from "react/jsx-runtime";
@@ -526,65 +677,196 @@ function AttachmentIcon({ kind }) {
526
677
  );
527
678
  }
528
679
 
529
- // src/document-renderers.tsx
530
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
531
- function DocumentLink({ media, download = false, children, labels, ...anchorProps }) {
532
- const { url } = useDevultur();
533
- if (!media) return null;
534
- const text = children ?? attachmentName(media, mergeAttachmentLabels(labels).untitled);
535
- if (download || media.visibility === "private") {
536
- return /* @__PURE__ */ jsx3(DownloadLink, { media, ...anchorProps, children: text });
537
- }
538
- return /* @__PURE__ */ jsx3("a", { href: url(media), ...anchorProps, children: text });
539
- }
540
- function AttachmentCard({
541
- media,
542
- contentType,
543
- onOpen,
544
- preview,
545
- headingLevel = 3,
546
- locale,
547
- labels,
548
- ...articleProps
549
- }) {
550
- const headingId = React5.useId();
551
- if (!media) return null;
552
- const text = mergeAttachmentLabels(labels);
553
- const kind = attachmentKindOf(media, contentType);
554
- const details = attachmentDetails(media, kind, text, locale ?? text.locale);
555
- const Heading = `h${headingLevel}`;
556
- return /* @__PURE__ */ jsxs2("article", { "aria-labelledby": headingId, "data-kind": kind, ...articleProps, children: [
557
- preview ?? /* @__PURE__ */ jsx3(AttachmentIcon, { kind }),
558
- /* @__PURE__ */ jsx3(Heading, { id: headingId, "data-part": "name", children: attachmentName(media, text.untitled) }),
559
- /* @__PURE__ */ jsx3("p", { "data-part": "details", children: details.join(" \xB7 ") }),
560
- /* @__PURE__ */ jsxs2("div", { "data-part": "actions", children: [
561
- /* @__PURE__ */ jsx3(DownloadLink, { media, "aria-describedby": headingId, "data-part": "download", children: text.download }),
562
- onOpen && /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => onOpen(media), "aria-describedby": headingId, "data-part": "open", children: text.open })
563
- ] })
564
- ] });
565
- }
566
-
567
- // src/image.tsx
680
+ // src/download-link.tsx
568
681
  import React6 from "react";
569
682
  import { mediaUrl as mediaUrl2 } from "@devultur/core";
570
683
 
571
- // src/responsive-image.ts
572
- function normalizeWidths(widths) {
573
- const validWidths = widths.filter((width) => Number.isInteger(width) && width > 0);
574
- return [...new Set(validWidths)].sort((a, b) => a - b);
684
+ // src/media-fetch.ts
685
+ import { SESSION_HEADER as SESSION_HEADER2 } from "@devultur/core";
686
+ var MediaFetchError = class extends Error {
687
+ code;
688
+ /** HTTP status, for `http_error` when the delivery answered. */
689
+ status;
690
+ /** The code's extra fields, e.g. `page_cap`'s `{ shown, total }`. */
691
+ details;
692
+ constructor(code, message, status, options) {
693
+ super(message, options);
694
+ this.name = "MediaFetchError";
695
+ this.code = code;
696
+ this.status = status;
697
+ this.details = options?.details;
698
+ }
699
+ };
700
+ function networkError(cause) {
701
+ return new MediaFetchError("http_error", "Media fetch failed: no response", void 0, { cause });
575
702
  }
576
- function buildSrcSet(widths, urlForWidth) {
577
- return widths.map((width) => `${urlForWidth(width)} ${width}w`).join(", ");
703
+ function pageCapError(shown, total) {
704
+ const message = `Showing the first ${shown} of ${total} pages`;
705
+ return new MediaFetchError("page_cap", message, void 0, { details: { shown, total } });
578
706
  }
579
- function scaledHeight(targetWidth, width, height) {
580
- if (!width || !height) return void 0;
581
- return Math.round(targetWidth * height / width);
707
+ function renderFailedError(cause) {
708
+ return new MediaFetchError("render_failed", "The media couldn't be rendered", void 0, { cause });
582
709
  }
583
- function pickWidth(widths, renderedWidth) {
584
- const ascending = normalizeWidths(widths);
585
- if (renderedWidth === null) return ascending.at(-1);
586
- const fitting = ascending.filter((width) => width <= renderedWidth * 2);
587
- return fitting.at(-1) ?? ascending[0];
710
+ function tokenUnavailableError(cause) {
711
+ return new MediaFetchError("token_unavailable", "No viewer token: its issuance failed", void 0, { cause });
712
+ }
713
+ async function fetchMedia(request) {
714
+ const { buildUrl, token, sessionSecret, reissueToken, signal, isPrivate = true, canReissue = true } = request;
715
+ const headers = isPrivate && sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0;
716
+ const response = await fetch(buildUrl(isPrivate ? token ?? void 0 : void 0), { headers, signal });
717
+ if (!isPrivate || !canReissue || response.status !== 401) return response;
718
+ if (!await isSessionMismatch(response)) return response;
719
+ request.onReissue?.();
720
+ const freshToken = await reissueToken();
721
+ if (!freshToken) return response;
722
+ return fetch(buildUrl(freshToken), { headers, signal });
723
+ }
724
+ async function loadMedia(request) {
725
+ const response = await fetchMedia(request);
726
+ if (!response.ok) {
727
+ throw new MediaFetchError("http_error", `Media fetch failed: ${response.status}`, response.status);
728
+ }
729
+ const trusted = trustedContentType(request.contentType, response.headers.get("content-type"));
730
+ if (!trusted.ok) {
731
+ await response.body?.cancel();
732
+ throw new MediaFetchError(trusted.reason, `Refusing to preview: ${trusted.reason}`);
733
+ }
734
+ const bytes = await readMediaBody(response, request.maxBytes);
735
+ return { bytes, contentType: trusted.contentType };
736
+ }
737
+ function tooLarge(maxBytes) {
738
+ return new MediaFetchError("too_large", `Media is larger than ${maxBytes} bytes`);
739
+ }
740
+ async function readMediaBody(response, maxBytes) {
741
+ if (maxBytes === void 0) return new Uint8Array(await response.arrayBuffer());
742
+ const declaredBytes = Number(response.headers.get("content-length") ?? Number.NaN);
743
+ if (declaredBytes > maxBytes) {
744
+ await response.body?.cancel();
745
+ throw tooLarge(maxBytes);
746
+ }
747
+ if (!response.body) return new Uint8Array(0);
748
+ return readCapped(response.body, maxBytes);
749
+ }
750
+ async function readCapped(body, maxBytes) {
751
+ const reader = body.getReader();
752
+ const chunks = [];
753
+ let byteCount = 0;
754
+ for (; ; ) {
755
+ const { done, value } = await reader.read();
756
+ if (done) break;
757
+ byteCount += value.byteLength;
758
+ if (byteCount > maxBytes) {
759
+ await reader.cancel();
760
+ throw tooLarge(maxBytes);
761
+ }
762
+ chunks.push(value);
763
+ }
764
+ const bytes = new Uint8Array(byteCount);
765
+ let offset = 0;
766
+ for (const chunk of chunks) {
767
+ bytes.set(chunk, offset);
768
+ offset += chunk.byteLength;
769
+ }
770
+ return bytes;
771
+ }
772
+
773
+ // src/download-link.tsx
774
+ import { jsx as jsx3 } from "react/jsx-runtime";
775
+ function DownloadLink({ media, filename, onDownloadError, children, ...anchorProps }) {
776
+ const devultur = useDevultur();
777
+ const { url, token, tokenStatus, tokenError, baseUrl, sessionSecret, reissueToken } = devultur;
778
+ useRequestTokenWhen(devultur, media?.visibility === "private");
779
+ const handleClick = React6.useCallback(
780
+ async (event) => {
781
+ if (media?.visibility !== "private" || !baseUrl) return;
782
+ event.preventDefault();
783
+ if (tokenStatus === "error") {
784
+ onDownloadError?.(tokenUnavailableError(tokenError));
785
+ return;
786
+ }
787
+ const response = await fetchMedia({
788
+ buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { download: filename ?? true, token: viewerToken }),
789
+ token,
790
+ sessionSecret,
791
+ reissueToken
792
+ });
793
+ if (!response.ok) {
794
+ const status = response.status;
795
+ onDownloadError?.(new MediaFetchError("http_error", `Media fetch failed: ${status}`, status));
796
+ return;
797
+ }
798
+ const objectUrl = URL.createObjectURL(await response.blob());
799
+ const anchor = document.createElement("a");
800
+ anchor.href = objectUrl;
801
+ anchor.download = filename ?? media.filename ?? "download";
802
+ anchor.click();
803
+ URL.revokeObjectURL(objectUrl);
804
+ },
805
+ [media, baseUrl, token, tokenStatus, tokenError, filename, sessionSecret, reissueToken, onDownloadError]
806
+ );
807
+ if (!media) return null;
808
+ return /* @__PURE__ */ jsx3("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
809
+ }
810
+
811
+ // src/document-renderers.tsx
812
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
813
+ function DocumentLink({ media, download = false, children, labels, ...anchorProps }) {
814
+ const { url } = useDevultur();
815
+ if (!media) return null;
816
+ const text = children ?? attachmentName(media, mergeAttachmentLabels(labels).untitled);
817
+ if (download || media.visibility === "private") {
818
+ return /* @__PURE__ */ jsx4(DownloadLink, { media, ...anchorProps, children: text });
819
+ }
820
+ return /* @__PURE__ */ jsx4("a", { href: url(media), ...anchorProps, children: text });
821
+ }
822
+ function AttachmentCard({
823
+ media,
824
+ contentType,
825
+ onOpen,
826
+ preview,
827
+ headingLevel = 3,
828
+ locale,
829
+ labels,
830
+ ...articleProps
831
+ }) {
832
+ const headingId = React7.useId();
833
+ if (!media) return null;
834
+ const text = mergeAttachmentLabels(labels);
835
+ const kind = attachmentKindOf(media, contentType);
836
+ const details = attachmentDetails(media, kind, text, locale ?? text.locale);
837
+ const Heading = `h${headingLevel}`;
838
+ return /* @__PURE__ */ jsxs2("article", { "aria-labelledby": headingId, "data-kind": kind, ...articleProps, children: [
839
+ preview ?? /* @__PURE__ */ jsx4(AttachmentIcon, { kind }),
840
+ /* @__PURE__ */ jsx4(Heading, { id: headingId, "data-part": "name", children: attachmentName(media, text.untitled) }),
841
+ /* @__PURE__ */ jsx4("p", { "data-part": "details", children: details.join(" \xB7 ") }),
842
+ /* @__PURE__ */ jsxs2("div", { "data-part": "actions", children: [
843
+ /* @__PURE__ */ jsx4(DownloadLink, { media, "aria-describedby": headingId, "data-part": "download", children: text.download }),
844
+ onOpen && /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => onOpen(media), "aria-describedby": headingId, "data-part": "open", children: text.open })
845
+ ] })
846
+ ] });
847
+ }
848
+
849
+ // src/image.tsx
850
+ import React8 from "react";
851
+ import { mediaUrl as mediaUrl3 } from "@devultur/core";
852
+
853
+ // src/responsive-image.ts
854
+ function normalizeWidths(widths) {
855
+ const validWidths = widths.filter((width) => Number.isInteger(width) && width > 0);
856
+ return [...new Set(validWidths)].sort((a, b) => a - b);
857
+ }
858
+ function buildSrcSet(widths, urlForWidth) {
859
+ return widths.map((width) => `${urlForWidth(width)} ${width}w`).join(", ");
860
+ }
861
+ function scaledHeight(targetWidth, width, height) {
862
+ if (!width || !height) return void 0;
863
+ return Math.round(targetWidth * height / width);
864
+ }
865
+ function pickWidth(widths, renderedWidth) {
866
+ const ascending = normalizeWidths(widths);
867
+ if (renderedWidth === null) return ascending.at(-1);
868
+ const fitting = ascending.filter((width) => width <= renderedWidth * 2);
869
+ return fitting.at(-1) ?? ascending[0];
588
870
  }
589
871
  var LENGTH = /^(\d+(?:\.\d+)?)(px|vw)$/;
590
872
  function lengthInPixels(length, viewportWidth) {
@@ -610,7 +892,7 @@ function renderedWidthFromSizes(sizes) {
610
892
  }
611
893
 
612
894
  // src/image.tsx
613
- import { Fragment, jsx as jsx4 } from "react/jsx-runtime";
895
+ import { Fragment, jsx as jsx5 } from "react/jsx-runtime";
614
896
  function Image({
615
897
  media,
616
898
  width,
@@ -657,14 +939,14 @@ function Image({
657
939
  } else {
658
940
  src = url(media, transforms);
659
941
  }
660
- if (!src) return /* @__PURE__ */ jsx4(Fragment, { children: fallback });
942
+ if (!src) return /* @__PURE__ */ jsx5(Fragment, { children: fallback });
661
943
  if (render) {
662
- return React6.cloneElement(render, {
944
+ return React8.cloneElement(render, {
663
945
  style: { ...render.props.style, backgroundImage: `url("${src}")` }
664
946
  });
665
947
  }
666
948
  const lazyProps = responsiveWidths ? { loading: "lazy", decoding: "async" } : {};
667
- return /* @__PURE__ */ jsx4(
949
+ return /* @__PURE__ */ jsx5(
668
950
  "img",
669
951
  {
670
952
  src,
@@ -681,10 +963,10 @@ function Image({
681
963
  function usePrivateImage(media, transforms, onLoadError) {
682
964
  const devultur = useDevultur();
683
965
  const { token, tokenStatus, tokenError, baseUrl, reissueToken, sessionSecret } = devultur;
684
- const [isFailed, setIsFailed] = React6.useState(false);
685
- const [blobSrc, setBlobSrc] = React6.useState(null);
686
- const sessionRef = React6.useRef({ token, reissueToken, onLoadError });
687
- React6.useEffect(() => {
966
+ const [isFailed, setIsFailed] = React8.useState(false);
967
+ const [blobSrc, setBlobSrc] = React8.useState(null);
968
+ const sessionRef = React8.useRef({ token, reissueToken, onLoadError });
969
+ React8.useEffect(() => {
688
970
  sessionRef.current = { token, reissueToken, onLoadError };
689
971
  });
690
972
  const isPrivate = media?.visibility === "private";
@@ -692,15 +974,15 @@ function usePrivateImage(media, transforms, onLoadError) {
692
974
  const hasToken = token !== null;
693
975
  const isTokenUnavailable = isPrivate && tokenStatus === "error";
694
976
  const { width, height, fit, format, quality } = transforms;
695
- React6.useEffect(() => {
977
+ React8.useEffect(() => {
696
978
  setIsFailed(false);
697
979
  setBlobSrc(null);
698
980
  }, [media]);
699
- React6.useEffect(() => {
981
+ React8.useEffect(() => {
700
982
  if (!isTokenUnavailable || !media) return;
701
983
  sessionRef.current.onLoadError?.(tokenUnavailableError(tokenError));
702
984
  }, [isTokenUnavailable, media, tokenError]);
703
- React6.useEffect(() => {
985
+ React8.useEffect(() => {
704
986
  if (!isPrivate || isFailed || !media || !hasToken) return;
705
987
  let cancelled = false;
706
988
  let objectUrl = null;
@@ -714,7 +996,7 @@ function usePrivateImage(media, transforms, onLoadError) {
714
996
  (async () => {
715
997
  try {
716
998
  const response = await fetchMedia({
717
- buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { ...fetchTransforms, token: viewerToken }),
999
+ buildUrl: (viewerToken) => mediaUrl3(baseUrl, media, { ...fetchTransforms, token: viewerToken }),
718
1000
  token: sessionRef.current.token,
719
1001
  sessionSecret,
720
1002
  reissueToken: sessionRef.current.reissueToken
@@ -739,22 +1021,22 @@ function usePrivateImage(media, transforms, onLoadError) {
739
1021
  if (objectUrl) URL.revokeObjectURL(objectUrl);
740
1022
  };
741
1023
  }, [isPrivate, isFailed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
742
- const handleError = React6.useCallback(() => setIsFailed(true), []);
1024
+ const handleError = React8.useCallback(() => setIsFailed(true), []);
743
1025
  return { blobSrc, isFailed: isFailed || isTokenUnavailable, handleError };
744
1026
  }
745
1027
 
746
1028
  // src/media-library.tsx
747
- import React15 from "react";
1029
+ import React17 from "react";
748
1030
 
749
1031
  // src/media-library-item.tsx
750
- import React12 from "react";
1032
+ import React14 from "react";
751
1033
 
752
1034
  // src/pdf-thumbnail.tsx
753
- import React11 from "react";
1035
+ import React13 from "react";
754
1036
 
755
1037
  // src/media-resource.ts
756
- import React7 from "react";
757
- import { mediaPath, mediaUrl as mediaUrl3 } from "@devultur/core";
1038
+ import React9 from "react";
1039
+ import { mediaPath as mediaPath3, mediaUrl as mediaUrl4 } from "@devultur/core";
758
1040
  var IDLE = { value: null, contentType: null, isLoading: false, error: null };
759
1041
  var LOADING = { value: null, contentType: null, isLoading: true, error: null };
760
1042
  function useMediaResource(media, options) {
@@ -764,17 +1046,17 @@ function useMediaResource(media, options) {
764
1046
  useRequestTokenWhen(devultur, isPrivate && isEnabled);
765
1047
  const isReady = !isPrivate || devultur.token !== null;
766
1048
  const isTokenUnavailable = isPrivate && devultur.tokenStatus === "error";
767
- const tokenFailure = React7.useMemo(
1049
+ const tokenFailure = React9.useMemo(
768
1050
  () => isTokenUnavailable ? tokenUnavailableError(devultur.tokenError) : null,
769
1051
  [isTokenUnavailable, devultur.tokenError]
770
1052
  );
771
- const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility, mediaPath(media), contentType ?? "", maxBytes ?? ""].join("|") : null;
772
- const [settled, setSettled] = React7.useState(null);
773
- const inputsRef = React7.useRef({ media, devultur, options });
774
- React7.useEffect(() => {
1053
+ const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility, mediaPath3(media), contentType ?? "", maxBytes ?? ""].join("|") : null;
1054
+ const [settled, setSettled] = React9.useState(null);
1055
+ const inputsRef = React9.useRef({ media, devultur, options });
1056
+ React9.useEffect(() => {
775
1057
  inputsRef.current = { media, devultur, options };
776
1058
  });
777
- React7.useEffect(() => {
1059
+ React9.useEffect(() => {
778
1060
  if (!requestKey || !isReady) return;
779
1061
  const { media: target, devultur: context, options: current } = inputsRef.current;
780
1062
  if (!target || !context.baseUrl) return;
@@ -782,7 +1064,7 @@ function useMediaResource(media, options) {
782
1064
  const controller = new AbortController();
783
1065
  let value = null;
784
1066
  loadMedia({
785
- buildUrl: (viewerToken) => mediaUrl3(baseUrl, target, { token: viewerToken }),
1067
+ buildUrl: (viewerToken) => mediaUrl4(baseUrl, target, { token: viewerToken }),
786
1068
  token: context.token,
787
1069
  sessionSecret: context.sessionSecret,
788
1070
  reissueToken: context.reissueToken,
@@ -812,7 +1094,7 @@ function useMediaResource(media, options) {
812
1094
  }
813
1095
 
814
1096
  // src/pdf-context.tsx
815
- import React8 from "react";
1097
+ import React10 from "react";
816
1098
 
817
1099
  // src/preview-labels.ts
818
1100
  var ERROR_TEXT = {
@@ -847,16 +1129,16 @@ function mergePreviewLabels(...sources) {
847
1129
  }
848
1130
 
849
1131
  // src/pdf-context.tsx
850
- import { jsx as jsx5 } from "react/jsx-runtime";
851
- var PdfPreviewContext = /* @__PURE__ */ React8.createContext({});
1132
+ import { jsx as jsx6 } from "react/jsx-runtime";
1133
+ var PdfPreviewContext = /* @__PURE__ */ React10.createContext({});
852
1134
  function PdfPreviewProvider({
853
1135
  pdfjs,
854
1136
  workerSrc,
855
1137
  labels,
856
1138
  children
857
1139
  }) {
858
- const config = React8.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
859
- return /* @__PURE__ */ jsx5(PdfPreviewContext.Provider, { value: config, children });
1140
+ const config = React10.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
1141
+ return /* @__PURE__ */ jsx6(PdfPreviewContext.Provider, { value: config, children });
860
1142
  }
861
1143
  var loadedModules = /* @__PURE__ */ new WeakMap();
862
1144
  function loadOnce(loader) {
@@ -867,9 +1149,9 @@ function loadOnce(loader) {
867
1149
  return loading;
868
1150
  }
869
1151
  function usePdfjs(workerSrcOverride) {
870
- const { pdfjs: loader, workerSrc } = React8.useContext(PdfPreviewContext);
871
- const [loaded, setLoaded] = React8.useState(null);
872
- React8.useEffect(() => {
1152
+ const { pdfjs: loader, workerSrc } = React10.useContext(PdfPreviewContext);
1153
+ const [loaded, setLoaded] = React10.useState(null);
1154
+ React10.useEffect(() => {
873
1155
  if (!loader) return;
874
1156
  let isCurrent = true;
875
1157
  loadOnce(loader).then((pdfjs) => {
@@ -885,8 +1167,8 @@ function usePdfjs(workerSrcOverride) {
885
1167
  return { status: "ready", pdfjs: loaded.pdfjs, workerSrc: workerSrcOverride ?? workerSrc };
886
1168
  }
887
1169
  function usePdfPageCounter() {
888
- const { pdfjs: loader, workerSrc } = React8.useContext(PdfPreviewContext);
889
- return React8.useMemo(() => {
1170
+ const { pdfjs: loader, workerSrc } = React10.useContext(PdfPreviewContext);
1171
+ return React10.useMemo(() => {
890
1172
  if (!loader) return null;
891
1173
  return async (file) => {
892
1174
  const pdfjs = await loadOnce(loader);
@@ -896,15 +1178,15 @@ function usePdfPageCounter() {
896
1178
  }, [loader, workerSrc]);
897
1179
  }
898
1180
  function usePreviewLabels(labels) {
899
- const providerLabels = React8.useContext(PdfPreviewContext).labels;
1181
+ const providerLabels = React10.useContext(PdfPreviewContext).labels;
900
1182
  return mergePreviewLabels(providerLabels, labels);
901
1183
  }
902
1184
 
903
1185
  // src/pdf-hooks.ts
904
- import React9 from "react";
1186
+ import React11 from "react";
905
1187
  function useIsVisible(ref, rootMargin = "200px") {
906
- const [isVisible, setIsVisible] = React9.useState(false);
907
- React9.useEffect(() => {
1188
+ const [isVisible, setIsVisible] = React11.useState(false);
1189
+ React11.useEffect(() => {
908
1190
  const element = ref.current;
909
1191
  if (isVisible || !element) return;
910
1192
  if (typeof IntersectionObserver === "undefined") {
@@ -925,12 +1207,12 @@ function useIsVisible(ref, rootMargin = "200px") {
925
1207
  return isVisible;
926
1208
  }
927
1209
  function usePdfDocument({ pdfjs, bytes, workerSrc }) {
928
- const [state, setState] = React9.useState({
1210
+ const [state, setState] = React11.useState({
929
1211
  bytes: null,
930
1212
  document: null,
931
1213
  error: null
932
1214
  });
933
- React9.useEffect(() => {
1215
+ React11.useEffect(() => {
934
1216
  let isCurrent = true;
935
1217
  const loadingTask = openPdf(pdfjs, bytes, workerSrc);
936
1218
  loadingTask.promise.then((document2) => {
@@ -949,8 +1231,8 @@ function usePdfDocument({ pdfjs, bytes, workerSrc }) {
949
1231
  }
950
1232
  function usePageRender(options) {
951
1233
  const { document: document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText = false } = options;
952
- const [state, setState] = React9.useState({ isRendered: false, text: "" });
953
- React9.useEffect(() => {
1234
+ const [state, setState] = React11.useState({ isRendered: false, text: "" });
1235
+ React11.useEffect(() => {
954
1236
  const canvas = canvasRef.current;
955
1237
  if (!document2 || !isEnabled || !canvas) return;
956
1238
  let isCurrent = true;
@@ -980,8 +1262,8 @@ function usePageRender(options) {
980
1262
  }
981
1263
 
982
1264
  // src/pdf-preview.tsx
983
- import React10 from "react";
984
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1265
+ import React12 from "react";
1266
+ import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
985
1267
  var VISUALLY_HIDDEN = {
986
1268
  position: "absolute",
987
1269
  width: 1,
@@ -1004,11 +1286,11 @@ function PdfPreview(props) {
1004
1286
  contentType: expectedType,
1005
1287
  decode: keepBytes
1006
1288
  });
1007
- if (error) return /* @__PURE__ */ jsx6(Fragment2, { children: renderFallback(error) });
1008
- if (!bytes || pdfjsState.status === "loading") return /* @__PURE__ */ jsx6(Fragment2, { children: loading });
1289
+ if (error) return /* @__PURE__ */ jsx7(Fragment2, { children: renderFallback(error) });
1290
+ if (!bytes || pdfjsState.status === "loading") return /* @__PURE__ */ jsx7(Fragment2, { children: loading });
1009
1291
  const label = title ?? media.filename ?? labels.untitledPdf;
1010
- if (pdfjsState.status === "unavailable") return /* @__PURE__ */ jsx6(PdfFrame, { bytes, label, className });
1011
- return /* @__PURE__ */ jsx6(
1292
+ if (pdfjsState.status === "unavailable") return /* @__PURE__ */ jsx7(PdfFrame, { bytes, label, className });
1293
+ return /* @__PURE__ */ jsx7(
1012
1294
  PdfCanvasDocument,
1013
1295
  {
1014
1296
  pdfjs: pdfjsState.pdfjs,
@@ -1026,13 +1308,13 @@ function PdfPreview(props) {
1026
1308
  function PdfCanvasDocument(props) {
1027
1309
  const { pdfjs, workerSrc, bytes, label, className, renderFallback, loading, labels, maxPages } = props;
1028
1310
  const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
1029
- const containerRef = React10.useRef(null);
1030
- const [width, setWidth] = React10.useState(null);
1031
- React10.useLayoutEffect(() => {
1311
+ const containerRef = React12.useRef(null);
1312
+ const [width, setWidth] = React12.useState(null);
1313
+ React12.useLayoutEffect(() => {
1032
1314
  if (document2) setWidth(containerRef.current?.clientWidth ?? 0);
1033
1315
  }, [document2]);
1034
- if (error) return /* @__PURE__ */ jsx6(Fragment2, { children: renderFallback(renderFailedError(error)) });
1035
- if (!document2) return /* @__PURE__ */ jsx6(Fragment2, { children: loading });
1316
+ if (error) return /* @__PURE__ */ jsx7(Fragment2, { children: renderFallback(renderFailedError(error)) });
1317
+ if (!document2) return /* @__PURE__ */ jsx7(Fragment2, { children: loading });
1036
1318
  const shownPageCount = Math.min(document2.numPages, Math.max(1, maxPages));
1037
1319
  const isTruncated = shownPageCount < document2.numPages;
1038
1320
  const pageNumbers = Array.from({ length: shownPageCount }, (_, index) => index + 1);
@@ -1044,7 +1326,7 @@ function PdfCanvasDocument(props) {
1044
1326
  "aria-label": labels.document(label, document2.numPages),
1045
1327
  className,
1046
1328
  children: [
1047
- width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */ jsx6(
1329
+ width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */ jsx7(
1048
1330
  PdfPageView,
1049
1331
  {
1050
1332
  document: document2,
@@ -1064,8 +1346,8 @@ function PdfCanvasDocument(props) {
1064
1346
  );
1065
1347
  }
1066
1348
  function PdfPageView({ document: document2, pageNumber, width, label }) {
1067
- const pageRef = React10.useRef(null);
1068
- const canvasRef = React10.useRef(null);
1349
+ const pageRef = React12.useRef(null);
1350
+ const canvasRef = React12.useRef(null);
1069
1351
  const isVisible = useIsVisible(pageRef);
1070
1352
  const { isRendered, text } = usePageRender({
1071
1353
  document: document2,
@@ -1077,28 +1359,28 @@ function PdfPageView({ document: document2, pageNumber, width, label }) {
1077
1359
  });
1078
1360
  const placeholderStyle = isRendered ? void 0 : { aspectRatio: "1 / 1.4142", maxWidth: "100%" };
1079
1361
  return /* @__PURE__ */ jsxs3("section", { ref: pageRef, "aria-label": label, style: placeholderStyle, children: [
1080
- /* @__PURE__ */ jsx6("canvas", { ref: canvasRef, style: { display: "block", maxWidth: "100%" } }),
1081
- text && /* @__PURE__ */ jsx6("p", { style: VISUALLY_HIDDEN, children: text })
1362
+ /* @__PURE__ */ jsx7("canvas", { ref: canvasRef, style: { display: "block", maxWidth: "100%" } }),
1363
+ text && /* @__PURE__ */ jsx7("p", { style: VISUALLY_HIDDEN, children: text })
1082
1364
  ] });
1083
1365
  }
1084
1366
  function PdfFrame({ bytes, label, className }) {
1085
- const [objectUrl, setObjectUrl] = React10.useState(null);
1086
- React10.useEffect(() => {
1367
+ const [objectUrl, setObjectUrl] = React12.useState(null);
1368
+ React12.useEffect(() => {
1087
1369
  const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
1088
1370
  setObjectUrl(url);
1089
1371
  return () => URL.revokeObjectURL(url);
1090
1372
  }, [bytes]);
1091
1373
  if (!objectUrl) return null;
1092
- return /* @__PURE__ */ jsx6("iframe", { src: objectUrl, title: label, referrerPolicy: "no-referrer", className });
1374
+ return /* @__PURE__ */ jsx7("iframe", { src: objectUrl, title: label, referrerPolicy: "no-referrer", className });
1093
1375
  }
1094
1376
 
1095
1377
  // src/pdf-thumbnail.tsx
1096
- import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
1378
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1097
1379
  var DEFAULT_THUMBNAIL_WIDTH = 96;
1098
1380
  var DEFAULT_THUMBNAIL_MAX_BYTES = 50 * 1024 * 1024;
1099
1381
  function PdfThumbnail(props) {
1100
1382
  const { media, contentType, width = DEFAULT_THUMBNAIL_WIDTH, fallback = null, loading = null } = props;
1101
- const containerRef = React11.useRef(null);
1383
+ const containerRef = React13.useRef(null);
1102
1384
  const isVisible = useIsVisible(containerRef);
1103
1385
  const pdfjsState = usePdfjs(props.pdfWorkerSrc);
1104
1386
  const labels = usePreviewLabels(props.labels);
@@ -1115,7 +1397,7 @@ function PdfThumbnail(props) {
1115
1397
  const content = () => {
1116
1398
  if (isUnavailable) return fallback;
1117
1399
  if (!bytes || pdfjsState.status !== "ready") return loading;
1118
- return /* @__PURE__ */ jsx7(
1400
+ return /* @__PURE__ */ jsx8(
1119
1401
  ThumbnailCanvas,
1120
1402
  {
1121
1403
  pdfjs: pdfjsState.pdfjs,
@@ -1128,21 +1410,21 @@ function PdfThumbnail(props) {
1128
1410
  }
1129
1411
  );
1130
1412
  };
1131
- return /* @__PURE__ */ jsx7("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
1413
+ return /* @__PURE__ */ jsx8("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
1132
1414
  }
1133
1415
  function ThumbnailCanvas({ pdfjs, workerSrc, bytes, width, label, fallback, loading }) {
1134
- const canvasRef = React11.useRef(null);
1416
+ const canvasRef = React13.useRef(null);
1135
1417
  const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
1136
1418
  const { isRendered } = usePageRender({ document: document2, pageNumber: 1, canvasRef, width, isEnabled: true });
1137
- if (error) return /* @__PURE__ */ jsx7(Fragment3, { children: fallback });
1419
+ if (error) return /* @__PURE__ */ jsx8(Fragment3, { children: fallback });
1138
1420
  return /* @__PURE__ */ jsxs4(Fragment3, { children: [
1139
1421
  !isRendered && loading,
1140
- /* @__PURE__ */ jsx7("canvas", { ref: canvasRef, role: "img", "aria-label": label, style: { display: "block" } })
1422
+ /* @__PURE__ */ jsx8("canvas", { ref: canvasRef, role: "img", "aria-label": label, style: { display: "block" } })
1141
1423
  ] });
1142
1424
  }
1143
1425
 
1144
1426
  // src/media-library-item.tsx
1145
- import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1427
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1146
1428
  var THUMBNAIL_WIDTH = 160;
1147
1429
  var THUMBNAIL_WIDTHS = [THUMBNAIL_WIDTH, THUMBNAIL_WIDTH * 2];
1148
1430
  function partClass(part, classNames) {
@@ -1162,10 +1444,10 @@ function kindOf(record) {
1162
1444
  }
1163
1445
  function Thumbnail({ record, media, pdfThumbnails }) {
1164
1446
  const kind = kindOf(record);
1165
- const icon = /* @__PURE__ */ jsx8(AttachmentIcon, { kind });
1447
+ const icon = /* @__PURE__ */ jsx9(AttachmentIcon, { kind });
1166
1448
  if (!media) return icon;
1167
1449
  if (record.type === "image") {
1168
- return /* @__PURE__ */ jsx8(
1450
+ return /* @__PURE__ */ jsx9(
1169
1451
  Image,
1170
1452
  {
1171
1453
  media,
@@ -1180,14 +1462,14 @@ function Thumbnail({ record, media, pdfThumbnails }) {
1180
1462
  );
1181
1463
  }
1182
1464
  if (kind !== "pdf" || !pdfThumbnails) return icon;
1183
- return /* @__PURE__ */ jsx8(PdfThumbnail, { media, contentType: record.contentType, width: THUMBNAIL_WIDTH, fallback: icon });
1465
+ return /* @__PURE__ */ jsx9(PdfThumbnail, { media, contentType: record.contentType, width: THUMBNAIL_WIDTH, fallback: icon });
1184
1466
  }
1185
1467
  function DefaultContent({ record, media, pdfThumbnails, labels, classNames }) {
1186
1468
  const name = record.title ?? record.filename;
1187
1469
  return /* @__PURE__ */ jsxs5(Fragment4, { children: [
1188
- /* @__PURE__ */ jsx8("span", { className: partClass("thumbnail", classNames), children: /* @__PURE__ */ jsx8(Thumbnail, { record, media, pdfThumbnails }) }),
1189
- /* @__PURE__ */ jsx8("span", { className: partClass("name", classNames), children: name }),
1190
- record.sizeBytes > 0 && /* @__PURE__ */ jsx8("span", { className: partClass("size", classNames), children: labels.size(record.sizeBytes) })
1470
+ /* @__PURE__ */ jsx9("span", { className: partClass("thumbnail", classNames), children: /* @__PURE__ */ jsx9(Thumbnail, { record, media, pdfThumbnails }) }),
1471
+ /* @__PURE__ */ jsx9("span", { className: partClass("name", classNames), children: name }),
1472
+ record.sizeBytes > 0 && /* @__PURE__ */ jsx9("span", { className: partClass("size", classNames), children: labels.size(record.sizeBytes) })
1191
1473
  ] });
1192
1474
  }
1193
1475
  var ITEM_STYLE = {
@@ -1199,13 +1481,13 @@ var ITEM_STYLE = {
1199
1481
  };
1200
1482
  function MediaLibraryItem(props) {
1201
1483
  const { ref, record, isSelected, isActive, tabIndex, classNames, renderItem, onChoose, onFocus } = props;
1202
- const media = React12.useMemo(() => mediaFromRecord(record), [record]);
1484
+ const media = React14.useMemo(() => mediaFromRecord(record), [record]);
1203
1485
  const handleKeyDown = (event) => {
1204
1486
  if (event.key !== "Enter" && event.key !== " ") return;
1205
1487
  event.preventDefault();
1206
1488
  onChoose();
1207
1489
  };
1208
- return /* @__PURE__ */ jsx8(
1490
+ return /* @__PURE__ */ jsx9(
1209
1491
  "div",
1210
1492
  {
1211
1493
  ref,
@@ -1222,7 +1504,7 @@ function MediaLibraryItem(props) {
1222
1504
  onClick: onChoose,
1223
1505
  onKeyDown: handleKeyDown,
1224
1506
  onFocus,
1225
- children: renderItem ? renderItem(record, { media, isSelected, isActive }) : /* @__PURE__ */ jsx8(
1507
+ children: renderItem ? renderItem(record, { media, isSelected, isActive }) : /* @__PURE__ */ jsx9(
1226
1508
  DefaultContent,
1227
1509
  {
1228
1510
  record,
@@ -1262,13 +1544,13 @@ function mergeMediaLibraryLabels(labels) {
1262
1544
  }
1263
1545
 
1264
1546
  // src/use-media-library.ts
1265
- import React13 from "react";
1547
+ import React15 from "react";
1266
1548
  import { usePaginatedQuery } from "convex/react";
1267
1549
  var SEARCH_DEBOUNCE_MS = 250;
1268
1550
  var DEFAULT_PAGE_SIZE = 24;
1269
1551
  function useDebouncedSearch(value, delayMs) {
1270
- const [debounced, setDebounced] = React13.useState(value);
1271
- React13.useEffect(() => {
1552
+ const [debounced, setDebounced] = React15.useState(value);
1553
+ React15.useEffect(() => {
1272
1554
  if (value === "") {
1273
1555
  setDebounced("");
1274
1556
  return;
@@ -1293,7 +1575,7 @@ function useMediaLibrary(options = {}) {
1293
1575
  const args = isSearching ? { search, ...typeArg } : typeArg;
1294
1576
  const result = usePaginatedQuery(reference, args, { initialNumItems: pageSize });
1295
1577
  const resultLoadMore = result.loadMore;
1296
- const loadMore = React13.useCallback(
1578
+ const loadMore = React15.useCallback(
1297
1579
  (numItems) => resultLoadMore(numItems ?? pageSize),
1298
1580
  [resultLoadMore, pageSize]
1299
1581
  );
@@ -1308,7 +1590,7 @@ function useMediaLibrary(options = {}) {
1308
1590
  }
1309
1591
 
1310
1592
  // src/use-roving-focus.ts
1311
- import React14 from "react";
1593
+ import React16 from "react";
1312
1594
  function nextIndex(key, current, count, columns) {
1313
1595
  const last = count - 1;
1314
1596
  const clamp = (index) => Math.min(Math.max(index, 0), last);
@@ -1336,16 +1618,16 @@ function columnCount(items) {
1336
1618
  return Math.max(inFirstRow.length, 1);
1337
1619
  }
1338
1620
  function useRovingFocus(count) {
1339
- const [activeIndex, setActiveIndex] = React14.useState(0);
1340
- const itemsRef = React14.useRef([]);
1621
+ const [activeIndex, setActiveIndex] = React16.useState(0);
1622
+ const itemsRef = React16.useRef([]);
1341
1623
  const currentIndex = Math.min(activeIndex, Math.max(count - 1, 0));
1342
- const itemRef = React14.useCallback(
1624
+ const itemRef = React16.useCallback(
1343
1625
  (index) => (element) => {
1344
1626
  itemsRef.current[index] = element;
1345
1627
  },
1346
1628
  []
1347
1629
  );
1348
- const handleKeyDown = React14.useCallback(
1630
+ const handleKeyDown = React16.useCallback(
1349
1631
  (event) => {
1350
1632
  if (count === 0) return;
1351
1633
  const items = itemsRef.current.slice(0, count);
@@ -1362,7 +1644,7 @@ function useRovingFocus(count) {
1362
1644
  }
1363
1645
 
1364
1646
  // src/media-library.tsx
1365
- import { Fragment as Fragment5, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1647
+ import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
1366
1648
  var MEDIA_TYPES = ["image", "video", "file"];
1367
1649
  var ROOT_STYLE = {
1368
1650
  display: "flex",
@@ -1377,16 +1659,16 @@ var LIST_STYLE = {
1377
1659
  padding: 0
1378
1660
  };
1379
1661
  function MediaLibrary(props) {
1380
- const labels = React15.useMemo(() => mergeMediaLibraryLabels(props.labels), [props.labels]);
1662
+ const labels = React17.useMemo(() => mergeMediaLibraryLabels(props.labels), [props.labels]);
1381
1663
  const classNames = { ...props.classNames, root: props.className ?? props.classNames?.root };
1382
- const [search, setSearch] = React15.useState("");
1383
- const [chosenType, setChosenType] = React15.useState("");
1384
- const [selectedKey, setSelectedKey] = React15.useState(null);
1664
+ const [search, setSearch] = React17.useState("");
1665
+ const [chosenType, setChosenType] = React17.useState("");
1666
+ const [selectedKey, setSelectedKey] = React17.useState(null);
1385
1667
  const type = props.type ?? (chosenType === "" ? void 0 : chosenType);
1386
1668
  const { onSelect } = props;
1387
1669
  const isSelectable = props.selectable ?? props.selectedKeys !== void 0;
1388
1670
  const selectedKeys = props.selectedKeys ?? (selectedKey === null ? [] : [selectedKey]);
1389
- const handleChoose = React15.useCallback(
1671
+ const handleChoose = React17.useCallback(
1390
1672
  (record) => {
1391
1673
  if (isSelectable) setSelectedKey(record.key);
1392
1674
  onSelect(record, mediaFromRecord(record));
@@ -1394,7 +1676,7 @@ function MediaLibrary(props) {
1394
1676
  [isSelectable, onSelect]
1395
1677
  );
1396
1678
  return /* @__PURE__ */ jsxs6("div", { className: partClass("root", classNames), style: ROOT_STYLE, children: [
1397
- /* @__PURE__ */ jsx9(
1679
+ /* @__PURE__ */ jsx10(
1398
1680
  "input",
1399
1681
  {
1400
1682
  type: "search",
@@ -1413,20 +1695,20 @@ function MediaLibrary(props) {
1413
1695
  value: chosenType,
1414
1696
  onChange: (event) => setChosenType(event.target.value),
1415
1697
  children: [
1416
- /* @__PURE__ */ jsx9("option", { value: "", children: labels.allTypes }),
1417
- MEDIA_TYPES.map((mediaType) => /* @__PURE__ */ jsx9("option", { value: mediaType, children: labels.types[mediaType] }, mediaType))
1698
+ /* @__PURE__ */ jsx10("option", { value: "", children: labels.allTypes }),
1699
+ MEDIA_TYPES.map((mediaType) => /* @__PURE__ */ jsx10("option", { value: mediaType, children: labels.types[mediaType] }, mediaType))
1418
1700
  ]
1419
1701
  }
1420
1702
  ),
1421
- /* @__PURE__ */ jsx9(
1703
+ /* @__PURE__ */ jsx10(
1422
1704
  LibraryErrorBoundary,
1423
1705
  {
1424
1706
  resetKey: `${type ?? ""}\0${search}`,
1425
1707
  renderError: (error, retry) => /* @__PURE__ */ jsxs6("div", { role: "alert", className: partClass("status", classNames), children: [
1426
- /* @__PURE__ */ jsx9("p", { children: labels.error(error) }),
1427
- /* @__PURE__ */ jsx9("button", { type: "button", className: partClass("more", classNames), onClick: retry, children: labels.retry })
1708
+ /* @__PURE__ */ jsx10("p", { children: labels.error(error) }),
1709
+ /* @__PURE__ */ jsx10("button", { type: "button", className: partClass("more", classNames), onClick: retry, children: labels.retry })
1428
1710
  ] }),
1429
- children: /* @__PURE__ */ jsx9(
1711
+ children: /* @__PURE__ */ jsx10(
1430
1712
  LibraryResults,
1431
1713
  {
1432
1714
  ...props,
@@ -1449,15 +1731,15 @@ function LibraryResults(props) {
1449
1731
  const items = props.filter ? library.items.filter(props.filter) : library.items;
1450
1732
  const roving = useRovingFocus(items.length);
1451
1733
  if (library.status === "LoadingFirstPage") {
1452
- return /* @__PURE__ */ jsx9("p", { role: "status", className: partClass("status", classNames), children: labels.loading });
1734
+ return /* @__PURE__ */ jsx10("p", { role: "status", className: partClass("status", classNames), children: labels.loading });
1453
1735
  }
1454
1736
  if (items.length === 0) {
1455
- return /* @__PURE__ */ jsx9("p", { role: "status", className: partClass("status", classNames), children: library.search ? labels.noResults(library.search) : labels.empty });
1737
+ return /* @__PURE__ */ jsx10("p", { role: "status", className: partClass("status", classNames), children: library.search ? labels.noResults(library.search) : labels.empty });
1456
1738
  }
1457
1739
  const isLoadingMore = library.status === "LoadingMore";
1458
1740
  const canLoadMore = library.status === "CanLoadMore" || isLoadingMore;
1459
1741
  return /* @__PURE__ */ jsxs6(Fragment5, { children: [
1460
- /* @__PURE__ */ jsx9(
1742
+ /* @__PURE__ */ jsx10(
1461
1743
  "div",
1462
1744
  {
1463
1745
  role: "listbox",
@@ -1467,7 +1749,7 @@ function LibraryResults(props) {
1467
1749
  className: partClass("list", classNames),
1468
1750
  style: LIST_STYLE,
1469
1751
  onKeyDown: roving.handleKeyDown,
1470
- children: items.map((record, index) => /* @__PURE__ */ jsx9(
1752
+ children: items.map((record, index) => /* @__PURE__ */ jsx10(
1471
1753
  MediaLibraryItem,
1472
1754
  {
1473
1755
  ref: roving.itemRef(index),
@@ -1486,7 +1768,7 @@ function LibraryResults(props) {
1486
1768
  ))
1487
1769
  }
1488
1770
  ),
1489
- canLoadMore && /* @__PURE__ */ jsx9(
1771
+ canLoadMore && /* @__PURE__ */ jsx10(
1490
1772
  "button",
1491
1773
  {
1492
1774
  type: "button",
@@ -1498,7 +1780,7 @@ function LibraryResults(props) {
1498
1780
  )
1499
1781
  ] });
1500
1782
  }
1501
- var LibraryErrorBoundary = class extends React15.Component {
1783
+ var LibraryErrorBoundary = class extends React17.Component {
1502
1784
  state = { hasError: false, error: null };
1503
1785
  static getDerivedStateFromError(error) {
1504
1786
  return { hasError: true, error };
@@ -1540,7 +1822,7 @@ function useMediaObjectUrl(media, options) {
1540
1822
  }
1541
1823
 
1542
1824
  // src/media-picker.tsx
1543
- import React22 from "react";
1825
+ import React24 from "react";
1544
1826
 
1545
1827
  // src/media-picker-accept.ts
1546
1828
  function matchesAccept(candidate, accept) {
@@ -1580,10 +1862,10 @@ function recordMatchesAccept(record, accept) {
1580
1862
  }
1581
1863
 
1582
1864
  // src/media-picker-context.ts
1583
- import React16 from "react";
1584
- var MediaPickerContext = /* @__PURE__ */ React16.createContext(null);
1865
+ import React18 from "react";
1866
+ var MediaPickerContext = /* @__PURE__ */ React18.createContext(null);
1585
1867
  function useMediaPicker() {
1586
- const picker = React16.use(MediaPickerContext);
1868
+ const picker = React18.use(MediaPickerContext);
1587
1869
  if (!picker) throw new Error("useMediaPicker must be used within <MediaPicker>");
1588
1870
  return picker;
1589
1871
  }
@@ -1624,17 +1906,17 @@ function mergeMediaPickerLabels(labels) {
1624
1906
  }
1625
1907
 
1626
1908
  // src/media-picker-library.tsx
1627
- import React17 from "react";
1628
- import { jsx as jsx10 } from "react/jsx-runtime";
1909
+ import React19 from "react";
1910
+ import { jsx as jsx11 } from "react/jsx-runtime";
1629
1911
  function MediaPickerLibrary(props) {
1630
1912
  const picker = useMediaPicker();
1631
1913
  const { accept, toggle } = picker;
1632
1914
  const selectedKeys = picker.selected.map((media) => media.id);
1633
- const filter = React17.useMemo(
1915
+ const filter = React19.useMemo(
1634
1916
  () => accept ? (record) => recordMatchesAccept(record, accept) : void 0,
1635
1917
  [accept]
1636
1918
  );
1637
- return /* @__PURE__ */ jsx10(
1919
+ return /* @__PURE__ */ jsx11(
1638
1920
  MediaLibrary,
1639
1921
  {
1640
1922
  ...props,
@@ -1651,15 +1933,15 @@ function MediaPickerLibrary(props) {
1651
1933
  }
1652
1934
 
1653
1935
  // src/media-picker-selection.tsx
1654
- import React19 from "react";
1936
+ import React21 from "react";
1655
1937
 
1656
1938
  // src/preview.tsx
1657
- import { mediaPath as mediaPath2 } from "@devultur/core";
1939
+ import { mediaPath as mediaPath4 } from "@devultur/core";
1658
1940
 
1659
1941
  // src/video-preview.tsx
1660
- import React18 from "react";
1942
+ import React20 from "react";
1661
1943
  import { VideoPlayer } from "@devultur/react";
1662
- import { Fragment as Fragment6, jsx as jsx11 } from "react/jsx-runtime";
1944
+ import { Fragment as Fragment6, jsx as jsx12 } from "react/jsx-runtime";
1663
1945
  var UNAUTHORIZED = 401;
1664
1946
  var HLS_NETWORK_FAILURE = /Load(Error|TimeOut)$/;
1665
1947
  function fetchErrorOf(error) {
@@ -1674,10 +1956,10 @@ function fetchErrorOf(error) {
1674
1956
  function VideoPreview({ media, renderFallback, loading, metrics, className }) {
1675
1957
  const { token, tokenStatus, tokenError, reissueToken } = useDevultur();
1676
1958
  const videoProps = useVideoProps(media);
1677
- const [failure, setFailure] = React18.useState(null);
1678
- const hasReissuedRef = React18.useRef(false);
1959
+ const [failure, setFailure] = React20.useState(null);
1960
+ const hasReissuedRef = React20.useRef(false);
1679
1961
  const playerToken = videoProps?.token;
1680
- const handleError = React18.useCallback(
1962
+ const handleError = React20.useCallback(
1681
1963
  (error) => {
1682
1964
  const isFirstMismatch = error.code === "session_mismatch" && !hasReissuedRef.current;
1683
1965
  if (isFirstMismatch) {
@@ -1691,14 +1973,14 @@ function VideoPreview({ media, renderFallback, loading, metrics, className }) {
1691
1973
  );
1692
1974
  if (!videoProps) return null;
1693
1975
  const isPrivate = media.visibility === "private";
1694
- if (isPrivate && tokenStatus === "error") return /* @__PURE__ */ jsx11(Fragment6, { children: renderFallback(tokenUnavailableError(tokenError)) });
1695
- if (isPrivate && !token) return /* @__PURE__ */ jsx11(Fragment6, { children: loading });
1696
- if (failure && failure.token === playerToken) return /* @__PURE__ */ jsx11(Fragment6, { children: renderFallback(failure.error) });
1697
- return /* @__PURE__ */ jsx11(VideoPlayer, { ...videoProps, className, metrics, onError: handleError });
1976
+ if (isPrivate && tokenStatus === "error") return /* @__PURE__ */ jsx12(Fragment6, { children: renderFallback(tokenUnavailableError(tokenError)) });
1977
+ if (isPrivate && !token) return /* @__PURE__ */ jsx12(Fragment6, { children: loading });
1978
+ if (failure && failure.token === playerToken) return /* @__PURE__ */ jsx12(Fragment6, { children: renderFallback(failure.error) });
1979
+ return /* @__PURE__ */ jsx12(VideoPlayer, { ...videoProps, className, metrics, onError: handleError });
1698
1980
  }
1699
1981
 
1700
1982
  // src/preview.tsx
1701
- import { Fragment as Fragment7, jsx as jsx12 } from "react/jsx-runtime";
1983
+ import { Fragment as Fragment7, jsx as jsx13 } from "react/jsx-runtime";
1702
1984
  var DEFAULT_MAX_TEXT_BYTES = 1024 * 1024;
1703
1985
  var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
1704
1986
  function Preview({ media, contentType, fallback, loading, labels, metrics, ...viewProps }) {
@@ -1708,11 +1990,11 @@ function Preview({ media, contentType, fallback, loading, labels, metrics, ...vi
1708
1990
  const fetchError = error instanceof MediaFetchError ? error : networkError(error);
1709
1991
  if (typeof fallback === "function") return fallback(fetchError);
1710
1992
  if (fallback !== void 0) return fallback;
1711
- return /* @__PURE__ */ jsx12(DefaultFallback, { media, error: fetchError, labels: text });
1993
+ return /* @__PURE__ */ jsx13(DefaultFallback, { media, error: fetchError, labels: text });
1712
1994
  };
1713
- const loadingNode = loading === void 0 ? /* @__PURE__ */ jsx12(LoadingStatus, { text: text.loading }) : loading;
1995
+ const loadingNode = loading === void 0 ? /* @__PURE__ */ jsx13(LoadingStatus, { text: text.loading }) : loading;
1714
1996
  if (media.kind === "video") {
1715
- return /* @__PURE__ */ jsx12(
1997
+ return /* @__PURE__ */ jsx13(
1716
1998
  VideoPreview,
1717
1999
  {
1718
2000
  media,
@@ -1721,23 +2003,23 @@ function Preview({ media, contentType, fallback, loading, labels, metrics, ...vi
1721
2003
  metrics,
1722
2004
  className: viewProps.className
1723
2005
  },
1724
- mediaPath2(media)
2006
+ mediaPath4(media)
1725
2007
  );
1726
2008
  }
1727
2009
  const expectedType = expectedContentType(media, contentType);
1728
2010
  const kind = previewKindOf(expectedType);
1729
- if (!expectedType || !kind) return /* @__PURE__ */ jsx12(Fragment7, { children: renderFallback(unsupportedType(expectedType)) });
2011
+ if (!expectedType || !kind) return /* @__PURE__ */ jsx13(Fragment7, { children: renderFallback(unsupportedType(expectedType)) });
1730
2012
  const props = { ...viewProps, media, expectedType, renderFallback, loading: loadingNode, labels: text };
1731
- if (kind === "text") return /* @__PURE__ */ jsx12(TextPreview, { ...props });
1732
- if (kind === "pdf") return /* @__PURE__ */ jsx12(PdfPreview, { ...props, maxBytes: props.maxBytes ?? DEFAULT_MAX_BYTES });
1733
- return /* @__PURE__ */ jsx12(BinaryPreview, { ...props, kind });
2013
+ if (kind === "text") return /* @__PURE__ */ jsx13(TextPreview, { ...props });
2014
+ if (kind === "pdf") return /* @__PURE__ */ jsx13(PdfPreview, { ...props, maxBytes: props.maxBytes ?? DEFAULT_MAX_BYTES });
2015
+ return /* @__PURE__ */ jsx13(BinaryPreview, { ...props, kind });
1734
2016
  }
1735
2017
  var unsupportedType = (contentType) => new MediaFetchError("unsupported_type", `Can't preview ${contentType ?? "an unknown type"} inline`);
1736
2018
  function DefaultFallback({ media, error, labels }) {
1737
- return /* @__PURE__ */ jsx12(DownloadLink, { media, title: labels.error(error.code), children: media.filename ?? labels.download });
2019
+ return /* @__PURE__ */ jsx13(DownloadLink, { media, title: labels.error(error.code), children: media.filename ?? labels.download });
1738
2020
  }
1739
2021
  function LoadingStatus({ text }) {
1740
- return /* @__PURE__ */ jsx12("span", { role: "status", style: VISUALLY_HIDDEN, children: text });
2022
+ return /* @__PURE__ */ jsx13("span", { role: "status", style: VISUALLY_HIDDEN, children: text });
1741
2023
  }
1742
2024
  function BinaryPreview({
1743
2025
  media,
@@ -1754,13 +2036,13 @@ function BinaryPreview({
1754
2036
  const isPublicImage = kind === "image" && media.visibility === "public";
1755
2037
  const verified = useMediaObjectUrl(media, { enabled: !isPublicImage, contentType: expectedType, maxBytes });
1756
2038
  const objectUrl = isPublicImage ? url(media) : verified.objectUrl;
1757
- if (verified.error) return /* @__PURE__ */ jsx12(Fragment7, { children: renderFallback(verified.error) });
1758
- if (!objectUrl) return /* @__PURE__ */ jsx12(Fragment7, { children: loading });
2039
+ if (verified.error) return /* @__PURE__ */ jsx13(Fragment7, { children: renderFallback(verified.error) });
2040
+ if (!objectUrl) return /* @__PURE__ */ jsx13(Fragment7, { children: loading });
1759
2041
  const label = title ?? media.filename ?? labels.untitledMedia;
1760
2042
  if (kind === "image") {
1761
- return /* @__PURE__ */ jsx12("img", { src: objectUrl, alt: label, width: media.width, height: media.height, className });
2043
+ return /* @__PURE__ */ jsx13("img", { src: objectUrl, alt: label, width: media.width, height: media.height, className });
1762
2044
  }
1763
- return /* @__PURE__ */ jsx12("audio", { src: objectUrl, controls: true, "aria-label": label, className });
2045
+ return /* @__PURE__ */ jsx13("audio", { src: objectUrl, controls: true, "aria-label": label, className });
1764
2046
  }
1765
2047
  var decodeText = (bytes) => new TextDecoder().decode(bytes);
1766
2048
  function TextPreview({
@@ -1778,19 +2060,19 @@ function TextPreview({
1778
2060
  contentType: expectedType,
1779
2061
  decode: decodeText
1780
2062
  });
1781
- if (error) return /* @__PURE__ */ jsx12(Fragment7, { children: renderFallback(error) });
1782
- if (text === null) return /* @__PURE__ */ jsx12(Fragment7, { children: loading });
1783
- return /* @__PURE__ */ jsx12("pre", { title, className, children: text });
2063
+ if (error) return /* @__PURE__ */ jsx13(Fragment7, { children: renderFallback(error) });
2064
+ if (text === null) return /* @__PURE__ */ jsx13(Fragment7, { children: loading });
2065
+ return /* @__PURE__ */ jsx13("pre", { title, className, children: text });
1784
2066
  }
1785
2067
 
1786
2068
  // src/media-picker-selection.tsx
1787
- import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
2069
+ import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
1788
2070
  var THUMBNAIL_WIDTH2 = 96;
1789
2071
  function MediaPickerSelection({ renderItem, headingLevel = 3, className }) {
1790
2072
  const picker = useMediaPicker();
1791
2073
  const { labels, selected } = picker;
1792
- const headingId = React19.useId();
1793
- const [previewedId, setPreviewedId] = React19.useState(null);
2074
+ const headingId = React21.useId();
2075
+ const [previewedId, setPreviewedId] = React21.useState(null);
1794
2076
  const Heading = `h${headingLevel}`;
1795
2077
  const cardHeadingLevel = headingLevel + 1;
1796
2078
  const regionClass = className ? `devultur-media-picker__selection ${className}` : "devultur-media-picker__selection";
@@ -1803,21 +2085,21 @@ function MediaPickerSelection({ renderItem, headingLevel = 3, className }) {
1803
2085
  "aria-labelledby": headingId,
1804
2086
  className: regionClass,
1805
2087
  children: [
1806
- /* @__PURE__ */ jsx13(Heading, { id: headingId, children: labels.selection }),
1807
- selected.length === 0 && /* @__PURE__ */ jsx13("p", { "data-part": "empty", children: labels.empty }),
1808
- selected.length > 0 && /* @__PURE__ */ jsx13("ul", { "data-part": "items", children: selected.map((media) => /* @__PURE__ */ jsxs7("li", { "data-kind": media.kind, children: [
1809
- renderItem ? renderItem(media) : /* @__PURE__ */ jsx13(
2088
+ /* @__PURE__ */ jsx14(Heading, { id: headingId, children: labels.selection }),
2089
+ selected.length === 0 && /* @__PURE__ */ jsx14("p", { "data-part": "empty", children: labels.empty }),
2090
+ selected.length > 0 && /* @__PURE__ */ jsx14("ul", { "data-part": "items", children: selected.map((media) => /* @__PURE__ */ jsxs7("li", { "data-kind": media.kind, children: [
2091
+ renderItem ? renderItem(media) : /* @__PURE__ */ jsx14(
1810
2092
  AttachmentCard,
1811
2093
  {
1812
2094
  media,
1813
2095
  headingLevel: cardHeadingLevel,
1814
2096
  labels: picker.attachmentLabels,
1815
- preview: media.kind === "image" ? /* @__PURE__ */ jsx13(Thumbnail2, { media }) : void 0,
2097
+ preview: media.kind === "image" ? /* @__PURE__ */ jsx14(Thumbnail2, { media }) : void 0,
1816
2098
  onOpen: handleOpen
1817
2099
  }
1818
2100
  ),
1819
- previewedId === media.id && /* @__PURE__ */ jsx13(Preview, { media, title: mediaName(media) }),
1820
- /* @__PURE__ */ jsx13(
2101
+ previewedId === media.id && /* @__PURE__ */ jsx14(Preview, { media, title: mediaName(media) }),
2102
+ /* @__PURE__ */ jsx14(
1821
2103
  "button",
1822
2104
  {
1823
2105
  type: "button",
@@ -1827,13 +2109,13 @@ function MediaPickerSelection({ renderItem, headingLevel = 3, className }) {
1827
2109
  }
1828
2110
  )
1829
2111
  ] }, media.id)) }),
1830
- /* @__PURE__ */ jsx13(SelectionActions, {})
2112
+ /* @__PURE__ */ jsx14(SelectionActions, {})
1831
2113
  ]
1832
2114
  }
1833
2115
  );
1834
2116
  }
1835
2117
  function Thumbnail2({ media }) {
1836
- return /* @__PURE__ */ jsx13(
2118
+ return /* @__PURE__ */ jsx14(
1837
2119
  Image,
1838
2120
  {
1839
2121
  media,
@@ -1850,18 +2132,18 @@ function SelectionActions() {
1850
2132
  const { labels } = picker;
1851
2133
  if (picker.selected.length === 0) return null;
1852
2134
  return /* @__PURE__ */ jsxs7("div", { "data-part": "actions", children: [
1853
- !picker.multiple && /* @__PURE__ */ jsx13("button", { type: "button", "data-part": "replace", onClick: picker.openFileDialog, children: labels.replace }),
1854
- /* @__PURE__ */ jsx13("button", { type: "button", "data-part": "clear", onClick: picker.clear, children: labels.clear })
2135
+ !picker.multiple && /* @__PURE__ */ jsx14("button", { type: "button", "data-part": "replace", onClick: picker.openFileDialog, children: labels.replace }),
2136
+ /* @__PURE__ */ jsx14("button", { type: "button", "data-part": "clear", onClick: picker.clear, children: labels.clear })
1855
2137
  ] });
1856
2138
  }
1857
2139
 
1858
2140
  // src/media-picker-upload.tsx
1859
- import React20 from "react";
1860
- import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
2141
+ import React22 from "react";
2142
+ import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
1861
2143
  function MediaPickerUpload({ className }) {
1862
2144
  const picker = useMediaPicker();
1863
2145
  const { labels, multiple } = picker;
1864
- const [isDragging, setIsDragging] = React20.useState(false);
2146
+ const [isDragging, setIsDragging] = React22.useState(false);
1865
2147
  const handleDragOver = (event) => {
1866
2148
  event.preventDefault();
1867
2149
  setIsDragging(true);
@@ -1888,9 +2170,9 @@ function MediaPickerUpload({ className }) {
1888
2170
  onDragLeave: handleDragLeave,
1889
2171
  onDrop: handleDrop,
1890
2172
  children: [
1891
- /* @__PURE__ */ jsx14("button", { type: "button", "data-part": "upload", onClick: picker.openFileDialog, children: labels.upload(multiple) }),
1892
- /* @__PURE__ */ jsx14("span", { "data-part": "drop-hint", children: labels.dropHint(multiple) }),
1893
- picker.uploads.length > 0 && /* @__PURE__ */ jsx14("ul", { "aria-label": labels.uploads, "data-part": "uploads", children: picker.uploads.map((upload) => /* @__PURE__ */ jsx14(UploadRow, { upload }, upload.id)) })
2173
+ /* @__PURE__ */ jsx15("button", { type: "button", "data-part": "upload", onClick: picker.openFileDialog, children: labels.upload(multiple) }),
2174
+ /* @__PURE__ */ jsx15("span", { "data-part": "drop-hint", children: labels.dropHint(multiple) }),
2175
+ picker.uploads.length > 0 && /* @__PURE__ */ jsx15("ul", { "aria-label": labels.uploads, "data-part": "uploads", children: picker.uploads.map((upload) => /* @__PURE__ */ jsx15(UploadRow, { upload }, upload.id)) })
1894
2176
  ]
1895
2177
  }
1896
2178
  )
@@ -1902,23 +2184,23 @@ function UploadRow({ upload }) {
1902
2184
  const filename = upload.file.name;
1903
2185
  if (upload.status === "failed") {
1904
2186
  return /* @__PURE__ */ jsxs8("li", { "data-status": "failed", children: [
1905
- /* @__PURE__ */ jsx14("span", { "data-part": "filename", children: filename }),
1906
- /* @__PURE__ */ jsx14("span", { role: "alert", "data-part": "error", children: upload.error }),
1907
- upload.canRetry && /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => picker.retry(upload.id), children: labels.retry }),
1908
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => picker.dismiss(upload.id), children: labels.dismiss })
2187
+ /* @__PURE__ */ jsx15("span", { "data-part": "filename", children: filename }),
2188
+ /* @__PURE__ */ jsx15("span", { role: "alert", "data-part": "error", children: upload.error }),
2189
+ upload.canRetry && /* @__PURE__ */ jsx15("button", { type: "button", onClick: () => picker.retry(upload.id), children: labels.retry }),
2190
+ /* @__PURE__ */ jsx15("button", { type: "button", onClick: () => picker.dismiss(upload.id), children: labels.dismiss })
1909
2191
  ] });
1910
2192
  }
1911
2193
  const percent = Math.round(upload.fraction * 100);
1912
2194
  return /* @__PURE__ */ jsxs8("li", { "data-status": "uploading", children: [
1913
- /* @__PURE__ */ jsx14("span", { "data-part": "filename", children: filename }),
1914
- /* @__PURE__ */ jsx14("progress", { max: 100, value: percent, "aria-label": labels.progress(filename) }),
1915
- /* @__PURE__ */ jsx14("span", { "data-part": "percent", children: labels.percent(percent) }),
1916
- /* @__PURE__ */ jsx14("button", { type: "button", "aria-label": labels.cancelUpload(filename), onClick: () => picker.cancel(upload.id), children: labels.cancel })
2195
+ /* @__PURE__ */ jsx15("span", { "data-part": "filename", children: filename }),
2196
+ /* @__PURE__ */ jsx15("progress", { max: 100, value: percent, "aria-label": labels.progress(filename) }),
2197
+ /* @__PURE__ */ jsx15("span", { "data-part": "percent", children: labels.percent(percent) }),
2198
+ /* @__PURE__ */ jsx15("button", { type: "button", "aria-label": labels.cancelUpload(filename), onClick: () => picker.cancel(upload.id), children: labels.cancel })
1917
2199
  ] });
1918
2200
  }
1919
2201
 
1920
2202
  // src/use-media-picker-uploads.ts
1921
- import React21 from "react";
2203
+ import React23 from "react";
1922
2204
  var isAbortError = (error) => typeof error === "object" && error !== null && error.name === "AbortError";
1923
2205
  var isPdf = (file) => file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
1924
2206
  var nextUploadId = 0;
@@ -1929,18 +2211,18 @@ async function pageCountOf(file, countPdfPages) {
1929
2211
  }
1930
2212
  function useMediaPickerUploads(options) {
1931
2213
  const { upload } = useDevultur();
1932
- const [uploads, setUploads] = React21.useState([]);
1933
- const controllers = React21.useRef(/* @__PURE__ */ new Map());
1934
- const optionsRef = React21.useRef(options);
2214
+ const [uploads, setUploads] = React23.useState([]);
2215
+ const controllers = React23.useRef(/* @__PURE__ */ new Map());
2216
+ const optionsRef = React23.useRef(options);
1935
2217
  optionsRef.current = options;
1936
- const patch = React21.useCallback((id, changes) => {
2218
+ const patch = React23.useCallback((id, changes) => {
1937
2219
  setUploads((current) => current.map((entry) => entry.id === id ? { ...entry, ...changes } : entry));
1938
2220
  }, []);
1939
- const drop = React21.useCallback((id) => {
2221
+ const drop = React23.useCallback((id) => {
1940
2222
  controllers.current.delete(id);
1941
2223
  setUploads((current) => current.filter((entry) => entry.id !== id));
1942
2224
  }, []);
1943
- const run = React21.useCallback(
2225
+ const run = React23.useCallback(
1944
2226
  async (id, file) => {
1945
2227
  const { labels, announce } = optionsRef.current;
1946
2228
  const controller = new AbortController();
@@ -1970,7 +2252,7 @@ function useMediaPickerUploads(options) {
1970
2252
  },
1971
2253
  [upload, patch, drop]
1972
2254
  );
1973
- const start = React21.useCallback(
2255
+ const start = React23.useCallback(
1974
2256
  (files) => {
1975
2257
  const { accept, multiple, labels } = optionsRef.current;
1976
2258
  const chosen = multiple ? [...files] : [...files].slice(0, 1);
@@ -1992,8 +2274,8 @@ function useMediaPickerUploads(options) {
1992
2274
  },
1993
2275
  [run]
1994
2276
  );
1995
- const cancel = React21.useCallback((id) => controllers.current.get(id)?.abort(), []);
1996
- const retry = React21.useCallback(
2277
+ const cancel = React23.useCallback((id) => controllers.current.get(id)?.abort(), []);
2278
+ const retry = React23.useCallback(
1997
2279
  (id) => {
1998
2280
  const entry = uploads.find((candidate) => candidate.id === id);
1999
2281
  if (!entry?.canRetry) return;
@@ -2001,7 +2283,7 @@ function useMediaPickerUploads(options) {
2001
2283
  },
2002
2284
  [uploads, run]
2003
2285
  );
2004
- React21.useEffect(() => {
2286
+ React23.useEffect(() => {
2005
2287
  const active = controllers.current;
2006
2288
  return () => {
2007
2289
  for (const controller of active.values()) controller.abort();
@@ -2011,7 +2293,7 @@ function useMediaPickerUploads(options) {
2011
2293
  }
2012
2294
 
2013
2295
  // src/media-picker.tsx
2014
- import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
2296
+ import { Fragment as Fragment8, jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
2015
2297
  var EMPTY = [];
2016
2298
  function selectionOf(props) {
2017
2299
  if (props.value === void 0) return void 0;
@@ -2028,22 +2310,22 @@ function emit(props, next) {
2028
2310
  var isSame = (a, b) => a.id === b.id;
2029
2311
  function MediaPickerRoot(props) {
2030
2312
  const isMultiple = props.multiple ?? false;
2031
- const labels = React22.useMemo(() => mergeMediaPickerLabels(props.labels), [props.labels]);
2313
+ const labels = React24.useMemo(() => mergeMediaPickerLabels(props.labels), [props.labels]);
2032
2314
  const accept = uploadAcceptOf(props.accept, props.type);
2033
2315
  const libraryType = props.type ?? mediaTypeOfAccept(props.accept);
2034
2316
  const countPdfPages = usePdfPageCounter();
2035
- const [announcement, setAnnouncement] = React22.useState("");
2036
- const inputRef = React22.useRef(null);
2037
- const selectionRef = React22.useRef(null);
2038
- const [ownSelection, setOwnSelection] = React22.useState(EMPTY);
2317
+ const [announcement, setAnnouncement] = React24.useState("");
2318
+ const inputRef = React24.useRef(null);
2319
+ const selectionRef = React24.useRef(null);
2320
+ const [ownSelection, setOwnSelection] = React24.useState(EMPTY);
2039
2321
  const controlledSelection = selectionOf(props);
2040
2322
  const selected = controlledSelection ?? ownSelection;
2041
- const selectedRef = React22.useRef(selected);
2323
+ const selectedRef = React24.useRef(selected);
2042
2324
  selectedRef.current = selected;
2043
- const propsRef = React22.useRef(props);
2325
+ const propsRef = React24.useRef(props);
2044
2326
  propsRef.current = props;
2045
2327
  const isControlled = controlledSelection !== void 0;
2046
- const commit = React22.useCallback(
2328
+ const commit = React24.useCallback(
2047
2329
  (next) => {
2048
2330
  selectedRef.current = next;
2049
2331
  if (!isControlled) setOwnSelection(next);
@@ -2051,15 +2333,15 @@ function MediaPickerRoot(props) {
2051
2333
  },
2052
2334
  [isControlled]
2053
2335
  );
2054
- const announce = React22.useCallback((message) => setAnnouncement(message), []);
2055
- const select = React22.useCallback(
2336
+ const announce = React24.useCallback((message) => setAnnouncement(message), []);
2337
+ const select = React24.useCallback(
2056
2338
  (media) => {
2057
2339
  const current = selectedRef.current.filter((item) => !isSame(item, media));
2058
2340
  commit(isMultiple ? [...current, media] : [media]);
2059
2341
  },
2060
2342
  [commit, isMultiple]
2061
2343
  );
2062
- const remove = React22.useCallback(
2344
+ const remove = React24.useCallback(
2063
2345
  (media) => {
2064
2346
  commit(selectedRef.current.filter((item) => !isSame(item, media)));
2065
2347
  announce(labels.removed(mediaName(media)));
@@ -2067,7 +2349,7 @@ function MediaPickerRoot(props) {
2067
2349
  },
2068
2350
  [commit, announce, labels]
2069
2351
  );
2070
- const toggle = React22.useCallback(
2352
+ const toggle = React24.useCallback(
2071
2353
  (media) => {
2072
2354
  const isSelected = selectedRef.current.some((item) => isSame(item, media));
2073
2355
  if (isMultiple && isSelected) {
@@ -2079,12 +2361,12 @@ function MediaPickerRoot(props) {
2079
2361
  },
2080
2362
  [isMultiple, remove, select, announce, labels]
2081
2363
  );
2082
- const clear = React22.useCallback(() => {
2364
+ const clear = React24.useCallback(() => {
2083
2365
  commit([]);
2084
2366
  announce(labels.cleared);
2085
2367
  selectionRef.current?.focus();
2086
2368
  }, [commit, announce, labels]);
2087
- const handleUploaded = React22.useCallback(
2369
+ const handleUploaded = React24.useCallback(
2088
2370
  (media) => {
2089
2371
  select(media);
2090
2372
  selectionRef.current?.focus();
@@ -2122,13 +2404,13 @@ function MediaPickerRoot(props) {
2122
2404
  selectionRef
2123
2405
  };
2124
2406
  const content = typeof props.children === "function" ? props.children(picker) : props.children ?? /* @__PURE__ */ jsxs9(Fragment8, { children: [
2125
- /* @__PURE__ */ jsx15(MediaPickerUpload, {}),
2126
- /* @__PURE__ */ jsx15(MediaPickerLibrary, {}),
2127
- /* @__PURE__ */ jsx15(MediaPickerSelection, {})
2407
+ /* @__PURE__ */ jsx16(MediaPickerUpload, {}),
2408
+ /* @__PURE__ */ jsx16(MediaPickerLibrary, {}),
2409
+ /* @__PURE__ */ jsx16(MediaPickerSelection, {})
2128
2410
  ] });
2129
2411
  const rootClass = props.className ? `devultur-media-picker ${props.className}` : "devultur-media-picker";
2130
- return /* @__PURE__ */ jsx15(MediaPickerContext, { value: picker, children: /* @__PURE__ */ jsxs9("div", { className: rootClass, children: [
2131
- /* @__PURE__ */ jsx15(
2412
+ return /* @__PURE__ */ jsx16(MediaPickerContext, { value: picker, children: /* @__PURE__ */ jsxs9("div", { className: rootClass, children: [
2413
+ /* @__PURE__ */ jsx16(
2132
2414
  "input",
2133
2415
  {
2134
2416
  ref: inputRef,
@@ -2143,7 +2425,7 @@ function MediaPickerRoot(props) {
2143
2425
  }
2144
2426
  ),
2145
2427
  content,
2146
- /* @__PURE__ */ jsx15("div", { role: "status", "aria-live": "polite", style: VISUALLY_HIDDEN, children: announcement })
2428
+ /* @__PURE__ */ jsx16("div", { role: "status", "aria-live": "polite", style: VISUALLY_HIDDEN, children: announcement })
2147
2429
  ] }) });
2148
2430
  }
2149
2431
  var MediaPicker = /* @__PURE__ */ Object.assign(MediaPickerRoot, {
@@ -2154,9 +2436,9 @@ var MediaPicker = /* @__PURE__ */ Object.assign(MediaPickerRoot, {
2154
2436
  });
2155
2437
 
2156
2438
  // src/viewer-provider.tsx
2157
- import React23 from "react";
2158
- import { useAction } from "convex/react";
2159
- import { jsx as jsx16 } from "react/jsx-runtime";
2439
+ import React25 from "react";
2440
+ import { useAction as useAction2 } from "convex/react";
2441
+ import { jsx as jsx17 } from "react/jsx-runtime";
2160
2442
  var VIEWER_ACTIONS = {
2161
2443
  uploadUrl: () => Promise.reject(new DevulturCapabilityError("uploadUrl")),
2162
2444
  upload: () => Promise.reject(new DevulturCapabilityError("upload")),
@@ -2166,15 +2448,15 @@ var VIEWER_ACTIONS = {
2166
2448
  };
2167
2449
  var NO_TOKEN_ACTION = () => Promise.reject(new Error("<DevulturViewerProvider> has no `issueViewerToken`: private media can't be shown"));
2168
2450
  function DevulturViewerProvider({ issueViewerToken, ...props }) {
2169
- if (!issueViewerToken) return /* @__PURE__ */ jsx16(ViewerSession, { ...props, issueViewerToken: NO_TOKEN_ACTION, lazyToken: true });
2170
- return /* @__PURE__ */ jsx16(ConvexViewerSession, { ...props, issueViewerToken });
2451
+ if (!issueViewerToken) return /* @__PURE__ */ jsx17(ViewerSession, { ...props, issueViewerToken: NO_TOKEN_ACTION, lazyToken: true });
2452
+ return /* @__PURE__ */ jsx17(ConvexViewerSession, { ...props, issueViewerToken });
2171
2453
  }
2172
2454
  function ConvexViewerSession({
2173
2455
  issueViewerToken,
2174
2456
  ...props
2175
2457
  }) {
2176
- const issueViewerTokenAction = useAction(issueViewerToken);
2177
- return /* @__PURE__ */ jsx16(ViewerSession, { issueViewerToken: issueViewerTokenAction, ...props });
2458
+ const issueViewerTokenAction = useAction2(issueViewerToken);
2459
+ return /* @__PURE__ */ jsx17(ViewerSession, { issueViewerToken: issueViewerTokenAction, ...props });
2178
2460
  }
2179
2461
  function ViewerSession({
2180
2462
  baseUrl,
@@ -2184,28 +2466,27 @@ function ViewerSession({
2184
2466
  children
2185
2467
  }) {
2186
2468
  const session = useMediaSession({ issueViewerToken, tokenArgs, baseUrl, isLazy: lazyToken });
2187
- const devulturValue = React23.useMemo(() => ({ ...session, ...VIEWER_ACTIONS }), [session]);
2188
- return /* @__PURE__ */ jsx16(DevulturContext, { value: devulturValue, children });
2469
+ const devulturValue = React25.useMemo(() => ({ ...session, ...VIEWER_ACTIONS }), [session]);
2470
+ return /* @__PURE__ */ jsx17(DevulturContext, { value: devulturValue, children });
2189
2471
  }
2190
2472
 
2191
2473
  // src/react.tsx
2192
- import { jsx as jsx17 } from "react/jsx-runtime";
2193
- var ProgressContext = /* @__PURE__ */ React24.createContext(null);
2474
+ import { jsx as jsx18 } from "react/jsx-runtime";
2194
2475
  function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children }) {
2195
- const issueViewerTokenAction = useAction2(issueViewerToken ?? api.issueViewerToken);
2476
+ const issueViewerTokenAction = useAction3(issueViewerToken ?? api.issueViewerToken);
2196
2477
  const session = useMediaSession({ issueViewerToken: issueViewerTokenAction, tokenArgs, baseUrl });
2197
- const createUploadUrlAction = useAction2(api.createUploadUrl);
2198
- const deleteMediaAction = useAction2(api.deleteMedia);
2199
- const confirmUploadAction = useAction2(api.confirmUpload ?? api.createUploadUrl);
2200
- const deleteVideoAction = useAction2(api.deleteVideo ?? api.deleteMedia);
2201
- const uploadUrl = React24.useCallback(
2478
+ const createUploadUrlAction = useAction3(api.createUploadUrl);
2479
+ const deleteMediaAction = useAction3(api.deleteMedia);
2480
+ const confirmUploadAction = useAction3(api.confirmUpload ?? api.createUploadUrl);
2481
+ const deleteVideoAction = useAction3(api.deleteVideo ?? api.deleteMedia);
2482
+ const uploadUrl = React26.useCallback(
2202
2483
  async (file) => {
2203
2484
  const result = await createUploadUrlAction({ filename: file.name, contentType: file.type });
2204
2485
  return { url: result.url, key: result.key };
2205
2486
  },
2206
2487
  [createUploadUrlAction]
2207
2488
  );
2208
- const upload = React24.useCallback(
2489
+ const upload = React26.useCallback(
2209
2490
  async (file, options) => {
2210
2491
  const dimensions = await readImageDimensions(file);
2211
2492
  options?.signal?.throwIfAborted();
@@ -2214,7 +2495,7 @@ function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children
2214
2495
  contentType: file.type,
2215
2496
  visibility: options?.visibility
2216
2497
  });
2217
- await uploadFileWithProgress(target.url, file, { onBytes: options?.onProgress, signal: options?.signal });
2498
+ await uploadFileWithProgress2(target.url, file, { onBytes: options?.onProgress, signal: options?.signal });
2218
2499
  if (api.confirmUpload) {
2219
2500
  const pageCount = options?.pageCount;
2220
2501
  const hints = { ...dimensions, ...pageCount !== void 0 ? { pageCount } : {} };
@@ -2231,23 +2512,23 @@ function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children
2231
2512
  },
2232
2513
  [createUploadUrlAction, confirmUploadAction, api.confirmUpload]
2233
2514
  );
2234
- const deleteMedia = React24.useCallback(
2515
+ const deleteMedia = React26.useCallback(
2235
2516
  (media) => {
2236
2517
  if (media.kind === "video" && api.deleteVideo) {
2237
2518
  deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
2238
2519
  return;
2239
2520
  }
2240
- deleteMediaAction({ key: mediaPath3(media) }).catch(console.error);
2521
+ deleteMediaAction({ key: mediaPath5(media) }).catch(console.error);
2241
2522
  },
2242
2523
  [deleteMediaAction, deleteVideoAction, api.deleteVideo]
2243
2524
  );
2244
- const devulturValue = React24.useMemo(
2525
+ const devulturValue = React26.useMemo(
2245
2526
  () => ({ ...session, uploadUrl, upload, deleteMedia }),
2246
2527
  [session, uploadUrl, upload, deleteMedia]
2247
2528
  );
2248
- let content = /* @__PURE__ */ jsx17(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx17(DevulturContext, { value: devulturValue, children }) });
2529
+ let content = /* @__PURE__ */ jsx18(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx18(DevulturContext, { value: devulturValue, children }) });
2249
2530
  if (api.saveProgress && api.loadProgress && api.markComplete && api.resetProgress) {
2250
- content = /* @__PURE__ */ jsx17(
2531
+ content = /* @__PURE__ */ jsx18(
2251
2532
  ProgressBridge,
2252
2533
  {
2253
2534
  save: api.saveProgress,
@@ -2271,13 +2552,13 @@ function ProgressBridge({
2271
2552
  const saveMutation = useMutation(save);
2272
2553
  const markCompleteMutation = useMutation(markComplete);
2273
2554
  const resetMutation = useMutation(reset);
2274
- const onSave = React24.useCallback(
2555
+ const onSave = React26.useCallback(
2275
2556
  async (videoId, position) => {
2276
2557
  await saveMutation({ videoId, position });
2277
2558
  },
2278
2559
  [saveMutation]
2279
2560
  );
2280
- const onLoad = React24.useCallback(
2561
+ const onLoad = React26.useCallback(
2281
2562
  async (videoId) => {
2282
2563
  const result = await convex.query(load, { videoId });
2283
2564
  if (!result) return null;
@@ -2288,354 +2569,31 @@ function ProgressBridge({
2288
2569
  },
2289
2570
  [convex, load]
2290
2571
  );
2291
- const onMarkComplete = React24.useCallback(
2572
+ const onMarkComplete = React26.useCallback(
2292
2573
  async (videoId) => {
2293
2574
  await markCompleteMutation({ videoId });
2294
2575
  },
2295
2576
  [markCompleteMutation]
2296
2577
  );
2297
- const onReset = React24.useCallback(
2578
+ const onReset = React26.useCallback(
2298
2579
  async (videoId) => {
2299
2580
  await resetMutation({ videoId });
2300
2581
  },
2301
2582
  [resetMutation]
2302
2583
  );
2303
- const value = React24.useMemo(
2584
+ const value = React26.useMemo(
2304
2585
  () => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
2305
2586
  [onSave, onLoad, onMarkComplete, onReset]
2306
2587
  );
2307
- return /* @__PURE__ */ jsx17(ProgressContext, { value, children });
2308
- }
2309
- function useDevulturProgress() {
2310
- const context = React24.use(ProgressContext);
2311
- if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
2312
- return context;
2313
- }
2314
- function useMediaUrl(key, options) {
2315
- const devultur = useDevultur();
2316
- const { token, baseUrl } = devultur;
2317
- useRequestTokenWhen(devultur, Boolean(key) && visibilityFromKey(key ?? "") !== "public");
2318
- return React24.useMemo(() => {
2319
- if (!key) return null;
2320
- if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
2321
- const isPublic = visibilityFromKey(key) === "public";
2322
- return buildMediaUrl2(baseUrl, key, { ...options, token: isPublic ? void 0 : token ?? void 0 });
2323
- }, [key, baseUrl, token, options]);
2324
- }
2325
- async function confirmUploadBestEffort(confirmUpload, key, file, hints) {
2326
- try {
2327
- await confirmUpload({ key, filename: file.name, ...hints });
2328
- } catch (error) {
2329
- console.warn("Devultur: could not record the upload; it is recorded later", error);
2330
- }
2331
- }
2332
- async function readImageDimensions(file) {
2333
- if (typeof createImageBitmap !== "function" || !file.type.startsWith("image/")) return {};
2334
- try {
2335
- const bitmap = await createImageBitmap(file);
2336
- const dimensions = { width: bitmap.width, height: bitmap.height };
2337
- bitmap.close();
2338
- return dimensions;
2339
- } catch {
2340
- return {};
2341
- }
2342
- }
2343
- function useVideoProps(media, options) {
2344
- const devultur = useDevultur();
2345
- const { token, baseUrl, sessionSecret } = devultur;
2346
- useRequestTokenWhen(devultur, media?.kind === "video" && media.visibility === "private");
2347
- const locales = options?.locales;
2348
- return React24.useMemo(() => {
2349
- if (media?.kind !== "video") return null;
2350
- if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
2351
- const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
2352
- const captions = (locales ?? []).flatMap((locale) => {
2353
- const path = mediaCaptionPath2(media, locale);
2354
- if (!path) return [];
2355
- const label = CaptionLocaleLabel[locale] ?? locale;
2356
- return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
2357
- });
2358
- return {
2359
- src: buildMediaUrl2(baseUrl, mediaPath3(media), { token: viewerToken }),
2360
- token: viewerToken,
2361
- captions,
2362
- sessionSecret: media.visibility === "private" ? sessionSecret : void 0
2363
- };
2364
- }, [media, baseUrl, token, locales, sessionSecret]);
2365
- }
2366
- function DownloadLink({ media, filename, onDownloadError, children, ...anchorProps }) {
2367
- const devultur = useDevultur();
2368
- const { url, token, tokenStatus, tokenError, baseUrl, sessionSecret, reissueToken } = devultur;
2369
- useRequestTokenWhen(devultur, media?.visibility === "private");
2370
- const handleClick = React24.useCallback(
2371
- async (event) => {
2372
- if (media?.visibility !== "private" || !baseUrl) return;
2373
- event.preventDefault();
2374
- if (tokenStatus === "error") {
2375
- onDownloadError?.(tokenUnavailableError(tokenError));
2376
- return;
2377
- }
2378
- const response = await fetchMedia({
2379
- buildUrl: (viewerToken) => mediaUrl4(baseUrl, media, { download: filename ?? true, token: viewerToken }),
2380
- token,
2381
- sessionSecret,
2382
- reissueToken
2383
- });
2384
- if (!response.ok) {
2385
- const status = response.status;
2386
- onDownloadError?.(new MediaFetchError("http_error", `Media fetch failed: ${status}`, status));
2387
- return;
2388
- }
2389
- const objectUrl = URL.createObjectURL(await response.blob());
2390
- const anchor = document.createElement("a");
2391
- anchor.href = objectUrl;
2392
- anchor.download = filename ?? media.filename ?? "download";
2393
- anchor.click();
2394
- URL.revokeObjectURL(objectUrl);
2395
- },
2396
- [media, baseUrl, token, tokenStatus, tokenError, filename, sessionSecret, reissueToken, onDownloadError]
2397
- );
2398
- if (!media) return null;
2399
- return /* @__PURE__ */ jsx17("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
2400
- }
2401
- function useDevulturMedia(key) {
2402
- const apiCtx = React24.use(DevulturApiContext);
2403
- if (!apiCtx?.getMedia) {
2404
- throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
2405
- }
2406
- const record = useQuery(apiCtx.getMedia, key ? { key } : "skip");
2407
- if (!record) {
2408
- return {
2409
- status: MediaStatus.IDLE,
2410
- progress: null,
2411
- error: null,
2412
- failureReason: null,
2413
- retriable: null,
2414
- isQueued: false,
2415
- isTranscoding: false,
2416
- isCaptioning: false,
2417
- isReady: false,
2418
- isFailed: false,
2419
- playlistUrl: null,
2420
- thumbnailKey: null,
2421
- captionLocales: [],
2422
- duration: null,
2423
- isLoading: record === void 0
2424
- };
2425
- }
2426
- const status = record.transcodeStatus ?? MediaStatus.IDLE;
2427
- const isReady = status === MediaStatus.READY;
2428
- const isFailed = status === MediaStatus.FAILED;
2429
- const progress = record.transcodeProgress ? {
2430
- current: record.transcodeProgress.current,
2431
- total: record.transcodeProgress.total,
2432
- percent: record.transcodeProgress.total > 0 ? Math.round(
2433
- record.transcodeProgress.current / record.transcodeProgress.total * 100
2434
- ) : 0,
2435
- phase: record.transcodeProgress.phase
2436
- } : null;
2437
- return {
2438
- status,
2439
- progress,
2440
- error: record.transcodeError ?? null,
2441
- failureReason: isFailed ? record.failureReason ?? "internal" : null,
2442
- retriable: isFailed ? record.retriable ?? true : null,
2443
- isQueued: status === MediaStatus.QUEUED,
2444
- isTranscoding: status === MediaStatus.TRANSCODING,
2445
- isCaptioning: status === MediaStatus.CAPTIONING,
2446
- isReady,
2447
- isFailed,
2448
- playlistUrl: record.playlistUrl ?? null,
2449
- thumbnailKey: record.thumbnailKey ?? null,
2450
- captionLocales: record.captionLocales ?? [],
2451
- duration: record.duration ?? null,
2452
- isLoading: false
2453
- };
2454
- }
2455
- function useVideoState(media, options) {
2456
- const devultur = useDevultur();
2457
- const { token, baseUrl, sessionSecret } = devultur;
2458
- useRequestTokenWhen(devultur, media?.kind === "video" && media.visibility === "private");
2459
- const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
2460
- const locales = options?.locales;
2461
- return React24.useMemo(() => {
2462
- if (media?.kind !== "video") return { phase: "idle" };
2463
- if (status.isFailed) {
2464
- return {
2465
- phase: "failed",
2466
- reason: status.failureReason ?? "internal",
2467
- message: status.error ?? "Video processing failed",
2468
- retriable: status.retriable ?? true
2469
- };
2470
- }
2471
- if (status.isReady) {
2472
- if (!baseUrl) throw new Error("useVideoState requires `baseUrl` on <DevulturProvider>");
2473
- const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
2474
- const playlist = buildMediaUrl2(baseUrl, mediaPath3(media), { token: viewerToken });
2475
- const captions = (locales ?? status.captionLocales).flatMap((locale) => {
2476
- const path = mediaCaptionPath2(media, locale);
2477
- if (!path) return [];
2478
- const label = CaptionLocaleLabel[locale] ?? locale;
2479
- return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
2480
- });
2481
- return {
2482
- phase: "ready",
2483
- media,
2484
- playlist,
2485
- duration: status.duration ?? 0,
2486
- captions,
2487
- playerProps: {
2488
- src: playlist,
2489
- token: viewerToken,
2490
- captions,
2491
- sessionSecret: media.visibility === "private" ? sessionSecret : void 0
2492
- }
2493
- };
2494
- }
2495
- if (status.isQueued) return { phase: "queued" };
2496
- if (status.isTranscoding || status.isCaptioning) {
2497
- const step = status.progress?.phase ?? (status.isCaptioning ? "captioning" : "encoding");
2498
- return { phase: "processing", step, percent: status.progress?.percent ?? 0 };
2499
- }
2500
- return { phase: "idle" };
2501
- }, [media, status, baseUrl, token, locales, sessionSecret]);
2502
- }
2503
- function useMediaUpload(options) {
2504
- const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
2505
- const api = useDevulturApi();
2506
- const createUploadUrlAction = useAction2(api.createUploadUrl);
2507
- const deleteMediaAction = useAction2(api.deleteMedia);
2508
- const confirmUploadAction = useAction2(api.confirmUpload ?? api.createUploadUrl);
2509
- const processVideoAction = useAction2(api.processVideo ?? api.createUploadUrl);
2510
- const [phase, setPhase] = React24.useState("idle");
2511
- const [percent, setPercent] = React24.useState(0);
2512
- const [currentFile, setCurrentFile] = React24.useState(null);
2513
- const [media, setMedia] = React24.useState(null);
2514
- const [uploadFailure, setUploadFailure] = React24.useState(null);
2515
- const [processRequested, setProcessRequested] = React24.useState(false);
2516
- const abortRef = React24.useRef(null);
2517
- const lastFileRef = React24.useRef(null);
2518
- const lastKeyRef = React24.useRef(null);
2519
- const videoState = useVideoState(media, { locales });
2520
- const triggerProcess = React24.useCallback(
2521
- (key) => {
2522
- if (!api.processVideo) {
2523
- throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
2524
- }
2525
- lastKeyRef.current = key;
2526
- setProcessRequested(true);
2527
- processVideoAction({ key }).catch((err) => {
2528
- console.error("useMediaUpload: could not request the transcode", err);
2529
- });
2530
- },
2531
- [api.processVideo, processVideoAction]
2532
- );
2533
- const runUpload = React24.useCallback(
2534
- async (file) => {
2535
- const controller = new AbortController();
2536
- abortRef.current = controller;
2537
- lastFileRef.current = file;
2538
- setUploadFailure(null);
2539
- setMedia(null);
2540
- setProcessRequested(false);
2541
- setCurrentFile(file);
2542
- setPercent(0);
2543
- setPhase("uploading");
2544
- const fail = (reason, message) => {
2545
- const failure = { stage: "upload", reason, retriable: true, message };
2546
- setUploadFailure(failure);
2547
- setPhase("upload-failed");
2548
- onError?.(failure);
2549
- return null;
2550
- };
2551
- let result;
2552
- try {
2553
- result = await createUploadUrlAction({
2554
- filename: file.name,
2555
- contentType: file.type,
2556
- visibility
2557
- });
2558
- } catch (err) {
2559
- return fail("rejected", err instanceof Error ? err.message : "Could not start the upload");
2560
- }
2561
- const dimensions = await readImageDimensions(file);
2562
- try {
2563
- await uploadFileWithProgress(result.url, file, { onProgress: setPercent, signal: controller.signal });
2564
- } catch (err) {
2565
- if (err instanceof DOMException && err.name === "AbortError") {
2566
- setPhase("idle");
2567
- setCurrentFile(null);
2568
- setPercent(0);
2569
- return null;
2570
- }
2571
- const rejected = err instanceof UploadHttpError && (err.status === 413 || err.status === 415);
2572
- return fail(rejected ? "rejected" : "network", err instanceof Error ? err.message : "The upload failed");
2573
- }
2574
- if (api.confirmUpload) await confirmUploadBestEffort(confirmUploadAction, result.key, file, dimensions);
2575
- const uploaded = {
2576
- id: result.id,
2577
- kind: result.kind,
2578
- visibility: result.visibility,
2579
- filename: file.name,
2580
- key: result.key,
2581
- ...dimensions
2582
- };
2583
- setMedia(uploaded);
2584
- setPhase("uploaded");
2585
- if (replaces) deleteMediaAction({ key: mediaPath3(replaces) }).catch(() => {
2586
- });
2587
- onComplete?.(uploaded);
2588
- if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
2589
- return uploaded;
2590
- },
2591
- [
2592
- createUploadUrlAction,
2593
- confirmUploadAction,
2594
- api.confirmUpload,
2595
- deleteMediaAction,
2596
- triggerProcess,
2597
- visibility,
2598
- replaces,
2599
- process,
2600
- onComplete,
2601
- onError
2602
- ]
2603
- );
2604
- const cancel = React24.useCallback(() => {
2605
- abortRef.current?.abort();
2606
- }, []);
2607
- const reset = React24.useCallback(() => {
2608
- abortRef.current?.abort();
2609
- setPhase("idle");
2610
- setPercent(0);
2611
- setCurrentFile(null);
2612
- setMedia(null);
2613
- setUploadFailure(null);
2614
- setProcessRequested(false);
2615
- }, []);
2616
- const retry = React24.useCallback(() => {
2617
- if (phase === "upload-failed" && lastFileRef.current) {
2618
- runUpload(lastFileRef.current);
2619
- return;
2620
- }
2621
- if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
2622
- }, [phase, runUpload, triggerProcess]);
2623
- React24.useEffect(() => () => abortRef.current?.abort(), []);
2624
- const state = React24.useMemo(
2625
- () => deriveUploadState(
2626
- { phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
2627
- videoState,
2628
- retry
2629
- ),
2630
- [phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
2631
- );
2632
- const upload = React24.useCallback((file) => runUpload(file), [runUpload]);
2633
- return { upload, state, cancel, reset };
2588
+ return /* @__PURE__ */ jsx18(DevulturProgressContext, { value, children });
2634
2589
  }
2635
2590
  export {
2636
2591
  AttachmentCard,
2637
2592
  CropUpload,
2593
+ DevulturApiContext,
2638
2594
  DevulturCapabilityError,
2595
+ DevulturContext,
2596
+ DevulturProgressContext,
2639
2597
  DevulturProvider,
2640
2598
  DevulturViewerProvider,
2641
2599
  DocumentLink,
@@ -2654,6 +2612,7 @@ export {
2654
2612
  formatDuration,
2655
2613
  formatFileSize,
2656
2614
  matchesAccept,
2615
+ useCanDeliver,
2657
2616
  useDevultur,
2658
2617
  useDevulturApi,
2659
2618
  useDevulturMedia,