@devultur/convex 1.3.2 → 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 +426 -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 +441 -66
  47. package/dist/react.js +1663 -550
  48. package/dist/types-LrusGVDf.d.ts +62 -0
  49. package/dist/validators.d.ts +17 -5
  50. package/dist/validators.js +1 -1
  51. package/package.json +8 -4
package/dist/pdfjs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { P as PdfjsModule } from './pdf-render-CiCK6V1X.js';
1
+ import { P as PdfjsLoader, a as PdfjsModule } from './pdf-render-BwJGagvz.js';
2
2
 
3
3
  /** The worker next to the legacy build, resolved by your bundler from this package. */
4
4
  declare const defaultPdfWorkerSrc: () => string;
@@ -7,5 +7,16 @@ declare const defaultPdfWorkerSrc: () => string;
7
7
  * already, points `GlobalWorkerOptions.workerSrc` at `defaultPdfWorkerSrc()`.
8
8
  */
9
9
  declare function loadPdfjs(): Promise<PdfjsModule>;
10
+ /**
11
+ * A PDF's page count, read in the browser with pdf.js (imported on the first call), or `null` when the file isn't a
12
+ * PDF pdf.js can open. Pass it as `pageCount` to `upload()` or `confirmUpload`: it's recorded as the client's hint.
13
+ *
14
+ * @example
15
+ * const pageCount = (await countPdfPages(file)) ?? undefined;
16
+ * await upload(file, { pageCount });
17
+ */
18
+ declare function countPdfPages(file: Blob, options?: {
19
+ loadPdfjs?: PdfjsLoader;
20
+ }): Promise<number | null>;
10
21
 
11
- export { defaultPdfWorkerSrc, loadPdfjs };
22
+ export { countPdfPages, defaultPdfWorkerSrc, loadPdfjs };
package/dist/pdfjs.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ countPages
3
+ } from "./chunk-HOFTKJWT.js";
4
+
1
5
  // src/pdfjs.ts
2
6
  var defaultPdfWorkerSrc = () => new URL("pdfjs-dist/legacy/build/pdf.worker.min.mjs", import.meta.url).href;
3
7
  async function loadPdfjs() {
@@ -5,7 +9,12 @@ async function loadPdfjs() {
5
9
  if (!pdfjs.GlobalWorkerOptions.workerSrc) pdfjs.GlobalWorkerOptions.workerSrc = defaultPdfWorkerSrc();
6
10
  return pdfjs;
7
11
  }
12
+ async function countPdfPages(file, options) {
13
+ const pdfjs = await (options?.loadPdfjs ?? loadPdfjs)();
14
+ return countPages(pdfjs, file);
15
+ }
8
16
  export {
17
+ countPdfPages,
9
18
  defaultPdfWorkerSrc,
10
19
  loadPdfjs
11
20
  };
package/dist/react.d.ts CHANGED
@@ -1,11 +1,35 @@
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
6
  import { Value } from 'convex/values';
7
- import { a as PdfjsLoader } from './pdf-render-CiCK6V1X.js';
8
- export { P as PdfjsModule } from './pdf-render-CiCK6V1X.js';
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
+ }
9
33
 
10
34
  /** What a viewer token action returns: the token with its lifetime, or the bare token (default lifetime). */
11
35
  type ViewerTokenResult = {
@@ -14,6 +38,52 @@ type ViewerTokenResult = {
14
38
  } | string;
15
39
  /** Extra arguments sent with `sessionSecret` to the viewer token action. */
16
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
+ }
17
87
 
