@ai-matrx/media 0.4.3 → 0.4.5

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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * @ai-matrx/media — the root types: the MediaClient port and the host ports.\n *\n * The package renders Matrx durable media; it NEVER talks to a network.\n * Everything data-shaped arrives through the injected {@link MediaClient}\n * (canonically implemented by `@ai-matrx/data/files`; any structural\n * implementation works — the strangler step wires it over an app's existing\n * file handler). The five file-handling laws are enforced here at the type\n * level:\n *\n * 1. Never fetch a media URL yourself — the package contains zero network\n * primitives; bytes exist only via {@link MediaClient.getBlob}.\n * 2. Off-boundary code resolves media itself — every component takes a\n * {@link MediaRefLike} and calls `resolve()` unconditionally. There is\n * NO `src` prop on any component in this package.\n * 3. A signed URL is a handoff, never an identity — the only string that\n * reaches a media element is the branded {@link DurableSrc}, which only\n * a MediaClient implementation can mint (via {@link mintDurableSrc}).\n * 4. Public-facing media is persisted public, never signed —\n * {@link MediaClient.shareableUrl} fails closed (`null` → the action\n * renders disabled).\n * 5. One URL contract; the engine builds URLs — zero URL construction in\n * this package; `resolve()` is the only door.\n */\n\n// ---------------------------------------------------------------------------\n// Identity\n// ---------------------------------------------------------------------------\n\n/** The canonical durable media reference. `file_id` is the identity. */\nexport interface MediaRef {\n file_id?: string | null;\n url?: string | null;\n mime_type?: string | null;\n metadata?: Record<string, unknown> | null;\n}\n\n/**\n * Identity in. A raw string is a fileId or an EXTERNAL public URL — never a\n * signed URL (the client's `resolve()` classifies and screams on one).\n */\nexport type MediaRefLike = MediaRef | string;\n\ndeclare const durable: unique symbol;\n\n/**\n * A URL that has passed through `MediaClient.resolve()` — the only string\n * type any media element in this package will bind. A caller cannot pass an\n * expiring URL through the package's types without an explicit unsafe cast.\n */\nexport type DurableSrc = string & { readonly [durable]: true };\n\n/**\n * Brand a URL as durable. FOR MediaClient IMPLEMENTATIONS ONLY — the\n * implementation must classify the URL first (the ONE signed-URL classifier\n * lives with the platform's files layer; never re-implement it here or in a\n * host). Calling this on an unclassified string defeats law 3.\n */\nexport function mintDurableSrc(url: string): DurableSrc {\n return url as DurableSrc;\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\nexport type MediaKind = \"image\" | \"video\" | \"audio\" | \"file\";\n\nexport interface MediaResolution {\n src: DurableSrc;\n /** RN auth lane (`<Image source={{uri, headers}}>`); absent on web (cookie session). */\n headers?: Record<string, string> | undefined;\n mimeType?: string | undefined;\n kind: MediaKind;\n /**\n * How pixels must travel. `\"element\"` (default): bind `src` directly to the\n * media element. `\"blob\"`: the bytes require bearer auth the element cannot\n * attach — consumers go through {@link MediaClient.getBlob} and render the\n * returned object URL (the private-image lane).\n */\n transport?: \"element\" | \"blob\" | undefined;\n /**\n * A server-rendered thumbnail for this file, when the client knows one\n * (Matrx: `FileRecord.thumbnail_url` / asset variants). Consumed by the\n * thumbnail tier machine.\n */\n thumbnailSrc?: DurableSrc | undefined;\n /**\n * True when this is a permanent public CDN URL — lets a host-supplied\n * `ImageComponent` (e.g. a next/image adapter) take the render, and marks\n * the URL CORS-safe for element-level `crossOrigin`.\n */\n isCdn?: boolean | undefined;\n /**\n * Whether a load error can be recovered by the client's ONE retry policy\n * (session-refresh → same-URL retry). False for foreign/external URLs —\n * a session refresh can't resurrect someone else's link rot. Default true.\n */\n recoverable?: boolean | undefined;\n}\n\n/** Typed unavailable states — render-as-state, never thrown as incidents. */\nexport type MediaUnavailableReason =\n | \"access_denied\"\n | \"not_found\"\n | \"deleted\"\n | \"unknown\";\n\n// ---------------------------------------------------------------------------\n// Diagnostics — the terminal-failure capture contract (the 2026-08-30 outage\n// class: every private image failed, the honest unavailable panel rendered,\n// and NOTHING reached the host's error system because the package had no\n// diagnostics port). Per C22 the capture logic lives IN the package; a host\n// binds only a sink. Absent sink → the package console.errors the same\n// structured payload once per failure — a media failure is NEVER silent.\n// ---------------------------------------------------------------------------\n\n/** Where in the pipeline a media failure surfaced. */\nexport type MediaFailurePhase =\n /** `MediaClient.resolve()` refused the ref (threw). */\n | \"resolve\"\n /** The client's session bootstrap/mint failed (`recoverLoadError` rejected),\n * or the server reported an ephemeral cookie secret (warning). */\n | \"session\"\n /** `MediaClient.getBlob()` rejected — private-pixels bytes never arrived. */\n | \"blob\"\n /** The media element errored and recovery was exhausted or impossible. */\n | \"element-load\"\n /** The client's recovery machinery declined to retry on the first attempt. */\n | \"recovery\";\n\n/** What the ONE retry policy did before the failure went terminal. */\nexport type MediaRetryOutcome =\n /** Recovery was impossible (unrecoverable source) or was never reached. */\n | \"not-attempted\"\n /** The client granted a session-refresh retry and the retry ALSO failed. */\n | \"retried\"\n /** The recovery call itself threw/rejected. */\n | \"recovery-failed\";\n\n/**\n * The strictly-typed structural payload every terminal media failure emits.\n * `mediaRef` is DURABLE IDENTITY ONLY — a `file:<id>` / query-stripped URL\n * key, NEVER a signed URL (signatures live in query strings, which\n * {@link failureMediaIdentity} removes).\n */\nexport interface MediaFailureInfo {\n source: \"media\";\n phase: MediaFailurePhase;\n /** File id / ref key identity — never a signed URL. */\n mediaRef: string;\n /** HTTP status when known (e.g. from a structured client error). */\n status?: number | undefined;\n /** Outcome of the client's ONE retry policy, when the phase reached it. */\n retryOutcome?: MediaRetryOutcome | undefined;\n /** True for a dead render; false for a captured warning (ephemeral secret). */\n terminal: boolean;\n message: string;\n}\n\n/**\n * The host's error-capture sink. Bind `capture` to the app's error system\n * (matrx-frontend: `captureError` in lib/diagnostics/errorCaptureStore).\n * When absent, the package console.errors the same payload once per failure\n * (the loud default) — it never downgrades to silence.\n */\nexport interface MediaDiagnosticsPort {\n capture?: ((info: MediaFailureInfo) => void) | undefined;\n}\n\n/** String verdicts of the ONE retry policy. */\nexport type MediaRecoveryVerdict = \"retry\" | \"terminal\";\n\n/**\n * Structured `recoverLoadError` result — optional; the bare string verdict\n * remains fully supported. Lets the client surface what it learned during the\n * session bootstrap without a second channel:\n *\n * - `status` — HTTP status of the failed mint/probe, forwarded into the\n * terminal {@link MediaFailureInfo};\n * - `secretSource` — the `/files/session` mint response's `secret_source`\n * when the server sends one (a matrx-connect field). `\"ephemeral\"` means\n * the server signs cookies with a boot-generated secret: private renders\n * WILL break across restarts/instances, and the package captures a\n * non-terminal warning immediately instead of waiting for the outage.\n */\nexport interface MediaRecoveryReport {\n verdict: MediaRecoveryVerdict;\n status?: number | undefined;\n secretSource?: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Bytes / upload\n// ---------------------------------------------------------------------------\n\nexport interface MediaBlobHandle {\n /** `blob:` object URL safe for any browser API. */\n url: string;\n blob: Blob;\n /** Release the handle. Implementations backed by a shared cache may no-op. */\n release(): void;\n}\n\nexport interface MediaUploadOptions {\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n fileName?: string | undefined;\n mimeType?: string | undefined;\n /** Host visibility vocabulary passes through structurally. */\n visibility?: string | undefined;\n parentFolderId?: string | null | undefined;\n metadata?: Record<string, unknown> | undefined;\n}\n\n/** Upload → durable identity. `fileId` is required at compile time and runtime. */\nexport interface MediaUploadResult {\n fileId: string;\n ref: MediaRef;\n}\n\nexport interface MediaMultiUploadResult {\n /** File ids for every successful upload. */\n uploaded: string[];\n /** Per-file failure with the real backend error message. */\n failed: Array<{ name: string; error: string }>;\n /** True when the host's pre-flight (e.g. a duplicate dialog) cancelled the batch. */\n cancelled: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// THE port — one structural interface, owned by this package, implemented by\n// the data layer (canonically `@ai-matrx/data/files`).\n// ---------------------------------------------------------------------------\n\nexport interface MediaClient {\n /**\n * Synchronous durable-URL build + classification. Returns `null` when the\n * ref carries no usable identity (components render their fallback).\n * THROWS (and screams via its own diagnostics sink) on a signed/expiring\n * URL — the D108 guard lives behind this door.\n */\n resolve(ref: MediaRefLike): MediaResolution | null;\n /** Bytes for canvas/crossOrigin/private-pixel consumers. */\n getBlob(ref: MediaRefLike): Promise<MediaBlobHandle>;\n /**\n * Session bootstrap + the ONE retry policy: refresh-then-same-URL-retry,\n * second failure terminal. Called by the package's onError path, never\n * re-implemented.\n */\n recoverLoadError(\n src: DurableSrc,\n attempt: number,\n ): Promise<MediaRecoveryVerdict | MediaRecoveryReport>;\n /** Upload → durable identity. */\n upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;\n /**\n * Optional batch upload with host pre-flight (dedup dialogs etc.). When\n * absent, `useMediaUpload().uploadMany` falls back to sequential `upload`.\n */\n uploadMany?(files: File[], opts?: MediaUploadOptions): Promise<MediaMultiUploadResult>;\n /** Share affordances — fails closed (null), never emits a signed URL. */\n shareableUrl(ref: MediaRefLike): Promise<string | null>;\n /** Typed unavailable states — render-as-state, never thrown as incidents. */\n classifyError(err: unknown): MediaUnavailableReason;\n}\n\n// ---------------------------------------------------------------------------\n// Host ports — all optional, all defaulting to inert.\n// ---------------------------------------------------------------------------\n\n/**\n * Structural component slot. `(props) => unknown` keeps the root entry free\n * of React type imports; any React function component satisfies it.\n */\nexport type MediaSlotComponent<P> = (props: P) => unknown;\n\nexport interface MediaImageComponentProps {\n src: string;\n alt: string;\n width: number;\n height: number;\n className?: string | undefined;\n onError?: ((event: unknown) => void) | undefined;\n onClick?: ((event: unknown) => void) | undefined;\n onKeyDown?: ((event: unknown) => void) | undefined;\n role?: string | undefined;\n tabIndex?: number | undefined;\n}\n\nexport interface PlaybackSessionRegistration {\n isPlaying: boolean;\n source: string;\n label: string;\n trackKey?: string | undefined;\n}\n\n/**\n * The host's audio system (exclusive-playback lock, output-sink routing).\n * Both members are HOOKS — the package calls them unconditionally on every\n * render of a media element, so implementations must obey the Rules of Hooks\n * and the port object must be referentially stable.\n */\nexport interface PlaybackSessionPort {\n /**\n * Returns a ref callback for the underlying media element (routes audio\n * output / setSinkId on hosts that support it) and forwards to the caller's\n * own element ref.\n */\n useMediaElementSink(\n forward:\n | ((node: unknown) => void)\n | { current: unknown }\n | null\n | undefined,\n ): (node: unknown) => void;\n /** Joins the host's unified playback session for the element. */\n usePlaybackSession(\n registration: PlaybackSessionRegistration,\n elementRef: { current: unknown },\n ): void;\n}\n\n// ---------------------------------------------------------------------------\n// THE SHARE MANDATE (C19) — the complete headless media-actions contract.\n//\n// v0.1 ships this contract plus toolbar/lightbox shells wired to it, typed so\n// the associations/sharing package can plug in WITHOUT a media release: it\n// implements MediaActionsPort (handlers and/or the SharePopover body) and the\n// host injects it through MediaHostPorts. Nothing here may change shape in a\n// breaking way for that wave.\n// ---------------------------------------------------------------------------\n\nexport type MediaActionKind = \"share\" | \"download\" | \"copy\" | \"open\";\n\nexport interface MediaActionContext {\n ref: MediaRefLike;\n resolution: MediaResolution | null;\n fileName?: string | undefined;\n alt?: string | undefined;\n}\n\nexport interface MediaSharePopoverProps {\n context: MediaActionContext;\n onClose: () => void;\n}\n\nexport type MediaActionHandler = (ctx: MediaActionContext) => Promise<void>;\n\nexport interface MediaActionsPort {\n /**\n * Quick-share handler (copy a share link, open the native share sheet…).\n * Implementations obtain URLs ONLY via `MediaClient.shareableUrl` /\n * durable identities — never a signed URL (law 4).\n */\n share?: MediaActionHandler | undefined;\n download?: MediaActionHandler | undefined;\n copy?: MediaActionHandler | undefined;\n open?: MediaActionHandler | undefined;\n /**\n * Rich share UI body — the slot the associations package fills. When\n * present, the shells open it instead of (or on top of) the `share`\n * handler. Receives {@link MediaSharePopoverProps}.\n */\n SharePopover?: MediaSlotComponent<MediaSharePopoverProps> | undefined;\n}\n\nexport interface MediaHostPorts {\n playbackSession?: PlaybackSessionPort | undefined;\n actions?: MediaActionsPort | undefined;\n /** Web host may pass a next/image adapter; default is a plain `<img>`. */\n ImageComponent?: MediaSlotComponent<MediaImageComponentProps> | undefined;\n /**\n * Error-capture sink for terminal media failures (and the ephemeral-secret\n * warning). Bind to the host's error system; absent → loud console.error\n * default. See {@link MediaDiagnosticsPort}.\n */\n diagnostics?: MediaDiagnosticsPort | undefined;\n}\n"],"mappings":";AA0DO,SAAS,eAAe,KAAyB;AACtD,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * @ai-matrx/media — the root types: the MediaClient port and the host ports.\n *\n * The package renders Matrx durable media; it NEVER talks to a network.\n * Everything data-shaped arrives through the injected {@link MediaClient}\n * (canonically implemented by `@ai-matrx/data/files`; any structural\n * implementation works — the strangler step wires it over an app's existing\n * file handler). The five file-handling laws are enforced here at the type\n * level:\n *\n * 1. Never fetch a media URL yourself — the package contains zero network\n * primitives; bytes exist only via {@link MediaClient.getBlob}.\n * 2. Off-boundary code resolves media itself — every component takes a\n * {@link MediaRefLike} and calls `resolve()` unconditionally. There is\n * NO `src` prop on any component in this package.\n * 3. A signed URL is a handoff, never an identity — the only string that\n * reaches a media element is the branded {@link DurableSrc}, which only\n * a MediaClient implementation can mint (via {@link mintDurableSrc}).\n * 4. Public-facing media is persisted public, never signed —\n * {@link MediaClient.shareableUrl} fails closed (`null` → the action\n * renders disabled).\n * 5. One URL contract; the engine builds URLs — zero URL construction in\n * this package; `resolve()` is the only door.\n */\n\n// ---------------------------------------------------------------------------\n// Identity\n// ---------------------------------------------------------------------------\n\n/** The canonical durable media reference. `file_id` is the identity. */\nexport interface MediaRef {\n file_id?: string | null;\n url?: string | null;\n mime_type?: string | null;\n metadata?: Record<string, unknown> | null;\n}\n\n/**\n * Identity in. A raw string is a fileId or an EXTERNAL public URL — never a\n * signed URL (the client's `resolve()` classifies and screams on one).\n */\nexport type MediaRefLike = MediaRef | string;\n\ndeclare const durable: unique symbol;\n\n/**\n * A URL that has passed through `MediaClient.resolve()` — the only string\n * type any media element in this package will bind. A caller cannot pass an\n * expiring URL through the package's types without an explicit unsafe cast.\n */\nexport type DurableSrc = string & { readonly [durable]: true };\n\n/**\n * Brand a URL as durable. FOR MediaClient IMPLEMENTATIONS ONLY — the\n * implementation must classify the URL first (the ONE signed-URL classifier\n * lives with the platform's files layer; never re-implement it here or in a\n * host). Calling this on an unclassified string defeats law 3.\n */\nexport function mintDurableSrc(url: string): DurableSrc {\n return url as DurableSrc;\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\nexport type MediaKind = \"image\" | \"video\" | \"audio\" | \"file\";\n\nexport interface MediaResolution {\n src: DurableSrc;\n /** RN auth lane (`<Image source={{uri, headers}}>`); absent on web (cookie session). */\n headers?: Record<string, string> | undefined;\n mimeType?: string | undefined;\n kind: MediaKind;\n /**\n * How pixels must travel. `\"element\"` (default): bind `src` directly to the\n * media element. `\"blob\"`: the bytes require bearer auth the element cannot\n * attach — consumers go through {@link MediaClient.getBlob} and render the\n * returned object URL (the private-image lane).\n */\n transport?: \"element\" | \"blob\" | undefined;\n /**\n * A server-rendered thumbnail for this file, when the client knows one\n * (Matrx: `FileRecord.thumbnail_url` / asset variants). Consumed by the\n * thumbnail tier machine.\n */\n thumbnailSrc?: DurableSrc | undefined;\n /**\n * True when this is a permanent public CDN URL — lets a host-supplied\n * `ImageComponent` (e.g. a next/image adapter) take the render, and marks\n * the URL CORS-safe for element-level `crossOrigin`.\n */\n isCdn?: boolean | undefined;\n /**\n * Whether a load error can be recovered by the client's ONE retry policy\n * (session-refresh → same-URL retry). False for foreign/external URLs —\n * a session refresh can't resurrect someone else's link rot. Default true.\n */\n recoverable?: boolean | undefined;\n}\n\n/** Typed unavailable states — render-as-state, never thrown as incidents. */\nexport type MediaUnavailableReason =\n | \"access_denied\"\n | \"not_found\"\n | \"deleted\"\n | \"unknown\";\n\n// ---------------------------------------------------------------------------\n// Diagnostics — the terminal-failure capture contract (the 2026-08-30 outage\n// class: every private image failed, the honest unavailable panel rendered,\n// and NOTHING reached the host's error system because the package had no\n// diagnostics port). Per C22 the capture logic lives IN the package; a host\n// binds only a sink. Absent sink → the package console.errors the same\n// structured payload once per failure — a media failure is NEVER silent.\n// ---------------------------------------------------------------------------\n\n/** Where in the pipeline a media failure surfaced. */\nexport type MediaFailurePhase =\n /** `MediaClient.resolve()` refused the ref (threw). */\n | \"resolve\"\n /** The client's session bootstrap/mint failed (`recoverLoadError` rejected),\n * or the server reported an ephemeral cookie secret (warning). */\n | \"session\"\n /** `MediaClient.getBlob()` rejected — private-pixels bytes never arrived. */\n | \"blob\"\n /** The media element errored and recovery was exhausted or impossible. */\n | \"element-load\"\n /** The client's recovery machinery declined to retry on the first attempt. */\n | \"recovery\";\n\n/** What the ONE retry policy did before the failure went terminal. */\nexport type MediaRetryOutcome =\n /** Recovery was impossible (unrecoverable source) or was never reached. */\n | \"not-attempted\"\n /** The client granted a session-refresh retry and the retry ALSO failed. */\n | \"retried\"\n /** The recovery call itself threw/rejected. */\n | \"recovery-failed\";\n\n/**\n * The strictly-typed structural payload every terminal media failure emits.\n * `mediaRef` is DURABLE IDENTITY ONLY — a `file:<id>` / query-stripped URL\n * key, NEVER a signed URL (signatures live in query strings, which\n * {@link failureMediaIdentity} removes).\n */\nexport interface MediaFailureInfo {\n source: \"media\";\n phase: MediaFailurePhase;\n /** File id / ref key identity — never a signed URL. */\n mediaRef: string;\n /** HTTP status when known (e.g. from a structured client error). */\n status?: number | undefined;\n /** Outcome of the client's ONE retry policy, when the phase reached it. */\n retryOutcome?: MediaRetryOutcome | undefined;\n /** True for a dead render; false for a captured warning (ephemeral secret). */\n terminal: boolean;\n message: string;\n}\n\n/**\n * The host's error-capture sink. Bind `capture` to the app's error system\n * (matrx-frontend: `captureError` in lib/diagnostics/errorCaptureStore).\n * When absent, the package console.errors the same payload once per failure\n * (the loud default) — it never downgrades to silence.\n */\nexport interface MediaDiagnosticsPort {\n capture?: ((info: MediaFailureInfo) => void) | undefined;\n}\n\n/** String verdicts of the ONE retry policy. */\nexport type MediaRecoveryVerdict = \"retry\" | \"terminal\";\n\n/**\n * Structured `recoverLoadError` result — optional; the bare string verdict\n * remains fully supported. Lets the client surface what it learned during the\n * session bootstrap without a second channel:\n *\n * - `status` — HTTP status of the failed mint/probe, forwarded into the\n * terminal {@link MediaFailureInfo};\n * - `secretSource` — the `/files/session` mint response's `secret_source`\n * when the server sends one (a matrx-connect field). `\"ephemeral\"` means\n * the server signs cookies with a boot-generated secret: private renders\n * WILL break across restarts/instances, and the package captures a\n * non-terminal warning immediately instead of waiting for the outage.\n */\nexport interface MediaRecoveryReport {\n verdict: MediaRecoveryVerdict;\n status?: number | undefined;\n secretSource?: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Bytes / upload\n// ---------------------------------------------------------------------------\n\nexport interface MediaBlobHandle {\n /** `blob:` object URL safe for any browser API. */\n url: string;\n blob: Blob;\n /** Release the handle. Implementations backed by a shared cache may no-op. */\n release(): void;\n}\n\nexport interface MediaUploadOptions {\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n fileName?: string | undefined;\n mimeType?: string | undefined;\n /** Host visibility vocabulary passes through structurally. */\n visibility?: string | undefined;\n parentFolderId?: string | null | undefined;\n metadata?: Record<string, unknown> | undefined;\n}\n\n/** Upload → durable identity. `fileId` is required at compile time and runtime. */\nexport interface MediaUploadResult {\n fileId: string;\n ref: MediaRef;\n}\n\nexport interface MediaMultiUploadResult {\n /** File ids for every successful upload. */\n uploaded: string[];\n /** Per-file failure with the real backend error message. */\n failed: Array<{ name: string; error: string }>;\n /** True when the host's pre-flight (e.g. a duplicate dialog) cancelled the batch. */\n cancelled: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// THE port — one structural interface, owned by this package, implemented by\n// the data layer (canonically `@ai-matrx/data/files`).\n// ---------------------------------------------------------------------------\n\nexport interface MediaClient {\n /**\n * Synchronous durable-URL build + classification. Returns `null` when the\n * ref carries no usable identity (components render their fallback).\n * THROWS (and screams via its own diagnostics sink) on a signed/expiring\n * URL — the D108 guard lives behind this door.\n */\n resolve(ref: MediaRefLike): MediaResolution | null;\n /** Bytes for canvas/crossOrigin/private-pixel consumers. */\n getBlob(ref: MediaRefLike): Promise<MediaBlobHandle>;\n /**\n * Session bootstrap + the ONE retry policy: refresh-then-same-URL-retry,\n * second failure terminal. Called by the package's onError path, never\n * re-implemented.\n */\n recoverLoadError(\n src: DurableSrc,\n attempt: number,\n ): Promise<MediaRecoveryVerdict | MediaRecoveryReport>;\n /**\n * Optional object-URL recovery policy for private blob-backed media.\n * The client decides whether the durable ref should be re-read; older\n * clients may omit this and fail terminally.\n */\n recoverBlobLoadError?(\n ref: MediaRefLike,\n attempt: number,\n ): Promise<MediaRecoveryVerdict>;\n /** Upload → durable identity. */\n upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;\n /**\n * Optional batch upload with host pre-flight (dedup dialogs etc.). When\n * absent, `useMediaUpload().uploadMany` falls back to sequential `upload`.\n */\n uploadMany?(files: File[], opts?: MediaUploadOptions): Promise<MediaMultiUploadResult>;\n /** Share affordances — fails closed (null), never emits a signed URL. */\n shareableUrl(ref: MediaRefLike): Promise<string | null>;\n /** Typed unavailable states — render-as-state, never thrown as incidents. */\n classifyError(err: unknown): MediaUnavailableReason;\n}\n\n// ---------------------------------------------------------------------------\n// Host ports — all optional, all defaulting to inert.\n// ---------------------------------------------------------------------------\n\n/**\n * Structural component slot. `(props) => unknown` keeps the root entry free\n * of React type imports; any React function component satisfies it.\n */\nexport type MediaSlotComponent<P> = (props: P) => unknown;\n\nexport interface MediaImageComponentProps {\n src: string;\n alt: string;\n width: number;\n height: number;\n className?: string | undefined;\n onError?: ((event: unknown) => void) | undefined;\n onClick?: ((event: unknown) => void) | undefined;\n onKeyDown?: ((event: unknown) => void) | undefined;\n role?: string | undefined;\n tabIndex?: number | undefined;\n}\n\nexport interface PlaybackSessionRegistration {\n isPlaying: boolean;\n source: string;\n label: string;\n trackKey?: string | undefined;\n}\n\n/**\n * The host's audio system (exclusive-playback lock, output-sink routing).\n * Both members are HOOKS — the package calls them unconditionally on every\n * render of a media element, so implementations must obey the Rules of Hooks\n * and the port object must be referentially stable.\n */\nexport interface PlaybackSessionPort {\n /**\n * Returns a ref callback for the underlying media element (routes audio\n * output / setSinkId on hosts that support it) and forwards to the caller's\n * own element ref.\n */\n useMediaElementSink(\n forward:\n | ((node: unknown) => void)\n | { current: unknown }\n | null\n | undefined,\n ): (node: unknown) => void;\n /** Joins the host's unified playback session for the element. */\n usePlaybackSession(\n registration: PlaybackSessionRegistration,\n elementRef: { current: unknown },\n ): void;\n}\n\n// ---------------------------------------------------------------------------\n// THE SHARE MANDATE (C19) — the complete headless media-actions contract.\n//\n// v0.1 ships this contract plus toolbar/lightbox shells wired to it, typed so\n// the associations/sharing package can plug in WITHOUT a media release: it\n// implements MediaActionsPort (handlers and/or the SharePopover body) and the\n// host injects it through MediaHostPorts. Nothing here may change shape in a\n// breaking way for that wave.\n// ---------------------------------------------------------------------------\n\nexport type MediaActionKind = \"share\" | \"download\" | \"copy\" | \"open\";\n\nexport interface MediaActionContext {\n ref: MediaRefLike;\n resolution: MediaResolution | null;\n fileName?: string | undefined;\n alt?: string | undefined;\n}\n\nexport interface MediaSharePopoverProps {\n context: MediaActionContext;\n onClose: () => void;\n}\n\nexport type MediaActionHandler = (ctx: MediaActionContext) => Promise<void>;\n\nexport interface MediaActionsPort {\n /**\n * Quick-share handler (copy a share link, open the native share sheet…).\n * Implementations obtain URLs ONLY via `MediaClient.shareableUrl` /\n * durable identities — never a signed URL (law 4).\n */\n share?: MediaActionHandler | undefined;\n download?: MediaActionHandler | undefined;\n copy?: MediaActionHandler | undefined;\n open?: MediaActionHandler | undefined;\n /**\n * Rich share UI body — the slot the associations package fills. When\n * present, the shells open it instead of (or on top of) the `share`\n * handler. Receives {@link MediaSharePopoverProps}.\n */\n SharePopover?: MediaSlotComponent<MediaSharePopoverProps> | undefined;\n}\n\nexport interface MediaHostPorts {\n playbackSession?: PlaybackSessionPort | undefined;\n actions?: MediaActionsPort | undefined;\n /** Web host may pass a next/image adapter; default is a plain `<img>`. */\n ImageComponent?: MediaSlotComponent<MediaImageComponentProps> | undefined;\n /**\n * Error-capture sink for terminal media failures (and the ephemeral-secret\n * warning). Bind to the host's error system; absent → loud console.error\n * default. See {@link MediaDiagnosticsPort}.\n */\n diagnostics?: MediaDiagnosticsPort | undefined;\n}\n"],"mappings":";AA0DO,SAAS,eAAe,KAAyB;AACtD,SAAO;AACT;","names":[]}
package/dist/react.cjs CHANGED
@@ -535,7 +535,7 @@ function FileIcon2({
535
535
  }
536
536
 
537
537
  // src/react/inline-media-ref.tsx
538
- var import_react6 = require("react");
538
+ var import_react7 = require("react");
539
539
 
540
540
  // src/core/context.ts
541
541
  var import_react = require("react");
@@ -681,8 +681,83 @@ function useMediaBlob(ref) {
681
681
  };
682
682
  }
