@ai-matrx/media 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/viewers.cjs +22 -117
- package/dist/viewers.cjs.map +1 -1
- package/dist/viewers.js +23 -118
- package/dist/viewers.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @ai-matrx/media
|
|
2
2
|
|
|
3
|
+
## 0.4.4 — 2026-08-31
|
|
4
|
+
|
|
5
|
+
### Consumer action (C28)
|
|
6
|
+
|
|
7
|
+
- Upgrade. No API change; SVG uploads stored with a generic XML MIME now render
|
|
8
|
+
as images while retaining their source view.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- The SVG viewer now creates and lifecycle-revokes a locally retyped
|
|
13
|
+
`image/svg+xml` object URL when authenticated SVG bytes arrive as
|
|
14
|
+
`application/xml` or another non-SVG MIME. Browsers no longer reject valid
|
|
15
|
+
uploaded SVGs because storage preserved a generic XML content type.
|
|
16
|
+
|
|
17
|
+
## 0.4.3 — 2026-08-31
|
|
18
|
+
|
|
19
|
+
### Consumer action (C28)
|
|
20
|
+
|
|
21
|
+
- Upgrade. No API change; private SVG refs now render from the client's
|
|
22
|
+
authenticated blob lifecycle instead of binding a bearer-less download URL
|
|
23
|
+
directly to an image element.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- The built-in SVG viewer now resolves refs through `MediaClient.getBlob()`
|
|
28
|
+
and renders the lifecycle-owned object URL. This makes personal/private SVG
|
|
29
|
+
files render in ordinary image elements while retaining the existing source
|
|
30
|
+
view and avoiding any host-side viewer fork.
|
|
31
|
+
- Blob sources remain client-free, and real byte-fetch errors are shown in the
|
|
32
|
+
viewer instead of being collapsed into a generic render failure.
|
|
33
|
+
|
|
3
34
|
## 0.4.2 — 2026-08-31
|
|
4
35
|
|
|
5
36
|
### Consumer action (C28)
|
package/dist/viewers.cjs
CHANGED
|
@@ -1433,105 +1433,9 @@ function HtmlSourceView({ source, maxBytes = 1024 * 1024 }) {
|
|
|
1433
1433
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("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 });
|
|
1434
1434
|
}
|
|
1435
1435
|
|
|
1436
|
-
// src/
|
|
1436
|
+
// src/viewers/svg-viewer.tsx
|
|
1437
1437
|
var import_react11 = require("react");
|
|
1438
1438
|
|
|
1439
|
-
// src/types.ts
|
|
1440
|
-
function mintDurableSrc(url) {
|
|
1441
|
-
return url;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// src/core/use-media-load-recovery.ts
|
|
1445
|
-
function normalizeOutcome(outcome) {
|
|
1446
|
-
return typeof outcome === "string" ? { verdict: outcome } : outcome;
|
|
1447
|
-
}
|
|
1448
|
-
var EPHEMERAL_SECRET_IDENTITY = "server:files-session-secret";
|
|
1449
|
-
function useMediaLoadRecovery(src, options) {
|
|
1450
|
-
const client = useMediaClient();
|
|
1451
|
-
const capture = useMediaFailureCapture();
|
|
1452
|
-
const recoverable = options?.recoverable !== false;
|
|
1453
|
-
const onTerminal = options?.onTerminal;
|
|
1454
|
-
const failureRef = options?.failureRef;
|
|
1455
|
-
const [result, setResult] = (0, import_react11.useState)({ source: src, retryKey: 0, failed: false });
|
|
1456
|
-
const attemptRef = (0, import_react11.useRef)(0);
|
|
1457
|
-
const recoveryInFlightRef = (0, import_react11.useRef)(false);
|
|
1458
|
-
(0, import_react11.useEffect)(() => {
|
|
1459
|
-
attemptRef.current = 0;
|
|
1460
|
-
recoveryInFlightRef.current = false;
|
|
1461
|
-
}, [src]);
|
|
1462
|
-
(0, import_react11.useLayoutEffect)(() => {
|
|
1463
|
-
recoveryInFlightRef.current = false;
|
|
1464
|
-
}, [result.retryKey]);
|
|
1465
|
-
const identity = () => failureMediaIdentity(failureRef ?? null, src);
|
|
1466
|
-
const onLoadError = (event) => {
|
|
1467
|
-
if (recoveryInFlightRef.current) return;
|
|
1468
|
-
if (!src || !recoverable) {
|
|
1469
|
-
setResult({ source: src, retryKey: 0, failed: true });
|
|
1470
|
-
if (src) {
|
|
1471
|
-
capture({
|
|
1472
|
-
phase: "element-load",
|
|
1473
|
-
mediaRef: identity(),
|
|
1474
|
-
terminal: true,
|
|
1475
|
-
retryOutcome: "not-attempted",
|
|
1476
|
-
message: "media element failed to load and the source is unrecoverable (foreign/object URL \u2014 a session refresh cannot fix it)"
|
|
1477
|
-
});
|
|
1478
|
-
}
|
|
1479
|
-
onTerminal?.(event);
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
recoveryInFlightRef.current = true;
|
|
1483
|
-
attemptRef.current += 1;
|
|
1484
|
-
const attempt = attemptRef.current;
|
|
1485
|
-
client.recoverLoadError(mintDurableSrc(src), attempt).then(
|
|
1486
|
-
(outcome) => {
|
|
1487
|
-
const report = normalizeOutcome(outcome);
|
|
1488
|
-
if (report.secretSource === "ephemeral") {
|
|
1489
|
-
capture({
|
|
1490
|
-
phase: "session",
|
|
1491
|
-
mediaRef: EPHEMERAL_SECRET_IDENTITY,
|
|
1492
|
-
terminal: false,
|
|
1493
|
-
message: "server cookie secret is ephemeral \u2014 private renders will break across restarts/instances (set a persistent files-session secret on the server)"
|
|
1494
|
-
});
|
|
1495
|
-
}
|
|
1496
|
-
if (report.verdict === "retry") {
|
|
1497
|
-
setResult((current) => ({
|
|
1498
|
-
source: src,
|
|
1499
|
-
retryKey: current.source === src ? current.retryKey + 1 : 1,
|
|
1500
|
-
failed: false
|
|
1501
|
-
}));
|
|
1502
|
-
} else {
|
|
1503
|
-
setResult({ source: src, retryKey: 0, failed: true });
|
|
1504
|
-
capture({
|
|
1505
|
-
phase: attempt > 1 ? "element-load" : "recovery",
|
|
1506
|
-
mediaRef: identity(),
|
|
1507
|
-
terminal: true,
|
|
1508
|
-
retryOutcome: attempt > 1 ? "retried" : "not-attempted",
|
|
1509
|
-
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)",
|
|
1510
|
-
...report.status !== void 0 ? { status: report.status } : {}
|
|
1511
|
-
});
|
|
1512
|
-
onTerminal?.(event);
|
|
1513
|
-
}
|
|
1514
|
-
},
|
|
1515
|
-
(err) => {
|
|
1516
|
-
setResult({ source: src, retryKey: 0, failed: true });
|
|
1517
|
-
capture({
|
|
1518
|
-
phase: "session",
|
|
1519
|
-
mediaRef: identity(),
|
|
1520
|
-
terminal: true,
|
|
1521
|
-
retryOutcome: "recovery-failed",
|
|
1522
|
-
message: `session recovery failed: ${err instanceof Error ? err.message : String(err ?? "unknown error")}`
|
|
1523
|
-
});
|
|
1524
|
-
onTerminal?.(event);
|
|
1525
|
-
}
|
|
1526
|
-
);
|
|
1527
|
-
};
|
|
1528
|
-
return {
|
|
1529
|
-
retryKey: result.source === src ? result.retryKey : 0,
|
|
1530
|
-
onLoadError,
|
|
1531
|
-
failed: result.source === src ? result.failed : false
|
|
1532
|
-
};
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
1439
|
// src/viewers/transparency-grid.ts
|
|
1536
1440
|
var TRANSPARENCY_GRID_STYLE = {
|
|
1537
1441
|
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%)",
|
|
@@ -1566,30 +1470,33 @@ function SvgViewer(props) {
|
|
|
1566
1470
|
);
|
|
1567
1471
|
}
|
|
1568
1472
|
function SvgRenderedView({ source, fileName }) {
|
|
1569
|
-
const
|
|
1570
|
-
const
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1473
|
+
const fromClient = useMediaBlob(source.kind === "ref" ? source.ref : null);
|
|
1474
|
+
const [normalizedUrl, setNormalizedUrl] = (0, import_react11.useState)(null);
|
|
1475
|
+
(0, import_react11.useEffect)(() => {
|
|
1476
|
+
const blob = fromClient.blob;
|
|
1477
|
+
if (!blob || blob.type === "image/svg+xml") {
|
|
1478
|
+
setNormalizedUrl(null);
|
|
1479
|
+
return void 0;
|
|
1480
|
+
}
|
|
1481
|
+
const normalized = new Blob([blob], { type: "image/svg+xml" });
|
|
1482
|
+
const objectUrl = URL.createObjectURL(normalized);
|
|
1483
|
+
setNormalizedUrl(objectUrl);
|
|
1484
|
+
return () => URL.revokeObjectURL(objectUrl);
|
|
1485
|
+
}, [fromClient.blob]);
|
|
1486
|
+
const url = source.kind === "blob" ? source.url : normalizedUrl ?? fromClient.url;
|
|
1487
|
+
const loading = source.kind === "ref" && fromClient.loading;
|
|
1488
|
+
const error = source.kind === "ref" ? fromClient.error : null;
|
|
1489
|
+
if (error) {
|
|
1585
1490
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1586
1491
|
ViewerError,
|
|
1587
1492
|
{
|
|
1588
1493
|
title: "SVG could not be rendered",
|
|
1494
|
+
message: error,
|
|
1589
1495
|
hint: "Switch to the Source view to inspect the markup."
|
|
1590
1496
|
}
|
|
1591
1497
|
);
|
|
1592
1498
|
}
|
|
1499
|
+
if (loading || !url) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ViewerSkeleton, { className: "flex-1" });
|
|
1593
1500
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1594
1501
|
"div",
|
|
1595
1502
|
{
|
|
@@ -1600,10 +1507,8 @@ function SvgRenderedView({ source, fileName }) {
|
|
|
1600
1507
|
{
|
|
1601
1508
|
src: url,
|
|
1602
1509
|
alt: fileName,
|
|
1603
|
-
className: "max-h-full max-w-full object-contain drop-shadow-sm"
|
|
1604
|
-
|
|
1605
|
-
},
|
|
1606
|
-
retryKey
|
|
1510
|
+
className: "max-h-full max-w-full object-contain drop-shadow-sm"
|
|
1511
|
+
}
|
|
1607
1512
|
)
|
|
1608
1513
|
}
|
|
1609
1514
|
);
|