18
88
  interface CropUploadRenderProps {
19
89
  /** The state machine of the full cycle (idle → uploading → ready / failed). */
@@ -62,22 +132,147 @@ interface CropUploadProps {
62
132
  */
63
133
  declare function CropUpload({ aspectRatio, visibility, outputFormat, maxWidth, minSourceWidth, replaces, onComplete, onReject, accept, cropperClassName, labels, children, }: CropUploadProps): React.JSX.Element;
64
134
 
65
- /** Why a media fetch was refused. Stable: match on it, not on `message`. */
66
- type MediaFetchErrorCode =
67
- /** The delivery answered with a non-2xx status (`status` has it). */
68
- "http_error"
69
- /** `Content-Length`, or the streamed body, exceeds `maxBytes`. */
70
- | "too_large"
71
- /** The expected content type isn't one that can be previewed inline. */
72
- | "unsupported_type"
73
- /** The response's `Content-Type` doesn't agree with the expected one. */
74
- | "type_mismatch";
75
- declare class MediaFetchError extends Error {
76
- readonly code: MediaFetchErrorCode;
77
- /** HTTP status, for `http_error`. */
78
- readonly status?: number;
79
- 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;
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;
80
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;
81
276
 
82
277
  interface UseMediaObjectUrlOptions {
83
278
  /** `false` → nothing is fetched (e.g. a list row that isn't selected). Default `true`. */
@@ -165,6 +360,153 @@ declare function PdfPreviewProvider({ pdfjs, workerSrc, labels, children, }: Pdf
165
360
  children: React.ReactNode;
166
361
  }): React.JSX.Element;
167
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
+
168
510
  interface PdfThumbnailProps {
169
511
  /** Typed reference. `null`/`undefined` → renders nothing. */
170
512
  media: DevulturMedia | null | undefined;
@@ -207,7 +549,8 @@ interface PreviewProps {
207
549
  /**
208
550
  * Shown for types that don't preview inline and when the fetch fails. Default: a `<DownloadLink>`.
209
551
  * As a function it receives why: a `MediaFetchError` when the fetch was refused (`http_error`, `too_large`,
210
- * `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
211
554
  * (a network failure, a PDF pdf.js can't open, or the link after the `maxPages` note).
212
555
  */
213
556
  fallback?: React.ReactNode | ((error: MediaFetchError | null) => React.ReactNode);
@@ -240,8 +583,40 @@ declare function Preview({ media, contentType, fallback, loading, labels, ...vie
240
583
  /** How a content type renders inline: `<img>`, a PDF view (pdf.js canvases or `<iframe>`), `<audio>` or `<pre>`. */
241
584
  type PreviewKind = "image" | "pdf" | "audio" | "text";
242
585
 
243
- /** Transform/delivery options for media URLs (without `token`, which the context injects). */
244
- 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
+
245
620
  type AnyAction = FunctionReference<"action">;
246
621
  type AnyMutation = FunctionReference<"mutation">;
247
622
  type AnyQuery = FunctionReference<"query">;
@@ -256,6 +631,10 @@ interface DevulturApi {
256
631
  /** Requests the transcode of an uploaded video. Required by `useMediaUpload` for auto-process. */
257
632
  processVideo?: AnyAction;
258
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;
259
638
  saveProgress?: AnyMutation;
260
639
  loadProgress?: AnyQuery;
261
640
  markComplete?: AnyMutation;
@@ -268,6 +647,8 @@ interface UploadOptions {
268
647
  onProgress?: (progress: UploadProgress) => void;
269
648
  /** Aborts the PUT → `upload()` rejects with an `AbortError`. */
270
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;
271
652
  }
272
653
  /**
273
654
  * The reference `upload()` resolves to: a `DevulturMedia` whose `key` is always set (the uploaded object's key; a
@@ -276,29 +657,19 @@ interface UploadOptions {
276
657
  type UploadedMedia = DevulturMedia & {
277
658
  readonly key: string;
278
659
  };
279
- interface DevulturContextValue {
280
- token: string | null;
281
- baseUrl: string | null;
282
- /** Browser session secret → sent in `${SESSION_HEADER}` on every private delivery. */
283
- 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 {
284
665
  uploadUrl: (file: File) => Promise<{
285
666
  url: string;
286
667
  key: string;
287
668
  }>;
288
669
  /** Uploads a file (reporting progress), records it in the component, and returns the reference ready to save. */
289
670
  upload: (file: File, options?: UploadOptions) => Promise<UploadedMedia>;
290
- /** Delivery URL from a reference (non-hook → usable in handlers and `.map`). Token-transparent. */
291
- url: (media: DevulturMedia, options?: MediaRenderOptions) => string;
292
- /** Downloads a video's subtitle text for a locale (auth-transparent). */
293
- getCaptionsText: (media: DevulturMedia, locale: string) => Promise<string>;
294
671
  /** Deletes a media by reference: video → full folder; image/file → the object. */
295
672
  deleteMedia: (media: DevulturMedia) => void;
296
- /**
297
- * Re-issues the viewer token (new session fingerprint with the current UA/IP) and updates the context.
298
- * Single-flight: concurrent calls share the same issuance. Returns the new token (or null if it
299
- * failed). The render uses it to transparently recover from a 401 `session_mismatch`.
300
- */
301
- reissueToken: () => Promise<string | null>;
302
673
  }
303
674
  interface ProgressContextValue {
304
675
  save: (videoId: string, position: number) => Promise<void>;
@@ -324,13 +695,37 @@ interface DevulturProviderProps {
324
695
  * Your own viewer token action, used instead of `api.issueViewerToken`. It receives `{ sessionSecret, ...tokenArgs }`
325
696
  * and returns what the component's `issueViewerToken` returns.
326
697
  */
327
- issueViewerToken?: FunctionReference<"action", "public", {
328
- sessionSecret: string;
329
- }>;
698
+ issueViewerToken?: ViewerTokenAction;
330
699
  children: React.ReactNode;
331
700
  }
332
701
  declare function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children }: DevulturProviderProps): React.JSX.Element;
333
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;
334
729
  declare function useDevulturProgress(): ProgressContextValue;
335
730
  /**
336
731
  * Ready-to-use delivery URL for a key, with the context's viewer token already injected. The consumer
@@ -361,35 +756,15 @@ interface VideoProps {
361
756
  declare function useVideoProps(media: DevulturMedia | null | undefined, options?: {
362
757
  locales?: string[];
363
758
  }): VideoProps | null;
364
- interface ImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height"> {
365
- /** Typed reference. `null`/`undefined` → renders `fallback` (or `externalFallback`). */
366
- media: DevulturMedia | null | undefined;
367
- width?: number;
368
- height?: number;
369
- fit?: MediaUrlOptions["fit"];
370
- format?: MediaUrlOptions["format"];
371
- quality?: number;
372
- /** What to show when there's no media (or it's loading). */
373
- fallback?: React.ReactNode;
374
- /** Explicit external URL (e.g. Unsplash) when `media` is null or the load fails. */
375
- externalFallback?: string;
376
- /** background-image mode: composes the URL as this element's `background-image` (doesn't force `<img>`). */
377
- render?: React.ReactElement<{
378
- style?: React.CSSProperties;
379
- }>;
380
- }
381
- /**
382
- * Renders any media declaratively: token-transparent, with typed transforms, and works inside a
383
- * `.map()` (no wrappers or hooks rules on the consumer's side).
384
- *
385
- * @example
386
- * <Image media={course.thumbnail} alt={course.title} width={512} format="webp" fallback={<Placeholder />} />
387
- */
388
- declare function Image({ media, width, height, fit, format, quality, fallback, externalFallback, render, alt, ...imgProps }: ImageProps): React.JSX.Element;
389
759
  interface DownloadLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href" | "download" | "media"> {
390
760
  media: DevulturMedia | null | undefined;
391
761
  /** Name it downloads as. Without this it uses the upload's original name. */
392
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;
393
768
  children: React.ReactNode;
394
769
  }
395
770
  /**
@@ -400,7 +775,7 @@ interface DownloadLinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEl
400
775
  * binding the gate gives a 401. So, for private media, we intercept the click, blob-fetch with the header
401
776
  * (like private `<Image>`) and trigger the download from the object URL. Public keeps native navigation.
402
777
  */
403
- 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;
404
779
  interface MediaStatusReturn {
405
780
  status: MediaStatusValue;
406
781
  /** Typed progress: `phase` is the pipeline step (no `as any`). */
@@ -494,4 +869,4 @@ interface UseMediaUploadReturn {
494
869
  */
495
870
  declare function useMediaUpload(options?: UseMediaUploadOptions): UseMediaUploadReturn;
496
871
 
497
- 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, type ViewerTokenArgs, type ViewerTokenResult, 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 };