Skip to content

Commit 37d01be

Browse files
davindicodeclaude
andcommitted
Multi-page PDF viewer with react-pdf, remove select mode, fix path overflow
- Replace iframe PDF viewer with react-pdf (PDF.js) for proper multi-page rendering - Zoom, text selection, page count, loading/error states - Remove select mode toggle (preview mode handles text selection) - Fix file path overlapping toolbar buttons with text-ellipsis truncation - Disable text selection on file path to prevent interference with buttons Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f58a53a commit 37d01be

4 files changed

Lines changed: 259 additions & 37 deletions

File tree

app/components/files/CodeEditor.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,6 @@ export default function CodeEditor({ path, content, onSave, onClose }: CodeEdito
193193
const [value, setValue] = useState(content);
194194
const [dirty, setDirty] = useState(false);
195195
const [mode, setMode] = useState<"edit" | "preview">(canPreview ? "preview" : "edit");
196-
const [selectMode, setSelectMode] = useState(false);
197196
const [editorFontSize, setEditorFontSize] = useState(6);
198197

199198
const handleChange = (v: string | undefined) => {
@@ -212,7 +211,7 @@ export default function CodeEditor({ path, content, onSave, onClose }: CodeEdito
212211
<div className="flex flex-col h-full">
213212
{/* Toolbar */}
214213
<div className="flex items-center gap-2 px-3 py-1.5 bg-[#16162a] border-b border-gray-700 shrink-0">
215-
<span className="text-sm text-gray-300 overflow-x-auto scrollbar-none whitespace-nowrap min-w-0 flex-1 select-all">{path}</span>
214+
<span className="text-sm text-gray-300 whitespace-nowrap min-w-0 flex-1 overflow-hidden text-ellipsis select-none" title={path}>{path}</span>
216215
<div className="flex items-center gap-1.5 shrink-0">
217216
{canPreview && (
218217
<div className="flex items-center bg-[#0d0d1a] rounded overflow-hidden border border-gray-700">
@@ -238,20 +237,6 @@ export default function CodeEditor({ path, content, onSave, onClose }: CodeEdito
238237
</button>
239238
</div>
240239
)}
241-
{/* Select mode toggle (for mobile text selection) */}
242-
{mode === "edit" && (
243-
<button
244-
onClick={() => setSelectMode((s) => !s)}
245-
className={`px-2 py-0.5 text-xs rounded border transition-colors ${
246-
selectMode
247-
? "bg-yellow-600 text-white border-yellow-500"
248-
: "text-gray-400 hover:text-white border-gray-700"
249-
}`}
250-
title={selectMode ? "Exit select mode (read-only)" : "Select mode (read-only, enables text selection)"}
251-
>
252-
Select
253-
</button>
254-
)}
255240
{/* Font size */}
256241
<div className="flex items-center border border-gray-700 rounded overflow-hidden">
257242
<button
@@ -328,9 +313,7 @@ export default function CodeEditor({ path, content, onSave, onClose }: CodeEdito
328313
automaticLayout: true,
329314
selectionHighlight: true,
330315
occurrencesHighlight: "singleFile",
331-
dragAndDrop: !selectMode,
332-
readOnly: selectMode,
333-
domReadOnly: selectMode,
316+
dragAndDrop: true,
334317
}}
335318
/>
336319
</Suspense>

app/components/files/FileViewer.tsx

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { useState, useRef } from "react";
1+
import { useState, useRef, useCallback } from "react";
2+
import { Document, Page, pdfjs } from "react-pdf";
3+
import "react-pdf/dist/Page/AnnotationLayer.css";
4+
import "react-pdf/dist/Page/TextLayer.css";
25
import CodeEditor from "./CodeEditor";
36
import ImageViewer from "./ImageViewer";
47

8+
pdfjs.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
9+
510
const IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "svg", "webp", "ico", "bmp"]);
611
const VIDEO_EXTS = new Set(["mp4", "webm", "ogg", "mov", "mkv", "avi"]);
712
const AUDIO_EXTS = new Set(["mp3", "wav", "ogg", "flac", "aac", "m4a"]);
@@ -22,7 +27,7 @@ function MediaViewer({ path, type, onClose }: { path: string; type: "video" | "a
2227
return (
2328
<div className="flex flex-col h-full">
2429
<div className="flex items-center justify-between px-3 py-2 bg-[#16162a] border-b border-gray-700 shrink-0">
25-
<span className="text-sm text-gray-300 overflow-x-auto scrollbar-none whitespace-nowrap min-w-0 flex-1 select-all">{path}</span>
30+
<span className="text-sm text-gray-300 whitespace-nowrap min-w-0 flex-1 overflow-hidden text-ellipsis select-none">{path}</span>
2631
<div className="flex items-center gap-2 shrink-0">
2732
<a
2833
href={`/api/files/download?path=${encodeURIComponent(path)}`}
@@ -53,23 +58,31 @@ function MediaViewer({ path, type, onClose }: { path: string; type: "video" | "a
5358
}
5459

5560
function PdfViewer({ path, onClose }: { path: string; onClose: () => void }) {
56-
const [zoomPct, setZoomPct] = useState(100);
61+
const [numPages, setNumPages] = useState<number>(0);
62+
const [scale, setScale] = useState(1.0);
63+
const containerRef = useRef<HTMLDivElement>(null);
64+
65+
const zoomIn = () => setScale((s) => Math.min(3, s + 0.25));
66+
const zoomOut = () => setScale((s) => Math.max(0.25, s - 0.25));
67+
const zoomFit = () => setScale(1.0);
5768

58-
const zoomIn = () => setZoomPct((z) => Math.min(500, z + 25));
59-
const zoomOut = () => setZoomPct((z) => Math.max(25, z - 25));
60-
const zoomFit = () => setZoomPct(100);
69+
const onDocumentLoadSuccess = useCallback(({ numPages }: { numPages: number }) => {
70+
setNumPages(numPages);
71+
}, []);
6172

62-
// Use PDF viewer's built-in zoom via URL fragment
63-
const pdfUrl = `/api/files/download?path=${encodeURIComponent(path)}&inline=1#zoom=${zoomPct}`;
73+
const pdfUrl = `/api/files/download?path=${encodeURIComponent(path)}&inline=1`;
6474

6575
return (
6676
<div className="flex flex-col h-full">
6777
<div className="flex items-center justify-between px-3 py-2 bg-[#16162a] border-b border-gray-700 shrink-0">
68-
<span className="text-sm text-gray-300 overflow-x-auto scrollbar-none whitespace-nowrap min-w-0 flex-1 select-all">{path}</span>
78+
<span className="text-sm text-gray-300 whitespace-nowrap min-w-0 flex-1 overflow-hidden text-ellipsis select-none">{path}</span>
6979
<div className="flex items-center gap-2">
80+
{numPages > 0 && (
81+
<span className="text-[10px] text-gray-500">{numPages} pg</span>
82+
)}
7083
<div className="flex items-center gap-1 border border-gray-700 rounded overflow-hidden">
7184
<button onClick={zoomOut} className="px-2 py-0.5 text-xs text-gray-400 hover:text-white hover:bg-gray-700">-</button>
72-
<button onClick={zoomFit} className="px-2 py-0.5 text-[10px] text-gray-400 hover:text-white hover:bg-gray-700 tabular-nums">{zoomPct}%</button>
85+
<button onClick={zoomFit} className="px-2 py-0.5 text-[10px] text-gray-400 hover:text-white hover:bg-gray-700 tabular-nums">{Math.round(scale * 100)}%</button>
7386
<button onClick={zoomIn} className="px-2 py-0.5 text-xs text-gray-400 hover:text-white hover:bg-gray-700">+</button>
7487
</div>
7588
<a
@@ -89,13 +102,30 @@ function PdfViewer({ path, onClose }: { path: string; onClose: () => void }) {
89102
</button>
90103
</div>
91104
</div>
92-
<div className="flex-1 min-h-0 bg-[#0d0d1a]">
93-
<iframe
94-
key={zoomPct}
95-
src={pdfUrl}
96-
title={path}
97-
className="w-full h-full border-0"
98-
/>
105+
<div ref={containerRef} className="flex-1 min-h-0 overflow-auto bg-[#0d0d1a]">
106+
<Document
107+
file={pdfUrl}
108+
onLoadSuccess={onDocumentLoadSuccess}
109+
loading={
110+
<div className="flex items-center justify-center h-full text-gray-500 text-sm">Loading PDF...</div>
111+
}
112+
error={
113+
<div className="flex items-center justify-center h-full text-red-400 text-sm">Failed to load PDF</div>
114+
}
115+
>
116+
<div className="flex flex-col items-center gap-2 p-4">
117+
{Array.from({ length: numPages }, (_, i) => (
118+
<Page
119+
key={i + 1}
120+
pageNumber={i + 1}
121+
scale={scale}
122+
className="shadow-lg"
123+
renderTextLayer={true}
124+
renderAnnotationLayer={true}
125+
/>
126+
))}
127+
</div>
128+
</Document>
99129
</div>
100130
</div>
101131
);
@@ -128,7 +158,7 @@ export default function FileViewer({ path, content, onSave, onClose }: FileViewe
128158
return (
129159
<div className="flex flex-col h-full">
130160
<div className="flex items-center justify-between px-3 py-2 bg-[#16162a] border-b border-gray-700 shrink-0">
131-
<span className="text-sm text-gray-300 overflow-x-auto scrollbar-none whitespace-nowrap min-w-0 flex-1 select-all">{path}</span>
161+
<span className="text-sm text-gray-300 whitespace-nowrap min-w-0 flex-1 overflow-hidden text-ellipsis select-none">{path}</span>
132162
<div className="flex items-center gap-2">
133163
<a
134164
href={`/api/files/download?path=${encodeURIComponent(path)}`}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"node-pty": "^1.1.0",
2727
"react": "^19.2.4",
2828
"react-dom": "^19.2.4",
29+
"react-pdf": "^10.4.1",
2930
"react-router": "7.12.0",
3031
"socket.io": "^4.8.3",
3132
"socket.io-client": "^4.8.3",

0 commit comments

Comments
 (0)