Skip to content

Commit 57dc707

Browse files
davindicodeclaude
andcommitted
Zoom viewers, focus indicators, nav keys, close X buttons, CSS polish
- PDF viewer: proper zoom via transform scale with scrollable spacer - Image viewer: pinch-to-zoom + zoom buttons - Terminal: focus-within blue outline on active area (output vs input) - Nav keys: added PgUp/PgDn - File viewers: replaced "Close" text with compact X icon buttons - Global CSS: button press feedback (scale 93% + opacity on active) - Mobile tab bar: reordered Files | Terminal | Browser - README: updated tagline Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 62774a3 commit 57dc707

7 files changed

Lines changed: 135 additions & 19 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<h1 align="center">otgcode</h1>
66
<p align="center"><em>on-the-go code</em></p>
77

8-
<p align="center">Mobile-terminal-UX first coding IDE. Control your VPS or local machine from anywhere — phone, tablet, or desktop browser.</p>
8+
<p align="center">Mobile-terminal-UX first coding IDE fully in browser. Code locally on your VPS or home machines from anywhere — phone, tablet, or desktop - with zero installation using Cloudfare Quick Tunnels.</p>
99

1010
<p align="center">
1111
<img src="https://img.shields.io/badge/version-0.1.0-blue" alt="Version">

