@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/CHANGELOG.md +40 -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 +167 -93
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +1 -11
- package/dist/react.d.ts +1 -11
- package/dist/react.js +163 -89
- package/dist/react.js.map +1 -1
- package/dist/viewers.cjs +75 -266
- package/dist/viewers.cjs.map +1 -1
- package/dist/viewers.js +73 -264
- package/dist/viewers.js.map +1 -1
- package/package.json +3 -2
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
|
|
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,
|
|
704
|
-
const attemptRef = (0,
|
|
705
|
-
const recoveryInFlightRef = (0,
|
|
706
|
-
(0,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
1187
|
+
recoverable: resolution?.recoverable !== false,
|
|
1111
1188
|
failureRef: mediaRef,
|
|
1112
|
-
onTerminal:
|
|
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,
|
|
1118
|
-
const [isMediaPlaying, setIsMediaPlaying] = (0,
|
|
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,
|
|
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
|
|
1150
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1426
|
+
retryKey
|
|
1342
1427
|
);
|
|
1343
1428
|
}
|
|
1344
1429
|
|
|
1345
1430
|
// src/react/media-thumbnail.tsx
|
|
1346
|
-
var
|
|
1431
|
+
var import_react9 = require("react");
|
|
1347
1432
|
|
|
1348
1433
|
// src/core/use-thumbnail-source.ts
|
|
1349
|
-
var
|
|
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,
|
|
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,
|
|
1505
|
-
(0,
|
|
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
|
|
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,
|
|
1559
|
-
const [pasteHighlight, setPasteHighlight] = (0,
|
|
1560
|
-
const dragDepth = (0,
|
|
1561
|
-
const inputRef = (0,
|
|
1562
|
-
const onFilesRef = (0,
|
|
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,
|
|
1649
|
+
const onRejectedRef = (0, import_react10.useRef)(onRejected);
|
|
1565
1650
|
onRejectedRef.current = onRejected;
|
|
1566
1651
|
const acceptKey = accept?.join(",") ?? "";
|
|
1567
|
-
const filterFiles = (0,
|
|
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,
|
|
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,
|
|
1677
|
+
const openPicker = (0, import_react10.useCallback)(() => {
|
|
1593
1678
|
if (!disabled) inputRef.current?.click();
|
|
1594
1679
|
}, [disabled]);
|
|
1595
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
1741
|
+
const inputAccept = (0, import_react10.useMemo)(
|
|
1657
1742
|
() => acceptKey.length ? acceptKey : void 0,
|
|
1658
1743
|
[acceptKey]
|
|
1659
1744
|
);
|
|
1660
|
-
const getInputProps = (0,
|
|
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
|
|
1767
|
+
var import_react13 = require("react");
|
|
1683
1768
|
|
|
1684
1769
|
// src/core/use-media-upload.ts
|
|
1685
|
-
var
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1871
|
+
const clearEntry = (0, import_react11.useCallback)((id) => {
|
|
1787
1872
|
setEntries((current) => current.filter((e) => e.id !== id));
|
|
1788
1873
|
}, []);
|
|
1789
|
-
const reset = (0,
|
|
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
|
|
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,
|
|
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,
|
|
1908
|
+
const timersRef = (0, import_react12.useRef)(
|
|
1835
1909
|
/* @__PURE__ */ new Map()
|
|
1836
1910
|
);
|
|
1837
|
-
const onDismissRef = (0,
|
|
1911
|
+
const onDismissRef = (0, import_react12.useRef)(onDismiss);
|
|
1838
1912
|
onDismissRef.current = onDismiss;
|
|
1839
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
2180
|
+
var import_react15 = require("react");
|
|
2107
2181
|
|
|
2108
2182
|
// src/core/use-media-actions.ts
|
|
2109
|
-
var
|
|
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,
|
|
2114
|
-
const [lastError, setLastError] = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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") {
|