@devultur/convex 1.3.1 → 1.4.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.
Files changed (51) hide show
  1. package/README.md +462 -9
  2. package/dist/bin/init.js +44 -23
  3. package/dist/chunk-EX3W77VY.js +245 -0
  4. package/dist/chunk-HOFTKJWT.js +58 -0
  5. package/dist/chunk-KAIVI7RB.js +21 -0
  6. package/dist/chunk-QBHIZBOQ.js +139 -0
  7. package/dist/{chunk-GKLFRSGI.js → chunk-YIPZZDGD.js} +8 -1
  8. package/dist/component/_generated/api.d.ts +6 -0
  9. package/dist/component/_generated/api.d.ts.map +1 -1
  10. package/dist/component/_generated/api.js.map +1 -1
  11. package/dist/component/_generated/component.d.ts +27 -0
  12. package/dist/component/_generated/component.d.ts.map +1 -1
  13. package/dist/component/library.d.ts +93 -0
  14. package/dist/component/library.d.ts.map +1 -0
  15. package/dist/component/library.js +74 -0
  16. package/dist/component/library.js.map +1 -0
  17. package/dist/component/mediaMeta.d.ts +46 -0
  18. package/dist/component/mediaMeta.d.ts.map +1 -0
  19. package/dist/component/mediaMeta.js +82 -0
  20. package/dist/component/mediaMeta.js.map +1 -0
  21. package/dist/component/metadata.d.ts +59 -0
  22. package/dist/component/metadata.d.ts.map +1 -0
  23. package/dist/component/metadata.js +32 -0
  24. package/dist/component/metadata.js.map +1 -0
  25. package/dist/component/public.d.ts +17 -1
  26. package/dist/component/public.d.ts.map +1 -1
  27. package/dist/component/public.js +20 -11
  28. package/dist/component/public.js.map +1 -1
  29. package/dist/component/schema.d.ts +17 -2
  30. package/dist/component/schema.d.ts.map +1 -1
  31. package/dist/component/schema.js +13 -1
  32. package/dist/component/schema.js.map +1 -1
  33. package/dist/component/uploads.d.ts +13 -0
  34. package/dist/component/uploads.d.ts.map +1 -1
  35. package/dist/component/uploads.js +24 -5
  36. package/dist/component/uploads.js.map +1 -1
  37. package/dist/error-labels-BfqcFh2R.d.ts +19 -0
  38. package/dist/index.d.ts +204 -93
  39. package/dist/index.js +200 -128
  40. package/dist/locales/es.d.ts +11 -0
  41. package/dist/locales/es.js +162 -0
  42. package/dist/media-picker-labels-CiVcNUZo.d.ts +114 -0
  43. package/dist/{pdf-render-CiCK6V1X.d.ts → pdf-render-BwJGagvz.d.ts} +1 -1
  44. package/dist/pdfjs.d.ts +13 -2
  45. package/dist/pdfjs.js +9 -0
  46. package/dist/react.d.ts +462 -64
  47. package/dist/react.js +1625 -493
  48. package/dist/types-LrusGVDf.d.ts +62 -0
  49. package/dist/validators.d.ts +13 -1
  50. package/dist/validators.js +1 -1
  51. package/package.json +8 -4
package/dist/react.d.ts CHANGED
@@ -1,10 +1,89 @@
1
1
  import React from 'react';
2
- import { Visibility, DevulturMedia, MediaUrlOptions, ProcessingPhase, FailureReason, MediaStatusValue } from '@devultur/core';
2
+ import { DevulturMedia, MediaUrlOptions, Visibility, ProcessingPhase, FailureReason, MediaStatusValue } from '@devultur/core';
3
3
  import { BakeOutputFormat, MediaUploadState, MediaUploadError, UploadProgress } from '@devultur/react';
4
4
  export { MediaUploadError, MediaUploadState, UploadProgress } from '@devultur/react';
5
5
  import { FunctionReference } from 'convex/server';
