@tangle-network/ui 11.7.0 → 11.8.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.
@@ -1,9 +1,16 @@
1
+ import {
2
+ formatBytes
3
+ } from "./chunk-4CLN43XT.js";
1
4
  import {
2
5
  detectFileFormat,
3
6
  fileExtension,
4
7
  getCodeLanguage,
5
- getFormatLabel
6
- } from "./chunk-ZRVH3WCA.js";
8
+ getFormatLabel,
9
+ resolveFilePreviewKind
10
+ } from "./chunk-XU3HB5QL.js";
11
+ import {
12
+ Button
13
+ } from "./chunk-MKTSMWVD.js";
7
14
  import {
8
15
  ArtifactPane
9
16
  } from "./chunk-LTEKI7YJ.js";
@@ -315,19 +322,26 @@ function RichFileTree({
315
322
  }
316
323
 
317
324
  // src/files/file-preview.tsx
325
+ import { useState as useState2 } from "react";
318
326
  import {
319
327
  Download,
320
- X,
321
- FileText as FileText2
328
+ FileSpreadsheet as FileSpreadsheet2,
329
+ FileText as FileText2,
330
+ Music,
331
+ X
322
332
  } from "lucide-react";
323
333
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
334
+ var CSV_PREVIEW_ROW_LIMIT = 500;
335
+ var NEEDS_DOWNLOAD_LINK = "This file needs a download link to preview.";
336
+ var NO_INLINE_PREVIEW = "This file type has no inline preview.";
337
+ var NO_INLINE_CONTENT = "This file has no inline content to preview yet.";
324
338
  function CodePreview({
325
339
  content,
326
340
  filename,
327
- format
341
+ kind
328
342
  }) {
329
343
  const lineCount = content.split("\n").length;
330
- const language = getCodeLanguage(filename, format);
344
+ const language = getCodeLanguage(filename, kind);
331
345
  const labelToken = fileExtension(filename) || language || "txt";
332
346
  return /* @__PURE__ */ jsx3(
333
347
  CodeBlock,
@@ -341,112 +355,272 @@ function CodePreview({
341
355
  }
342
356
  );
343
357
  }
344
- function parseCsvRow(line) {
345
- const cells = [];
346
- let current = "";
358
+ function parseCsv(content) {
359
+ const rows = [];
360
+ let row = [];
361
+ let cell = "";
362
+ let quoted = false;
347
363
  let inQuotes = false;
348
- for (let index = 0; index < line.length; index += 1) {
349
- const char = line[index];
350
- const next = line[index + 1];
351
- if (char === '"') {
352
- if (inQuotes && next === '"') {
353
- current += '"';
354
- index += 1;
355
- continue;
364
+ const endCell = () => {
365
+ row.push(quoted ? cell : cell.trim());
366
+ cell = "";
367
+ quoted = false;
368
+ };
369
+ const endRow = () => {
370
+ endCell();
371
+ if (row.length > 1 || row[0] !== "") rows.push(row);
372
+ row = [];
373
+ };
374
+ for (let index = 0; index < content.length; index += 1) {
375
+ const char = content[index];
376
+ if (inQuotes) {
377
+ if (char === '"') {
378
+ if (content[index + 1] === '"') {
379
+ cell += '"';
380
+ index += 1;
381
+ } else {
382
+ inQuotes = false;
383
+ }
384
+ } else {
385
+ cell += char;
356
386
  }
357
- inQuotes = !inQuotes;
358
387
  continue;
359
388
  }
360
- if (char === "," && !inQuotes) {
361
- cells.push(current.trim());
362
- current = "";
389
+ if (char === '"') {
390
+ inQuotes = true;
391
+ quoted = true;
392
+ continue;
393
+ }
394
+ if (char === ",") {
395
+ endCell();
396
+ continue;
397
+ }
398
+ if (char === "\r" || char === "\n") {
399
+ if (char === "\r" && content[index + 1] === "\n") index += 1;
400
+ endRow();
363
401
  continue;
364
402
  }
365
- current += char;
403
+ cell += char;
366
404
  }
367
- cells.push(current.trim());
368
- return cells;
405
+ if (cell.length > 0 || quoted || row.length > 0) endRow();
406
+ return rows;
369
407
  }
370
408
  function CsvPreview({ content }) {
371
- const lines = content.trim().split(/\r?\n/).filter(Boolean);
372
- if (lines.length === 0) return null;
373
- const headers = parseCsvRow(lines[0]).map((header) => header.replace(/^"|"$/g, ""));
374
- const rows = lines.slice(1).map(
375
- (line) => parseCsvRow(line).map((cell) => cell.replace(/^"|"$/g, ""))
376
- );
377
- return /* @__PURE__ */ jsx3("div", { className: "overflow-auto max-h-[70vh] rounded-[var(--radius-md)] border border-border", children: /* @__PURE__ */ jsxs2("table", { className: "w-full text-sm", children: [
378
- /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsx3("tr", { className: "bg-muted/50 sticky top-0", children: headers.map((h, i) => /* @__PURE__ */ jsx3(
379
- "th",
380
- {
381
- className: "px-3 py-2 text-left text-xs font-semibold text-foreground border-b border-border whitespace-nowrap",
382
- children: h
383
- },
384
- i
385
- )) }) }),
386
- /* @__PURE__ */ jsx3("tbody", { children: rows.map((row, i) => /* @__PURE__ */ jsx3("tr", { className: "border-b border-border hover:bg-accent", children: row.map((cell, j) => /* @__PURE__ */ jsx3(
387
- "td",
388
- {
389
- className: "px-3 py-1.5 text-foreground font-mono text-xs whitespace-nowrap",
390
- children: cell
391
- },
392
- j
393
- )) }, i)) })
394
- ] }) });
409
+ const [headers = [], ...body] = parseCsv(content);
410
+ if (headers.length === 0) return null;
411
+ const visible = body.slice(0, CSV_PREVIEW_ROW_LIMIT);
412
+ const columnCount = Math.max(headers.length, ...visible.map((cells) => cells.length));
413
+ const capped = body.length > visible.length;
414
+ return /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1 flex-col gap-2", children: [
415
+ /* @__PURE__ */ jsx3("div", { className: "min-h-0 overflow-auto rounded-[var(--radius-md)] border border-border", children: /* @__PURE__ */ jsxs2("table", { className: "w-full text-sm", children: [
416
+ /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsx3("tr", { children: Array.from({ length: columnCount }, (_, column) => /* @__PURE__ */ jsx3(
417
+ "th",
418
+ {
419
+ className: "sticky top-0 whitespace-nowrap border-b border-border bg-card px-3 py-2 text-left text-xs font-semibold text-foreground",
420
+ children: headers[column] ?? ""
421
+ },
422
+ column
423
+ )) }) }),
424
+ /* @__PURE__ */ jsx3("tbody", { children: visible.map((cells, rowIndex) => /* @__PURE__ */ jsx3("tr", { className: "border-b border-border hover:bg-accent", children: Array.from({ length: columnCount }, (_, column) => /* @__PURE__ */ jsx3(
425
+ "td",
426
+ {
427
+ className: "whitespace-nowrap px-3 py-1.5 font-mono text-xs text-foreground",
428
+ children: cells[column] ?? ""
429
+ },
430
+ column
431
+ )) }, rowIndex)) })
432
+ ] }) }),
433
+ /* @__PURE__ */ jsxs2("p", { className: "shrink-0 text-xs text-muted-foreground", children: [
434
+ capped ? `Showing ${visible.length} of ${body.length} rows` : `${body.length} ${body.length === 1 ? "row" : "rows"}`,
435
+ ` \xB7 ${columnCount} ${columnCount === 1 ? "column" : "columns"}`
436
+ ] })
437
+ ] });
395
438
  }