app/app.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,32 @@ body {
140140
.prose img { max-width: 100%; border-radius: 4px; }
141141
.prose hr { border: none; border-top: 1px solid #333; margin: 1.5em 0; }
142142

143+
/* Terminal focus indicator — light border when area is actively focused */
144+
.terminal-focus-area {
145+
outline: 1px solid transparent;
146+
transition: outline-color 0.2s;
147+
}
148+
149+
.terminal-focus-area:focus-within {
150+
outline-color: #569cd6;
151+
}
152+
153+
/* Global button press feedback — visual response on tap/click */
154+
button, a[download], [role="button"] {
155+
cursor: pointer;
156+
-webkit-tap-highlight-color: transparent;
157+
transition: transform 0.1s, opacity 0.1s, background-color 0.15s;
158+
}
159+
160+
button:active:not(:disabled), a[download]:active, [role="button"]:active {
161+
transform: scale(0.93);
162+
opacity: 0.8;
163+
}
164+
165+
button:disabled {
166+
cursor: not-allowed;
167+
}
168+
143169
/* Scrollbar styling */
144170
::-webkit-scrollbar {
145171
width: 6px;

app/components/files/CodeEditor.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,10 @@ export default function CodeEditor({ path, content, onSave, onClose }: CodeEdito
252252
</button>
253253
<button
254254
onClick={onClose}
255-
className="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded text-xs transition-colors"
255+
className="p-1 text-gray-400 hover:text-white transition-colors"
256+
title="Close"
256257
>
257-
Close
258+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
258259
</button>
259260
</div>
260261
</div>

app/components/files/FileViewer.tsx

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useState, useRef } from "react";
12
import CodeEditor from "./CodeEditor";
23
import ImageViewer from "./ImageViewer";
34

@@ -15,11 +16,37 @@ interface FileViewerProps {
1516
}
1617

1718
function PdfViewer({ path, onClose }: { path: string; onClose: () => void }) {
19+
const [zoom, setZoom] = useState(1);
20+
const lastDistance = useRef(0);
21+
22+
const zoomIn = () => setZoom((z) => Math.min(5, z + 0.25));
23+
const zoomOut = () => setZoom((z) => Math.max(0.25, z - 0.25));
24+
const zoomFit = () => setZoom(1);
25+
26+
const handleTouchMove = (e: React.TouchEvent) => {
27+
if (e.touches.length !== 2) return;
28+
const dx = e.touches[0].clientX - e.touches[1].clientX;
29+
const dy = e.touches[0].clientY - e.touches[1].clientY;
30+
const dist = Math.sqrt(dx * dx + dy * dy);
31+
if (lastDistance.current > 0) {
32+
const delta = (dist - lastDistance.current) * 0.005;
33+
setZoom((z) => Math.max(0.25, Math.min(5, z + delta)));
34+
}
35+
lastDistance.current = dist;
36+
};
37+
38+
const handleTouchEnd = () => { lastDistance.current = 0; };
39+
1840
return (
1941
<div className="flex flex-col h-full">
2042
<div className="flex items-center justify-between px-3 py-2 bg-[#16162a] border-b border-gray-700 shrink-0">
2143
<span className="text-sm text-gray-300 truncate">{path}</span>
2244
<div className="flex items-center gap-2">
45+
<div className="flex items-center gap-1 border border-gray-700 rounded overflow-hidden">
46+
<button onClick={zoomOut} className="px-2 py-0.5 text-xs text-gray-400 hover:text-white hover:bg-gray-700">-</button>
47+
<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(zoom * 100)}%</button>
48+
<button onClick={zoomIn} className="px-2 py-0.5 text-xs text-gray-400 hover:text-white hover:bg-gray-700">+</button>
49+
</div>
2350
<a
2451
href={`/api/files/download?path=${encodeURIComponent(path)}`}
2552
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium transition-colors"
@@ -29,17 +56,34 @@ function PdfViewer({ path, onClose }: { path: string; onClose: () => void }) {
2956
</a>
3057
<button
3158
onClick={onClose}
32-
className="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded text-xs transition-colors"
59+
className="p-1 text-gray-400 hover:text-white transition-colors"
60+
title="Close"
3361
>
34-
Close
62+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
3563
</button>
3664
</div>
3765
</div>
38-
<div className="flex-1 min-h-0">
66+
<div
67+
className="flex-1 min-h-0 overflow-auto relative"
68+
onTouchMove={handleTouchMove}
69+
onTouchEnd={handleTouchEnd}
70+
>
71+
{/* Spacer div sized to the scaled iframe dimensions so scrolling works */}
72+
<div style={{ width: `${100 * zoom}%`, height: `${100 * zoom}%`, pointerEvents: "none" }} />
73+
{/* Iframe scaled from top-left, positioned absolutely */}
3974
<iframe
4075
src={`/api/files/download?path=${encodeURIComponent(path)}&inline=1`}
41-
className="w-full h-full border-0"
4276
title={path}
77+
style={{
78+
position: "absolute",
79+
top: 0,
80+
left: 0,
81+
width: `${100 / zoom}%`,
82+
height: `${100 / zoom}%`,
83+
border: "none",
84+
transform: `scale(${zoom})`,
85+
transformOrigin: "top left",
86+
}}
4387
/>
4488
</div>
4589
</div>
@@ -76,9 +120,10 @@ export default function FileViewer({ path, content, onSave, onClose }: FileViewe
76120
</a>
77121
<button
78122
onClick={onClose}
79-
className="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded text-xs transition-colors"
123+
className="p-1 text-gray-400 hover:text-white transition-colors"
124+
title="Close"
80125
>
81-
Close
126+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
82127
</button>
83128
</div>
84129
</div>

app/components/files/ImageViewer.tsx

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,47 @@
1+
import { useState, useRef, useCallback } from "react";
2+
13
interface ImageViewerProps {
24
path: string;
35
onClose: () => void;
46
}
57

68
export default function ImageViewer({ path, onClose }: ImageViewerProps) {
9+
const [zoom, setZoom] = useState(1);
10+
const containerRef = useRef<HTMLDivElement>(null);
11+
12+
const zoomIn = () => setZoom((z) => Math.min(5, z + 0.25));
13+
const zoomOut = () => setZoom((z) => Math.max(0.1, z - 0.25));
14+
const zoomFit = () => setZoom(1);
15+
16+
// Pinch-to-zoom
17+
const lastDistance = useRef(0);
18+
const handleTouchMove = useCallback((e: React.TouchEvent) => {
19+
if (e.touches.length !== 2) return;
20+
const dx = e.touches[0].clientX - e.touches[1].clientX;
21+
const dy = e.touches[0].clientY - e.touches[1].clientY;
22+
const dist = Math.sqrt(dx * dx + dy * dy);
23+
if (lastDistance.current > 0) {
24+
const delta = (dist - lastDistance.current) * 0.005;
25+
setZoom((z) => Math.max(0.1, Math.min(5, z + delta)));
26+
}
27+
lastDistance.current = dist;
28+
}, []);
29+
30+
const handleTouchEnd = useCallback(() => {
31+
lastDistance.current = 0;
32+
}, []);
33+
734
return (
835
<div className="flex flex-col h-full">
936
<div className="flex items-center justify-between px-3 py-2 bg-[#16162a] border-b border-gray-700 shrink-0">
1037
<span className="text-sm text-gray-300 truncate">{path}</span>
1138
<div className="flex items-center gap-2 shrink-0">
39+
{/* Zoom controls */}
40+
<div className="flex items-center gap-1 border border-gray-700 rounded overflow-hidden">
41+
<button onClick={zoomOut} className="px-2 py-0.5 text-xs text-gray-400 hover:text-white hover:bg-gray-700">-</button>
42+
<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(zoom * 100)}%</button>
43+
<button onClick={zoomIn} className="px-2 py-0.5 text-xs text-gray-400 hover:text-white hover:bg-gray-700">+</button>
44+
</div>
1245
<a
1346
href={`/api/files/download?path=${encodeURIComponent(path)}`}
1447
download
@@ -18,18 +51,27 @@ export default function ImageViewer({ path, onClose }: ImageViewerProps) {
1851
</a>
1952
<button
2053
onClick={onClose}
21-
className="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded text-xs transition-colors"
54+
className="p-1 text-gray-400 hover:text-white transition-colors"
55+
title="Close"
2256
>
23-
Close
57+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
2458
</button>
2559
</div>
2660
</div>
27-
<div className="flex-1 flex items-center justify-center overflow-auto p-4 bg-[#0d0d1a]">
28-
<img
29-
src={`/api/files/download?path=${encodeURIComponent(path)}&inline=1`}
30-
alt={path}
31-
className="max-w-full max-h-full object-contain"
32-
/>
61+
<div
62+
ref={containerRef}
63+
className="flex-1 overflow-auto bg-[#0d0d1a]"
64+
onTouchMove={handleTouchMove}
65+
onTouchEnd={handleTouchEnd}
66+
>
67+
<div className="min-w-full min-h-full flex items-center justify-center p-4" style={{ transform: `scale(${zoom})`, transformOrigin: "center center" }}>
68+
<img
69+
src={`/api/files/download?path=${encodeURIComponent(path)}&inline=1`}
70+
alt={path}
71+
className="max-w-none"
72+
draggable={false}
73+
/>
74+
</div>
3375
</div>
3476
</div>
3577
);

app/components/terminal/InputBox.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const NAV_KEYS: QuickKey[] = [
2323
{ label: "Down", key: "\x1b[B", title: "Next command" },
2424
{ label: "Left", key: "\x1b[D", title: "Cursor left" },
2525
{ label: "Right", key: "\x1b[C", title: "Cursor right" },
26+
{ label: "PgUp", key: "\x1b[5~", title: "Page up" },
27+
{ label: "PgDn", key: "\x1b[6~", title: "Page down" },
2628
{ label: "y", key: "y", title: "Yes" },
2729
{ label: "n", key: "n", title: "No" },
2830
{ label: "q", key: "q", title: "Quit (htop/less)" },
@@ -147,7 +149,7 @@ export default function InputBox() {
147149
const group = activeGroup ? GROUPS.find((g) => g.label === activeGroup) : null;
148150

149151
return (
150-
<div className="bg-[#16162a] border-t border-gray-700 shrink-0 overflow-hidden" style={{ minWidth: 0 }}>
152+
<div className="bg-[#16162a] border-t border-gray-700 shrink-0 overflow-hidden terminal-focus-area rounded-sm" style={{ minWidth: 0 }}>
151153
{/* Quick keys */}
152154
<div className="border-b border-gray-700/50 overflow-x-auto scrollbar-none" style={{ minWidth: 0 }}>
153155
<div className="flex items-center gap-1 px-2 py-1 w-max">

app/components/terminal/TerminalPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function TerminalPage() {
2626

2727
return (
2828
<div className="flex flex-col flex-1 min-h-0 min-w-0">
29-
<div className="flex-1 relative min-h-0 overflow-hidden">
29+
<div className="flex-1 relative min-h-0 overflow-hidden terminal-focus-area rounded-sm">
3030
{sessionsArray.length === 0 ? (
3131
<div className="flex items-center justify-center h-full text-gray-500">
3232
<p className="text-sm">Click + to create a new terminal</p>

0 commit comments

Comments
 (0)