@devultur/convex 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -1,16 +1,16 @@
1
1
  "use client";
2
2
 
3
3
  // src/react.tsx
4
- import React2 from "react";
4
+ import React7 from "react";
5
5
  import {
6
6
  buildMediaUrl,
7
7
  CaptionLocaleLabel,
8
8
  DEFAULT_TOKEN_EXPIRY_SECONDS,
9
9
  MediaStatus,
10
10
  mediaCaptionPath,
11
- mediaPath,
12
- mediaUrl,
13
- SESSION_HEADER,
11
+ mediaPath as mediaPath2,
12
+ mediaUrl as mediaUrl2,
13
+ SESSION_HEADER as SESSION_HEADER2,
14
14
  visibilityFromKey
15
15
  } from "@devultur/core";
16
16
  import {
@@ -29,6 +29,149 @@ async function isSessionMismatch(response) {
29
29
  }
30
30
  }
31
31
 
32
+ // src/media-fetch.ts
33
+ import { SESSION_HEADER } from "@devultur/core";
34
+
35
+ // src/preview-type.ts
36
+ var PREVIEW_KIND_BY_TYPE = /* @__PURE__ */ new Map([
37
+ ["image/png", "image"],
38
+ ["image/jpeg", "image"],
39
+ ["image/webp", "image"],
40
+ ["image/gif", "image"],
41
+ ["image/avif", "image"],
42
+ ["application/pdf", "pdf"],
43
+ ["audio/mpeg", "audio"],
44
+ ["audio/mp4", "audio"],
45
+ ["audio/aac", "audio"],
46
+ ["audio/ogg", "audio"],
47
+ ["audio/wav", "audio"],
48
+ ["audio/webm", "audio"],
49
+ ["audio/flac", "audio"],
50
+ ["text/plain", "text"],
51
+ ["text/csv", "text"],
52
+ ["application/json", "text"]
53
+ ]);
54
+ var TYPE_BY_EXTENSION = /* @__PURE__ */ new Map([
55
+ ["png", "image/png"],
56
+ ["jpg", "image/jpeg"],
57
+ ["jpeg", "image/jpeg"],
58
+ ["webp", "image/webp"],
59
+ ["gif", "image/gif"],
60
+ ["avif", "image/avif"],
61
+ ["pdf", "application/pdf"],
62
+ ["mp3", "audio/mpeg"],
63
+ ["m4a", "audio/mp4"],
64
+ ["aac", "audio/aac"],
65
+ ["ogg", "audio/ogg"],
66
+ ["oga", "audio/ogg"],
67
+ ["opus", "audio/ogg"],
68
+ ["wav", "audio/wav"],
69
+ ["weba", "audio/webm"],
70
+ ["flac", "audio/flac"],
71
+ ["txt", "text/plain"],
72
+ ["csv", "text/csv"],
73
+ ["json", "application/json"]
74
+ ]);
75
+ var OPAQUE_CONTENT_TYPE = "application/octet-stream";
76
+ function normalizeContentType(value) {
77
+ const essence = value?.split(";")[0]?.trim().toLowerCase();
78
+ return essence || null;
79
+ }
80
+ function previewKindOf(contentType) {
81
+ const normalized = normalizeContentType(contentType);
82
+ if (!normalized) return null;
83
+ return PREVIEW_KIND_BY_TYPE.get(normalized) ?? null;
84
+ }
85
+ function expectedContentType(media, declaredType) {
86
+ if (declaredType) return normalizeContentType(declaredType);
87
+ const extension = media.filename?.split(".").pop()?.toLowerCase();
88
+ if (!extension || extension === media.filename?.toLowerCase()) return null;
89
+ return TYPE_BY_EXTENSION.get(extension) ?? null;
90
+ }
91
+ function trustedContentType(expectedType, responseType) {
92
+ const receivedKind = previewKindOf(responseType);
93
+ if (!expectedType) {
94
+ const contentType = receivedKind ? normalizeContentType(responseType) : null;
95
+ return { ok: true, contentType: contentType ?? OPAQUE_CONTENT_TYPE };
96
+ }
97
+ const expectedKind = previewKindOf(expectedType);
98
+ if (!expectedKind) return { ok: false, reason: "unsupported_type" };
99
+ if (receivedKind !== expectedKind) return { ok: false, reason: "type_mismatch" };
100
+ return { ok: true, contentType: normalizeContentType(expectedType) ?? OPAQUE_CONTENT_TYPE };
101
+ }
102
+
103
+ // src/media-fetch.ts
104
+ var MediaFetchError = class extends Error {
105
+ code;
106
+ /** HTTP status, for `http_error`. */
107
+ status;
108
+ constructor(code, message, status) {
109
+ super(message);
110
+ this.name = "MediaFetchError";
111
+ this.code = code;
112
+ this.status = status;
113
+ }
114
+ };
115
+ async function fetchMedia(request) {
116
+ const { buildUrl, token, sessionSecret, reissueToken, signal, isPrivate = true, canReissue = true } = request;
117
+ const headers = isPrivate && sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0;
118
+ const response = await fetch(buildUrl(isPrivate ? token ?? void 0 : void 0), { headers, signal });
119
+ if (!isPrivate || !canReissue || response.status !== 401) return response;
120
+ if (!await isSessionMismatch(response)) return response;
121
+ request.onReissue?.();
122
+ const freshToken = await reissueToken();
123
+ if (!freshToken) return response;
124
+ return fetch(buildUrl(freshToken), { headers, signal });
125
+ }
126
+ async function loadMedia(request) {
127
+ const response = await fetchMedia(request);
128
+ if (!response.ok) {
129
+ throw new MediaFetchError("http_error", `Media fetch failed: ${response.status}`, response.status);
130
+ }
131
+ const trusted = trustedContentType(request.contentType, response.headers.get("content-type"));
132
+ if (!trusted.ok) {
133
+ await response.body?.cancel();
134
+ throw new MediaFetchError(trusted.reason, `Refusing to preview: ${trusted.reason}`);
135
+ }
136
+ const bytes = await readMediaBody(response, request.maxBytes);
137
+ return { bytes, contentType: trusted.contentType };
138
+ }
139
+ function tooLarge(maxBytes) {
140
+ return new MediaFetchError("too_large", `Media is larger than ${maxBytes} bytes`);
141
+ }
142
+ async function readMediaBody(response, maxBytes) {
143
+ if (maxBytes === void 0) return new Uint8Array(await response.arrayBuffer());
144
+ const declaredBytes = Number(response.headers.get("content-length") ?? Number.NaN);
145
+ if (declaredBytes > maxBytes) {
146
+ await response.body?.cancel();
147
+ throw tooLarge(maxBytes);
148
+ }
149
+ if (!response.body) return new Uint8Array(0);
150
+ return readCapped(response.body, maxBytes);
151
+ }
152
+ async function readCapped(body, maxBytes) {
153
+ const reader = body.getReader();
154
+ const chunks = [];
155
+ let byteCount = 0;
156
+ for (; ; ) {
157
+ const { done, value } = await reader.read();
158
+ if (done) break;
159
+ byteCount += value.byteLength;
160
+ if (byteCount > maxBytes) {
161
+ await reader.cancel();
162
+ throw tooLarge(maxBytes);
163
+ }
164
+ chunks.push(value);
165
+ }
166
+ const bytes = new Uint8Array(byteCount);
167
+ let offset = 0;
168
+ for (const chunk of chunks) {
169
+ bytes.set(chunk, offset);
170
+ offset += chunk.byteLength;
171
+ }
172
+ return bytes;
173
+ }
174
+
32
175
  // src/media-upload-derive.ts
