@ai-matrx/media 0.2.0 → 0.2.2

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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # @ai-matrx/media
2
2
 
3
+ ## 0.2.2 — 2026-08-30
4
+
5
+ Two thumbnail defect fixes (QA F3/F4 — silent stalls and wasted byte
6
+ downloads in grids and chat), no API change.
7
+
8
+ - **The tier-3 blob lane is kind-gated to image/video** in
9
+ `useThumbnailSource`. The 0.1.0 port dropped the original
10
+ `MediaThumbnail` `needsBytes` kind gate, so a private markdown/PDF/HTML
11
+ file with `transport: "blob"` downloaded its ENTIRE bytes just to feed an
12
+ `<img>` that could never decode them. Non-image/video blob resolutions now
13
+ fall straight to the icon tier, and `getBlob` is never called for them.
14
+ - **Blob object URLs render eagerly; only network thumbnails lazy-load** in
15
+ `MediaThumbnail`. `loading="lazy"` on a `blob:` src defers pixels that are
16
+ already in memory, and lazy-load never fires in hidden documents (agent
17
+ browser panes, background tabs) or offscreen scroll cells — grids and
18
+ persisted chat attachments stalled silently forever with
19
+ `complete=false`, no error, no event. `loading` is now
20
+ `eager` for `blob:` srcs and stays `lazy` for network URLs.
21
+
22
+ ## 0.2.1 — 2026-08-30
23
+
24
+ Additive only — every default unchanged. Closes the wave-2 gap that kept the
25
+ host's video lightbox host-side: `MediaLightbox` had no video passthrough.
26
+
27
+ - **`MediaLightbox`** gains `autoPlay`, `playsInline`, `poster`, and
28
+ `playbackLabel`, forwarded to the underlying `InlineMediaRef`:
29
+ - `poster` is typed as the branded `DurableSrc` (law 3 — the host
30
+ resolves/mints it, exactly like the element src; no raw-string lane);
31
+ - `playbackLabel` sets the label reported to the host's existing
32
+ `playbackSession` port while the media plays (e.g. "Video
33
+ (fullscreen)") — playback still rides the ONE port, no new lane.
34
+ - **`InlineMediaRef`** gains the same two new props (`poster` →
35
+ `<video poster>`; `playbackLabel` → the playback-session registration,
36
+ defaulting to `alt` then "Media file" as before). `autoPlay`/`playsInline`
37
+ already existed there.
38
+
3
39
  ## 0.2.0 — 2026-08-29
4
40
 
5
41
  **THE SHARE MANDATE row is satisfied (wave M-SHARE, ruling C19).** The
package/dist/core.cjs CHANGED
@@ -334,7 +334,7 @@ function useThumbnailSource(ref, options = {}) {
334
334
  return null;
335
335
  }
336
336
  }, [client, thumbnailUrl]);
337
- const needsBlob = allowSourceFallback && !explicitThumb && !resolution?.thumbnailSrc && resolution?.transport === "blob";
337
+ const needsBlob = allowSourceFallback && !explicitThumb && !resolution?.thumbnailSrc && resolution?.transport === "blob" && (resolution.kind === "image" || resolution.kind === "video");
338
338
  const blob = useMediaBlob(needsBlob ? ref : null);
