@devultur/convex 1.5.0 → 1.5.1

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