@eka-care/medical-records-ui 1.0.57 → 1.0.59

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.
Files changed (66) hide show
  1. package/dist/{DocumentViewer-5AXKL75U.mjs → DocumentViewer-AL4TAYBP.mjs} +62 -63
  2. package/dist/DocumentViewer-AL4TAYBP.mjs.map +1 -0
  3. package/dist/{DocumentViewer-Y3SQ2BMS.js → DocumentViewer-LN7UYALN.js} +52 -53
  4. package/dist/DocumentViewer-LN7UYALN.js.map +1 -0
  5. package/dist/{OriginalRecordPanel-NGLSSJLE.mjs → OriginalRecordPanel-4MVFXW74.mjs} +44 -37
  6. package/dist/OriginalRecordPanel-4MVFXW74.mjs.map +1 -0
  7. package/dist/{OriginalRecordPanel-DCGRXQST.js → OriginalRecordPanel-DJR23U6B.js} +61 -54
  8. package/dist/OriginalRecordPanel-DJR23U6B.js.map +1 -0
  9. package/dist/{PdfViewer-CG2577E4.mjs → PdfViewer-6A7RUCNH.mjs} +34 -6
  10. package/dist/PdfViewer-6A7RUCNH.mjs.map +1 -0
  11. package/dist/{PdfViewer-GIWRL6FQ.js → PdfViewer-PE3NFPLH.js} +35 -7
  12. package/dist/PdfViewer-PE3NFPLH.js.map +1 -0
  13. package/dist/SmartUploadModal-DUXA6OPB.js +9 -0
  14. package/dist/{SmartUploadModal-RHGZPTB3.js.map → SmartUploadModal-DUXA6OPB.js.map} +1 -1
  15. package/dist/SmartUploadModal-WMVWA6K5.mjs +9 -0
  16. package/dist/VitalHistoryPanel-2UINJHLO.mjs +10 -0
  17. package/dist/VitalHistoryPanel-4FYRSDFN.js +10 -0
  18. package/dist/{VitalHistoryPanel-VQQ27P6Q.js.map → VitalHistoryPanel-4FYRSDFN.js.map} +1 -1
  19. package/dist/chunk-3LGE3VHA.mjs +33 -0
  20. package/dist/chunk-3LGE3VHA.mjs.map +1 -0
  21. package/dist/{chunk-STQYPHI3.js → chunk-BVSVRBJV.js} +9 -9
  22. package/dist/{chunk-STQYPHI3.js.map → chunk-BVSVRBJV.js.map} +1 -1
  23. package/dist/chunk-NUHZPKCQ.js +33 -0
  24. package/dist/chunk-NUHZPKCQ.js.map +1 -0
  25. package/dist/{chunk-3J6XMPXB.mjs → chunk-PF2RQ62E.mjs} +2 -2
  26. package/dist/{chunk-NZNI6YJ5.mjs → chunk-Q2TRUL57.mjs} +8 -1
  27. package/dist/chunk-Q2TRUL57.mjs.map +1 -0
  28. package/dist/{chunk-WXCGQDIL.js → chunk-QVNT2JO4.js} +21 -21
  29. package/dist/{chunk-WXCGQDIL.js.map → chunk-QVNT2JO4.js.map} +1 -1
  30. package/dist/{chunk-QGGLSO4W.js → chunk-RHC62IQZ.js} +9 -2
  31. package/dist/chunk-RHC62IQZ.js.map +1 -0
  32. package/dist/{chunk-5TPILQYM.mjs → chunk-XGE37LLS.mjs} +3 -3
  33. package/dist/{chunk-KTHLLAAG.js → chunk-Y3B4UZRD.js} +14 -14
  34. package/dist/{chunk-KTHLLAAG.js.map → chunk-Y3B4UZRD.js.map} +1 -1
  35. package/dist/{chunk-7AO6M347.mjs → chunk-ZL3UK6HA.mjs} +2 -2
  36. package/dist/index.css +3 -0
  37. package/dist/index.css.map +1 -1
  38. package/dist/index.js +94 -89
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +15 -10
  41. package/dist/index.mjs.map +1 -1
  42. package/dist/styles.css +3 -0
  43. package/dist/styles.css.map +1 -1
  44. package/dist/{widgets-RJVE37HS.mjs → widgets-HWVMNBC2.mjs} +2 -2
  45. package/dist/widgets-XR5AOBBG.js +43 -0
  46. package/dist/{widgets-GSEIOGU4.js.map → widgets-XR5AOBBG.js.map} +1 -1
  47. package/package.json +1 -1
  48. package/dist/DocumentViewer-5AXKL75U.mjs.map +0 -1
  49. package/dist/DocumentViewer-Y3SQ2BMS.js.map +0 -1
  50. package/dist/OriginalRecordPanel-DCGRXQST.js.map +0 -1
  51. package/dist/OriginalRecordPanel-NGLSSJLE.mjs.map +0 -1
  52. package/dist/PdfViewer-CG2577E4.mjs.map +0 -1
  53. package/dist/PdfViewer-GIWRL6FQ.js.map +0 -1
  54. package/dist/SmartUploadModal-5SITEKEJ.mjs +0 -9
  55. package/dist/SmartUploadModal-RHGZPTB3.js +0 -9
  56. package/dist/VitalHistoryPanel-FT5VMJ37.mjs +0 -10
  57. package/dist/VitalHistoryPanel-VQQ27P6Q.js +0 -10
  58. package/dist/chunk-NZNI6YJ5.mjs.map +0 -1
  59. package/dist/chunk-QGGLSO4W.js.map +0 -1
  60. package/dist/widgets-GSEIOGU4.js +0 -43
  61. /package/dist/{SmartUploadModal-5SITEKEJ.mjs.map → SmartUploadModal-WMVWA6K5.mjs.map} +0 -0
  62. /package/dist/{VitalHistoryPanel-FT5VMJ37.mjs.map → VitalHistoryPanel-2UINJHLO.mjs.map} +0 -0
  63. /package/dist/{chunk-3J6XMPXB.mjs.map → chunk-PF2RQ62E.mjs.map} +0 -0
  64. /package/dist/{chunk-5TPILQYM.mjs.map → chunk-XGE37LLS.mjs.map} +0 -0
  65. /package/dist/{chunk-7AO6M347.mjs.map → chunk-ZL3UK6HA.mjs.map} +0 -0
  66. /package/dist/{widgets-RJVE37HS.mjs.map → widgets-HWVMNBC2.mjs.map} +0 -0
@@ -1,5 +1,9 @@
1
+ import {
2
+ useIframeLoadStatus
3
+ } from "./chunk-3LGE3VHA.mjs";
4
+
1
5
  // src/views/RecordPreview/DocumentViewer.tsx