339
339
  if (explicitThumb) {
340
340
  return {
package/dist/core.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core.ts","../src/core/context.ts","../src/core/ref-key.ts","../src/core/use-media-resolution.ts","../src/core/use-media-load-recovery.ts","../src/types.ts","../src/core/use-media-blob.ts","../src/core/use-media-upload.ts","../src/core/use-thumbnail-source.ts","../src/core/use-media-actions.ts","../src/core/file-kind.ts"],"sourcesContent":["/**\n * @ai-matrx/media/core — headless React hooks: the platform-free resolution,\n * recovery, bytes, upload, thumbnail-tier and actions state machines. The\n * `/react` DOM bindings (and a future `/react-native` binding) are thin\n * shells over exactly these.\n */\n\nexport {\n MediaProvider,\n useMediaClient,\n useMediaHostPorts,\n type MediaProviderProps,\n} from \"./core/context\";\nexport { stableRefKey } from \"./core/ref-key\";\nexport {\n useMediaResolution,\n type MediaResolutionState,\n} from \"./core/use-media-resolution\";\nexport {\n useMediaLoadRecovery,\n type MediaLoadRecovery,\n} from \"./core/use-media-load-recovery\";\nexport { useMediaBlob, type MediaBlobState } from \"./core/use-media-blob\";\nexport {\n useMediaUpload,\n type MediaUploadEntry,\n type MediaUploadEntryStatus,\n type MediaUploadProgress,\n type UseMediaUploadResult,\n} from \"./core/use-media-upload\";\nexport {\n useThumbnailSource,\n type ThumbnailMode,\n type ThumbnailSourceState,\n type UseThumbnailSourceOptions,\n} from \"./core/use-thumbnail-source\";\nexport {\n useMediaActions,\n type MediaActionsState,\n type UseMediaActionsOptions,\n} from \"./core/use-media-actions\";\nexport {\n getFileKindDetails,\n getFolderKindDetails,\n type FileCategory,\n type FileIconName,\n type FileKindDetails,\n} from \"./core/file-kind\";\n","/**\n * src/core/context.ts — the MediaClient / host-ports React contexts.\n *\n * The context OBJECTS live on `globalThis` under `Symbol.for` slots — NOT at\n * module level. With `splitting: false` this module is duplicated into the\n * `/core` and `/react` bundles, and CJS/ESM each instantiate their own module\n * graph; a module-level `createContext` would silently produce two different\n * contexts (a Provider from one graph invisible to a consumer from the\n * other) — the kit tap-target/link-registry lesson. Never \"clean this up\".\n */\n\nimport {\n createContext,\n createElement,\n useContext,\n type Context,\n type ReactNode,\n} from \"react\";\nimport type { MediaClient, MediaHostPorts } from \"../types\";\n\nconst CLIENT_SLOT = Symbol.for(\"ai-matrx.media.client-context\");\nconst PORTS_SLOT = Symbol.for(\"ai-matrx.media.host-ports-context\");\n\ntype SlotHost = typeof globalThis & {\n [CLIENT_SLOT]?: Context<MediaClient | null>;\n [PORTS_SLOT]?: Context<MediaHostPorts>;\n};\n\nfunction clientContext(): Context<MediaClient | null> {\n const host = globalThis as SlotHost;\n host[CLIENT_SLOT] ??= createContext<MediaClient | null>(null);\n return host[CLIENT_SLOT];\n}\n\nconst INERT_PORTS: MediaHostPorts = {};\n\nfunction portsContext(): Context<MediaHostPorts> {\n const host = globalThis as SlotHost;\n host[PORTS_SLOT] ??= createContext<MediaHostPorts>(INERT_PORTS);\n return host[PORTS_SLOT];\n}\n\nexport interface MediaProviderProps {\n client: MediaClient;\n /**\n * Optional host ports (playback session, actions, ImageComponent). Must be\n * referentially stable — the playback port members are hooks.\n */\n ports?: MediaHostPorts | undefined;\n children?: ReactNode;\n}\n\n/** Wire the injected MediaClient (and optional host ports) into the tree. */\nexport function MediaProvider({ client, ports, children }: MediaProviderProps) {\n const ClientCtx = clientContext();\n const PortsCtx = portsContext();\n return createElement(\n ClientCtx.Provider,\n { value: client },\n createElement(PortsCtx.Provider, { value: ports ?? INERT_PORTS }, children),\n );\n}\n\n/** The injected MediaClient. Throws when no MediaProvider is mounted. */\nexport function useMediaClient(): MediaClient {\n const client = useContext(clientContext());\n if (!client) {\n throw new Error(\n \"@ai-matrx/media: no MediaClient. Mount <MediaProvider client={...}> \" +\n \"above every media surface (canonically wired to @ai-matrx/data/files).\",\n );\n }\n return client;\n}\n\n/** The injected host ports; inert defaults when none were provided. */\nexport function useMediaHostPorts(): MediaHostPorts {\n return useContext(portsContext());\n}\n","/**\n * src/core/ref-key.ts — stable identity key for a MediaRefLike.\n *\n * Callers routinely pass fresh object literals every render; hooks key their\n * effects/memos on this string instead of object identity. Ported from the\n * original `stableSourceKey` (matrx-frontend `handler/hooks/useFileAs.ts`).\n */\n\nimport type { MediaRefLike } from \"../types\";\n\nexport function stableRefKey(ref: MediaRefLike | null | undefined): string {\n if (!ref) return \"<none>\";\n if (typeof ref === \"string\") return `s:${ref}`;\n if (ref.file_id) return `id:${ref.file_id}:${ref.mime_type ?? \"\"}`;\n if (ref.url) return `url:${ref.url}:${ref.mime_type ?? \"\"}`;\n return \"<empty-ref>\";\n}\n","/**\n * src/core/use-media-resolution.ts — ref → durable resolution, as state.\n *\n * Law 2: every consumer resolves media through the client, unconditionally —\n * even a caller-supplied URL string is classified, never trusted. Law 15 of\n * the handler (unavailable is a rendered state): a throwing `resolve()` is\n * classified via `classifyError` and returned as typed state — never\n * re-thrown, never console.error'd here (the client owns loud recovery for\n * unexpected failures via its own diagnostics sink).\n */\n\nimport { useMemo } from \"react\";\nimport type {\n MediaRefLike,\n MediaResolution,\n MediaUnavailableReason,\n} from \"../types\";\nimport { useMediaClient } from \"./context\";\nimport { stableRefKey } from \"./ref-key\";\n\nexport interface MediaResolutionState {\n resolution: MediaResolution | null;\n /**\n * \"empty\" — no ref, or a ref with no usable identity (render fallback)\n * \"ready\" — resolution available\n * \"unavailable\" — resolve() refused (signed URL, access denied…); render\n * the typed unavailable state, never an incident\n */\n status: \"empty\" | \"ready\" | \"unavailable\";\n reason: MediaUnavailableReason | null;\n}\n\nexport function useMediaResolution(\n ref: MediaRefLike | null | undefined,\n): MediaResolutionState {\n const client = useMediaClient();\n const refKey = stableRefKey(ref);\n return useMemo<MediaResolutionState>(() => {\n if (!ref) return { resolution: null, status: \"empty\", reason: null };\n try {\n const resolution = client.resolve(ref);\n if (!resolution) return { resolution: null, status: \"empty\", reason: null };\n return { resolution, status: \"ready\", reason: null };\n } catch (err) {\n return {\n resolution: null,\n status: \"unavailable\",\n reason: client.classifyError(err),\n };\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, refKey]);\n}\n","/**\n * src/core/use-media-load-recovery.ts — the durable-URL load-error machine.\n *\n * Ported from matrx-frontend `handler/hooks/useDurableSrc.ts`, with the\n * recovery POLICY inverted onto `MediaClient.recoverLoadError` (session\n * refresh → same-URL retry → terminal; the package may not implement a retry\n * of its own — that invariant is what killed the divergent\n * useUnifiedImageUrl/useUnifiedVideoUrl copies).\n *\n * `recoverable: false` resolutions (foreign URLs, blob: object URLs) skip the\n * client and fail straight through — a session refresh can't resurrect\n * someone else's link rot.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { mintDurableSrc, type DurableSrc } from \"../types\";\nimport { useMediaClient } from \"./context\";\n\nexport interface MediaLoadRecovery {\n /**\n * Bump this into the media element's `key` (or otherwise force a remount)\n * so a retry actually re-fires the network request for the same src.\n */\n retryKey: number;\n /** Wire to the media element's `onError`. */\n onLoadError: (event?: unknown) => void;\n /** True once recovery is exhausted (or was never possible). */\n failed: boolean;\n}\n\nexport function useMediaLoadRecovery(\n src: DurableSrc | string | null,\n options?: {\n recoverable?: boolean | undefined;\n /** Fires once recovery is exhausted, with the original element event. */\n onTerminal?: ((event?: unknown) => void) | undefined;\n },\n): MediaLoadRecovery {\n const client = useMediaClient();\n const recoverable = options?.recoverable !== false;\n const onTerminal = options?.onTerminal;\n const [result, setResult] = useState<{\n source: string | null;\n retryKey: number;\n failed: boolean;\n }>({ source: src, retryKey: 0, failed: false });\n const attemptRef = useRef(0);\n\n // A different URL deserves a fresh recovery allowance. The visible state is\n // keyed by source below, so this effect only resets the attempt counter.\n useEffect(() => {\n attemptRef.current = 0;\n }, [src]);\n\n const onLoadError = (event?: unknown) => {\n if (!src || !recoverable) {\n setResult({ source: src, retryKey: 0, failed: true });\n onTerminal?.(event);\n return;\n }\n attemptRef.current += 1;\n const attempt = attemptRef.current;\n // Re-branding is sound here: only resolution srcs reach the recoverable\n // lane (blob:/foreign URLs come in with `recoverable: false`).\n client.recoverLoadError(mintDurableSrc(src), attempt).then(\n (verdict) => {\n if (verdict === \"retry\") {\n setResult((current) => ({\n source: src,\n retryKey: current.source === src ? current.retryKey + 1 : 1,\n failed: false,\n }));\n } else {\n setResult({ source: src, retryKey: 0, failed: true });\n onTerminal?.(event);\n }\n },\n () => {\n setResult({ source: src, retryKey: 0, failed: true });\n onTerminal?.(event);\n },\n );\n };\n\n return {\n retryKey: result.source === src ? result.retryKey : 0,\n onLoadError,\n failed: result.source === src ? result.failed : false,\n };\n}\n","/**\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// 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(src: DurableSrc, attempt: number): Promise<\"retry\" | \"terminal\">;\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","/**\n * src/core/use-media-blob.ts — bytes for canvas/crossOrigin/private-pixel\n * consumers, as state.\n *\n * The behavior contract is ported from matrx-frontend `hooks/useFileBlob.ts`;\n * the byte machinery (auth, caching, in-flight dedup, IDB tiers, progress) is\n * the CLIENT's (C10 persistence-injected — the package owns no cache). The\n * hook owns only the React lifecycle: load on mount/ref change, release the\n * handle on unmount, expose a retry.\n */\n\nimport { useEffect, useState } from \"react\";\nimport type { MediaRefLike } from \"../types\";\nimport { useMediaClient } from \"./context\";\nimport { stableRefKey } from \"./ref-key\";\n\nexport interface MediaBlobState {\n /** `blob:` object URL safe to feed into any browser API. `null` until ready. */\n url: string | null;\n blob: Blob | null;\n loading: boolean;\n error: string | null;\n /** Force a fresh byte read (the client decides whether to drop its cache). */\n retry: () => void;\n}\n\nexport function useMediaBlob(\n ref: MediaRefLike | null | undefined,\n): MediaBlobState {\n const client = useMediaClient();\n const refKey = stableRefKey(ref);\n const [state, setState] = useState<{\n url: string | null;\n blob: Blob | null;\n loading: boolean;\n error: string | null;\n }>({ url: null, blob: null, loading: !!ref, error: null });\n const [retryToken, setRetryToken] = useState(0);\n\n useEffect(() => {\n if (!ref) {\n setState({ url: null, blob: null, loading: false, error: null });\n return undefined;\n }\n let cancelled = false;\n let release: (() => void) | null = null;\n setState({ url: null, blob: null, loading: true, error: null });\n client.getBlob(ref).then(\n (handle) => {\n if (cancelled) {\n handle.release();\n return;\n }\n release = handle.release;\n setState({ url: handle.url, blob: handle.blob, loading: false, error: null });\n },\n (err: unknown) => {\n if (cancelled) return;\n const message =\n err instanceof Error ? err.message : String(err ?? \"Failed to load file\");\n setState({ url: null, blob: null, loading: false, error: message });\n },\n );\n return () => {\n cancelled = true;\n release?.();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, refKey, retryToken]);\n\n return {\n ...state,\n retry: () => setRetryToken((t) => t + 1),\n };\n}\n","/**\n * src/core/use-media-upload.ts — the React-side upload primitive.\n *\n * Ported from matrx-frontend `handler/hooks/useFileUpload.ts` with two seam\n * inversions: the transport is `MediaClient.upload` (never touched directly),\n * and the multi-file pre-flight (SHA-256 dedup dialog) is the client's\n * optional `uploadMany` — when absent, the batch runs sequentially through\n * `upload` with per-file results.\n *\n * Additionally exposes a per-file `entries` tray state (name, size, bytes,\n * status) so the dropzone's UploadProgressList works without any app store —\n * the Redux `selectVisibleUploads` coupling of the original, inverted.\n */\n\nimport { useCallback, useRef, useState } from \"react\";\nimport type {\n MediaMultiUploadResult,\n MediaUploadOptions,\n MediaUploadResult,\n} from \"../types\";\nimport { useMediaClient } from \"./context\";\n\nexport interface MediaUploadProgress {\n loaded: number;\n total: number;\n ratio: number;\n}\n\nexport type MediaUploadEntryStatus = \"uploading\" | \"success\" | \"error\";\n\nexport interface MediaUploadEntry {\n id: string;\n fileName: string;\n fileSize: number;\n bytesUploaded: number;\n status: MediaUploadEntryStatus;\n error: string | null;\n fileId: string | null;\n completedAt: number | null;\n}\n\nexport interface UseMediaUploadResult {\n /** Upload one file/blob. Returns the durable identity. */\n upload: (\n file: File | Blob,\n opts?: MediaUploadOptions,\n ) => Promise<MediaUploadResult>;\n /**\n * Upload many files. Uses the client's `uploadMany` (host pre-flight /\n * dedup dialog) when implemented; otherwise a sequential per-file loop with\n * real per-file error messages.\n */\n uploadMany: (\n files: File[],\n opts?: MediaUploadOptions,\n ) => Promise<MediaMultiUploadResult>;\n uploading: boolean;\n progress: MediaUploadProgress | null;\n result: MediaUploadResult | null;\n error: Error | null;\n /** Live per-file tray entries for progress UI. */\n entries: MediaUploadEntry[];\n clearEntry: (id: string) => void;\n reset: () => void;\n}\n\nlet entrySeq = 0;\n\nexport function useMediaUpload(): UseMediaUploadResult {\n const client = useMediaClient();\n const [uploading, setUploading] = useState(false);\n const [progress, setProgress] = useState<MediaUploadProgress | null>(null);\n const [result, setResult] = useState<MediaUploadResult | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [entries, setEntries] = useState<MediaUploadEntry[]>([]);\n const activeCount = useRef(0);\n\n const patchEntry = useCallback(\n (id: string, patch: Partial<MediaUploadEntry>) => {\n setEntries((current) =>\n current.map((e) => (e.id === id ? { ...e, ...patch } : e)),\n );\n },\n [],\n );\n\n const upload = useCallback(\n async (\n file: File | Blob,\n opts: MediaUploadOptions = {},\n ): Promise<MediaUploadResult> => {\n const id = `up-${++entrySeq}`;\n const fileName =\n opts.fileName ?? (file instanceof File ? file.name : \"file\");\n setEntries((current) => [\n ...current,\n {\n id,\n fileName,\n fileSize: file.size,\n bytesUploaded: 0,\n status: \"uploading\",\n error: null,\n fileId: null,\n completedAt: null,\n },\n ]);\n activeCount.current += 1;\n setUploading(true);\n setError(null);\n setProgress(null);\n try {\n const uploaded = await client.upload(file, {\n ...opts,\n onProgress: (loaded, total) => {\n setProgress({ loaded, total, ratio: total > 0 ? loaded / total : 0 });\n patchEntry(id, { bytesUploaded: loaded });\n opts.onProgress?.(loaded, total);\n },\n });\n setResult(uploaded);\n patchEntry(id, {\n status: \"success\",\n fileId: uploaded.fileId,\n bytesUploaded: file.size,\n completedAt: Date.now(),\n });\n return uploaded;\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n patchEntry(id, {\n status: \"error\",\n error: e.message,\n completedAt: Date.now(),\n });\n throw e;\n } finally {\n activeCount.current -= 1;\n if (activeCount.current <= 0) setUploading(false);\n }\n },\n [client, patchEntry],\n );\n\n const uploadMany = useCallback(\n async (\n files: File[],\n opts: MediaUploadOptions = {},\n ): Promise<MediaMultiUploadResult> => {\n if (client.uploadMany) {\n activeCount.current += 1;\n setUploading(true);\n try {\n return await client.uploadMany(files, opts);\n } finally {\n activeCount.current -= 1;\n if (activeCount.current <= 0) setUploading(false);\n }\n }\n const uploaded: string[] = [];\n const failed: Array<{ name: string; error: string }> = [];\n for (const file of files) {\n try {\n const one = await upload(file, opts);\n uploaded.push(one.fileId);\n } catch (err) {\n // Surface BOTH the file and the real backend reason — joining\n // objects rendered \"[object Object]\" to the user in the original.\n failed.push({\n name: file.name,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n return { uploaded, failed, cancelled: false };\n },\n [client, upload],\n );\n\n const clearEntry = useCallback((id: string) => {\n setEntries((current) => current.filter((e) => e.id !== id));\n }, []);\n\n const reset = useCallback(() => {\n setResult(null);\n setError(null);\n setProgress(null);\n setEntries([]);\n }, []);\n\n return {\n upload,\n uploadMany,\n uploading,\n progress,\n result,\n error,\n entries,\n clearEntry,\n reset,\n };\n}\n","/**\n * src/core/use-thumbnail-source.ts — the thumbnail tier machine.\n *\n * Ported from matrx-frontend `MediaThumbnail.tsx`'s source-priority logic\n * with the data seams inverted onto the MediaClient:\n *\n * 1. Caller-supplied `thumbnailUrl` (`FileRecord.thumbnail_url`) —\n * CLASSIFIED through `resolve()` like every string (law 2), never\n * trusted raw.\n * 2. `resolution.thumbnailSrc` — the client's asset-variant lookup\n * (rides `MediaClient.resolve` metadata).\n * 3. Live source render — the master itself, `<img>` for images or\n * `<video preload=\"metadata\">` for a video poster frame; the private\n * bearer-auth lane (`transport: \"blob\"`) goes through `getBlob`.\n * 4. Icon fallback (`src: null`).\n */\n\nimport { useMemo } from \"react\";\nimport type { DurableSrc, MediaRefLike, MediaResolution } from \"../types\";\nimport { useMediaClient } from \"./context\";\nimport { useMediaBlob } from \"./use-media-blob\";\nimport { useMediaResolution } from \"./use-media-resolution\";\n\nexport type ThumbnailMode =\n | \"thumbnail\"\n | \"live-image\"\n | \"live-video-poster\"\n | \"icon\";\n\nexport interface ThumbnailSourceState {\n /** URL to render, or null → render the icon fallback. */\n src: DurableSrc | string | null;\n mode: ThumbnailMode;\n /** True while the private-pixels blob lane is still loading. */\n loading: boolean;\n resolution: MediaResolution | null;\n /** False for blob object URLs — wire into useMediaLoadRecovery. */\n recoverable: boolean;\n}\n\nexport interface UseThumbnailSourceOptions {\n /** A known server-rendered thumbnail URL (e.g. `FileRecord.thumbnail_url`). */\n thumbnailUrl?: string | null | undefined;\n /**\n * Whether to fall back to the source file itself when no rendered\n * thumbnail exists. Disable in dense pickers to avoid per-row byte loads.\n */\n allowSourceFallback?: boolean | undefined;\n}\n\nexport function useThumbnailSource(\n ref: MediaRefLike | null | undefined,\n options: UseThumbnailSourceOptions = {},\n): ThumbnailSourceState {\n const { thumbnailUrl, allowSourceFallback = true } = options;\n const client = useMediaClient();\n const { resolution } = useMediaResolution(ref);\n\n // Tier 1 — caller-supplied thumbnail URL, classified like everything else.\n const explicitThumb = useMemo(() => {\n if (!thumbnailUrl) return null;\n try {\n return client.resolve(thumbnailUrl);\n } catch {\n // A refused thumbnail URL (signed, foreign junk) falls to lower tiers.\n return null;\n }\n }, [client, thumbnailUrl]);\n\n // Tier 3 blob lane — only engaged when we would actually render the source.\n const needsBlob =\n allowSourceFallback &&\n !explicitThumb &&\n !resolution?.thumbnailSrc &&\n resolution?.transport === \"blob\";\n const blob = useMediaBlob(needsBlob ? ref : null);\n\n if (explicitThumb) {\n return {\n src: explicitThumb.src,\n mode: \"thumbnail\",\n loading: false,\n resolution,\n recoverable: explicitThumb.recoverable !== false,\n };\n }\n if (resolution?.thumbnailSrc) {\n return {\n src: resolution.thumbnailSrc,\n mode: \"thumbnail\",\n loading: false,\n resolution,\n recoverable: resolution.recoverable !== false,\n };\n }\n if (allowSourceFallback && resolution) {\n if (needsBlob) {\n return {\n src: blob.url,\n mode: blob.url\n ? resolution.kind === \"video\"\n ? \"live-video-poster\"\n : \"live-image\"\n : \"icon\",\n loading: blob.loading,\n resolution,\n recoverable: false,\n };\n }\n if (resolution.kind === \"image\") {\n return {\n src: resolution.src,\n mode: \"live-image\",\n loading: false,\n resolution,\n recoverable: resolution.recoverable !== false,\n };\n }\n if (resolution.kind === \"video\") {\n return {\n src: resolution.src,\n mode: \"live-video-poster\",\n loading: false,\n resolution,\n recoverable: resolution.recoverable !== false,\n };\n }\n }\n return {\n src: null,\n mode: \"icon\",\n loading: false,\n resolution,\n recoverable: false,\n };\n}\n","/**\n * src/core/use-media-actions.ts — THE SHARE MANDATE's headless engine.\n *\n * Wires a MediaRefLike to the host's injected {@link MediaActionsPort}\n * (share / download / copy / open + the SharePopover slot the associations\n * package fills). FAILS CLOSED: with no port — or no handler for a given\n * action — the action reports unavailable and the shells render it disabled.\n * Nothing here mints URLs; handlers reach bytes/links only through the\n * MediaClient they close over.\n */\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport type {\n MediaActionContext,\n MediaActionKind,\n MediaRefLike,\n MediaSharePopoverProps,\n MediaSlotComponent,\n} from \"../types\";\nimport { useMediaHostPorts } from \"./context\";\nimport { useMediaResolution } from \"./use-media-resolution\";\n\nexport interface UseMediaActionsOptions {\n fileName?: string | undefined;\n alt?: string | undefined;\n}\n\nexport interface MediaActionsState {\n /** Null when no ref was supplied. */\n context: MediaActionContext | null;\n /** True when the host wired a handler for this action (share also counts the popover). */\n isAvailable: (kind: MediaActionKind) => boolean;\n /** Run an action. No-op (resolved) when unavailable. */\n run: (kind: MediaActionKind) => Promise<void>;\n /** The action currently executing, or null. */\n busy: MediaActionKind | null;\n /** Last handler failure, surfaced as state for the shell to render. */\n lastError: { kind: MediaActionKind; message: string } | null;\n /** The rich share body slot (associations package), when injected. */\n SharePopover: MediaSlotComponent<MediaSharePopoverProps> | null;\n}\n\nexport function useMediaActions(\n ref: MediaRefLike | null | undefined,\n options: UseMediaActionsOptions = {},\n): MediaActionsState {\n const ports = useMediaHostPorts();\n const { resolution } = useMediaResolution(ref);\n const [busy, setBusy] = useState<MediaActionKind | null>(null);\n const [lastError, setLastError] = useState<{\n kind: MediaActionKind;\n message: string;\n } | null>(null);\n\n const actions = ports.actions;\n const { fileName, alt } = options;\n\n const context = useMemo<MediaActionContext | null>(() => {\n if (!ref) return null;\n return { ref, resolution, fileName, alt };\n }, [ref, resolution, fileName, alt]);\n\n const isAvailable = useCallback(\n (kind: MediaActionKind): boolean => {\n if (!context || !actions) return false;\n if (kind === \"share\") return Boolean(actions.share || actions.SharePopover);\n return Boolean(actions[kind]);\n },\n [context, actions],\n );\n\n const run = useCallback(\n async (kind: MediaActionKind): Promise<void> => {\n if (!context || !actions) return;\n const handler = actions[kind];\n if (!handler) return;\n setBusy(kind);\n setLastError(null);\n try {\n await handler(context);\n } catch (err) {\n setLastError({\n kind,\n message: err instanceof Error ? err.message : String(err),\n });\n } finally {\n setBusy(null);\n }\n },\n [context, actions],\n );\n\n return {\n context,\n isAvailable,\n run,\n busy,\n lastError,\n SharePopover: actions?.SharePopover ?? null,\n };\n}\n","/**\n * src/core/file-kind.ts — filename/MIME → category, icon name, color.\n *\n * Condensed port of the icon-relevant slice of matrx-frontend\n * `features/files/utils/file-types.ts` (the FILE_TYPES registry): every\n * extension→{category,icon,color,displayName} row is preserved; the\n * preview/thumbnail/size-cap columns stay in the app (C8 — they belong to\n * the previewer world, not the media kit). MIME-prefix fallback, the\n * dotfile-looks-like-text heuristic, folder details and the unknown default\n * are ported verbatim in behavior.\n *\n * Platform-free: icon NAMES only — `/react` maps names to its inlined SVGs;\n * a future `/react-native` binding maps the same names to its own glyphs.\n */\n\nexport type FileCategory =\n | \"CODE\"\n | \"DOCUMENT\"\n | \"AUDIO\"\n | \"IMAGE\"\n | \"VIDEO\"\n | \"ARCHIVE\"\n | \"DATA\"\n | \"NOTEBOOK\"\n | \"SUBTITLES\"\n | \"EMAIL\"\n | \"EBOOK\"\n | \"MODEL_3D\"\n | \"UNKNOWN\"\n | \"FOLDER\";\n\nexport type FileIconName =\n | \"image\"\n | \"pen-tool\"\n | \"video\"\n | \"music\"\n | \"file-type\"\n | \"file-text\"\n | \"code\"\n | \"braces\"\n | \"subtitles\"\n | \"file-code\"\n | \"file-json\"\n | \"file-spreadsheet\"\n | \"database\"\n | \"package\"\n | \"archive\"\n | \"folder\"\n | \"folder-open\"\n | \"file\"\n | \"file-audio\"\n | \"file-video\";\n\nexport interface FileKindDetails {\n category: FileCategory;\n displayName: string;\n /** Tailwind text color token, matching the original registry exactly. */\n colorClass: string;\n icon: FileIconName;\n}\n\ntype Row = [\n extensions: string[],\n category: FileCategory,\n colorClass: string,\n icon: FileIconName,\n displayName: string,\n];\n\n// Rows ported from the FILE_TYPES registry (icon-relevant columns), grouped\n// exactly as the original's icon/color assignments group them.\nconst ROWS: Row[] = [\n [[\"jpg\", \"jpeg\", \"png\", \"gif\", \"webp\", \"avif\", \"heic\", \"heif\", \"bmp\", \"tif\", \"tiff\", \"ico\"], \"IMAGE\", \"text-emerald-500\", \"image\", \"Image\"],\n [[\"svg\"], \"IMAGE\", \"text-amber-500\", \"pen-tool\", \"SVG image\"],\n [[\"mp4\", \"mov\", \"webm\", \"mkv\", \"avi\", \"m4v\"], \"VIDEO\", \"text-purple-500\", \"video\", \"Video\"],\n [[\"mp3\", \"wav\", \"ogg\", \"m4a\", \"aac\", \"flac\", \"opus\"], \"AUDIO\", \"text-pink-500\", \"music\", \"Audio\"],\n [[\"pdf\"], \"DOCUMENT\", \"text-red-500\", \"file-type\", \"PDF Document\"],\n [[\"md\", \"markdown\", \"mdx\", \"rst\", \"adoc\", \"asciidoc\", \"org\"], \"DOCUMENT\", \"text-slate-400\", \"file-text\", \"Markdown\"],\n [[\"txt\", \"text\", \"asc\", \"me\", \"log\", \"out\", \"err\"], \"DOCUMENT\", \"text-muted-foreground\", \"file-text\", \"Text\"],\n [[\"dockerfile\", \"containerfile\", \"lua\"], \"CODE\", \"text-blue-500\", \"code\", \"Code\"],\n [[\"mk\", \"make\", \"swift\", \"ml\", \"mli\", \"fs\", \"fsi\", \"fsx\", \"svelte\"], \"CODE\", \"text-orange-500\", \"code\", \"Code\"],\n [[\"diff\", \"patch\", \"pem\", \"csr\", \"crt\", \"cer\", \"der\", \"key\", \"pub\", \"p7b\", \"p7c\", \"pfx\", \"p12\"], \"CODE\", \"text-amber-500\", \"file-text\", \"Text\"],\n [[\"ini\", \"cfg\", \"conf\", \"config\", \"properties\", \"prefs\"], \"CODE\", \"text-amber-400\", \"braces\", \"Config\"],\n [[\"tex\", \"latex\", \"ltx\", \"sty\", \"cls\", \"bib\"], \"CODE\", \"text-emerald-500\", \"file-text\", \"LaTeX\"],\n [[\"srt\", \"vtt\"], \"SUBTITLES\", \"text-cyan-500\", \"subtitles\", \"Subtitles\"],\n [[\"js\", \"mjs\", \"cjs\", \"jsx\"], \"CODE\", \"text-yellow-500\", \"file-code\", \"JavaScript\"],\n [[\"ts\", \"tsx\"], \"CODE\", \"text-blue-500\", \"file-code\", \"TypeScript\"],\n [[\"py\", \"clj\", \"cljs\", \"cljc\", \"edn\", \"vue\", \"styl\", \"stylus\"], \"CODE\", \"text-emerald-500\", \"code\", \"Code\"],\n [[\"rb\", \"rbw\", \"rake\", \"ru\", \"gemspec\", \"scala\", \"sbt\"], \"CODE\", \"text-red-500\", \"code\", \"Code\"],\n [[\"go\", \"dart\"], \"CODE\", \"text-cyan-500\", \"code\", \"Code\"],\n [[\"rs\"], \"CODE\", \"text-orange-600\", \"code\", \"Rust\"],\n [[\"java\"], \"CODE\", \"text-red-600\", \"code\", \"Java\"],\n [[\"c\", \"h\"], \"CODE\", \"text-blue-600\", \"code\", \"C\"],\n [[\"cpp\", \"cc\", \"cxx\", \"hpp\"], \"CODE\", \"text-blue-700\", \"code\", \"C++\"],\n [[\"cs\"], \"CODE\", \"text-purple-600\", \"code\", \"C#\"],\n [[\"html\", \"htm\", \"kt\", \"kts\"], \"CODE\", \"text-orange-400\", \"code\", \"Code\"],\n [[\"css\"], \"CODE\", \"text-sky-400\", \"code\", \"CSS\"],\n [[\"scss\"], \"CODE\", \"text-pink-400\", \"code\", \"SCSS\"],\n [[\"sh\", \"bash\", \"zsh\", \"fish\", \"ksh\", \"ash\", \"bat\", \"cmd\", \"twig\", \"jinja\", \"j2\", \"njk\"], \"CODE\", \"text-emerald-400\", \"code\", \"Script\"],\n [[\"sql\"], \"CODE\", \"text-amber-400\", \"code\", \"SQL\"],\n [[\"pl\", \"pm\"], \"CODE\", \"text-indigo-400\", \"code\", \"Perl\"],\n [[\"r\", \"rmd\", \"proto\", \"ps1\", \"psm1\", \"psd1\", \"less\"], \"CODE\", \"text-blue-400\", \"code\", \"Code\"],\n [[\"ex\", \"exs\"], \"CODE\", \"text-purple-400\", \"code\", \"Elixir\"],\n [[\"erl\", \"hrl\"], \"CODE\", \"text-rose-500\", \"code\", \"Erlang\"],\n [[\"hs\", \"lhs\"], \"CODE\", \"text-violet-500\", \"code\", \"Haskell\"],\n [[\"zig\"], \"CODE\", \"text-yellow-500\", \"code\", \"Zig\"],\n [[\"nim\", \"nims\"], \"CODE\", \"text-yellow-400\", \"code\", \"Nim\"],\n [[\"jl\"], \"CODE\", \"text-purple-500\", \"code\", \"Julia\"],\n [[\"astro\"], \"CODE\", \"text-fuchsia-500\", \"code\", \"Astro\"],\n [[\"graphql\", \"gql\", \"graphqls\"], \"CODE\", \"text-pink-500\", \"code\", \"GraphQL\"],\n [[\"sol\"], \"CODE\", \"text-slate-400\", \"code\", \"Solidity\"],\n [[\"php\", \"phtml\", \"phps\"], \"CODE\", \"text-indigo-500\", \"code\", \"PHP\"],\n [[\"hbs\", \"handlebars\", \"mustache\", \"ejs\", \"liquid\"], \"CODE\", \"text-amber-500\", \"code\", \"Template\"],\n [[\"json\", \"jsonc\", \"json5\", \"har\", \"geojson\", \"topojson\"], \"DATA\", \"text-amber-500\", \"file-json\", \"JSON\"],\n [[\"yaml\", \"yml\", \"toml\"], \"CODE\", \"text-amber-500\", \"braces\", \"Config\"],\n [[\"xml\"], \"DATA\", \"text-amber-500\", \"braces\", \"XML\"],\n [[\"csv\", \"tsv\"], \"DATA\", \"text-orange-500\", \"file-spreadsheet\", \"Spreadsheet data\"],\n [[\"sqlite\", \"sqlite3\", \"db\"], \"DATA\", \"text-blue-400\", \"database\", \"Database\"],\n [[\"ttl\", \"n3\", \"nt\", \"nq\", \"trig\"], \"DATA\", \"text-fuchsia-400\", \"braces\", \"RDF data\"],\n [[\"xlsx\", \"xls\"], \"DOCUMENT\", \"text-emerald-600\", \"file-spreadsheet\", \"Excel workbook\"],\n [[\"doc\", \"docx\"], \"DOCUMENT\", \"text-blue-500\", \"file-text\", \"Word document\"],\n [[\"ppt\", \"pptx\"], \"DOCUMENT\", \"text-orange-600\", \"file-type\", \"Presentation\"],\n [[\"ipynb\"], \"NOTEBOOK\", \"text-orange-400\", \"file-json\", \"Notebook\"],\n [[\"epub\"], \"EBOOK\", \"text-indigo-500\", \"file-text\", \"eBook\"],\n [[\"eml\"], \"EMAIL\", \"text-blue-400\", \"file-text\", \"Email\"],\n [[\"glb\", \"gltf\", \"stl\", \"obj\", \"fbx\"], \"MODEL_3D\", \"text-violet-400\", \"package\", \"3D model\"],\n [[\"zip\", \"rar\", \"7z\"], \"ARCHIVE\", \"text-amber-600\", \"archive\", \"Archive\"],\n [[\"tar\", \"gz\", \"tgz\"], \"ARCHIVE\", \"text-amber-600\", \"package\", \"Archive\"],\n];\n\nconst BY_EXTENSION = new Map<string, FileKindDetails>();\nfor (const [extensions, category, colorClass, icon, displayName] of ROWS) {\n for (const ext of extensions) {\n BY_EXTENSION.set(ext, { category, colorClass, icon, displayName });\n }\n}\n\nconst UNKNOWN_DETAILS: FileKindDetails = {\n category: \"UNKNOWN\",\n displayName: \"File\",\n colorClass: \"text-muted-foreground\",\n icon: \"file\",\n};\n\nconst ASSUMED_TEXT_DETAILS: FileKindDetails = {\n category: \"DOCUMENT\",\n displayName: \"Text\",\n colorClass: \"text-muted-foreground\",\n icon: \"file-text\",\n};\n\nconst FOLDER_DETAILS: FileKindDetails = {\n category: \"FOLDER\",\n displayName: \"Folder\",\n colorClass: \"text-sky-500\",\n icon: \"folder\",\n};\n\nconst IMAGE_DETAILS = BY_EXTENSION.get(\"png\") as FileKindDetails;\nconst VIDEO_DETAILS = BY_EXTENSION.get(\"mp4\") as FileKindDetails;\nconst AUDIO_DETAILS = BY_EXTENSION.get(\"mp3\") as FileKindDetails;\nconst PDF_DETAILS = BY_EXTENSION.get(\"pdf\") as FileKindDetails;\nconst SVG_DETAILS = BY_EXTENSION.get(\"svg\") as FileKindDetails;\n\nfunction basenameOf(path: string): string {\n const idx = Math.max(path.lastIndexOf(\"/\"), path.lastIndexOf(\"\\\\\"));\n return idx >= 0 ? path.slice(idx + 1) : path;\n}\n\nfunction extOf(name: string): string | null {\n const idx = name.lastIndexOf(\".\");\n if (idx <= 0 || idx === name.length - 1) return null;\n return name.slice(idx + 1).toLowerCase();\n}\n\n/**\n * Dotfile heuristic (ported): unrecognized dotfiles are almost always text —\n * bias toward the text profile instead of the binary \"File\" default.\n */\nfunction dotfileLooksLikeText(filename: string): boolean {\n const name = basenameOf(filename);\n if (!name.startsWith(\".\")) return false;\n for (let i = 1; i < name.length; i++) {\n const c = name.charCodeAt(i);\n if (\n (c >= 48 && c <= 57) ||\n (c >= 65 && c <= 90) ||\n (c >= 97 && c <= 122) ||\n c === 95 ||\n c === 45\n ) {\n return true;\n }\n }\n return false;\n}\n\n/** Strip MIME parameters (`;codecs=opus`, etc.). */\nfunction canonicalMimeBase(raw: string): string {\n return raw.split(\";\")[0]?.trim().toLowerCase() ?? raw;\n}\n\n/**\n * Resolve icon details from a filename and (optionally) the server MIME.\n * MIME-prefix override handles \"the browser knows `image/avif` but the\n * extension table doesn't\" — and disambiguates audio/webm vs video/webm.\n */\nexport function getFileKindDetails(\n fileName: string,\n mimeType?: string | null,\n): FileKindDetails {\n const name = basenameOf(fileName);\n const lower = name.toLowerCase();\n const ext = lower === \"dockerfile\" || lower === \"containerfile\" ? lower : extOf(name);\n const fromExt = ext ? BY_EXTENSION.get(ext) : undefined;\n\n const mime = mimeType ? canonicalMimeBase(mimeType) : null;\n if (mime) {\n if (mime === \"image/svg+xml\") return fromExt ?? SVG_DETAILS;\n if (mime.startsWith(\"image/\")) return fromExt?.category === \"IMAGE\" ? fromExt : IMAGE_DETAILS;\n if (mime.startsWith(\"video/\")) return fromExt?.category === \"VIDEO\" ? fromExt : VIDEO_DETAILS;\n if (mime.startsWith(\"audio/\")) return fromExt?.category === \"AUDIO\" ? fromExt : AUDIO_DETAILS;\n if (mime === \"application/pdf\") return PDF_DETAILS;\n }\n\n if (fromExt) return fromExt;\n if (dotfileLooksLikeText(fileName)) return ASSUMED_TEXT_DETAILS;\n return UNKNOWN_DETAILS;\n}\n\n/** Folder icon details (open picks the open-folder glyph). */\nexport function getFolderKindDetails(open = false): FileKindDetails {\n return open ? { ...FOLDER_DETAILS, icon: \"folder-open\" } : FOLDER_DETAILS;\n}\n"],"mappings":"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","names":["import_react","import_react","import_react","import_react","import_react","import_react"]}
1
+ {"version":3,"sources":["../src/core.ts","../src/core/context.ts","../src/core/ref-key.ts","../src/core/use-media-resolution.ts","../src/core/use-media-load-recovery.ts","../src/types.ts","../src/core/use-media-blob.ts","../src/core/use-media-upload.ts","../src/core/use-thumbnail-source.ts","../src/core/use-media-actions.ts","../src/core/file-kind.ts"],"sourcesContent":["/**\n * @ai-matrx/media/core — headless React hooks: the platform-free resolution,\n * recovery, bytes, upload, thumbnail-tier and actions state machines. The\n * `/react` DOM bindings (and a future `/react-native` binding) are thin\n * shells over exactly these.\n */\n\nexport {\n MediaProvider,\n useMediaClient,\n useMediaHostPorts,\n type MediaProviderProps,\n} from \"./core/context\";\nexport { stableRefKey } from \"./core/ref-key\";\nexport {\n useMediaResolution,\n type MediaResolutionState,\n} from \"./core/use-media-resolution\";\nexport {\n useMediaLoadRecovery,\n type MediaLoadRecovery,\n} from \"./core/use-media-load-recovery\";\nexport { useMediaBlob, type MediaBlobState } from \"./core/use-media-blob\";\nexport {\n useMediaUpload,\n type MediaUploadEntry,\n type MediaUploadEntryStatus,\n type MediaUploadProgress,\n type UseMediaUploadResult,\n} from \"./core/use-media-upload\";\nexport {\n useThumbnailSource,\n type ThumbnailMode,\n type ThumbnailSourceState,\n type UseThumbnailSourceOptions,\n} from \"./core/use-thumbnail-source\";\nexport {\n useMediaActions,\n type MediaActionsState,\n type UseMediaActionsOptions,\n} from \"./core/use-media-actions\";\nexport {\n getFileKindDetails,\n getFolderKindDetails,\n type FileCategory,\n type FileIconName,\n type FileKindDetails,\n} from \"./core/file-kind\";\n","/**\n * src/core/context.ts — the MediaClient / host-ports React contexts.\n *\n * The context OBJECTS live on `globalThis` under `Symbol.for` slots — NOT at\n * module level. With `splitting: false` this module is duplicated into the\n * `/core` and `/react` bundles, and CJS/ESM each instantiate their own module\n * graph; a module-level `createContext` would silently produce two different\n * contexts (a Provider from one graph invisible to a consumer from the\n * other) — the kit tap-target/link-registry lesson. Never \"clean this up\".\n */\n\nimport {\n createContext,\n createElement,\n useContext,\n type Context,\n type ReactNode,\n} from \"react\";\nimport type { MediaClient, MediaHostPorts } from \"../types\";\n\nconst CLIENT_SLOT = Symbol.for(\"ai-matrx.media.client-context\");\nconst PORTS_SLOT = Symbol.for(\"ai-matrx.media.host-ports-context\");\n\ntype SlotHost = typeof globalThis & {\n [CLIENT_SLOT]?: Context<MediaClient | null>;\n [PORTS_SLOT]?: Context<MediaHostPorts>;\n};\n\nfunction clientContext(): Context<MediaClient | null> {\n const host = globalThis as SlotHost;\n host[CLIENT_SLOT] ??= createContext<MediaClient | null>(null);\n return host[CLIENT_SLOT];\n}\n\nconst INERT_PORTS: MediaHostPorts = {};\n\nfunction portsContext(): Context<MediaHostPorts> {\n const host = globalThis as SlotHost;\n host[PORTS_SLOT] ??= createContext<MediaHostPorts>(INERT_PORTS);\n return host[PORTS_SLOT];\n}\n\nexport interface MediaProviderProps {\n client: MediaClient;\n /**\n * Optional host ports (playback session, actions, ImageComponent). Must be\n * referentially stable — the playback port members are hooks.\n */\n ports?: MediaHostPorts | undefined;\n children?: ReactNode;\n}\n\n/** Wire the injected MediaClient (and optional host ports) into the tree. */\nexport function MediaProvider({ client, ports, children }: MediaProviderProps) {\n const ClientCtx = clientContext();\n const PortsCtx = portsContext();\n return createElement(\n ClientCtx.Provider,\n { value: client },\n createElement(PortsCtx.Provider, { value: ports ?? INERT_PORTS }, children),\n );\n}\n\n/** The injected MediaClient. Throws when no MediaProvider is mounted. */\nexport function useMediaClient(): MediaClient {\n const client = useContext(clientContext());\n if (!client) {\n throw new Error(\n \"@ai-matrx/media: no MediaClient. Mount <MediaProvider client={...}> \" +\n \"above every media surface (canonically wired to @ai-matrx/data/files).\",\n );\n }\n return client;\n}\n\n/** The injected host ports; inert defaults when none were provided. */\nexport function useMediaHostPorts(): MediaHostPorts {\n return useContext(portsContext());\n}\n","/**\n * src/core/ref-key.ts — stable identity key for a MediaRefLike.\n *\n * Callers routinely pass fresh object literals every render; hooks key their\n * effects/memos on this string instead of object identity. Ported from the\n * original `stableSourceKey` (matrx-frontend `handler/hooks/useFileAs.ts`).\n */\n\nimport type { MediaRefLike } from \"../types\";\n\nexport function stableRefKey(ref: MediaRefLike | null | undefined): string {\n if (!ref) return \"<none>\";\n if (typeof ref === \"string\") return `s:${ref}`;\n if (ref.file_id) return `id:${ref.file_id}:${ref.mime_type ?? \"\"}`;\n if (ref.url) return `url:${ref.url}:${ref.mime_type ?? \"\"}`;\n return \"<empty-ref>\";\n}\n","/**\n * src/core/use-media-resolution.ts — ref → durable resolution, as state.\n *\n * Law 2: every consumer resolves media through the client, unconditionally —\n * even a caller-supplied URL string is classified, never trusted. Law 15 of\n * the handler (unavailable is a rendered state): a throwing `resolve()` is\n * classified via `classifyError` and returned as typed state — never\n * re-thrown, never console.error'd here (the client owns loud recovery for\n * unexpected failures via its own diagnostics sink).\n */\n\nimport { useMemo } from \"react\";\nimport type {\n MediaRefLike,\n MediaResolution,\n MediaUnavailableReason,\n} from \"../types\";\nimport { useMediaClient } from \"./context\";\nimport { stableRefKey } from \"./ref-key\";\n\nexport interface MediaResolutionState {\n resolution: MediaResolution | null;\n /**\n * \"empty\" — no ref, or a ref with no usable identity (render fallback)\n * \"ready\" — resolution available\n * \"unavailable\" — resolve() refused (signed URL, access denied…); render\n * the typed unavailable state, never an incident\n */\n status: \"empty\" | \"ready\" | \"unavailable\";\n reason: MediaUnavailableReason | null;\n}\n\nexport function useMediaResolution(\n ref: MediaRefLike | null | undefined,\n): MediaResolutionState {\n const client = useMediaClient();\n const refKey = stableRefKey(ref);\n return useMemo<MediaResolutionState>(() => {\n if (!ref) return { resolution: null, status: \"empty\", reason: null };\n try {\n const resolution = client.resolve(ref);\n if (!resolution) return { resolution: null, status: \"empty\", reason: null };\n return { resolution, status: \"ready\", reason: null };\n } catch (err) {\n return {\n resolution: null,\n status: \"unavailable\",\n reason: client.classifyError(err),\n };\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, refKey]);\n}\n","/**\n * src/core/use-media-load-recovery.ts — the durable-URL load-error machine.\n *\n * Ported from matrx-frontend `handler/hooks/useDurableSrc.ts`, with the\n * recovery POLICY inverted onto `MediaClient.recoverLoadError` (session\n * refresh → same-URL retry → terminal; the package may not implement a retry\n * of its own — that invariant is what killed the divergent\n * useUnifiedImageUrl/useUnifiedVideoUrl copies).\n *\n * `recoverable: false` resolutions (foreign URLs, blob: object URLs) skip the\n * client and fail straight through — a session refresh can't resurrect\n * someone else's link rot.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { mintDurableSrc, type DurableSrc } from \"../types\";\nimport { useMediaClient } from \"./context\";\n\nexport interface MediaLoadRecovery {\n /**\n * Bump this into the media element's `key` (or otherwise force a remount)\n * so a retry actually re-fires the network request for the same src.\n */\n retryKey: number;\n /** Wire to the media element's `onError`. */\n onLoadError: (event?: unknown) => void;\n /** True once recovery is exhausted (or was never possible). */\n failed: boolean;\n}\n\nexport function useMediaLoadRecovery(\n src: DurableSrc | string | null,\n options?: {\n recoverable?: boolean | undefined;\n /** Fires once recovery is exhausted, with the original element event. */\n onTerminal?: ((event?: unknown) => void) | undefined;\n },\n): MediaLoadRecovery {\n const client = useMediaClient();\n const recoverable = options?.recoverable !== false;\n const onTerminal = options?.onTerminal;\n const [result, setResult] = useState<{\n source: string | null;\n retryKey: number;\n failed: boolean;\n }>({ source: src, retryKey: 0, failed: false });\n const attemptRef = useRef(0);\n\n // A different URL deserves a fresh recovery allowance. The visible state is\n // keyed by source below, so this effect only resets the attempt counter.\n useEffect(() => {\n attemptRef.current = 0;\n }, [src]);\n\n const onLoadError = (event?: unknown) => {\n if (!src || !recoverable) {\n setResult({ source: src, retryKey: 0, failed: true });\n onTerminal?.(event);\n return;\n }\n attemptRef.current += 1;\n const attempt = attemptRef.current;\n // Re-branding is sound here: only resolution srcs reach the recoverable\n // lane (blob:/foreign URLs come in with `recoverable: false`).\n client.recoverLoadError(mintDurableSrc(src), attempt).then(\n (verdict) => {\n if (verdict === \"retry\") {\n setResult((current) => ({\n source: src,\n retryKey: current.source === src ? current.retryKey + 1 : 1,\n failed: false,\n }));\n } else {\n setResult({ source: src, retryKey: 0, failed: true });\n onTerminal?.(event);\n }\n },\n () => {\n setResult({ source: src, retryKey: 0, failed: true });\n onTerminal?.(event);\n },\n );\n };\n\n return {\n retryKey: result.source === src ? result.retryKey : 0,\n onLoadError,\n failed: result.source === src ? result.failed : false,\n };\n}\n","/**\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// 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(src: DurableSrc, attempt: number): Promise<\"retry\" | \"terminal\">;\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","/**\n * src/core/use-media-blob.ts — bytes for canvas/crossOrigin/private-pixel\n * consumers, as state.\n *\n * The behavior contract is ported from matrx-frontend `hooks/useFileBlob.ts`;\n * the byte machinery (auth, caching, in-flight dedup, IDB tiers, progress) is\n * the CLIENT's (C10 persistence-injected — the package owns no cache). The\n * hook owns only the React lifecycle: load on mount/ref change, release the\n * handle on unmount, expose a retry.\n */\n\nimport { useEffect, useState } from \"react\";\nimport type { MediaRefLike } from \"../types\";\nimport { useMediaClient } from \"./context\";\nimport { stableRefKey } from \"./ref-key\";\n\nexport interface MediaBlobState {\n /** `blob:` object URL safe to feed into any browser API. `null` until ready. */\n url: string | null;\n blob: Blob | null;\n loading: boolean;\n error: string | null;\n /** Force a fresh byte read (the client decides whether to drop its cache). */\n retry: () => void;\n}\n\nexport function useMediaBlob(\n ref: MediaRefLike | null | undefined,\n): MediaBlobState {\n const client = useMediaClient();\n const refKey = stableRefKey(ref);\n const [state, setState] = useState<{\n url: string | null;\n blob: Blob | null;\n loading: boolean;\n error: string | null;\n }>({ url: null, blob: null, loading: !!ref, error: null });\n const [retryToken, setRetryToken] = useState(0);\n\n useEffect(() => {\n if (!ref) {\n setState({ url: null, blob: null, loading: false, error: null });\n return undefined;\n }\n let cancelled = false;\n let release: (() => void) | null = null;\n setState({ url: null, blob: null, loading: true, error: null });\n client.getBlob(ref).then(\n (handle) => {\n if (cancelled) {\n handle.release();\n return;\n }\n release = handle.release;\n setState({ url: handle.url, blob: handle.blob, loading: false, error: null });\n },\n (err: unknown) => {\n if (cancelled) return;\n const message =\n err instanceof Error ? err.message : String(err ?? \"Failed to load file\");\n setState({ url: null, blob: null, loading: false, error: message });\n },\n );\n return () => {\n cancelled = true;\n release?.();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, refKey, retryToken]);\n\n return {\n ...state,\n retry: () => setRetryToken((t) => t + 1),\n };\n}\n","/**\n * src/core/use-media-upload.ts — the React-side upload primitive.\n *\n * Ported from matrx-frontend `handler/hooks/useFileUpload.ts` with two seam\n * inversions: the transport is `MediaClient.upload` (never touched directly),\n * and the multi-file pre-flight (SHA-256 dedup dialog) is the client's\n * optional `uploadMany` — when absent, the batch runs sequentially through\n * `upload` with per-file results.\n *\n * Additionally exposes a per-file `entries` tray state (name, size, bytes,\n * status) so the dropzone's UploadProgressList works without any app store —\n * the Redux `selectVisibleUploads` coupling of the original, inverted.\n */\n\nimport { useCallback, useRef, useState } from \"react\";\nimport type {\n MediaMultiUploadResult,\n MediaUploadOptions,\n MediaUploadResult,\n} from \"../types\";\nimport { useMediaClient } from \"./context\";\n\nexport interface MediaUploadProgress {\n loaded: number;\n total: number;\n ratio: number;\n}\n\nexport type MediaUploadEntryStatus = \"uploading\" | \"success\" | \"error\";\n\nexport interface MediaUploadEntry {\n id: string;\n fileName: string;\n fileSize: number;\n bytesUploaded: number;\n status: MediaUploadEntryStatus;\n error: string | null;\n fileId: string | null;\n completedAt: number | null;\n}\n\nexport interface UseMediaUploadResult {\n /** Upload one file/blob. Returns the durable identity. */\n upload: (\n file: File | Blob,\n opts?: MediaUploadOptions,\n ) => Promise<MediaUploadResult>;\n /**\n * Upload many files. Uses the client's `uploadMany` (host pre-flight /\n * dedup dialog) when implemented; otherwise a sequential per-file loop with\n * real per-file error messages.\n */\n uploadMany: (\n files: File[],\n opts?: MediaUploadOptions,\n ) => Promise<MediaMultiUploadResult>;\n uploading: boolean;\n progress: MediaUploadProgress | null;\n result: MediaUploadResult | null;\n error: Error | null;\n /** Live per-file tray entries for progress UI. */\n entries: MediaUploadEntry[];\n clearEntry: (id: string) => void;\n reset: () => void;\n}\n\nlet entrySeq = 0;\n\nexport function useMediaUpload(): UseMediaUploadResult {\n const client = useMediaClient();\n const [uploading, setUploading] = useState(false);\n const [progress, setProgress] = useState<MediaUploadProgress | null>(null);\n const [result, setResult] = useState<MediaUploadResult | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [entries, setEntries] = useState<MediaUploadEntry[]>([]);\n const activeCount = useRef(0);\n\n const patchEntry = useCallback(\n (id: string, patch: Partial<MediaUploadEntry>) => {\n setEntries((current) =>\n current.map((e) => (e.id === id ? { ...e, ...patch } : e)),\n );\n },\n [],\n );\n\n const upload = useCallback(\n async (\n file: File | Blob,\n opts: MediaUploadOptions = {},\n ): Promise<MediaUploadResult> => {\n const id = `up-${++entrySeq}`;\n const fileName =\n opts.fileName ?? (file instanceof File ? file.name : \"file\");\n setEntries((current) => [\n ...current,\n {\n id,\n fileName,\n fileSize: file.size,\n bytesUploaded: 0,\n status: \"uploading\",\n error: null,\n fileId: null,\n completedAt: null,\n },\n ]);\n activeCount.current += 1;\n setUploading(true);\n setError(null);\n setProgress(null);\n try {\n const uploaded = await client.upload(file, {\n ...opts,\n onProgress: (loaded, total) => {\n setProgress({ loaded, total, ratio: total > 0 ? loaded / total : 0 });\n patchEntry(id, { bytesUploaded: loaded });\n opts.onProgress?.(loaded, total);\n },\n });\n setResult(uploaded);\n patchEntry(id, {\n status: \"success\",\n fileId: uploaded.fileId,\n bytesUploaded: file.size,\n completedAt: Date.now(),\n });\n return uploaded;\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n setError(e);\n patchEntry(id, {\n status: \"error\",\n error: e.message,\n completedAt: Date.now(),\n });\n throw e;\n } finally {\n activeCount.current -= 1;\n if (activeCount.current <= 0) setUploading(false);\n }\n },\n [client, patchEntry],\n );\n\n const uploadMany = useCallback(\n async (\n files: File[],\n opts: MediaUploadOptions = {},\n ): Promise<MediaMultiUploadResult> => {\n if (client.uploadMany) {\n activeCount.current += 1;\n setUploading(true);\n try {\n return await client.uploadMany(files, opts);\n } finally {\n activeCount.current -= 1;\n if (activeCount.current <= 0) setUploading(false);\n }\n }\n const uploaded: string[] = [];\n const failed: Array<{ name: string; error: string }> = [];\n for (const file of files) {\n try {\n const one = await upload(file, opts);\n uploaded.push(one.fileId);\n } catch (err) {\n // Surface BOTH the file and the real backend reason — joining\n // objects rendered \"[object Object]\" to the user in the original.\n failed.push({\n name: file.name,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n return { uploaded, failed, cancelled: false };\n },\n [client, upload],\n );\n\n const clearEntry = useCallback((id: string) => {\n setEntries((current) => current.filter((e) => e.id !== id));\n }, []);\n\n const reset = useCallback(() => {\n setResult(null);\n setError(null);\n setProgress(null);\n setEntries([]);\n }, []);\n\n return {\n upload,\n uploadMany,\n uploading,\n progress,\n result,\n error,\n entries,\n clearEntry,\n reset,\n };\n}\n","/**\n * src/core/use-thumbnail-source.ts — the thumbnail tier machine.\n *\n * Ported from matrx-frontend `MediaThumbnail.tsx`'s source-priority logic\n * with the data seams inverted onto the MediaClient:\n *\n * 1. Caller-supplied `thumbnailUrl` (`FileRecord.thumbnail_url`) —\n * CLASSIFIED through `resolve()` like every string (law 2), never\n * trusted raw.\n * 2. `resolution.thumbnailSrc` — the client's asset-variant lookup\n * (rides `MediaClient.resolve` metadata).\n * 3. Live source render — the master itself, `<img>` for images or\n * `<video preload=\"metadata\">` for a video poster frame; the private\n * bearer-auth lane (`transport: \"blob\"`) goes through `getBlob`.\n * 4. Icon fallback (`src: null`).\n */\n\nimport { useMemo } from \"react\";\nimport type { DurableSrc, MediaRefLike, MediaResolution } from \"../types\";\nimport { useMediaClient } from \"./context\";\nimport { useMediaBlob } from \"./use-media-blob\";\nimport { useMediaResolution } from \"./use-media-resolution\";\n\nexport type ThumbnailMode =\n | \"thumbnail\"\n | \"live-image\"\n | \"live-video-poster\"\n | \"icon\";\n\nexport interface ThumbnailSourceState {\n /** URL to render, or null → render the icon fallback. */\n src: DurableSrc | string | null;\n mode: ThumbnailMode;\n /** True while the private-pixels blob lane is still loading. */\n loading: boolean;\n resolution: MediaResolution | null;\n /** False for blob object URLs — wire into useMediaLoadRecovery. */\n recoverable: boolean;\n}\n\nexport interface UseThumbnailSourceOptions {\n /** A known server-rendered thumbnail URL (e.g. `FileRecord.thumbnail_url`). */\n thumbnailUrl?: string | null | undefined;\n /**\n * Whether to fall back to the source file itself when no rendered\n * thumbnail exists. Disable in dense pickers to avoid per-row byte loads.\n */\n allowSourceFallback?: boolean | undefined;\n}\n\nexport function useThumbnailSource(\n ref: MediaRefLike | null | undefined,\n options: UseThumbnailSourceOptions = {},\n): ThumbnailSourceState {\n const { thumbnailUrl, allowSourceFallback = true } = options;\n const client = useMediaClient();\n const { resolution } = useMediaResolution(ref);\n\n // Tier 1 — caller-supplied thumbnail URL, classified like everything else.\n const explicitThumb = useMemo(() => {\n if (!thumbnailUrl) return null;\n try {\n return client.resolve(thumbnailUrl);\n } catch {\n // A refused thumbnail URL (signed, foreign junk) falls to lower tiers.\n return null;\n }\n }, [client, thumbnailUrl]);\n\n // Tier 3 blob lane — only engaged when we would actually render the source,\n // which (matching the non-blob branch below and the original MediaThumbnail\n // `needsBytes` gate) means image/video kinds ONLY. Without the kind gate a\n // private markdown/PDF/archive file downloads its ENTIRE bytes just to feed\n // an <img> that can never decode them (the F3 defect class).\n const needsBlob =\n allowSourceFallback &&\n !explicitThumb &&\n !resolution?.thumbnailSrc &&\n resolution?.transport === \"blob\" &&\n (resolution.kind === \"image\" || resolution.kind === \"video\");\n const blob = useMediaBlob(needsBlob ? ref : null);\n\n if (explicitThumb) {\n return {\n src: explicitThumb.src,\n mode: \"thumbnail\",\n loading: false,\n resolution,\n recoverable: explicitThumb.recoverable !== false,\n };\n }\n if (resolution?.thumbnailSrc) {\n return {\n src: resolution.thumbnailSrc,\n mode: \"thumbnail\",\n loading: false,\n resolution,\n recoverable: resolution.recoverable !== false,\n };\n }\n if (allowSourceFallback && resolution) {\n if (needsBlob) {\n return {\n src: blob.url,\n mode: blob.url\n ? resolution.kind === \"video\"\n ? \"live-video-poster\"\n : \"live-image\"\n : \"icon\",\n loading: blob.loading,\n resolution,\n recoverable: false,\n };\n }\n if (resolution.kind === \"image\") {\n return {\n src: resolution.src,\n mode: \"live-image\",\n loading: false,\n resolution,\n recoverable: resolution.recoverable !== false,\n };\n }\n if (resolution.kind === \"video\") {\n return {\n src: resolution.src,\n mode: \"live-video-poster\",\n loading: false,\n resolution,\n recoverable: resolution.recoverable !== false,\n };\n }\n }\n return {\n src: null,\n mode: \"icon\",\n loading: false,\n resolution,\n recoverable: false,\n };\n}\n","/**\n * src/core/use-media-actions.ts — THE SHARE MANDATE's headless engine.\n *\n * Wires a MediaRefLike to the host's injected {@link MediaActionsPort}\n * (share / download / copy / open + the SharePopover slot the associations\n * package fills). FAILS CLOSED: with no port — or no handler for a given\n * action — the action reports unavailable and the shells render it disabled.\n * Nothing here mints URLs; handlers reach bytes/links only through the\n * MediaClient they close over.\n */\n\nimport { useCallback, useMemo, useState } from \"react\";\nimport type {\n MediaActionContext,\n MediaActionKind,\n MediaRefLike,\n MediaSharePopoverProps,\n MediaSlotComponent,\n} from \"../types\";\nimport { useMediaHostPorts } from \"./context\";\nimport { useMediaResolution } from \"./use-media-resolution\";\n\nexport interface UseMediaActionsOptions {\n fileName?: string | undefined;\n alt?: string | undefined;\n}\n\nexport interface MediaActionsState {\n /** Null when no ref was supplied. */\n context: MediaActionContext | null;\n /** True when the host wired a handler for this action (share also counts the popover). */\n isAvailable: (kind: MediaActionKind) => boolean;\n /** Run an action. No-op (resolved) when unavailable. */\n run: (kind: MediaActionKind) => Promise<void>;\n /** The action currently executing, or null. */\n busy: MediaActionKind | null;\n /** Last handler failure, surfaced as state for the shell to render. */\n lastError: { kind: MediaActionKind; message: string } | null;\n /** The rich share body slot (associations package), when injected. */\n SharePopover: MediaSlotComponent<MediaSharePopoverProps> | null;\n}\n\nexport function useMediaActions(\n ref: MediaRefLike | null | undefined,\n options: UseMediaActionsOptions = {},\n): MediaActionsState {\n const ports = useMediaHostPorts();\n const { resolution } = useMediaResolution(ref);\n const [busy, setBusy] = useState<MediaActionKind | null>(null);\n const [lastError, setLastError] = useState<{\n kind: MediaActionKind;\n message: string;\n } | null>(null);\n\n const actions = ports.actions;\n const { fileName, alt } = options;\n\n const context = useMemo<MediaActionContext | null>(() => {\n if (!ref) return null;\n return { ref, resolution, fileName, alt };\n }, [ref, resolution, fileName, alt]);\n\n const isAvailable = useCallback(\n (kind: MediaActionKind): boolean => {\n if (!context || !actions) return false;\n if (kind === \"share\") return Boolean(actions.share || actions.SharePopover);\n return Boolean(actions[kind]);\n },\n [context, actions],\n );\n\n const run = useCallback(\n async (kind: MediaActionKind): Promise<void> => {\n if (!context || !actions) return;\n const handler = actions[kind];\n if (!handler) return;\n setBusy(kind);\n setLastError(null);\n try {\n await handler(context);\n } catch (err) {\n setLastError({\n kind,\n message: err instanceof Error ? err.message : String(err),\n });\n } finally {\n setBusy(null);\n }\n },\n [context, actions],\n );\n\n return {\n context,\n isAvailable,\n run,\n busy,\n lastError,\n SharePopover: actions?.SharePopover ?? null,\n };\n}\n","/**\n * src/core/file-kind.ts — filename/MIME → category, icon name, color.\n *\n * Condensed port of the icon-relevant slice of matrx-frontend\n * `features/files/utils/file-types.ts` (the FILE_TYPES registry): every\n * extension→{category,icon,color,displayName} row is preserved; the\n * preview/thumbnail/size-cap columns stay in the app (C8 — they belong to\n * the previewer world, not the media kit). MIME-prefix fallback, the\n * dotfile-looks-like-text heuristic, folder details and the unknown default\n * are ported verbatim in behavior.\n *\n * Platform-free: icon NAMES only — `/react` maps names to its inlined SVGs;\n * a future `/react-native` binding maps the same names to its own glyphs.\n */\n\nexport type FileCategory =\n | \"CODE\"\n | \"DOCUMENT\"\n | \"AUDIO\"\n | \"IMAGE\"\n | \"VIDEO\"\n | \"ARCHIVE\"\n | \"DATA\"\n | \"NOTEBOOK\"\n | \"SUBTITLES\"\n | \"EMAIL\"\n | \"EBOOK\"\n | \"MODEL_3D\"\n | \"UNKNOWN\"\n | \"FOLDER\";\n\nexport type FileIconName =\n | \"image\"\n | \"pen-tool\"\n | \"video\"\n | \"music\"\n | \"file-type\"\n | \"file-text\"\n | \"code\"\n | \"braces\"\n | \"subtitles\"\n | \"file-code\"\n | \"file-json\"\n | \"file-spreadsheet\"\n | \"database\"\n | \"package\"\n | \"archive\"\n | \"folder\"\n | \"folder-open\"\n | \"file\"\n | \"file-audio\"\n | \"file-video\";\n\nexport interface FileKindDetails {\n category: FileCategory;\n displayName: string;\n /** Tailwind text color token, matching the original registry exactly. */\n colorClass: string;\n icon: FileIconName;\n}\n\ntype Row = [\n extensions: string[],\n category: FileCategory,\n colorClass: string,\n icon: FileIconName,\n displayName: string,\n];\n\n// Rows ported from the FILE_TYPES registry (icon-relevant columns), grouped\n// exactly as the original's icon/color assignments group them.\nconst ROWS: Row[] = [\n [[\"jpg\", \"jpeg\", \"png\", \"gif\", \"webp\", \"avif\", \"heic\", \"heif\", \"bmp\", \"tif\", \"tiff\", \"ico\"], \"IMAGE\", \"text-emerald-500\", \"image\", \"Image\"],\n [[\"svg\"], \"IMAGE\", \"text-amber-500\", \"pen-tool\", \"SVG image\"],\n [[\"mp4\", \"mov\", \"webm\", \"mkv\", \"avi\", \"m4v\"], \"VIDEO\", \"text-purple-500\", \"video\", \"Video\"],\n [[\"mp3\", \"wav\", \"ogg\", \"m4a\", \"aac\", \"flac\", \"opus\"], \"AUDIO\", \"text-pink-500\", \"music\", \"Audio\"],\n [[\"pdf\"], \"DOCUMENT\", \"text-red-500\", \"file-type\", \"PDF Document\"],\n [[\"md\", \"markdown\", \"mdx\", \"rst\", \"adoc\", \"asciidoc\", \"org\"], \"DOCUMENT\", \"text-slate-400\", \"file-text\", \"Markdown\"],\n [[\"txt\", \"text\", \"asc\", \"me\", \"log\", \"out\", \"err\"], \"DOCUMENT\", \"text-muted-foreground\", \"file-text\", \"Text\"],\n [[\"dockerfile\", \"containerfile\", \"lua\"], \"CODE\", \"text-blue-500\", \"code\", \"Code\"],\n [[\"mk\", \"make\", \"swift\", \"ml\", \"mli\", \"fs\", \"fsi\", \"fsx\", \"svelte\"], \"CODE\", \"text-orange-500\", \"code\", \"Code\"],\n [[\"diff\", \"patch\", \"pem\", \"csr\", \"crt\", \"cer\", \"der\", \"key\", \"pub\", \"p7b\", \"p7c\", \"pfx\", \"p12\"], \"CODE\", \"text-amber-500\", \"file-text\", \"Text\"],\n [[\"ini\", \"cfg\", \"conf\", \"config\", \"properties\", \"prefs\"], \"CODE\", \"text-amber-400\", \"braces\", \"Config\"],\n [[\"tex\", \"latex\", \"ltx\", \"sty\", \"cls\", \"bib\"], \"CODE\", \"text-emerald-500\", \"file-text\", \"LaTeX\"],\n [[\"srt\", \"vtt\"], \"SUBTITLES\", \"text-cyan-500\", \"subtitles\", \"Subtitles\"],\n [[\"js\", \"mjs\", \"cjs\", \"jsx\"], \"CODE\", \"text-yellow-500\", \"file-code\", \"JavaScript\"],\n [[\"ts\", \"tsx\"], \"CODE\", \"text-blue-500\", \"file-code\", \"TypeScript\"],\n [[\"py\", \"clj\", \"cljs\", \"cljc\", \"edn\", \"vue\", \"styl\", \"stylus\"], \"CODE\", \"text-emerald-500\", \"code\", \"Code\"],\n [[\"rb\", \"rbw\", \"rake\", \"ru\", \"gemspec\", \"scala\", \"sbt\"], \"CODE\", \"text-red-500\", \"code\", \"Code\"],\n [[\"go\", \"dart\"], \"CODE\", \"text-cyan-500\", \"code\", \"Code\"],\n [[\"rs\"], \"CODE\", \"text-orange-600\", \"code\", \"Rust\"],\n [[\"java\"], \"CODE\", \"text-red-600\", \"code\", \"Java\"],\n [[\"c\", \"h\"], \"CODE\", \"text-blue-600\", \"code\", \"C\"],\n [[\"cpp\", \"cc\", \"cxx\", \"hpp\"], \"CODE\", \"text-blue-700\", \"code\", \"C++\"],\n [[\"cs\"], \"CODE\", \"text-purple-600\", \"code\", \"C#\"],\n [[\"html\", \"htm\", \"kt\", \"kts\"], \"CODE\", \"text-orange-400\", \"code\", \"Code\"],\n [[\"css\"], \"CODE\", \"text-sky-400\", \"code\", \"CSS\"],\n [[\"scss\"], \"CODE\", \"text-pink-400\", \"code\", \"SCSS\"],\n [[\"sh\", \"bash\", \"zsh\", \"fish\", \"ksh\", \"ash\", \"bat\", \"cmd\", \"twig\", \"jinja\", \"j2\", \"njk\"], \"CODE\", \"text-emerald-400\", \"code\", \"Script\"],\n [[\"sql\"], \"CODE\", \"text-amber-400\", \"code\", \"SQL\"],\n [[\"pl\", \"pm\"], \"CODE\", \"text-indigo-400\", \"code\", \"Perl\"],\n [[\"r\", \"rmd\", \"proto\", \"ps1\", \"psm1\", \"psd1\", \"less\"], \"CODE\", \"text-blue-400\", \"code\", \"Code\"],\n [[\"ex\", \"exs\"], \"CODE\", \"text-purple-400\", \"code\", \"Elixir\"],\n [[\"erl\", \"hrl\"], \"CODE\", \"text-rose-500\", \"code\", \"Erlang\"],\n [[\"hs\", \"lhs\"], \"CODE\", \"text-violet-500\", \"code\", \"Haskell\"],\n [[\"zig\"], \"CODE\", \"text-yellow-500\", \"code\", \"Zig\"],\n [[\"nim\", \"nims\"], \"CODE\", \"text-yellow-400\", \"code\", \"Nim\"],\n [[\"jl\"], \"CODE\", \"text-purple-500\", \"code\", \"Julia\"],\n [[\"astro\"], \"CODE\", \"text-fuchsia-500\", \"code\", \"Astro\"],\n [[\"graphql\", \"gql\", \"graphqls\"], \"CODE\", \"text-pink-500\", \"code\", \"GraphQL\"],\n [[\"sol\"], \"CODE\", \"text-slate-400\", \"code\", \"Solidity\"],\n [[\"php\", \"phtml\", \"phps\"], \"CODE\", \"text-indigo-500\", \"code\", \"PHP\"],\n [[\"hbs\", \"handlebars\", \"mustache\", \"ejs\", \"liquid\"], \"CODE\", \"text-amber-500\", \"code\", \"Template\"],\n [[\"json\", \"jsonc\", \"json5\", \"har\", \"geojson\", \"topojson\"], \"DATA\", \"text-amber-500\", \"file-json\", \"JSON\"],\n [[\"yaml\", \"yml\", \"toml\"], \"CODE\", \"text-amber-500\", \"braces\", \"Config\"],\n [[\"xml\"], \"DATA\", \"text-amber-500\", \"braces\", \"XML\"],\n [[\"csv\", \"tsv\"], \"DATA\", \"text-orange-500\", \"file-spreadsheet\", \"Spreadsheet data\"],\n [[\"sqlite\", \"sqlite3\", \"db\"], \"DATA\", \"text-blue-400\", \"database\", \"Database\"],\n [[\"ttl\", \"n3\", \"nt\", \"nq\", \"trig\"], \"DATA\", \"text-fuchsia-400\", \"braces\", \"RDF data\"],\n [[\"xlsx\", \"xls\"], \"DOCUMENT\", \"text-emerald-600\", \"file-spreadsheet\", \"Excel workbook\"],\n [[\"doc\", \"docx\"], \"DOCUMENT\", \"text-blue-500\", \"file-text\", \"Word document\"],\n [[\"ppt\", \"pptx\"], \"DOCUMENT\", \"text-orange-600\", \"file-type\", \"Presentation\"],\n [[\"ipynb\"], \"NOTEBOOK\", \"text-orange-400\", \"file-json\", \"Notebook\"],\n [[\"epub\"], \"EBOOK\", \"text-indigo-500\", \"file-text\", \"eBook\"],\n [[\"eml\"], \"EMAIL\", \"text-blue-400\", \"file-text\", \"Email\"],\n [[\"glb\", \"gltf\", \"stl\", \"obj\", \"fbx\"], \"MODEL_3D\", \"text-violet-400\", \"package\", \"3D model\"],\n [[\"zip\", \"rar\", \"7z\"], \"ARCHIVE\", \"text-amber-600\", \"archive\", \"Archive\"],\n [[\"tar\", \"gz\", \"tgz\"], \"ARCHIVE\", \"text-amber-600\", \"package\", \"Archive\"],\n];\n\nconst BY_EXTENSION = new Map<string, FileKindDetails>();\nfor (const [extensions, category, colorClass, icon, displayName] of ROWS) {\n for (const ext of extensions) {\n BY_EXTENSION.set(ext, { category, colorClass, icon, displayName });\n }\n}\n\nconst UNKNOWN_DETAILS: FileKindDetails = {\n category: \"UNKNOWN\",\n displayName: \"File\",\n colorClass: \"text-muted-foreground\",\n icon: \"file\",\n};\n\nconst ASSUMED_TEXT_DETAILS: FileKindDetails = {\n category: \"DOCUMENT\",\n displayName: \"Text\",\n colorClass: \"text-muted-foreground\",\n icon: \"file-text\",\n};\n\nconst FOLDER_DETAILS: FileKindDetails = {\n category: \"FOLDER\",\n displayName: \"Folder\",\n colorClass: \"text-sky-500\",\n icon: \"folder\",\n};\n\nconst IMAGE_DETAILS = BY_EXTENSION.get(\"png\") as FileKindDetails;\nconst VIDEO_DETAILS = BY_EXTENSION.get(\"mp4\") as FileKindDetails;\nconst AUDIO_DETAILS = BY_EXTENSION.get(\"mp3\") as FileKindDetails;\nconst PDF_DETAILS = BY_EXTENSION.get(\"pdf\") as FileKindDetails;\nconst SVG_DETAILS = BY_EXTENSION.get(\"svg\") as FileKindDetails;\n\nfunction basenameOf(path: string): string {\n const idx = Math.max(path.lastIndexOf(\"/\"), path.lastIndexOf(\"\\\\\"));\n return idx >= 0 ? path.slice(idx + 1) : path;\n}\n\nfunction extOf(name: string): string | null {\n const idx = name.lastIndexOf(\".\");\n if (idx <= 0 || idx === name.length - 1) return null;\n return name.slice(idx + 1).toLowerCase();\n}\n\n/**\n * Dotfile heuristic (ported): unrecognized dotfiles are almost always text —\n * bias toward the text profile instead of the binary \"File\" default.\n */\nfunction dotfileLooksLikeText(filename: string): boolean {\n const name = basenameOf(filename);\n if (!name.startsWith(\".\")) return false;\n for (let i = 1; i < name.length; i++) {\n const c = name.charCodeAt(i);\n if (\n (c >= 48 && c <= 57) ||\n (c >= 65 && c <= 90) ||\n (c >= 97 && c <= 122) ||\n c === 95 ||\n c === 45\n ) {\n return true;\n }\n }\n return false;\n}\n\n/** Strip MIME parameters (`;codecs=opus`, etc.). */\nfunction canonicalMimeBase(raw: string): string {\n return raw.split(\";\")[0]?.trim().toLowerCase() ?? raw;\n}\n\n/**\n * Resolve icon details from a filename and (optionally) the server MIME.\n * MIME-prefix override handles \"the browser knows `image/avif` but the\n * extension table doesn't\" — and disambiguates audio/webm vs video/webm.\n */\nexport function getFileKindDetails(\n fileName: string,\n mimeType?: string | null,\n): FileKindDetails {\n const name = basenameOf(fileName);\n const lower = name.toLowerCase();\n const ext = lower === \"dockerfile\" || lower === \"containerfile\" ? lower : extOf(name);\n const fromExt = ext ? BY_EXTENSION.get(ext) : undefined;\n\n const mime = mimeType ? canonicalMimeBase(mimeType) : null;\n if (mime) {\n if (mime === \"image/svg+xml\") return fromExt ?? SVG_DETAILS;\n if (mime.startsWith(\"image/\")) return fromExt?.category === \"IMAGE\" ? fromExt : IMAGE_DETAILS;\n if (mime.startsWith(\"video/\")) return fromExt?.category === \"VIDEO\" ? fromExt : VIDEO_DETAILS;\n if (mime.startsWith(\"audio/\")) return fromExt?.category === \"AUDIO\" ? fromExt : AUDIO_DETAILS;\n if (mime === \"application/pdf\") return PDF_DETAILS;\n }\n\n if (fromExt) return fromExt;\n if (dotfileLooksLikeText(fileName)) return ASSUMED_TEXT_DETAILS;\n return UNKNOWN_DETAILS;\n}\n\n/** Folder icon details (open picks the open-folder glyph). */\nexport function getFolderKindDetails(open = false): FileKindDetails {\n return open ? { ...FOLDER_DETAILS, icon: \"folder-open\" } : FOLDER_DETAILS;\n}\n"],"mappings":"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","names":["import_react","import_react","import_react","import_react","import_react","import_react"]}
package/dist/core.js CHANGED
@@ -302,7 +302,7 @@ function useThumbnailSource(ref, options = {}) {
302
302
  return null;
303
303
  }
304
304
  }, [client, thumbnailUrl]);
305
- const needsBlob = allowSourceFallback && !explicitThumb && !resolution?.thumbnailSrc && resolution?.transport === "blob";
305
+ const needsBlob = allowSourceFallback && !explicitThumb && !resolution?.thumbnailSrc && resolution?.transport === "blob" && (resolution.kind === "image" || resolution.kind === "video");
306
306
  const blob = useMediaBlob(needsBlob ? ref : null);
307
307
  if (explicitThumb) {
308
308
  return {