439
+ var CHECKER_STYLE = {
440
+ backgroundColor: "var(--color-background)",
441
+ backgroundImage: "repeating-conic-gradient(var(--color-muted) 0% 25%, transparent 0% 50%)",
442
+ backgroundSize: "16px 16px"
443
+ };
396
444
  function ImagePreview({ src, filename }) {
397
- return /* @__PURE__ */ jsx3("div", { className: "flex items-center justify-center p-4 bg-background rounded-[var(--radius-md)] border border-border", children: /* @__PURE__ */ jsx3("img", { src, alt: filename, className: "max-w-full max-h-[70vh] object-contain rounded" }) });
445
+ const [naturalSize, setNaturalSize] = useState2(false);
446
+ return /* @__PURE__ */ jsx3(
447
+ "div",
448
+ {
449
+ className: "flex min-h-[12rem] flex-1 overflow-auto rounded-[var(--radius-md)] border border-border",
450
+ style: CHECKER_STYLE,
451
+ children: /* @__PURE__ */ jsx3(
452
+ "button",
453
+ {
454
+ type: "button",
455
+ onClick: () => setNaturalSize((value) => !value),
456
+ "aria-pressed": naturalSize,
457
+ "aria-label": naturalSize ? "Fit image to pane" : "Show image at natural size",
458
+ className: cn(
459
+ "flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/60",
460
+ naturalSize ? "m-auto cursor-zoom-out" : "h-full w-full cursor-zoom-in p-4"
461
+ ),
462
+ children: /* @__PURE__ */ jsx3(
463
+ "img",
464
+ {
465
+ src,
466
+ alt: filename,
467
+ draggable: false,
468
+ className: naturalSize ? "max-w-none" : "max-h-full max-w-full object-contain"
469
+ }
470
+ )
471
+ }
472
+ )
473
+ }
474
+ );
398
475
  }
