@ai-matrx/media 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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
@@ -66,7 +66,6 @@ __export(react_exports, {
66
66
  VolumeXIcon: () => VolumeXIcon,
67
67
  XIcon: () => XIcon,
68
68
  cn: () => cn,
69
- formatFileSize: () => formatFileSize,
70
69
  useMediaDropzone: () => useMediaDropzone
71
70
  });
72
71
  module.exports = __toCommonJS(react_exports);
@@ -535,7 +534,7 @@ function FileIcon2({
535
534
  }
536
535
 
537
536
  // src/react/inline-media-ref.tsx
538
- var import_react6 = require("react");
537
+ var import_react7 = require("react");
539
538
 
540
539
  // src/core/context.ts
541
540
  var import_react = require("react");
@@ -681,8 +680,83 @@ function useMediaBlob(ref) {
681
680
  };
682
681
  }
683
682
 
684
- // src/core/use-media-load-recovery.ts
683
+ // src/core/use-media-blob-load-recovery.ts
685
684
  var import_react4 = require("react");
685
+ function useMediaBlobLoadRecovery(ref, source, options) {
686
+ const client = useMediaClient();
687
+ const capture = useMediaFailureCapture();
688
+ const refKey = stableRefKey(ref);
689
+ const [failed, setFailed] = (0, import_react4.useState)(false);
690
+ const attemptRef = (0, import_react4.useRef)(0);
691
+ const recoveryInFlightRef = (0, import_react4.useRef)(false);
692
+ const onRetry = options.onRetry;
693
+ const onTerminal = options.onTerminal;
694
+ (0, import_react4.useEffect)(() => {
695
+ attemptRef.current = 0;
696
+ recoveryInFlightRef.current = false;
697
+ setFailed(false);
698
+ }, [refKey]);
699
+ (0, import_react4.useEffect)(() => {
700
+ recoveryInFlightRef.current = false;
701
+ }, [source]);
702
+ const finishTerminal = (0, import_react4.useCallback)(
703
+ (event, attempt, message) => {
704
+ setFailed(true);
705
+ capture({
706
+ phase: "element-load",
707
+ mediaRef: failureMediaIdentity(ref ?? null, source),
708
+ terminal: true,
709
+ retryOutcome: attempt > 1 ? "retried" : "not-attempted",
710
+ message
711
+ });
712
+ onTerminal?.(event);
713
+ },
714
+ [capture, onTerminal, ref, source]
715
+ );
716
+ const onLoadError = (0, import_react4.useCallback)(
717
+ (event) => {
718
+ if (recoveryInFlightRef.current || !source) return;
719
+ recoveryInFlightRef.current = true;
720
+ attemptRef.current += 1;
721
+ const attempt = attemptRef.current;
722
+ const recover = client.recoverBlobLoadError;
723
+ if (!ref || !recover) {
724
+ finishTerminal(
725
+ event,
726
+ attempt,
727
+ "blob-backed media element failed to load and the client exposes no object-URL recovery policy"
728
+ );
729
+ return;
730
+ }
731
+ recover(ref, attempt).then(
732
+ (verdict) => {
733
+ if (verdict === "retry") {
734
+ setFailed(false);
735
+ onRetry();
736
+ return;
737
+ }
738
+ finishTerminal(
739
+ event,
740
+ attempt,
741
+ attempt > 1 ? "blob-backed media element failed after the client-approved re-read" : "blob-backed media recovery was declined on the first attempt"
742
+ );
743
+ },
744
+ (err) => {
745
+ finishTerminal(
746
+ event,
747
+ attempt,
748
+ `blob-backed media recovery failed: ${err instanceof Error ? err.message : String(err ?? "unknown error")}`
749
+ );
750
+ }
751
+ );
752
+ },
753
+ [client, finishTerminal, onRetry, ref, source]
754
+ );
755
+ return { onLoadError, failed };
756
+ }
757
+
758
+ // src/core/use-media-load-recovery.ts
759
+ var import_react5 = require("react");
686
760
 
687
761
  // src/types.ts
