@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.
@@ -1,69 +1,74 @@
1
- import { useRef as c, useState as h, useCallback as R, useEffect as x } from "react";
2
- function A(t) {
3
- if (typeof t == "string")
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(t, "http://localhost").pathname.split("/").pop();
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 t instanceof File ? t.name : "PDF Preview";
10
+ return r instanceof File ? r.name : "PDF Preview";
10
11
  }
11
- function W(t) {
12
- const { src: u, open: n, onOpenChange: a, onError: L, onLoad: P } = t, i = c(u);
13
- i.current = u;
14
- const f = c(P);
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 = c(L);
17
- d.current = L;
18
- const [k, w] = h(!1), [O, p] = h("idle"), [S, b] = h(null), [j, g] = h(null), v = c(null), m = c(null), y = n !== void 0, C = y ? n : k, o = R(() => {
19
- v.current && (URL.revokeObjectURL(v.current), v.current = null);
20
- }, []), U = R(async () => {
21
- var l, E, F;
22
- (l = m.current) == null || l.abort();
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
- m.current = e, p("loading"), g(null), b(null), o();
27
+ v.current = e, p("loading"), h(null), m(null), o();
25
28
  try {
26
- let r;
27
- if (typeof i.current == "function" ? r = await i.current(e.signal) : r = i.current, e.signal.aborted) return;
28
- if (typeof r == "string")
29
- b(r);
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 = URL.createObjectURL(r);
32
- v.current = s, b(s);
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 (r) {
40
+ } catch (t) {
36
41
  if (e.signal.aborted) return;
37
42
  const s = {
38
- message: r instanceof Error ? r.message : "Failed to load PDF",
39
- cause: r
43
+ message: t instanceof Error ? t.message : "Failed to load PDF",
44
+ cause: t
40
45
  };
41
- g(s), p("error"), (F = d.current) == null || F.call(d, s);
46
+ h(s), p("error"), (j = d.current) == null || j.call(d, s);
42
47
  }
43
- }, [o]), D = R(
48
+ }, [o]), I = g(
44
49
  (e) => {
45
- var l;
46
- y || w(e), a == null || a(e), e || ((l = m.current) == null || l.abort(), p("idle"), b(null), g(null), o());
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, a, o]
53
+ [y, i, o]
49
54
  );
50
- x(() => (C && U(), () => {
55
+ q(() => (F && R(), () => {
51
56
  var e;
52
- (e = m.current) == null || e.abort(), o();
53
- }), [C, U, o]);
54
- const I = R(() => {
55
- U();
56
- }, [U]), T = A(u);
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: C,
59
- handleOpenChange: D,
60
- status: O,
61
- resolvedUrl: S,
62
- error: j,
63
- retry: I,
64
- resolvedTitle: T
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
- W as usePdfPreview
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 v from "react";
3
- import { cn as j } from "../../lib/utils.js";
4
- import { Z2Dialog as L, Z2DialogTrigger as O, Z2DialogContent as U, Z2DialogTitle as W } from "../dialog/dialog.js";
5
- import { PDF_PREVIEW_DEFAULTS as m, pdfDialogContentVariants as z } from "./pdf-preview.const.js";
6
- import { usePdfPreview as A } from "./pdf-preview.hook.js";
7
- import { PdfToolbar as F } from "./components/pdf-toolbar.js";
8
- import { PdfViewer as I } from "./components/pdf-viewer.js";
9
- import { PdfLoadingState as $ } from "./components/pdf-loading-state.js";
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: y,
13
- title: w,
14
- size: D = m.size,
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: c = m.showToolbar,
19
- showDownload: T = m.showDownload,
20
- downloadFilename: u,
21
- onDownload: l,
18
+ showToolbar: g = c.showToolbar,
19
+ showDownload: T = c.showDownload,
20
+ downloadFilename: f,
21
+ onDownload: d,
22
22
  loadingContent: b,
23
- errorContent: n,
24
- renderPdf: g,
23
+ errorContent: i,
24
+ renderPdf: h,
25
25
  className: N,
26
26
  viewerClassName: Z,
27
27
  onError: k,
28
28
  onLoad: x,
29
- children: h
29
+ children: v
30
30
  }) => {
31
- const { open: f, handleOpenChange: d, status: a, resolvedUrl: e, error: s, retry: R, resolvedTitle: S } = A({
32
- src: y,
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
- }), i = w || S, V = v.useCallback(() => {
38
- if (!e)
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
- l && l();
41
- const r = document.createElement("a");
42
- r.href = e;
43
- const p = u || i || "document.pdf";
44
- r.download = p.endsWith(".pdf") ? p : `${p}.pdf`, r.target = "_blank", r.rel = "noopener noreferrer", document.body.appendChild(r), r.click(), document.body.removeChild(r);
45
- }, [l, e, u, i]), _ = () => a === "idle" || a === "loading" ? b || /* @__PURE__ */ o($, {}) : a === "error" && s ? n ? typeof n == "function" ? n(s) : n : /* @__PURE__ */ o(q, { error: s, onRetry: R }) : a === "ready" && e ? g ? g(e) : /* @__PURE__ */ o(I, { url: e, title: i, className: Z }) : null;
46
- return v.useEffect(() => {
47
- t && f && d(!1);
48
- }, [t, f, d]), // Z2Dialog wraps Radix Dialog — manages focus trapping, escape key, and overlay click-to-close
49
- /* @__PURE__ */ P(L, { open: t ? !1 : f, onOpenChange: t ? void 0 : d, children: [
50
- h && /* @__PURE__ */ o(O, { asChild: !0, disabled: t, "data-slot": "pdf-preview-trigger", children: h }),
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
- U,
57
+ O,
53
58
  {
54
59
  "data-slot": "pdf-preview-content",
55
- className: j(z({ size: D }), N),
60
+ className: _($({ size: D }), N),
56
61
  "aria-describedby": void 0,
57
62
  children: [
58
- c && /* @__PURE__ */ o(
59
- F,
63
+ g && /* @__PURE__ */ o(
64
+ A,
60
65
  {
61
- title: i,
66
+ title: n,
62
67
  showDownload: T,
63
68
  onDownload: V,
64
- downloadDisabled: !e
69
+ downloadDisabled: !r
65
70
  }
66
71
  ),
67
- !c && /* @__PURE__ */ o(W, { className: "sr-only", children: i }),
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ztwoint/z-ui",
3
- "version": "0.1.148",
3
+ "version": "0.1.151",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",