2
- import { lazy, Suspense, useCallback, useEffect as useEffect2, useRef, useState as useState2 } from "react";
6
+ import { lazy, Suspense, useCallback, useEffect as useEffect2, useRef, useState } from "react";
3
7
  import {
4
8
  ChevronLeft,
5
9
  ChevronRight,
@@ -12,64 +16,57 @@ import {
12
16
  } from "lucide-react";
13
17
 
14
18
  // src/views/RecordPreview/TextViewer.tsx
15
- import { useEffect, useState } from "react";
16
- import { jsx } from "react/jsx-runtime";
17
- function TextViewer({ url }) {
18
- const [state, setState] = useState({
19
- status: "loading",
20
- text: ""
21
- });
19
+ import { useEffect } from "react";
20
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
21
+ function TextViewer({ url, onStatusChange }) {
22
+ const { status, onLoad, onError } = useIframeLoadStatus(url);
22
23
  useEffect(() => {
23
- let cancelled = false;
24
- setState({ status: "loading", text: "" });
25
- (async () => {
26
- try {
27
- const res = await fetch(url);
28
- if (!res.ok) throw new Error(`fetch ${res.status}`);
29
- const text = await res.text();
30
- if (text.trimStart().startsWith("%PDF")) throw new Error("binary");
31
- if (!cancelled) setState({ status: "loaded", text });
32
- } catch {
33
- if (!cancelled) setState({ status: "error", text: "" });
34
- }
35
- })();
36
- return () => {
37
- cancelled = true;
38
- };
39
- }, [url]);
40
- if (state.status === "loading") {
41
- return /* @__PURE__ */ jsx("div", { className: "mr-doc-viewer__status", style: { color: "var(--mr-muted-foreground)" }, children: "Loading document..." });
42
- }
43
- if (state.status === "error") {
44
- return /* @__PURE__ */ jsx("div", { className: "mr-doc-viewer__status", style: { color: "var(--mr-muted-foreground)" }, children: "Couldn't load this document." });
45
- }
46
- if (!state.text.trim()) {
47
- return /* @__PURE__ */ jsx("div", { className: "mr-doc-viewer__status", style: { color: "var(--mr-muted-foreground)" }, children: "This record appears to be empty." });
24
+ onStatusChange?.(status);
25
+ }, [status, onStatusChange]);
26
+ if (!url) {
27
+ return /* @__PURE__ */ jsx("div", { className: "mr-doc-viewer__status", children: "This record appears to be empty." });
48
28
  }
49
- return /* @__PURE__ */ jsx(
50
- "iframe",
51
- {
52
- className: "mr-doc-viewer__html-frame",
53
- srcDoc: state.text,
54
- sandbox: "allow-same-origin",
55
- title: "HTML preview"
56
- }
57
- );
29
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
30
+ status === "loading" && /* @__PURE__ */ jsx("div", { className: "mr-doc-viewer__status", children: "Loading document..." }),
31
+ status === "error" && /* @__PURE__ */ jsx("div", { className: "mr-doc-viewer__status", children: "Couldn't load this document." }),
32
+ /* @__PURE__ */ jsx(
33
+ "iframe",
34
+ {
35
+ className: "mr-doc-viewer__html-frame",
36
+ src: url,
37
+ sandbox: "allow-same-origin",
38
+ title: "HTML preview",
39
+ hidden: status !== "loaded",
40
+ onLoad,
41
+ onError
42
+ },
43
+ url
44
+ )
45
+ ] });
58
46
  }
59
47
 
60
48
  // src/views/RecordPreview/DocumentViewer.tsx
61
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
62
- var PdfViewer = lazy(() => import("./PdfViewer-CG2577E4.mjs").then((m) => ({ default: m.PdfViewer })));
49
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
50
+ var PdfViewer = lazy(() => import("./PdfViewer-6A7RUCNH.mjs").then((m) => ({ default: m.PdfViewer })));
63
51
  var PAGE_WIDTH = 595;
64
52
  var ZOOM_STEP = 0.25;
65
53
  var ZOOM_MIN = 0.5;
66
54
  var ZOOM_MAX = 3;