33
176
  function deriveUploadState(local, videoState, retry) {
34
177
  const { phase, percent, file, failure, media, processRequested } = local;
@@ -159,11 +302,461 @@ function CropUpload({
159
302
  ] });
160
303
  }
161
304
 
305
+ // src/media-resource.ts
306
+ import React2 from "react";
307
+ import { mediaPath, mediaUrl } from "@devultur/core";
308
+ var IDLE = { value: null, contentType: null, isLoading: false, error: null };
309
+ var LOADING = { value: null, contentType: null, isLoading: true, error: null };
310
+ function useMediaResource(media, options) {
311
+ const devultur = useDevultur();
312
+ const { isEnabled, maxBytes, contentType } = options;
313
+ const isPrivate = media?.visibility === "private";
314
+ const isReady = !isPrivate || devultur.token !== null;
315
+ const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility, mediaPath(media), contentType ?? "", maxBytes ?? ""].join("|") : null;
316
+ const [settled, setSettled] = React2.useState(null);
317
+ const inputsRef = React2.useRef({ media, devultur, options });
318
+ React2.useEffect(() => {
319
+ inputsRef.current = { media, devultur, options };
320
+ });
321
+ React2.useEffect(() => {
322
+ if (!requestKey || !isReady) return;
323
+ const { media: target, devultur: context, options: current } = inputsRef.current;
324
+ if (!target || !context.baseUrl) return;
325
+ const baseUrl = context.baseUrl;
326
+ const controller = new AbortController();
327
+ let value = null;
328
+ loadMedia({
329
+ buildUrl: (viewerToken) => mediaUrl(baseUrl, target, { token: viewerToken }),
330
+ token: context.token,
331
+ sessionSecret: context.sessionSecret,
332
+ reissueToken: context.reissueToken,
333
+ isPrivate: target.visibility === "private",
334
+ signal: controller.signal,
335
+ maxBytes: current.maxBytes,
336
+ contentType: current.contentType
337
+ }).then((loaded) => {
338
+ if (controller.signal.aborted) return;
339
+ value = current.decode(loaded.bytes, loaded.contentType);
340
+ setSettled({ requestKey, value, contentType: loaded.contentType, isLoading: false, error: null });
341
+ }).catch((error) => {
342
+ if (controller.signal.aborted) return;
343
+ const failure = error instanceof Error ? error : new Error(String(error));
344
+ setSettled({ requestKey, value: null, contentType: null, isLoading: false, error: failure });
345
+ });
346
+ return () => {
347
+ controller.abort();
348
+ if (value !== null) current.release?.(value);
349
+ };
350
+ }, [requestKey, isReady]);
351
+ if (!requestKey) return IDLE;
352
+ if (!devultur.baseUrl) throw new Error("Devultur media previews require `baseUrl` on <DevulturProvider>");
353
+ if (settled?.requestKey !== requestKey) return LOADING;
354
+ return { value: settled.value, contentType: settled.contentType, isLoading: false, error: settled.error };
355
+ }
356
+
357
+ // src/media-object-url.ts
358
+ var createObjectUrl = (bytes, contentType) => URL.createObjectURL(new Blob([bytes], { type: contentType }));
359
+ var revokeObjectUrl = (objectUrl) => URL.revokeObjectURL(objectUrl);
360
+ function useMediaObjectUrl(media, options) {
361
+ const { url } = useDevultur();
362
+ const isEnabled = (options?.enabled ?? true) && Boolean(media);
363
+ const isUnverifiedPublic = media?.visibility === "public" && !options?.contentType;
364
+ const resource = useMediaResource(media, {
365
+ isEnabled: isEnabled && !isUnverifiedPublic,
366
+ maxBytes: options?.maxBytes,
367
+ contentType: options?.contentType,
368
+ decode: createObjectUrl,
369
+ release: revokeObjectUrl
370
+ });
371
+ if (media && isEnabled && isUnverifiedPublic) {
372
+ return { objectUrl: url(media), contentType: null, isLoading: false, error: null };
373
+ }
374
+ return {
375
+ objectUrl: resource.value,
376
+ contentType: resource.contentType,
377
+ isLoading: resource.isLoading,
378
+ error: resource.error
379
+ };
380
+ }
381
+
382
+ // src/pdf-context.tsx
383
+ import React3 from "react";
384
+ import { jsx as jsx2 } from "react/jsx-runtime";
385
+ var PdfPreviewContext = React3.createContext({});
386
+ function PdfPreviewProvider({ pdfjs, workerSrc, children }) {
387
+ const config = React3.useMemo(() => ({ pdfjs, workerSrc }), [pdfjs, workerSrc]);
388
+ return /* @__PURE__ */ jsx2(PdfPreviewContext.Provider, { value: config, children });
389
+ }
390
+ var loadedModules = /* @__PURE__ */ new WeakMap();
391
+ function loadOnce(loader) {
392
+ const cached = loadedModules.get(loader);
393
+ if (cached) return cached;
394
+ const loading = loader().catch(() => null);
395
+ loadedModules.set(loader, loading);
396
+ return loading;
397
+ }
398
+ function usePdfjs(workerSrcOverride) {
399
+ const { pdfjs: loader, workerSrc } = React3.useContext(PdfPreviewContext);
400
+ const [loaded, setLoaded] = React3.useState(null);
401
+ React3.useEffect(() => {
402
+ if (!loader) return;
403
+ let isCurrent = true;
404
+ loadOnce(loader).then((pdfjs) => {
405
+ if (isCurrent) setLoaded({ loader, pdfjs });
406
+ });
407
+ return () => {
408
+ isCurrent = false;
409
+ };
410
+ }, [loader]);
411
+ if (!loader) return { status: "unavailable" };
412
+ if (loaded?.loader !== loader) return { status: "loading" };
413
+ if (!loaded.pdfjs) return { status: "unavailable" };
414
+ return { status: "ready", pdfjs: loaded.pdfjs, workerSrc: workerSrcOverride ?? workerSrc };
415
+ }
416
+
417
+ // src/pdf-thumbnail.tsx
418
+ import React6 from "react";
419
+
420
+ // src/pdf-hooks.ts
421
+ import React4 from "react";
422
+
423
+ // src/pdf-render.ts
424
+ var MOBILE_MAX_CANVAS_PIXELS = 5242880;
425
+ var DESKTOP_MAX_CANVAS_PIXELS = 2 ** 25;
426
+ function openPdf(pdfjs, bytes, workerSrc) {
427
+ if (workerSrc) pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
428
+ const source = { data: bytes.slice(), enableScripting: false, isEvalSupported: false };
429
+ return pdfjs.getDocument(source);
430
+ }
431
+ function isMobileDevice() {
432
+ if (typeof navigator === "undefined") return false;
433
+ return /Android|iPhone|iPad|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1;
434
+ }
435
+ function maxCanvasPixels() {
436
+ return isMobileDevice() ? MOBILE_MAX_CANVAS_PIXELS : DESKTOP_MAX_CANVAS_PIXELS;
437
+ }
438
+ function outputScale(cssWidth, cssHeight, maxPixels) {
439
+ const devicePixelRatio = typeof window !== "undefined" && window.devicePixelRatio || 1;
440
+ const cssPixels = cssWidth * cssHeight;
441
+ if (cssPixels <= 0) return devicePixelRatio;
442
+ if (cssPixels * devicePixelRatio ** 2 <= maxPixels) return devicePixelRatio;
443
+ return Math.sqrt(maxPixels / cssPixels);
444
+ }
445
+ function renderPage(page, canvas, cssWidth) {
446
+ const naturalViewport = page.getViewport({ scale: 1 });
447
+ const cssScale = cssWidth > 0 ? cssWidth / naturalViewport.width : 1;
448
+ const cssViewport = page.getViewport({ scale: cssScale });
449
+ const scale = outputScale(cssViewport.width, cssViewport.height, maxCanvasPixels());
450
+ const viewport = page.getViewport({ scale: cssScale * scale });
451
+ canvas.width = Math.floor(viewport.width);
452
+ canvas.height = Math.floor(viewport.height);
453
+ canvas.style.width = `${Math.floor(cssViewport.width)}px`;
454
+ canvas.style.height = `${Math.floor(cssViewport.height)}px`;
455
+ return page.render({ canvas, viewport });
456
+ }
457
+ async function pageText(page) {
458
+ const { items } = await page.getTextContent();
459
+ return items.map((item) => "str" in item && typeof item.str === "string" ? item.str : "").join(" ").replace(/\s+/g, " ").trim();
460
+ }
461
+ var isRenderCancelled = (error) => error instanceof Error && error.name === "RenderingCancelledException";
462
+
463
+ // src/pdf-hooks.ts
464
+ function useIsVisible(ref, rootMargin = "200px") {
465
+ const [isVisible, setIsVisible] = React4.useState(false);
466
+ React4.useEffect(() => {
467
+ const element = ref.current;
468
+ if (isVisible || !element) return;
469
+ if (typeof IntersectionObserver === "undefined") {
470
+ setIsVisible(true);
471
+ return;
472
+ }
473
+ const observer = new IntersectionObserver(
474
+ (entries) => {
475
+ if (!entries.some((entry) => entry.isIntersecting)) return;
476
+ setIsVisible(true);
477
+ observer.disconnect();
478
+ },
479
+ { rootMargin }
480
+ );
481
+ observer.observe(element);
482
+ return () => observer.disconnect();
483
+ }, [ref, rootMargin, isVisible]);
484
+ return isVisible;
485
+ }
486
+ function usePdfDocument({ pdfjs, bytes, workerSrc }) {
487
+ const [state, setState] = React4.useState({
488
+ bytes: null,
489
+ document: null,
490
+ error: null
491
+ });
492
+ React4.useEffect(() => {
493
+ let isCurrent = true;
494
+ const loadingTask = openPdf(pdfjs, bytes, workerSrc);
495
+ loadingTask.promise.then((document2) => {
496
+ if (isCurrent) setState({ bytes, document: document2, error: null });
497
+ }).catch((error) => {
498
+ if (!isCurrent) return;
499
+ setState({ bytes, document: null, error: error instanceof Error ? error : new Error(String(error)) });
500
+ });
501
+ return () => {
502
+ isCurrent = false;
503
+ void loadingTask.destroy();
504
+ };
505
+ }, [pdfjs, bytes, workerSrc]);
506
+ if (state.bytes !== bytes) return { document: null, error: null };
507
+ return { document: state.document, error: state.error };
508
+ }
509
+ function usePageRender(options) {
510
+ const { document: document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText = false } = options;
511
+ const [state, setState] = React4.useState({ isRendered: false, text: "" });
512
+ React4.useEffect(() => {
513
+ const canvas = canvasRef.current;
514
+ if (!document2 || !isEnabled || !canvas) return;
515
+ let isCurrent = true;
516
+ let page = null;
517
+ let renderTask = null;
518
+ const draw = async () => {
519
+ page = await document2.getPage(pageNumber);
520
+ if (!isCurrent) return;
521
+ renderTask = renderPage(page, canvas, width);
522
+ await renderTask.promise;
523
+ if (isCurrent) setState((previous) => ({ ...previous, isRendered: true }));
524
+ if (!shouldExtractText) return;
525
+ const text = await pageText(page);
526
+ if (isCurrent) setState((previous) => ({ ...previous, text }));
527
+ };
528
+ draw().catch((error) => {
529
+ if (isRenderCancelled(error) || !isCurrent) return;
530
+ console.warn(`[devultur] PDF page ${pageNumber} failed to render`, error);
531
+ });
532
+ return () => {
533
+ isCurrent = false;
534
+ renderTask?.cancel();
535
+ page?.cleanup();
536
+ };
537
+ }, [document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText]);
538
+ return state;
539
+ }
540
+
541
+ // src/pdf-preview.tsx
542
+ import React5 from "react";
543
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
544
+ var VISUALLY_HIDDEN = {
545
+ position: "absolute",
546
+ width: 1,
547
+ height: 1,
548
+ margin: -1,
549
+ padding: 0,
550
+ overflow: "hidden",
551
+ clip: "rect(0 0 0 0)",
552
+ whiteSpace: "nowrap",
553
+ border: 0
554
+ };
555
+ var DEFAULT_MAX_PAGES = 500;
556
+ var keepBytes = (bytes) => bytes;
557
+ function PdfPreview(props) {
558
+ const { media, expectedType, maxBytes, fallback, loading, title, className, pdfWorkerSrc, maxPages } = props;
559
+ const pdfjsState = usePdfjs(pdfWorkerSrc);
560
+ const { value: bytes, error } = useMediaResource(media, {
561
+ isEnabled: true,
562
+ maxBytes,
563
+ contentType: expectedType,
564
+ decode: keepBytes
565
+ });
566
+ if (error) return /* @__PURE__ */ jsx3(Fragment, { children: fallback });
567
+ if (!bytes || pdfjsState.status === "loading") return /* @__PURE__ */ jsx3(Fragment, { children: loading });
568
+ const label = title ?? media.filename ?? "PDF";
569
+ if (pdfjsState.status === "unavailable") return /* @__PURE__ */ jsx3(PdfFrame, { bytes, label, className });
570
+ return /* @__PURE__ */ jsx3(
571
+ PdfCanvasDocument,
572
+ {
573
+ pdfjs: pdfjsState.pdfjs,
574
+ workerSrc: pdfjsState.workerSrc,
575
+ bytes,
576
+ label,
577
+ className,
578
+ fallback,
579
+ loading,
580
+ maxPages: maxPages ?? DEFAULT_MAX_PAGES
581
+ }
582
+ );
583
+ }
584
+ function PdfCanvasDocument(props) {
585
+ const { pdfjs, workerSrc, bytes, label, className, fallback, loading, maxPages } = props;
586
+ const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
587
+ const containerRef = React5.useRef(null);
588
+ const [width, setWidth] = React5.useState(null);
589
+ React5.useLayoutEffect(() => {
590
+ if (document2) setWidth(containerRef.current?.clientWidth ?? 0);
591
+ }, [document2]);
592
+ if (error) return /* @__PURE__ */ jsx3(Fragment, { children: fallback });
593
+ if (!document2) return /* @__PURE__ */ jsx3(Fragment, { children: loading });
594
+ const shownPageCount = Math.min(document2.numPages, Math.max(1, maxPages));
595
+ const isTruncated = shownPageCount < document2.numPages;
596
+ const pageNumbers = Array.from({ length: shownPageCount }, (_, index) => index + 1);
597
+ const pageCountLabel = document2.numPages === 1 ? "1 page" : `${document2.numPages} pages`;
598
+ return /* @__PURE__ */ jsxs2("div", { ref: containerRef, role: "document", "aria-label": `${label}, ${pageCountLabel}`, className, children: [
599
+ width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */ jsx3(PdfPageView, { document: document2, pageNumber, width }, pageNumber)),
600
+ isTruncated && /* @__PURE__ */ jsxs2("p", { children: [
601
+ "Showing the first ",
602
+ shownPageCount,
603
+ " of ",
604
+ document2.numPages,
605
+ " pages. ",
606
+ fallback
607
+ ] })
608
+ ] });
609
+ }
610
+ function PdfPageView({ document: document2, pageNumber, width }) {
611
+ const pageRef = React5.useRef(null);
612
+ const canvasRef = React5.useRef(null);
613
+ const isVisible = useIsVisible(pageRef);
614
+ const { isRendered, text } = usePageRender({
615
+ document: document2,
616
+ pageNumber,
617
+ canvasRef,
618
+ width,
619
+ isEnabled: isVisible,
620
+ shouldExtractText: true
621
+ });
622
+ const placeholderStyle = isRendered ? void 0 : { aspectRatio: "1 / 1.4142", maxWidth: "100%" };
623
+ return /* @__PURE__ */ jsxs2("section", { ref: pageRef, "aria-label": `Page ${pageNumber} of ${document2.numPages}`, style: placeholderStyle, children: [
624
+ /* @__PURE__ */ jsx3("canvas", { ref: canvasRef, style: { display: "block", maxWidth: "100%" } }),
625
+ text && /* @__PURE__ */ jsx3("p", { style: VISUALLY_HIDDEN, children: text })
626
+ ] });
627
+ }
628
+ function PdfFrame({ bytes, label, className }) {
629
+ const [objectUrl, setObjectUrl] = React5.useState(null);
630
+ React5.useEffect(() => {
631
+ const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
632
+ setObjectUrl(url);
633
+ return () => URL.revokeObjectURL(url);
634
+ }, [bytes]);
635
+ if (!objectUrl) return null;
636
+ return /* @__PURE__ */ jsx3("iframe", { src: objectUrl, title: label, referrerPolicy: "no-referrer", className });
637
+ }
638
+
639
+ // src/pdf-thumbnail.tsx
640
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
641
+ var DEFAULT_THUMBNAIL_WIDTH = 96;
642
+ var DEFAULT_THUMBNAIL_MAX_BYTES = 50 * 1024 * 1024;
643
+ function PdfThumbnail(props) {
644
+ const { media, contentType, width = DEFAULT_THUMBNAIL_WIDTH, fallback = null, loading = null } = props;
645
+ const containerRef = React6.useRef(null);
646
+ const isVisible = useIsVisible(containerRef);
647
+ const pdfjsState = usePdfjs(props.pdfWorkerSrc);
648
+ const expectedType = media ? expectedContentType(media, contentType) : null;
649
+ const isPdf = previewKindOf(expectedType) === "pdf";
650
+ const { value: bytes, error } = useMediaResource(media, {
651
+ isEnabled: isVisible && isPdf && pdfjsState.status === "ready",
652
+ maxBytes: props.maxBytes ?? DEFAULT_THUMBNAIL_MAX_BYTES,
653
+ contentType: expectedType ?? void 0,
654
+ decode: keepBytes
655
+ });
656
+ if (!media) return null;
657
+ const isUnavailable = !isPdf || pdfjsState.status === "unavailable" || error !== null;
658
+ const content = () => {
659
+ if (isUnavailable) return fallback;
660
+ if (!bytes || pdfjsState.status !== "ready") return loading;
661
+ return /* @__PURE__ */ jsx4(
662
+ ThumbnailCanvas,
663
+ {
664
+ pdfjs: pdfjsState.pdfjs,
665
+ workerSrc: pdfjsState.workerSrc,
666
+ bytes,
667
+ width,
668
+ label: props.title ?? media.filename ?? "PDF",
669
+ fallback,
670
+ loading
671
+ }
672
+ );
673
+ };
674
+ return /* @__PURE__ */ jsx4("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
675
+ }
676
+ function ThumbnailCanvas({ pdfjs, workerSrc, bytes, width, label, fallback, loading }) {
677
+ const canvasRef = React6.useRef(null);
678
+ const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
679
+ const { isRendered } = usePageRender({ document: document2, pageNumber: 1, canvasRef, width, isEnabled: true });
680
+ if (error) return /* @__PURE__ */ jsx4(Fragment2, { children: fallback });
681
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
682
+ !isRendered && loading,
683
+ /* @__PURE__ */ jsx4("canvas", { ref: canvasRef, role: "img", "aria-label": label, style: { display: "block" } })
684
+ ] });
685
+ }
686
+
687
+ // src/preview.tsx
688
+ import { VideoPlayer } from "@devultur/react";
689
+ import { Fragment as Fragment3, jsx as jsx5 } from "react/jsx-runtime";
690
+ var DEFAULT_MAX_TEXT_BYTES = 1024 * 1024;
691
+ var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
692
+ function Preview({ media, contentType, fallback, ...viewProps }) {
693
+ if (!media) return null;
694
+ const fallbackNode = fallback === void 0 ? /* @__PURE__ */ jsx5(DownloadLink, { media, children: media.filename ?? "Download" }) : fallback;
695
+ if (media.kind === "video") return /* @__PURE__ */ jsx5(VideoPreview, { media, className: viewProps.className });
696
+ const expectedType = expectedContentType(media, contentType);
697
+ const kind = previewKindOf(expectedType);
698
+ if (!expectedType || !kind) return /* @__PURE__ */ jsx5(Fragment3, { children: fallbackNode });
699
+ const props = { ...viewProps, media, expectedType, fallback: fallbackNode };
700
+ if (kind === "text") return /* @__PURE__ */ jsx5(TextPreview, { ...props });
701
+ if (kind === "pdf") {
702
+ const { maxBytes = DEFAULT_MAX_BYTES, loading = null } = props;
703
+ return /* @__PURE__ */ jsx5(PdfPreview, { ...props, maxBytes, loading });
704
+ }
705
+ return /* @__PURE__ */ jsx5(BinaryPreview, { ...props, kind });
706
+ }
707
+ function VideoPreview({ media, className }) {
708
+ const videoProps = useVideoProps(media);
709
+ if (!videoProps) return null;
710
+ return /* @__PURE__ */ jsx5(VideoPlayer, { ...videoProps, className });
711
+ }
712
+ function BinaryPreview({
713
+ media,
714
+ kind,
715
+ expectedType,
716
+ maxBytes = DEFAULT_MAX_BYTES,
717
+ fallback,
718
+ loading = null,
719
+ title,
720
+ className
721
+ }) {
722
+ const { url } = useDevultur();
723
+ const isPublicImage = kind === "image" && media.visibility === "public";
724
+ const verified = useMediaObjectUrl(media, { enabled: !isPublicImage, contentType: expectedType, maxBytes });
725
+ const objectUrl = isPublicImage ? url(media) : verified.objectUrl;
726
+ if (verified.error) return /* @__PURE__ */ jsx5(Fragment3, { children: fallback });
727
+ if (!objectUrl) return /* @__PURE__ */ jsx5(Fragment3, { children: loading });
728
+ const label = title ?? media.filename ?? "Preview";
729
+ if (kind === "image") {
730
+ return /* @__PURE__ */ jsx5("img", { src: objectUrl, alt: label, width: media.width, height: media.height, className });
731
+ }
732
+ return /* @__PURE__ */ jsx5("audio", { src: objectUrl, controls: true, "aria-label": label, className });
733
+ }
734
+ var decodeText = (bytes) => new TextDecoder().decode(bytes);
735
+ function TextPreview({
736
+ media,
737
+ expectedType,
738
+ maxTextBytes = DEFAULT_MAX_TEXT_BYTES,
739
+ fallback,
740
+ loading = null,
741
+ title,
742
+ className
743
+ }) {
744
+ const { value: text, error } = useMediaResource(media, {
745
+ isEnabled: true,
746
+ maxBytes: maxTextBytes,
747
+ contentType: expectedType,
748
+ decode: decodeText
749
+ });
750
+ if (error) return /* @__PURE__ */ jsx5(Fragment3, { children: fallback });
751
+ if (text === null) return /* @__PURE__ */ jsx5(Fragment3, { children: loading });
752
+ return /* @__PURE__ */ jsx5("pre", { title, className, children: text });
753
+ }
754
+
162
755
  // src/react.tsx