399
- function PdfPreview({ blobUrl, filename }) {
400
- return /* @__PURE__ */ jsx3("div", { className: "rounded-[var(--radius-md)] border border-border overflow-hidden bg-background", children: /* @__PURE__ */ jsx3(
401
- "iframe",
476
+ function PdfPreview({
477
+ blobUrl,
478
+ filename,
479
+ size,
480
+ onDownload
481
+ }) {
482
+ return /* @__PURE__ */ jsx3(
483
+ "object",
402
484
  {
403
- src: blobUrl,
485
+ data: blobUrl,
486
+ type: "application/pdf",
404
487
  title: filename,
405
- className: "w-full h-[70vh] border-0"
488
+ className: "min-h-[24rem] w-full flex-1 rounded-[var(--radius-md)]",
489
+ children: /* @__PURE__ */ jsx3(
490
+ DownloadCard,
491
+ {
492
+ filename,
493
+ size,
494
+ sentence: "This browser does not show PDFs inline.",
495
+ onDownload,
496
+ className: "h-full"
497
+ }
498
+ )
499
+ }
500
+ );
501
+ }
502
+ function VideoPreview({ src, filename }) {
503
+ return /* @__PURE__ */ jsx3("div", { className: "flex min-h-[12rem] flex-1 items-center justify-center overflow-hidden rounded-[var(--radius-md)] border border-border bg-background", children: /* @__PURE__ */ jsx3(
504
+ "video",
505
+ {
506
+ controls: true,
507
+ preload: "metadata",
508
+ src,
509
+ "aria-label": filename,
510
+ className: "max-h-full max-w-full"
406
511
  }
407
512
  ) });
408
513
  }
514
+ function AudioPreview({ src, filename }) {
515
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center gap-4 rounded-[var(--radius-md)] border border-border bg-background px-6 py-10", children: [
516
+ /* @__PURE__ */ jsx3(Music, { className: "h-10 w-10 text-muted-foreground opacity-60" }),
517
+ /* @__PURE__ */ jsx3("p", { className: "max-w-full truncate text-sm font-medium text-foreground", children: filename }),
518
+ /* @__PURE__ */ jsx3("audio", { controls: true, preload: "metadata", src, "aria-label": filename, className: "w-full max-w-md" })
519
+ ] });
520
+ }
409
521
  function TextPreview({ content }) {
410
522
  return /* @__PURE__ */ jsx3("pre", { className: "bg-card rounded-[var(--radius-md)] border border-border p-4 overflow-auto max-h-[70vh] text-sm text-foreground font-mono leading-[1.55]", children: content });
411
523
  }
412
- function UnsupportedPreview({
413
- filename,
414
- title,
415
- description
416
- }) {
417
- return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center rounded-[var(--radius-md)] border border-dashed border-border bg-background px-6 py-16 text-center text-muted-foreground", children: [
418
- /* @__PURE__ */ jsx3(FileText2, { className: "mb-3 h-12 w-12 opacity-30" }),
419
- /* @__PURE__ */ jsx3("p", { className: "text-sm text-foreground", children: title }),
420
- /* @__PURE__ */ jsx3("p", { className: "mt-1 max-w-md text-xs", children: description }),
421
- /* @__PURE__ */ jsx3("p", { className: "mt-4 text-[11px] uppercase tracking-[0.12em]", children: filename })
422
- ] });
423
- }
424
524
  function MarkdownPreview({ content }) {
425
525
  return /* @__PURE__ */ jsx3("div", { className: "rounded-[var(--radius-md)] border border-border bg-background p-5", children: /* @__PURE__ */ jsx3(Markdown, { children: content }) });
426
526
  }
