@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.
@@ -51,13 +51,16 @@ export interface FileArtifactPaneProps extends Omit<FilePreviewProps, "className
51
51
 
52
52
  /**
53
53
  * FileArtifactPane — opinionated artifact frame for file previews with tabs and
54
- * header actions.
54
+ * header actions. The body is `FilePreview`, so every kind it renders (image,
55
+ * PDF, video, audio, CSV, spreadsheet, markdown, code, text) renders here too;
56
+ * `onDownload` drives both the header button and the body's download cards.
55
57
  */
56
58
  export function FileArtifactPane({
57
59
  filename,
58
60
  content,
59
61
  blobUrl,
60
62
  mimeType,
63
+ size,
61
64
  onClose,
62
65
  onDownload,
63
66
  path,
@@ -188,6 +191,8 @@ export function FileArtifactPane({
188
191
  content={content}
189
192
  blobUrl={blobUrl}
190
193
  mimeType={mimeType}
194
+ size={size}
195
+ onDownload={onDownload}
191
196
  hideHeader={true}
192
197
  />
193
198
  </ArtifactPane>
@@ -8,6 +8,8 @@
8
8
  export type FileFormat =
9
9
  | "pdf"
10
10
  | "image"
11
+ | "video"
12
+ | "audio"
11
13
  | "csv"
12
14
  | "spreadsheet"
13
15
  | "code"
@@ -17,7 +19,20 @@ export type FileFormat =
17
19
  | "text"
18
20
  | "unknown";
19
21
 
20
- const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "svg", "webp"];
22
+ /**
23
+ * How a file surface renders a file. Every format maps to its own renderer;
24
+ * `unknown` becomes `binary`, which renders string content as text and offers a
25
+ * download for anything else.
26
+ */
27
+ export type FilePreviewKind = Exclude<FileFormat, "unknown"> | "binary";
28
+
29
+ const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"];
30
+ const VIDEO_EXTENSIONS = ["mp4", "webm", "mov"];
31
+ const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a"];
32
+ const SPREADSHEET_MIME_TYPES = [
33
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
34
+ "application/vnd.ms-excel",
35
+ ];
21
36
 
22
37
  const EXTENSION_TO_SYNTAX_LANGUAGE: Record<string, string> = {
23
38
  ts: "typescript",
@@ -97,14 +112,19 @@ export function detectFileFormat(filename: string, mimeType?: string): FileForma
97
112
  // 1. Specific MIME types are authoritative — they outrank the extension.
98
113
  if (mime === "application/pdf") return "pdf";
99
114
  if (mime.startsWith("image/")) return "image";
115
+ if (mime.startsWith("video/")) return "video";
116
+ if (mime.startsWith("audio/")) return "audio";
100
117
  if (mime === "text/markdown") return "markdown";
101
118
  if (mime === "application/json") return "json";
102
119
  if (mime === "text/csv" || mime === "application/csv") return "csv";
120
+ if (SPREADSHEET_MIME_TYPES.includes(mime)) return "spreadsheet";
103
121
  if (mime === "application/yaml" || mime === "application/x-yaml" || mime === "text/yaml") return "yaml";
104
122
 
105
123
  // 2. Fall back to the file extension.
106
124
  if (ext === "pdf") return "pdf";
107
125
  if (IMAGE_EXTENSIONS.includes(ext)) return "image";
126
+ if (VIDEO_EXTENSIONS.includes(ext)) return "video";
127
+ if (AUDIO_EXTENSIONS.includes(ext)) return "audio";
108
128
  if (ext === "csv") return "csv";
109
129
  if (ext === "xlsx" || ext === "xls") return "spreadsheet";
110
130
  if (CODE_EXTENSIONS.has(ext)) return "code";
@@ -119,13 +139,28 @@ export function detectFileFormat(filename: string, mimeType?: string): FileForma
119
139
  return "unknown";
120
140
  }
121
141
 
122
- /** Human-facing label for a detected format. */
123
- export function getFormatLabel(format: FileFormat): string {
142
+ /**
143
+ * The preview renderer for a file, from its name and optional MIME type only.
144
+ * Same precedence as `detectFileFormat` (MIME outranks extension); a file that
145
+ * matches nothing is `binary`. Content is not an input: the preview renders a
146
+ * `binary` file's string content as text and offers a download otherwise.
147
+ */
148
+ export function resolveFilePreviewKind(filename: string, mimeType?: string): FilePreviewKind {
149
+ const format = detectFileFormat(filename, mimeType);
150
+ return format === "unknown" ? "binary" : format;
151
+ }
152
+
153
+ /** Human-facing label for a detected format or preview kind. */
154
+ export function getFormatLabel(format: FileFormat | FilePreviewKind): string {
124
155
  switch (format) {
125
156
  case "pdf":
126
157
  return "PDF";
127
158
  case "image":
128
159
  return "Image";
160
+ case "video":
161
+ return "Video";
162
+ case "audio":
163
+ return "Audio";
129
164
  case "csv":
130
165
  return "CSV";
131
166
  case "spreadsheet":
@@ -161,7 +196,10 @@ export function getSyntaxLanguage(filename: string): string | undefined {
161
196
  * a MIME type on an extensionless file (where the extension can't reveal it);
162
197
  * any other code format keys off the extension.
163
198
  */
164
- export function getCodeLanguage(filename: string, format: FileFormat): string | undefined {
199
+ export function getCodeLanguage(
200
+ filename: string,
201
+ format: FileFormat | FilePreviewKind,
202
+ ): string | undefined {
165
203
  if (format === "json" || format === "yaml") return format;
166
204
  return getSyntaxLanguage(filename);
167
205
  }
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ detectFileFormat,
4
+ getFormatLabel,
5
+ resolveFilePreviewKind,
6
+ type FilePreviewKind,
7
+ } from "./file-format";
8
+
9
+ describe("resolveFilePreviewKind", () => {
10
+ it.each<[string, string | undefined, FilePreviewKind]>([
11
+ // Images
12
+ ["photo.png", undefined, "image"],
13
+ ["photo.jpg", undefined, "image"],
14
+ ["photo.jpeg", undefined, "image"],
15
+ ["anim.gif", undefined, "image"],
16
+ ["photo.webp", undefined, "image"],
17
+ ["icon.svg", undefined, "image"],
18
+ ["photo.avif", undefined, "image"],
19
+ // Documents
20
+ ["report.pdf", undefined, "pdf"],
21
+ // Video
22
+ ["clip.mp4", undefined, "video"],
23
+ ["clip.webm", undefined, "video"],
24
+ ["clip.mov", undefined, "video"],
25
+ // Audio
26
+ ["track.mp3", undefined, "audio"],
27
+ ["track.wav", undefined, "audio"],
28
+ ["track.ogg", undefined, "audio"],
29
+ ["track.m4a", undefined, "audio"],
30
+ // Tabular
31
+ ["data.csv", undefined, "csv"],
32
+ ["book.xlsx", undefined, "spreadsheet"],
33
+ ["legacy.xls", undefined, "spreadsheet"],
34
+ // Prose and structured text
35
+ ["README.md", undefined, "markdown"],
36
+ ["notes.markdown", undefined, "markdown"],
37
+ ["config.json", undefined, "json"],
38
+ ["compose.yaml", undefined, "yaml"],
39
+ ["compose.yml", undefined, "yaml"],
40
+ ["server.ts", undefined, "code"],
41
+ ["main.py", undefined, "code"],
42
+ ["output.log", undefined, "text"],
43
+ ["notes.txt", undefined, "text"],
44
+ ])("%s (%s) → %s", (filename, mimeType, kind) => {
45
+ expect(resolveFilePreviewKind(filename, mimeType)).toBe(kind);
46
+ });
47
+
48
+ it("lets an authoritative MIME type outrank the extension", () => {
49
+ expect(resolveFilePreviewKind("photo.txt", "image/png")).toBe("image");
50
+ expect(resolveFilePreviewKind("scan.png", "application/pdf")).toBe("pdf");
51
+ expect(resolveFilePreviewKind("clip.bin", "video/mp4")).toBe("video");
52
+ expect(resolveFilePreviewKind("track.webm", "audio/webm")).toBe("audio");
53
+ expect(
54
+ resolveFilePreviewKind(
55
+ "export.csv",
56
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
57
+ ),
58
+ ).toBe("spreadsheet");
59
+ expect(resolveFilePreviewKind("book.bin", "application/vnd.ms-excel")).toBe("spreadsheet");
60
+ expect(resolveFilePreviewKind("notes.json", "text/markdown")).toBe("markdown");
61
+ });
62
+
63
+ it("resolves MIME-only files with no extension", () => {
64
+ expect(resolveFilePreviewKind("blob", "image/webp")).toBe("image");
65
+ expect(resolveFilePreviewKind("blob", "video/webm")).toBe("video");
66
+ expect(resolveFilePreviewKind("blob", "audio/mpeg")).toBe("audio");
67
+ expect(resolveFilePreviewKind("blob", "application/json; charset=utf-8")).toBe("json");
68
+ expect(resolveFilePreviewKind("blob", "text/plain")).toBe("text");
69
+ });
70
+
71
+ it("ignores a generic MIME type and keeps the extension", () => {
72
+ expect(resolveFilePreviewKind("photo.png", "application/octet-stream")).toBe("image");
73
+ expect(resolveFilePreviewKind("main.py", "text/plain")).toBe("code");
74
+ });
75
+
76
+ it("resolves everything unmatched to binary", () => {
77
+ expect(resolveFilePreviewKind("mystery.bin")).toBe("binary");
78
+ expect(resolveFilePreviewKind("noextension")).toBe("binary");
79
+ expect(resolveFilePreviewKind("weights", "application/octet-stream")).toBe("binary");
80
+ expect(resolveFilePreviewKind("pdf")).toBe("binary");
81
+ });
82
+
83
+ it("agrees with detectFileFormat for every format that has a renderer", () => {
84
+ for (const filename of [
85
+ "photo.png",
86
+ "report.pdf",
87
+ "clip.mp4",
88
+ "track.mp3",
89
+ "data.csv",
90
+ "book.xlsx",
91
+ "README.md",
92
+ "config.json",
93
+ "compose.yml",
94
+ "server.ts",
95
+ "notes.txt",
96
+ ]) {
97
+ expect(resolveFilePreviewKind(filename)).toBe(detectFileFormat(filename));
98
+ }
99
+ expect(detectFileFormat("mystery.bin")).toBe("unknown");
100
+ expect(resolveFilePreviewKind("mystery.bin")).toBe("binary");
101
+ });
102
+
103
+ it("labels every preview kind", () => {
104
+ expect(getFormatLabel("video")).toBe("Video");
105
+ expect(getFormatLabel("audio")).toBe("Audio");
106
+ expect(getFormatLabel("image")).toBe("Image");
107
+ expect(getFormatLabel("binary")).toBe("File");
108
+ });
109
+ });