6
- import { a as PdfjsLoader } from './pdf-render-CiCK6V1X.js';
7
- export { P as PdfjsModule } from './pdf-render-CiCK6V1X.js';
6
+ import { Value } from 'convex/values';
7
+ import { A as AttachmentLabels, M as MediaLibraryLabels, a as MediaPickerLabels } from './media-picker-labels-CiVcNUZo.js';
8
+ export { b as AttachmentKind, d as defaultAttachmentLabels, c as defaultMediaLibraryLabels, e as defaultMediaPickerLabels, f as formatDuration, g as formatFileSize } from './media-picker-labels-CiVcNUZo.js';
9
+ import { M as MediaRecord } from './types-LrusGVDf.js';
10
+ import { P as PdfjsLoader } from './pdf-render-BwJGagvz.js';
11
+ export { a as PdfjsModule } from './pdf-render-BwJGagvz.js';
12
+
13
+ /** Why a media fetch was refused. Stable: match on it, not on `message`. */
14
+ type MediaFetchErrorCode =
15
+ /** The delivery answered with a non-2xx status (`status` has it). */
16
+ "http_error"
17
+ /** `Content-Length`, or the streamed body, exceeds `maxBytes`. */
18
+ | "too_large"
19
+ /** The expected content type isn't one that can be previewed inline. */
20
+ | "unsupported_type"
21
+ /** The response's `Content-Type` doesn't agree with the expected one. */
22
+ | "type_mismatch"
23
+ /** No viewer token for private media: its issuance failed (`useDevultur().tokenError` has why) or was refused. */
24
+ | "token_unavailable"
25
+ /** The delivery refused the token for this browser session, also after a re-issue (video playback). */
26
+ | "session_mismatch";
27
+ declare class MediaFetchError extends Error {
28
+ readonly code: MediaFetchErrorCode;
29
+ /** HTTP status, for `http_error`. */
30
+ readonly status?: number;
31
+ constructor(code: MediaFetchErrorCode, message: string, status?: number, options?: ErrorOptions);
32
+ }
33
+
34
+ /** What a viewer token action returns: the token with its lifetime, or the bare token (default lifetime). */
35
+ type ViewerTokenResult = {
36
+ token: string;
37
+ expiresIn: number;
38
+ } | string;
39
+ /** Extra arguments sent with `sessionSecret` to the viewer token action. */
40
+ type ViewerTokenArgs = Record<string, Value>;
41
+ /**
42
+ * Where the viewer token for the current `tokenArgs` stands: not requested yet, being issued (no token yet), held, or
43
+ * failed (the action threw or returned no token). A renewal in flight, or one that failed, keeps `"ready"`.
44
+ */
45
+ type TokenStatus = "idle" | "loading" | "ready" | "error";
46
+
47
+ /** Transform/delivery options for media URLs (without `token`, which the context injects). */
48
+ type MediaRenderOptions = Omit<MediaUrlOptions, "token">;
49
+ type ViewerTokenAction = FunctionReference<"action", "public", {
50
+ sessionSecret: string;
51
+ }>;
52
+ interface MediaSession {
53
+ token: string | null;
54
+ /** Where the viewer token stands; `"error"` makes private previews fail fast with `token_unavailable`. */
55
+ tokenStatus: TokenStatus;
56
+ /** Why the last issuance failed (the action threw or returned no token), while `tokenStatus` is `"error"`. */
57
+ tokenError: Error | null;
58
+ /** Issues the viewer token again (after a failure, for example). Resolves to the new token, or `null`. */
59
+ retryToken: () => Promise<string | null>;
60
+ baseUrl: string | null;
61
+ /** Browser session secret → sent in `${SESSION_HEADER}` on every private delivery. */
62
+ sessionSecret: string;
63
+ /** Delivery URL from a reference (non-hook → usable in handlers and `.map`). Token-transparent. */
64
+ url: (media: DevulturMedia, options?: MediaRenderOptions) => string;
65
+ /** Downloads a video's subtitle text for a locale (auth-transparent). */
66
+ getCaptionsText: (media: DevulturMedia, locale: string) => Promise<string>;
67
+ /**
68
+ * Re-issues the viewer token (new session fingerprint with the current UA/IP) and updates the context.
69
+ * Single-flight: concurrent calls share the same issuance. Returns the new token (or null if it
70
+ * failed). The render uses it to transparently recover from a 401 `session_mismatch`.
71
+ */
72
+ reissueToken: () => Promise<string | null>;
73
+ }
74
+
75
+ /** What `useDevultur()` can do only under a `<DevulturProvider>` given the matching actions. */
76
+ type DevulturCapability = "upload" | "uploadUrl" | "deleteMedia" | "api";
77
+ /**
78
+ * Thrown (or rejected) when an app calls something its provider wasn't given: `upload`, `uploadUrl` or `deleteMedia`
79
+ * under a `<DevulturViewerProvider>`, or a hook that needs the `api` (`useMediaUpload`, `<MediaLibrary>`, ...).
80
+ * Stable `code`: `capability_unavailable`.
81
+ */
82
+ declare class DevulturCapabilityError extends Error {
83
+ readonly code = "capability_unavailable";
84
+ readonly capability: DevulturCapability;
85
+ constructor(capability: DevulturCapability);
86
+ }
8
87
 