427
- function EmptyPreview({ filename }) {
428
- return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center py-16 text-muted-foreground", children: [
429
- /* @__PURE__ */ jsx3(FileText2, { className: "h-12 w-12 mb-3 opacity-30" }),
430
- /* @__PURE__ */ jsxs2("p", { className: "text-sm", children: [
431
- "Cannot preview ",
432
- filename
433
- ] }),
434
- /* @__PURE__ */ jsx3("p", { className: "text-xs mt-1", children: "Download to view this file" })
435
- ] });
527
+ function DownloadCard({
528
+ filename,
529
+ size,
530
+ sentence,
531
+ icon: Icon = FileText2,
532
+ onDownload,
533
+ className
534
+ }) {
535
+ return /* @__PURE__ */ jsxs2(
536
+ "div",
537
+ {
538
+ className: cn(
539
+ "flex flex-col items-center justify-center rounded-[var(--radius-md)] border border-dashed border-border bg-background px-6 py-16 text-center",
540
+ className
541
+ ),
542
+ children: [
543
+ /* @__PURE__ */ jsx3(Icon, { className: "mb-3 h-12 w-12 text-muted-foreground opacity-40" }),
544
+ /* @__PURE__ */ jsx3("p", { className: "max-w-full truncate text-sm font-medium text-foreground", children: filename }),
545
+ size !== void 0 && /* @__PURE__ */ jsx3("p", { className: "mt-1 text-xs text-muted-foreground", children: formatBytes(size) }),
546
+ /* @__PURE__ */ jsx3("p", { className: "mt-3 max-w-md text-sm text-muted-foreground", children: sentence }),
547
+ onDownload && /* @__PURE__ */ jsxs2(Button, { type: "button", variant: "outline", size: "sm", className: "mt-5", onClick: onDownload, children: [
548
+ /* @__PURE__ */ jsx3(Download, {}),
549
+ "Download"
550
+ ] })
551
+ ]
552
+ }
553
+ );
554
+ }
555
+ function PreviewBody({
556
+ kind,
557
+ filename,
558
+ content,
559
+ blobUrl,
560
+ size,
561
+ onDownload
562
+ }) {
563
+ const hasText = typeof content === "string";
564
+ const card = (sentence, icon) => /* @__PURE__ */ jsx3(
565
+ DownloadCard,
566
+ {
567
+ filename,
568
+ size,
569
+ sentence,
570
+ icon,
571
+ onDownload
572
+ }
573
+ );
574
+ switch (kind) {
575
+ case "pdf":
576
+ if (blobUrl) {
577
+ return /* @__PURE__ */ jsx3(PdfPreview, { blobUrl, filename, size, onDownload });
578
+ }
579
+ return card(NEEDS_DOWNLOAD_LINK);
580
+ case "image":
581
+ if (blobUrl) return /* @__PURE__ */ jsx3(ImagePreview, { src: blobUrl, filename });
582
+ if (hasText) return /* @__PURE__ */ jsx3(CodePreview, { content, filename, kind: "code" });
583
+ return card(NEEDS_DOWNLOAD_LINK);
584
+ case "video":
585
+ if (blobUrl) return /* @__PURE__ */ jsx3(VideoPreview, { src: blobUrl, filename });
586
+ return card(NEEDS_DOWNLOAD_LINK);
587
+ case "audio":
588
+ if (blobUrl) return /* @__PURE__ */ jsx3(AudioPreview, { src: blobUrl, filename });
589
+ return card(NEEDS_DOWNLOAD_LINK, Music);
590
+ case "csv":
591
+ if (hasText) return /* @__PURE__ */ jsx3(CsvPreview, { content });
592
+ return card(NO_INLINE_CONTENT, FileSpreadsheet2);
593
+ case "code":
594
+ case "json":
595
+ case "yaml":
596
+ if (hasText) return /* @__PURE__ */ jsx3(CodePreview, { content, filename, kind });
597
+ return card(NO_INLINE_CONTENT);
598
+ case "markdown":
599
+ if (hasText) return /* @__PURE__ */ jsx3(MarkdownPreview, { content });
600
+ return card(NO_INLINE_CONTENT);
601
+ case "text":
602
+ if (hasText) return /* @__PURE__ */ jsx3(TextPreview, { content });
603
+ return card(NO_INLINE_CONTENT);
604
+ case "spreadsheet":
605
+ return card("Download to open this workbook in a spreadsheet app.", FileSpreadsheet2);
606
+ case "binary":
607
+ if (hasText) return /* @__PURE__ */ jsx3(TextPreview, { content });
608
+ return card(blobUrl ? NO_INLINE_PREVIEW : NEEDS_DOWNLOAD_LINK);
609
+ }
436
610
  }
