@realtek/core-theme-live 0.0.407 → 0.0.408

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 (24) hide show
  1. package/dist-lib/{DocumentViewer-DesrL_6B.js → DocumentViewer-DSxsilzL.js} +2 -2
  2. package/dist-lib/{DocumentViewer-DesrL_6B.js.map → DocumentViewer-DSxsilzL.js.map} +1 -1
  3. package/dist-lib/{DocumentViewer-CcGppqVn.cjs → DocumentViewer-Dd9_2U9M.cjs} +2 -2
  4. package/dist-lib/{DocumentViewer-CcGppqVn.cjs.map → DocumentViewer-Dd9_2U9M.cjs.map} +1 -1
  5. package/dist-lib/{LinkedRecordFileField-DFH2ESFd.js → LinkedRecordFileField-BTGcd4mR.js} +4 -4
  6. package/dist-lib/{LinkedRecordFileField-DFH2ESFd.js.map → LinkedRecordFileField-BTGcd4mR.js.map} +1 -1
  7. package/dist-lib/{LinkedRecordFileField-wry-EYvc.cjs → LinkedRecordFileField-BeSGeGbS.cjs} +3 -3
  8. package/dist-lib/{LinkedRecordFileField-wry-EYvc.cjs.map → LinkedRecordFileField-BeSGeGbS.cjs.map} +1 -1
  9. package/dist-lib/{addFormApi-Bly_wUfr.js → addFormApi-DBeQrNX1.js} +3 -3
  10. package/dist-lib/{addFormApi-Bly_wUfr.js.map → addFormApi-DBeQrNX1.js.map} +1 -1
  11. package/dist-lib/{addFormApi-CKdtiVhh.cjs → addFormApi-DLfsOijp.cjs} +2 -2
  12. package/dist-lib/{addFormApi-CKdtiVhh.cjs.map → addFormApi-DLfsOijp.cjs.map} +1 -1
  13. package/dist-lib/authApi-DiRtkJl2.cjs +2 -0
  14. package/dist-lib/authApi-DiRtkJl2.cjs.map +1 -0
  15. package/dist-lib/{authApi-Flvm7WKc.js → authApi-Dsc4NnFr.js} +88 -86
  16. package/dist-lib/authApi-Dsc4NnFr.js.map +1 -0
  17. package/dist-lib/index.cjs +3 -3
  18. package/dist-lib/index.cjs.map +1 -1
  19. package/dist-lib/index.js +39 -7
  20. package/dist-lib/index.js.map +1 -1
  21. package/package.json +1 -1
  22. package/dist-lib/authApi-B9mjMdVC.cjs +0 -2
  23. package/dist-lib/authApi-B9mjMdVC.cjs.map +0 -1
  24. package/dist-lib/authApi-Flvm7WKc.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./rolldown-runtime-Dy4uBu1J.js";
