@ai-matrx/media 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.cjs ADDED
@@ -0,0 +1,577 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/core.ts
22
+ var core_exports = {};
23
+ __export(core_exports, {
24
+ MediaProvider: () => MediaProvider,
25
+ getFileKindDetails: () => getFileKindDetails,
26
+ getFolderKindDetails: () => getFolderKindDetails,
27
+ stableRefKey: () => stableRefKey,
28
+ useMediaActions: () => useMediaActions,
29
+ useMediaBlob: () => useMediaBlob,
30
+ useMediaClient: () => useMediaClient,
31
+ useMediaHostPorts: () => useMediaHostPorts,
32
+ useMediaLoadRecovery: () => useMediaLoadRecovery,
33
+ useMediaResolution: () => useMediaResolution,
34
+ useMediaUpload: () => useMediaUpload,
35
+ useThumbnailSource: () => useThumbnailSource
36
+ });
37
+ module.exports = __toCommonJS(core_exports);
38
+
39
+ // src/core/context.ts
40
+ var import_react = require("react");
41
+ var CLIENT_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.media.client-context");
42
+ var PORTS_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.media.host-ports-context");
43
+ function clientContext() {
44
+ const host = globalThis;
45
+ host[CLIENT_SLOT] ??= (0, import_react.createContext)(null);
46
+ return host[CLIENT_SLOT];
47
+ }
48
+ var INERT_PORTS = {};
49
+ function portsContext() {
50
+ const host = globalThis;
51
+ host[PORTS_SLOT] ??= (0, import_react.createContext)(INERT_PORTS);
52
+ return host[PORTS_SLOT];
53
+ }
54
+ function MediaProvider({ client, ports, children }) {
55
+ const ClientCtx = clientContext();
56
+ const PortsCtx = portsContext();
57
+ return (0, import_react.createElement)(
58
+ ClientCtx.Provider,
59
+ { value: client },
60
+ (0, import_react.createElement)(PortsCtx.Provider, { value: ports ?? INERT_PORTS }, children)
61
+ );
62
+ }
63
+ function useMediaClient() {
64
+ const client = (0, import_react.useContext)(clientContext());
65
+ if (!client) {
66
+ throw new Error(
67
+ "@ai-matrx/media: no MediaClient. Mount <MediaProvider client={...}> above every media surface (canonically wired to @ai-matrx/data/files)."
68
+ );
69
+ }
70
+ return client;
71
+ }
72
+ function useMediaHostPorts() {
73
+ return (0, import_react.useContext)(portsContext());
74
+ }
75
+
76
+ // src/core/ref-key.ts
77
+ function stableRefKey(ref) {
78
+ if (!ref) return "<none>";
79
+ if (typeof ref === "string") return `s:${ref}`;
80
+ if (ref.file_id) return `id:${ref.file_id}:${ref.mime_type ?? ""}`;
81
+ if (ref.url) return `url:${ref.url}:${ref.mime_type ?? ""}`;
82
+ return "<empty-ref>";
83
+ }
84
+
85
+ // src/core/use-media-resolution.ts
86
+ var import_react2 = require("react");
87
+ function useMediaResolution(ref) {
88
+ const client = useMediaClient();
89
+ const refKey = stableRefKey(ref);
90
+ return (0, import_react2.useMemo)(() => {
91
+ if (!ref) return { resolution: null, status: "empty", reason: null };
92
+ try {
93
+ const resolution = client.resolve(ref);
94
+ if (!resolution) return { resolution: null, status: "empty", reason: null };
95
+ return { resolution, status: "ready", reason: null };
96
+ } catch (err) {
97
+ return {
98
+ resolution: null,
99
+ status: "unavailable",
100
+ reason: client.classifyError(err)
101
+ };
102
+ }
103
+ }, [client, refKey]);
104
+ }
105
+
106
+ // src/core/use-media-load-recovery.ts
107
+ var import_react3 = require("react");
108
+
109
+ // src/types.ts
110
+ function mintDurableSrc(url) {
111
+ return url;
112
+ }
113
+
114
+ // src/core/use-media-load-recovery.ts
115
+ function useMediaLoadRecovery(src, options) {
116
+ const client = useMediaClient();
117
+ const recoverable = options?.recoverable !== false;
118
+ const onTerminal = options?.onTerminal;
119
+ const [result, setResult] = (0, import_react3.useState)({ source: src, retryKey: 0, failed: false });
120
+ const attemptRef = (0, import_react3.useRef)(0);
121
+ (0, import_react3.useEffect)(() => {
122
+ attemptRef.current = 0;
123
+ }, [src]);
124
+ const onLoadError = (event) => {
125
+ if (!src || !recoverable) {
126
+ setResult({ source: src, retryKey: 0, failed: true });
127
+ onTerminal?.(event);
128
+ return;
129
+ }
130
+ attemptRef.current += 1;
131
+ const attempt = attemptRef.current;
132
+ client.recoverLoadError(mintDurableSrc(src), attempt).then(
133
+ (verdict) => {
134
+ if (verdict === "retry") {
135
+ setResult((current) => ({
136
+ source: src,
137
+ retryKey: current.source === src ? current.retryKey + 1 : 1,
138
+ failed: false
139
+ }));
140
+ } else {
141
+ setResult({ source: src, retryKey: 0, failed: true });
142
+ onTerminal?.(event);
143
+ }
144
+ },
145
+ () => {
146
+ setResult({ source: src, retryKey: 0, failed: true });
147
+ onTerminal?.(event);
148
+ }
149
+ );
150
+ };
151
+ return {
152
+ retryKey: result.source === src ? result.retryKey : 0,
153
+ onLoadError,
154
+ failed: result.source === src ? result.failed : false
155
+ };
156
+ }
157
+
158
+ // src/core/use-media-blob.ts
159
+ var import_react4 = require("react");
160
+ function useMediaBlob(ref) {
161
+ const client = useMediaClient();
162
+ const refKey = stableRefKey(ref);
163
+ const [state, setState] = (0, import_react4.useState)({ url: null, blob: null, loading: !!ref, error: null });
164
+ const [retryToken, setRetryToken] = (0, import_react4.useState)(0);
165
+ (0, import_react4.useEffect)(() => {
166
+ if (!ref) {
167
+ setState({ url: null, blob: null, loading: false, error: null });
168
+ return void 0;
169
+ }
170
+ let cancelled = false;
171
+ let release = null;
172
+ setState({ url: null, blob: null, loading: true, error: null });
173
+ client.getBlob(ref).then(
174
+ (handle) => {
175
+ if (cancelled) {
176
+ handle.release();
177
+ return;
178
+ }
179
+ release = handle.release;
180
+ setState({ url: handle.url, blob: handle.blob, loading: false, error: null });
181
+ },
182
+ (err) => {
183
+ if (cancelled) return;
184
+ const message = err instanceof Error ? err.message : String(err ?? "Failed to load file");
185
+ setState({ url: null, blob: null, loading: false, error: message });
186
+ }
187
+ );
188
+ return () => {
189
+ cancelled = true;
190
+ release?.();
191
+ };
192
+ }, [client, refKey, retryToken]);
193
+ return {
194
+ ...state,
195
+ retry: () => setRetryToken((t) => t + 1)
196
+ };
197
+ }
198
+
199
+ // src/core/use-media-upload.ts
200
+ var import_react5 = require("react");
201
+ var entrySeq = 0;
202
+ function useMediaUpload() {
203
+ const client = useMediaClient();
204
+ const [uploading, setUploading] = (0, import_react5.useState)(false);
205
+ const [progress, setProgress] = (0, import_react5.useState)(null);
206
+ const [result, setResult] = (0, import_react5.useState)(null);
207
+ const [error, setError] = (0, import_react5.useState)(null);
208
+ const [entries, setEntries] = (0, import_react5.useState)([]);
209
+ const activeCount = (0, import_react5.useRef)(0);
210
+ const patchEntry = (0, import_react5.useCallback)(
211
+ (id, patch) => {
212
+ setEntries(
213
+ (current) => current.map((e) => e.id === id ? { ...e, ...patch } : e)
214
+ );
215
+ },
216
+ []
217
+ );
218
+ const upload = (0, import_react5.useCallback)(
219
+ async (file, opts = {}) => {
220
+ const id = `up-${++entrySeq}`;
221
+ const fileName = opts.fileName ?? (file instanceof File ? file.name : "file");
222
+ setEntries((current) => [
223
+ ...current,
224
+ {
225
+ id,
226
+ fileName,
227
+ fileSize: file.size,
228
+ bytesUploaded: 0,
229
+ status: "uploading",
230
+ error: null,
231
+ fileId: null,
232
+ completedAt: null
233
+ }
234
+ ]);
235
+ activeCount.current += 1;
236
+ setUploading(true);
237
+ setError(null);
238
+ setProgress(null);
239
+ try {
240
+ const uploaded = await client.upload(file, {
241
+ ...opts,
242
+ onProgress: (loaded, total) => {
243
+ setProgress({ loaded, total, ratio: total > 0 ? loaded / total : 0 });
244
+ patchEntry(id, { bytesUploaded: loaded });
245
+ opts.onProgress?.(loaded, total);
246
+ }
247
+ });
248
+ setResult(uploaded);
249
+ patchEntry(id, {
250
+ status: "success",
251
+ fileId: uploaded.fileId,
252
+ bytesUploaded: file.size,
253
+ completedAt: Date.now()
254
+ });
255
+ return uploaded;
256
+ } catch (err) {
257
+ const e = err instanceof Error ? err : new Error(String(err));
258
+ setError(e);
259
+ patchEntry(id, {
260
+ status: "error",
261
+ error: e.message,
262
+ completedAt: Date.now()
263
+ });
264
+ throw e;
265
+ } finally {
266
+ activeCount.current -= 1;
267
+ if (activeCount.current <= 0) setUploading(false);
268
+ }
269
+ },
270
+ [client, patchEntry]
271
+ );
272
+ const uploadMany = (0, import_react5.useCallback)(
273
+ async (files, opts = {}) => {
274
+ if (client.uploadMany) {
275
+ activeCount.current += 1;
276
+ setUploading(true);
277
+ try {
278
+ return await client.uploadMany(files, opts);
279
+ } finally {
280
+ activeCount.current -= 1;
281
+ if (activeCount.current <= 0) setUploading(false);
282
+ }
283
+ }
284
+ const uploaded = [];
285
+ const failed = [];
286
+ for (const file of files) {
287
+ try {
288
+ const one = await upload(file, opts);
289
+ uploaded.push(one.fileId);
290
+ } catch (err) {
291
+ failed.push({
292
+ name: file.name,
293
+ error: err instanceof Error ? err.message : String(err)
294
+ });
295
+ }
296
+ }
297
+ return { uploaded, failed, cancelled: false };
298
+ },
299
+ [client, upload]
300
+ );
301
+ const clearEntry = (0, import_react5.useCallback)((id) => {
302
+ setEntries((current) => current.filter((e) => e.id !== id));
303
+ }, []);
304
+ const reset = (0, import_react5.useCallback)(() => {
305
+ setResult(null);
306
+ setError(null);
307
+ setProgress(null);
308
+ setEntries([]);
309
+ }, []);
310
+ return {
311
+ upload,
312
+ uploadMany,
313
+ uploading,
314
+ progress,
315
+ result,
316
+ error,
317
+ entries,
318
+ clearEntry,
319
+ reset
320
+ };
321
+ }
322
+
323
+ // src/core/use-thumbnail-source.ts
324
+ var import_react6 = require("react");
325
+ function useThumbnailSource(ref, options = {}) {
326
+ const { thumbnailUrl, allowSourceFallback = true } = options;
327
+ const client = useMediaClient();
328
+ const { resolution } = useMediaResolution(ref);
329
+ const explicitThumb = (0, import_react6.useMemo)(() => {
330
+ if (!thumbnailUrl) return null;
331
+ try {
332
+ return client.resolve(thumbnailUrl);
333
+ } catch {
334
+ return null;
335
+ }
336
+ }, [client, thumbnailUrl]);
337
+ const needsBlob = allowSourceFallback && !explicitThumb && !resolution?.thumbnailSrc && resolution?.transport === "blob";
338
+ const blob = useMediaBlob(needsBlob ? ref : null);
339
+ if (explicitThumb) {
340
+ return {
341
+ src: explicitThumb.src,
342
+ mode: "thumbnail",
343
+ loading: false,
344
+ resolution,
345
+ recoverable: explicitThumb.recoverable !== false
346
+ };
347
+ }
348
+ if (resolution?.thumbnailSrc) {
349
+ return {
350
+ src: resolution.thumbnailSrc,
351
+ mode: "thumbnail",
352
+ loading: false,
353
+ resolution,
354
+ recoverable: resolution.recoverable !== false
355
+ };
356
+ }
357
+ if (allowSourceFallback && resolution) {
358
+ if (needsBlob) {
359
+ return {
360
+ src: blob.url,
361
+ mode: blob.url ? resolution.kind === "video" ? "live-video-poster" : "live-image" : "icon",
362
+ loading: blob.loading,
363
+ resolution,
364
+ recoverable: false
365
+ };
366
+ }
367
+ if (resolution.kind === "image") {
368
+ return {
369
+ src: resolution.src,
370
+ mode: "live-image",
371
+ loading: false,
372
+ resolution,
373
+ recoverable: resolution.recoverable !== false
374
+ };
375
+ }
376
+ if (resolution.kind === "video") {
377
+ return {
378
+ src: resolution.src,
379
+ mode: "live-video-poster",
380
+ loading: false,
381
+ resolution,
382
+ recoverable: resolution.recoverable !== false
383
+ };
384
+ }
385
+ }
386
+ return {
387
+ src: null,
388
+ mode: "icon",
389
+ loading: false,
390
+ resolution,
391
+ recoverable: false
392
+ };
393
+ }
394
+
395
+ // src/core/use-media-actions.ts
396
+ var import_react7 = require("react");
397
+ function useMediaActions(ref, options = {}) {
398
+ const ports = useMediaHostPorts();
399
+ const { resolution } = useMediaResolution(ref);
400
+ const [busy, setBusy] = (0, import_react7.useState)(null);
401
+ const [lastError, setLastError] = (0, import_react7.useState)(null);
402
+ const actions = ports.actions;
403
+ const { fileName, alt } = options;
404
+ const context = (0, import_react7.useMemo)(() => {
405
+ if (!ref) return null;
406
+ return { ref, resolution, fileName, alt };
407
+ }, [ref, resolution, fileName, alt]);
408
+ const isAvailable = (0, import_react7.useCallback)(
409
+ (kind) => {
410
+ if (!context || !actions) return false;
411
+ if (kind === "share") return Boolean(actions.share || actions.SharePopover);
412
+ return Boolean(actions[kind]);
413
+ },
414
+ [context, actions]
415
+ );
416
+ const run = (0, import_react7.useCallback)(
417
+ async (kind) => {
418
+ if (!context || !actions) return;
419
+ const handler = actions[kind];
420
+ if (!handler) return;
421
+ setBusy(kind);
422
+ setLastError(null);
423
+ try {
424
+ await handler(context);
425
+ } catch (err) {
426
+ setLastError({
427
+ kind,
428
+ message: err instanceof Error ? err.message : String(err)
429
+ });
430
+ } finally {
431
+ setBusy(null);
432
+ }
433
+ },
434
+ [context, actions]
435
+ );
436
+ return {
437
+ context,
438
+ isAvailable,
439
+ run,
440
+ busy,
441
+ lastError,
442
+ SharePopover: actions?.SharePopover ?? null
443
+ };
444
+ }
445
+
446
+ // src/core/file-kind.ts
447
+ var ROWS = [
448
+ [["jpg", "jpeg", "png", "gif", "webp", "avif", "heic", "heif", "bmp", "tif", "tiff", "ico"], "IMAGE", "text-emerald-500", "image", "Image"],
449
+ [["svg"], "IMAGE", "text-amber-500", "pen-tool", "SVG image"],
450
+ [["mp4", "mov", "webm", "mkv", "avi", "m4v"], "VIDEO", "text-purple-500", "video", "Video"],
451
+ [["mp3", "wav", "ogg", "m4a", "aac", "flac", "opus"], "AUDIO", "text-pink-500", "music", "Audio"],
452
+ [["pdf"], "DOCUMENT", "text-red-500", "file-type", "PDF Document"],
453
+ [["md", "markdown", "mdx", "rst", "adoc", "asciidoc", "org"], "DOCUMENT", "text-slate-400", "file-text", "Markdown"],
454
+ [["txt", "text", "asc", "me", "log", "out", "err"], "DOCUMENT", "text-muted-foreground", "file-text", "Text"],
455
+ [["dockerfile", "containerfile", "lua"], "CODE", "text-blue-500", "code", "Code"],
456
+ [["mk", "make", "swift", "ml", "mli", "fs", "fsi", "fsx", "svelte"], "CODE", "text-orange-500", "code", "Code"],
457
+ [["diff", "patch", "pem", "csr", "crt", "cer", "der", "key", "pub", "p7b", "p7c", "pfx", "p12"], "CODE", "text-amber-500", "file-text", "Text"],
458
+ [["ini", "cfg", "conf", "config", "properties", "prefs"], "CODE", "text-amber-400", "braces", "Config"],
459
+ [["tex", "latex", "ltx", "sty", "cls", "bib"], "CODE", "text-emerald-500", "file-text", "LaTeX"],
460
+ [["srt", "vtt"], "SUBTITLES", "text-cyan-500", "subtitles", "Subtitles"],
461
+ [["js", "mjs", "cjs", "jsx"], "CODE", "text-yellow-500", "file-code", "JavaScript"],
462
+ [["ts", "tsx"], "CODE", "text-blue-500", "file-code", "TypeScript"],
463
+ [["py", "clj", "cljs", "cljc", "edn", "vue", "styl", "stylus"], "CODE", "text-emerald-500", "code", "Code"],
464
+ [["rb", "rbw", "rake", "ru", "gemspec", "scala", "sbt"], "CODE", "text-red-500", "code", "Code"],
465
+ [["go", "dart"], "CODE", "text-cyan-500", "code", "Code"],
466
+ [["rs"], "CODE", "text-orange-600", "code", "Rust"],
467
+ [["java"], "CODE", "text-red-600", "code", "Java"],
468
+ [["c", "h"], "CODE", "text-blue-600", "code", "C"],
469
+ [["cpp", "cc", "cxx", "hpp"], "CODE", "text-blue-700", "code", "C++"],
470
+ [["cs"], "CODE", "text-purple-600", "code", "C#"],
471
+ [["html", "htm", "kt", "kts"], "CODE", "text-orange-400", "code", "Code"],
472
+ [["css"], "CODE", "text-sky-400", "code", "CSS"],
473
+ [["scss"], "CODE", "text-pink-400", "code", "SCSS"],
474
+ [["sh", "bash", "zsh", "fish", "ksh", "ash", "bat", "cmd", "twig", "jinja", "j2", "njk"], "CODE", "text-emerald-400", "code", "Script"],
475
+ [["sql"], "CODE", "text-amber-400", "code", "SQL"],
476
+ [["pl", "pm"], "CODE", "text-indigo-400", "code", "Perl"],
477
+ [["r", "rmd", "proto", "ps1", "psm1", "psd1", "less"], "CODE", "text-blue-400", "code", "Code"],
478
+ [["ex", "exs"], "CODE", "text-purple-400", "code", "Elixir"],
479
+ [["erl", "hrl"], "CODE", "text-rose-500", "code", "Erlang"],
480
+ [["hs", "lhs"], "CODE", "text-violet-500", "code", "Haskell"],
481
+ [["zig"], "CODE", "text-yellow-500", "code", "Zig"],
482
+ [["nim", "nims"], "CODE", "text-yellow-400", "code", "Nim"],
483
+ [["jl"], "CODE", "text-purple-500", "code", "Julia"],
484
+ [["astro"], "CODE", "text-fuchsia-500", "code", "Astro"],
485
+ [["graphql", "gql", "graphqls"], "CODE", "text-pink-500", "code", "GraphQL"],
486
+ [["sol"], "CODE", "text-slate-400", "code", "Solidity"],
487
+ [["php", "phtml", "phps"], "CODE", "text-indigo-500", "code", "PHP"],
488
+ [["hbs", "handlebars", "mustache", "ejs", "liquid"], "CODE", "text-amber-500", "code", "Template"],
489
+ [["json", "jsonc", "json5", "har", "geojson", "topojson"], "DATA", "text-amber-500", "file-json", "JSON"],
490
+ [["yaml", "yml", "toml"], "CODE", "text-amber-500", "braces", "Config"],
491
+ [["xml"], "DATA", "text-amber-500", "braces", "XML"],
492
+ [["csv", "tsv"], "DATA", "text-orange-500", "file-spreadsheet", "Spreadsheet data"],
493
+ [["sqlite", "sqlite3", "db"], "DATA", "text-blue-400", "database", "Database"],
494
+ [["ttl", "n3", "nt", "nq", "trig"], "DATA", "text-fuchsia-400", "braces", "RDF data"],
495
+ [["xlsx", "xls"], "DOCUMENT", "text-emerald-600", "file-spreadsheet", "Excel workbook"],
496
+ [["doc", "docx"], "DOCUMENT", "text-blue-500", "file-text", "Word document"],
497
+ [["ppt", "pptx"], "DOCUMENT", "text-orange-600", "file-type", "Presentation"],
498
+ [["ipynb"], "NOTEBOOK", "text-orange-400", "file-json", "Notebook"],
499
+ [["epub"], "EBOOK", "text-indigo-500", "file-text", "eBook"],
500
+ [["eml"], "EMAIL", "text-blue-400", "file-text", "Email"],
501
+ [["glb", "gltf", "stl", "obj", "fbx"], "MODEL_3D", "text-violet-400", "package", "3D model"],
502
+ [["zip", "rar", "7z"], "ARCHIVE", "text-amber-600", "archive", "Archive"],
503
+ [["tar", "gz", "tgz"], "ARCHIVE", "text-amber-600", "package", "Archive"]
504
+ ];
505
+ var BY_EXTENSION = /* @__PURE__ */ new Map();
506
+ for (const [extensions, category, colorClass, icon, displayName] of ROWS) {
507
+ for (const ext of extensions) {
508
+ BY_EXTENSION.set(ext, { category, colorClass, icon, displayName });
509
+ }
510
+ }
511
+ var UNKNOWN_DETAILS = {
512
+ category: "UNKNOWN",
513
+ displayName: "File",
514
+ colorClass: "text-muted-foreground",
515
+ icon: "file"
516
+ };
517
+ var ASSUMED_TEXT_DETAILS = {
518
+ category: "DOCUMENT",
519
+ displayName: "Text",
520
+ colorClass: "text-muted-foreground",
521
+ icon: "file-text"
522
+ };
523
+ var FOLDER_DETAILS = {
524
+ category: "FOLDER",
525
+ displayName: "Folder",
526
+ colorClass: "text-sky-500",
527
+ icon: "folder"
528
+ };
529
+ var IMAGE_DETAILS = BY_EXTENSION.get("png");
530
+ var VIDEO_DETAILS = BY_EXTENSION.get("mp4");
531
+ var AUDIO_DETAILS = BY_EXTENSION.get("mp3");
532
+ var PDF_DETAILS = BY_EXTENSION.get("pdf");
533
+ var SVG_DETAILS = BY_EXTENSION.get("svg");
534
+ function basenameOf(path) {
535
+ const idx = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
536
+ return idx >= 0 ? path.slice(idx + 1) : path;
537
+ }
538
+ function extOf(name) {
539
+ const idx = name.lastIndexOf(".");
540
+ if (idx <= 0 || idx === name.length - 1) return null;
541
+ return name.slice(idx + 1).toLowerCase();
542
+ }
543
+ function dotfileLooksLikeText(filename) {
544
+ const name = basenameOf(filename);
545
+ if (!name.startsWith(".")) return false;
546
+ for (let i = 1; i < name.length; i++) {
547
+ const c = name.charCodeAt(i);
548
+ if (c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 45) {
549
+ return true;
550
+ }
551
+ }
552
+ return false;
553
+ }
554
+ function canonicalMimeBase(raw) {
555
+ return raw.split(";")[0]?.trim().toLowerCase() ?? raw;
556
+ }
557
+ function getFileKindDetails(fileName, mimeType) {
558
+ const name = basenameOf(fileName);
559
+ const lower = name.toLowerCase();
560
+ const ext = lower === "dockerfile" || lower === "containerfile" ? lower : extOf(name);
561
+ const fromExt = ext ? BY_EXTENSION.get(ext) : void 0;
562
+ const mime = mimeType ? canonicalMimeBase(mimeType) : null;
563
+ if (mime) {
564
+ if (mime === "image/svg+xml") return fromExt ?? SVG_DETAILS;
565
+ if (mime.startsWith("image/")) return fromExt?.category === "IMAGE" ? fromExt : IMAGE_DETAILS;
566
+ if (mime.startsWith("video/")) return fromExt?.category === "VIDEO" ? fromExt : VIDEO_DETAILS;
567
+ if (mime.startsWith("audio/")) return fromExt?.category === "AUDIO" ? fromExt : AUDIO_DETAILS;
568
+ if (mime === "application/pdf") return PDF_DETAILS;
569
+ }
570
+ if (fromExt) return fromExt;
571
+ if (dotfileLooksLikeText(fileName)) return ASSUMED_TEXT_DETAILS;
572
+ return UNKNOWN_DETAILS;
573
+ }
574
+ function getFolderKindDetails(open = false) {
575
+ return open ? { ...FOLDER_DETAILS, icon: "folder-open" } : FOLDER_DETAILS;
576
+ }
577
+ //# sourceMappingURL=core.cjs.map