683
683
 
684
- // src/core/use-media-load-recovery.ts
684
+ // src/core/use-media-blob-load-recovery.ts
685
685
  var import_react4 = require("react");
686
+ function useMediaBlobLoadRecovery(ref, source, options) {
687
+ const client = useMediaClient();
688
+ const capture = useMediaFailureCapture();
689
+ const refKey = stableRefKey(ref);
690
+ const [failed, setFailed] = (0, import_react4.useState)(false);
691
+ const attemptRef = (0, import_react4.useRef)(0);
692
+ const recoveryInFlightRef = (0, import_react4.useRef)(false);
693
+ const onRetry = options.onRetry;
694
+ const onTerminal = options.onTerminal;
695
+ (0, import_react4.useEffect)(() => {
696
+ attemptRef.current = 0;
697
+ recoveryInFlightRef.current = false;
698
+ setFailed(false);
699
+ }, [refKey]);
700
+ (0, import_react4.useEffect)(() => {
701
+ recoveryInFlightRef.current = false;
702
+ }, [source]);
703
+ const finishTerminal = (0, import_react4.useCallback)(
704
+ (event, attempt, message) => {
705
+ setFailed(true);
706
+ capture({
707
+ phase: "element-load",
708
+ mediaRef: failureMediaIdentity(ref ?? null, source),
709
+ terminal: true,
710
+ retryOutcome: attempt > 1 ? "retried" : "not-attempted",
711
+ message
712
+ });
713
+ onTerminal?.(event);
714
+ },
715
+ [capture, onTerminal, ref, source]
716
+ );
717
+ const onLoadError = (0, import_react4.useCallback)(
718
+ (event) => {
719
+ if (recoveryInFlightRef.current || !source) return;
720
+ recoveryInFlightRef.current = true;
721
+ attemptRef.current += 1;
722
+ const attempt = attemptRef.current;
723
+ const recover = client.recoverBlobLoadError;
724
+ if (!ref || !recover) {
725
+ finishTerminal(
726
+ event,
727
+ attempt,
728
+ "blob-backed media element failed to load and the client exposes no object-URL recovery policy"
729
+ );
730
+ return;
731
+ }
732
+ recover(ref, attempt).then(
733
+ (verdict) => {
734
+ if (verdict === "retry") {
735
+ setFailed(false);
736
+ onRetry();
737
+ return;
738
+ }
739
+ finishTerminal(
740
+ event,
741
+ attempt,
742
+ attempt > 1 ? "blob-backed media element failed after the client-approved re-read" : "blob-backed media recovery was declined on the first attempt"
743
+ );
744
+ },
745
+ (err) => {
746
+ finishTerminal(
747
+ event,
748
+ attempt,
749
+ `blob-backed media recovery failed: ${err instanceof Error ? err.message : String(err ?? "unknown error")}`
750
+ );
751
+ }
752
+ );
753
+ },
754
+ [client, finishTerminal, onRetry, ref, source]
755
+ );
756
+ return { onLoadError, failed };
757
+ }
758
+
759
+ // src/core/use-media-load-recovery.ts
760
+ var import_react5 = require("react");
686
761
 