67
- function DocumentViewer({ files, title, isHtml }) {
68
- const [zoom, setZoom] = useState2(1);
69
- const [pageInfo, setPageInfo] = useState2({ current: 1, total: 0 });
70
- const [scrollToPage, setScrollToPage] = useState2(1);
71
- const [isFullscreen, setIsFullscreen] = useState2(false);
55
+ function DocumentViewer({ files, title, isHtml, onHtmlStatusChange }) {
56
+ const [zoom, setZoom] = useState(1);
57
+ const [pageInfo, setPageInfo] = useState({ current: 1, total: 0 });
58
+ const [scrollToPage, setScrollToPage] = useState(1);
59
+ const [isFullscreen, setIsFullscreen] = useState(false);
60
+ const [htmlStatus, setHtmlStatus] = useState("loading");
61
+ const [pdfStatus, setPdfStatus] = useState("loading");
72
62
  const rootRef = useRef(null);
63
+ const handleHtmlStatusChange = useCallback(
64
+ (status) => {
65
+ setHtmlStatus(status);
66
+ onHtmlStatusChange?.(status);
67
+ },
68
+ [onHtmlStatusChange]
69
+ );
73
70
  useEffect2(() => {
74
71
  const onChange = () => setIsFullscreen(document.fullscreenElement === rootRef.current);
75
72
  document.addEventListener("fullscreenchange", onChange);
@@ -81,8 +78,9 @@ function DocumentViewer({ files, title, isHtml }) {
81
78
  };
82
79
  const primary = files[0] ?? null;
83
80
  const isPdf = primary?.kind === "pdf";
81
+ const isPdfFallback = isPdf && pdfStatus === "fallback";
84
82
  const images = files.filter((f) => f.kind === "image");
85
- const isZoomable = isPdf || images.length > 0;
83
+ const isZoomable = isPdf && !isPdfFallback || images.length > 0;
86
84
  const zoomOut = () => setZoom((z) => Math.max(ZOOM_MIN, +(z - ZOOM_STEP).toFixed(2)));
87
85
  const zoomIn = () => setZoom((z) => Math.min(ZOOM_MAX, +(z + ZOOM_STEP).toFixed(2)));
88
86
  const prevPage = () => setScrollToPage(Math.max(1, pageInfo.current - 1));
@@ -96,7 +94,7 @@ function DocumentViewer({ files, title, isHtml }) {
96
94
  fn(objUrl);
97
95
  setTimeout(() => URL.revokeObjectURL(objUrl), 12e4);
98
96
  } catch {
99
- fn(primary.url);
97
+ window.open(primary.url, "_blank", "noopener,noreferrer");
100
98
  }
101
99
  }, [primary]);
102
100
  const handleDownload = () => withBlobUrl((objUrl) => {
@@ -105,10 +103,10 @@ function DocumentViewer({ files, title, isHtml }) {
105
103
  a.download = title || "document";
106
104
  a.click();
107
105
  });
108
- return /* @__PURE__ */ jsxs("div", { className: `mr-doc-viewer${isHtml ? " mr-doc-viewer--html" : ""}`, ref: rootRef, children: [
109
- !isHtml && /* @__PURE__ */ jsxs("div", { className: "mr-doc-viewer__toolbar", children: [
106
+ return /* @__PURE__ */ jsxs2("div", { className: `mr-doc-viewer${isHtml ? " mr-doc-viewer--html" : ""}`, ref: rootRef, children: [
107
+ !isHtml && /* @__PURE__ */ jsxs2("div", { className: "mr-doc-viewer__toolbar", children: [
110
108
  /* @__PURE__ */ jsx2("span", { className: "mr-doc-viewer__filename", children: title }),
111
- isPdf && /* @__PURE__ */ jsxs("div", { className: "mr-doc-viewer__pages", children: [
109
+ isPdf && !isPdfFallback && /* @__PURE__ */ jsxs2("div", { className: "mr-doc-viewer__pages", children: [
112
110
  /* @__PURE__ */ jsx2(
113
111
  "button",
114
112
  {
@@ -133,16 +131,16 @@ function DocumentViewer({ files, title, isHtml }) {
133
131
  }
134
132
  )
135
133
  ] }),
136
- isZoomable && /* @__PURE__ */ jsxs("div", { className: "mr-doc-viewer__zoom", children: [
134
+ isZoomable && /* @__PURE__ */ jsxs2("div", { className: "mr-doc-viewer__zoom", children: [
137
135
  /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Zoom out", onClick: zoomOut, disabled: zoom <= ZOOM_MIN, children: /* @__PURE__ */ jsx2(Minus, { size: 20, "aria-hidden": true }) }),
138
- /* @__PURE__ */ jsxs("span", { className: "mr-doc-viewer__zoom-badge", children: [
136
+ /* @__PURE__ */ jsxs2("span", { className: "mr-doc-viewer__zoom-badge", children: [
139
137
  Math.round(zoom * 100),
140
138
  "%"
141
139
  ] }),
142
140
  /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Zoom in", onClick: zoomIn, disabled: zoom >= ZOOM_MAX, children: /* @__PURE__ */ jsx2(Plus, { size: 20, "aria-hidden": true }) })
143
141
  ] }),
144
- /* @__PURE__ */ jsxs("div", { className: "mr-doc-viewer__actions", children: [
145
- /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Download", onClick: handleDownload, disabled: !primary, children: /* @__PURE__ */ jsx2(Download, { size: 20, "aria-hidden": true }) }),
142
+ /* @__PURE__ */ jsxs2("div", { className: "mr-doc-viewer__actions", children: [
143
+ /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Download", onClick: handleDownload, disabled: !primary || isPdfFallback, children: /* @__PURE__ */ jsx2(Download, { size: 20, "aria-hidden": true }) }),
146
144
  /* @__PURE__ */ jsx2(
147
145
  "button",
148
146
  {
@@ -154,8 +152,8 @@ function DocumentViewer({ files, title, isHtml }) {
154
152
  )
155
153
  ] })
156
154
  ] }),
157
- isHtml && /* @__PURE__ */ jsxs("div", { className: "mr-doc-viewer__html-bar", children: [
158
- /* @__PURE__ */ jsxs("div", { className: "mr-doc-viewer__html-bar-left", children: [
155
+ isHtml && htmlStatus === "loaded" && /* @__PURE__ */ jsxs2("div", { className: "mr-doc-viewer__html-bar", children: [
156
+ /* @__PURE__ */ jsxs2("div", { className: "mr-doc-viewer__html-bar-left", children: [
159
157
  /* @__PURE__ */ jsx2(Globe, { size: 12, "aria-hidden": true }),
160
158
  /* @__PURE__ */ jsx2("span", { children: "Previewing HTML file \u2013 you can copy to a note and edit this file. The original record that you are viewing now will not be affected." })
161
159
  ] }),
@@ -178,7 +176,8 @@ function DocumentViewer({ files, title, isHtml }) {
178
176
  baseWidth: PAGE_WIDTH,
179
177
  scale: zoom,
180
178
  scrollToPage,
181
- onPageInfo: setPageInfo
179
+ onPageInfo: setPageInfo,
180
+ onStatusChange: setPdfStatus
182
181
  }
183
182
  ) }) : images.length > 0 ? images.map((img, i) => /* @__PURE__ */ jsx2(
184
183
  "img",
@@ -189,10 +188,10 @@ function DocumentViewer({ files, title, isHtml }) {
189
188
  style: { transform: `scale(${zoom})`, transformOrigin: "top center" }
190
189
  },
191
190
  i
192
- )) : /* @__PURE__ */ jsx2(TextViewer, { url: primary.url }) })
191
+ )) : /* @__PURE__ */ jsx2(TextViewer, { url: primary.url, onStatusChange: handleHtmlStatusChange }) })
193
192
  ] });
194
193
  }
195
194
  export {
196
195
  DocumentViewer
197
196
  };
198
- //# sourceMappingURL=DocumentViewer-5AXKL75U.mjs.map
197
+ //# sourceMappingURL=DocumentViewer-AL4TAYBP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/views/RecordPreview/DocumentViewer.tsx","../src/views/RecordPreview/TextViewer.tsx"],"sourcesContent":["import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';\nimport {\n ChevronLeft,\n ChevronRight,\n Minus,\n Plus,\n Download,\n Maximize,\n Minimize,\n Globe,\n} from 'lucide-react';\nimport { TextViewer, type TextViewerStatus } from './TextViewer';\nimport type { PdfViewerStatus } from './PdfViewer';\nimport type { PreviewFile } from '../../connection';\n\nconst PdfViewer = lazy(() => import('./PdfViewer').then((m) => ({ default: m.PdfViewer })));\n\nexport interface DocumentViewerProps {\n files: PreviewFile[];\n title: string;\n isHtml?: boolean;\n onHtmlStatusChange?: (status: TextViewerStatus) => void;\n}\n\nconst PAGE_WIDTH = 595; \nconst ZOOM_STEP = 0.25;\nconst ZOOM_MIN = 0.5;\nconst ZOOM_MAX = 3;\n\nexport function DocumentViewer({ files, title, isHtml, onHtmlStatusChange }: DocumentViewerProps) {\n const [zoom, setZoom] = useState(1);\n const [pageInfo, setPageInfo] = useState({ current: 1, total: 0 });\n const [scrollToPage, setScrollToPage] = useState(1);\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [htmlStatus, setHtmlStatus] = useState<TextViewerStatus>('loading');\n const [pdfStatus, setPdfStatus] = useState<PdfViewerStatus>('loading');\n const rootRef = useRef<HTMLDivElement>(null);\n\n const handleHtmlStatusChange = useCallback(\n (status: TextViewerStatus) => {\n setHtmlStatus(status);\n onHtmlStatusChange?.(status);\n },\n [onHtmlStatusChange],\n );\n\n useEffect(() => {\n const onChange = () => setIsFullscreen(document.fullscreenElement === rootRef.current);\n document.addEventListener('fullscreenchange', onChange);\n return () => document.removeEventListener('fullscreenchange', onChange);\n }, []);\n\n const toggleFullscreen = () => {\n if (document.fullscreenElement) void document.exitFullscreen();\n else void rootRef.current?.requestFullscreen?.();\n };\n\n const primary = files[0] ?? null;\n const isPdf = primary?.kind === 'pdf';\n // When pdf.js can't fetch the PDF (CORS/mixed content), PdfViewer falls\n // back to a native iframe — our custom page/zoom controls and the blob-based\n // Download button don't apply to that (and Download would fail the same way).\n const isPdfFallback = isPdf && pdfStatus === 'fallback';\n const images = files.filter((f) => f.kind === 'image');\n const isZoomable = (isPdf && !isPdfFallback) || images.length > 0;\n\n const zoomOut = () => setZoom((z) => Math.max(ZOOM_MIN, +(z - ZOOM_STEP).toFixed(2)));\n const zoomIn = () => setZoom((z) => Math.min(ZOOM_MAX, +(z + ZOOM_STEP).toFixed(2)));\n\n const prevPage = () => setScrollToPage(Math.max(1, pageInfo.current - 1));\n const nextPage = () => setScrollToPage(Math.min(pageInfo.total, pageInfo.current + 1));\n\n // Download / print fetch the file into a blob URL rather than re-opening the\n // signed URL directly, so it works even when the document is already loaded.\n const withBlobUrl = useCallback(async (fn: (objUrl: string) => void) => {\n if (!primary) return;\n try {\n const res = await fetch(primary.url);\n const blob = await res.blob();\n const objUrl = URL.createObjectURL(blob);\n fn(objUrl);\n // Revoke after the consumer has had a chance to use it.\n setTimeout(() => URL.revokeObjectURL(objUrl), 120_000);\n } catch {\n // The blob fetch failed (e.g. the asset host doesn't send CORS\n // headers). `download` is ignored by browsers for cross-origin\n // non-blob URLs, so clicking `fn`'s anchor here would just navigate\n // the whole app away to the raw document instead of downloading it —\n // open it in a new tab instead so the user can save it manually.\n window.open(primary.url, '_blank', 'noopener,noreferrer');\n }\n }, [primary]);\n\n const handleDownload = () =>\n withBlobUrl((objUrl) => {\n const a = document.createElement('a');\n a.href = objUrl;\n a.download = title || 'document';\n a.click();\n });\n\n return (\n <div className={`mr-doc-viewer${isHtml ? ' mr-doc-viewer--html' : ''}`} ref={rootRef}>\n {!isHtml && <div className=\"mr-doc-viewer__toolbar\">\n <span className=\"mr-doc-viewer__filename\">{title}</span>\n {isPdf && !isPdfFallback && (\n <div className=\"mr-doc-viewer__pages\">\n <button\n type=\"button\"\n aria-label=\"Previous page\"\n onClick={prevPage}\n disabled={pageInfo.current <= 1}\n >\n <ChevronLeft size={18} aria-hidden />\n </button>\n <span className=\"mr-doc-viewer__page-badge\">{pageInfo.current}</span>\n <span>/</span>\n <span>{pageInfo.total || '—'}</span>\n <button\n type=\"button\"\n aria-label=\"Next page\"\n onClick={nextPage}\n disabled={pageInfo.current >= pageInfo.total}\n >\n <ChevronRight size={18} aria-hidden />\n </button>\n </div>\n )}\n {isZoomable && (\n <div className=\"mr-doc-viewer__zoom\">\n <button type=\"button\" aria-label=\"Zoom out\" onClick={zoomOut} disabled={zoom <= ZOOM_MIN}>\n <Minus size={20} aria-hidden />\n </button>\n <span className=\"mr-doc-viewer__zoom-badge\">{Math.round(zoom * 100)}%</span>\n <button type=\"button\" aria-label=\"Zoom in\" onClick={zoomIn} disabled={zoom >= ZOOM_MAX}>\n <Plus size={20} aria-hidden />\n </button>\n </div>\n )}\n <div className=\"mr-doc-viewer__actions\">\n <button type=\"button\" aria-label=\"Download\" onClick={handleDownload} disabled={!primary || isPdfFallback}>\n <Download size={20} aria-hidden />\n </button>\n <button\n type=\"button\"\n aria-label={isFullscreen ? 'Exit full screen' : 'Full screen'}\n onClick={toggleFullscreen}\n >\n {isFullscreen ? <Minimize size={20} aria-hidden /> : <Maximize size={20} aria-hidden />}\n </button>\n </div>\n </div>}\n\n {isHtml && htmlStatus === 'loaded' && (\n <div className=\"mr-doc-viewer__html-bar\">\n <div className=\"mr-doc-viewer__html-bar-left\">\n <Globe size={12} aria-hidden />\n <span>Previewing HTML file – you can copy to a note and edit this file. The original record that you are viewing now will not be affected.</span>\n </div>\n <button\n type=\"button\"\n className=\"mr-doc-viewer__html-download\"\n aria-label=\"Download\"\n onClick={handleDownload}\n disabled={!primary}\n >\n <Download size={16} aria-hidden />\n </button>\n </div>\n )}\n\n <div className=\"mr-doc-viewer__canvas\">\n {!primary ? (\n <div className=\"mr-doc-viewer__status\">No preview available for this record.</div>\n ) : isPdf ? (\n <Suspense fallback={<div className=\"mr-doc-viewer__status\">Loading viewer…</div>}>\n <PdfViewer\n url={primary.url}\n baseWidth={PAGE_WIDTH}\n scale={zoom}\n scrollToPage={scrollToPage}\n onPageInfo={setPageInfo}\n onStatusChange={setPdfStatus}\n />\n </Suspense>\n ) : images.length > 0 ? (\n images.map((img, i) => (\n <img\n key={i}\n className=\"mr-doc-viewer__image\"\n src={img.url}\n alt={`${title} ${i + 1}`}\n style={{ transform: `scale(${zoom})`, transformOrigin: 'top center' }}\n />\n ))\n ) : (\n <TextViewer url={primary.url} onStatusChange={handleHtmlStatusChange} />\n )}\n </div>\n </div>\n );\n}\n","import { useEffect } from 'react';\nimport { useIframeLoadStatus } from './useIframeLoadStatus';\n\nexport type TextViewerStatus = 'loading' | 'loaded' | 'error' | 'empty';\n\nexport interface TextViewerProps {\n url: string;\n onStatusChange?: (status: TextViewerStatus) => void;\n}\n\n/**\n * Loads the document via direct iframe navigation rather than `fetch` +\n * `srcDoc`. `fetch` requires the asset host to send CORS headers for our\n * origin — many hospital-hosted document servers don't. Iframe navigation\n * doesn't need CORS at all.\n */\nexport function TextViewer({ url, onStatusChange }: TextViewerProps) {\n const { status, onLoad, onError } = useIframeLoadStatus(url);\n\n useEffect(() => {\n onStatusChange?.(status);\n }, [status, onStatusChange]);\n\n if (!url) {\n return <div className=\"mr-doc-viewer__status\">This record appears to be empty.</div>;\n }\n\n return (\n <>\n {status === 'loading' && (\n <div className=\"mr-doc-viewer__status\">Loading document...</div>\n )}\n {status === 'error' && (\n <div className=\"mr-doc-viewer__status\">Couldn't load this document.</div>\n )}\n <iframe\n key={url}\n className=\"mr-doc-viewer__html-frame\"\n src={url}\n sandbox=\"allow-same-origin\"\n title=\"HTML preview\"\n hidden={status !== 'loaded'}\n onLoad={onLoad}\n onError={onError}\n />\n </>\n );\n}\n"],"mappings":"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","names":["useEffect","jsx","jsxs","useEffect"]}
@@ -1,4 +1,8 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/views/RecordPreview/DocumentViewer.tsx
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+ var _chunkNUHZPKCQjs = require('./chunk-NUHZPKCQ.js');
4
+
5
+ // src/views/RecordPreview/DocumentViewer.tsx
2
6
  var _react = require('react');
3
7
 
4
8
 
@@ -14,62 +18,55 @@ var _lucidereact = require('lucide-react');
14
18
  // src/views/RecordPreview/TextViewer.tsx
15
19
 
16
20
  var _jsxruntime = require('react/jsx-runtime');
17
- function TextViewer({ url }) {
18
- const [state, setState] = _react.useState.call(void 0, {
19
- status: "loading",
20
- text: ""
21
- });
21
+ function TextViewer({ url, onStatusChange }) {
22
+ const { status, onLoad, onError } = _chunkNUHZPKCQjs.useIframeLoadStatus.call(void 0, url);
22
23
  _react.useEffect.call(void 0, () => {
23
- let cancelled = false;
24
- setState({ status: "loading", text: "" });
25
- (async () => {
26
- try {
27
- const res = await fetch(url);
28
- if (!res.ok) throw new Error(`fetch ${res.status}`);
29
- const text = await res.text();
30
- if (text.trimStart().startsWith("%PDF")) throw new Error("binary");
31
- if (!cancelled) setState({ status: "loaded", text });
32
- } catch (e) {
33
- if (!cancelled) setState({ status: "error", text: "" });
34
- }
35
- })();
36
- return () => {
37
- cancelled = true;
38
- };
39
- }, [url]);
40
- if (state.status === "loading") {
41
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "mr-doc-viewer__status", style: { color: "var(--mr-muted-foreground)" }, children: "Loading document..." });
42
- }
43
- if (state.status === "error") {
44
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "mr-doc-viewer__status", style: { color: "var(--mr-muted-foreground)" }, children: "Couldn't load this document." });
45
- }
46
- if (!state.text.trim()) {
47
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "mr-doc-viewer__status", style: { color: "var(--mr-muted-foreground)" }, children: "This record appears to be empty." });
24
+ _optionalChain([onStatusChange, 'optionalCall', _ => _(status)]);
25
+ }, [status, onStatusChange]);
26
+ if (!url) {
27
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "mr-doc-viewer__status", children: "This record appears to be empty." });
48
28
  }
49
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
50
- "iframe",
51
- {
52
- className: "mr-doc-viewer__html-frame",
53
- srcDoc: state.text,
54
- sandbox: "allow-same-origin",
55
- title: "HTML preview"
56
- }
57
- );
29
+ return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
30
+ status === "loading" && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "mr-doc-viewer__status", children: "Loading document..." }),
31
+ status === "error" && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "mr-doc-viewer__status", children: "Couldn't load this document." }),
32
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
33
+ "iframe",
34
+ {
35
+ className: "mr-doc-viewer__html-frame",
36
+ src: url,
37
+ sandbox: "allow-same-origin",
38
+ title: "HTML preview",
39
+ hidden: status !== "loaded",
40
+ onLoad,
41
+ onError
42
+ },
43
+ url
44
+ )
45
+ ] });
58
46
  }
59
47
 
60
48
  // src/views/RecordPreview/DocumentViewer.tsx
61
49
 
62
- var PdfViewer = _react.lazy.call(void 0, () => Promise.resolve().then(() => _interopRequireWildcard(require("./PdfViewer-GIWRL6FQ.js"))).then((m) => ({ default: m.PdfViewer })));
50
+ var PdfViewer = _react.lazy.call(void 0, () => Promise.resolve().then(() => _interopRequireWildcard(require("./PdfViewer-PE3NFPLH.js"))).then((m) => ({ default: m.PdfViewer })));
63
51
  var PAGE_WIDTH = 595;
64
52
  var ZOOM_STEP = 0.25;
65
53
  var ZOOM_MIN = 0.5;
66
54
  var ZOOM_MAX = 3;
67
- function DocumentViewer({ files, title, isHtml }) {
55
+ function DocumentViewer({ files, title, isHtml, onHtmlStatusChange }) {
68
56
  const [zoom, setZoom] = _react.useState.call(void 0, 1);
69
57
  const [pageInfo, setPageInfo] = _react.useState.call(void 0, { current: 1, total: 0 });
70
58
  const [scrollToPage, setScrollToPage] = _react.useState.call(void 0, 1);
71
59
  const [isFullscreen, setIsFullscreen] = _react.useState.call(void 0, false);
60
+ const [htmlStatus, setHtmlStatus] = _react.useState.call(void 0, "loading");
61
+ const [pdfStatus, setPdfStatus] = _react.useState.call(void 0, "loading");
72
62
  const rootRef = _react.useRef.call(void 0, null);
63
+ const handleHtmlStatusChange = _react.useCallback.call(void 0,
64
+ (status) => {
65
+ setHtmlStatus(status);
66
+ _optionalChain([onHtmlStatusChange, 'optionalCall', _2 => _2(status)]);
67
+ },
68
+ [onHtmlStatusChange]
69
+ );
73
70
  _react.useEffect.call(void 0, () => {
74
71
  const onChange = () => setIsFullscreen(document.fullscreenElement === rootRef.current);
75
72
  document.addEventListener("fullscreenchange", onChange);
@@ -77,12 +74,13 @@ function DocumentViewer({ files, title, isHtml }) {
77
74
  }, []);
78
75
  const toggleFullscreen = () => {
79
76
  if (document.fullscreenElement) void document.exitFullscreen();
80
- else void _optionalChain([rootRef, 'access', _ => _.current, 'optionalAccess', _2 => _2.requestFullscreen, 'optionalCall', _3 => _3()]);
77
+ else void _optionalChain([rootRef, 'access', _3 => _3.current, 'optionalAccess', _4 => _4.requestFullscreen, 'optionalCall', _5 => _5()]);
81
78
  };
82
79
  const primary = _nullishCoalesce(files[0], () => ( null));
83
- const isPdf = _optionalChain([primary, 'optionalAccess', _4 => _4.kind]) === "pdf";
80
+ const isPdf = _optionalChain([primary, 'optionalAccess', _6 => _6.kind]) === "pdf";
81
+ const isPdfFallback = isPdf && pdfStatus === "fallback";
84
82
  const images = files.filter((f) => f.kind === "image");
85
- const isZoomable = isPdf || images.length > 0;
83
+ const isZoomable = isPdf && !isPdfFallback || images.length > 0;
86
84
  const zoomOut = () => setZoom((z) => Math.max(ZOOM_MIN, +(z - ZOOM_STEP).toFixed(2)));
87
85
  const zoomIn = () => setZoom((z) => Math.min(ZOOM_MAX, +(z + ZOOM_STEP).toFixed(2)));
88
86
  const prevPage = () => setScrollToPage(Math.max(1, pageInfo.current - 1));
@@ -95,8 +93,8 @@ function DocumentViewer({ files, title, isHtml }) {
95
93
  const objUrl = URL.createObjectURL(blob);
96
94
  fn(objUrl);
97
95
  setTimeout(() => URL.revokeObjectURL(objUrl), 12e4);
98
- } catch (e2) {
99
- fn(primary.url);
96
+ } catch (e) {
97
+ window.open(primary.url, "_blank", "noopener,noreferrer");
100
98
  }
101
99
  }, [primary]);
102
100
  const handleDownload = () => withBlobUrl((objUrl) => {
@@ -108,7 +106,7 @@ function DocumentViewer({ files, title, isHtml }) {
108
106
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: `mr-doc-viewer${isHtml ? " mr-doc-viewer--html" : ""}`, ref: rootRef, children: [
109
107
  !isHtml && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mr-doc-viewer__toolbar", children: [
110
108
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "mr-doc-viewer__filename", children: title }),
111
- isPdf && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mr-doc-viewer__pages", children: [
109
+ isPdf && !isPdfFallback && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mr-doc-viewer__pages", children: [
112
110
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
113
111
  "button",
114
112
  {
@@ -142,7 +140,7 @@ function DocumentViewer({ files, title, isHtml }) {
142
140
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { type: "button", "aria-label": "Zoom in", onClick: zoomIn, disabled: zoom >= ZOOM_MAX, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Plus, { size: 20, "aria-hidden": true }) })
143
141
  ] }),
144
142
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mr-doc-viewer__actions", children: [
145
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { type: "button", "aria-label": "Download", onClick: handleDownload, disabled: !primary, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Download, { size: 20, "aria-hidden": true }) }),
143
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { type: "button", "aria-label": "Download", onClick: handleDownload, disabled: !primary || isPdfFallback, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Download, { size: 20, "aria-hidden": true }) }),
146
144
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
147
145
  "button",
148
146
  {
@@ -154,7 +152,7 @@ function DocumentViewer({ files, title, isHtml }) {
154
152
  )
155
153
  ] })