2
- import { a as t, x as n } from "./authApi-Flvm7WKc.js";
2
+ import { a as t, x as n } from "./authApi-Dsc4NnFr.js";
3
3
  import { useCallback as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
4
4
  import { Modal as c, Spin as l, Tooltip as u, message as d } from "antd";
5
5
  import { CloseOutlined as f, DownloadOutlined as p, FileUnknownOutlined as m, LeftOutlined as ee, RightOutlined as te, ZoomInOutlined as h, ZoomOutOutlined as g } from "@ant-design/icons";
@@ -568,4 +568,4 @@ $.propTypes = {
568
568
  //#endregion
569
569
  export { j as n, $ as t };
570
570
 
571
- //# sourceMappingURL=DocumentViewer-DesrL_6B.js.map
571
+ //# sourceMappingURL=DocumentViewer-DSxsilzL.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DocumentViewer-DesrL_6B.js","names":[],"sources":["../src/services/documentContentApi.js","../src/components/DocumentViewer.jsx"],"sourcesContent":["import { ensureToken } from './authApi';\nimport { AUTH_URL } from './apiConfig';\n\n// ── Authenticated document content ──────────────────────────────────────────\n//\n// WHY THIS EXISTS\n//\n// Documents live in a PRIVATE S3 bucket. The app was building a raw URL from\n// the stored object key:\n//\n// s3BaseUrl + \"candidate/resume/resume_<id>_<ts>.pdf\"\n//\n// That URL carries no signature, so S3 correctly answers 403 and no resume\n// could be opened. Making the bucket public is not an option.\n//\n// A presigned URL is necessary but NOT sufficient: react-pdf and docx-preview\n// fetch the bytes themselves, and the bucket's CORS policy does not allowlist\n// our origins, so the fetch is blocked even with a valid signature. The\n// backend therefore streams the bytes through our own origin, and we turn them\n// into a blob: URL the viewer can render.\n//\n// THE AUTHORIZATION RULE: this module never sends an S3 key. It names a RECORD\n// and lets the server resolve the key. Sending a key would let any user read\n// any object in the bucket.\n\n/** Error carrying the HTTP status, so callers can distinguish 403 from 404. */\nexport class DocumentAccessError extends Error {\n constructor(message, status) {\n super(message);\n this.name = 'DocumentAccessError';\n this.status = status;\n }\n}\n\nfunction messageForStatus(status, serverMessage) {\n // Distinct messages per status. \"Could not load document\" for all four sends\n // a user to the wrong remedy: a permissions problem and a deleted file need\n // completely different actions.\n switch (status) {\n case 400: return serverMessage || 'This document request was not valid.';\n case 401: return 'Your session has expired. Please sign in again.';\n case 403: return 'You do not have permission to view this document.';\n case 404: return serverMessage || 'This document is no longer available.';\n case 502:\n case 503: return 'Document storage is temporarily unavailable. Please try again.';\n case 504: return 'The document took too long to load. Please try again.';\n default: return serverMessage || 'This document could not be loaded.';\n }\n}\n\n/**\n * fetchDocumentBlob — retrieves a document's bytes through the authenticated\n * backend and returns a Blob.\n *\n * Identify the document by EITHER:\n * { documentId }\n * { module, refId, fieldName }\n *\n * @returns {Promise<{ blob: Blob, contentType: string, fileName: string }>}\n * @throws {DocumentAccessError}\n */\nexport async function fetchDocumentBlob({ documentId, module, refId, fieldName, index } = {}) {\n const params = new URLSearchParams();\n if (documentId) {\n params.set('documentId', String(documentId));\n } else if (module && refId && fieldName) {\n params.set('module', String(module));\n params.set('refId', String(refId));\n params.set('fieldName', String(fieldName));\n // One row of an array of documents stored on the record (REQ-20260911-01 R4).\n if (Number.isInteger(index) && index >= 0) params.set('index', String(index));\n } else {\n throw new DocumentAccessError(\n 'A documentId, or module + refId + fieldName, is required to load a document.',\n 400,\n );\n }\n\n const token = await ensureToken();\n const res = await fetch(`${AUTH_URL}/documents/content?${params.toString()}`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (!res.ok) {\n // The error body is JSON even though a success is binary.\n let serverMessage = '';\n try {\n const body = await res.json();\n serverMessage = body?.error || body?.message || '';\n } catch {\n // A non-JSON error body is not worth surfacing raw.\n }\n throw new DocumentAccessError(messageForStatus(res.status, serverMessage), res.status);\n }\n\n const blob = await res.blob();\n return {\n blob,\n contentType: res.headers.get('content-type') || blob.type || '',\n fileName: fileNameFromContentDisposition(res.headers.get('content-disposition')),\n };\n}\n\n/**\n * createDocumentObjectUrl — fetches a document and wraps it in a blob: URL.\n *\n * THE CALLER MUST REVOKE the returned url (URL.revokeObjectURL) when the\n * viewer closes, the source changes, or the component unmounts. A blob URL\n * pins its bytes in memory for the lifetime of the document, so an unrevoked\n * multi-megabyte resume is a real leak — and a list view that resolved one per\n * row would exhaust memory. Resolve lazily, on open, never on render.\n *\n * @returns {Promise<{ url: string, revoke: () => void, contentType: string, fileName: string }>}\n */\nexport async function createDocumentObjectUrl(source) {\n const { blob, contentType, fileName } = await fetchDocumentBlob(source);\n const url = URL.createObjectURL(blob);\n let revoked = false;\n return {\n url,\n contentType,\n fileName,\n // Idempotent: revoking twice is harmless, but a guard makes double-revoke\n // in a StrictMode double-invoked effect provably safe.\n revoke: () => {\n if (revoked) return;\n revoked = true;\n URL.revokeObjectURL(url);\n },\n };\n}\n\nfunction fileNameFromContentDisposition(header) {\n if (!header) return '';\n const match = /filename\\*?=(?:UTF-8'')?\"?([^\";]+)\"?/i.exec(header);\n if (!match) return '';\n try {\n return decodeURIComponent(match[1]);\n } catch {\n return match[1];\n }\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { createDocumentObjectUrl } from '../services/documentContentApi';\nimport PropTypes from 'prop-types';\nimport { Modal, Spin, Tooltip, message } from 'antd';\nimport {\n CloseOutlined,\n DownloadOutlined,\n FileUnknownOutlined,\n LeftOutlined,\n RightOutlined,\n ZoomInOutlined,\n ZoomOutOutlined,\n} from '@ant-design/icons';\nimport { Document, Page, pdfjs } from 'react-pdf';\nimport { renderAsync } from 'docx-preview';\nimport DOMPurify from 'dompurify';\nimport 'react-pdf/dist/Page/AnnotationLayer.css';\nimport 'react-pdf/dist/Page/TextLayer.css';\nimport '../styles/DocumentViewer.css';\n\n// pdf.js needs a web worker; Vite resolves this URL at build time so it works\n// in dev and production without copying files into /public.\npdfjs.GlobalWorkerOptions.workerSrc = new URL(\n 'pdfjs-dist/build/pdf.worker.min.mjs',\n import.meta.url,\n).toString();\n\nconst IMAGE_EXT = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg', 'avif', 'ico'];\nconst TEXT_EXT = ['txt', 'csv', 'log', 'json', 'md', 'xml'];\nconst VIDEO_EXT = ['mp4', 'webm', 'ogg', 'ogv', 'mov', 'm4v'];\nconst ZOOM_STEP = 0.2;\nconst ZOOM_MIN = 0.4;\nconst ZOOM_MAX = 3;\n\nfunction fileNameFromUrl(url) {\n if (!url) return 'Document';\n const clean = String(url).split('?')[0].split('#')[0];\n return decodeURIComponent(clean.split('/').pop() || clean) || 'Document';\n}\n\nfunction extOf(nameOrUrl) {\n const clean = String(nameOrUrl || '').split('?')[0].split('#')[0];\n const dot = clean.lastIndexOf('.');\n return dot === -1 ? '' : clean.slice(dot + 1).toLowerCase();\n}\n\nconst KNOWN_BUCKETS = ['pdf', 'docx', 'image', 'text', 'video', 'html'];\n\n// bucketFromExt maps a bare extension (no dot) to a renderer bucket, or null.\nfunction bucketFromExt(ext) {\n if (!ext) return null;\n if (ext === 'pdf') return 'pdf';\n if (ext === 'doc' || ext === 'docx') return 'docx';\n if (IMAGE_EXT.includes(ext)) return 'image';\n if (TEXT_EXT.includes(ext)) return 'text';\n if (VIDEO_EXT.includes(ext)) return 'video';\n return null;\n}\n\n// kindOf maps a document to a renderer bucket: pdf | docx | image | text |\n// video | html | unknown.\n//\n// `doc.type` is normalized rather than trusted verbatim, because it arrives\n// in different shapes depending on the caller: an already-correct bucket\n// name (\"image\", inline html), a bare file extension as the backend upload\n// handler stores it (\"jpg\", \"png\", \"pdf\" — see timesheetController.go's\n// `ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(...), \".\"))`), or a\n// MIME type (\"image/jpeg\"). Returning a bare extension straight through only\n// happened to work for \"pdf\"/\"docx\" because those extensions equal their own\n// bucket name — every image extension (\"jpg\", \"png\", …) doesn't equal\n// \"image\", so it silently fell through to the \"no preview available\"\n// fallback. This maps all three shapes through the same extension table.\nfunction kindOf(doc) {\n const rawType = String(doc.type || '').toLowerCase().trim();\n if (KNOWN_BUCKETS.includes(rawType)) return rawType;\n\n const mimeExt = rawType.includes('/') ? rawType.split('/').pop() : rawType;\n const fromType = bucketFromExt(mimeExt);\n if (fromType) return fromType;\n\n const fromName = bucketFromExt(extOf(doc.name || doc.url));\n if (fromName) return fromName;\n\n return 'unknown';\n}\n\n// normalizeDocs accepts either bare URL strings, file objects, or inline\n// document objects ({ name, type: 'text'|'html', content }).\nfunction normalizeDocs(documents) {\n return (documents || [])\n .map((d, i) => {\n if (typeof d === 'string') {\n return { url: d, name: fileNameFromUrl(d), key: String(i) };\n }\n // `d.location` is deliberately NOT a url fallback any more. It holds a\n // PRIVATE S3 object key, and treating it as a url produced an unsigned\n // request that the bucket answered with 403 — the whole reason resumes\n // would not open. A doc with only a key must carry `authSource` so the\n // bytes are fetched through the authenticated backend instead.\n const url = d.url || d.path || '';\n return {\n url,\n name: d.name || fileNameFromUrl(url || d.location || ''),\n type: d.type,\n content: typeof d.content === 'string' ? d.content : '',\n authSource: d.authSource || null,\n key: String(d.id ?? d._id ?? i),\n };\n })\n .filter((d) => d.url || d.content || d.authSource);\n}\n\n/* ------------------------------- renderers ------------------------------- */\n\nfunction PdfRenderer({ url, scale, onNativeFallback }) {\n const wrapRef = useRef(null);\n const [numPages, setNumPages] = useState(0);\n const [width, setWidth] = useState(0);\n const [error, setError] = useState(false);\n\n useEffect(() => {\n const el = wrapRef.current;\n if (!el) return undefined;\n const update = () => setWidth(el.clientWidth);\n update();\n const ro = new ResizeObserver(update);\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n // pdf.js streams the file via its own fetch, which fails cross-origin on\n // storage URLs the bucket's CORS policy doesn't allowlist this origin for\n // (common — see the S3 bucket's CORS config, not something fixable here) —\n // distinct from a plain navigation (an <iframe> load), which is NOT subject\n // to CORS at all and works regardless. Fall back to that rather than\n // dead-ending the preview; the native viewer brings its own zoom/controls,\n // so the toolbar hides its (now inert) zoom buttons via onNativeFallback.\n //\n // This intentionally points the iframe at the raw presigned `url`, not a\n // fetched blob — fetching it would hit the same CORS wall react-pdf just\n // did. Whether this renders inline vs. downloads depends entirely on the\n // `Content-Disposition` the presigned URL responds with; that's set\n // server-side (GetPresignedURLInline in Be_Auth_DevOps) rather than worked\n // around here, so every consumer of the URL — this iframe, a plain link,\n // anything — gets the same correct inline behavior.\n if (error) {\n return <iframe title=\"PDF preview\" src={url} className=\"dv-pdf-native\" />;\n }\n\n return (\n <div ref={wrapRef} className=\"dv-pdf-wrap\">\n <Document\n file={url}\n loading={<Spin />}\n error={<FallbackStage label=\"This PDF could not be displayed.\" />}\n onLoadSuccess={({ numPages: n }) => setNumPages(n)}\n onLoadError={() => {\n setError(true);\n onNativeFallback?.();\n }}\n >\n {Array.from({ length: numPages }, (_, i) => (\n <Page\n key={`page-${i + 1}`}\n pageNumber={i + 1}\n width={width ? width * scale : undefined}\n className=\"dv-pdf-page\"\n renderTextLayer\n renderAnnotationLayer\n />\n ))}\n </Document>\n </div>\n );\n}\n\nPdfRenderer.propTypes = {\n url: PropTypes.string.isRequired,\n scale: PropTypes.number.isRequired,\n onNativeFallback: PropTypes.func,\n};\n\nfunction DocxRenderer({ url }) {\n const ref = useRef(null);\n const [status, setStatus] = useState('loading'); // loading | ready | error\n\n useEffect(() => {\n let cancelled = false;\n\n fetch(url)\n .then((r) => {\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n return r.blob();\n })\n .then((blob) => {\n if (cancelled || !ref.current) return undefined;\n ref.current.innerHTML = '';\n return renderAsync(blob, ref.current, undefined, {\n className: 'dv-docx',\n inWrapper: true,\n ignoreWidth: false,\n ignoreHeight: false,\n });\n })\n .then(() => {\n if (!cancelled) setStatus('ready');\n })\n .catch(() => {\n if (!cancelled) setStatus('error');\n });\n\n return () => {\n cancelled = true;\n };\n }, [url]);\n\n if (status === 'error') {\n return <FallbackStage label=\"This document could not be displayed.\" />;\n }\n\n return (\n <div className=\"dv-docx-scroll\">\n {status === 'loading' && <div className=\"dv-center\"><Spin /></div>}\n <div ref={ref} style={{ visibility: status === 'ready' ? 'visible' : 'hidden' }} />\n </div>\n );\n}\n\nDocxRenderer.propTypes = { url: PropTypes.string.isRequired };\n\nfunction TextRenderer({ url, content = '' }) {\n const [text, setText] = useState(null);\n const [status, setStatus] = useState('loading');\n\n useEffect(() => {\n if (content) return undefined;\n\n let cancelled = false;\n fetch(url)\n .then((r) => {\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n return r.text();\n })\n .then((t) => {\n if (!cancelled) {\n setText(t);\n setStatus('ready');\n }\n })\n .catch(() => !cancelled && setStatus('error'));\n return () => {\n cancelled = true;\n };\n }, [content, url]);\n\n if (content) return <pre className=\"dv-text\">{content}</pre>;\n if (status === 'loading') return <div className=\"dv-center\"><Spin /></div>;\n if (status === 'error') return <FallbackStage label=\"This file could not be displayed.\" />;\n return <pre className=\"dv-text\">{text}</pre>;\n}\n\nTextRenderer.propTypes = {\n url: PropTypes.string,\n content: PropTypes.string,\n};\n\nfunction HtmlRenderer({ content }) {\n const safeHtml = useMemo(() => DOMPurify.sanitize(content), [content]);\n return <div className=\"dv-html\" dangerouslySetInnerHTML={{ __html: safeHtml }} />;\n}\n\nHtmlRenderer.propTypes = { content: PropTypes.string.isRequired };\n\nfunction ImageRenderer({ url, name, scale }) {\n const [error, setError] = useState(false);\n if (error) return <FallbackStage label=\"This image could not be displayed.\" />;\n return (\n <div className=\"dv-image-wrap\">\n <img\n className=\"dv-image\"\n src={url}\n alt={name}\n style={{ transform: `scale(${scale})` }}\n onError={() => setError(true)}\n />\n </div>\n );\n}\n\nImageRenderer.propTypes = {\n url: PropTypes.string.isRequired,\n name: PropTypes.string.isRequired,\n scale: PropTypes.number.isRequired,\n};\n\nfunction VideoRenderer({ url, name }) {\n const [error, setError] = useState(false);\n if (error) return <FallbackStage label=\"This video could not be played.\" />;\n return (\n <div className=\"dv-video-wrap\">\n <video\n className=\"dv-video\"\n src={url}\n title={name}\n controls\n preload=\"metadata\"\n controlsList=\"nodownload\"\n onError={() => setError(true)}\n />\n </div>\n );\n}\n\nVideoRenderer.propTypes = {\n url: PropTypes.string.isRequired,\n name: PropTypes.string.isRequired,\n};\n\nfunction FallbackStage({ label }) {\n return (\n <div className=\"dv-center dv-fallback\">\n <FileUnknownOutlined className=\"dv-fallback-icon\" />\n <p>{label}</p>\n <span className=\"dv-fallback-hint\">Use the download button to open it.</span>\n </div>\n );\n}\n\nFallbackStage.propTypes = { label: PropTypes.string.isRequired };\n\n/* ----------------------------- main component ---------------------------- */\n\n/**\n * useAuthenticatedDocument — turns a doc carrying `authSource` into one\n * carrying a usable blob: `url`.\n *\n * Returns { doc, loading, error }. A doc that already has a url passes through\n * untouched, so this costs nothing for inline content and public links.\n *\n * REVOCATION is the point of the cleanup function: a blob URL keeps its bytes\n * alive until revoked, so a multi-megabyte resume leaks for the lifetime of\n * the page if this is skipped. The effect revokes on document change, on\n * viewer close (ViewerBody unmounts — the Modal uses destroyOnHidden), and on\n * a superseded in-flight fetch.\n */\nfunction useAuthenticatedDocument(doc) {\n // State carries the key it belongs to, so a stale result is detectable\n // during render. That removes the need to reset via setState inside the\n // effect body, which causes a cascading render (and is a lint error).\n const [state, setState] = useState({ key: '', url: '', error: '' });\n\n const source = doc?.authSource;\n // Stringified so the effect compares by VALUE. An object identity dependency\n // would refetch on every parent render and leak one blob per render.\n const sourceKey = source ? JSON.stringify(source) : '';\n const alreadyUsable = Boolean(doc?.url || doc?.content);\n const needsFetch = Boolean(sourceKey) && !alreadyUsable;\n\n useEffect(() => {\n if (!needsFetch) return undefined;\n\n let cancelled = false;\n let revokeFn = null;\n\n createDocumentObjectUrl(JSON.parse(sourceKey))\n .then(({ url, revoke }) => {\n revokeFn = revoke;\n // Losing the race means the user already moved on: revoke immediately\n // rather than handing back a URL nothing will ever revoke.\n if (cancelled) {\n revoke();\n return;\n }\n setState({ key: sourceKey, url, error: '' });\n })\n .catch((error) => {\n if (cancelled) return;\n setState({\n key: sourceKey,\n url: '',\n error: error?.message || 'This document could not be loaded.',\n });\n });\n\n return () => {\n cancelled = true;\n if (revokeFn) revokeFn();\n };\n }, [needsFetch, sourceKey]);\n\n if (!doc) return { doc: null, loading: false, error: '' };\n if (!needsFetch) return { doc, loading: false, error: '' };\n\n // A result from a PREVIOUS document must never be shown against this one.\n const settled = state.key === sourceKey;\n if (!settled) return { doc, loading: true, error: '' };\n if (state.error) return { doc, loading: false, error: state.error };\n return { doc: state.url ? { ...doc, url: state.url } : doc, loading: false, error: '' };\n}\n\n// ViewerBody holds the per-session state (active index + zoom). It lives inside\n// the Modal body, which is destroyed on close (destroyOnHidden), so it remounts\n// fresh on every open — no manual \"reset on open\" effects required.\nfunction ViewerBody({ docs, initialIndex, onClose }) {\n const total = docs.length;\n const safeInitial = Math.min(Math.max(initialIndex, 0), Math.max(0, total - 1));\n const [index, setIndex] = useState(safeInitial);\n const [scale, setScale] = useState(1);\n // Set when a PDF falls back to the browser's native viewer (see\n // PdfRenderer) — that viewer has its own zoom UI, so ours would sit there\n // doing nothing if left visible.\n const [pdfNativeFallback, setPdfNativeFallback] = useState(false);\n\n // Resolve the ACTIVE document's bytes through the authenticated backend when\n // all we hold is a private-bucket object key.\n //\n // Lazy by design — only the document actually being viewed is fetched. A\n // blob: URL pins its bytes in memory until revoked, so resolving every\n // document in the list up front would hold a whole attachment set resident\n // for as long as the viewer is open.\n const resolved = useAuthenticatedDocument(docs[index]);\n const current = resolved.doc;\n const kind = current ? kindOf(current) : 'unknown';\n const zoomable = (kind === 'pdf' && !pdfNativeFallback) || kind === 'image';\n\n // setActive changes the document and resets zoom/fallback in one event\n // handler, so we never have to reset them from an effect.\n const setActive = useCallback((next) => {\n setIndex(next);\n setScale(1);\n setPdfNativeFallback(false);\n }, []);\n\n const goPrev = useCallback(() => setActive(Math.max(0, index - 1)), [index, setActive]);\n const goNext = useCallback(\n () => setActive(Math.min(total - 1, index + 1)),\n [index, total, setActive],\n );\n\n // Keyboard navigation.\n useEffect(() => {\n const onKey = (e) => {\n // A focused <video> uses arrow keys to seek — don't switch documents.\n if (e.target?.tagName === 'VIDEO') return;\n if (e.key === 'ArrowLeft') goPrev();\n else if (e.key === 'ArrowRight') goNext();\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [goPrev, goNext]);\n\n const download = useCallback(async (doc) => {\n if (!doc) return;\n try {\n let blob;\n if (doc.content) {\n const mimeType = doc.type === 'html' ? 'text/html;charset=utf-8' : 'text/plain;charset=utf-8';\n blob = new Blob([doc.content], { type: mimeType });\n } else {\n const res = await fetch(doc.url);\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n blob = await res.blob();\n }\n const objUrl = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = objUrl;\n a.download = doc.name || 'document';\n document.body.appendChild(a);\n a.click();\n a.remove();\n URL.revokeObjectURL(objUrl);\n } catch {\n // Cross-origin without CORS: fall back to opening in a new tab.\n message.info('Opening file in a new tab…');\n if (doc.url) window.open(doc.url, '_blank', 'noopener,noreferrer');\n }\n }, []);\n\n function renderStage() {\n if (!current) {\n return <FallbackStage label=\"No document to preview.\" />;\n }\n // States are distinct on purpose. One generic \"could not load\" message\n // sends the user to the wrong remedy — \"you lack permission\", \"the file is\n // gone\" and \"storage is briefly down\" need three different actions, and\n // the backend already classifies them (403 / 404 / 502).\n if (resolved.error) {\n return <FallbackStage label={resolved.error} />;\n }\n if (resolved.loading) {\n return <FallbackStage label=\"Loading document…\" />;\n }\n switch (kind) {\n case 'pdf':\n return (\n <PdfRenderer\n key={current.key}\n url={current.url}\n scale={scale}\n onNativeFallback={() => setPdfNativeFallback(true)}\n />\n );\n case 'docx':\n return <DocxRenderer key={current.key} url={current.url} />;\n case 'image':\n return (\n <ImageRenderer key={current.key} url={current.url} name={current.name} scale={scale} />\n );\n case 'text':\n return <TextRenderer key={current.key} url={current.url} content={current.content} />;\n case 'html':\n return <HtmlRenderer key={current.key} content={current.content} />;\n case 'video':\n return <VideoRenderer key={current.key} url={current.url} name={current.name} />;\n default:\n return <FallbackStage label=\"Preview is not available for this file type.\" />;\n }\n }\n\n return (\n <div className=\"dv-root\">\n {/* toolbar */}\n <div className=\"dv-toolbar\">\n <div className=\"dv-title\" title={current?.name}>\n <span className=\"dv-title-text\">{current?.name || 'Document'}</span>\n {total > 1 && <span className=\"dv-counter\">{index + 1} / {total}</span>}\n </div>\n <div className=\"dv-actions\">\n {zoomable && (\n <>\n <Tooltip title=\"Zoom out\">\n <button\n type=\"button\"\n className=\"dv-icon-btn\"\n onClick={() => setScale((s) => Math.max(ZOOM_MIN, +(s - ZOOM_STEP).toFixed(2)))}\n disabled={scale <= ZOOM_MIN}\n >\n <ZoomOutOutlined />\n </button>\n </Tooltip>\n <span className=\"dv-zoom-label\">{Math.round(scale * 100)}%</span>\n <Tooltip title=\"Zoom in\">\n <button\n type=\"button\"\n className=\"dv-icon-btn\"\n onClick={() => setScale((s) => Math.min(ZOOM_MAX, +(s + ZOOM_STEP).toFixed(2)))}\n disabled={scale >= ZOOM_MAX}\n >\n <ZoomInOutlined />\n </button>\n </Tooltip>\n <span className=\"dv-divider\" />\n </>\n )}\n <Tooltip title=\"Download\">\n <button type=\"button\" className=\"dv-icon-btn dv-download\" onClick={() => download(current)}>\n <DownloadOutlined />\n </button>\n </Tooltip>\n <Tooltip title=\"Close\">\n <button type=\"button\" className=\"dv-icon-btn\" onClick={onClose}>\n <CloseOutlined />\n </button>\n </Tooltip>\n </div>\n </div>\n\n {/* stage + slider arrows */}\n <div className=\"dv-stage\">\n {total > 1 && (\n <button\n type=\"button\"\n className=\"dv-nav dv-nav--prev\"\n onClick={goPrev}\n disabled={index === 0}\n aria-label=\"Previous document\"\n >\n <LeftOutlined />\n </button>\n )}\n\n <div className=\"dv-canvas\">{renderStage()}</div>\n\n {total > 1 && (\n <button\n type=\"button\"\n className=\"dv-nav dv-nav--next\"\n onClick={goNext}\n disabled={index === total - 1}\n aria-label=\"Next document\"\n >\n <RightOutlined />\n </button>\n )}\n </div>\n\n {/* dots */}\n {total > 1 && (\n <div className=\"dv-dots\">\n {docs.map((d, i) => (\n <button\n key={d.key}\n type=\"button\"\n className={`dv-dot${i === index ? ' dv-dot--active' : ''}`}\n onClick={() => setActive(i)}\n aria-label={`Go to document ${i + 1}`}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nViewerBody.propTypes = {\n docs: PropTypes.arrayOf(PropTypes.object).isRequired,\n initialIndex: PropTypes.number.isRequired,\n onClose: PropTypes.func.isRequired,\n};\n\n// DocumentViewer is the public component: a Modal shell that mounts ViewerBody\n// only while open. `documents` may be URL strings or document objects.\n//\n// Pass `inline` to render ViewerBody directly with no Modal — used by the timesheet\n// manager review, which shows the uploaded screenshot side-by-side with the calendar\n// rather than in a popup. Everything ViewerBody already does (pdf/image/docx render,\n// zoom, download, multi-document navigation) comes along for free.\nexport default function DocumentViewer({ documents, open, onClose, initialIndex = 0, inline = false }) {\n const docs = useMemo(() => normalizeDocs(documents), [documents]);\n\n if (inline) {\n return (\n <div className=\"dv-root--inline\">\n <ViewerBody docs={docs} initialIndex={initialIndex} onClose={onClose ?? (() => {})} />\n </div>\n );\n }\n\n return (\n <Modal\n open={open}\n onCancel={onClose}\n footer={null}\n title={null}\n closable={false}\n centered\n width=\"min(1100px, 94vw)\"\n className=\"dv-modal\"\n styles={{ content: { padding: 0, overflow: 'hidden', borderRadius: 14 }, body: { padding: 0 } }}\n destroyOnHidden\n >\n <ViewerBody docs={docs} initialIndex={initialIndex} onClose={onClose} />\n </Modal>\n );\n}\n\nDocumentViewer.propTypes = {\n // URL strings, file objects, or inline objects ({ content, type: 'text'|'html' }).\n documents: PropTypes.arrayOf(\n PropTypes.oneOfType([PropTypes.string, PropTypes.object]),\n ).isRequired,\n // Required for the Modal shell; unused (and optional) when `inline` is set.\n open: PropTypes.bool,\n onClose: PropTypes.func,\n initialIndex: PropTypes.number,\n // Render the viewer body directly, with no Modal wrapper.\n inline: PropTypes.bool,\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"DocumentViewer-DSxsilzL.js","names":[],"sources":["../src/services/documentContentApi.js","../src/components/DocumentViewer.jsx"],"sourcesContent":["import { ensureToken } from './authApi';\nimport { AUTH_URL } from './apiConfig';\n\n// ── Authenticated document content ──────────────────────────────────────────\n//\n// WHY THIS EXISTS\n//\n// Documents live in a PRIVATE S3 bucket. The app was building a raw URL from\n// the stored object key:\n//\n// s3BaseUrl + \"candidate/resume/resume_<id>_<ts>.pdf\"\n//\n// That URL carries no signature, so S3 correctly answers 403 and no resume\n// could be opened. Making the bucket public is not an option.\n//\n// A presigned URL is necessary but NOT sufficient: react-pdf and docx-preview\n// fetch the bytes themselves, and the bucket's CORS policy does not allowlist\n// our origins, so the fetch is blocked even with a valid signature. The\n// backend therefore streams the bytes through our own origin, and we turn them\n// into a blob: URL the viewer can render.\n//\n// THE AUTHORIZATION RULE: this module never sends an S3 key. It names a RECORD\n// and lets the server resolve the key. Sending a key would let any user read\n// any object in the bucket.\n\n/** Error carrying the HTTP status, so callers can distinguish 403 from 404. */\nexport class DocumentAccessError extends Error {\n constructor(message, status) {\n super(message);\n this.name = 'DocumentAccessError';\n this.status = status;\n }\n}\n\nfunction messageForStatus(status, serverMessage) {\n // Distinct messages per status. \"Could not load document\" for all four sends\n // a user to the wrong remedy: a permissions problem and a deleted file need\n // completely different actions.\n switch (status) {\n case 400: return serverMessage || 'This document request was not valid.';\n case 401: return 'Your session has expired. Please sign in again.';\n case 403: return 'You do not have permission to view this document.';\n case 404: return serverMessage || 'This document is no longer available.';\n case 502:\n case 503: return 'Document storage is temporarily unavailable. Please try again.';\n case 504: return 'The document took too long to load. Please try again.';\n default: return serverMessage || 'This document could not be loaded.';\n }\n}\n\n/**\n * fetchDocumentBlob — retrieves a document's bytes through the authenticated\n * backend and returns a Blob.\n *\n * Identify the document by EITHER:\n * { documentId }\n * { module, refId, fieldName }\n *\n * @returns {Promise<{ blob: Blob, contentType: string, fileName: string }>}\n * @throws {DocumentAccessError}\n */\nexport async function fetchDocumentBlob({ documentId, module, refId, fieldName, index } = {}) {\n const params = new URLSearchParams();\n if (documentId) {\n params.set('documentId', String(documentId));\n } else if (module && refId && fieldName) {\n params.set('module', String(module));\n params.set('refId', String(refId));\n params.set('fieldName', String(fieldName));\n // One row of an array of documents stored on the record (REQ-20260911-01 R4).\n if (Number.isInteger(index) && index >= 0) params.set('index', String(index));\n } else {\n throw new DocumentAccessError(\n 'A documentId, or module + refId + fieldName, is required to load a document.',\n 400,\n );\n }\n\n const token = await ensureToken();\n const res = await fetch(`${AUTH_URL}/documents/content?${params.toString()}`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (!res.ok) {\n // The error body is JSON even though a success is binary.\n let serverMessage = '';\n try {\n const body = await res.json();\n serverMessage = body?.error || body?.message || '';\n } catch {\n // A non-JSON error body is not worth surfacing raw.\n }\n throw new DocumentAccessError(messageForStatus(res.status, serverMessage), res.status);\n }\n\n const blob = await res.blob();\n return {\n blob,\n contentType: res.headers.get('content-type') || blob.type || '',\n fileName: fileNameFromContentDisposition(res.headers.get('content-disposition')),\n };\n}\n\n/**\n * createDocumentObjectUrl — fetches a document and wraps it in a blob: URL.\n *\n * THE CALLER MUST REVOKE the returned url (URL.revokeObjectURL) when the\n * viewer closes, the source changes, or the component unmounts. A blob URL\n * pins its bytes in memory for the lifetime of the document, so an unrevoked\n * multi-megabyte resume is a real leak — and a list view that resolved one per\n * row would exhaust memory. Resolve lazily, on open, never on render.\n *\n * @returns {Promise<{ url: string, revoke: () => void, contentType: string, fileName: string }>}\n */\nexport async function createDocumentObjectUrl(source) {\n const { blob, contentType, fileName } = await fetchDocumentBlob(source);\n const url = URL.createObjectURL(blob);\n let revoked = false;\n return {\n url,\n contentType,\n fileName,\n // Idempotent: revoking twice is harmless, but a guard makes double-revoke\n // in a StrictMode double-invoked effect provably safe.\n revoke: () => {\n if (revoked) return;\n revoked = true;\n URL.revokeObjectURL(url);\n },\n };\n}\n\nfunction fileNameFromContentDisposition(header) {\n if (!header) return '';\n const match = /filename\\*?=(?:UTF-8'')?\"?([^\";]+)\"?/i.exec(header);\n if (!match) return '';\n try {\n return decodeURIComponent(match[1]);\n } catch {\n return match[1];\n }\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { createDocumentObjectUrl } from '../services/documentContentApi';\nimport PropTypes from 'prop-types';\nimport { Modal, Spin, Tooltip, message } from 'antd';\nimport {\n CloseOutlined,\n DownloadOutlined,\n FileUnknownOutlined,\n LeftOutlined,\n RightOutlined,\n ZoomInOutlined,\n ZoomOutOutlined,\n} from '@ant-design/icons';\nimport { Document, Page, pdfjs } from 'react-pdf';\nimport { renderAsync } from 'docx-preview';\nimport DOMPurify from 'dompurify';\nimport 'react-pdf/dist/Page/AnnotationLayer.css';\nimport 'react-pdf/dist/Page/TextLayer.css';\nimport '../styles/DocumentViewer.css';\n\n// pdf.js needs a web worker; Vite resolves this URL at build time so it works\n// in dev and production without copying files into /public.\npdfjs.GlobalWorkerOptions.workerSrc = new URL(\n 'pdfjs-dist/build/pdf.worker.min.mjs',\n import.meta.url,\n).toString();\n\nconst IMAGE_EXT = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg', 'avif', 'ico'];\nconst TEXT_EXT = ['txt', 'csv', 'log', 'json', 'md', 'xml'];\nconst VIDEO_EXT = ['mp4', 'webm', 'ogg', 'ogv', 'mov', 'm4v'];\nconst ZOOM_STEP = 0.2;\nconst ZOOM_MIN = 0.4;\nconst ZOOM_MAX = 3;\n\nfunction fileNameFromUrl(url) {\n if (!url) return 'Document';\n const clean = String(url).split('?')[0].split('#')[0];\n return decodeURIComponent(clean.split('/').pop() || clean) || 'Document';\n}\n\nfunction extOf(nameOrUrl) {\n const clean = String(nameOrUrl || '').split('?')[0].split('#')[0];\n const dot = clean.lastIndexOf('.');\n return dot === -1 ? '' : clean.slice(dot + 1).toLowerCase();\n}\n\nconst KNOWN_BUCKETS = ['pdf', 'docx', 'image', 'text', 'video', 'html'];\n\n// bucketFromExt maps a bare extension (no dot) to a renderer bucket, or null.\nfunction bucketFromExt(ext) {\n if (!ext) return null;\n if (ext === 'pdf') return 'pdf';\n if (ext === 'doc' || ext === 'docx') return 'docx';\n if (IMAGE_EXT.includes(ext)) return 'image';\n if (TEXT_EXT.includes(ext)) return 'text';\n if (VIDEO_EXT.includes(ext)) return 'video';\n return null;\n}\n\n// kindOf maps a document to a renderer bucket: pdf | docx | image | text |\n// video | html | unknown.\n//\n// `doc.type` is normalized rather than trusted verbatim, because it arrives\n// in different shapes depending on the caller: an already-correct bucket\n// name (\"image\", inline html), a bare file extension as the backend upload\n// handler stores it (\"jpg\", \"png\", \"pdf\" — see timesheetController.go's\n// `ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(...), \".\"))`), or a\n// MIME type (\"image/jpeg\"). Returning a bare extension straight through only\n// happened to work for \"pdf\"/\"docx\" because those extensions equal their own\n// bucket name — every image extension (\"jpg\", \"png\", …) doesn't equal\n// \"image\", so it silently fell through to the \"no preview available\"\n// fallback. This maps all three shapes through the same extension table.\nfunction kindOf(doc) {\n const rawType = String(doc.type || '').toLowerCase().trim();\n if (KNOWN_BUCKETS.includes(rawType)) return rawType;\n\n const mimeExt = rawType.includes('/') ? rawType.split('/').pop() : rawType;\n const fromType = bucketFromExt(mimeExt);\n if (fromType) return fromType;\n\n const fromName = bucketFromExt(extOf(doc.name || doc.url));\n if (fromName) return fromName;\n\n return 'unknown';\n}\n\n// normalizeDocs accepts either bare URL strings, file objects, or inline\n// document objects ({ name, type: 'text'|'html', content }).\nfunction normalizeDocs(documents) {\n return (documents || [])\n .map((d, i) => {\n if (typeof d === 'string') {\n return { url: d, name: fileNameFromUrl(d), key: String(i) };\n }\n // `d.location` is deliberately NOT a url fallback any more. It holds a\n // PRIVATE S3 object key, and treating it as a url produced an unsigned\n // request that the bucket answered with 403 — the whole reason resumes\n // would not open. A doc with only a key must carry `authSource` so the\n // bytes are fetched through the authenticated backend instead.\n const url = d.url || d.path || '';\n return {\n url,\n name: d.name || fileNameFromUrl(url || d.location || ''),\n type: d.type,\n content: typeof d.content === 'string' ? d.content : '',\n authSource: d.authSource || null,\n key: String(d.id ?? d._id ?? i),\n };\n })\n .filter((d) => d.url || d.content || d.authSource);\n}\n\n/* ------------------------------- renderers ------------------------------- */\n\nfunction PdfRenderer({ url, scale, onNativeFallback }) {\n const wrapRef = useRef(null);\n const [numPages, setNumPages] = useState(0);\n const [width, setWidth] = useState(0);\n const [error, setError] = useState(false);\n\n useEffect(() => {\n const el = wrapRef.current;\n if (!el) return undefined;\n const update = () => setWidth(el.clientWidth);\n update();\n const ro = new ResizeObserver(update);\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n // pdf.js streams the file via its own fetch, which fails cross-origin on\n // storage URLs the bucket's CORS policy doesn't allowlist this origin for\n // (common — see the S3 bucket's CORS config, not something fixable here) —\n // distinct from a plain navigation (an <iframe> load), which is NOT subject\n // to CORS at all and works regardless. Fall back to that rather than\n // dead-ending the preview; the native viewer brings its own zoom/controls,\n // so the toolbar hides its (now inert) zoom buttons via onNativeFallback.\n //\n // This intentionally points the iframe at the raw presigned `url`, not a\n // fetched blob — fetching it would hit the same CORS wall react-pdf just\n // did. Whether this renders inline vs. downloads depends entirely on the\n // `Content-Disposition` the presigned URL responds with; that's set\n // server-side (GetPresignedURLInline in Be_Auth_DevOps) rather than worked\n // around here, so every consumer of the URL — this iframe, a plain link,\n // anything — gets the same correct inline behavior.\n if (error) {\n return <iframe title=\"PDF preview\" src={url} className=\"dv-pdf-native\" />;\n }\n\n return (\n <div ref={wrapRef} className=\"dv-pdf-wrap\">\n <Document\n file={url}\n loading={<Spin />}\n error={<FallbackStage label=\"This PDF could not be displayed.\" />}\n onLoadSuccess={({ numPages: n }) => setNumPages(n)}\n onLoadError={() => {\n setError(true);\n onNativeFallback?.();\n }}\n >\n {Array.from({ length: numPages }, (_, i) => (\n <Page\n key={`page-${i + 1}`}\n pageNumber={i + 1}\n width={width ? width * scale : undefined}\n className=\"dv-pdf-page\"\n renderTextLayer\n renderAnnotationLayer\n />\n ))}\n </Document>\n </div>\n );\n}\n\nPdfRenderer.propTypes = {\n url: PropTypes.string.isRequired,\n scale: PropTypes.number.isRequired,\n onNativeFallback: PropTypes.func,\n};\n\nfunction DocxRenderer({ url }) {\n const ref = useRef(null);\n const [status, setStatus] = useState('loading'); // loading | ready | error\n\n useEffect(() => {\n let cancelled = false;\n\n fetch(url)\n .then((r) => {\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n return r.blob();\n })\n .then((blob) => {\n if (cancelled || !ref.current) return undefined;\n ref.current.innerHTML = '';\n return renderAsync(blob, ref.current, undefined, {\n className: 'dv-docx',\n inWrapper: true,\n ignoreWidth: false,\n ignoreHeight: false,\n });\n })\n .then(() => {\n if (!cancelled) setStatus('ready');\n })\n .catch(() => {\n if (!cancelled) setStatus('error');\n });\n\n return () => {\n cancelled = true;\n };\n }, [url]);\n\n if (status === 'error') {\n return <FallbackStage label=\"This document could not be displayed.\" />;\n }\n\n return (\n <div className=\"dv-docx-scroll\">\n {status === 'loading' && <div className=\"dv-center\"><Spin /></div>}\n <div ref={ref} style={{ visibility: status === 'ready' ? 'visible' : 'hidden' }} />\n </div>\n );\n}\n\nDocxRenderer.propTypes = { url: PropTypes.string.isRequired };\n\nfunction TextRenderer({ url, content = '' }) {\n const [text, setText] = useState(null);\n const [status, setStatus] = useState('loading');\n\n useEffect(() => {\n if (content) return undefined;\n\n let cancelled = false;\n fetch(url)\n .then((r) => {\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n return r.text();\n })\n .then((t) => {\n if (!cancelled) {\n setText(t);\n setStatus('ready');\n }\n })\n .catch(() => !cancelled && setStatus('error'));\n return () => {\n cancelled = true;\n };\n }, [content, url]);\n\n if (content) return <pre className=\"dv-text\">{content}</pre>;\n if (status === 'loading') return <div className=\"dv-center\"><Spin /></div>;\n if (status === 'error') return <FallbackStage label=\"This file could not be displayed.\" />;\n return <pre className=\"dv-text\">{text}</pre>;\n}\n\nTextRenderer.propTypes = {\n url: PropTypes.string,\n content: PropTypes.string,\n};\n\nfunction HtmlRenderer({ content }) {\n const safeHtml = useMemo(() => DOMPurify.sanitize(content), [content]);\n return <div className=\"dv-html\" dangerouslySetInnerHTML={{ __html: safeHtml }} />;\n}\n\nHtmlRenderer.propTypes = { content: PropTypes.string.isRequired };\n\nfunction ImageRenderer({ url, name, scale }) {\n const [error, setError] = useState(false);\n if (error) return <FallbackStage label=\"This image could not be displayed.\" />;\n return (\n <div className=\"dv-image-wrap\">\n <img\n className=\"dv-image\"\n src={url}\n alt={name}\n style={{ transform: `scale(${scale})` }}\n onError={() => setError(true)}\n />\n </div>\n );\n}\n\nImageRenderer.propTypes = {\n url: PropTypes.string.isRequired,\n name: PropTypes.string.isRequired,\n scale: PropTypes.number.isRequired,\n};\n\nfunction VideoRenderer({ url, name }) {\n const [error, setError] = useState(false);\n if (error) return <FallbackStage label=\"This video could not be played.\" />;\n return (\n <div className=\"dv-video-wrap\">\n <video\n className=\"dv-video\"\n src={url}\n title={name}\n controls\n preload=\"metadata\"\n controlsList=\"nodownload\"\n onError={() => setError(true)}\n />\n </div>\n );\n}\n\nVideoRenderer.propTypes = {\n url: PropTypes.string.isRequired,\n name: PropTypes.string.isRequired,\n};\n\nfunction FallbackStage({ label }) {\n return (\n <div className=\"dv-center dv-fallback\">\n <FileUnknownOutlined className=\"dv-fallback-icon\" />\n <p>{label}</p>\n <span className=\"dv-fallback-hint\">Use the download button to open it.</span>\n </div>\n );\n}\n\nFallbackStage.propTypes = { label: PropTypes.string.isRequired };\n\n/* ----------------------------- main component ---------------------------- */\n\n/**\n * useAuthenticatedDocument — turns a doc carrying `authSource` into one\n * carrying a usable blob: `url`.\n *\n * Returns { doc, loading, error }. A doc that already has a url passes through\n * untouched, so this costs nothing for inline content and public links.\n *\n * REVOCATION is the point of the cleanup function: a blob URL keeps its bytes\n * alive until revoked, so a multi-megabyte resume leaks for the lifetime of\n * the page if this is skipped. The effect revokes on document change, on\n * viewer close (ViewerBody unmounts — the Modal uses destroyOnHidden), and on\n * a superseded in-flight fetch.\n */\nfunction useAuthenticatedDocument(doc) {\n // State carries the key it belongs to, so a stale result is detectable\n // during render. That removes the need to reset via setState inside the\n // effect body, which causes a cascading render (and is a lint error).\n const [state, setState] = useState({ key: '', url: '', error: '' });\n\n const source = doc?.authSource;\n // Stringified so the effect compares by VALUE. An object identity dependency\n // would refetch on every parent render and leak one blob per render.\n const sourceKey = source ? JSON.stringify(source) : '';\n const alreadyUsable = Boolean(doc?.url || doc?.content);\n const needsFetch = Boolean(sourceKey) && !alreadyUsable;\n\n useEffect(() => {\n if (!needsFetch) return undefined;\n\n let cancelled = false;\n let revokeFn = null;\n\n createDocumentObjectUrl(JSON.parse(sourceKey))\n .then(({ url, revoke }) => {\n revokeFn = revoke;\n // Losing the race means the user already moved on: revoke immediately\n // rather than handing back a URL nothing will ever revoke.\n if (cancelled) {\n revoke();\n return;\n }\n setState({ key: sourceKey, url, error: '' });\n })\n .catch((error) => {\n if (cancelled) return;\n setState({\n key: sourceKey,\n url: '',\n error: error?.message || 'This document could not be loaded.',\n });\n });\n\n return () => {\n cancelled = true;\n if (revokeFn) revokeFn();\n };\n }, [needsFetch, sourceKey]);\n\n if (!doc) return { doc: null, loading: false, error: '' };\n if (!needsFetch) return { doc, loading: false, error: '' };\n\n // A result from a PREVIOUS document must never be shown against this one.\n const settled = state.key === sourceKey;\n if (!settled) return { doc, loading: true, error: '' };\n if (state.error) return { doc, loading: false, error: state.error };\n return { doc: state.url ? { ...doc, url: state.url } : doc, loading: false, error: '' };\n}\n\n// ViewerBody holds the per-session state (active index + zoom). It lives inside\n// the Modal body, which is destroyed on close (destroyOnHidden), so it remounts\n// fresh on every open — no manual \"reset on open\" effects required.\nfunction ViewerBody({ docs, initialIndex, onClose }) {\n const total = docs.length;\n const safeInitial = Math.min(Math.max(initialIndex, 0), Math.max(0, total - 1));\n const [index, setIndex] = useState(safeInitial);\n const [scale, setScale] = useState(1);\n // Set when a PDF falls back to the browser's native viewer (see\n // PdfRenderer) — that viewer has its own zoom UI, so ours would sit there\n // doing nothing if left visible.\n const [pdfNativeFallback, setPdfNativeFallback] = useState(false);\n\n // Resolve the ACTIVE document's bytes through the authenticated backend when\n // all we hold is a private-bucket object key.\n //\n // Lazy by design — only the document actually being viewed is fetched. A\n // blob: URL pins its bytes in memory until revoked, so resolving every\n // document in the list up front would hold a whole attachment set resident\n // for as long as the viewer is open.\n const resolved = useAuthenticatedDocument(docs[index]);\n const current = resolved.doc;\n const kind = current ? kindOf(current) : 'unknown';\n const zoomable = (kind === 'pdf' && !pdfNativeFallback) || kind === 'image';\n\n // setActive changes the document and resets zoom/fallback in one event\n // handler, so we never have to reset them from an effect.\n const setActive = useCallback((next) => {\n setIndex(next);\n setScale(1);\n setPdfNativeFallback(false);\n }, []);\n\n const goPrev = useCallback(() => setActive(Math.max(0, index - 1)), [index, setActive]);\n const goNext = useCallback(\n () => setActive(Math.min(total - 1, index + 1)),\n [index, total, setActive],\n );\n\n // Keyboard navigation.\n useEffect(() => {\n const onKey = (e) => {\n // A focused <video> uses arrow keys to seek — don't switch documents.\n if (e.target?.tagName === 'VIDEO') return;\n if (e.key === 'ArrowLeft') goPrev();\n else if (e.key === 'ArrowRight') goNext();\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [goPrev, goNext]);\n\n const download = useCallback(async (doc) => {\n if (!doc) return;\n try {\n let blob;\n if (doc.content) {\n const mimeType = doc.type === 'html' ? 'text/html;charset=utf-8' : 'text/plain;charset=utf-8';\n blob = new Blob([doc.content], { type: mimeType });\n } else {\n const res = await fetch(doc.url);\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n blob = await res.blob();\n }\n const objUrl = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = objUrl;\n a.download = doc.name || 'document';\n document.body.appendChild(a);\n a.click();\n a.remove();\n URL.revokeObjectURL(objUrl);\n } catch {\n // Cross-origin without CORS: fall back to opening in a new tab.\n message.info('Opening file in a new tab…');\n if (doc.url) window.open(doc.url, '_blank', 'noopener,noreferrer');\n }\n }, []);\n\n function renderStage() {\n if (!current) {\n return <FallbackStage label=\"No document to preview.\" />;\n }\n // States are distinct on purpose. One generic \"could not load\" message\n // sends the user to the wrong remedy — \"you lack permission\", \"the file is\n // gone\" and \"storage is briefly down\" need three different actions, and\n // the backend already classifies them (403 / 404 / 502).\n if (resolved.error) {\n return <FallbackStage label={resolved.error} />;\n }\n if (resolved.loading) {\n return <FallbackStage label=\"Loading document…\" />;\n }\n switch (kind) {\n case 'pdf':\n return (\n <PdfRenderer\n key={current.key}\n url={current.url}\n scale={scale}\n onNativeFallback={() => setPdfNativeFallback(true)}\n />\n );\n case 'docx':\n return <DocxRenderer key={current.key} url={current.url} />;\n case 'image':\n return (\n <ImageRenderer key={current.key} url={current.url} name={current.name} scale={scale} />\n );\n case 'text':\n return <TextRenderer key={current.key} url={current.url} content={current.content} />;\n case 'html':\n return <HtmlRenderer key={current.key} content={current.content} />;\n case 'video':\n return <VideoRenderer key={current.key} url={current.url} name={current.name} />;\n default:\n return <FallbackStage label=\"Preview is not available for this file type.\" />;\n }\n }\n\n return (\n <div className=\"dv-root\">\n {/* toolbar */}\n <div className=\"dv-toolbar\">\n <div className=\"dv-title\" title={current?.name}>\n <span className=\"dv-title-text\">{current?.name || 'Document'}</span>\n {total > 1 && <span className=\"dv-counter\">{index + 1} / {total}</span>}\n </div>\n <div className=\"dv-actions\">\n {zoomable && (\n <>\n <Tooltip title=\"Zoom out\">\n <button\n type=\"button\"\n className=\"dv-icon-btn\"\n onClick={() => setScale((s) => Math.max(ZOOM_MIN, +(s - ZOOM_STEP).toFixed(2)))}\n disabled={scale <= ZOOM_MIN}\n >\n <ZoomOutOutlined />\n </button>\n </Tooltip>\n <span className=\"dv-zoom-label\">{Math.round(scale * 100)}%</span>\n <Tooltip title=\"Zoom in\">\n <button\n type=\"button\"\n className=\"dv-icon-btn\"\n onClick={() => setScale((s) => Math.min(ZOOM_MAX, +(s + ZOOM_STEP).toFixed(2)))}\n disabled={scale >= ZOOM_MAX}\n >\n <ZoomInOutlined />\n </button>\n </Tooltip>\n <span className=\"dv-divider\" />\n </>\n )}\n <Tooltip title=\"Download\">\n <button type=\"button\" className=\"dv-icon-btn dv-download\" onClick={() => download(current)}>\n <DownloadOutlined />\n </button>\n </Tooltip>\n <Tooltip title=\"Close\">\n <button type=\"button\" className=\"dv-icon-btn\" onClick={onClose}>\n <CloseOutlined />\n </button>\n </Tooltip>\n </div>\n </div>\n\n {/* stage + slider arrows */}\n <div className=\"dv-stage\">\n {total > 1 && (\n <button\n type=\"button\"\n className=\"dv-nav dv-nav--prev\"\n onClick={goPrev}\n disabled={index === 0}\n aria-label=\"Previous document\"\n >\n <LeftOutlined />\n </button>\n )}\n\n <div className=\"dv-canvas\">{renderStage()}</div>\n\n {total > 1 && (\n <button\n type=\"button\"\n className=\"dv-nav dv-nav--next\"\n onClick={goNext}\n disabled={index === total - 1}\n aria-label=\"Next document\"\n >\n <RightOutlined />\n </button>\n )}\n </div>\n\n {/* dots */}\n {total > 1 && (\n <div className=\"dv-dots\">\n {docs.map((d, i) => (\n <button\n key={d.key}\n type=\"button\"\n className={`dv-dot${i === index ? ' dv-dot--active' : ''}`}\n onClick={() => setActive(i)}\n aria-label={`Go to document ${i + 1}`}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nViewerBody.propTypes = {\n docs: PropTypes.arrayOf(PropTypes.object).isRequired,\n initialIndex: PropTypes.number.isRequired,\n onClose: PropTypes.func.isRequired,\n};\n\n// DocumentViewer is the public component: a Modal shell that mounts ViewerBody\n// only while open. `documents` may be URL strings or document objects.\n//\n// Pass `inline` to render ViewerBody directly with no Modal — used by the timesheet\n// manager review, which shows the uploaded screenshot side-by-side with the calendar\n// rather than in a popup. Everything ViewerBody already does (pdf/image/docx render,\n// zoom, download, multi-document navigation) comes along for free.\nexport default function DocumentViewer({ documents, open, onClose, initialIndex = 0, inline = false }) {\n const docs = useMemo(() => normalizeDocs(documents), [documents]);\n\n if (inline) {\n return (\n <div className=\"dv-root--inline\">\n <ViewerBody docs={docs} initialIndex={initialIndex} onClose={onClose ?? (() => {})} />\n </div>\n );\n }\n\n return (\n <Modal\n open={open}\n onCancel={onClose}\n footer={null}\n title={null}\n closable={false}\n centered\n width=\"min(1100px, 94vw)\"\n className=\"dv-modal\"\n styles={{ content: { padding: 0, overflow: 'hidden', borderRadius: 14 }, body: { padding: 0 } }}\n destroyOnHidden\n >\n <ViewerBody docs={docs} initialIndex={initialIndex} onClose={onClose} />\n </Modal>\n );\n}\n\nDocumentViewer.propTypes = {\n // URL strings, file objects, or inline objects ({ content, type: 'text'|'html' }).\n documents: PropTypes.arrayOf(\n PropTypes.oneOfType([PropTypes.string, PropTypes.object]),\n ).isRequired,\n // Required for the Modal shell; unused (and optional) when `inline` is set.\n open: PropTypes.bool,\n onClose: PropTypes.func,\n initialIndex: PropTypes.number,\n // Render the viewer body directly, with no Modal wrapper.\n inline: PropTypes.bool,\n};\n"],"mappings":"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"}