687
762
  // src/types.ts
688
763
  function mintDurableSrc(url) {
@@ -700,14 +775,14 @@ function useMediaLoadRecovery(src, options) {
700
775
  const recoverable = options?.recoverable !== false;
701
776
  const onTerminal = options?.onTerminal;
702
777
  const failureRef = options?.failureRef;
703
- const [result, setResult] = (0, import_react4.useState)({ source: src, retryKey: 0, failed: false });
704
- const attemptRef = (0, import_react4.useRef)(0);
705
- const recoveryInFlightRef = (0, import_react4.useRef)(false);
706
- (0, import_react4.useEffect)(() => {
778
+ const [result, setResult] = (0, import_react5.useState)({ source: src, retryKey: 0, failed: false });
779
+ const attemptRef = (0, import_react5.useRef)(0);
780
+ const recoveryInFlightRef = (0, import_react5.useRef)(false);
781
+ (0, import_react5.useEffect)(() => {
707
782
  attemptRef.current = 0;
708
783
  recoveryInFlightRef.current = false;
709
784
  }, [src]);
710
- (0, import_react4.useLayoutEffect)(() => {
785
+ (0, import_react5.useLayoutEffect)(() => {
711
786
  recoveryInFlightRef.current = false;
712
787
  }, [result.retryKey]);
713
788
  const identity = () => failureMediaIdentity(failureRef ?? null, src);
@@ -781,12 +856,12 @@ function useMediaLoadRecovery(src, options) {
781
856
  }
782
857
 
783
858
  // src/core/use-media-resolution.ts
784
- var import_react5 = require("react");
859
+ var import_react6 = require("react");
785
860
  function useMediaResolution(ref) {
786
861
  const client = useMediaClient();
787
862
  const capture = useMediaFailureCapture();
788
863
  const refKey = stableRefKey(ref);
789
- const { state, failureMessage } = (0, import_react5.useMemo)(() => {
864
+ const { state, failureMessage } = (0, import_react6.useMemo)(() => {
790
865
  if (!ref) {
791
866
  return {
792
867
  state: { resolution: null, status: "empty", reason: null },
@@ -816,7 +891,7 @@ function useMediaResolution(ref) {
816
891
  };
817
892
  }
818
893
  }, [client, refKey]);
819
- (0, import_react5.useEffect)(() => {
894
+ (0, import_react6.useEffect)(() => {
820
895
  if (state.status !== "unavailable") return;
821
896
  capture({
822
897
  phase: "resolve",
@@ -871,7 +946,7 @@ function prettifyUrl(url) {
871
946
  }
872
947
  var INERT_PLAYBACK = {
873
948
  useMediaElementSink(forward) {
874
- return (0, import_react6.useCallback)(
949
+ return (0, import_react7.useCallback)(
875
950
  (node) => {
876
951
  if (!forward) return;
877
952
  if (typeof forward === "function") forward(node);
@@ -922,7 +997,7 @@ function InformativeErrorFallback({
922
997
  }) {
923
998
  const capture = useMediaFailureCapture();
924
999
  const identity = failureMediaIdentity(mediaRef ?? null, url);
925
- (0, import_react6.useEffect)(() => {
1000
+ (0, import_react7.useEffect)(() => {
926
1001
  capture({
927
1002
  phase: failurePhase,
928
1003
  mediaRef: identity,
@@ -986,10 +1061,10 @@ function InformativeErrorPanel({
986
1061
  icon,
987
1062
  tooltip
988
1063
  }) {
989
- const [copied, setCopied] = (0, import_react6.useState)(false);
1064
+ const [copied, setCopied] = (0, import_react7.useState)(false);
990
1065
  const isFill = dimensions === "fill";
991
1066
  const { host, tail } = prettifyUrl(url);
992
- const handleCopy = (0, import_react6.useCallback)(
1067
+ const handleCopy = (0, import_react7.useCallback)(
993
1068
  (event) => {
994
1069
  event.stopPropagation();
995
1070
  event.preventDefault();
@@ -1106,16 +1181,25 @@ function InlineMediaRef({
1106
1181
  );
1107
1182
  const blob = useMediaBlob(needsBlob ? mediaRef : null);
1108
1183
  const url = needsBlob ? blob.url : resolution?.src ?? null;
1184
+ const terminalCallback = (event) => onError?.(
1185
+ event
1186
+ );
1109
1187
  const recovery = useMediaLoadRecovery(url, {
1110
- recoverable: !needsBlob && resolution?.recoverable !== false,
1188
+ recoverable: resolution?.recoverable !== false,
1111
1189
  failureRef: mediaRef,
1112
- onTerminal: (event) => onError?.(
1113
- event
1114
- )
1190
+ onTerminal: terminalCallback
1115
1191
  });
1192
+ const blobRecovery = useMediaBlobLoadRecovery(
1193
+ needsBlob ? mediaRef : null,
1194
+ needsBlob ? url : null,
1195
+ {
1196
+ onRetry: blob.retry,
1197
+ onTerminal: terminalCallback
1198
+ }
1199
+ );
1116
1200
  const playback = ports.playbackSession ?? INERT_PLAYBACK;
1117
- const sessionElRef = (0, import_react6.useRef)(null);
1118
- const [isMediaPlaying, setIsMediaPlaying] = (0, import_react6.useState)(false);
1201
+ const sessionElRef = (0, import_react7.useRef)(null);
1202
+ const [isMediaPlaying, setIsMediaPlaying] = (0, import_react7.useState)(false);
1119
1203
  const portSinkRef = playback.useMediaElementSink(
1120
1204
  mediaElementRef
1121
1205
  );
@@ -1123,7 +1207,7 @@ function InlineMediaRef({
1123
1207
  sessionElRef.current = node;
1124
1208
  portSinkRef(node);
1125
1209
  };
1126
- const trackKey = (0, import_react6.useMemo)(() => {
1210
+ const trackKey = (0, import_react7.useMemo)(() => {
1127
1211
  if (mediaRef && typeof mediaRef === "object" && mediaRef.file_id) {
1128
1212
  return mediaRef.file_id;
1129
1213
  }
@@ -1146,8 +1230,10 @@ function InlineMediaRef({
1146
1230
  const mediaResolutionError = status === "unavailable" ? reason ?? "unknown" : blob.error ?? null;
1147
1231
  const dimensions = resolveDimensions(size);
1148
1232
  const isFill = dimensions === "fill";
1149
- const hasLoadError = recovery.failed;
1150
- const handleLoadError = recovery.onLoadError;
1233
+ const activeRecovery = needsBlob ? blobRecovery : recovery;
1234
+ const retryKey = needsBlob ? 0 : recovery.retryKey;
1235
+ const hasLoadError = activeRecovery.failed;
1236
+ const handleLoadError = activeRecovery.onLoadError;
1151
1237
  const defaultIcon = elementType === "video" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FileVideoIcon, { className: "h-1/2 w-1/2" }) : elementType === "audio" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FileAudioIcon, { className: "h-1/2 w-1/2" }) : mediaRef ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ImageIcon, { className: "h-1/2 w-1/2" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FileIcon, { className: "h-1/2 w-1/2" });
1152
1238
  const unresolvedDisplayUrl = mediaRef && typeof mediaRef === "object" && mediaRef.file_id ? `file:${mediaRef.file_id}` : "unresolved-media";
1153
1239
  if (!url) {
@@ -1283,7 +1369,7 @@ function InlineMediaRef({
1283
1369
  ...mediaSessionEvents,
1284
1370
  ...interactiveProps
1285
1371
  },
1286
- recovery.retryKey
1372
+ retryKey
1287
1373
  );
1288
1374
  }
1289
1375
  if (elementType === "audio") {
@@ -1304,7 +1390,7 @@ function InlineMediaRef({
1304
1390
  crossOrigin,
1305
1391
  ...interactiveProps
1306
1392
  },
1307
- recovery.retryKey
1393
+ retryKey
1308
1394
  );
1309
1395
  }
1310
1396
  const needsPlainImg = !!onLoad || !!mediaElementRef || !!crossOrigin;
@@ -1322,7 +1408,7 @@ function InlineMediaRef({
1322
1408
  onError: handleLoadError,
1323
1409
  ...interactiveProps
1324
1410
  },
1325
- recovery.retryKey
1411
+ retryKey
1326
1412
  );
1327
1413
  }
1328
1414
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -1338,20 +1424,20 @@ function InlineMediaRef({
1338
1424
  crossOrigin,
1339
1425
  ...interactiveProps
1340
1426
  },
1341
- recovery.retryKey
1427
+ retryKey
1342
1428
  );
1343
1429
  }
1344
1430
 
1345
1431
  // src/react/media-thumbnail.tsx
1346
- var import_react8 = require("react");
1432
+ var import_react9 = require("react");
1347
1433
 
1348
1434
  // src/core/use-thumbnail-source.ts
1349
- var import_react7 = require("react");
1435
+ var import_react8 = require("react");
1350
1436
  function useThumbnailSource(ref, options = {}) {
1351
1437
  const { thumbnailUrl, allowSourceFallback = true } = options;
1352
1438
  const client = useMediaClient();
1353
1439
  const { resolution } = useMediaResolution(ref);
1354
- const explicitThumb = (0, import_react7.useMemo)(() => {
1440
+ const explicitThumb = (0, import_react8.useMemo)(() => {
1355
1441
  if (!thumbnailUrl) return null;
1356
1442
  try {
1357
1443
  return client.resolve(thumbnailUrl);
@@ -1501,8 +1587,8 @@ function VideoPosterThumb({
1501
1587
  recoverable,
1502
1588
  failureRef: mediaRef
1503
1589
  });
1504
- const ref = (0, import_react8.useRef)(null);
1505
- (0, import_react8.useEffect)(() => {
1590
+ const ref = (0, import_react9.useRef)(null);
1591
+ (0, import_react9.useEffect)(() => {
1506
1592
  const v = ref.current;
1507
1593
  if (!v) return void 0;
1508
1594
  const onMeta = () => {
@@ -1531,7 +1617,7 @@ function VideoPosterThumb({
1531
1617
  }
1532
1618
 
1533
1619
  // src/react/use-media-dropzone.ts
1534
- var import_react9 = require("react");
1620
+ var import_react10 = require("react");
1535
1621
  function matchesAccept(file, accept) {
1536
1622
  if (!accept || accept.length === 0) return true;
1537
1623
  const name = file.name.toLowerCase();
@@ -1555,16 +1641,16 @@ function useMediaDropzone(options) {
1555
1641
  disabled = false,
1556
1642
  multiple = true
1557
1643
  } = options;
1558
- const [isDragActive, setIsDragActive] = (0, import_react9.useState)(false);
1559
- const [pasteHighlight, setPasteHighlight] = (0, import_react9.useState)(false);
1560
- const dragDepth = (0, import_react9.useRef)(0);
1561
- const inputRef = (0, import_react9.useRef)(null);
1562
- const onFilesRef = (0, import_react9.useRef)(onFiles);
1644
+ const [isDragActive, setIsDragActive] = (0, import_react10.useState)(false);
1645
+ const [pasteHighlight, setPasteHighlight] = (0, import_react10.useState)(false);
1646
+ const dragDepth = (0, import_react10.useRef)(0);
1647
+ const inputRef = (0, import_react10.useRef)(null);
1648
+ const onFilesRef = (0, import_react10.useRef)(onFiles);
1563
1649
  onFilesRef.current = onFiles;
1564
- const onRejectedRef = (0, import_react9.useRef)(onRejected);
1650
+ const onRejectedRef = (0, import_react10.useRef)(onRejected);
1565
1651
  onRejectedRef.current = onRejected;
1566
1652
  const acceptKey = accept?.join(",") ?? "";
1567
- const filterFiles = (0, import_react9.useCallback)(
1653
+ const filterFiles = (0, import_react10.useCallback)(
1568
1654
  (files) => {
1569
1655
  const accepted = [];
1570
1656
  const rejected = [];
@@ -1582,23 +1668,23 @@ function useMediaDropzone(options) {
1582
1668
  },
1583
1669
  [acceptKey, maxSize]
1584
1670
  );
1585
- const emit = (0, import_react9.useCallback)(
1671
+ const emit = (0, import_react10.useCallback)(
1586
1672
  (files) => {
1587
1673
  const accepted = filterFiles(files);
1588
1674
  if (accepted.length) onFilesRef.current(accepted);
1589
1675
  },
1590
1676
  [filterFiles]
1591
1677
  );
1592
- const openPicker = (0, import_react9.useCallback)(() => {
1678
+ const openPicker = (0, import_react10.useCallback)(() => {
1593
1679
  if (!disabled) inputRef.current?.click();
1594
1680
  }, [disabled]);
1595
- (0, import_react9.useEffect)(() => {
1681
+ (0, import_react10.useEffect)(() => {
1596
1682
  if (!pickerEventName || typeof window === "undefined") return void 0;
1597
1683
  const handler = () => openPicker();
1598
1684
  window.addEventListener(pickerEventName, handler);
1599
1685
  return () => window.removeEventListener(pickerEventName, handler);
1600
1686
  }, [openPicker, pickerEventName]);
1601
- (0, import_react9.useEffect)(() => {
1687
+ (0, import_react10.useEffect)(() => {
1602
1688
  if (!enablePaste || disabled || typeof window === "undefined") {
1603
1689
  return void 0;
1604
1690
  }
@@ -1624,7 +1710,7 @@ function useMediaDropzone(options) {
1624
1710
  window.addEventListener("paste", handler);
1625
1711
  return () => window.removeEventListener("paste", handler);
1626
1712
  }, [enablePaste, disabled, emit]);
1627
- const getRootProps = (0, import_react9.useCallback)(
1713
+ const getRootProps = (0, import_react10.useCallback)(
1628
1714
  () => ({
1629
1715
  onDragEnter: (event) => {
1630
1716
  if (disabled) return;
@@ -1653,11 +1739,11 @@ function useMediaDropzone(options) {
1653
1739
  }),
1654
1740
  [disabled, emit, multiple]
1655
1741
  );
1656
- const inputAccept = (0, import_react9.useMemo)(
1742
+ const inputAccept = (0, import_react10.useMemo)(
1657
1743
  () => acceptKey.length ? acceptKey : void 0,
1658
1744
  [acceptKey]
1659
1745
  );
1660
- const getInputProps = (0, import_react9.useCallback)(
1746
+ const getInputProps = (0, import_react10.useCallback)(
1661
1747
  () => ({
1662
1748
  ref: (node) => {
1663
1749
  inputRef.current = node;
@@ -1679,20 +1765,20 @@ function useMediaDropzone(options) {
1679
1765
  }
1680
1766
 
1681
1767
  // src/react/file-upload-dropzone.tsx
1682
- var import_react12 = require("react");
1768
+ var import_react13 = require("react");
1683
1769
 
1684
1770
  // src/core/use-media-upload.ts
1685
- var import_react10 = require("react");
1771
+ var import_react11 = require("react");
1686
1772
  var entrySeq = 0;
1687
1773
  function useMediaUpload() {
1688
1774
  const client = useMediaClient();
1689
- const [uploading, setUploading] = (0, import_react10.useState)(false);
1690
- const [progress, setProgress] = (0, import_react10.useState)(null);
1691
- const [result, setResult] = (0, import_react10.useState)(null);
1692
- const [error, setError] = (0, import_react10.useState)(null);
1693
- const [entries, setEntries] = (0, import_react10.useState)([]);
1694
- const activeCount = (0, import_react10.useRef)(0);
1695
- const patchEntry = (0, import_react10.useCallback)(
1775
+ const [uploading, setUploading] = (0, import_react11.useState)(false);
1776
+ const [progress, setProgress] = (0, import_react11.useState)(null);
1777
+ const [result, setResult] = (0, import_react11.useState)(null);
1778
+ const [error, setError] = (0, import_react11.useState)(null);
1779
+ const [entries, setEntries] = (0, import_react11.useState)([]);
1780
+ const activeCount = (0, import_react11.useRef)(0);
1781
+ const patchEntry = (0, import_react11.useCallback)(
1696
1782
  (id, patch) => {
1697
1783
  setEntries(
1698
1784
  (current) => current.map((e) => e.id === id ? { ...e, ...patch } : e)
@@ -1700,7 +1786,7 @@ function useMediaUpload() {
1700
1786
  },
1701
1787
  []
1702
1788
  );
1703
- const upload = (0, import_react10.useCallback)(
1789
+ const upload = (0, import_react11.useCallback)(
1704
1790
  async (file, opts = {}) => {
1705
1791
  const id = `up-${++entrySeq}`;
1706
1792
  const fileName = opts.fileName ?? (file instanceof File ? file.name : "file");
@@ -1754,7 +1840,7 @@ function useMediaUpload() {
1754
1840
  },
1755
1841
  [client, patchEntry]
1756
1842
  );
1757
- const uploadMany = (0, import_react10.useCallback)(
1843
+ const uploadMany = (0, import_react11.useCallback)(
1758
1844
  async (files, opts = {}) => {
1759
1845
  if (client.uploadMany) {
1760
1846
  activeCount.current += 1;
@@ -1783,10 +1869,10 @@ function useMediaUpload() {
1783
1869
  },
1784
1870
  [client, upload]
1785
1871
  );
1786
- const clearEntry = (0, import_react10.useCallback)((id) => {
1872
+ const clearEntry = (0, import_react11.useCallback)((id) => {
1787
1873
  setEntries((current) => current.filter((e) => e.id !== id));
1788
1874
  }, []);
1789
- const reset = (0, import_react10.useCallback)(() => {
1875
+ const reset = (0, import_react11.useCallback)(() => {
1790
1876
  setResult(null);
1791
1877
  setError(null);
1792
1878
  setProgress(null);
@@ -1806,7 +1892,7 @@ function useMediaUpload() {
1806
1892
  }
1807
1893
 
1808
1894
  // src/react/upload-progress-list.tsx
1809
- var import_react11 = require("react");
1895
+ var import_react12 = require("react");
1810
1896
  var import_jsx_runtime5 = require("react/jsx-runtime");
1811
1897
  var AUTO_DISMISS_MS = 1e4;
1812
1898
  function formatFileSize(bytes) {
@@ -1827,16 +1913,16 @@ function UploadProgressListImpl({
1827
1913
  onOpenFile,
1828
1914
  className
1829
1915
  }) {
1830
- const [hovered, setHovered] = (0, import_react11.useState)(false);
1916
+ const [hovered, setHovered] = (0, import_react12.useState)(false);
1831
1917
  const activeCount = uploads.filter((u) => u.status === "uploading").length;
1832
1918
  const successCount = uploads.filter((u) => u.status === "success").length;
1833
1919
  const errorCount = uploads.filter((u) => u.status === "error").length;
1834
- const timersRef = (0, import_react11.useRef)(
1920
+ const timersRef = (0, import_react12.useRef)(
1835
1921
  /* @__PURE__ */ new Map()
1836
1922
  );
1837
- const onDismissRef = (0, import_react11.useRef)(onDismiss);
1923
+ const onDismissRef = (0, import_react12.useRef)(onDismiss);
1838
1924
  onDismissRef.current = onDismiss;
1839
- (0, import_react11.useEffect)(() => {
1925
+ (0, import_react12.useEffect)(() => {
1840
1926
  const timers = timersRef.current;
1841
1927
  if (hovered) {
1842
1928
  timers.forEach((t) => clearTimeout(t));
@@ -1864,7 +1950,7 @@ function UploadProgressListImpl({
1864
1950
  }
1865
1951
  }
1866
1952
  }, [uploads, hovered]);
1867
- (0, import_react11.useEffect)(() => {
1953
+ (0, import_react12.useEffect)(() => {
1868
1954
  const timers = timersRef.current;
1869
1955
  return () => {
1870
1956
  timers.forEach((t) => clearTimeout(t));
@@ -1976,7 +2062,7 @@ function UploadProgressListImpl({
1976
2062
  }
1977
2063
  );
1978
2064
  }
1979
- var UploadProgressList = (0, import_react11.memo)(UploadProgressListImpl);
2065
+ var UploadProgressList = (0, import_react12.memo)(UploadProgressListImpl);
1980
2066
 
1981
2067
  // src/react/file-upload-dropzone.tsx
1982
2068
  var import_jsx_runtime6 = require("react/jsx-runtime");
@@ -1996,7 +2082,7 @@ function FileUploadDropzone({
1996
2082
  pickerEventName
1997
2083
  }) {
1998
2084
  const { uploadMany, entries, clearEntry } = useMediaUpload();
1999
- const handleUpload = (0, import_react12.useCallback)(
2085
+ const handleUpload = (0, import_react13.useCallback)(
2000
2086
  async (files) => {
2001
2087
  if (!files.length) return;
2002
2088
  try {
@@ -2103,22 +2189,22 @@ function FileUploadDropzone({
2103
2189
  }
2104
2190
 
2105
2191
  // src/react/media-action-toolbar.tsx
2106
- var import_react14 = require("react");
2192
+ var import_react15 = require("react");
2107
2193
 
2108
2194
  // src/core/use-media-actions.ts
2109
- var import_react13 = require("react");
2195
+ var import_react14 = require("react");
2110
2196
  function useMediaActions(ref, options = {}) {
2111
2197
  const ports = useMediaHostPorts();
2112
2198
  const { resolution } = useMediaResolution(ref);
2113
- const [busy, setBusy] = (0, import_react13.useState)(null);
2114
- const [lastError, setLastError] = (0, import_react13.useState)(null);
2199
+ const [busy, setBusy] = (0, import_react14.useState)(null);
2200
+ const [lastError, setLastError] = (0, import_react14.useState)(null);
2115
2201
  const actions = ports.actions;
2116
2202
  const { fileName, alt } = options;
2117
- const context = (0, import_react13.useMemo)(() => {
2203
+ const context = (0, import_react14.useMemo)(() => {
2118
2204
  if (!ref) return null;
2119
2205
  return { ref, resolution, fileName, alt };
2120
2206
  }, [ref, resolution, fileName, alt]);
2121
- const isAvailable = (0, import_react13.useCallback)(
2207
+ const isAvailable = (0, import_react14.useCallback)(
2122
2208
  (kind) => {
2123
2209
  if (!context || !actions) return false;
2124
2210
  if (kind === "share") return Boolean(actions.share || actions.SharePopover);
@@ -2126,7 +2212,7 @@ function useMediaActions(ref, options = {}) {
2126
2212
  },
2127
2213
  [context, actions]
2128
2214
  );
2129
- const run = (0, import_react13.useCallback)(
2215
+ const run = (0, import_react14.useCallback)(
2130
2216
  async (kind) => {
2131
2217
  if (!context || !actions) return;
2132
2218
  const handler = actions[kind];
@@ -2175,7 +2261,7 @@ function MediaActionToolbar({
2175
2261
  buttonClassName
2176
2262
  }) {
2177
2263
  const state = useMediaActions(mediaRef, { fileName, alt });
2178
- const [shareOpen, setShareOpen] = (0, import_react14.useState)(false);
2264
+ const [shareOpen, setShareOpen] = (0, import_react15.useState)(false);
2179
2265
  const SharePopover = state.SharePopover;
2180
2266
  const visible = hideUnavailable ? actions.filter((kind) => state.isAvailable(kind)) : actions;
2181
2267
  if (visible.length === 0) return null;
@@ -2232,7 +2318,7 @@ function MediaActionToolbar({
2232
2318
  }
2233
2319
 
2234
2320
  // src/react/media-lightbox.tsx
2235
- var import_react15 = require("react");
2321
+ var import_react16 = require("react");
2236
2322
  var import_jsx_runtime8 = require("react/jsx-runtime");
2237
2323
  function MediaLightbox({
2238
2324
  open,
@@ -2250,7 +2336,7 @@ function MediaLightbox({
2250
2336
  headerExtra,
2251
2337
  className
2252
2338
  }) {
2253
- (0, import_react15.useEffect)(() => {
2339
+ (0, import_react16.useEffect)(() => {
2254
2340
  if (!open || typeof window === "undefined") return void 0;
2255
2341
  const handler = (event) => {
2256
2342
  if (event.key === "Escape") {