156
154
  ] }),
157
- isHtml && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mr-doc-viewer__html-bar", children: [
155
+ isHtml && htmlStatus === "loaded" && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mr-doc-viewer__html-bar", children: [
158
156
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mr-doc-viewer__html-bar-left", children: [
159
157
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Globe, { size: 12, "aria-hidden": true }),
160
158
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: "Previewing HTML file \u2013 you can copy to a note and edit this file. The original record that you are viewing now will not be affected." })
@@ -178,7 +176,8 @@ function DocumentViewer({ files, title, isHtml }) {
178
176
  baseWidth: PAGE_WIDTH,
179
177
  scale: zoom,
180
178
  scrollToPage,
181
- onPageInfo: setPageInfo
179
+ onPageInfo: setPageInfo,
180
+ onStatusChange: setPdfStatus
182
181
  }
183
182
  ) }) : images.length > 0 ? images.map((img, i) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
184
183
  "img",
@@ -189,10 +188,10 @@ function DocumentViewer({ files, title, isHtml }) {
189
188
  style: { transform: `scale(${zoom})`, transformOrigin: "top center" }
190
189
  },
191
190
  i
192
- )) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, TextViewer, { url: primary.url }) })
191
+ )) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, TextViewer, { url: primary.url, onStatusChange: handleHtmlStatusChange }) })
193
192
  ] });