688
762
  function mintDurableSrc(url) {
@@ -700,14 +774,14 @@ function useMediaLoadRecovery(src, options) {
700
774
  const recoverable = options?.recoverable !== false;
701
775
  const onTerminal = options?.onTerminal;
702
776
  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)(() => {
777
+ const [result, setResult] = (0, import_react5.useState)({ source: src, retryKey: 0, failed: false });
778
+ const attemptRef = (0, import_react5.useRef)(0);
779
+ const recoveryInFlightRef = (0, import_react5.useRef)(false);
780
+ (0, import_react5.useEffect)(() => {
707
781
  attemptRef.current = 0;
708
782
  recoveryInFlightRef.current = false;
709
783
  }, [src]);
710
- (0, import_react4.useLayoutEffect)(() => {
784
+ (0, import_react5.useLayoutEffect)(() => {
711
785
  recoveryInFlightRef.current = false;
712
786
  }, [result.retryKey]);
713
787
  const identity = () => failureMediaIdentity(failureRef ?? null, src);
@@ -781,12 +855,12 @@ function useMediaLoadRecovery(src, options) {
781
855
  }
782
856
 
783
857
  // src/core/use-media-resolution.ts
784
- var import_react5 = require("react");
858
+ var import_react6 = require("react");
785
859
  function useMediaResolution(ref) {
786
860
  const client = useMediaClient();
787
861
  const capture = useMediaFailureCapture();
788
862
  const refKey = stableRefKey(ref);
789
- const { state, failureMessage } = (0, import_react5.useMemo)(() => {
863
+ const { state, failureMessage } = (0, import_react6.useMemo)(() => {
790
864
  if (!ref) {
791
865
  return {
792
866
  state: { resolution: null, status: "empty", reason: null },
@@ -816,7 +890,7 @@ function useMediaResolution(ref) {
816
890
  };
817
891
  }
818
892
  }, [client, refKey]);
819
- (0, import_react5.useEffect)(() => {
893
+ (0, import_react6.useEffect)(() => {
820
894
  if (state.status !== "unavailable") return;
821
895
  capture({
822
896
  phase: "resolve",
@@ -871,7 +945,7 @@ function prettifyUrl(url) {
871
945
  }
872
946
  var INERT_PLAYBACK = {
873
947
  useMediaElementSink(forward) {
874
- return (0, import_react6.useCallback)(
948
+ return (0, import_react7.useCallback)(
875
949
  (node) => {
876
950
  if (!forward) return;
877
951
  if (typeof forward === "function") forward(node);
@@ -922,7 +996,7 @@ function InformativeErrorFallback({
922
996
  }) {
923
997
  const capture = useMediaFailureCapture();
924
998
  const identity = failureMediaIdentity(mediaRef ?? null, url);
925
- (0, import_react6.useEffect)(() => {
999
+ (0, import_react7.useEffect)(() => {
926
1000
  capture({
927
1001
  phase: failurePhase,
928
1002
  mediaRef: identity,
@@ -986,10 +1060,10 @@ function InformativeErrorPanel({
986
1060
  icon,
987
1061
  tooltip
988
1062
  }) {
989
- const [copied, setCopied] = (0, import_react6.useState)(false);
1063
+ const [copied, setCopied] = (0, import_react7.useState)(false);
990
1064
  const isFill = dimensions === "fill";
991
1065
  const { host, tail } = prettifyUrl(url);
992
- const handleCopy = (0, import_react6.useCallback)(
1066
+ const handleCopy = (0, import_react7.useCallback)(
993
1067
  (event) => {
994
1068
  event.stopPropagation();
995
1069
  event.preventDefault();
@@ -1106,16 +1180,25 @@ function InlineMediaRef({
1106
1180
  );
1107
1181
  const blob = useMediaBlob(needsBlob ? mediaRef : null);
1108
1182
  const url = needsBlob ? blob.url : resolution?.src ?? null;
1183
+ const terminalCallback = (event) => onError?.(
1184
+ event
1185
+ );
1109
1186
  const recovery = useMediaLoadRecovery(url, {
1110
- recoverable: !needsBlob && resolution?.recoverable !== false,
1187
+ recoverable: resolution?.recoverable !== false,
1111
1188
  failureRef: mediaRef,
1112
- onTerminal: (event) => onError?.(
1113
- event
1114
- )
1189
+ onTerminal: terminalCallback
1115
1190
  });
1191
+ const blobRecovery = useMediaBlobLoadRecovery(
1192
+ needsBlob ? mediaRef : null,
1193
+ needsBlob ? url : null,
1194
+ {
1195
+ onRetry: blob.retry,
1196
+ onTerminal: terminalCallback
1197
+ }
1198
+ );
1116
1199
  const playback = ports.playbackSession ?? INERT_PLAYBACK;
1117
- const sessionElRef = (0, import_react6.useRef)(null);
1118
- const [isMediaPlaying, setIsMediaPlaying] = (0, import_react6.useState)(false);
1200
+ const sessionElRef = (0, import_react7.useRef)(null);
1201
+ const [isMediaPlaying, setIsMediaPlaying] = (0, import_react7.useState)(false);
1119
1202
  const portSinkRef = playback.useMediaElementSink(
1120
1203
  mediaElementRef
1121
1204
  );
@@ -1123,7 +1206,7 @@ function InlineMediaRef({
1123
1206
  sessionElRef.current = node;
1124
1207
  portSinkRef(node);
1125
1208
  };
1126
- const trackKey = (0, import_react6.useMemo)(() => {
1209
+ const trackKey = (0, import_react7.useMemo)(() => {
1127
1210
  if (mediaRef && typeof mediaRef === "object" && mediaRef.file_id) {
1128
1211
  return mediaRef.file_id;
1129
1212
  }
@@ -1146,8 +1229,10 @@ function InlineMediaRef({
1146
1229
  const mediaResolutionError = status === "unavailable" ? reason ?? "unknown" : blob.error ?? null;
1147
1230
  const dimensions = resolveDimensions(size);
1148
1231
  const isFill = dimensions === "fill";
1149
- const hasLoadError = recovery.failed;
1150
- const handleLoadError = recovery.onLoadError;
1232
+ const activeRecovery = needsBlob ? blobRecovery : recovery;
1233
+ const retryKey = needsBlob ? 0 : recovery.retryKey;
1234
+ const hasLoadError = activeRecovery.failed;
1235
+ const handleLoadError = activeRecovery.onLoadError;
1151
1236
  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
1237
  const unresolvedDisplayUrl = mediaRef && typeof mediaRef === "object" && mediaRef.file_id ? `file:${mediaRef.file_id}` : "unresolved-media";
1153
1238
  if (!url) {
@@ -1283,7 +1368,7 @@ function InlineMediaRef({
1283
1368
  ...mediaSessionEvents,
1284
1369
  ...interactiveProps
1285
1370
  },
1286
- recovery.retryKey
1371
+ retryKey
1287
1372
  );
1288
1373
  }
1289
1374
  if (elementType === "audio") {
@@ -1304,7 +1389,7 @@ function InlineMediaRef({
1304
1389
  crossOrigin,
1305
1390
  ...interactiveProps
1306
1391
  },
1307
- recovery.retryKey
1392
+ retryKey
1308
1393
  );
1309
1394
  }
1310
1395
  const needsPlainImg = !!onLoad || !!mediaElementRef || !!crossOrigin;
@@ -1322,7 +1407,7 @@ function InlineMediaRef({
1322
1407
  onError: handleLoadError,
1323
1408
  ...interactiveProps
1324
1409
  },
1325
- recovery.retryKey
1410
+ retryKey
1326
1411
  );
1327
1412
  }
1328
1413
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -1338,20 +1423,20 @@ function InlineMediaRef({
1338
1423
  crossOrigin,
1339
1424
  ...interactiveProps
1340
1425
  },
1341
- recovery.retryKey
1426
+ retryKey
1342
1427
  );
1343
1428
  }
1344
1429
 
1345
1430
  // src/react/media-thumbnail.tsx
1346
- var import_react8 = require("react");
1431
+ var import_react9 = require("react");
1347
1432
 
1348
1433
  // src/core/use-thumbnail-source.ts
1349
- var import_react7 = require("react");
1434
+ var import_react8 = require("react");
1350
1435
  function useThumbnailSource(ref, options = {}) {
1351
1436
  const { thumbnailUrl, allowSourceFallback = true } = options;
1352
1437
  const client = useMediaClient();
1353
1438
  const { resolution } = useMediaResolution(ref);
1354
- const explicitThumb = (0, import_react7.useMemo)(() => {
1439
+ const explicitThumb = (0, import_react8.useMemo)(() => {
1355
1440
  if (!thumbnailUrl) return null;
1356
1441
  try {
1357
1442
  return client.resolve(thumbnailUrl);
@@ -1501,8 +1586,8 @@ function VideoPosterThumb({
1501
1586
  recoverable,
1502
1587
  failureRef: mediaRef
1503
1588
  });
1504
- const ref = (0, import_react8.useRef)(null);
1505
- (0, import_react8.useEffect)(() => {
1589
+ const ref = (0, import_react9.useRef)(null);
1590
+ (0, import_react9.useEffect)(() => {
1506
1591
  const v = ref.current;
1507
1592
  if (!v) return void 0;
1508
1593
  const onMeta = () => {
@@ -1531,7 +1616,7 @@ function VideoPosterThumb({
1531
1616
  }
1532
1617
 
1533
1618
  // src/react/use-media-dropzone.ts
1534
- var import_react9 = require("react");
1619
+ var import_react10 = require("react");
1535
1620
  function matchesAccept(file, accept) {
1536
1621
  if (!accept || accept.length === 0) return true;
1537
1622
  const name = file.name.toLowerCase();
@@ -1555,16 +1640,16 @@ function useMediaDropzone(options) {
1555
1640
  disabled = false,
1556
1641
  multiple = true
1557
1642
  } = 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);
1643
+ const [isDragActive, setIsDragActive] = (0, import_react10.useState)(false);
1644
+ const [pasteHighlight, setPasteHighlight] = (0, import_react10.useState)(false);
1645
+ const dragDepth = (0, import_react10.useRef)(0);
1646
+ const inputRef = (0, import_react10.useRef)(null);
1647
+ const onFilesRef = (0, import_react10.useRef)(onFiles);
1563
1648
  onFilesRef.current = onFiles;
1564
- const onRejectedRef = (0, import_react9.useRef)(onRejected);
1649
+ const onRejectedRef = (0, import_react10.useRef)(onRejected);
1565
1650
  onRejectedRef.current = onRejected;
1566
1651
  const acceptKey = accept?.join(",") ?? "";
1567
- const filterFiles = (0, import_react9.useCallback)(
1652
+ const filterFiles = (0, import_react10.useCallback)(
1568
1653
  (files) => {
1569
1654
  const accepted = [];
1570
1655
  const rejected = [];
@@ -1582,23 +1667,23 @@ function useMediaDropzone(options) {
1582
1667
  },
1583
1668
  [acceptKey, maxSize]
1584
1669
  );
1585
- const emit = (0, import_react9.useCallback)(
1670
+ const emit = (0, import_react10.useCallback)(
1586
1671
  (files) => {
1587
1672
  const accepted = filterFiles(files);
1588
1673
  if (accepted.length) onFilesRef.current(accepted);
1589
1674
  },
1590
1675
  [filterFiles]
1591
1676
  );
1592
- const openPicker = (0, import_react9.useCallback)(() => {
1677
+ const openPicker = (0, import_react10.useCallback)(() => {
1593
1678
  if (!disabled) inputRef.current?.click();
1594
1679
  }, [disabled]);
1595
- (0, import_react9.useEffect)(() => {
1680
+ (0, import_react10.useEffect)(() => {
1596
1681
  if (!pickerEventName || typeof window === "undefined") return void 0;
1597
1682
  const handler = () => openPicker();
1598
1683
  window.addEventListener(pickerEventName, handler);
1599
1684
  return () => window.removeEventListener(pickerEventName, handler);
1600
1685
  }, [openPicker, pickerEventName]);
1601
- (0, import_react9.useEffect)(() => {
1686
+ (0, import_react10.useEffect)(() => {
1602
1687
  if (!enablePaste || disabled || typeof window === "undefined") {
1603
1688
  return void 0;
1604
1689
  }
@@ -1624,7 +1709,7 @@ function useMediaDropzone(options) {
1624
1709
  window.addEventListener("paste", handler);
1625
1710
  return () => window.removeEventListener("paste", handler);
1626
1711
  }, [enablePaste, disabled, emit]);
1627
- const getRootProps = (0, import_react9.useCallback)(
1712
+ const getRootProps = (0, import_react10.useCallback)(
1628
1713
  () => ({
1629
1714
  onDragEnter: (event) => {
1630
1715
  if (disabled) return;
@@ -1653,11 +1738,11 @@ function useMediaDropzone(options) {
1653
1738
  }),
1654
1739
  [disabled, emit, multiple]
1655
1740
  );
1656
- const inputAccept = (0, import_react9.useMemo)(
1741
+ const inputAccept = (0, import_react10.useMemo)(
1657
1742
  () => acceptKey.length ? acceptKey : void 0,
1658
1743
  [acceptKey]
1659
1744
  );
1660
- const getInputProps = (0, import_react9.useCallback)(
1745
+ const getInputProps = (0, import_react10.useCallback)(
1661
1746
  () => ({
1662
1747
  ref: (node) => {
1663
1748
  inputRef.current = node;
@@ -1679,20 +1764,20 @@ function useMediaDropzone(options) {
1679
1764
  }
1680
1765
 
1681
1766
  // src/react/file-upload-dropzone.tsx
1682
- var import_react12 = require("react");
1767
+ var import_react13 = require("react");
1683
1768
 
1684
1769
  // src/core/use-media-upload.ts
1685
- var import_react10 = require("react");
1770
+ var import_react11 = require("react");
1686
1771
  var entrySeq = 0;
1687
1772
  function useMediaUpload() {
1688
1773
  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)(
1774
+ const [uploading, setUploading] = (0, import_react11.useState)(false);
1775
+ const [progress, setProgress] = (0, import_react11.useState)(null);
1776
+ const [result, setResult] = (0, import_react11.useState)(null);
1777
+ const [error, setError] = (0, import_react11.useState)(null);
1778
+ const [entries, setEntries] = (0, import_react11.useState)([]);
1779
+ const activeCount = (0, import_react11.useRef)(0);
1780
+ const patchEntry = (0, import_react11.useCallback)(
1696
1781
  (id, patch) => {
1697
1782
  setEntries(
1698
1783
  (current) => current.map((e) => e.id === id ? { ...e, ...patch } : e)
@@ -1700,7 +1785,7 @@ function useMediaUpload() {
1700
1785
  },
1701
1786
  []
1702
1787
  );
1703
- const upload = (0, import_react10.useCallback)(
1788
+ const upload = (0, import_react11.useCallback)(
1704
1789
  async (file, opts = {}) => {
1705
1790
  const id = `up-${++entrySeq}`;
1706
1791
  const fileName = opts.fileName ?? (file instanceof File ? file.name : "file");
@@ -1754,7 +1839,7 @@ function useMediaUpload() {
1754
1839
  },
1755
1840
  [client, patchEntry]
1756
1841
  );
1757
- const uploadMany = (0, import_react10.useCallback)(
1842
+ const uploadMany = (0, import_react11.useCallback)(
1758
1843
  async (files, opts = {}) => {
1759
1844
  if (client.uploadMany) {
1760
1845
  activeCount.current += 1;
@@ -1783,10 +1868,10 @@ function useMediaUpload() {
1783
1868
  },
1784
1869
  [client, upload]
1785
1870
  );
1786
- const clearEntry = (0, import_react10.useCallback)((id) => {
1871
+ const clearEntry = (0, import_react11.useCallback)((id) => {
1787
1872
  setEntries((current) => current.filter((e) => e.id !== id));
1788
1873
  }, []);
1789
- const reset = (0, import_react10.useCallback)(() => {
1874
+ const reset = (0, import_react11.useCallback)(() => {
1790
1875
  setResult(null);
1791
1876
  setError(null);
1792
1877
  setProgress(null);
@@ -1806,37 +1891,26 @@ function useMediaUpload() {
1806
1891
  }
1807
1892
 
1808
1893
  // src/react/upload-progress-list.tsx
1809
- var import_react11 = require("react");
1894
+ var import_react12 = require("react");
1895
+ var import_format = require("@ai-matrx/kit/format");
1810
1896
  var import_jsx_runtime5 = require("react/jsx-runtime");
1811
1897
  var AUTO_DISMISS_MS = 1e4;
1812
- function formatFileSize(bytes) {
1813
- if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "\u2014";
1814
- if (bytes < 1024) return `${bytes} B`;
1815
- const units = ["KB", "MB", "GB", "TB"];
1816
- let value = bytes;
1817
- let unit = -1;
1818
- do {
1819
- value /= 1024;
1820
- unit += 1;
1821
- } while (value >= 1024 && unit < units.length - 1);
1822
- return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit] ?? "TB"}`;
1823
- }
1824
1898
  function UploadProgressListImpl({
1825
1899
  uploads,
1826
1900
  onDismiss,
1827
1901
  onOpenFile,
1828
1902
  className
1829
1903
  }) {
1830
- const [hovered, setHovered] = (0, import_react11.useState)(false);
1904
+ const [hovered, setHovered] = (0, import_react12.useState)(false);
1831
1905
  const activeCount = uploads.filter((u) => u.status === "uploading").length;
1832
1906
  const successCount = uploads.filter((u) => u.status === "success").length;
1833
1907
  const errorCount = uploads.filter((u) => u.status === "error").length;
1834
- const timersRef = (0, import_react11.useRef)(
1908
+ const timersRef = (0, import_react12.useRef)(
1835
1909
  /* @__PURE__ */ new Map()
1836
1910
  );
1837
- const onDismissRef = (0, import_react11.useRef)(onDismiss);
1911
+ const onDismissRef = (0, import_react12.useRef)(onDismiss);
1838
1912
  onDismissRef.current = onDismiss;
1839
- (0, import_react11.useEffect)(() => {
1913
+ (0, import_react12.useEffect)(() => {
1840
1914
  const timers = timersRef.current;
1841
1915
  if (hovered) {
1842
1916
  timers.forEach((t) => clearTimeout(t));
@@ -1864,7 +1938,7 @@ function UploadProgressListImpl({
1864
1938
  }
1865
1939
  }
1866
1940
  }, [uploads, hovered]);
1867
- (0, import_react11.useEffect)(() => {
1941
+ (0, import_react12.useEffect)(() => {
1868
1942
  const timers = timersRef.current;
1869
1943
  return () => {
1870
1944
  timers.forEach((t) => clearTimeout(t));
@@ -1930,10 +2004,10 @@ function UploadProgressListImpl({
1930
2004
  }
1931
2005
  ) }),
1932
2006
  failed && u.error ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mt-0.5 truncate text-[10px] text-destructive", children: u.error }) : !done ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "mt-0.5 text-[10px] text-muted-foreground", children: [
1933
- formatFileSize(u.bytesUploaded),
2007
+ (0, import_format.formatFileSize)(u.bytesUploaded),
1934
2008
  " /",
1935
2009
  " ",
1936
- formatFileSize(u.fileSize)
2010
+ (0, import_format.formatFileSize)(u.fileSize)
1937
2011
  ] }) : null
1938
2012
  ] }),
1939
2013
  canOpen ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-medium text-primary", children: [
@@ -1976,7 +2050,7 @@ function UploadProgressListImpl({
1976
2050
  }
1977
2051
  );
1978
2052
  }
1979
- var UploadProgressList = (0, import_react11.memo)(UploadProgressListImpl);
2053
+ var UploadProgressList = (0, import_react12.memo)(UploadProgressListImpl);
1980
2054
 
1981
2055
  // src/react/file-upload-dropzone.tsx
1982
2056
  var import_jsx_runtime6 = require("react/jsx-runtime");
@@ -1996,7 +2070,7 @@ function FileUploadDropzone({
1996
2070
  pickerEventName
1997
2071
  }) {
1998
2072
  const { uploadMany, entries, clearEntry } = useMediaUpload();
1999
- const handleUpload = (0, import_react12.useCallback)(
2073
+ const handleUpload = (0, import_react13.useCallback)(
2000
2074
  async (files) => {
2001
2075
  if (!files.length) return;
2002
2076
  try {
@@ -2103,22 +2177,22 @@ function FileUploadDropzone({
2103
2177
  }
2104
2178
 
2105
2179
  // src/react/media-action-toolbar.tsx
2106
- var import_react14 = require("react");
2180
+ var import_react15 = require("react");
2107
2181
 
2108
2182
  // src/core/use-media-actions.ts
2109
- var import_react13 = require("react");
2183
+ var import_react14 = require("react");
2110
2184
  function useMediaActions(ref, options = {}) {
2111
2185
  const ports = useMediaHostPorts();
2112
2186
  const { resolution } = useMediaResolution(ref);
2113
- const [busy, setBusy] = (0, import_react13.useState)(null);
2114
- const [lastError, setLastError] = (0, import_react13.useState)(null);
2187
+ const [busy, setBusy] = (0, import_react14.useState)(null);
2188
+ const [lastError, setLastError] = (0, import_react14.useState)(null);
2115
2189
  const actions = ports.actions;
2116
2190
  const { fileName, alt } = options;
2117
- const context = (0, import_react13.useMemo)(() => {
2191
+ const context = (0, import_react14.useMemo)(() => {
2118
2192
  if (!ref) return null;
2119
2193
  return { ref, resolution, fileName, alt };
2120
2194
  }, [ref, resolution, fileName, alt]);
2121
- const isAvailable = (0, import_react13.useCallback)(
2195
+ const isAvailable = (0, import_react14.useCallback)(
2122
2196
  (kind) => {
2123
2197
  if (!context || !actions) return false;
2124
2198
  if (kind === "share") return Boolean(actions.share || actions.SharePopover);
@@ -2126,7 +2200,7 @@ function useMediaActions(ref, options = {}) {
2126
2200
  },
2127
2201
  [context, actions]
2128
2202
  );
2129
- const run = (0, import_react13.useCallback)(
2203
+ const run = (0, import_react14.useCallback)(
2130
2204
  async (kind) => {
2131
2205
  if (!context || !actions) return;
2132
2206
  const handler = actions[kind];
@@ -2175,7 +2249,7 @@ function MediaActionToolbar({
2175
2249
  buttonClassName
2176
2250
  }) {
2177
2251
  const state = useMediaActions(mediaRef, { fileName, alt });
2178
- const [shareOpen, setShareOpen] = (0, import_react14.useState)(false);
2252
+ const [shareOpen, setShareOpen] = (0, import_react15.useState)(false);
2179
2253
  const SharePopover = state.SharePopover;
2180
2254
  const visible = hideUnavailable ? actions.filter((kind) => state.isAvailable(kind)) : actions;
2181
2255
  if (visible.length === 0) return null;
@@ -2232,7 +2306,7 @@ function MediaActionToolbar({
2232
2306
  }
2233
2307
 
2234
2308
  // src/react/media-lightbox.tsx
2235
- var import_react15 = require("react");
2309
+ var import_react16 = require("react");
2236
2310
  var import_jsx_runtime8 = require("react/jsx-runtime");
2237
2311
  function MediaLightbox({
2238
2312
  open,
@@ -2250,7 +2324,7 @@ function MediaLightbox({
2250
2324
  headerExtra,
2251
2325
  className
2252
2326
  }) {
2253
- (0, import_react15.useEffect)(() => {
2327
+ (0, import_react16.useEffect)(() => {
2254
2328
  if (!open || typeof window === "undefined") return void 0;
2255
2329
  const handler = (event) => {
2256
2330
  if (event.key === "Escape") {