437
611
  function FilePreview({
438
612
  filename,
439
613
  content,
440
614
  blobUrl,
441
615
  mimeType,
616
+ size,
442
617
  onClose,
443
618
  onDownload,
444
619
  hideHeader = false,
445
620
  className
446
621
  }) {
447
- const format = detectFileFormat(filename, mimeType);
448
- const previewLabel = getFormatLabel(format);
449
- const hasRenderableSource = Boolean(content) || Boolean(blobUrl) || format === "unknown" || format === "spreadsheet";
622
+ const kind = resolveFilePreviewKind(filename, mimeType);
623
+ const previewLabel = getFormatLabel(kind);
450
624
  return /* @__PURE__ */ jsxs2("div", { className: cn("flex flex-col h-full", className), children: [
451
625
  !hideHeader && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 px-3 py-2 border-b border-border shrink-0", children: [
452
626
  /* @__PURE__ */ jsxs2("div", { className: "min-w-0 flex-1", children: [
@@ -474,42 +648,27 @@ function FilePreview({
474
648
  }
475
649
  )
476
650
  ] }),
477
- /* @__PURE__ */ jsxs2("div", { className: "flex-1 overflow-auto p-3", children: [
478
- format === "pdf" && blobUrl && /* @__PURE__ */ jsx3(PdfPreview, { blobUrl, filename }),
479
- format === "image" && blobUrl && /* @__PURE__ */ jsx3(ImagePreview, { src: blobUrl, filename }),
480
- format === "csv" && typeof content === "string" && /* @__PURE__ */ jsx3(CsvPreview, { content }),
481
- (format === "code" || format === "json" || format === "yaml") && typeof content === "string" && /* @__PURE__ */ jsx3(CodePreview, { content, filename, format }),
482
- format === "text" && typeof content === "string" && /* @__PURE__ */ jsx3(TextPreview, { content }),
483
- format === "markdown" && typeof content === "string" && /* @__PURE__ */ jsx3(MarkdownPreview, { content }),
484
- format === "spreadsheet" && /* @__PURE__ */ jsx3(
485
- UnsupportedPreview,
486
- {
487
- filename,
488
- title: "Spreadsheet preview is not available in this surface",
489
- description: "Download the workbook or convert it to CSV when you need an inline preview."
490
- }
491
- ),
492
- format === "unknown" && typeof content !== "string" && /* @__PURE__ */ jsx3(EmptyPreview, { filename }),
493
- format === "unknown" && typeof content === "string" && /* @__PURE__ */ jsx3(TextPreview, { content }),
494
- !hasRenderableSource && typeof content !== "string" && /* @__PURE__ */ jsx3(
495
- UnsupportedPreview,
496
- {
497
- filename,
498
- title: "Preview data is not available yet",
499
- description: "This artifact can be shown once the app provides inline content or a downloadable blob."
500
- }
501
- )
502
- ] })
651
+ /* @__PURE__ */ jsx3("div", { className: "flex min-h-0 flex-1 flex-col overflow-auto p-3", children: /* @__PURE__ */ jsx3(
652
+ PreviewBody,
653
+ {
654
+ kind,
655
+ filename,
656
+ content,
657
+ blobUrl,
658
+ size,
659
+ onDownload
660
+ }
661
+ ) })
503
662
  ] });
504
663
  }