194
193
  }
195
194
 
196
195
 
197
196
  exports.DocumentViewer = DocumentViewer;
198
- //# sourceMappingURL=DocumentViewer-Y3SQ2BMS.js.map
197
+ //# sourceMappingURL=DocumentViewer-LN7UYALN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/medical-records-ui/medical-records-ui/dist/DocumentViewer-LN7UYALN.js","../src/views/RecordPreview/DocumentViewer.tsx","../src/views/RecordPreview/TextViewer.tsx"],"names":["useEffect","jsxs","jsx"],"mappings":"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","file":"/home/runner/work/medical-records-ui/medical-records-ui/dist/DocumentViewer-LN7UYALN.js","sourcesContent":[null,"import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';\nimport {\n ChevronLeft,\n ChevronRight,\n Minus,\n Plus,\n Download,\n Maximize,\n Minimize,\n Globe,\n} from 'lucide-react';\nimport { TextViewer, type TextViewerStatus } from './TextViewer';\nimport type { PdfViewerStatus } from './PdfViewer';\nimport type { PreviewFile } from '../../connection';\n\nconst PdfViewer = lazy(() => import('./PdfViewer').then((m) => ({ default: m.PdfViewer })));\n\nexport interface DocumentViewerProps {\n files: PreviewFile[];\n title: string;\n isHtml?: boolean;\n onHtmlStatusChange?: (status: TextViewerStatus) => void;\n}\n\nconst PAGE_WIDTH = 595; \nconst ZOOM_STEP = 0.25;\nconst ZOOM_MIN = 0.5;\nconst ZOOM_MAX = 3;\n\nexport function DocumentViewer({ files, title, isHtml, onHtmlStatusChange }: DocumentViewerProps) {\n const [zoom, setZoom] = useState(1);\n const [pageInfo, setPageInfo] = useState({ current: 1, total: 0 });\n const [scrollToPage, setScrollToPage] = useState(1);\n const [isFullscreen, setIsFullscreen] = useState(false);\n const [htmlStatus, setHtmlStatus] = useState<TextViewerStatus>('loading');\n const [pdfStatus, setPdfStatus] = useState<PdfViewerStatus>('loading');\n const rootRef = useRef<HTMLDivElement>(null);\n\n const handleHtmlStatusChange = useCallback(\n (status: TextViewerStatus) => {\n setHtmlStatus(status);\n onHtmlStatusChange?.(status);\n },\n [onHtmlStatusChange],\n );\n\n useEffect(() => {\n const onChange = () => setIsFullscreen(document.fullscreenElement === rootRef.current);\n document.addEventListener('fullscreenchange', onChange);\n return () => document.removeEventListener('fullscreenchange', onChange);\n }, []);\n\n const toggleFullscreen = () => {\n if (document.fullscreenElement) void document.exitFullscreen();\n else void rootRef.current?.requestFullscreen?.();\n };\n\n const primary = files[0] ?? null;\n const isPdf = primary?.kind === 'pdf';\n // When pdf.js can't fetch the PDF (CORS/mixed content), PdfViewer falls\n // back to a native iframe — our custom page/zoom controls and the blob-based\n // Download button don't apply to that (and Download would fail the same way).\n const isPdfFallback = isPdf && pdfStatus === 'fallback';\n const images = files.filter((f) => f.kind === 'image');\n const isZoomable = (isPdf && !isPdfFallback) || images.length > 0;\n\n const zoomOut = () => setZoom((z) => Math.max(ZOOM_MIN, +(z - ZOOM_STEP).toFixed(2)));\n const zoomIn = () => setZoom((z) => Math.min(ZOOM_MAX, +(z + ZOOM_STEP).toFixed(2)));\n\n const prevPage = () => setScrollToPage(Math.max(1, pageInfo.current - 1));\n const nextPage = () => setScrollToPage(Math.min(pageInfo.total, pageInfo.current + 1));\n\n // Download / print fetch the file into a blob URL rather than re-opening the\n // signed URL directly, so it works even when the document is already loaded.\n const withBlobUrl = useCallback(async (fn: (objUrl: string) => void) => {\n if (!primary) return;\n try {\n const res = await fetch(primary.url);\n const blob = await res.blob();\n const objUrl = URL.createObjectURL(blob);\n fn(objUrl);\n // Revoke after the consumer has had a chance to use it.\n setTimeout(() => URL.revokeObjectURL(objUrl), 120_000);\n } catch {\n // The blob fetch failed (e.g. the asset host doesn't send CORS\n // headers). `download` is ignored by browsers for cross-origin\n // non-blob URLs, so clicking `fn`'s anchor here would just navigate\n // the whole app away to the raw document instead of downloading it —\n // open it in a new tab instead so the user can save it manually.\n window.open(primary.url, '_blank', 'noopener,noreferrer');\n }\n }, [primary]);\n\n const handleDownload = () =>\n withBlobUrl((objUrl) => {\n const a = document.createElement('a');\n a.href = objUrl;\n a.download = title || 'document';\n a.click();\n });\n\n return (\n <div className={`mr-doc-viewer${isHtml ? ' mr-doc-viewer--html' : ''}`} ref={rootRef}>\n {!isHtml && <div className=\"mr-doc-viewer__toolbar\">\n <span className=\"mr-doc-viewer__filename\">{title}</span>\n {isPdf && !isPdfFallback && (\n <div className=\"mr-doc-viewer__pages\">\n <button\n type=\"button\"\n aria-label=\"Previous page\"\n onClick={prevPage}\n disabled={pageInfo.current <= 1}\n >\n <ChevronLeft size={18} aria-hidden />\n </button>\n <span className=\"mr-doc-viewer__page-badge\">{pageInfo.current}</span>\n <span>/</span>\n <span>{pageInfo.total || '—'}</span>\n <button\n type=\"button\"\n aria-label=\"Next page\"\n onClick={nextPage}\n disabled={pageInfo.current >= pageInfo.total}\n >\n <ChevronRight size={18} aria-hidden />\n </button>\n </div>\n )}\n {isZoomable && (\n <div className=\"mr-doc-viewer__zoom\">\n <button type=\"button\" aria-label=\"Zoom out\" onClick={zoomOut} disabled={zoom <= ZOOM_MIN}>\n <Minus size={20} aria-hidden />\n </button>\n <span className=\"mr-doc-viewer__zoom-badge\">{Math.round(zoom * 100)}%</span>\n <button type=\"button\" aria-label=\"Zoom in\" onClick={zoomIn} disabled={zoom >= ZOOM_MAX}>\n <Plus size={20} aria-hidden />\n </button>\n </div>\n )}\n <div className=\"mr-doc-viewer__actions\">\n <button type=\"button\" aria-label=\"Download\" onClick={handleDownload} disabled={!primary || isPdfFallback}>\n <Download size={20} aria-hidden />\n </button>\n <button\n type=\"button\"\n aria-label={isFullscreen ? 'Exit full screen' : 'Full screen'}\n onClick={toggleFullscreen}\n >\n {isFullscreen ? <Minimize size={20} aria-hidden /> : <Maximize size={20} aria-hidden />}\n </button>\n </div>\n </div>}\n\n {isHtml && htmlStatus === 'loaded' && (\n <div className=\"mr-doc-viewer__html-bar\">\n <div className=\"mr-doc-viewer__html-bar-left\">\n <Globe size={12} aria-hidden />\n <span>Previewing HTML file – you can copy to a note and edit this file. The original record that you are viewing now will not be affected.</span>\n </div>\n <button\n type=\"button\"\n className=\"mr-doc-viewer__html-download\"\n aria-label=\"Download\"\n onClick={handleDownload}\n disabled={!primary}\n >\n <Download size={16} aria-hidden />\n </button>\n </div>\n )}\n\n <div className=\"mr-doc-viewer__canvas\">\n {!primary ? (\n <div className=\"mr-doc-viewer__status\">No preview available for this record.</div>\n ) : isPdf ? (\n <Suspense fallback={<div className=\"mr-doc-viewer__status\">Loading viewer…</div>}>\n <PdfViewer\n url={primary.url}\n baseWidth={PAGE_WIDTH}\n scale={zoom}\n scrollToPage={scrollToPage}\n onPageInfo={setPageInfo}\n onStatusChange={setPdfStatus}\n />\n </Suspense>\n ) : images.length > 0 ? (\n images.map((img, i) => (\n <img\n key={i}\n className=\"mr-doc-viewer__image\"\n src={img.url}\n alt={`${title} ${i + 1}`}\n style={{ transform: `scale(${zoom})`, transformOrigin: 'top center' }}\n />\n ))\n ) : (\n <TextViewer url={primary.url} onStatusChange={handleHtmlStatusChange} />\n )}\n </div>\n </div>\n );\n}\n","import { useEffect } from 'react';\nimport { useIframeLoadStatus } from './useIframeLoadStatus';\n\nexport type TextViewerStatus = 'loading' | 'loaded' | 'error' | 'empty';\n\nexport interface TextViewerProps {\n url: string;\n onStatusChange?: (status: TextViewerStatus) => void;\n}\n\n/**\n * Loads the document via direct iframe navigation rather than `fetch` +\n * `srcDoc`. `fetch` requires the asset host to send CORS headers for our\n * origin — many hospital-hosted document servers don't. Iframe navigation\n * doesn't need CORS at all.\n */\nexport function TextViewer({ url, onStatusChange }: TextViewerProps) {\n const { status, onLoad, onError } = useIframeLoadStatus(url);\n\n useEffect(() => {\n onStatusChange?.(status);\n }, [status, onStatusChange]);\n\n if (!url) {\n return <div className=\"mr-doc-viewer__status\">This record appears to be empty.</div>;\n }\n\n return (\n <>\n {status === 'loading' && (\n <div className=\"mr-doc-viewer__status\">Loading document...</div>\n )}\n {status === 'error' && (\n <div className=\"mr-doc-viewer__status\">Couldn't load this document.</div>\n )}\n <iframe\n key={url}\n className=\"mr-doc-viewer__html-frame\"\n src={url}\n sandbox=\"allow-same-origin\"\n title=\"HTML preview\"\n hidden={status !== 'loaded'}\n onLoad={onLoad}\n onError={onError}\n />\n </>\n );\n}\n"]}
@@ -8,7 +8,7 @@ import {
8
8
  useRecordPanelTable,
9
9
  useRecordPreview,
10
10
  useSmartRecordsActions
11
- } from "./chunk-7AO6M347.mjs";
11
+ } from "./chunk-ZL3UK6HA.mjs";
12
12
  import {
13
13
  IconButton,
14
14
  SearchInput_default,
@@ -16,7 +16,7 @@ import {
16
16
  Typography,
17
17
  epochToDisplay,
18
18
  useSdk
19
- } from "./chunk-NZNI6YJ5.mjs";
19
+ } from "./chunk-Q2TRUL57.mjs";
20
20
 
