@ai-matrx/media 0.3.0 → 0.4.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.
@@ -0,0 +1,1729 @@
1
+ "use client";
2
+
3
+ // src/viewers/registry.ts
4
+ var VIEWER_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.media.viewer-registry");
5
+ var HIGHLIGHTER_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.media.code-highlighter");
6
+ function slots() {
7
+ return globalThis;
8
+ }
9
+ function viewerMap() {
10
+ const host = slots();
11
+ const existing = host[VIEWER_SLOT];
12
+ if (existing) return existing;
13
+ const created = /* @__PURE__ */ new Map();
14
+ host[VIEWER_SLOT] = created;
15
+ return created;
16
+ }
17
+ function registerMediaViewer(kind, viewer) {
18
+ const map = viewerMap();
19
+ const previous = map.get(kind);
20
+ map.set(kind, viewer);
21
+ return () => {
22
+ if (previous) map.set(kind, previous);
23
+ else map.delete(kind);
24
+ };
25
+ }
26
+ function getMediaViewer(kind) {
27
+ return viewerMap().get(kind) ?? null;
28
+ }
29
+ function registeredMediaViewerKinds() {
30
+ return [...viewerMap().keys()];
31
+ }
32
+ function registerMediaCodeHighlighter(highlighter) {
33
+ const host = slots();
34
+ const previous = host[HIGHLIGHTER_SLOT] ?? null;
35
+ host[HIGHLIGHTER_SLOT] = highlighter;
36
+ return () => {
37
+ host[HIGHLIGHTER_SLOT] = previous;
38
+ };
39
+ }
40
+ function getMediaCodeHighlighter() {
41
+ return slots()[HIGHLIGHTER_SLOT] ?? null;
42
+ }
43
+
44
+ // src/viewers/code-viewer.tsx
45
+ import { useMemo as useMemo2, useState as useState4 } from "react";
46
+
47
+ // src/react/cn.ts
48
+ import { twMerge } from "tailwind-merge";
49
+ function cn(...inputs) {
50
+ return twMerge(inputs.filter(Boolean).join(" "));
51
+ }
52
+
53
+ // src/viewers/chrome.tsx
54
+ import { useCallback, useState } from "react";
55
+
56
+ // src/react/icons.tsx
57
+ import { jsx, jsxs } from "react/jsx-runtime";
58
+ function svgProps(props) {
59
+ return {
60
+ xmlns: "http://www.w3.org/2000/svg",
61
+ viewBox: "0 0 24 24",
62
+ width: 24,
63
+ height: 24,
64
+ fill: "none",
65
+ stroke: "currentColor",
66
+ strokeWidth: 2,
67
+ strokeLinecap: "round",
68
+ strokeLinejoin: "round",
69
+ "aria-hidden": true,
70
+ ...props
71
+ };
72
+ }
73
+ function ArchiveIcon(props) {
74
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
75
+ /* @__PURE__ */ jsx("rect", { width: "20", height: "5", x: "2", y: "3", rx: "1" }),
76
+ /* @__PURE__ */ jsx("path", { d: "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" }),
77
+ /* @__PURE__ */ jsx("path", { d: "M10 12h4" })
78
+ ] });
79
+ }
80
+ function BracesIcon(props) {
81
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
82
+ /* @__PURE__ */ jsx("path", { d: "M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1" }),
83
+ /* @__PURE__ */ jsx("path", { d: "M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1" })
84
+ ] });
85
+ }
86
+ function CheckIcon(props) {
87
+ return /* @__PURE__ */ jsx("svg", { ...svgProps(props), children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) });
88
+ }
89
+ function CodeIcon(props) {
90
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
91
+ /* @__PURE__ */ jsx("path", { d: "m16 18 6-6-6-6" }),
92
+ /* @__PURE__ */ jsx("path", { d: "m8 6-6 6 6 6" })
93
+ ] });
94
+ }
95
+ function CopyIcon(props) {
96
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
97
+ /* @__PURE__ */ jsx("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }),
98
+ /* @__PURE__ */ jsx("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })
99
+ ] });
100
+ }
101
+ function DatabaseIcon(props) {
102
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
103
+ /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "5", rx: "9", ry: "3" }),
104
+ /* @__PURE__ */ jsx("path", { d: "M3 5V19A9 3 0 0 0 21 19V5" }),
105
+ /* @__PURE__ */ jsx("path", { d: "M3 12A9 3 0 0 0 21 12" })
106
+ ] });
107
+ }
108
+ function DownloadIcon(props) {
109
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
110
+ /* @__PURE__ */ jsx("path", { d: "M12 15V3" }),
111
+ /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
112
+ /* @__PURE__ */ jsx("path", { d: "m7 10 5 5 5-5" })
113
+ ] });
114
+ }
115
+ function ExternalLinkIcon(props) {
116
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
117
+ /* @__PURE__ */ jsx("path", { d: "M15 3h6v6" }),
118
+ /* @__PURE__ */ jsx("path", { d: "M10 14 21 3" }),
119
+ /* @__PURE__ */ jsx("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
120
+ ] });
121
+ }
122
+ function FileIcon(props) {
123
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
124
+ /* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }),
125
+ /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" })
126
+ ] });
127
+ }
128
+ function FileAudioIcon(props) {
129
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
130
+ /* @__PURE__ */ jsx("path", { d: "M4 6.835V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-.343" }),
131
+ /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }),
132
+ /* @__PURE__ */ jsx("path", { d: "M2 19a2 2 0 0 1 4 0v1a2 2 0 0 1-4 0v-4a6 6 0 0 1 12 0v4a2 2 0 0 1-4 0v-1a2 2 0 0 1 4 0" })
133
+ ] });
134
+ }
135
+ function FileCodeIcon(props) {
136
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
137
+ /* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }),
138
+ /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }),
139
+ /* @__PURE__ */ jsx("path", { d: "M10 12.5 8 15l2 2.5" }),
140
+ /* @__PURE__ */ jsx("path", { d: "m14 12.5 2 2.5-2 2.5" })
141
+ ] });
142
+ }
143
+ function FileJsonIcon(props) {
144
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
145
+ /* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }),
146
+ /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }),
147
+ /* @__PURE__ */ jsx("path", { d: "M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1" }),
148
+ /* @__PURE__ */ jsx("path", { d: "M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1" })
149
+ ] });
150
+ }
151
+ function FileSpreadsheetIcon(props) {
152
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
153
+ /* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }),
154
+ /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }),
155
+ /* @__PURE__ */ jsx("path", { d: "M8 13h2" }),
156
+ /* @__PURE__ */ jsx("path", { d: "M14 13h2" }),
157
+ /* @__PURE__ */ jsx("path", { d: "M8 17h2" }),
158
+ /* @__PURE__ */ jsx("path", { d: "M14 17h2" })
159
+ ] });
160
+ }
161
+ function FileTextIcon(props) {
162
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
163
+ /* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }),
164
+ /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }),
165
+ /* @__PURE__ */ jsx("path", { d: "M10 9H8" }),
166
+ /* @__PURE__ */ jsx("path", { d: "M16 13H8" }),
167
+ /* @__PURE__ */ jsx("path", { d: "M16 17H8" })
168
+ ] });
169
+ }
170
+ function FileTypeIcon(props) {
171
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
172
+ /* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }),
173
+ /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }),
174
+ /* @__PURE__ */ jsx("path", { d: "M11 18h2" }),
175
+ /* @__PURE__ */ jsx("path", { d: "M12 12v6" }),
176
+ /* @__PURE__ */ jsx("path", { d: "M9 13v-.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v.5" })
177
+ ] });
178
+ }
179
+ function FileVideoIcon(props) {
180
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
181
+ /* @__PURE__ */ jsx("path", { d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" }),
182
+ /* @__PURE__ */ jsx("path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }),
183
+ /* @__PURE__ */ jsx("path", { d: "M15.033 13.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56v-4.704a.645.645 0 0 1 .967-.56z" })
184
+ ] });
185
+ }
186
+ function FolderIcon(props) {
187
+ return /* @__PURE__ */ jsx("svg", { ...svgProps(props), children: /* @__PURE__ */ jsx("path", { d: "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" }) });
188
+ }
189
+ function FolderOpenIcon(props) {
190
+ return /* @__PURE__ */ jsx("svg", { ...svgProps(props), children: /* @__PURE__ */ jsx("path", { d: "m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" }) });
191
+ }
192
+ function ImageIcon(props) {
193
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
194
+ /* @__PURE__ */ jsx("rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }),
195
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "2" }),
196
+ /* @__PURE__ */ jsx("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })
197
+ ] });
198
+ }
199
+ function MusicIcon(props) {
200
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
201
+ /* @__PURE__ */ jsx("path", { d: "M9 18V5l12-2v13" }),
202
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "18", r: "3" }),
203
+ /* @__PURE__ */ jsx("circle", { cx: "18", cy: "16", r: "3" })
204
+ ] });
205
+ }
206
+ function PackageBoxIcon(props) {
207
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
208
+ /* @__PURE__ */ jsx("path", { d: "M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" }),
209
+ /* @__PURE__ */ jsx("path", { d: "M12 22V12" }),
210
+ /* @__PURE__ */ jsx("polyline", { points: "3.29 7 12 12 20.71 7" }),
211
+ /* @__PURE__ */ jsx("path", { d: "m7.5 4.27 9 5.15" })
212
+ ] });
213
+ }
214
+ function PenToolIcon(props) {
215
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
216
+ /* @__PURE__ */ jsx("path", { d: "M15.707 21.293a1 1 0 0 1-1.414 0l-1.586-1.586a1 1 0 0 1 0-1.414l5.586-5.586a1 1 0 0 1 1.414 0l1.586 1.586a1 1 0 0 1 0 1.414z" }),
217
+ /* @__PURE__ */ jsx("path", { d: "m18 13-1.375-6.874a1 1 0 0 0-.746-.776L3.235 2.028a1 1 0 0 0-1.207 1.207L5.35 15.879a1 1 0 0 0 .776.746L13 18" }),
218
+ /* @__PURE__ */ jsx("path", { d: "m2.3 2.3 7.286 7.286" }),
219
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "2" })
220
+ ] });
221
+ }
222
+ function SubtitlesIcon(props) {
223
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
224
+ /* @__PURE__ */ jsx("rect", { width: "18", height: "14", x: "3", y: "5", rx: "2", ry: "2" }),
225
+ /* @__PURE__ */ jsx("path", { d: "M7 15h4M15 15h2M7 11h2M13 11h4" })
226
+ ] });
227
+ }
228
+ function VideoIcon(props) {
229
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
230
+ /* @__PURE__ */ jsx("path", { d: "m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5" }),
231
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "6", width: "14", height: "12", rx: "2" })
232
+ ] });
233
+ }
234
+ function XIcon(props) {
235
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
236
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
237
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
238
+ ] });
239
+ }
240
+ function AlertCircleIcon(props) {
241
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
242
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
243
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12", y1: "8", y2: "12" }),
244
+ /* @__PURE__ */ jsx("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })
245
+ ] });
246
+ }
247
+ function CheckCircle2Icon(props) {
248
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
249
+ /* @__PURE__ */ jsx("path", { d: "M21.801 10A10 10 0 1 1 17 3.335" }),
250
+ /* @__PURE__ */ jsx("path", { d: "m9 11 3 3L22 4" })
251
+ ] });
252
+ }
253
+ function GlobeIcon(props) {
254
+ return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
255
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10" }),
256
+ /* @__PURE__ */ jsx("path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }),
257
+ /* @__PURE__ */ jsx("path", { d: "M2 12h20" })
258
+ ] });
259
+ }
260
+ function SpinnerIcon(props) {
261
+ return /* @__PURE__ */ jsx("svg", { ...svgProps(props), children: /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) });
262
+ }
263
+
264
+ // src/viewers/chrome.tsx
265
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
266
+ function ViewerSkeleton({
267
+ className,
268
+ tone = "muted"
269
+ }) {
270
+ return /* @__PURE__ */ jsx2(
271
+ "div",
272
+ {
273
+ className: cn(
274
+ "flex h-full w-full items-center justify-center",
275
+ tone === "muted" && "bg-muted/20",
276
+ className
277
+ ),
278
+ children: /* @__PURE__ */ jsx2("div", { className: "h-6 w-40 animate-pulse rounded bg-muted" })
279
+ }
280
+ );
281
+ }
282
+ function ViewerError({
283
+ title = "Couldn't load this file",
284
+ message,
285
+ hint,
286
+ className
287
+ }) {
288
+ return /* @__PURE__ */ jsxs2(
289
+ "div",
290
+ {
291
+ className: cn(
292
+ "flex h-full w-full flex-col items-center justify-center gap-3 p-6 text-center",
293
+ className
294
+ ),
295
+ role: "alert",
296
+ children: [
297
+ /* @__PURE__ */ jsx2("div", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10", children: /* @__PURE__ */ jsx2(AlertCircleIcon, { className: "h-6 w-6 text-destructive" }) }),
298
+ /* @__PURE__ */ jsxs2("div", { className: "space-y-1", children: [
299
+ /* @__PURE__ */ jsx2("h3", { className: "text-sm font-semibold", children: title }),
300
+ message ? /* @__PURE__ */ jsx2("p", { className: "max-w-md text-xs break-words text-muted-foreground", children: message }) : null,
301
+ hint ? /* @__PURE__ */ jsx2("p", { className: "max-w-md text-[11px] text-muted-foreground/80", children: hint }) : null
302
+ ] })
303
+ ]
304
+ }
305
+ );
306
+ }
307
+ function ViewerCopyButton({
308
+ text,
309
+ className
310
+ }) {
311
+ const [copied, setCopied] = useState(false);
312
+ const onCopy = useCallback(() => {
313
+ if (text == null) return;
314
+ void navigator.clipboard.writeText(text).then(() => {
315
+ setCopied(true);
316
+ setTimeout(() => setCopied(false), 1600);
317
+ }).catch(() => {
318
+ });
319
+ }, [text]);
320
+ return /* @__PURE__ */ jsxs2(
321
+ "button",
322
+ {
323
+ type: "button",
324
+ onClick: onCopy,
325
+ "aria-label": "Copy text to clipboard",
326
+ title: "Copy text to clipboard",
327
+ className: cn(
328
+ "inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground",
329
+ copied && "text-emerald-600 dark:text-emerald-400",
330
+ className
331
+ ),
332
+ children: [
333
+ copied ? /* @__PURE__ */ jsx2(CheckIcon, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx2(CopyIcon, { className: "h-3.5 w-3.5" }),
334
+ copied ? "Copied" : "Copy"
335
+ ]
336
+ }
337
+ );
338
+ }
339
+ function ViewerToolbar({
340
+ children,
341
+ className
342
+ }) {
343
+ return /* @__PURE__ */ jsx2(
344
+ "div",
345
+ {
346
+ className: cn(
347
+ "flex shrink-0 items-center justify-between gap-2 border-b border-border/60 bg-muted/20 px-3 py-1 text-xs",
348
+ className
349
+ ),
350
+ children
351
+ }
352
+ );
353
+ }
354
+ function truncationNote(maxBytes) {
355
+ const mb = maxBytes / 1024 / 1024;
356
+ return `truncated to the first ${mb >= 1 ? `${mb.toFixed(0)}MB` : `${(maxBytes / 1024).toFixed(0)}KB`}`;
357
+ }
358
+
359
+ // src/viewers/use-viewer-bytes.ts
360
+ import { useEffect as useEffect3, useState as useState3 } from "react";
361
+
362
+ // src/core/use-media-blob.ts
363
+ import { useEffect, useState as useState2 } from "react";
364
+
365
+ // src/core/context.ts
366
+ import {
367
+ createContext,
368
+ createElement,
369
+ useContext
370
+ } from "react";
371
+ var CLIENT_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.media.client-context");
372
+ var PORTS_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.media.host-ports-context");
373
+ function clientContext() {
374
+ const host = globalThis;
375
+ host[CLIENT_SLOT] ??= createContext(null);
376
+ return host[CLIENT_SLOT];
377
+ }
378
+ var INERT_PORTS = {};
379
+ function portsContext() {
380
+ const host = globalThis;
381
+ host[PORTS_SLOT] ??= createContext(INERT_PORTS);
382
+ return host[PORTS_SLOT];
383
+ }
384
+ function useMediaClient() {
385
+ const client = useContext(clientContext());
386
+ if (!client) {
387
+ throw new Error(
388
+ "@ai-matrx/media: no MediaClient. Mount <MediaProvider client={...}> above every media surface (canonically wired to @ai-matrx/data/files)."
389
+ );
390
+ }
391
+ return client;
392
+ }
393
+ function useMediaHostPorts() {
394
+ return useContext(portsContext());
395
+ }
396
+
397
+ // src/core/diagnostics.ts
398
+ import { useCallback as useCallback2, useRef } from "react";
399
+ var LATCH_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.media.failure-latch");
400
+ function latch() {
401
+ const host = globalThis;
402
+ host[LATCH_SLOT] ??= /* @__PURE__ */ new Set();
403
+ return host[LATCH_SLOT];
404
+ }
405
+ function failureMediaIdentity(ref, src) {
406
+ if (ref && typeof ref === "object" && ref.file_id) return `file:${ref.file_id}`;
407
+ const raw = typeof ref === "string" ? ref : (ref && typeof ref === "object" ? ref.url ?? null : null) ?? src ?? null;
408
+ if (!raw) return "<unknown-media>";
409
+ if (raw.startsWith("blob:")) return "blob:object-url";
410
+ try {
411
+ const u = new URL(raw);
412
+ return `url:${u.origin}${u.pathname}`;
413
+ } catch {
414
+ const clean = raw.split(/[?#]/, 1)[0] ?? raw;
415
+ return `ref:${clean}`;
416
+ }
417
+ }
418
+ function emitMediaFailure(capture, info) {
419
+ const key = `${info.terminal ? "terminal" : "warning"}|${info.mediaRef}`;
420
+ const seen = latch();
421
+ if (seen.has(key)) return;
422
+ seen.add(key);
423
+ if (capture) {
424
+ try {
425
+ capture(info);
426
+ return;
427
+ } catch {
428
+ }
429
+ }
430
+ console.error(
431
+ "[@ai-matrx/media] media failure (no diagnostics sink captured it \u2014 bind MediaHostPorts.diagnostics.capture to your error system)",
432
+ info
433
+ );
434
+ }
435
+ function useMediaFailureCapture() {
436
+ const ports = useMediaHostPorts();
437
+ const capture = ports.diagnostics?.capture;
438
+ const captureRef = useRef(capture);
439
+ captureRef.current = capture;
440
+ return useCallback2((info) => {
441
+ emitMediaFailure(captureRef.current, { source: "media", ...info });
442
+ }, []);
443
+ }
444
+
445
+ // src/core/ref-key.ts
446
+ function stableRefKey(ref) {
447
+ if (!ref) return "<none>";
448
+ if (typeof ref === "string") return `s:${ref}`;
449
+ if (ref.file_id) return `id:${ref.file_id}:${ref.mime_type ?? ""}`;
450
+ if (ref.url) return `url:${ref.url}:${ref.mime_type ?? ""}`;
451
+ return "<empty-ref>";
452
+ }
453
+
454
+ // src/core/use-media-blob.ts
455
+ function statusOf(err) {
456
+ if (err && typeof err === "object") {
457
+ const candidate = err.status ?? err.statusCode;
458
+ if (typeof candidate === "number") return candidate;
459
+ }
460
+ return void 0;
461
+ }
462
+ function useMediaBlob(ref) {
463
+ const client = useMediaClient();
464
+ const capture = useMediaFailureCapture();
465
+ const refKey = stableRefKey(ref);
466
+ const [state, setState] = useState2({ url: null, blob: null, loading: !!ref, error: null });
467
+ const [retryToken, setRetryToken] = useState2(0);
468
+ useEffect(() => {
469
+ if (!ref) {
470
+ setState({ url: null, blob: null, loading: false, error: null });
471
+ return void 0;
472
+ }
473
+ let cancelled = false;
474
+ let release = null;
475
+ setState({ url: null, blob: null, loading: true, error: null });
476
+ client.getBlob(ref).then(
477
+ (handle) => {
478
+ if (cancelled) {
479
+ handle.release();
480
+ return;
481
+ }
482
+ release = handle.release;
483
+ setState({ url: handle.url, blob: handle.blob, loading: false, error: null });
484
+ },
485
+ (err) => {
486
+ if (cancelled) return;
487
+ const message = err instanceof Error ? err.message : String(err ?? "Failed to load file");
488
+ setState({ url: null, blob: null, loading: false, error: message });
489
+ const status = statusOf(err);
490
+ capture({
491
+ phase: "blob",
492
+ mediaRef: failureMediaIdentity(ref),
493
+ terminal: true,
494
+ message: `getBlob() failed: ${message}`,
495
+ ...status !== void 0 ? { status } : {}
496
+ });
497
+ }
498
+ );
499
+ return () => {
500
+ cancelled = true;
501
+ release?.();
502
+ };
503
+ }, [client, refKey, retryToken]);
504
+ return {
505
+ ...state,
506
+ retry: () => setRetryToken((t) => t + 1)
507
+ };
508
+ }
509
+
510
+ // src/core/use-media-resolution.ts
511
+ import { useEffect as useEffect2, useMemo } from "react";
512
+ function useMediaResolution(ref) {
513
+ const client = useMediaClient();
514
+ const capture = useMediaFailureCapture();
515
+ const refKey = stableRefKey(ref);
516
+ const { state, failureMessage } = useMemo(() => {
517
+ if (!ref) {
518
+ return {
519
+ state: { resolution: null, status: "empty", reason: null },
520
+ failureMessage: null
521
+ };
522
+ }
523
+ try {
524
+ const resolution = client.resolve(ref);
525
+ if (!resolution) {
526
+ return {
527
+ state: { resolution: null, status: "empty", reason: null },
528
+ failureMessage: null
529
+ };
530
+ }
531
+ return {
532
+ state: { resolution, status: "ready", reason: null },
533
+ failureMessage: null
534
+ };
535
+ } catch (err) {
536
+ return {
537
+ state: {
538
+ resolution: null,
539
+ status: "unavailable",
540
+ reason: client.classifyError(err)
541
+ },
542
+ failureMessage: err instanceof Error ? err.message : String(err ?? "resolve() failed")
543
+ };
544
+ }
545
+ }, [client, refKey]);
546
+ useEffect2(() => {
547
+ if (state.status !== "unavailable") return;
548
+ capture({
549
+ phase: "resolve",
550
+ mediaRef: failureMediaIdentity(ref ?? null),
551
+ terminal: true,
552
+ message: `resolve() refused (${state.reason ?? "unknown"}): ${failureMessage ?? "no error message"}`
553
+ });
554
+ }, [refKey, state.status]);
555
+ return state;
556
+ }
557
+
558
+ // src/viewers/use-viewer-bytes.ts
559
+ function useViewerBytes(source) {
560
+ const fromClient = useMediaBlob(source.kind === "ref" ? source.ref : null);
561
+ if (source.kind === "blob") {
562
+ return { blob: source.blob, loading: false, error: null };
563
+ }
564
+ return {
565
+ blob: fromClient.blob,
566
+ loading: fromClient.loading,
567
+ error: fromClient.error
568
+ };
569
+ }
570
+ async function readBlobText(blob) {
571
+ const maybeText = blob.text;
572
+ if (typeof maybeText === "function") return await maybeText.call(blob);
573
+ const maybeArrayBuffer = blob.arrayBuffer;
574
+ if (typeof maybeArrayBuffer === "function") {
575
+ return new TextDecoder().decode(await maybeArrayBuffer.call(blob));
576
+ }
577
+ return await new Promise((resolve, reject) => {
578
+ const reader = new FileReader();
579
+ reader.onload = () => resolve(String(reader.result ?? ""));
580
+ reader.onerror = () => reject(reader.error ?? new Error("could not read the file's bytes"));
581
+ reader.readAsText(blob);
582
+ });
583
+ }
584
+ function useViewerText(source, maxBytes = 1024 * 1024) {
585
+ const { blob, loading, error: bytesError } = useViewerBytes(source);
586
+ const [text, setText] = useState3(null);
587
+ const [readError, setReadError] = useState3(null);
588
+ const [truncated, setTruncated] = useState3(false);
589
+ useEffect3(() => {
590
+ if (!blob) {
591
+ setText(null);
592
+ return void 0;
593
+ }
594
+ let cancelled = false;
595
+ setReadError(null);
596
+ setText(null);
597
+ setTruncated(false);
598
+ void (async () => {
599
+ try {
600
+ const oversized = blob.size > maxBytes;
601
+ const value = await readBlobText(
602
+ oversized ? blob.slice(0, maxBytes) : blob
603
+ );
604
+ if (cancelled) return;
605
+ setTruncated(oversized);
606
+ setText(value);
607
+ } catch (err) {
608
+ if (cancelled) return;
609
+ setReadError(err instanceof Error ? err.message : String(err));
610
+ }
611
+ })();
612
+ return () => {
613
+ cancelled = true;
614
+ };
615
+ }, [blob, maxBytes]);
616
+ const error = readError ?? bytesError;
617
+ return {
618
+ text,
619
+ error,
620
+ truncated,
621
+ loading: (loading || text === null) && error === null
622
+ };
623
+ }
624
+ function useViewerDisplayUrl(source) {
625
+ const resolved = useMediaResolution(
626
+ source.kind === "ref" ? source.ref : null
627
+ );
628
+ if (source.kind === "blob") {
629
+ return {
630
+ url: source.url,
631
+ recoverable: false,
632
+ unavailable: false,
633
+ loading: false
634
+ };
635
+ }
636
+ return {
637
+ url: resolved.resolution?.src ?? null,
638
+ recoverable: resolved.resolution?.recoverable !== false,
639
+ unavailable: resolved.status === "unavailable",
640
+ loading: resolved.status === "empty"
641
+ };
642
+ }
643
+
644
+ // src/viewers/code-viewer.tsx
645
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
646
+ var EXT_TO_LANGUAGE = {
647
+ js: "javascript",
648
+ jsx: "jsx",
649
+ mjs: "javascript",
650
+ cjs: "javascript",
651
+ ts: "typescript",
652
+ tsx: "tsx",
653
+ py: "python",
654
+ rb: "ruby",
655
+ go: "go",
656
+ rs: "rust",
657
+ java: "java",
658
+ kt: "kotlin",
659
+ swift: "swift",
660
+ c: "c",
661
+ h: "c",
662
+ cpp: "cpp",
663
+ cc: "cpp",
664
+ cxx: "cpp",
665
+ hpp: "cpp",
666
+ cs: "csharp",
667
+ php: "php",
668
+ scala: "scala",
669
+ html: "markup",
670
+ htm: "markup",
671
+ xml: "markup",
672
+ svg: "markup",
673
+ css: "css",
674
+ scss: "scss",
675
+ less: "less",
676
+ json: "json",
677
+ yaml: "yaml",
678
+ yml: "yaml",
679
+ toml: "toml",
680
+ ini: "ini",
681
+ sh: "bash",
682
+ bash: "bash",
683
+ zsh: "bash",
684
+ fish: "bash",
685
+ ps1: "powershell",
686
+ sql: "sql",
687
+ graphql: "graphql",
688
+ gql: "graphql",
689
+ proto: "protobuf",
690
+ dockerfile: "docker",
691
+ makefile: "makefile",
692
+ lua: "lua",
693
+ r: "r"
694
+ };
695
+ function extensionOf(fileName) {
696
+ const base = fileName.split(/[\\/]/).pop() ?? fileName;
697
+ const dot = base.lastIndexOf(".");
698
+ if (dot <= 0) return "";
699
+ return base.slice(dot + 1).toLowerCase();
700
+ }
701
+ function detectCodeLanguage(fileName) {
702
+ const ext = extensionOf(fileName);
703
+ if (ext) return EXT_TO_LANGUAGE[ext] ?? "text";
704
+ const lower = fileName.toLowerCase();
705
+ if (lower === "dockerfile") return "docker";
706
+ if (lower === "makefile") return "makefile";
707
+ return "text";
708
+ }
709
+ function CodeViewer({
710
+ source,
711
+ fileName,
712
+ className,
713
+ maxBytes = 1024 * 1024
714
+ }) {
715
+ const { text, error, loading, truncated } = useViewerText(source, maxBytes);
716
+ const [wrap, setWrap] = useState4(false);
717
+ const language = useMemo2(() => detectCodeLanguage(fileName), [fileName]);
718
+ const highlighter = getMediaCodeHighlighter();
719
+ if (error) return /* @__PURE__ */ jsx3(ViewerError, { message: error, className });
720
+ if (loading || text === null) return /* @__PURE__ */ jsx3(ViewerSkeleton, { className });
721
+ const lines = text.split("\n");
722
+ return /* @__PURE__ */ jsxs3(
723
+ "div",
724
+ {
725
+ className: cn(
726
+ "flex h-full w-full flex-col overflow-hidden bg-card",
727
+ className
728
+ ),
729
+ children: [
730
+ /* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-border bg-muted/30 px-3 py-1.5", children: [
731
+ /* @__PURE__ */ jsxs3("div", { className: "flex min-w-0 items-center gap-2 text-xs text-muted-foreground", children: [
732
+ /* @__PURE__ */ jsx3("span", { className: "font-mono tracking-wide uppercase", children: language }),
733
+ truncated ? /* @__PURE__ */ jsxs3("span", { className: "text-warning", children: [
734
+ "\xB7 ",
735
+ truncationNote(maxBytes)
736
+ ] }) : null
737
+ ] }),
738
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
739
+ highlighter ? null : /* @__PURE__ */ jsx3(
740
+ "button",
741
+ {
742
+ type: "button",
743
+ onClick: () => setWrap((value) => !value),
744
+ "aria-pressed": wrap,
745
+ className: cn(
746
+ "rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-accent hover:text-foreground",
747
+ wrap && "bg-accent text-accent-foreground"
748
+ ),
749
+ children: "Wrap"
750
+ }
751
+ ),
752
+ /* @__PURE__ */ jsx3(ViewerCopyButton, { text })
753
+ ] })
754
+ ] }),
755
+ /* @__PURE__ */ jsx3("div", { className: "min-h-0 flex-1 overflow-auto", children: highlighter ? /* @__PURE__ */ jsx3(Fragment, { children: highlighter({ code: text, language }) }) : /* @__PURE__ */ jsxs3("pre", { className: "m-0 flex p-3 font-mono text-xs leading-5", children: [
756
+ /* @__PURE__ */ jsx3(
757
+ "code",
758
+ {
759
+ "aria-hidden": true,
760
+ className: "mr-3 shrink-0 border-r border-border/60 pr-3 text-right text-muted-foreground/60 select-none",
761
+ children: lines.map((_, index) => `${index + 1}
762
+ `).join("")
763
+ }
764
+ ),
765
+ /* @__PURE__ */ jsx3(
766
+ "code",
767
+ {
768
+ className: cn(
769
+ "min-w-0 flex-1 text-foreground",
770
+ wrap ? "break-words whitespace-pre-wrap" : "whitespace-pre"
771
+ ),
772
+ children: text
773
+ }
774
+ )
775
+ ] }) })
776
+ ]
777
+ }
778
+ );
779
+ }
780
+
781
+ // src/core/file-kind.ts
782
+ var ROWS = [
783
+ [["jpg", "jpeg", "png", "gif", "webp", "avif", "heic", "heif", "bmp", "tif", "tiff", "ico"], "IMAGE", "text-emerald-500", "image", "Image"],
784
+ [["svg"], "IMAGE", "text-amber-500", "pen-tool", "SVG image"],
785
+ [["mp4", "mov", "webm", "mkv", "avi", "m4v"], "VIDEO", "text-purple-500", "video", "Video"],
786
+ [["mp3", "wav", "ogg", "m4a", "aac", "flac", "opus"], "AUDIO", "text-pink-500", "music", "Audio"],
787
+ [["pdf"], "DOCUMENT", "text-red-500", "file-type", "PDF Document"],
788
+ [["md", "markdown", "mdx", "rst", "adoc", "asciidoc", "org"], "DOCUMENT", "text-slate-400", "file-text", "Markdown"],
789
+ [["txt", "text", "asc", "me", "log", "out", "err"], "DOCUMENT", "text-muted-foreground", "file-text", "Text"],
790
+ [["dockerfile", "containerfile", "lua"], "CODE", "text-blue-500", "code", "Code"],
791
+ [["mk", "make", "swift", "ml", "mli", "fs", "fsi", "fsx", "svelte"], "CODE", "text-orange-500", "code", "Code"],
792
+ [["diff", "patch", "pem", "csr", "crt", "cer", "der", "key", "pub", "p7b", "p7c", "pfx", "p12"], "CODE", "text-amber-500", "file-text", "Text"],
793
+ [["ini", "cfg", "conf", "config", "properties", "prefs"], "CODE", "text-amber-400", "braces", "Config"],
794
+ [["tex", "latex", "ltx", "sty", "cls", "bib"], "CODE", "text-emerald-500", "file-text", "LaTeX"],
795
+ [["srt", "vtt"], "SUBTITLES", "text-cyan-500", "subtitles", "Subtitles"],
796
+ [["js", "mjs", "cjs", "jsx"], "CODE", "text-yellow-500", "file-code", "JavaScript"],
797
+ [["ts", "tsx"], "CODE", "text-blue-500", "file-code", "TypeScript"],
798
+ [["py", "clj", "cljs", "cljc", "edn", "vue", "styl", "stylus"], "CODE", "text-emerald-500", "code", "Code"],
799
+ [["rb", "rbw", "rake", "ru", "gemspec", "scala", "sbt"], "CODE", "text-red-500", "code", "Code"],
800
+ [["go", "dart"], "CODE", "text-cyan-500", "code", "Code"],
801
+ [["rs"], "CODE", "text-orange-600", "code", "Rust"],
802
+ [["java"], "CODE", "text-red-600", "code", "Java"],
803
+ [["c", "h"], "CODE", "text-blue-600", "code", "C"],
804
+ [["cpp", "cc", "cxx", "hpp"], "CODE", "text-blue-700", "code", "C++"],
805
+ [["cs"], "CODE", "text-purple-600", "code", "C#"],
806
+ [["html", "htm", "kt", "kts"], "CODE", "text-orange-400", "code", "Code"],
807
+ [["css"], "CODE", "text-sky-400", "code", "CSS"],
808
+ [["scss"], "CODE", "text-pink-400", "code", "SCSS"],
809
+ [["sh", "bash", "zsh", "fish", "ksh", "ash", "bat", "cmd", "twig", "jinja", "j2", "njk"], "CODE", "text-emerald-400", "code", "Script"],
810
+ [["sql"], "CODE", "text-amber-400", "code", "SQL"],
811
+ [["pl", "pm"], "CODE", "text-indigo-400", "code", "Perl"],
812
+ [["r", "rmd", "proto", "ps1", "psm1", "psd1", "less"], "CODE", "text-blue-400", "code", "Code"],
813
+ [["ex", "exs"], "CODE", "text-purple-400", "code", "Elixir"],
814
+ [["erl", "hrl"], "CODE", "text-rose-500", "code", "Erlang"],
815
+ [["hs", "lhs"], "CODE", "text-violet-500", "code", "Haskell"],
816
+ [["zig"], "CODE", "text-yellow-500", "code", "Zig"],
817
+ [["nim", "nims"], "CODE", "text-yellow-400", "code", "Nim"],
818
+ [["jl"], "CODE", "text-purple-500", "code", "Julia"],
819
+ [["astro"], "CODE", "text-fuchsia-500", "code", "Astro"],
820
+ [["graphql", "gql", "graphqls"], "CODE", "text-pink-500", "code", "GraphQL"],
821
+ [["sol"], "CODE", "text-slate-400", "code", "Solidity"],
822
+ [["php", "phtml", "phps"], "CODE", "text-indigo-500", "code", "PHP"],
823
+ [["hbs", "handlebars", "mustache", "ejs", "liquid"], "CODE", "text-amber-500", "code", "Template"],
824
+ [["json", "jsonc", "json5", "har", "geojson", "topojson"], "DATA", "text-amber-500", "file-json", "JSON"],
825
+ [["yaml", "yml", "toml"], "CODE", "text-amber-500", "braces", "Config"],
826
+ [["xml"], "DATA", "text-amber-500", "braces", "XML"],
827
+ [["csv", "tsv"], "DATA", "text-orange-500", "file-spreadsheet", "Spreadsheet data"],
828
+ [["sqlite", "sqlite3", "db"], "DATA", "text-blue-400", "database", "Database"],
829
+ [["ttl", "n3", "nt", "nq", "trig"], "DATA", "text-fuchsia-400", "braces", "RDF data"],
830
+ [["xlsx", "xls"], "DOCUMENT", "text-emerald-600", "file-spreadsheet", "Excel workbook"],
831
+ [["doc", "docx"], "DOCUMENT", "text-blue-500", "file-text", "Word document"],
832
+ [["ppt", "pptx"], "DOCUMENT", "text-orange-600", "file-type", "Presentation"],
833
+ [["ipynb"], "NOTEBOOK", "text-orange-400", "file-json", "Notebook"],
834
+ [["epub"], "EBOOK", "text-indigo-500", "file-text", "eBook"],
835
+ [["eml"], "EMAIL", "text-blue-400", "file-text", "Email"],
836
+ [["glb", "gltf", "stl", "obj", "fbx"], "MODEL_3D", "text-violet-400", "package", "3D model"],
837
+ [["zip", "rar", "7z"], "ARCHIVE", "text-amber-600", "archive", "Archive"],
838
+ [["tar", "gz", "tgz"], "ARCHIVE", "text-amber-600", "package", "Archive"]
839
+ ];
840
+ var BY_EXTENSION = /* @__PURE__ */ new Map();
841
+ for (const [extensions, category, colorClass, icon, displayName] of ROWS) {
842
+ for (const ext of extensions) {
843
+ BY_EXTENSION.set(ext, { category, colorClass, icon, displayName });
844
+ }
845
+ }
846
+ var UNKNOWN_DETAILS = {
847
+ category: "UNKNOWN",
848
+ displayName: "File",
849
+ colorClass: "text-muted-foreground",
850
+ icon: "file"
851
+ };
852
+ var ASSUMED_TEXT_DETAILS = {
853
+ category: "DOCUMENT",
854
+ displayName: "Text",
855
+ colorClass: "text-muted-foreground",
856
+ icon: "file-text"
857
+ };
858
+ var FOLDER_DETAILS = {
859
+ category: "FOLDER",
860
+ displayName: "Folder",
861
+ colorClass: "text-sky-500",
862
+ icon: "folder"
863
+ };
864
+ var IMAGE_DETAILS = BY_EXTENSION.get("png");
865
+ var VIDEO_DETAILS = BY_EXTENSION.get("mp4");
866
+ var AUDIO_DETAILS = BY_EXTENSION.get("mp3");
867
+ var PDF_DETAILS = BY_EXTENSION.get("pdf");
868
+ var SVG_DETAILS = BY_EXTENSION.get("svg");
869
+ function basenameOf(path) {
870
+ const idx = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
871
+ return idx >= 0 ? path.slice(idx + 1) : path;
872
+ }
873
+ function extOf(name) {
874
+ const idx = name.lastIndexOf(".");
875
+ if (idx <= 0 || idx === name.length - 1) return null;
876
+ return name.slice(idx + 1).toLowerCase();
877
+ }
878
+ function dotfileLooksLikeText(filename) {
879
+ const name = basenameOf(filename);
880
+ if (!name.startsWith(".")) return false;
881
+ for (let i = 1; i < name.length; i++) {
882
+ const c = name.charCodeAt(i);
883
+ if (c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 45) {
884
+ return true;
885
+ }
886
+ }
887
+ return false;
888
+ }
889
+ function canonicalMimeBase(raw) {
890
+ return raw.split(";")[0]?.trim().toLowerCase() ?? raw;
891
+ }
892
+ function getFileKindDetails(fileName, mimeType) {
893
+ const name = basenameOf(fileName);
894
+ const lower = name.toLowerCase();
895
+ const ext = lower === "dockerfile" || lower === "containerfile" ? lower : extOf(name);
896
+ const fromExt = ext ? BY_EXTENSION.get(ext) : void 0;
897
+ const mime = mimeType ? canonicalMimeBase(mimeType) : null;
898
+ if (mime) {
899
+ if (mime === "image/svg+xml") return fromExt ?? SVG_DETAILS;
900
+ if (mime.startsWith("image/")) return fromExt?.category === "IMAGE" ? fromExt : IMAGE_DETAILS;
901
+ if (mime.startsWith("video/")) return fromExt?.category === "VIDEO" ? fromExt : VIDEO_DETAILS;
902
+ if (mime.startsWith("audio/")) return fromExt?.category === "AUDIO" ? fromExt : AUDIO_DETAILS;
903
+ if (mime === "application/pdf") return PDF_DETAILS;
904
+ }
905
+ if (fromExt) return fromExt;
906
+ if (dotfileLooksLikeText(fileName)) return ASSUMED_TEXT_DETAILS;
907
+ return UNKNOWN_DETAILS;
908
+ }
909
+ function getFolderKindDetails(open = false) {
910
+ return open ? { ...FOLDER_DETAILS, icon: "folder-open" } : FOLDER_DETAILS;
911
+ }
912
+
913
+ // src/react/file-icon.tsx
914
+ import { jsx as jsx4 } from "react/jsx-runtime";
915
+ var ICONS = {
916
+ image: ImageIcon,
917
+ "pen-tool": PenToolIcon,
918
+ video: VideoIcon,
919
+ music: MusicIcon,
920
+ "file-type": FileTypeIcon,
921
+ "file-text": FileTextIcon,
922
+ code: CodeIcon,
923
+ braces: BracesIcon,
924
+ subtitles: SubtitlesIcon,
925
+ "file-code": FileCodeIcon,
926
+ "file-json": FileJsonIcon,
927
+ "file-spreadsheet": FileSpreadsheetIcon,
928
+ database: DatabaseIcon,
929
+ package: PackageBoxIcon,
930
+ archive: ArchiveIcon,
931
+ folder: FolderIcon,
932
+ "folder-open": FolderOpenIcon,
933
+ file: FileIcon,
934
+ "file-audio": FileAudioIcon,
935
+ "file-video": FileVideoIcon
936
+ };
937
+ function FileIcon2({
938
+ fileName,
939
+ mimeType = null,
940
+ isFolder,
941
+ open,
942
+ colorClass,
943
+ className,
944
+ size = 16
945
+ }) {
946
+ const details = isFolder ? getFolderKindDetails(open) : getFileKindDetails(fileName ?? "", mimeType);
947
+ const Icon = ICONS[details.icon];
948
+ const style = {
949
+ width: `${size}px`,
950
+ height: `${size}px`,
951
+ minWidth: `${size}px`
952
+ };
953
+ return /* @__PURE__ */ jsx4(
954
+ Icon,
955
+ {
956
+ style,
957
+ className: cn("shrink-0", colorClass ?? details.colorClass, className),
958
+ "aria-hidden": "true"
959
+ }
960
+ );
961
+ }
962
+
963
+ // src/react/upload-progress-list.tsx
964
+ import { memo, useEffect as useEffect4, useRef as useRef2, useState as useState5 } from "react";
965
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
966
+ var AUTO_DISMISS_MS = 1e4;
967
+ function formatFileSize(bytes) {
968
+ if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "\u2014";
969
+ if (bytes < 1024) return `${bytes} B`;
970
+ const units = ["KB", "MB", "GB", "TB"];
971
+ let value = bytes;
972
+ let unit = -1;
973
+ do {
974
+ value /= 1024;
975
+ unit += 1;
976
+ } while (value >= 1024 && unit < units.length - 1);
977
+ return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit] ?? "TB"}`;
978
+ }
979
+ function UploadProgressListImpl({
980
+ uploads,
981
+ onDismiss,
982
+ onOpenFile,
983
+ className
984
+ }) {
985
+ const [hovered, setHovered] = useState5(false);
986
+ const activeCount = uploads.filter((u) => u.status === "uploading").length;
987
+ const successCount = uploads.filter((u) => u.status === "success").length;
988
+ const errorCount = uploads.filter((u) => u.status === "error").length;
989
+ const timersRef = useRef2(
990
+ /* @__PURE__ */ new Map()
991
+ );
992
+ const onDismissRef = useRef2(onDismiss);
993
+ onDismissRef.current = onDismiss;
994
+ useEffect4(() => {
995
+ const timers = timersRef.current;
996
+ if (hovered) {
997
+ timers.forEach((t) => clearTimeout(t));
998
+ timers.clear();
999
+ return;
1000
+ }
1001
+ for (const u of uploads) {
1002
+ if (u.status !== "success") continue;
1003
+ if (timers.has(u.id)) continue;
1004
+ const elapsed = u.completedAt ? Date.now() - u.completedAt : 0;
1005
+ const remaining = Math.max(0, AUTO_DISMISS_MS - elapsed);
1006
+ const handle = setTimeout(() => {
1007
+ timers.delete(u.id);
1008
+ onDismissRef.current(u.id);
1009
+ }, remaining);
1010
+ timers.set(u.id, handle);
1011
+ }
1012
+ const liveSuccessIds = new Set(
1013
+ uploads.filter((u) => u.status === "success").map((u) => u.id)
1014
+ );
1015
+ for (const [id, handle] of timers) {
1016
+ if (!liveSuccessIds.has(id)) {
1017
+ clearTimeout(handle);
1018
+ timers.delete(id);
1019
+ }
1020
+ }
1021
+ }, [uploads, hovered]);
1022
+ useEffect4(() => {
1023
+ const timers = timersRef.current;
1024
+ return () => {
1025
+ timers.forEach((t) => clearTimeout(t));
1026
+ timers.clear();
1027
+ };
1028
+ }, []);
1029
+ if (uploads.length === 0) return null;
1030
+ const headerLabel = activeCount > 0 ? `Uploading ${activeCount} file${activeCount === 1 ? "" : "s"}\u2026` : errorCount > 0 && successCount === 0 ? `${errorCount} upload${errorCount === 1 ? "" : "s"} failed` : `Uploaded ${successCount} file${successCount === 1 ? "" : "s"}`;
1031
+ const completedIds = uploads.filter((u) => u.status === "success" || u.status === "error").map((u) => u.id);
1032
+ return /* @__PURE__ */ jsxs4(
1033
+ "div",
1034
+ {
1035
+ className: cn(
1036
+ "overflow-hidden rounded-lg border bg-background/95 shadow-lg backdrop-blur",
1037
+ className
1038
+ ),
1039
+ onMouseEnter: () => setHovered(true),
1040
+ onMouseLeave: () => setHovered(false),
1041
+ "aria-label": "Uploads",
1042
+ children: [
1043
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between gap-2 border-b bg-muted/40 px-3 py-1.5", children: [
1044
+ /* @__PURE__ */ jsx5("span", { className: "truncate text-xs font-medium text-foreground", children: headerLabel }),
1045
+ completedIds.length > 0 ? /* @__PURE__ */ jsx5(
1046
+ "button",
1047
+ {
1048
+ type: "button",
1049
+ onClick: () => {
1050
+ for (const id of completedIds) onDismiss(id);
1051
+ },
1052
+ className: "shrink-0 rounded px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground",
1053
+ children: "Clear"
1054
+ }
1055
+ ) : null
1056
+ ] }),
1057
+ /* @__PURE__ */ jsx5("ul", { className: "max-h-72 space-y-1 overflow-y-auto p-2", children: uploads.map((u) => {
1058
+ const percent = u.fileSize > 0 ? Math.min(100, Math.round(u.bytesUploaded / u.fileSize * 100)) : 0;
1059
+ const done = u.status === "success";
1060
+ const failed = u.status === "error";
1061
+ const canOpen = done && !!u.fileId && !!onOpenFile;
1062
+ return /* @__PURE__ */ jsxs4(
1063
+ "li",
1064
+ {
1065
+ className: cn(
1066
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-xs",
1067
+ canOpen && "cursor-pointer hover:bg-accent/60"
1068
+ ),
1069
+ onClick: canOpen ? () => onOpenFile(u.fileId) : void 0,
1070
+ children: [
1071
+ /* @__PURE__ */ jsx5(FileIcon2, { fileName: u.fileName, size: 18, className: "shrink-0" }),
1072
+ /* @__PURE__ */ jsxs4("div", { className: "min-w-0 flex-1", children: [
1073
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between gap-2", children: [
1074
+ /* @__PURE__ */ jsx5("span", { className: "truncate font-medium text-foreground", children: u.fileName }),
1075
+ /* @__PURE__ */ jsx5("span", { className: "shrink-0 tabular-nums text-muted-foreground", children: failed ? "Failed" : done ? "Done" : `${percent}%` })
1076
+ ] }),
1077
+ done ? null : /* @__PURE__ */ jsx5("div", { className: "mt-1 h-1 w-full rounded bg-muted", children: /* @__PURE__ */ jsx5(
1078
+ "div",
1079
+ {
1080
+ className: cn(
1081
+ "h-1 rounded transition-all",
1082
+ failed ? "bg-destructive" : "bg-primary"
1083
+ ),
1084
+ style: { width: failed ? "100%" : `${percent}%` }
1085
+ }
1086
+ ) }),
1087
+ failed && u.error ? /* @__PURE__ */ jsx5("p", { className: "mt-0.5 truncate text-[10px] text-destructive", children: u.error }) : !done ? /* @__PURE__ */ jsxs4("p", { className: "mt-0.5 text-[10px] text-muted-foreground", children: [
1088
+ formatFileSize(u.bytesUploaded),
1089
+ " /",
1090
+ " ",
1091
+ formatFileSize(u.fileSize)
1092
+ ] }) : null
1093
+ ] }),
1094
+ canOpen ? /* @__PURE__ */ jsxs4("span", { className: "flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-medium text-primary", children: [
1095
+ /* @__PURE__ */ jsx5(ExternalLinkIcon, { className: "h-3 w-3", width: 12, height: 12 }),
1096
+ "Open"
1097
+ ] }) : done ? /* @__PURE__ */ jsx5(
1098
+ CheckCircle2Icon,
1099
+ {
1100
+ className: "h-4 w-4 shrink-0 text-emerald-500",
1101
+ width: 16,
1102
+ height: 16
1103
+ }
1104
+ ) : failed ? /* @__PURE__ */ jsx5(
1105
+ AlertCircleIcon,
1106
+ {
1107
+ className: "h-4 w-4 shrink-0 text-destructive",
1108
+ width: 16,
1109
+ height: 16
1110
+ }
1111
+ ) : null,
1112
+ done || failed ? /* @__PURE__ */ jsx5(
1113
+ "button",
1114
+ {
1115
+ type: "button",
1116
+ onClick: (e) => {
1117
+ e.stopPropagation();
1118
+ onDismiss(u.id);
1119
+ },
1120
+ "aria-label": "Dismiss",
1121
+ className: "flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground",
1122
+ children: /* @__PURE__ */ jsx5(XIcon, { className: "h-3 w-3", width: 12, height: 12 })
1123
+ }
1124
+ ) : null
1125
+ ]
1126
+ },
1127
+ u.id
1128
+ );
1129
+ }) })
1130
+ ]
1131
+ }
1132
+ );
1133
+ }
1134
+ var UploadProgressList = memo(UploadProgressListImpl);
1135
+
1136
+ // src/viewers/generic-viewer.tsx
1137
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1138
+ function GenericViewer({
1139
+ fileName,
1140
+ fileSize,
1141
+ generic,
1142
+ className
1143
+ }) {
1144
+ const onDownload = generic?.onDownload;
1145
+ const onViewAsText = generic?.onViewAsText;
1146
+ const viewAsTextLabel = generic?.viewAsTextLabel ?? "View as text";
1147
+ const viewAsTextPrimary = generic?.viewAsTextPrimary ?? true;
1148
+ const viewAsTextBusy = generic?.viewAsTextBusy ?? false;
1149
+ const hasViewAsText = typeof onViewAsText === "function";
1150
+ const hasDownload = typeof onDownload === "function";
1151
+ return /* @__PURE__ */ jsxs5(
1152
+ "div",
1153
+ {
1154
+ className: cn(
1155
+ "relative flex h-full w-full flex-col items-center justify-center gap-4 p-6 text-center",
1156
+ className
1157
+ ),
1158
+ children: [
1159
+ /* @__PURE__ */ jsx6(FileIcon2, { fileName, size: 48 }),
1160
+ /* @__PURE__ */ jsxs5("div", { children: [
1161
+ /* @__PURE__ */ jsx6("p", { className: "max-w-md truncate text-sm font-medium", children: fileName }),
1162
+ /* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: formatFileSize(fileSize ?? null) })
1163
+ ] }),
1164
+ /* @__PURE__ */ jsx6("p", { className: "max-w-sm text-sm text-muted-foreground", children: generic?.message ?? (hasViewAsText ? "No built-in previewer for this file type." : "This file type can't be previewed here.") }),
1165
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [
1166
+ onViewAsText && viewAsTextPrimary ? /* @__PURE__ */ jsx6(
1167
+ ViewAsTextButton,
1168
+ {
1169
+ onClick: onViewAsText,
1170
+ label: viewAsTextLabel,
1171
+ busy: viewAsTextBusy,
1172
+ variant: "primary"
1173
+ }
1174
+ ) : null,
1175
+ onDownload ? /* @__PURE__ */ jsxs5(
1176
+ "button",
1177
+ {
1178
+ type: "button",
1179
+ onClick: onDownload,
1180
+ className: cn(
1181
+ "inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm",
1182
+ hasViewAsText && viewAsTextPrimary ? "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground" : "bg-primary text-primary-foreground hover:bg-primary/90"
1183
+ ),
1184
+ children: [
1185
+ /* @__PURE__ */ jsx6(DownloadIcon, { className: "h-4 w-4" }),
1186
+ "Download"
1187
+ ]
1188
+ }
1189
+ ) : null,
1190
+ onViewAsText && !viewAsTextPrimary ? /* @__PURE__ */ jsx6(
1191
+ ViewAsTextButton,
1192
+ {
1193
+ onClick: onViewAsText,
1194
+ label: viewAsTextLabel,
1195
+ busy: viewAsTextBusy,
1196
+ variant: "secondary"
1197
+ }
1198
+ ) : null
1199
+ ] }),
1200
+ !hasDownload && !hasViewAsText ? /* @__PURE__ */ jsx6("p", { className: "max-w-sm text-[11px] text-muted-foreground/70", children: "Download it from the file's actions menu to open it in another application." }) : null
1201
+ ]
1202
+ }
1203
+ );
1204
+ }
1205
+ function ViewAsTextButton({
1206
+ onClick,
1207
+ label,
1208
+ busy,
1209
+ variant
1210
+ }) {
1211
+ return /* @__PURE__ */ jsxs5(
1212
+ "button",
1213
+ {
1214
+ type: "button",
1215
+ onClick,
1216
+ disabled: busy,
1217
+ className: cn(
1218
+ "inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm transition-colors",
1219
+ "disabled:cursor-not-allowed disabled:opacity-60",
1220
+ variant === "primary" ? "bg-primary text-primary-foreground hover:bg-primary/90" : "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground"
1221
+ ),
1222
+ children: [
1223
+ busy ? /* @__PURE__ */ jsx6(SpinnerIcon, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx6(FileTextIcon, { className: "h-4 w-4" }),
1224
+ label
1225
+ ]
1226
+ }
1227
+ );
1228
+ }
1229
+
1230
+ // src/viewers/html-viewer.tsx
1231
+ import { useEffect as useEffect5, useState as useState7 } from "react";
1232
+
1233
+ // src/viewers/markup-chrome.tsx
1234
+ import { useState as useState6 } from "react";
1235
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1236
+ function useMarkupMode(controls) {
1237
+ const [localMode, setLocalMode] = useState6("rendered");
1238
+ const controlled = controls?.mode !== void 0;
1239
+ return {
1240
+ mode: controls?.mode ?? localMode,
1241
+ setMode: controls?.onModeChange ?? setLocalMode,
1242
+ controlled
1243
+ };
1244
+ }
1245
+ function MarkupModeToggle({
1246
+ mode,
1247
+ onChange,
1248
+ renderedLabel = "Rendered",
1249
+ renderedIcon = "image",
1250
+ className
1251
+ }) {
1252
+ return /* @__PURE__ */ jsxs6("div", { className: cn("flex items-center gap-0.5", className), children: [
1253
+ /* @__PURE__ */ jsx7(
1254
+ ToggleButton,
1255
+ {
1256
+ active: mode === "rendered",
1257
+ onClick: () => onChange("rendered"),
1258
+ label: renderedLabel,
1259
+ icon: renderedIcon === "globe" ? /* @__PURE__ */ jsx7(GlobeIcon, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx7(ImageIcon, { className: "h-3.5 w-3.5" })
1260
+ }
1261
+ ),
1262
+ /* @__PURE__ */ jsx7(
1263
+ ToggleButton,
1264
+ {
1265
+ active: mode === "source",
1266
+ onClick: () => onChange("source"),
1267
+ label: "Source",
1268
+ icon: /* @__PURE__ */ jsx7(CodeIcon, { className: "h-3.5 w-3.5" })
1269
+ }
1270
+ )
1271
+ ] });
1272
+ }
1273
+ function ToggleButton({
1274
+ active,
1275
+ onClick,
1276
+ icon,
1277
+ label
1278
+ }) {
1279
+ return /* @__PURE__ */ jsxs6(
1280
+ "button",
1281
+ {
1282
+ type: "button",
1283
+ onClick,
1284
+ "aria-pressed": active,
1285
+ className: cn(
1286
+ "inline-flex items-center gap-1.5 rounded px-2 py-1 text-[11px] font-medium transition-colors",
1287
+ active ? "bg-accent text-accent-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"
1288
+ ),
1289
+ children: [
1290
+ icon,
1291
+ /* @__PURE__ */ jsx7("span", { children: label })
1292
+ ]
1293
+ }
1294
+ );
1295
+ }
1296
+
1297
+ // src/viewers/html-viewer.tsx
1298
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1299
+ var VIEWPORT_WIDTH = {
1300
+ mobile: 390,
1301
+ tablet: 768,
1302
+ desktop: 1280
1303
+ };
1304
+ function HtmlViewer(props) {
1305
+ const { className, fileName, markupControls } = props;
1306
+ const { mode, setMode, controlled } = useMarkupMode(markupControls);
1307
+ return /* @__PURE__ */ jsxs7("div", { className: cn("flex h-full w-full flex-col", className), children: [
1308
+ controlled ? null : /* @__PURE__ */ jsxs7("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-border/60 bg-background px-3 py-1.5 text-xs", children: [
1309
+ /* @__PURE__ */ jsx8("span", { className: "truncate font-medium text-foreground", children: fileName }),
1310
+ /* @__PURE__ */ jsx8(
1311
+ MarkupModeToggle,
1312
+ {
1313
+ mode,
1314
+ onChange: setMode,
1315
+ renderedLabel: "Rendered",
1316
+ renderedIcon: "globe"
1317
+ }
1318
+ )
1319
+ ] }),
1320
+ /* @__PURE__ */ jsx8("div", { className: "relative min-h-0 flex-1", children: mode === "rendered" ? /* @__PURE__ */ jsx8(HtmlRenderedView, { ...props }) : /* @__PURE__ */ jsx8(HtmlSourceView, { ...props }) })
1321
+ ] });
1322
+ }
1323
+ function HtmlRenderedView({ source, fileName, markupControls }) {
1324
+ const { url, unavailable } = useViewerDisplayUrl(source);
1325
+ const viewport = markupControls?.viewport ?? "auto";
1326
+ const reloadKey = markupControls?.reloadKey ?? 0;
1327
+ const [errored, setErrored] = useState7(false);
1328
+ useEffect5(() => {
1329
+ setErrored(false);
1330
+ }, [reloadKey, url]);
1331
+ if (unavailable) {
1332
+ return /* @__PURE__ */ jsx8(
1333
+ ViewerError,
1334
+ {
1335
+ title: "This page isn't available",
1336
+ hint: "You may not have access to it, or it may have been deleted."
1337
+ }
1338
+ );
1339
+ }
1340
+ if (!url) return /* @__PURE__ */ jsx8(ViewerSkeleton, {});
1341
+ if (errored) return /* @__PURE__ */ jsx8(ViewerError, { title: "Preview unavailable" });
1342
+ const constrained = viewport !== "auto";
1343
+ const width = constrained ? VIEWPORT_WIDTH[viewport] : void 0;
1344
+ return /* @__PURE__ */ jsx8(
1345
+ "div",
1346
+ {
1347
+ className: cn(
1348
+ "h-full w-full overflow-auto bg-muted/10",
1349
+ constrained && "flex items-start justify-center"
1350
+ ),
1351
+ children: /* @__PURE__ */ jsx8(
1352
+ "div",
1353
+ {
1354
+ className: cn(
1355
+ "h-full bg-background",
1356
+ constrained ? "my-4 overflow-hidden rounded-md border border-border/60 shadow-lg" : "w-full"
1357
+ ),
1358
+ style: constrained ? { width } : void 0,
1359
+ children: /* @__PURE__ */ jsx8(
1360
+ "iframe",
1361
+ {
1362
+ src: url,
1363
+ title: fileName,
1364
+ sandbox: "allow-scripts allow-popups allow-forms",
1365
+ referrerPolicy: "no-referrer",
1366
+ className: "h-full w-full border-0 bg-white",
1367
+ onError: () => setErrored(true)
1368
+ },
1369
+ `${reloadKey}-${url}`
1370
+ )
1371
+ }
1372
+ )
1373
+ }
1374
+ );
1375
+ }
1376
+ function HtmlSourceView({ source, maxBytes = 1024 * 1024 }) {
1377
+ const { text, error, loading } = useViewerText(source, maxBytes);
1378
+ if (error) return /* @__PURE__ */ jsx8(ViewerError, { title: "Couldn't load source", message: error });
1379
+ if (loading || text === null) return /* @__PURE__ */ jsx8(ViewerSkeleton, {});
1380
+ return /* @__PURE__ */ jsx8("pre", { className: "h-full w-full overflow-auto bg-muted/10 p-4 font-mono text-xs leading-relaxed break-all whitespace-pre-wrap", children: text });
1381
+ }
1382
+
1383
+ // src/core/use-media-load-recovery.ts
1384
+ import { useEffect as useEffect6, useRef as useRef3, useState as useState8 } from "react";
1385
+
1386
+ // src/types.ts
1387
+ function mintDurableSrc(url) {
1388
+ return url;
1389
+ }
1390
+
1391
+ // src/core/use-media-load-recovery.ts
1392
+ function normalizeOutcome(outcome) {
1393
+ return typeof outcome === "string" ? { verdict: outcome } : outcome;
1394
+ }
1395
+ var EPHEMERAL_SECRET_IDENTITY = "server:files-session-secret";
1396
+ function useMediaLoadRecovery(src, options) {
1397
+ const client = useMediaClient();
1398
+ const capture = useMediaFailureCapture();
1399
+ const recoverable = options?.recoverable !== false;
1400
+ const onTerminal = options?.onTerminal;
1401
+ const failureRef = options?.failureRef;
1402
+ const [result, setResult] = useState8({ source: src, retryKey: 0, failed: false });
1403
+ const attemptRef = useRef3(0);
1404
+ useEffect6(() => {
1405
+ attemptRef.current = 0;
1406
+ }, [src]);
1407
+ const identity = () => failureMediaIdentity(failureRef ?? null, src);
1408
+ const onLoadError = (event) => {
1409
+ if (!src || !recoverable) {
1410
+ setResult({ source: src, retryKey: 0, failed: true });
1411
+ if (src) {
1412
+ capture({
1413
+ phase: "element-load",
1414
+ mediaRef: identity(),
1415
+ terminal: true,
1416
+ retryOutcome: "not-attempted",
1417
+ message: "media element failed to load and the source is unrecoverable (foreign/object URL \u2014 a session refresh cannot fix it)"
1418
+ });
1419
+ }
1420
+ onTerminal?.(event);
1421
+ return;
1422
+ }
1423
+ attemptRef.current += 1;
1424
+ const attempt = attemptRef.current;
1425
+ client.recoverLoadError(mintDurableSrc(src), attempt).then(
1426
+ (outcome) => {
1427
+ const report = normalizeOutcome(outcome);
1428
+ if (report.secretSource === "ephemeral") {
1429
+ capture({
1430
+ phase: "session",
1431
+ mediaRef: EPHEMERAL_SECRET_IDENTITY,
1432
+ terminal: false,
1433
+ message: "server cookie secret is ephemeral \u2014 private renders will break across restarts/instances (set a persistent files-session secret on the server)"
1434
+ });
1435
+ }
1436
+ if (report.verdict === "retry") {
1437
+ setResult((current) => ({
1438
+ source: src,
1439
+ retryKey: current.source === src ? current.retryKey + 1 : 1,
1440
+ failed: false
1441
+ }));
1442
+ } else {
1443
+ setResult({ source: src, retryKey: 0, failed: true });
1444
+ capture({
1445
+ phase: attempt > 1 ? "element-load" : "recovery",
1446
+ mediaRef: identity(),
1447
+ terminal: true,
1448
+ retryOutcome: attempt > 1 ? "retried" : "not-attempted",
1449
+ message: attempt > 1 ? "media element failed to load after the session-refresh retry" : "media load recovery declined on the first attempt (session refresh did not produce a usable session)",
1450
+ ...report.status !== void 0 ? { status: report.status } : {}
1451
+ });
1452
+ onTerminal?.(event);
1453
+ }
1454
+ },
1455
+ (err) => {
1456
+ setResult({ source: src, retryKey: 0, failed: true });
1457
+ capture({
1458
+ phase: "session",
1459
+ mediaRef: identity(),
1460
+ terminal: true,
1461
+ retryOutcome: "recovery-failed",
1462
+ message: `session recovery failed: ${err instanceof Error ? err.message : String(err ?? "unknown error")}`
1463
+ });
1464
+ onTerminal?.(event);
1465
+ }
1466
+ );
1467
+ };
1468
+ return {
1469
+ retryKey: result.source === src ? result.retryKey : 0,
1470
+ onLoadError,
1471
+ failed: result.source === src ? result.failed : false
1472
+ };
1473
+ }
1474
+
1475
+ // src/viewers/transparency-grid.ts
1476
+ var TRANSPARENCY_GRID_STYLE = {
1477
+ backgroundImage: "linear-gradient(45deg, rgb(0 0 0 / 0.08) 25%, transparent 25%), linear-gradient(-45deg, rgb(0 0 0 / 0.08) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, rgb(0 0 0 / 0.08) 75%), linear-gradient(-45deg, transparent 75%, rgb(0 0 0 / 0.08) 75%)",
1478
+ backgroundSize: "16px 16px",
1479
+ backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0"
1480
+ };
1481
+
1482
+ // src/viewers/svg-viewer.tsx
1483
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1484
+ function SvgViewer(props) {
1485
+ const { className, markupControls } = props;
1486
+ const { mode, setMode, controlled } = useMarkupMode(markupControls);
1487
+ return /* @__PURE__ */ jsxs8(
1488
+ "div",
1489
+ {
1490
+ className: cn(
1491
+ "relative flex h-full min-h-0 w-full flex-col bg-background",
1492
+ className
1493
+ ),
1494
+ children: [
1495
+ controlled ? null : /* @__PURE__ */ jsx9(
1496
+ MarkupModeToggle,
1497
+ {
1498
+ mode,
1499
+ onChange: setMode,
1500
+ className: "absolute top-3 right-3 z-10 rounded-md border border-border bg-background/95 p-0.5 shadow-sm backdrop-blur-sm"
1501
+ }
1502
+ ),
1503
+ mode === "rendered" ? /* @__PURE__ */ jsx9(SvgRenderedView, { ...props }) : /* @__PURE__ */ jsx9(SvgSourceView, { ...props })
1504
+ ]
1505
+ }
1506
+ );
1507
+ }
1508
+ function SvgRenderedView({ source, fileName }) {
1509
+ const { url, recoverable, unavailable } = useViewerDisplayUrl(source);
1510
+ const { retryKey, onLoadError, failed } = useMediaLoadRecovery(url, {
1511
+ recoverable,
1512
+ ...source.kind === "ref" ? { failureRef: source.ref } : {}
1513
+ });
1514
+ if (unavailable) {
1515
+ return /* @__PURE__ */ jsx9(
1516
+ ViewerError,
1517
+ {
1518
+ title: "This SVG isn't available",
1519
+ hint: "You may not have access to it, or it may have been deleted."
1520
+ }
1521
+ );
1522
+ }
1523
+ if (!url) return /* @__PURE__ */ jsx9(ViewerSkeleton, { className: "flex-1" });
1524
+ if (failed) {
1525
+ return /* @__PURE__ */ jsx9(
1526
+ ViewerError,
1527
+ {
1528
+ title: "SVG could not be rendered",
1529
+ hint: "Switch to the Source view to inspect the markup."
1530
+ }
1531
+ );
1532
+ }
1533
+ return /* @__PURE__ */ jsx9(
1534
+ "div",
1535
+ {
1536
+ className: "flex flex-1 items-center justify-center overflow-auto p-4",
1537
+ style: TRANSPARENCY_GRID_STYLE,
1538
+ children: /* @__PURE__ */ jsx9(
1539
+ "img",
1540
+ {
1541
+ src: url,
1542
+ alt: fileName,
1543
+ className: "max-h-full max-w-full object-contain drop-shadow-sm",
1544
+ onError: onLoadError
1545
+ },
1546
+ retryKey
1547
+ )
1548
+ }
1549
+ );
1550
+ }
1551
+ function SvgSourceView({ source, maxBytes = 1024 * 1024 }) {
1552
+ const { text, error, loading } = useViewerText(source, maxBytes);
1553
+ if (error) return /* @__PURE__ */ jsx9(ViewerError, { title: "Could not load source", message: error });
1554
+ if (loading || text === null) return /* @__PURE__ */ jsx9(ViewerSkeleton, { className: "flex-1" });
1555
+ return /* @__PURE__ */ jsx9("pre", { className: "m-0 flex-1 overflow-auto bg-muted/20 px-4 py-3 font-mono text-xs leading-relaxed break-words whitespace-pre-wrap text-foreground", children: text });
1556
+ }
1557
+
1558
+ // src/viewers/text-viewer.tsx
1559
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1560
+ function TextViewer({
1561
+ source,
1562
+ className,
1563
+ maxBytes = 1024 * 1024
1564
+ }) {
1565
+ const { text, error, loading, truncated } = useViewerText(source, maxBytes);
1566
+ if (error) return /* @__PURE__ */ jsx10(ViewerError, { message: error, className });
1567
+ if (loading || text === null) {
1568
+ return /* @__PURE__ */ jsx10(ViewerSkeleton, { tone: "plain", className });
1569
+ }
1570
+ return /* @__PURE__ */ jsxs9(
1571
+ "div",
1572
+ {
1573
+ className: cn("flex h-full w-full flex-col overflow-hidden", className),
1574
+ children: [
1575
+ /* @__PURE__ */ jsxs9(ViewerToolbar, { children: [
1576
+ /* @__PURE__ */ jsx10("span", { className: "text-muted-foreground", children: truncated ? `Preview ${truncationNote(maxBytes)}.` : `${text.length.toLocaleString()} characters` }),
1577
+ /* @__PURE__ */ jsx10(ViewerCopyButton, { text })
1578
+ ] }),
1579
+ /* @__PURE__ */ jsx10("pre", { className: "flex-1 overflow-auto bg-background p-3 font-mono text-xs leading-5 break-words whitespace-pre-wrap", children: text })
1580
+ ]
1581
+ }
1582
+ );
1583
+ }
1584
+
1585
+ // src/viewers/unavailable-viewer.tsx
1586
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1587
+ var ENGINE_FOR_KIND = {
1588
+ pdf: "a PDF engine (e.g. PDF.js / react-pdf)",
1589
+ markdown: "a Markdown renderer",
1590
+ data: "a spreadsheet/CSV parser",
1591
+ spreadsheet: "a spreadsheet/CSV parser",
1592
+ office: "an Office document extractor",
1593
+ image: "an image viewer",
1594
+ video: "a video player",
1595
+ audio: "an audio player"
1596
+ };
1597
+ function MediaViewerUnavailable({
1598
+ kind,
1599
+ ...props
1600
+ }) {
1601
+ const engine = ENGINE_FOR_KIND[kind] ?? `a "${kind}" viewer`;
1602
+ const generic = props.generic;
1603
+ return /* @__PURE__ */ jsxs10("div", { className: cn("flex h-full w-full flex-col", props.className), children: [
1604
+ /* @__PURE__ */ jsxs10("div", { className: "shrink-0 border-b border-amber-500/30 bg-amber-500/10 px-3 py-2 text-center text-xs text-amber-700 dark:text-amber-300", children: [
1605
+ "No ",
1606
+ kind,
1607
+ " viewer is registered in this app, so this file can't be displayed here. An app developer registers ",
1608
+ engine,
1609
+ " with",
1610
+ " ",
1611
+ /* @__PURE__ */ jsxs10("code", { className: "font-mono", children: [
1612
+ 'registerMediaViewer("',
1613
+ kind,
1614
+ '", \u2026)'
1615
+ ] }),
1616
+ "."
1617
+ ] }),
1618
+ /* @__PURE__ */ jsx11("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx11(
1619
+ GenericViewer,
1620
+ {
1621
+ ...props,
1622
+ className: void 0,
1623
+ generic: {
1624
+ ...generic,
1625
+ message: generic?.message ?? "You can still download this file and open it in another application."
1626
+ }
1627
+ }
1628
+ ) })
1629
+ ] });
1630
+ }
1631
+
1632
+ // src/viewers/preview-kind.ts
1633
+ var EXTENSION_KIND = {
1634
+ svg: "svg",
1635
+ pdf: "pdf",
1636
+ md: "markdown",
1637
+ markdown: "markdown",
1638
+ mdx: "markdown",
1639
+ html: "html",
1640
+ htm: "html",
1641
+ csv: "data",
1642
+ tsv: "data",
1643
+ xlsx: "spreadsheet",
1644
+ xls: "spreadsheet",
1645
+ doc: "office",
1646
+ docx: "office",
1647
+ ppt: "office",
1648
+ pptx: "office",
1649
+ txt: "text",
1650
+ log: "text",
1651
+ text: "text"
1652
+ };
1653
+ function extensionOf2(fileName) {
1654
+ const base = fileName.split(/[\\/]/).pop() ?? fileName;
1655
+ const dot = base.lastIndexOf(".");
1656
+ if (dot <= 0) return "";
1657
+ return base.slice(dot + 1).toLowerCase();
1658
+ }
1659
+ function getPreviewKind(fileName, mimeType) {
1660
+ const explicit = EXTENSION_KIND[extensionOf2(fileName)];
1661
+ if (explicit) return explicit;
1662
+ const details = getFileKindDetails(fileName, mimeType ?? void 0);
1663
+ switch (details.category) {
1664
+ case "IMAGE":
1665
+ return details.icon === "pen-tool" ? "svg" : "image";
1666
+ case "VIDEO":
1667
+ return "video";
1668
+ case "AUDIO":
1669
+ return "audio";
1670
+ case "CODE":
1671
+ return "code";
1672
+ case "DATA":
1673
+ return "code";
1674
+ case "NOTEBOOK":
1675
+ return "code";
1676
+ case "SUBTITLES":
1677
+ return "text";
1678
+ case "DOCUMENT":
1679
+ return "text";
1680
+ default:
1681
+ return "generic";
1682
+ }
1683
+ }
1684
+
1685
+ // src/viewers/media-file-viewer.tsx
1686
+ import { Fragment as Fragment2, jsx as jsx12 } from "react/jsx-runtime";
1687
+ var BUILT_IN = {
1688
+ text: TextViewer,
1689
+ code: CodeViewer,
1690
+ svg: SvgViewer,
1691
+ html: HtmlViewer,
1692
+ generic: GenericViewer
1693
+ };
1694
+ function MediaFileViewer({
1695
+ previewKind,
1696
+ ...props
1697
+ }) {
1698
+ const kind = previewKind ?? getPreviewKind(props.fileName, props.mimeType ?? null);
1699
+ const registered = getMediaViewer(kind);
1700
+ if (registered) return /* @__PURE__ */ jsx12(Fragment2, { children: registered(props) });
1701
+ const builtIn = BUILT_IN[kind];
1702
+ if (builtIn) return /* @__PURE__ */ jsx12(Fragment2, { children: builtIn(props) });
1703
+ return /* @__PURE__ */ jsx12(MediaViewerUnavailable, { ...props, kind });
1704
+ }
1705
+ export {
1706
+ CodeViewer,
1707
+ GenericViewer,
1708
+ HtmlViewer,
1709
+ MediaFileViewer,
1710
+ MediaViewerUnavailable,
1711
+ SvgViewer,
1712
+ TRANSPARENCY_GRID_STYLE,
1713
+ TextViewer,
1714
+ ViewerCopyButton,
1715
+ ViewerError,
1716
+ ViewerSkeleton,
1717
+ ViewerToolbar,
1718
+ detectCodeLanguage,
1719
+ getMediaCodeHighlighter,
1720
+ getMediaViewer,
1721
+ getPreviewKind,
1722
+ registerMediaCodeHighlighter,
1723
+ registerMediaViewer,
1724
+ registeredMediaViewerKinds,
1725
+ useViewerBytes,
1726
+ useViewerDisplayUrl,
1727
+ useViewerText
1728
+ };
1729
+ //# sourceMappingURL=viewers.js.map