@ztwoint/z-ui 0.1.148 → 0.1.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/pdf-preview/pdf-preview.hook.js +50 -45
- package/dist/components/pdf-preview/pdf-preview.js +47 -42
- package/dist/components/pdf-preview/pdf-preview.type.d.ts +3 -0
- package/dist/components/pdf-preview/pdf-title-injector.d.ts +48 -0
- package/dist/components/pdf-preview/pdf-title-injector.js +39 -0
- package/dist/types/components/pdf-preview/pdf-preview.type.d.ts +3 -0
- package/dist/types/components/pdf-preview/pdf-title-injector.d.ts +48 -0
- package/package.json +1 -1
|
@@ -1,69 +1,74 @@
|
|
|
1
|
-
import { useRef as
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { useRef as l, useState as U, useCallback as g, useEffect as q } from "react";
|
|
2
|
+
import { injectPdfTitle as z } from "./pdf-title-injector.js";
|
|
3
|
+
function G(r) {
|
|
4
|
+
if (typeof r == "string")
|
|
4
5
|
try {
|
|
5
|
-
const n = new URL(
|
|
6
|
+
const n = new URL(r, "http://localhost").pathname.split("/").pop();
|
|
6
7
|
if (n && n.endsWith(".pdf")) return decodeURIComponent(n);
|
|
7
8
|
} catch {
|
|
8
9
|
}
|
|
9
|
-
return
|
|
10
|
+
return r instanceof File ? r.name : "PDF Preview";
|
|
10
11
|
}
|
|
11
|
-
function
|
|
12
|
-
const { src:
|
|
13
|
-
|
|
14
|
-
const f =
|
|
12
|
+
function K(r) {
|
|
13
|
+
const { src: a, open: n, onOpenChange: i, onError: w, onLoad: P, downloadFilename: C } = r, u = l(a);
|
|
14
|
+
u.current = a;
|
|
15
|
+
const f = l(P);
|
|
15
16
|
f.current = P;
|
|
16
|
-
const d =
|
|
17
|
-
d.current =
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
const d = l(w);
|
|
18
|
+
d.current = w;
|
|
19
|
+
const L = l(C);
|
|
20
|
+
L.current = C;
|
|
21
|
+
const [O, S] = U(!1), [T, p] = U("idle"), [B, m] = U(null), [D, h] = U(null), b = l(null), v = l(null), y = n !== void 0, F = y ? n : O, o = g(() => {
|
|
22
|
+
b.current && (URL.revokeObjectURL(b.current), b.current = null);
|
|
23
|
+
}, []), R = g(async () => {
|
|
24
|
+
var c, E, j;
|
|
25
|
+
(c = v.current) == null || c.abort();
|
|
23
26
|
const e = new AbortController();
|
|
24
|
-
|
|
27
|
+
v.current = e, p("loading"), h(null), m(null), o();
|
|
25
28
|
try {
|
|
26
|
-
let
|
|
27
|
-
if (typeof
|
|
28
|
-
if (typeof
|
|
29
|
-
|
|
29
|
+
let t;
|
|
30
|
+
if (typeof u.current == "function" ? t = await u.current(e.signal) : t = u.current, e.signal.aborted) return;
|
|
31
|
+
if (typeof t == "string")
|
|
32
|
+
m(t);
|
|
30
33
|
else {
|
|
31
|
-
const s =
|
|
32
|
-
|
|
34
|
+
const s = L.current || (t instanceof File ? t.name : null), W = s ? await z(t, s) : t;
|
|
35
|
+
if (e.signal.aborted) return;
|
|
36
|
+
const k = URL.createObjectURL(W);
|
|
37
|
+
b.current = k, m(k);
|
|
33
38
|
}
|
|
34
39
|
p("ready"), (E = f.current) == null || E.call(f);
|
|
35
|
-
} catch (
|
|
40
|
+
} catch (t) {
|
|
36
41
|
if (e.signal.aborted) return;
|
|
37
42
|
const s = {
|
|
38
|
-
message:
|
|
39
|
-
cause:
|
|
43
|
+
message: t instanceof Error ? t.message : "Failed to load PDF",
|
|
44
|
+
cause: t
|
|
40
45
|
};
|
|
41
|
-
|
|
46
|
+
h(s), p("error"), (j = d.current) == null || j.call(d, s);
|
|
42
47
|
}
|
|
43
|
-
}, [o]),
|
|
48
|
+
}, [o]), I = g(
|
|
44
49
|
(e) => {
|
|
45
|
-
var
|
|
46
|
-
y ||
|
|
50
|
+
var c;
|
|
51
|
+
y || S(e), i == null || i(e), e || ((c = v.current) == null || c.abort(), p("idle"), m(null), h(null), o());
|
|
47
52
|
},
|
|
48
|
-
[y,
|
|
53
|
+
[y, i, o]
|
|
49
54
|
);
|
|
50
|
-
|
|
55
|
+
q(() => (F && R(), () => {
|
|
51
56
|
var e;
|
|
52
|
-
(e =
|
|
53
|
-
}), [
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
}, [
|
|
57
|
+
(e = v.current) == null || e.abort(), o();
|
|
58
|
+
}), [F, R, o]);
|
|
59
|
+
const x = g(() => {
|
|
60
|
+
R();
|
|
61
|
+
}, [R]), A = G(a);
|
|
57
62
|
return {
|
|
58
|
-
open:
|
|
59
|
-
handleOpenChange:
|
|
60
|
-
status:
|
|
61
|
-
resolvedUrl:
|
|
62
|
-
error:
|
|
63
|
-
retry:
|
|
64
|
-
resolvedTitle:
|
|
63
|
+
open: F,
|
|
64
|
+
handleOpenChange: I,
|
|
65
|
+
status: T,
|
|
66
|
+
resolvedUrl: B,
|
|
67
|
+
error: D,
|
|
68
|
+
retry: x,
|
|
69
|
+
resolvedTitle: A
|
|
65
70
|
};
|
|
66
71
|
}
|
|
67
72
|
export {
|
|
68
|
-
|
|
73
|
+
K as usePdfPreview
|
|
69
74
|
};
|
|
@@ -1,71 +1,76 @@
|
|
|
1
1
|
import { jsxs as P, jsx as o } from "react/jsx-runtime";
|
|
2
|
-
import * as
|
|
3
|
-
import { cn as
|
|
4
|
-
import { Z2Dialog as
|
|
5
|
-
import { PDF_PREVIEW_DEFAULTS as
|
|
6
|
-
import { usePdfPreview as
|
|
7
|
-
import { PdfToolbar as
|
|
8
|
-
import { PdfViewer as
|
|
9
|
-
import { PdfLoadingState as
|
|
2
|
+
import * as y from "react";
|
|
3
|
+
import { cn as _ } from "../../lib/utils.js";
|
|
4
|
+
import { Z2Dialog as j, Z2DialogTrigger as L, Z2DialogContent as O, Z2DialogTitle as U } from "../dialog/dialog.js";
|
|
5
|
+
import { PDF_PREVIEW_DEFAULTS as c, pdfDialogContentVariants as $ } from "./pdf-preview.const.js";
|
|
6
|
+
import { usePdfPreview as z } from "./pdf-preview.hook.js";
|
|
7
|
+
import { PdfToolbar as A } from "./components/pdf-toolbar.js";
|
|
8
|
+
import { PdfViewer as F } from "./components/pdf-viewer.js";
|
|
9
|
+
import { PdfLoadingState as I } from "./components/pdf-loading-state.js";
|
|
10
10
|
import { PdfErrorState as q } from "./components/pdf-error-state.js";
|
|
11
11
|
const B = ({
|
|
12
|
-
src:
|
|
13
|
-
title:
|
|
14
|
-
size: D =
|
|
12
|
+
src: w,
|
|
13
|
+
title: u,
|
|
14
|
+
size: D = c.size,
|
|
15
15
|
open: C,
|
|
16
16
|
onOpenChange: E,
|
|
17
17
|
disabled: t = !1,
|
|
18
|
-
showToolbar:
|
|
19
|
-
showDownload: T =
|
|
20
|
-
downloadFilename:
|
|
21
|
-
onDownload:
|
|
18
|
+
showToolbar: g = c.showToolbar,
|
|
19
|
+
showDownload: T = c.showDownload,
|
|
20
|
+
downloadFilename: f,
|
|
21
|
+
onDownload: d,
|
|
22
22
|
loadingContent: b,
|
|
23
|
-
errorContent:
|
|
24
|
-
renderPdf:
|
|
23
|
+
errorContent: i,
|
|
24
|
+
renderPdf: h,
|
|
25
25
|
className: N,
|
|
26
26
|
viewerClassName: Z,
|
|
27
27
|
onError: k,
|
|
28
28
|
onLoad: x,
|
|
29
|
-
children:
|
|
29
|
+
children: v
|
|
30
30
|
}) => {
|
|
31
|
-
const { open:
|
|
32
|
-
src:
|
|
31
|
+
const { open: l, handleOpenChange: s, status: a, resolvedUrl: r, error: p, retry: R, resolvedTitle: S } = z({
|
|
32
|
+
src: w,
|
|
33
33
|
open: C,
|
|
34
34
|
onOpenChange: E,
|
|
35
35
|
onError: k,
|
|
36
|
-
onLoad: x
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
onLoad: x,
|
|
37
|
+
downloadFilename: (() => {
|
|
38
|
+
const e = f || u;
|
|
39
|
+
if (e)
|
|
40
|
+
return e.endsWith(".pdf") ? e : `${e}.pdf`;
|
|
41
|
+
})()
|
|
42
|
+
}), n = u || S, V = y.useCallback(() => {
|
|
43
|
+
if (!r)
|
|
39
44
|
return;
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
}, [
|
|
46
|
-
return
|
|
47
|
-
t &&
|
|
48
|
-
}, [t,
|
|
49
|
-
/* @__PURE__ */ P(
|
|
50
|
-
|
|
45
|
+
d && d();
|
|
46
|
+
const e = document.createElement("a");
|
|
47
|
+
e.href = r;
|
|
48
|
+
const m = f || n || "document.pdf";
|
|
49
|
+
e.download = m.endsWith(".pdf") ? m : `${m}.pdf`, e.target = "_blank", e.rel = "noopener noreferrer", document.body.appendChild(e), e.click(), document.body.removeChild(e);
|
|
50
|
+
}, [d, r, f, n]), W = () => a === "idle" || a === "loading" ? b || /* @__PURE__ */ o(I, {}) : a === "error" && p ? i ? typeof i == "function" ? i(p) : i : /* @__PURE__ */ o(q, { error: p, onRetry: R }) : a === "ready" && r ? h ? h(r) : /* @__PURE__ */ o(F, { url: r, title: n, className: Z }) : null;
|
|
51
|
+
return y.useEffect(() => {
|
|
52
|
+
t && l && s(!1);
|
|
53
|
+
}, [t, l, s]), // Z2Dialog wraps Radix Dialog — manages focus trapping, escape key, and overlay click-to-close
|
|
54
|
+
/* @__PURE__ */ P(j, { open: t ? !1 : l, onOpenChange: t ? void 0 : s, children: [
|
|
55
|
+
v && /* @__PURE__ */ o(L, { asChild: !0, disabled: t, "data-slot": "pdf-preview-trigger", children: v }),
|
|
51
56
|
/* @__PURE__ */ P(
|
|
52
|
-
|
|
57
|
+
O,
|
|
53
58
|
{
|
|
54
59
|
"data-slot": "pdf-preview-content",
|
|
55
|
-
className:
|
|
60
|
+
className: _($({ size: D }), N),
|
|
56
61
|
"aria-describedby": void 0,
|
|
57
62
|
children: [
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
g && /* @__PURE__ */ o(
|
|
64
|
+
A,
|
|
60
65
|
{
|
|
61
|
-
title:
|
|
66
|
+
title: n,
|
|
62
67
|
showDownload: T,
|
|
63
68
|
onDownload: V,
|
|
64
|
-
downloadDisabled: !
|
|
69
|
+
downloadDisabled: !r
|
|
65
70
|
}
|
|
66
71
|
),
|
|
67
|
-
!
|
|
68
|
-
|
|
72
|
+
!g && /* @__PURE__ */ o(U, { className: "sr-only", children: n }),
|
|
73
|
+
W()
|
|
69
74
|
]
|
|
70
75
|
}
|
|
71
76
|
)
|
|
@@ -69,6 +69,9 @@ export interface UsePdfPreviewProps {
|
|
|
69
69
|
onOpenChange?: (open: boolean) => void;
|
|
70
70
|
onError?: (error: PdfPreviewError) => void;
|
|
71
71
|
onLoad?: () => void;
|
|
72
|
+
/** Filename appended as a URL fragment to blob URLs so the browser's
|
|
73
|
+
* built-in PDF viewer uses it for "Save As" instead of the blob UUID. */
|
|
74
|
+
downloadFilename?: string;
|
|
72
75
|
}
|
|
73
76
|
/** Return value of usePdfPreview — provides state and handlers to the main component. */
|
|
74
77
|
export interface UsePdfPreviewReturn {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injects a /Title into a PDF's metadata via an incremental update.
|
|
3
|
+
*
|
|
4
|
+
* ## Problem
|
|
5
|
+
* When displaying a PDF via a blob URL (`blob:http://.../<uuid>`), Chrome's
|
|
6
|
+
* built-in PDF viewer (PDFium) shows the blob UUID as the document name in
|
|
7
|
+
* the toolbar — e.g. "f48f9554-646a-4...". There is no standard web API to
|
|
8
|
+
* control this; the File constructor name, URL fragments (#filename=), and
|
|
9
|
+
* Content-Disposition headers are all ignored for blob URLs.
|
|
10
|
+
*
|
|
11
|
+
* ## Solution
|
|
12
|
+
* Chrome's PDF viewer reads the `/Title` field from the PDF's Info dictionary
|
|
13
|
+
* and displays it in the toolbar when present. This function appends a new
|
|
14
|
+
* Info dictionary object containing the desired title using a PDF incremental
|
|
15
|
+
* update — the original content is never modified, only ~200 bytes are
|
|
16
|
+
* appended at the end. This keeps it fast even for large PDFs (50MB+ with
|
|
17
|
+
* heavy images), since we never re-parse or re-serialize the PDF body.
|
|
18
|
+
*
|
|
19
|
+
* ## What it covers
|
|
20
|
+
* - Chrome toolbar title: Shows the injected /Title instead of the UUID
|
|
21
|
+
* - Download button: Handled separately by the component via <a download="filename">
|
|
22
|
+
*
|
|
23
|
+
* ## What it does NOT cover
|
|
24
|
+
* - Ctrl+S "Save As" filename: Chrome still derives this from the blob URL,
|
|
25
|
+
* not the PDF metadata. Only a Service Worker + Cache API approach can fix
|
|
26
|
+
* this (see https://stackoverflow.com/q/53548182), which is too complex
|
|
27
|
+
* for this use case.
|
|
28
|
+
*
|
|
29
|
+
* ## Edge cases (falls back silently to original blob — UUID shown)
|
|
30
|
+
* - Password-protected / encrypted PDFs: The appended Info dictionary is
|
|
31
|
+
* unencrypted, which Chrome may ignore. For full encrypted PDF support,
|
|
32
|
+
* use `pdf-lib` (lazy-loaded via `await import('pdf-lib')`) which
|
|
33
|
+
* understands PDF encryption and can set metadata correctly.
|
|
34
|
+
* - PDFs with cross-reference streams (PDF 1.5+): Works if `/Size` appears
|
|
35
|
+
* in plain text within the xref stream object dictionary (most common).
|
|
36
|
+
* May fail on heavily compressed xref streams where `/Size` is not
|
|
37
|
+
* visible as plain text.
|
|
38
|
+
* - Malformed or non-PDF blobs: Regex finds no `startxref` → returns
|
|
39
|
+
* original blob unchanged.
|
|
40
|
+
*
|
|
41
|
+
* ## Performance
|
|
42
|
+
* - Reads the blob once as ArrayBuffer (already in memory from the fetch)
|
|
43
|
+
* - Decodes to string for two regex searches (`/Size`, `startxref`)
|
|
44
|
+
* - Appends ~200 bytes (new object + xref + trailer)
|
|
45
|
+
* - No re-parsing, no re-serialization of the PDF body
|
|
46
|
+
* - Significantly lighter than pdf-lib which parses + rewrites everything
|
|
47
|
+
*/
|
|
48
|
+
export declare function injectPdfTitle(blob: Blob, title: string): Promise<Blob>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
async function O(t, r) {
|
|
2
|
+
try {
|
|
3
|
+
const n = await t.arrayBuffer(), e = new Uint8Array(n), s = new TextDecoder("latin1").decode(e), i = s.lastIndexOf("startxref");
|
|
4
|
+
if (i === -1) return t;
|
|
5
|
+
const y = s.substring(i + 9).trim(), f = parseInt(y, 10);
|
|
6
|
+
if (isNaN(f)) return t;
|
|
7
|
+
const o = s.match(/\/Size\s+(\d+)/g);
|
|
8
|
+
if (!o) return t;
|
|
9
|
+
const d = o[o.length - 1].match(/\d+/);
|
|
10
|
+
if (!d) return t;
|
|
11
|
+
const l = parseInt(d[0], 10), c = l, w = l + 1, $ = F(r), p = e[e.length - 1], x = p !== 10 && p !== 13 ? `
|
|
12
|
+
` : "", S = new TextEncoder(), j = S.encode(x), h = e.length + j.length, g = `${c} 0 obj
|
|
13
|
+
<< /Title ${$} >>
|
|
14
|
+
endobj
|
|
15
|
+
`, z = S.encode(g), B = h + z.length, I = String(h).padStart(10, "0"), T = `xref
|
|
16
|
+
${c} 1
|
|
17
|
+
${I} 00000 n\r
|
|
18
|
+
`, m = `trailer
|
|
19
|
+
<< /Size ${w} /Info ${c} 0 R /Prev ${f} >>
|
|
20
|
+
startxref
|
|
21
|
+
${B}
|
|
22
|
+
%%EOF
|
|
23
|
+
`, E = x + g + T + m, u = new TextEncoder().encode(E), a = new Uint8Array(e.length + u.length);
|
|
24
|
+
return a.set(e), a.set(u, e.length), new Blob([a], { type: "application/pdf" });
|
|
25
|
+
} catch {
|
|
26
|
+
return t;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function F(t) {
|
|
30
|
+
let r = "FEFF";
|
|
31
|
+
for (let n = 0; n < t.length; n++) {
|
|
32
|
+
const e = t.charCodeAt(n);
|
|
33
|
+
r += e.toString(16).padStart(4, "0").toUpperCase();
|
|
34
|
+
}
|
|
35
|
+
return `<${r}>`;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
O as injectPdfTitle
|
|
39
|
+
};
|
|
@@ -69,6 +69,9 @@ export interface UsePdfPreviewProps {
|
|
|
69
69
|
onOpenChange?: (open: boolean) => void;
|
|
70
70
|
onError?: (error: PdfPreviewError) => void;
|
|
71
71
|
onLoad?: () => void;
|
|
72
|
+
/** Filename appended as a URL fragment to blob URLs so the browser's
|
|
73
|
+
* built-in PDF viewer uses it for "Save As" instead of the blob UUID. */
|
|
74
|
+
downloadFilename?: string;
|
|
72
75
|
}
|
|
73
76
|
/** Return value of usePdfPreview — provides state and handlers to the main component. */
|
|
74
77
|
export interface UsePdfPreviewReturn {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injects a /Title into a PDF's metadata via an incremental update.
|
|
3
|
+
*
|
|
4
|
+
* ## Problem
|
|
5
|
+
* When displaying a PDF via a blob URL (`blob:http://.../<uuid>`), Chrome's
|
|
6
|
+
* built-in PDF viewer (PDFium) shows the blob UUID as the document name in
|
|
7
|
+
* the toolbar — e.g. "f48f9554-646a-4...". There is no standard web API to
|
|
8
|
+
* control this; the File constructor name, URL fragments (#filename=), and
|
|
9
|
+
* Content-Disposition headers are all ignored for blob URLs.
|
|
10
|
+
*
|
|
11
|
+
* ## Solution
|
|
12
|
+
* Chrome's PDF viewer reads the `/Title` field from the PDF's Info dictionary
|
|
13
|
+
* and displays it in the toolbar when present. This function appends a new
|
|
14
|
+
* Info dictionary object containing the desired title using a PDF incremental
|
|
15
|
+
* update — the original content is never modified, only ~200 bytes are
|
|
16
|
+
* appended at the end. This keeps it fast even for large PDFs (50MB+ with
|
|
17
|
+
* heavy images), since we never re-parse or re-serialize the PDF body.
|
|
18
|
+
*
|
|
19
|
+
* ## What it covers
|
|
20
|
+
* - Chrome toolbar title: Shows the injected /Title instead of the UUID
|
|
21
|
+
* - Download button: Handled separately by the component via <a download="filename">
|
|
22
|
+
*
|
|
23
|
+
* ## What it does NOT cover
|
|
24
|
+
* - Ctrl+S "Save As" filename: Chrome still derives this from the blob URL,
|
|
25
|
+
* not the PDF metadata. Only a Service Worker + Cache API approach can fix
|
|
26
|
+
* this (see https://stackoverflow.com/q/53548182), which is too complex
|
|
27
|
+
* for this use case.
|
|
28
|
+
*
|
|
29
|
+
* ## Edge cases (falls back silently to original blob — UUID shown)
|
|
30
|
+
* - Password-protected / encrypted PDFs: The appended Info dictionary is
|
|
31
|
+
* unencrypted, which Chrome may ignore. For full encrypted PDF support,
|
|
32
|
+
* use `pdf-lib` (lazy-loaded via `await import('pdf-lib')`) which
|
|
33
|
+
* understands PDF encryption and can set metadata correctly.
|
|
34
|
+
* - PDFs with cross-reference streams (PDF 1.5+): Works if `/Size` appears
|
|
35
|
+
* in plain text within the xref stream object dictionary (most common).
|
|
36
|
+
* May fail on heavily compressed xref streams where `/Size` is not
|
|
37
|
+
* visible as plain text.
|
|
38
|
+
* - Malformed or non-PDF blobs: Regex finds no `startxref` → returns
|
|
39
|
+
* original blob unchanged.
|
|
40
|
+
*
|
|
41
|
+
* ## Performance
|
|
42
|
+
* - Reads the blob once as ArrayBuffer (already in memory from the fetch)
|
|
43
|
+
* - Decodes to string for two regex searches (`/Size`, `startxref`)
|
|
44
|
+
* - Appends ~200 bytes (new object + xref + trailer)
|
|
45
|
+
* - No re-parsing, no re-serialization of the PDF body
|
|
46
|
+
* - Significantly lighter than pdf-lib which parses + rewrites everything
|
|
47
|
+
*/
|
|
48
|
+
export declare function injectPdfTitle(blob: Blob, title: string): Promise<Blob>;
|