9
88
  interface CropUploadRenderProps {
10
89
  /** The state machine of the full cycle (idle → uploading → ready / failed). */
@@ -53,22 +132,147 @@ interface CropUploadProps {
53
132
  */
54
133
  declare function CropUpload({ aspectRatio, visibility, outputFormat, maxWidth, minSourceWidth, replaces, onComplete, onReject, accept, cropperClassName, labels, children, }: CropUploadProps): React.JSX.Element;
55
134
 
56
- /** Why a media fetch was refused. Stable: match on it, not on `message`. */
57
- type MediaFetchErrorCode =
58
- /** The delivery answered with a non-2xx status (`status` has it). */
59
- "http_error"
60
- /** `Content-Length`, or the streamed body, exceeds `maxBytes`. */
61
- | "too_large"
62
- /** The expected content type isn't one that can be previewed inline. */
63
- | "unsupported_type"
64
- /** The response's `Content-Type` doesn't agree with the expected one. */
65
- | "type_mismatch";
66
- declare class MediaFetchError extends Error {
67
- readonly code: MediaFetchErrorCode;
68
- /** HTTP status, for `http_error`. */
69
- readonly status?: number;
70
- constructor(code: MediaFetchErrorCode, message: string, status?: number);
135
+ interface DocumentLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "download" | "media"> {
136
+ /** Typed reference. `null`/`undefined` → renders nothing. */
137
+ media: DevulturMedia | null | undefined;
138
+ /** `true` → the link downloads the file instead of opening it. Private media always downloads. Default `false`. */
139
+ download?: boolean;
140
+ /** Text of the link. Default: the media's `title`, else its filename, else `labels.untitled`. */
141
+ children?: React.ReactNode;
142
+ labels?: Partial<AttachmentLabels>;
143
+ }
144
+ /**
145
+ * A link to a media's file, named after it. Public: a plain link to the delivery URL (it opens in the browser, or
146
+ * downloads with `download`). Private: a navigation can't send the session header, so the click fetches the file
147
+ * with it and downloads it, as `<DownloadLink>` does.
148
+ *
149
+ * @example
150
+ * <DocumentLink media={invoice.file} target="_blank" rel="noopener" />
151
+ */
152
+ declare function DocumentLink({ media, download, children, labels, ...anchorProps }: DocumentLinkProps): React.JSX.Element | null;
153
+ type HeadingLevel = 2 | 3 | 4 | 5 | 6;
154
+ interface AttachmentCardProps extends Omit<React.HTMLAttributes<HTMLElement>, "children"> {
155
+ /** Typed reference. `null`/`undefined` → renders nothing. */
156
+ media: DevulturMedia | null | undefined;
157
+ /** The type you know the media has. Default: inferred from `media.filename`'s extension (picks the icon). */
158
+ contentType?: string;
159
+ /** Shows an "Open preview" button that calls this, e.g. to render `<Preview>` in a dialog. */
160
+ onOpen?: (media: DevulturMedia) => void;
161
+ /** Shown in place of the type icon, e.g. a `<PdfThumbnail>` or an `<Image>`. */
162
+ preview?: React.ReactNode;
163
+ /** Level of the name's heading. Default `3`. */
164
+ headingLevel?: HeadingLevel;
165
+ /** Locale for the size. Default: `labels.locale` (`en`). */
166
+ locale?: string;
167
+ labels?: Partial<AttachmentLabels>;
168
+ }
169
+ /**
170
+ * A card for an attachment: an icon by type (or your `preview`), its name, its type and size, its page count (PDF)
171
+ * or duration (audio, video), a download link and, with `onOpen`, an "Open preview" button. Unstyled: target its
172
+ * parts with `data-part` (`icon`, `name`, `details`, `actions`, `download`, `open`).
173
+ *
174
+ * @example
175
+ * <AttachmentCard media={file} onOpen={setPreviewed} />
176
+ */
177
+ declare function AttachmentCard({ media, contentType, onOpen, preview, headingLevel, locale, labels, ...articleProps }: AttachmentCardProps): React.JSX.Element | null;
178
+
179
+ /** An explicit `srcSet` is passed through to the `<img>` as is, and wins over the one `widths` builds. */
180
+ interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height"> {
181
+ /** Typed reference. `null`/`undefined` → renders `fallback` (or `externalFallback`). Its `alt` is the default `alt`. */
182
+ media: DevulturMedia | null | undefined;
183
+ /** Resize width (px). With `widths`, the width a private image is picked for when `sizes` can't tell. */
184
+ width?: number;
185
+ /** Resize height (px). With `widths` and `width`, it sets the ratio every variant keeps. */
186
+ height?: number;
187
+ fit?: MediaUrlOptions["fit"];
188
+ format?: MediaUrlOptions["format"];
189
+ quality?: number;
190
+ /**
191
+ * Responsive widths (px). Public images get a `srcset` with one transformed variant per width, and default to
192
+ * `loading="lazy"` and `decoding="async"`. Private images are fetched once, at the largest width up to twice the
193
+ * width `sizes` (or `width`) gives them: a `blob:` URL can't be a `srcset` candidate.
194
+ */
195
+ widths?: readonly number[];
196
+ /** What to show when there's no media (or it's loading). */
197
+ fallback?: React.ReactNode;
198
+ /** Explicit external URL (e.g. Unsplash) when `media` is null or the load fails. */
199
+ externalFallback?: string;
200
+ /** background-image mode: composes the URL as this element's `background-image` (doesn't force `<img>`). */
201
+ render?: React.ReactElement<{
202
+ style?: React.CSSProperties;
203
+ }>;
204
+ /**
205
+ * A private image that can't be shown: a `MediaFetchError` (`token_unavailable` right away when the viewer token
206
+ * couldn't be issued, `http_error` when the delivery refused it) or a network error. `fallback` shows meanwhile.
207
+ */
208
+ onLoadError?: (error: Error) => void;
209
+ }
210
+ /**
211
+ * Renders any media declaratively: token-transparent, with typed transforms, and works inside a
212
+ * `.map()` (no wrappers or hooks rules on the consumer's side).
213
+ *
214
+ * @example
215
+ * <Image media={course.thumbnail} alt={course.title} width={512} format="webp" fallback={<Placeholder />} />
216
+ * <Image media={post.cover} widths={[480, 960, 1440]} sizes="(min-width: 768px) 50vw, 100vw" format="webp" />
217
+ */
218
+ declare function Image({ media, width, height, fit, format, quality, widths, sizes, fallback, externalFallback, render, alt, onLoadError, ...imgProps }: ImageProps): React.JSX.Element;
219
+
220
+ /** What `renderItem` gets besides the record. */
221
+ interface MediaLibraryItemState {
222
+ /** The record's reference (`mediaFromRecord`), `null` for a record without a visibility. */
223
+ media: DevulturMedia | null;
224
+ /** Chosen (only with `selectable`). */
225
+ isSelected: boolean;
226
+ /** The item the arrow keys move from (the one in the tab order). */
227
+ isActive: boolean;
71
228
  }
229
+ /** The parts of `<MediaLibrary>` that have a class name (`devultur-media-library__{part}`, `root`'s without the suffix). */
230
+ type MediaLibraryPart = "root" | "search" | "filter" | "list" | "item" | "thumbnail" | "name" | "size" | "status" | "more";
231
+ type MediaLibraryClassNames = Partial<Record<MediaLibraryPart, string>>;
232
+
233
+ type MediaType$1 = MediaRecord["type"];
234
+ interface MediaLibraryProps {
235
+ /** Called when an item is chosen (click, Enter or Space), with its reference (`null` for a record without one). */
236
+ onSelect: (record: MediaRecord, media: DevulturMedia | null) => void;
237
+ /** Overrides any of the strings; the rest stay English. */
238
+ labels?: Partial<MediaLibraryLabels>;
239
+ /** Only this kind of media; the type filter is then hidden. */
240
+ type?: MediaType$1;
241
+ /** Keeps the chosen item selected (`aria-selected`, `data-selected`), as a picker. Default `false`. */
242
+ selectable?: boolean;
243
+ /**
244
+ * The selected records' keys (the media ids), controlled by the app: with it, the library shows these as selected
245
+ * instead of the last one chosen. Implies `selectable`.
246
+ */
247
+ selectedKeys?: readonly string[];
248
+ /** Several items can be selected at once (`aria-multiselectable`), for `selectedKeys`. Default `false`. */
249
+ multiple?: boolean;
250
+ /** Shows only the records it returns `true` for, of each loaded page (a page can then show fewer items). */
251
+ filter?: (record: MediaRecord) => boolean;
252
+ /** Replaces an item's content (thumbnail, name and size). The item itself stays the focusable option. */
253
+ renderItem?: (record: MediaRecord, state: MediaLibraryItemState) => React.ReactNode;
254
+ /** Records per page. Default 24. */
255
+ pageSize?: number;
256
+ /**
257
+ * Draws page 1 of PDFs (`<PdfThumbnail>`, inside a `PdfPreviewProvider`). Each one downloads its PDF as it nears the
258
+ * viewport, so it's off by default: PDFs show an icon.
259
+ */
260
+ pdfThumbnails?: boolean;
261
+ /** Added to the root's `devultur-media-library` class. */
262
+ className?: string;
263
+ /** Added to each part's `devultur-media-library__{part}` class. */
264
+ classNames?: MediaLibraryClassNames;
265
+ }
266
+ /**
267
+ * A searchable grid of the media library: a search box, a type filter and the items (a thumbnail for images, an icon,
268
+ * the name and the size), a page at a time. Arrow keys, Home and End move between items. Needs `listMediaPage` and
269
+ * `searchMedia` in `<DevulturProvider api>`. Unstyled but for the layout; style it through the class names and the
270
+ * `--devultur-library-*` custom properties.
271
+ *
272
+ * @example
273
+ * <MediaLibrary type="image" selectable onSelect={(record, media) => setCover(media)} />
274
+ */
275
+ declare function MediaLibrary(props: MediaLibraryProps): React.JSX.Element;
72
276
 
73
277
  interface UseMediaObjectUrlOptions {
74
278
  /** `false` → nothing is fetched (e.g. a list row that isn't selected). Default `true`. */
@@ -156,6 +360,153 @@ declare function PdfPreviewProvider({ pdfjs, workerSrc, labels, children, }: Pdf
156
360
  children: React.ReactNode;
157
361
  }): React.JSX.Element;
158
362
 
363
+ /** A file the picker is uploading, or one that failed (a finished upload leaves the list for the selection). */
364
+ interface MediaPickerUpload$1 {
365
+ /** Stable for the upload's lifetime, for React keys and `cancel(id)`. */
366
+ id: string;
367
+ file: File;
368
+ /** Bytes sent over the file's size, 0 to 1. */
369
+ fraction: number;
370
+ status: "uploading" | "failed";
371
+ /** Why it failed (`labels.uploadError` or `labels.notAccepted`), `null` while uploading. */
372
+ error: string | null;
373
+ /** A failed upload `accept` let through can be retried; a rejected file can't. */
374
+ canRetry: boolean;
375
+ }
376
+ interface MediaPickerUploads {
377
+ uploads: MediaPickerUpload$1[];
378
+ /** Uploads the files `accept` lets through (only the first one without `multiple`) and lists the rest as failed. */
379
+ start: (files: Iterable<File>) => void;
380
+ cancel: (id: string) => void;
381
+ retry: (id: string) => void;
382
+ dismiss: (id: string) => void;
383
+ }
384
+
385
+ /** What every part of a `<MediaPicker>` (and its render prop, and `useMediaPicker()`) gets. */
386
+ interface MediaPickerState extends MediaPickerUploads {
387
+ /** The selection, in order (one item at most without `multiple`). */
388
+ selected: readonly DevulturMedia[];
389
+ multiple: boolean;
390
+ /** The upload `accept`: the picker's own, else the one its `type` implies. */
391
+ accept: readonly string[] | undefined;
392
+ /** The library's type filter: the picker's `type`, else the one `accept` implies. */
393
+ libraryType: MediaRecord["type"] | undefined;
394
+ labels: MediaPickerLabels;
395
+ libraryLabels: Partial<MediaLibraryLabels> | undefined;
396
+ attachmentLabels: Partial<AttachmentLabels> | undefined;
397
+ /** Selects a media (replaces the selection without `multiple`). */
398
+ select: (media: DevulturMedia) => void;
399
+ /** Selects a media, or removes it when it's selected already (with `multiple`). */
400
+ toggle: (media: DevulturMedia) => void;
401
+ remove: (media: DevulturMedia) => void;
402
+ clear: () => void;
403
+ /** Opens the browser's file chooser. */
404
+ openFileDialog: () => void;
405
+ /** Says `message` in the picker's polite live region. */
406
+ announce: (message: string) => void;
407
+ /** The selection's region, focused after an upload and a removal (the `<MediaPicker.Selection>` sets it). */
408
+ selectionRef: React.RefObject<HTMLElement | null>;
409
+ }
410
+ /** The state of the nearest `<MediaPicker>`, to build a part of your own. */
411
+ declare function useMediaPicker(): MediaPickerState;
412
+
413
+ /** What `<MediaPicker.Library>` passes through to `<MediaLibrary>`; the picker sets the rest. */
414
+ type MediaPickerLibraryProps = Pick<MediaLibraryProps, "renderItem" | "pageSize" | "pdfThumbnails" | "className" | "classNames">;
415
+ /**
416
+ * The picker's `<MediaLibrary>`: shows the selection as selected, picks (and, with `multiple`, unpicks) an item on
417
+ * click, Enter or Space, and lists only what `accept` lets through.
418
+ */
419
+ declare function MediaPickerLibrary(props: MediaPickerLibraryProps): React.JSX.Element;
420
+
421
+ interface MediaPickerSelectionProps {
422
+ /** Replaces an item's card. The remove button stays below it. */
423
+ renderItem?: (media: DevulturMedia) => React.ReactNode;
424
+ /** Level of the selection's heading (the cards' are one below). Default `3`. */
425
+ headingLevel?: 2 | 3 | 4 | 5;
426
+ /** Added to the region's `devultur-media-picker__selection` class. */
427
+ className?: string;
428
+ }
429
+ /**
430
+ * The picker's selection: a card per item (a thumbnail for images, the name, type, size and a download link) with
431
+ * "Open preview" (an inline `<Preview>`) and remove, plus clear, and replace without `multiple`. Focused after an
432
+ * upload and a removal, so a keyboard user lands on the result.
433
+ */
434
+ declare function MediaPickerSelection({ renderItem, headingLevel, className }: MediaPickerSelectionProps): React.JSX.Element;
435
+
436
+ interface MediaPickerUploadProps {
437
+ /** Added to the drop zone's `devultur-media-picker__upload` class. */
438
+ className?: string;
439
+ }
440
+ /**
441
+ * The picker's upload area: a button that opens the file chooser, a drop target for files, and each upload's
442
+ * progress with a cancel button (or its error, with retry and dismiss).
443
+ */
444
+ declare function MediaPickerUpload({ className }: MediaPickerUploadProps): React.JSX.Element;
445
+
446
+ interface SingleValueProps {
447
+ multiple?: false;
448
+ /** The selected media, controlled. Omitted → the picker keeps its own. */
449
+ value?: DevulturMedia | null;
450
+ /** Called with the new selection: on an upload, a pick from the library, a removal or a clear. */
451
+ onChange: (value: DevulturMedia | null) => void;
452
+ }
453
+ interface MultipleValueProps {
454
+ /** Several media can be selected; `value` and `onChange` take arrays. */
455
+ multiple: true;
456
+ value?: readonly DevulturMedia[];
457
+ onChange: (value: DevulturMedia[]) => void;
458
+ }
459
+ interface CommonProps {
460
+ /**
461
+ * File types to pick, as an `<input accept>` takes them (`image/*`, `application/pdf`, `.pdf`). Filters the files to
462
+ * upload and the library's records, and sets the library's type filter when they're all of one type.
463
+ */
464
+ accept?: readonly string[];
465
+ /** Only this kind of media in the library; without `accept`, also what can be uploaded (`image/*`, `video/*`). */
466
+ type?: MediaRecord["type"];
467
+ /** Overrides any of the picker's strings; the rest stay English. */
468
+ labels?: Partial<MediaPickerLabels>;
469
+ /** Labels for the library (`<MediaPicker.Library>`). */
470
+ libraryLabels?: Partial<MediaLibraryLabels>;
471
+ /** Labels for the selection's cards (`<AttachmentCard>`). */
472
+ attachmentLabels?: Partial<AttachmentLabels>;
473
+ /** Added to the root's `devultur-media-picker` class. */
474
+ className?: string;
475
+ /**
476
+ * The picker's layout: the parts (`<MediaPicker.Upload>`, `.Library`, `.Selection`) in any order and markup, or a
477
+ * function of the picker's state. Default: the upload, the library and the selection, in that order.
478
+ */
479
+ children?: React.ReactNode | ((picker: MediaPickerState) => React.ReactNode);
480
+ }
481
+ type MediaPickerProps = CommonProps & (SingleValueProps | MultipleValueProps);
482
+ /**
483
+ * Lets an editor pick media: upload new files (progress, cancel, retry), browse and search the library, and review
484
+ * the selection. Returns `DevulturMedia` references through `onChange`; a new upload is selected. It renders inline,
485
+ * with no dialog of its own: put it in yours. Needs `createUploadUrl`, `listMediaPage` and `searchMedia` in
486
+ * `<DevulturProvider api>` (and `confirmUpload` to record uploads). Under a `PdfPreviewProvider` with pdf.js, a PDF's
487
+ * page count is read before it uploads.
488
+ *
489
+ * @example
490
+ * <MediaPicker accept={["image/*"]} value={cover} onChange={setCover} />
491
+ */
492
+ declare function MediaPicker(props: MediaPickerProps): React.JSX.Element;
493
+ declare namespace MediaPicker {
494
+ var Upload: typeof MediaPickerUpload;
495
+ var Library: typeof MediaPickerLibrary;
496
+ var Selection: typeof MediaPickerSelection;
497
+ }
498
+
499
+ /** A file's name and type, or a record's filename and content type. */
500
+ interface Candidate {
501
+ name: string;
502
+ type: string;
503
+ }
504
+ /**
505
+ * Whether `candidate` matches one of the `accept` patterns, as an `<input accept>` does: a MIME type
506
+ * (`application/pdf`), a wildcard (`image/*`) or an extension (`.pdf`). No `accept` (or an empty one) accepts all.
507
+ */
508
+ declare function matchesAccept(candidate: Candidate, accept: readonly string[] | undefined): boolean;
509
+
159
510
  interface PdfThumbnailProps {
160
511
  /** Typed reference. `null`/`undefined` → renders nothing. */
161
512
  media: DevulturMedia | null | undefined;
@@ -198,7 +549,8 @@ interface PreviewProps {
198
549
  /**
199
550
  * Shown for types that don't preview inline and when the fetch fails. Default: a `<DownloadLink>`.
200
551
  * As a function it receives why: a `MediaFetchError` when the fetch was refused (`http_error`, `too_large`,
201
- * `type_mismatch`) or the type doesn't preview inline (`unsupported_type`, nothing fetched), `null` otherwise
552
+ * `type_mismatch`), the viewer token couldn't be issued (`token_unavailable`, nothing fetched), a video's session
553
+ * was refused (`session_mismatch`) or the type doesn't preview inline (`unsupported_type`), `null` otherwise
202
554
  * (a network failure, a PDF pdf.js can't open, or the link after the `maxPages` note).
203
555
  */
204
556
  fallback?: React.ReactNode | ((error: MediaFetchError | null) => React.ReactNode);
@@ -231,8 +583,40 @@ declare function Preview({ media, contentType, fallback, loading, labels, ...vie
231
583
  /** How a content type renders inline: `<img>`, a PDF view (pdf.js canvases or `<iframe>`), `<audio>` or `<pre>`. */
232
584
  type PreviewKind = "image" | "pdf" | "audio" | "text";
233
585
 
234
- /** Transform/delivery options for media URLs (without `token`, which the context injects). */
235
- type MediaRenderOptions = Omit<MediaUrlOptions, "token">;
586
+ type MediaType = MediaRecord["type"];
587
+ interface UseMediaLibraryOptions {
588
+ /** Only this kind of media. Default: every kind. */
589
+ type?: MediaType;
590
+ /** Matches the title and filename. Debounced 250 ms; clearing it applies at once. Blank → newest first. */
591
+ search?: string;
592
+ /** Records in the first page and in each `loadMore()`. Default 24. */
593
+ pageSize?: number;
594
+ }
595
+ /** Convex's pagination status. */
596
+ type MediaLibraryStatus = "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted";
597
+ interface MediaLibraryState {
598
+ /** The records loaded so far: newest first, or best match first while searching. */
599
+ items: MediaRecord[];
600
+ status: MediaLibraryStatus;
601
+ /** Loads `numItems` more (default `pageSize`). Does nothing unless `status` is `"CanLoadMore"`. */
602
+ loadMore: (numItems?: number) => void;
603
+ /** The first page or a further one is loading. */
604
+ isLoading: boolean;
605
+ /** The search the items are for (the debounced `search`, trimmed). */
606
+ search: string;
607
+ /** `search` changed and the debounce hasn't applied it yet. */
608
+ isSearchPending: boolean;
609
+ }
610
+ /**
611
+ * The media library a page at a time, from the `listMediaPage` and `searchMedia` your `<DevulturProvider api>`
612
+ * passes. A new search or type starts again from the first page. Like Convex's `usePaginatedQuery`, a failed query
613
+ * throws to the nearest error boundary (`<MediaLibrary>` has its own).
614
+ *
615
+ * @example
616
+ * const { items, status, loadMore } = useMediaLibrary({ type: "image", search });
617
+ */
618
+ declare function useMediaLibrary(options?: UseMediaLibraryOptions): MediaLibraryState;
619
+
236
620
  type AnyAction = FunctionReference<"action">;
237
621
  type AnyMutation = FunctionReference<"mutation">;
238
622
  type AnyQuery = FunctionReference<"query">;
@@ -247,6 +631,10 @@ interface DevulturApi {
247
631
  /** Requests the transcode of an uploaded video. Required by `useMediaUpload` for auto-process. */
248
632
  processVideo?: AnyAction;
249
633
  getMedia?: AnyQuery;
634
+ /** The library, newest first. Required by `useMediaLibrary` and `<MediaLibrary>`. */
635
+ listMediaPage?: AnyQuery;
636
+ /** The library matching a search. Required by `useMediaLibrary` and `<MediaLibrary>` to search. */
637
+ searchMedia?: AnyQuery;
250
638
  saveProgress?: AnyMutation;
251
639
  loadProgress?: AnyQuery;
252
640
  markComplete?: AnyMutation;
@@ -259,6 +647,8 @@ interface UploadOptions {
259
647
  onProgress?: (progress: UploadProgress) => void;
260
648
  /** Aborts the PUT → `upload()` rejects with an `AbortError`. */
261
649
  signal?: AbortSignal;
650
+ /** A PDF's page count to record with it, a hint (`countPdfPages` from `@devultur/convex/pdfjs` counts it). */
651
+ pageCount?: number;
262
652
  }
263
653
  /**
264
654
  * The reference `upload()` resolves to: a `DevulturMedia` whose `key` is always set (the uploaded object's key; a
@@ -267,29 +657,19 @@ interface UploadOptions {
267
657
  type UploadedMedia = DevulturMedia & {
268
658
  readonly key: string;
269
659
  };
270
- interface DevulturContextValue {
271
- token: string | null;
272
- baseUrl: string | null;
273
- /** Browser session secret → sent in `${SESSION_HEADER}` on every private delivery. */
274
- sessionSecret: string;
660
+ /**
661
+ * What `useDevultur()` returns. Under a `<DevulturViewerProvider>`, `uploadUrl`, `upload` and `deleteMedia` throw a
662
+ * `DevulturCapabilityError` (`capability_unavailable`).
663
+ */
664
+ interface DevulturContextValue extends MediaSession {
275
665
  uploadUrl: (file: File) => Promise<{
276
666
  url: string;
277
667
  key: string;
278
668
  }>;
279
669
  /** Uploads a file (reporting progress), records it in the component, and returns the reference ready to save. */
280
670
  upload: (file: File, options?: UploadOptions) => Promise<UploadedMedia>;
281
- /** Delivery URL from a reference (non-hook → usable in handlers and `.map`). Token-transparent. */
282
- url: (media: DevulturMedia, options?: MediaRenderOptions) => string;
283
- /** Downloads a video's subtitle text for a locale (auth-transparent). */
284
- getCaptionsText: (media: DevulturMedia, locale: string) => Promise<string>;
285
671
  /** Deletes a media by reference: video → full folder; image/file → the object. */
286
672
  deleteMedia: (media: DevulturMedia) => void;
287
- /**
288
- * Re-issues the viewer token (new session fingerprint with the current UA/IP) and updates the context.
289
- * Single-flight: concurrent calls share the same issuance. Returns the new token (or null if it
290
- * failed). The render uses it to transparently recover from a 401 `session_mismatch`.
291
- */
292
- reissueToken: () => Promise<string | null>;
293
673
  }
294
674
  interface ProgressContextValue {
295
675
  save: (videoId: string, position: number) => Promise<void>;
@@ -304,10 +684,48 @@ interface DevulturProviderProps {
304
684
  api: DevulturApi;
305
685
  /** Media worker base (e.g. `https://media.your-domain.com`). Required for `useMediaUrl`. */
306
686
  baseUrl?: string;
687
+ /**
688
+ * Extra arguments sent with `sessionSecret` on every viewer token issuance (first, renewal and re-issue), for
689
+ * example `{ workspaceId }`. Compared by value: when they change, the current token is dropped and a new one is
690
+ * issued. The component's own `issueViewerToken` accepts only `sessionSecret`, so pass your own action in
691
+ * `issueViewerToken` that takes these arguments.
692
+ */
693
+ tokenArgs?: ViewerTokenArgs;
694
+ /**
695
+ * Your own viewer token action, used instead of `api.issueViewerToken`. It receives `{ sessionSecret, ...tokenArgs }`
696
+ * and returns what the component's `issueViewerToken` returns.
697
+ */
698
+ issueViewerToken?: ViewerTokenAction;
307
699
  children: React.ReactNode;
308
700
  }
309
- declare function DevulturProvider({ api, baseUrl, children }: DevulturProviderProps): React.JSX.Element;
701
+ declare function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children }: DevulturProviderProps): React.JSX.Element;
310
702
  declare function useDevultur(): DevulturContextValue;
703
+ /**
704
+ * The `api` passed to `<DevulturProvider>`: your app's exported Devultur functions. Under a `<DevulturViewerProvider>`
705
+ * it throws a `DevulturCapabilityError`.
706
+ */
707
+ declare function useDevulturApi(): DevulturApi;
708
+ interface DevulturViewerProviderProps {
709
+ /** Media worker base (e.g. `https://media.your-domain.com`). */
710
+ baseUrl: string;
711
+ /** Your viewer token action. It receives `{ sessionSecret, ...tokenArgs }`, like `<DevulturProvider>`'s. */
712
+ issueViewerToken: ViewerTokenAction;
713
+ /** Extra arguments for `issueViewerToken`, compared by value (see `<DevulturProvider tokenArgs>`). */
714
+ tokenArgs?: ViewerTokenArgs;
715
+ children: React.ReactNode;
716
+ }
717
+ /**
718
+ * A provider for apps that only display media: it issues and renews the viewer token, so `<Image>`, `<Preview>`,
719
+ * `<DownloadLink>`, `useMediaObjectUrl` and the video hooks work, and it needs no upload or delete actions. `upload`,
720
+ * `uploadUrl` and `deleteMedia` throw a `DevulturCapabilityError`, as do the hooks that need the `api`
721
+ * (`useMediaUpload`, `useDevulturApi`, `<MediaLibrary>`, `<MediaPicker>`).
722
+ *
723
+ * @example
724
+ * <DevulturViewerProvider baseUrl={MEDIA_URL} issueViewerToken={api.admin.issueViewerToken} tokenArgs={{ workspaceId }}>
725
+ * <Preview media={document.file} />
726
+ * </DevulturViewerProvider>
727
+ */
728
+ declare function DevulturViewerProvider({ baseUrl, issueViewerToken, tokenArgs, children, }: DevulturViewerProviderProps): React.JSX.Element;
311
729
  declare function useDevulturProgress(): ProgressContextValue;
312
730
  /**
313
731
  * Ready-to-use delivery URL for a key, with the context's viewer token already injected. The consumer
@@ -338,35 +756,15 @@ interface VideoProps {
338
756
  declare function useVideoProps(media: DevulturMedia | null | undefined, options?: {
339
757
  locales?: string[];
340
758
  }): VideoProps | null;
341
- interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height"> {
342
- /** Typed reference. `null`/`undefined` → renders `fallback` (or `externalFallback`). */
343
- media: DevulturMedia | null | undefined;
344
- width?: number;
345
- height?: number;
346
- fit?: MediaUrlOptions["fit"];
347
- format?: MediaUrlOptions["format"];
348
- quality?: number;
349
- /** What to show when there's no media (or it's loading). */
350
- fallback?: React.ReactNode;
351
- /** Explicit external URL (e.g. Unsplash) when `media` is null or the load fails. */
352
- externalFallback?: string;
353
- /** background-image mode: composes the URL as this element's `background-image` (doesn't force `<img>`). */
354
- render?: React.ReactElement<{
355
- style?: React.CSSProperties;
356
- }>;
357
- }
358
- /**
359
- * Renders any media declaratively: token-transparent, with typed transforms, and works inside a
360
- * `.map()` (no wrappers or hooks rules on the consumer's side).
361
- *
362
- * @example
363
- * <Image media={course.thumbnail} alt={course.title} width={512} format="webp" fallback={<Placeholder />} />
364
- */
365
- declare function Image({ media, width, height, fit, format, quality, fallback, externalFallback, render, alt, ...imgProps }: ImageProps): React.JSX.Element;
366
759
  interface DownloadLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "download" | "media"> {
367
760
  media: DevulturMedia | null | undefined;
368
761
  /** Name it downloads as. Without this it uses the upload's original name. */
369
762
  filename?: string;
763
+ /**
764
+ * A private download that failed: `token_unavailable` right away when the viewer token couldn't be issued
765
+ * (`useDevultur().retryToken()` tries again), `http_error` when the delivery refused it.
766
+ */
767
+ onDownloadError?: (error: MediaFetchError) => void;
370
768
  children: React.ReactNode;
371
769
  }
372
770
  /**
@@ -377,7 +775,7 @@ interface DownloadLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEl
377
775
  * binding the gate gives a 401. So, for private media, we intercept the click, blob-fetch with the header
378
776
  * (like private `<Image>`) and trigger the download from the object URL. Public keeps native navigation.
379
777
  */
380
- declare function DownloadLink({ media, filename, children, ...anchorProps }: DownloadLinkProps): React.JSX.Element | null;
778
+ declare function DownloadLink({ media, filename, onDownloadError, children, ...anchorProps }: DownloadLinkProps): React.JSX.Element | null;
381
779
  interface MediaStatusReturn {
382
780
  status: MediaStatusValue;
383
781
  /** Typed progress: `phase` is the pipeline step (no `as any`). */
@@ -471,4 +869,4 @@ interface UseMediaUploadReturn {
471
869
  */
472
870
  declare function useMediaUpload(options?: UseMediaUploadOptions): UseMediaUploadReturn;
473
871
 
474
- export { type CaptionTrack, CropUpload, type CropUploadLabels, type CropUploadProps, type CropUploadRenderProps, DevulturProvider, DownloadLink, Image, MediaFetchError, type MediaFetchErrorCode, type MediaObjectUrl, type PdfPreviewConfig, PdfPreviewProvider, PdfThumbnail, type PdfThumbnailProps, PdfjsLoader, Preview, type PreviewKind, type PreviewLabels, type PreviewProps, type UploadedMedia, type UseMediaObjectUrlOptions, type UseMediaUploadOptions, type UseMediaUploadReturn, type VideoProps, type VideoState, defaultPreviewLabels, useDevultur, useDevulturMedia, useDevulturProgress, useMediaObjectUrl, useMediaUpload, useMediaUrl, useVideoProps, useVideoState };
872
+ export { AttachmentCard, type AttachmentCardProps, AttachmentLabels, type CaptionTrack, CropUpload, type CropUploadLabels, type CropUploadProps, type CropUploadRenderProps, type DevulturApi, type DevulturCapability, DevulturCapabilityError, DevulturProvider, DevulturViewerProvider, DocumentLink, type DocumentLinkProps, DownloadLink, Image, type ImageProps, MediaFetchError, type MediaFetchErrorCode, MediaLibrary, type MediaLibraryItemState, MediaLibraryLabels, type MediaLibraryPart, type MediaLibraryProps, type MediaLibraryState, type MediaLibraryStatus, type MediaObjectUrl, MediaPicker, MediaPickerLabels, type MediaPickerLibraryProps, type MediaPickerProps, type MediaPickerSelectionProps, type MediaPickerState, type MediaPickerUpload$1 as MediaPickerUpload, type MediaPickerUploadProps, type MediaPickerUploads, type PdfPreviewConfig, PdfPreviewProvider, PdfThumbnail, type PdfThumbnailProps, PdfjsLoader, Preview, type PreviewKind, type PreviewLabels, type PreviewProps, type TokenStatus, type UploadedMedia, type UseMediaLibraryOptions, type UseMediaObjectUrlOptions, type UseMediaUploadOptions, type UseMediaUploadReturn, type VideoProps, type VideoState, type ViewerTokenArgs, type ViewerTokenResult, defaultPreviewLabels, matchesAccept, useDevultur, useDevulturApi, useDevulturMedia, useDevulturProgress, useMediaLibrary, useMediaObjectUrl, useMediaPicker, useMediaUpload, useMediaUrl, useVideoProps, useVideoState };