21
21
  // src/views/components/shared/OriginalRecordPanel.tsx
22
22
  import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -24,7 +24,7 @@ import { ChevronLeft, ChevronRight, ChevronDown, ExternalLink, Check, LoaderCirc
24
24
  import { useStore } from "zustand";
25
25
  import { jsx, jsxs } from "react/jsx-runtime";
26
26
  var DocumentViewer = lazy(
27
- () => import("./DocumentViewer-5AXKL75U.mjs").then((m) => ({
27
+ () => import("./DocumentViewer-AL4TAYBP.mjs").then((m) => ({
28
28
  default: m.DocumentViewer
29
29
  }))
30
30
  );
@@ -44,7 +44,7 @@ function OriginalRecordPanel({
44
44
  const record = useStore(store, (s) => s.records.byId[recordId]);
45
45
  const recordsById = useStore(store, (s) => s.records.byId);
46
46
  const preview = useRecordPreview(recordId);
47
- const { replaceOriginal, onCopyToNote, onCopyVitalsToNote, hasCopyToNote, onToast } = useSmartRecordsActions();
47
+ const { replaceOriginal, onCopyToNote, onCopyVitalsToNote, hasCopyToNote } = useSmartRecordsActions();
48
48
  const { status: vitalsStatus, table } = useRecordPanelTable(
49
49
  record?.isSmart ? recordId : null
50
50
  );
@@ -155,34 +155,43 @@ function OriginalRecordPanel({
155
155
  }
156
156
  }, [filteredRows, srSelected, onCopyVitalsToNote, onCopyToNote]);
157
157
  const htmlCopyBtnRef = useRef(null);
158
- const [isHtmlCopying, setIsHtmlCopying] = useState(false);
159
- const handleHtmlCopyToNote = useCallback(async () => {
160
- const url = preview.files[0]?.url;
158
+ const [htmlCopyText, setHtmlCopyText] = useState(null);
159
+ useEffect(() => {
160
+ setHtmlCopyText(null);
161
+ const url = preview.isHtml ? preview.files[0]?.url : void 0;
161
162
  if (!url) return;
162
- setIsHtmlCopying(true);
163
- try {
164
- const res = await fetch(url);
165
- const html = await res.text();
166
- const doc = new DOMParser().parseFromString(html, "text/html");
167
- const text = doc.body.textContent?.trim() ?? "";
168
- if (!text) return;
169
- if (onCopyVitalsToNote) {
170
- void onCopyVitalsToNote(text);
171
- } else if (onCopyToNote) {
172
- const rect = htmlCopyBtnRef.current?.getBoundingClientRect();
173
- void onCopyToNote(
174
- text,
175
- rect ? { x: rect.left, y: rect.bottom } : void 0
176
- );
163
+ let cancelled = false;
164
+ (async () => {
165
+ try {
166
+ const res = await fetch(url);
167
+ if (!res.ok) throw new Error(`fetch ${res.status}`);
168
+ const html = await res.text();
169
+ const doc = new DOMParser().parseFromString(html, "text/html");
170
+ const text = doc.body.textContent?.trim() ?? "";
171
+ if (!cancelled && text) setHtmlCopyText(text);
172
+ } catch {
177
173
  }
178
- } catch {
179
- onToast?.("Failed to copy document text", "error");
180
- } finally {
181
- setIsHtmlCopying(false);
174
+ })();
175
+ return () => {
176
+ cancelled = true;
177
+ };
178
+ }, [preview.isHtml, preview.files]);
179
+ const handleHtmlCopyToNote = useCallback(() => {
180
+ if (!htmlCopyText) return;
181
+ if (onCopyVitalsToNote) {
182
+ void onCopyVitalsToNote(htmlCopyText);
183
+ } else if (onCopyToNote) {
184
+ const rect = htmlCopyBtnRef.current?.getBoundingClientRect();
185
+ void onCopyToNote(
186
+ htmlCopyText,
187
+ rect ? { x: rect.left, y: rect.bottom } : void 0
188
+ );
182
189
  }
183
- }, [preview.files, onCopyVitalsToNote, onCopyToNote, onToast]);
190
+ }, [htmlCopyText, onCopyVitalsToNote, onCopyToNote]);
191
+ const [htmlPreviewStatus, setHtmlPreviewStatus] = useState("loading");
184
192
  const isSmart = record?.isSmart ?? false;
185
193
  const hasPreview = preview.status === "loaded" && preview.files.length > 0;
194
+ const showOpenInNewTab = hasPreview && (!preview.isHtml || htmlPreviewStatus === "loaded");
186
195
  return /* @__PURE__ */ jsxs("div", { className: "mr-sv__panel", children: [
187
196
  /* @__PURE__ */ jsxs("div", { className: "mr-sv__panel-header", children: [
188
197
  /* @__PURE__ */ jsxs("button", { type: "button", className: "mr-sv__back-btn", onClick: onBack, children: [
@@ -194,21 +203,17 @@ function OriginalRecordPanel({
194
203
  /* @__PURE__ */ jsx(Typography, { variant: "hint", as: "span", className: "mr-sv__panel-meta-text", children: epochToDisplay(record.createdAtEpoch) })
195
204
  ] }) : null,
196
205
  /* @__PURE__ */ jsxs("div", { className: "mr-orig__header-actions", children: [
197
- preview.isHtml && hasCopyToNote && /* @__PURE__ */ jsxs(
206
+ htmlCopyText && hasCopyToNote && /* @__PURE__ */ jsx(
198
207
  "button",
199
208
  {
200
209
  ref: htmlCopyBtnRef,
201
210
  type: "button",
202
211
  className: "mr-orig__html-copy-btn",
203
212
  onClick: handleHtmlCopyToNote,
204
- disabled: isHtmlCopying,
205
- children: [
206
- isHtmlCopying && /* @__PURE__ */ jsx(LoaderCircle, { size: 12, className: "mr-sv__loading-spinner", "aria-hidden": true }),
207
- "Copy to note"
208
- ]
213
+ children: "Copy to note"
209
214
  }
210
215
  ),
211
- hasPreview && /* @__PURE__ */ jsx(
216
+ showOpenInNewTab && /* @__PURE__ */ jsx(
212
217
  IconButton,
213
218
  {
214
219
  icon: /* @__PURE__ */ jsx(ExternalLink, { size: 16, "aria-hidden": true }),
@@ -250,7 +255,8 @@ function OriginalRecordPanel({
250
255
  {
251
256
  files: preview.files,
252
257
  title: record?.title ?? "",
253
- isHtml: preview.isHtml
258
+ isHtml: preview.isHtml,
259
+ onHtmlStatusChange: setHtmlPreviewStatus
254
260
  }
255
261
  ) })
256
262
  }
@@ -261,7 +267,8 @@ function OriginalRecordPanel({
261
267
  {
262
268
  files: preview.files,
263
269
  title: record?.title ?? "",
264
- isHtml: preview.isHtml
270
+ isHtml: preview.isHtml,
271
+ onHtmlStatusChange: setHtmlPreviewStatus
265
272
  }
266
273
  ) }) }),
267
274
  smartPanelOpen ? /* @__PURE__ */ jsxs("div", { className: "mr-orig__smart-side", children: [
@@ -489,4 +496,4 @@ function VitalReadingsExpanded({ vitalName, ekaId }) {
489
496
  export {
490
497
  OriginalRecordPanel
491
498
  };
492
- //# sourceMappingURL=OriginalRecordPanel-NGLSSJLE.mjs.map
499
+ //# sourceMappingURL=OriginalRecordPanel-4MVFXW74.mjs.map