@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/CHANGELOG.md +32 -0
- package/dist/core.cjs.map +1 -1
- package/dist/core.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +164 -78
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +162 -76
- package/dist/react.js.map +1 -1
- package/dist/viewers.cjs +18 -3
- package/dist/viewers.cjs.map +1 -1
- package/dist/viewers.js +16 -1
- package/dist/viewers.js.map +1 -1
- package/package.json +1 -1
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
|
|
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,
|
|
704
|
-
const attemptRef = (0,
|
|
705
|
-
const recoveryInFlightRef = (0,
|
|
706
|
-
(0,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
1188
|
+
recoverable: resolution?.recoverable !== false,
|
|
1111
1189
|
failureRef: mediaRef,
|
|
1112
|
-
onTerminal:
|
|
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,
|
|
1118
|
-
const [isMediaPlaying, setIsMediaPlaying] = (0,
|
|
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,
|
|
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
|
|
1150
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1427
|
+
retryKey
|
|
1342
1428
|
);
|
|
1343
1429
|
}
|
|
1344
1430
|
|
|
1345
1431
|
// src/react/media-thumbnail.tsx
|
|
1346
|
-
var
|
|
1432
|
+
var import_react9 = require("react");
|
|
1347
1433
|
|
|
1348
1434
|
// src/core/use-thumbnail-source.ts
|
|
1349
|
-
var
|
|
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,
|
|
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,
|
|
1505
|
-
(0,
|
|
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
|
|
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,
|
|
1559
|
-
const [pasteHighlight, setPasteHighlight] = (0,
|
|
1560
|
-
const dragDepth = (0,
|
|
1561
|
-
const inputRef = (0,
|
|
1562
|
-
const onFilesRef = (0,
|
|
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,
|
|
1650
|
+
const onRejectedRef = (0, import_react10.useRef)(onRejected);
|
|
1565
1651
|
onRejectedRef.current = onRejected;
|
|
1566
1652
|
const acceptKey = accept?.join(",") ?? "";
|
|
1567
|
-
const filterFiles = (0,
|
|
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,
|
|
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,
|
|
1678
|
+
const openPicker = (0, import_react10.useCallback)(() => {
|
|
1593
1679
|
if (!disabled) inputRef.current?.click();
|
|
1594
1680
|
}, [disabled]);
|
|
1595
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
1742
|
+
const inputAccept = (0, import_react10.useMemo)(
|
|
1657
1743
|
() => acceptKey.length ? acceptKey : void 0,
|
|
1658
1744
|
[acceptKey]
|
|
1659
1745
|
);
|
|
1660
|
-
const getInputProps = (0,
|
|
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
|
|
1768
|
+
var import_react13 = require("react");
|
|
1683
1769
|
|
|
1684
1770
|
// src/core/use-media-upload.ts
|
|
1685
|
-
var
|
|
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,
|
|
1690
|
-
const [progress, setProgress] = (0,
|
|
1691
|
-
const [result, setResult] = (0,
|
|
1692
|
-
const [error, setError] = (0,
|
|
1693
|
-
const [entries, setEntries] = (0,
|
|
1694
|
-
const activeCount = (0,
|
|
1695
|
-
const patchEntry = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
1872
|
+
const clearEntry = (0, import_react11.useCallback)((id) => {
|
|
1787
1873
|
setEntries((current) => current.filter((e) => e.id !== id));
|
|
1788
1874
|
}, []);
|
|
1789
|
-
const reset = (0,
|
|
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
|
|
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,
|
|
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,
|
|
1920
|
+
const timersRef = (0, import_react12.useRef)(
|
|
1835
1921
|
/* @__PURE__ */ new Map()
|
|
1836
1922
|
);
|
|
1837
|
-
const onDismissRef = (0,
|
|
1923
|
+
const onDismissRef = (0, import_react12.useRef)(onDismiss);
|
|
1838
1924
|
onDismissRef.current = onDismiss;
|
|
1839
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
2192
|
+
var import_react15 = require("react");
|
|
2107
2193
|
|
|
2108
2194
|
// src/core/use-media-actions.ts
|
|
2109
|
-
var
|
|
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,
|
|
2114
|
-
const [lastError, setLastError] = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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") {
|