505
664
 
506
665
  // src/files/file-tabs.tsx
507
- import { X as X2, FileText as FileText3, FileCode as FileCode2, FileSpreadsheet as FileSpreadsheet2 } from "lucide-react";
666
+ import { X as X2, FileText as FileText3, FileCode as FileCode2, FileSpreadsheet as FileSpreadsheet3 } from "lucide-react";
508
667
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
509
668
  function getTabIcon(name) {
510
669
  const ext = name.split(".").pop()?.toLowerCase() || "";
511
670
  if (["pdf"].includes(ext)) return FileText3;
512
- if (["csv", "xlsx"].includes(ext)) return FileSpreadsheet2;
671
+ if (["csv", "xlsx"].includes(ext)) return FileSpreadsheet3;
513
672
  return FileCode2;
514
673
  }
515
674
  function FileTabs({ tabs, activeId, onSelect, onClose, className }) {
@@ -568,6 +727,7 @@ function FileArtifactPane({
568
727
  content,
569
728
  blobUrl,
570
729
  mimeType,
730
+ size,
571
731
  onClose,
572
732
  onDownload,
573
733
  path,
@@ -694,6 +854,8 @@ function FileArtifactPane({
694
854
  content,
695
855
  blobUrl,
696
856
  mimeType,
857
+ size,
858
+ onDownload,
697
859
  hideHeader: true
698
860
  }
699
861
  )
@@ -705,6 +867,8 @@ export {
705
867
  filterFileTree,
706
868
  FileTree,
707
869
  RichFileTree,
870
+ CSV_PREVIEW_ROW_LIMIT,
871
+ parseCsv,
708
872
  FilePreview,
709
873
  FileTabs,
710
874
  FileArtifactPane
@@ -1,5 +1,11 @@
1
1
  // src/files/file-format.ts
2
- var IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "svg", "webp"];
2
+ var IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"];
3
+ var VIDEO_EXTENSIONS = ["mp4", "webm", "mov"];
4
+ var AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a"];
5
+ var SPREADSHEET_MIME_TYPES = [
6
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
7
+ "application/vnd.ms-excel"
8
+ ];
3
9
  var EXTENSION_TO_SYNTAX_LANGUAGE = {
4
10
  ts: "typescript",
5
11
  tsx: "tsx",
@@ -50,12 +56,17 @@ function detectFileFormat(filename, mimeType) {
50
56
  const mime = mimeEssence(mimeType);
51
57
  if (mime === "application/pdf") return "pdf";
52
58
  if (mime.startsWith("image/")) return "image";
59
+ if (mime.startsWith("video/")) return "video";
60
+ if (mime.startsWith("audio/")) return "audio";
53
61
  if (mime === "text/markdown") return "markdown";
54
62
  if (mime === "application/json") return "json";
55
63
  if (mime === "text/csv" || mime === "application/csv") return "csv";
64
+ if (SPREADSHEET_MIME_TYPES.includes(mime)) return "spreadsheet";
56
65
  if (mime === "application/yaml" || mime === "application/x-yaml" || mime === "text/yaml") return "yaml";
57
66
  if (ext === "pdf") return "pdf";
58
67
  if (IMAGE_EXTENSIONS.includes(ext)) return "image";
68
+ if (VIDEO_EXTENSIONS.includes(ext)) return "video";
69
+ if (AUDIO_EXTENSIONS.includes(ext)) return "audio";
59
70
  if (ext === "csv") return "csv";
60
71
  if (ext === "xlsx" || ext === "xls") return "spreadsheet";
61
72
  if (CODE_EXTENSIONS.has(ext)) return "code";
@@ -66,12 +77,20 @@ function detectFileFormat(filename, mimeType) {
66
77
  if (mime === "text/plain") return "text";
67
78
  return "unknown";
68
79
  }
80
+ function resolveFilePreviewKind(filename, mimeType) {
81
+ const format = detectFileFormat(filename, mimeType);
82
+ return format === "unknown" ? "binary" : format;
83
+ }
69
84
  function getFormatLabel(format) {
70
85
  switch (format) {
71
86
  case "pdf":
72
87
  return "PDF";
73
88
  case "image":
74
89
  return "Image";
90
+ case "video":
91
+ return "Video";
92
+ case "audio":
93
+ return "Audio";
75
94
  case "csv":
76
95
  return "CSV";
77
96
  case "spreadsheet":
@@ -101,6 +120,7 @@ function getCodeLanguage(filename, format) {
101
120
  export {
102
121
  fileExtension,
103
122
  detectFileFormat,
123
+ resolveFilePreviewKind,
104
124
  getFormatLabel,
105
125
  getSyntaxLanguage,
106
126
  getCodeLanguage
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  formatDuration
3
- } from "./chunk-EIUE5GV2.js";
3
+ } from "./chunk-4CLN43XT.js";
4
4
 
5
5
  // src/run/run-item-primitives.tsx
6
6
  import { useEffect, useState } from "react";
package/dist/files.d.ts CHANGED
@@ -98,25 +98,39 @@ declare function RichFileTree({ root, paths, selectedPath, onSelect, search, ini
98
98
  /**
99
99
  * FilePreview — universal file renderer.
100
100
  *
101
- * Renders any file type beautifully:
102
- * - PDF: embedded viewer
103
- * - CSV/XLSX: tabular preview
101
+ * Renders any file type:
102
+ * - PDF: embedded viewer at full pane height
103
+ * - Images: fit to the pane on a checker ground; click toggles natural size
104
+ * - Video / audio: native players
105
+ * - CSV: sticky-header table, capped at CSV_PREVIEW_ROW_LIMIT rows
106
+ * - XLSX / XLS: download card
104
107
  * - Code (py/json/yaml/ts/js): syntax-highlighted, line-numbered viewer
105
108
  * - Markdown: rendered prose
106
- * - Images: inline display
107
109
  * - Text: monospace preview
110
+ * - Anything else: text when content is a string, otherwise a download card
108
111
  */
109
112
  interface FilePreviewProps {
110
113
  filename: string;
111
114
  content?: string;
115
+ /** Object URL or remote URL for binary content: images, PDF, video, audio. */
112
116
  blobUrl?: string;
113
117
  mimeType?: string;
118
+ /** Size in bytes; download cards show it when known. */
119
+ size?: number;
114
120
  onClose?: () => void;
115
121
  onDownload?: () => void;
116
122
  hideHeader?: boolean;
117
123
  className?: string;
118
124
  }
119
- declare function FilePreview({ filename, content, blobUrl, mimeType, onClose, onDownload, hideHeader, className, }: FilePreviewProps): react_jsx_runtime.JSX.Element;
125
+ /** Rows rendered by the CSV preview; the count line reports the total. */
126
+ declare const CSV_PREVIEW_ROW_LIMIT = 500;
127
+ /**
128
+ * Split CSV text into rows of cells (RFC 4180): a quoted cell may hold commas,
129
+ * line breaks, and doubled quotes. Unquoted cells are trimmed; blank lines and
130
+ * a trailing line break add no row.
131
+ */
132
+ declare function parseCsv(content: string): string[][];
133
+ declare function FilePreview({ filename, content, blobUrl, mimeType, size, onClose, onDownload, hideHeader, className, }: FilePreviewProps): react_jsx_runtime.JSX.Element;
120
134
 
121
135
  /**
122
136
  * FileTabs — tab bar for open files in the preview panel.
@@ -170,9 +184,11 @@ interface FileArtifactPaneProps extends Omit<FilePreviewProps, "className"> {
170
184
  }
171
185
  /**
172
186
  * FileArtifactPane — opinionated artifact frame for file previews with tabs and
173
- * header actions.
187
+ * header actions. The body is `FilePreview`, so every kind it renders (image,
188
+ * PDF, video, audio, CSV, spreadsheet, markdown, code, text) renders here too;
189
+ * `onDownload` drives both the header button and the body's download cards.
174
190
  */
175
- declare function FileArtifactPane({ filename, content, blobUrl, mimeType, onClose, onDownload, path, tabs, activeTabId, onTabSelect, onTabClose, eyebrow, meta, toolbar, footer, className, headerClassName, hideTitleBlock, editor, }: FileArtifactPaneProps): react_jsx_runtime.JSX.Element;
191
+ declare function FileArtifactPane({ filename, content, blobUrl, mimeType, size, onClose, onDownload, path, tabs, activeTabId, onTabSelect, onTabClose, eyebrow, meta, toolbar, footer, className, headerClassName, hideTitleBlock, editor, }: FileArtifactPaneProps): react_jsx_runtime.JSX.Element;
176
192
 
177
193
  /**
178
194
  * Shared file-format detection for every file surface — the preview pane, the
@@ -180,7 +196,13 @@ declare function FileArtifactPane({ filename, content, blobUrl, mimeType, onClos
180
196
  * keeps chat and artifact views consistent and avoids the same mapping drifting
181
197
  * across components.
182
198
  */
183
- type FileFormat = "pdf" | "image" | "csv" | "spreadsheet" | "code" | "json" | "yaml" | "markdown" | "text" | "unknown";
199
+ type FileFormat = "pdf" | "image" | "video" | "audio" | "csv" | "spreadsheet" | "code" | "json" | "yaml" | "markdown" | "text" | "unknown";
200
+ /**
201
+ * How a file surface renders a file. Every format maps to its own renderer;
202
+ * `unknown` becomes `binary`, which renders string content as text and offers a
203
+ * download for anything else.
204
+ */
205
+ type FilePreviewKind = Exclude<FileFormat, "unknown"> | "binary";
184
206
  /**
185
207
  * Lowercased trailing extension. Returns "" for a name with no extension
186
208
  * ("README", "json" → ""), and the post-dot name for a dotfile (".bashrc" →
@@ -194,8 +216,15 @@ declare function fileExtension(filename: string): string;
194
216
  * decides; a generic text/plain payload is the final fallback.
195
217
  */
196
218
  declare function detectFileFormat(filename: string, mimeType?: string): FileFormat;
197
- /** Human-facing label for a detected format. */
198
- declare function getFormatLabel(format: FileFormat): string;
219
+ /**
220
+ * The preview renderer for a file, from its name and optional MIME type only.
221
+ * Same precedence as `detectFileFormat` (MIME outranks extension); a file that
222
+ * matches nothing is `binary`. Content is not an input: the preview renders a
223
+ * `binary` file's string content as text and offers a download otherwise.
224
+ */
225
+ declare function resolveFilePreviewKind(filename: string, mimeType?: string): FilePreviewKind;
226
+ /** Human-facing label for a detected format or preview kind. */
227
+ declare function getFormatLabel(format: FileFormat | FilePreviewKind): string;
199
228
  /**
200
229
  * Map a filename (or path) to a highlight.js language id for CodeBlock. Returns
201
230
  * undefined when there is no confident mapping; CodeBlock then renders themed
@@ -209,6 +238,6 @@ declare function getSyntaxLanguage(filename: string): string | undefined;
209
238
  * a MIME type on an extensionless file (where the extension can't reveal it);
210
239
  * any other code format keys off the extension.
211
240
  */
212
- declare function getCodeLanguage(filename: string, format: FileFormat): string | undefined;
241
+ declare function getCodeLanguage(filename: string, format: FileFormat | FilePreviewKind): string | undefined;
213
242
 
214
- export { FileArtifactPane, type FileArtifactPaneProps, type FileFormat, type FileNode, FilePreview, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, RichFileTree, type RichFileTreeGitEntry, type RichFileTreeGitStatus, type RichFileTreeProps, type RichFileTreeThemeVars, detectFileFormat, fileExtension, filterFileTree, getCodeLanguage, getFormatLabel, getSyntaxLanguage };
243
+ export { CSV_PREVIEW_ROW_LIMIT, FileArtifactPane, type FileArtifactPaneProps, type FileFormat, type FileNode, FilePreview, type FilePreviewKind, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, RichFileTree, type RichFileTreeGitEntry, type RichFileTreeGitStatus, type RichFileTreeProps, type RichFileTreeThemeVars, detectFileFormat, fileExtension, filterFileTree, getCodeLanguage, getFormatLabel, getSyntaxLanguage, parseCsv, resolveFilePreviewKind };