163
- import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
164
- var DevulturContext = React2.createContext(null);
165
- var DevulturApiContext = React2.createContext(null);
166
- var ProgressContext = React2.createContext(null);
756
+ import { Fragment as Fragment4, jsx as jsx6 } from "react/jsx-runtime";
757
+ var DevulturContext = React7.createContext(null);
758
+ var DevulturApiContext = React7.createContext(null);
759
+ var ProgressContext = React7.createContext(null);
167
760
  var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
168
761
  var MIN_REFRESH_MS = 6e4;
169
762
  var DEFAULT_TTL_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
@@ -173,16 +766,16 @@ function extractTokenResult(result) {
173
766
  return { tokenValue, ttlMs };
174
767
  }
175
768
  function DevulturProvider({ api, baseUrl, children }) {
176
- const [token, setToken] = React2.useState(null);
177
- const [sessionSecret] = React2.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
769
+ const [token, setToken] = React7.useState(null);
770
+ const [sessionSecret] = React7.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
178
771
  const issueViewerToken = useAction(api.issueViewerToken);
179
772
  const createUploadUrlAction = useAction(api.createUploadUrl);
180
773
  const deleteMediaAction = useAction(api.deleteMedia);
181
774
  const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
182
775
  const deleteVideoAction = useAction(api.deleteVideo ?? api.deleteMedia);
183
- const refreshTimerRef = React2.useRef(null);
184
- const reissueInFlightRef = React2.useRef(null);
185
- const scheduleRefresh = React2.useCallback(
776
+ const refreshTimerRef = React7.useRef(null);
777
+ const reissueInFlightRef = React7.useRef(null);
778
+ const scheduleRefresh = React7.useCallback(
186
779
  (ttlMs) => {
187
780
  if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
188
781
  const refreshIn = Math.max(ttlMs - REFRESH_BUFFER_MS, MIN_REFRESH_MS);
@@ -196,7 +789,7 @@ function DevulturProvider({ api, baseUrl, children }) {
196
789
  },
197
790
  [issueViewerToken, sessionSecret]
198
791
  );
199
- React2.useEffect(() => {
792
+ React7.useEffect(() => {
200
793
  if (token) return;
201
794
  issueViewerToken({ sessionSecret }).then((result) => {
202
795
  const { tokenValue, ttlMs } = extractTokenResult(result);
@@ -204,7 +797,7 @@ function DevulturProvider({ api, baseUrl, children }) {
204
797
  scheduleRefresh(ttlMs);
205
798
  });
206
799
  }, [token, issueViewerToken, scheduleRefresh, sessionSecret]);
207
- const reissueToken = React2.useCallback(() => {
800
+ const reissueToken = React7.useCallback(() => {
208
801
  if (reissueInFlightRef.current) return reissueInFlightRef.current;
209
802
  const pending = issueViewerToken({ sessionSecret }).then((result) => {
210
803
  const { tokenValue, ttlMs } = extractTokenResult(result);
@@ -219,19 +812,19 @@ function DevulturProvider({ api, baseUrl, children }) {
219
812
  reissueInFlightRef.current = pending;
220
813
  return pending;
221
814
  }, [issueViewerToken, scheduleRefresh, sessionSecret]);
222
- React2.useEffect(() => {
815
+ React7.useEffect(() => {
223
816
  return () => {
224
817
  if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
225
818
  };
226
819
  }, []);
227
- const uploadUrl = React2.useCallback(
820
+ const uploadUrl = React7.useCallback(
228
821
  async (file) => {
229
822
  const result = await createUploadUrlAction({ filename: file.name, contentType: file.type });
230
823
  return { url: result.url, key: result.key };
231
824
  },
232
825
  [createUploadUrlAction]
233
826
  );
234
- const upload = React2.useCallback(
827
+ const upload = React7.useCallback(
235
828
  async (file, options) => {
236
829
  const dimensions = await readImageDimensions(file);
237
830
  const target = await createUploadUrlAction({
@@ -252,21 +845,21 @@ function DevulturProvider({ api, baseUrl, children }) {
252
845
  },
253
846
  [createUploadUrlAction, confirmUploadAction, api.confirmUpload]
254
847
  );
255
- const url = React2.useCallback(
848
+ const url = React7.useCallback(
256
849
  (media, options) => {
257
850
  if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
258
- return mediaUrl(baseUrl, media, { ...options, token: token ?? void 0 });
851
+ return mediaUrl2(baseUrl, media, { ...options, token: token ?? void 0 });
259
852
  },
260
853
  [baseUrl, token]
261
854
  );
262
- const getCaptionsText = React2.useCallback(
855
+ const getCaptionsText = React7.useCallback(
263
856
  async (media, locale) => {
264
857
  if (!baseUrl) throw new Error("Devultur `getCaptionsText()` requires `baseUrl` on <DevulturProvider>");
265
858
  const path = mediaCaptionPath(media, locale);
266
859
  if (!path) throw new Error("getCaptionsText: the reference is not a video");
267
860
  const isPrivate = media.visibility === "private";
268
861
  const fetchWith = (viewerToken) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken }), {
269
- headers: sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0
862
+ headers: sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0
270
863
  });
271
864
  let response = await fetchWith(isPrivate ? token ?? void 0 : void 0);
272
865
  if (isPrivate && response.status === 401 && await isSessionMismatch(response)) {
@@ -278,17 +871,17 @@ function DevulturProvider({ api, baseUrl, children }) {
278
871
  },
279
872
  [baseUrl, token, reissueToken, sessionSecret]
280
873
  );
281
- const deleteMedia = React2.useCallback(
874
+ const deleteMedia = React7.useCallback(
282
875
  (media) => {
283
876
  if (media.kind === "video" && api.deleteVideo) {
284
877
  deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
285
878
  return;
286
879
  }
287
- deleteMediaAction({ key: mediaPath(media) }).catch(console.error);
880
+ deleteMediaAction({ key: mediaPath2(media) }).catch(console.error);
288
881
  },
289
882
  [deleteMediaAction, deleteVideoAction, api.deleteVideo]
290
883
  );
291
- const devulturValue = React2.useMemo(
884
+ const devulturValue = React7.useMemo(
292
885
  () => ({
293
886
  token,
294
887
  baseUrl: baseUrl ?? null,
@@ -302,9 +895,9 @@ function DevulturProvider({ api, baseUrl, children }) {
302
895
  }),
303
896
  [token, baseUrl, sessionSecret, uploadUrl, upload, url, getCaptionsText, deleteMedia, reissueToken]
304
897
  );
305
- let content = /* @__PURE__ */ jsx2(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx2(DevulturContext, { value: devulturValue, children }) });
898
+ let content = /* @__PURE__ */ jsx6(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx6(DevulturContext, { value: devulturValue, children }) });
306
899
  if (api.saveProgress && api.loadProgress && api.markComplete && api.resetProgress) {
307
- content = /* @__PURE__ */ jsx2(
900
+ content = /* @__PURE__ */ jsx6(
308
901
  ProgressBridge,
309
902
  {
310
903
  save: api.saveProgress,
@@ -328,13 +921,13 @@ function ProgressBridge({
328
921
  const saveMutation = useMutation(save);
329
922
  const markCompleteMutation = useMutation(markComplete);
330
923
  const resetMutation = useMutation(reset);
331
- const onSave = React2.useCallback(
924
+ const onSave = React7.useCallback(
332
925
  async (videoId, position) => {
333
926
  await saveMutation({ videoId, position });
334
927
  },
335
928
  [saveMutation]
336
929
  );
337
- const onLoad = React2.useCallback(
930
+ const onLoad = React7.useCallback(
338
931
  async (videoId) => {
339
932
  const result = await convex.query(load, { videoId });
340
933
  if (!result) return null;
@@ -345,37 +938,37 @@ function ProgressBridge({
345
938
  },
346
939
  [convex, load]
347
940
  );
348
- const onMarkComplete = React2.useCallback(
941
+ const onMarkComplete = React7.useCallback(
349
942
  async (videoId) => {
350
943
  await markCompleteMutation({ videoId });
351
944
  },
352
945
  [markCompleteMutation]
353
946
  );
354
- const onReset = React2.useCallback(
947
+ const onReset = React7.useCallback(
355
948
  async (videoId) => {
356
949
  await resetMutation({ videoId });
357
950
  },
358
951
  [resetMutation]
359
952
  );
360
- const value = React2.useMemo(
953
+ const value = React7.useMemo(
361
954
  () => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
362
955
  [onSave, onLoad, onMarkComplete, onReset]
363
956
  );
364
- return /* @__PURE__ */ jsx2(ProgressContext, { value, children });
957
+ return /* @__PURE__ */ jsx6(ProgressContext, { value, children });
365
958
  }
366
959
  function useDevultur() {
367
- const context = React2.use(DevulturContext);
960
+ const context = React7.use(DevulturContext);
368
961
  if (!context) throw new Error("useDevultur must be used within <DevulturProvider>");
369
962
  return context;
370
963
  }
371
964
  function useDevulturProgress() {
372
- const context = React2.use(ProgressContext);
965
+ const context = React7.use(ProgressContext);
373
966
  if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
374
967
  return context;
375
968
  }
376
969
  function useMediaUrl(key, options) {
377
970
  const { token, baseUrl } = useDevultur();
378
- return React2.useMemo(() => {
971
+ return React7.useMemo(() => {
379
972
  if (!key) return null;
380
973
  if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
381
974
  const isPublic = visibilityFromKey(key) === "public";
@@ -403,7 +996,7 @@ async function readImageDimensions(file) {
403
996
  function useVideoProps(media, options) {
404
997
  const { token, baseUrl, sessionSecret } = useDevultur();
405
998
  const locales = options?.locales;
406
- return React2.useMemo(() => {
999
+ return React7.useMemo(() => {
407
1000
  if (media?.kind !== "video") return null;
408
1001
  if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
409
1002
  const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
@@ -414,7 +1007,7 @@ function useVideoProps(media, options) {
414
1007
  return [{ locale, label, src: buildMediaUrl(baseUrl, path, { token: viewerToken }) }];
415
1008
  });
416
1009
  return {
417
- src: buildMediaUrl(baseUrl, mediaPath(media), { token: viewerToken }),
1010
+ src: buildMediaUrl(baseUrl, mediaPath2(media), { token: viewerToken }),
418
1011
  token: viewerToken,
419
1012
  captions,
420
1013
  sessionSecret: media.visibility === "private" ? sessionSecret : void 0
@@ -434,25 +1027,32 @@ function Image({
434
1027
  alt,
435
1028
  ...imgProps
436
1029
  }) {
437
- const { url, reissueToken, sessionSecret } = useDevultur();
438
- const [failed, setFailed] = React2.useState(false);
439
- const [blobSrc, setBlobSrc] = React2.useState(null);
440
- const retriedRef = React2.useRef(false);
1030
+ const { url, token, baseUrl, reissueToken, sessionSecret } = useDevultur();
1031
+ const [failed, setFailed] = React7.useState(false);
1032
+ const [blobSrc, setBlobSrc] = React7.useState(null);
1033
+ const sessionRef = React7.useRef({ token, reissueToken });
1034
+ React7.useEffect(() => {
1035
+ sessionRef.current = { token, reissueToken };
1036
+ });
441
1037
  const isPrivate = media?.visibility === "private";
442
- React2.useEffect(() => {
443
- retriedRef.current = false;
1038
+ const hasToken = token !== null;
1039
+ React7.useEffect(() => {
444
1040
  setFailed(false);
445
1041
  setBlobSrc(null);
446
1042
  }, [media]);
447
- React2.useEffect(() => {
448
- if (!isPrivate || failed || !media) return;
1043
+ React7.useEffect(() => {
1044
+ if (!isPrivate || failed || !media || !hasToken) return;
449
1045
  let cancelled = false;
450
1046
  let objectUrl = null;
451
- const targetUrl = url(media, { width, height, fit, format, quality });
1047
+ if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
1048
+ const transforms = { width, height, fit, format, quality };
452
1049
  (async () => {
453
1050
  try {
454
- const response = await fetch(targetUrl, {
455
- headers: sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0
1051
+ const response = await fetchMedia({
1052
+ buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { ...transforms, token: viewerToken }),
1053
+ token: sessionRef.current.token,
1054
+ sessionSecret,
1055
+ reissueToken: sessionRef.current.reissueToken
456
1056
  });
457
1057
  if (response.ok) {
458
1058
  objectUrl = URL.createObjectURL(await response.blob());
@@ -463,11 +1063,6 @@ function Image({
463
1063
  setBlobSrc(objectUrl);
464
1064
  return;
465
1065
  }
466
- if (response.status === 401 && !retriedRef.current && await isSessionMismatch(response)) {
467
- retriedRef.current = true;
468
- await reissueToken();
469
- return;
470
- }
471
1066
  } catch {
472
1067
  }
473
1068
  if (!cancelled) setFailed(true);
@@ -476,8 +1071,8 @@ function Image({
476
1071
  cancelled = true;
477
1072
  if (objectUrl) URL.revokeObjectURL(objectUrl);
478
1073
  };
479
- }, [isPrivate, failed, media, url, sessionSecret, reissueToken, width, height, fit, format, quality]);
480
- const handleError = React2.useCallback(() => setFailed(true), []);
1074
+ }, [isPrivate, failed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
1075
+ const handleError = React7.useCallback(() => setFailed(true), []);
481
1076
  let src;
482
1077
  if (failed || !media) {
483
1078
  src = externalFallback ?? null;
@@ -486,13 +1081,13 @@ function Image({
486
1081
  } else {
487
1082
  src = url(media, { width, height, fit, format, quality });
488
1083
  }
489
- if (!src) return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
1084
+ if (!src) return /* @__PURE__ */ jsx6(Fragment4, { children: fallback });
490
1085
  if (render) {
491
- return React2.cloneElement(render, {
1086
+ return React7.cloneElement(render, {
492
1087
  style: { ...render.props.style, backgroundImage: `url("${src}")` }
493
1088
  });
494
1089
  }
495
- return /* @__PURE__ */ jsx2(
1090
+ return /* @__PURE__ */ jsx6(
496
1091
  "img",
497
1092
  {
498
1093
  src,
@@ -506,17 +1101,16 @@ function Image({
506
1101
  }
507
1102
  function DownloadLink({ media, filename, children, ...anchorProps }) {
508
1103
  const { url, token, baseUrl, sessionSecret, reissueToken } = useDevultur();
509
- const handleClick = React2.useCallback(
1104
+ const handleClick = React7.useCallback(
510
1105
  async (event) => {
511
1106
  if (media?.visibility !== "private" || !baseUrl) return;
512
1107
  event.preventDefault();
513
- const headers = sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0;
514
- const buildUrl = (viewerToken) => mediaUrl(baseUrl, media, { download: filename ?? true, token: viewerToken });
515
- let response = await fetch(buildUrl(token ?? void 0), { headers });
516
- if (response.status === 401 && await isSessionMismatch(response)) {
517
- const fresh = await reissueToken();
518
- if (fresh) response = await fetch(buildUrl(fresh), { headers });
519
- }
1108
+ const response = await fetchMedia({
1109
+ buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { download: filename ?? true, token: viewerToken }),
1110
+ token,
1111
+ sessionSecret,
1112
+ reissueToken
1113
+ });
520
1114
  if (!response.ok) return;
521
1115
  const objectUrl = URL.createObjectURL(await response.blob());
522
1116
  const anchor = document.createElement("a");
@@ -528,10 +1122,10 @@ function DownloadLink({ media, filename, children, ...anchorProps }) {
528
1122
  [media, baseUrl, token, filename, sessionSecret, reissueToken]
529
1123
  );
530
1124
  if (!media) return null;
531
- return /* @__PURE__ */ jsx2("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
1125
+ return /* @__PURE__ */ jsx6("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
532
1126
  }
533
1127
  function useDevulturMedia(key) {
534
- const apiCtx = React2.use(DevulturApiContext);
1128
+ const apiCtx = React7.use(DevulturApiContext);
535
1129
  if (!apiCtx?.getMedia) {
536
1130
  throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
537
1131
  }
@@ -588,7 +1182,7 @@ function useVideoState(media, options) {
588
1182
  const { token, baseUrl, sessionSecret } = useDevultur();
589
1183
  const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
590
1184
  const locales = options?.locales;
591
- return React2.useMemo(() => {
1185
+ return React7.useMemo(() => {
592
1186
  if (media?.kind !== "video") return { phase: "idle" };
593
1187
  if (status.isFailed) {
594
1188
  return {
@@ -601,7 +1195,7 @@ function useVideoState(media, options) {
601
1195
  if (status.isReady) {
602
1196
  if (!baseUrl) throw new Error("useVideoState requires `baseUrl` on <DevulturProvider>");
603
1197
  const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
604
- const playlist = buildMediaUrl(baseUrl, mediaPath(media), { token: viewerToken });
1198
+ const playlist = buildMediaUrl(baseUrl, mediaPath2(media), { token: viewerToken });
605
1199
  const captions = (locales ?? status.captionLocales).flatMap((locale) => {
606
1200
  const path = mediaCaptionPath(media, locale);
607
1201
  if (!path) return [];
@@ -632,23 +1226,23 @@ function useVideoState(media, options) {
632
1226
  }
633
1227
  function useMediaUpload(options) {
634
1228
  const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
635
- const api = React2.use(DevulturApiContext);
1229
+ const api = React7.use(DevulturApiContext);
636
1230
  if (!api) throw new Error("useMediaUpload must be used within <DevulturProvider>");
637
1231
  const createUploadUrlAction = useAction(api.createUploadUrl);
638
1232
  const deleteMediaAction = useAction(api.deleteMedia);
639
1233
  const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
640
1234
  const processVideoAction = useAction(api.processVideo ?? api.createUploadUrl);
641
- const [phase, setPhase] = React2.useState("idle");
642
- const [percent, setPercent] = React2.useState(0);
643
- const [currentFile, setCurrentFile] = React2.useState(null);
644
- const [media, setMedia] = React2.useState(null);
645
- const [uploadFailure, setUploadFailure] = React2.useState(null);
646
- const [processRequested, setProcessRequested] = React2.useState(false);
647
- const abortRef = React2.useRef(null);
648
- const lastFileRef = React2.useRef(null);
649
- const lastKeyRef = React2.useRef(null);
1235
+ const [phase, setPhase] = React7.useState("idle");
1236
+ const [percent, setPercent] = React7.useState(0);
1237
+ const [currentFile, setCurrentFile] = React7.useState(null);
1238
+ const [media, setMedia] = React7.useState(null);
1239
+ const [uploadFailure, setUploadFailure] = React7.useState(null);
1240
+ const [processRequested, setProcessRequested] = React7.useState(false);
1241
+ const abortRef = React7.useRef(null);
1242
+ const lastFileRef = React7.useRef(null);
1243
+ const lastKeyRef = React7.useRef(null);
650
1244
  const videoState = useVideoState(media, { locales });
651
- const triggerProcess = React2.useCallback(
1245
+ const triggerProcess = React7.useCallback(
652
1246
  (key) => {
653
1247
  if (!api.processVideo) {
654
1248
  throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
@@ -661,7 +1255,7 @@ function useMediaUpload(options) {
661
1255
  },
662
1256
  [api.processVideo, processVideoAction]
663
1257
  );
664
- const runUpload = React2.useCallback(
1258
+ const runUpload = React7.useCallback(
665
1259
  async (file) => {
666
1260
  const controller = new AbortController();
667
1261
  abortRef.current = controller;
@@ -713,7 +1307,7 @@ function useMediaUpload(options) {
713
1307
  };
714
1308
  setMedia(uploaded);
715
1309
  setPhase("uploaded");
716
- if (replaces) deleteMediaAction({ key: mediaPath(replaces) }).catch(() => {
1310
+ if (replaces) deleteMediaAction({ key: mediaPath2(replaces) }).catch(() => {
717
1311
  });
718
1312
  onComplete?.(uploaded);
719
1313
  if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
@@ -732,10 +1326,10 @@ function useMediaUpload(options) {
732
1326
  onError
733
1327
  ]
734
1328
  );
735
- const cancel = React2.useCallback(() => {
1329
+ const cancel = React7.useCallback(() => {
736
1330
  abortRef.current?.abort();
737
1331
  }, []);
738
- const reset = React2.useCallback(() => {
1332
+ const reset = React7.useCallback(() => {
739
1333
  abortRef.current?.abort();
740
1334
  setPhase("idle");
741
1335
  setPercent(0);
@@ -744,15 +1338,15 @@ function useMediaUpload(options) {
744
1338
  setUploadFailure(null);
745
1339
  setProcessRequested(false);
746
1340
  }, []);
747
- const retry = React2.useCallback(() => {
1341
+ const retry = React7.useCallback(() => {
748
1342
  if (phase === "upload-failed" && lastFileRef.current) {
749
1343
  runUpload(lastFileRef.current);
750
1344
  return;
751
1345
  }
752
1346
  if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
753
1347
  }, [phase, runUpload, triggerProcess]);
754
- React2.useEffect(() => () => abortRef.current?.abort(), []);
755
- const state = React2.useMemo(
1348
+ React7.useEffect(() => () => abortRef.current?.abort(), []);
1349
+ const state = React7.useMemo(
756
1350
  () => deriveUploadState(
757
1351
  { phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
758
1352
  videoState,
@@ -760,7 +1354,7 @@ function useMediaUpload(options) {
760
1354
  ),
761
1355
  [phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
762
1356
  );
763
- const upload = React2.useCallback((file) => runUpload(file), [runUpload]);
1357
+ const upload = React7.useCallback((file) => runUpload(file), [runUpload]);
764
1358
  return { upload, state, cancel, reset };
765
1359
  }
766
1360
  export {
@@ -768,9 +1362,14 @@ export {
768
1362
  DevulturProvider,
769
1363
  DownloadLink,
770
1364
  Image,
1365
+ MediaFetchError,
1366
+ PdfPreviewProvider,
1367
+ PdfThumbnail,
1368
+ Preview,
771
1369
  useDevultur,
772
1370
  useDevulturMedia,
773
1371
  useDevulturProgress,
1372
+ useMediaObjectUrl,
774
1373
  useMediaUpload,
775
1374
  useMediaUrl,
776
1375
  useVideoProps,