@mjasano/devtunnel 1.4.0 → 1.5.1
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.
- package/.claude/settings.local.json +5 -1
- package/CHANGELOG.md +18 -0
- package/bin/cli.js +16 -1
- package/package.json +1 -1
- package/public/app.js +161 -9
- package/public/index.html +17 -8
- package/public/login.html +32 -0
- package/public/styles.css +419 -11
- package/server.js +1 -1
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
"Bash(git add:*)",
|
|
17
17
|
"Bash(git commit:*)",
|
|
18
18
|
"Bash(git push:*)",
|
|
19
|
-
"Bash(gh release create:*)"
|
|
19
|
+
"Bash(gh release create:*)",
|
|
20
|
+
"Bash(PORT=3099 node:*)",
|
|
21
|
+
"Bash(/tmp/devtunnel-test/node_modules/.bin/devtunnel:*)",
|
|
22
|
+
"Bash(lsof:*)",
|
|
23
|
+
"Bash(kill:*)"
|
|
20
24
|
]
|
|
21
25
|
}
|
|
22
26
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.5.1] - 2026-01-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Korean (Hangul) IME input on mobile devices - characters no longer split into individual jamo
|
|
12
|
+
|
|
13
|
+
## [1.5.0] - 2026-01-02
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Mobile-responsive UI with touch-friendly design
|
|
17
|
+
- Sidebar close button for mobile devices
|
|
18
|
+
- Full-width sidebar on small screens (480px and below)
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Improved touch targets (minimum 44px for buttons/tabs)
|
|
22
|
+
- Dynamic terminal height using flex layout
|
|
23
|
+
- Input font-size set to 16px to prevent iOS auto-zoom
|
|
24
|
+
- Smoother sidebar slide animation with overlay
|
|
25
|
+
|
|
8
26
|
## [1.4.0] - 2026-01-02
|
|
9
27
|
|
|
10
28
|
### Added
|
package/bin/cli.js
CHANGED
|
@@ -117,9 +117,18 @@ function startServer(port, passcode = null) {
|
|
|
117
117
|
detached: false
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
// Track the actual port the server is running on
|
|
121
|
+
server.actualPort = port;
|
|
122
|
+
|
|
120
123
|
server.stdout.on('data', (data) => {
|
|
121
124
|
const msg = data.toString().trim();
|
|
122
125
|
if (msg) log(`${c.dim}[server] ${msg}${c.reset}`);
|
|
126
|
+
|
|
127
|
+
// Parse actual port from server output
|
|
128
|
+
const portMatch = msg.match(/running on http:\/\/localhost:(\d+)/);
|
|
129
|
+
if (portMatch) {
|
|
130
|
+
server.actualPort = parseInt(portMatch[1]);
|
|
131
|
+
}
|
|
123
132
|
});
|
|
124
133
|
|
|
125
134
|
server.stderr.on('data', (data) => {
|
|
@@ -237,8 +246,14 @@ async function main() {
|
|
|
237
246
|
log(`${c.dim}Starting server on port ${port}...${c.reset}`);
|
|
238
247
|
const server = startServer(port, passcode);
|
|
239
248
|
|
|
249
|
+
// Wait a bit for server to output its actual port
|
|
250
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
251
|
+
|
|
240
252
|
try {
|
|
241
|
-
|
|
253
|
+
// Use the actual port the server is running on
|
|
254
|
+
const actualPort = server.actualPort;
|
|
255
|
+
await waitForServer(actualPort);
|
|
256
|
+
port = actualPort; // Update port for tunnel
|
|
242
257
|
log(`${c.green}✓${c.reset} Server running`);
|
|
243
258
|
} catch (err) {
|
|
244
259
|
log(`${c.red}✗ Failed to start server: ${err.message}${c.reset}`);
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -110,24 +110,110 @@ require(['vs/editor/editor.main'], function() {
|
|
|
110
110
|
document.getElementById('monaco-editor').style.display = 'none';
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
// View mode state
|
|
114
|
+
let currentViewMode = 'split'; // 'terminal', 'editor', 'split'
|
|
115
|
+
|
|
116
|
+
// View mode switching
|
|
117
|
+
function setViewMode(mode) {
|
|
118
|
+
currentViewMode = mode;
|
|
117
119
|
|
|
120
|
+
// Update button states
|
|
121
|
+
document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
|
|
122
|
+
const activeBtn = document.querySelector(`.view-btn[data-view="${mode}"]`);
|
|
123
|
+
if (activeBtn) activeBtn.classList.add('active');
|
|
124
|
+
|
|
125
|
+
const mainContainer = document.querySelector('.main-container');
|
|
118
126
|
const terminalContainer = document.getElementById('terminal-container');
|
|
119
127
|
const editorContainer = document.getElementById('editor-container');
|
|
120
128
|
|
|
121
|
-
|
|
129
|
+
// Reset flex styles
|
|
130
|
+
terminalContainer.style.flex = '';
|
|
131
|
+
editorContainer.style.flex = '';
|
|
132
|
+
|
|
133
|
+
if (mode === 'split') {
|
|
134
|
+
// Split view - show both
|
|
135
|
+
mainContainer.classList.add('split-view');
|
|
136
|
+
terminalContainer.classList.remove('hidden');
|
|
137
|
+
editorContainer.classList.add('active');
|
|
138
|
+
setTimeout(() => fitAddon.fit(), 0);
|
|
139
|
+
} else if (mode === 'terminal') {
|
|
140
|
+
// Terminal only
|
|
141
|
+
mainContainer.classList.remove('split-view');
|
|
122
142
|
terminalContainer.classList.remove('hidden');
|
|
123
143
|
editorContainer.classList.remove('active');
|
|
124
144
|
setTimeout(() => fitAddon.fit(), 0);
|
|
125
145
|
term.focus();
|
|
126
146
|
} else {
|
|
147
|
+
// Editor only
|
|
148
|
+
mainContainer.classList.remove('split-view');
|
|
127
149
|
terminalContainer.classList.add('hidden');
|
|
128
150
|
editorContainer.classList.add('active');
|
|
129
151
|
if (monacoEditor) monacoEditor.focus();
|
|
130
152
|
}
|
|
153
|
+
|
|
154
|
+
// Save preference
|
|
155
|
+
localStorage.setItem('devtunnel-view-mode', mode);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Legacy switchTab for compatibility
|
|
159
|
+
function switchTab(tab) {
|
|
160
|
+
if (tab === 'terminal') {
|
|
161
|
+
setViewMode('terminal');
|
|
162
|
+
} else {
|
|
163
|
+
setViewMode('editor');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Panel resizer functionality
|
|
168
|
+
function initPanelResizer() {
|
|
169
|
+
const resizer = document.getElementById('panel-resizer');
|
|
170
|
+
const terminalContainer = document.getElementById('terminal-container');
|
|
171
|
+
const editorContainer = document.getElementById('editor-container');
|
|
172
|
+
|
|
173
|
+
if (!resizer) return;
|
|
174
|
+
|
|
175
|
+
let startX, startTerminalWidth, startEditorWidth;
|
|
176
|
+
|
|
177
|
+
resizer.addEventListener('mousedown', (e) => {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
startX = e.clientX;
|
|
180
|
+
const terminalRect = terminalContainer.getBoundingClientRect();
|
|
181
|
+
const editorRect = editorContainer.getBoundingClientRect();
|
|
182
|
+
startTerminalWidth = terminalRect.width;
|
|
183
|
+
startEditorWidth = editorRect.width;
|
|
184
|
+
|
|
185
|
+
document.body.classList.add('resizing');
|
|
186
|
+
resizer.classList.add('dragging');
|
|
187
|
+
|
|
188
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
189
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
function onMouseMove(e) {
|
|
193
|
+
const dx = e.clientX - startX;
|
|
194
|
+
const totalWidth = startTerminalWidth + startEditorWidth;
|
|
195
|
+
const minWidth = 200;
|
|
196
|
+
const newTerminalWidth = Math.max(minWidth, Math.min(totalWidth - minWidth, startTerminalWidth + dx));
|
|
197
|
+
const newEditorWidth = totalWidth - newTerminalWidth;
|
|
198
|
+
|
|
199
|
+
terminalContainer.style.flex = `0 0 ${newTerminalWidth}px`;
|
|
200
|
+
editorContainer.style.flex = `0 0 ${newEditorWidth}px`;
|
|
201
|
+
|
|
202
|
+
fitAddon.fit();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function onMouseUp() {
|
|
206
|
+
document.body.classList.remove('resizing');
|
|
207
|
+
resizer.classList.remove('dragging');
|
|
208
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
209
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
210
|
+
|
|
211
|
+
// Save panel sizes
|
|
212
|
+
const terminalRect = terminalContainer.getBoundingClientRect();
|
|
213
|
+
const editorRect = editorContainer.getBoundingClientRect();
|
|
214
|
+
const total = terminalRect.width + editorRect.width;
|
|
215
|
+
localStorage.setItem('devtunnel-panel-ratio', (terminalRect.width / total).toFixed(3));
|
|
216
|
+
}
|
|
131
217
|
}
|
|
132
218
|
|
|
133
219
|
// File browser functions
|
|
@@ -227,7 +313,10 @@ function navigateToPath(path) {
|
|
|
227
313
|
|
|
228
314
|
// File operations
|
|
229
315
|
async function openFile(path) {
|
|
230
|
-
|
|
316
|
+
// In terminal-only mode, switch to split view to show editor
|
|
317
|
+
if (currentViewMode === 'terminal') {
|
|
318
|
+
setViewMode('split');
|
|
319
|
+
}
|
|
231
320
|
|
|
232
321
|
if (openFiles.has(path)) {
|
|
233
322
|
activateFile(path);
|
|
@@ -488,6 +577,7 @@ function promptRename(path) {
|
|
|
488
577
|
|
|
489
578
|
// Export functions
|
|
490
579
|
window.switchTab = switchTab;
|
|
580
|
+
window.setViewMode = setViewMode;
|
|
491
581
|
window.navigateToPath = navigateToPath;
|
|
492
582
|
window.openFile = openFile;
|
|
493
583
|
window.activateFile = activateFile;
|
|
@@ -539,6 +629,43 @@ term.loadAddon(webLinksAddon);
|
|
|
539
629
|
term.open(terminalContainer);
|
|
540
630
|
fitAddon.fit();
|
|
541
631
|
|
|
632
|
+
// IME 한글 입력 처리
|
|
633
|
+
let isComposing = false;
|
|
634
|
+
const xtermTextarea = terminalContainer.querySelector('.xterm-helper-textarea');
|
|
635
|
+
if (xtermTextarea) {
|
|
636
|
+
xtermTextarea.addEventListener('compositionstart', () => {
|
|
637
|
+
isComposing = true;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
xtermTextarea.addEventListener('compositionend', (e) => {
|
|
641
|
+
isComposing = false;
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Initialize panel resizer
|
|
646
|
+
initPanelResizer();
|
|
647
|
+
|
|
648
|
+
// Initialize view mode (default to split, or restore from localStorage)
|
|
649
|
+
const savedViewMode = localStorage.getItem('devtunnel-view-mode') || 'split';
|
|
650
|
+
setViewMode(savedViewMode);
|
|
651
|
+
|
|
652
|
+
// Restore panel ratio if saved
|
|
653
|
+
const savedRatio = localStorage.getItem('devtunnel-panel-ratio');
|
|
654
|
+
if (savedRatio && savedViewMode === 'split') {
|
|
655
|
+
setTimeout(() => {
|
|
656
|
+
const terminalContainer = document.getElementById('terminal-container');
|
|
657
|
+
const editorContainer = document.getElementById('editor-container');
|
|
658
|
+
const sidebar = document.querySelector('.sidebar');
|
|
659
|
+
const mainContainer = document.querySelector('.main-container');
|
|
660
|
+
const totalWidth = mainContainer.clientWidth - sidebar.clientWidth;
|
|
661
|
+
const terminalWidth = totalWidth * parseFloat(savedRatio);
|
|
662
|
+
const editorWidth = totalWidth - terminalWidth;
|
|
663
|
+
terminalContainer.style.flex = `0 0 ${terminalWidth}px`;
|
|
664
|
+
editorContainer.style.flex = `0 0 ${editorWidth}px`;
|
|
665
|
+
fitAddon.fit();
|
|
666
|
+
}, 100);
|
|
667
|
+
}
|
|
668
|
+
|
|
542
669
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
543
670
|
const wsUrl = `${protocol}//${window.location.host}`;
|
|
544
671
|
let ws;
|
|
@@ -613,7 +740,11 @@ function attachToTmux(sessionName) {
|
|
|
613
740
|
localStorage.removeItem('devtunnel-session-id');
|
|
614
741
|
term.clear();
|
|
615
742
|
ws.send(JSON.stringify({ type: 'attach-tmux', tmuxSession: sessionName }));
|
|
616
|
-
|
|
743
|
+
// In editor-only mode, switch to split view to show terminal
|
|
744
|
+
if (currentViewMode === 'editor') {
|
|
745
|
+
setViewMode('split');
|
|
746
|
+
}
|
|
747
|
+
term.focus();
|
|
617
748
|
}
|
|
618
749
|
|
|
619
750
|
function refreshTmuxSessions() {
|
|
@@ -688,7 +819,11 @@ function createNewSession() {
|
|
|
688
819
|
localStorage.removeItem('devtunnel-session-id');
|
|
689
820
|
term.clear();
|
|
690
821
|
ws.send(JSON.stringify({ type: 'attach', sessionId: null }));
|
|
691
|
-
|
|
822
|
+
// In editor-only mode, switch to split view to show terminal
|
|
823
|
+
if (currentViewMode === 'editor') {
|
|
824
|
+
setViewMode('split');
|
|
825
|
+
}
|
|
826
|
+
term.focus();
|
|
692
827
|
}
|
|
693
828
|
|
|
694
829
|
function attachToSession(sessionId) {
|
|
@@ -700,7 +835,11 @@ function attachToSession(sessionId) {
|
|
|
700
835
|
localStorage.setItem('devtunnel-session-id', sessionId);
|
|
701
836
|
term.clear();
|
|
702
837
|
ws.send(JSON.stringify({ type: 'attach', sessionId }));
|
|
703
|
-
|
|
838
|
+
// In editor-only mode, switch to split view to show terminal
|
|
839
|
+
if (currentViewMode === 'editor') {
|
|
840
|
+
setViewMode('split');
|
|
841
|
+
}
|
|
842
|
+
term.focus();
|
|
704
843
|
}
|
|
705
844
|
|
|
706
845
|
function killSession(sessionId) {
|
|
@@ -712,6 +851,16 @@ function toggleSection(section) {
|
|
|
712
851
|
content.style.display = content.style.display === 'none' ? 'block' : 'none';
|
|
713
852
|
}
|
|
714
853
|
|
|
854
|
+
// Mobile sidebar toggle
|
|
855
|
+
function toggleMobileSidebar() {
|
|
856
|
+
const sidebar = document.querySelector('.sidebar');
|
|
857
|
+
const overlay = document.getElementById('sidebar-overlay');
|
|
858
|
+
sidebar.classList.toggle('open');
|
|
859
|
+
overlay.classList.toggle('show');
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
window.toggleMobileSidebar = toggleMobileSidebar;
|
|
863
|
+
|
|
715
864
|
window.copyUrl = copyUrl;
|
|
716
865
|
window.stopTunnel = stopTunnel;
|
|
717
866
|
window.createNewSession = createNewSession;
|
|
@@ -809,6 +958,9 @@ function connect() {
|
|
|
809
958
|
}
|
|
810
959
|
|
|
811
960
|
term.onData((data) => {
|
|
961
|
+
// IME 조합 중이면 무시 (xterm이 compositionend 후 자동 처리)
|
|
962
|
+
if (isComposing) return;
|
|
963
|
+
|
|
812
964
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
813
965
|
ws.send(JSON.stringify({ type: 'input', data }));
|
|
814
966
|
}
|
package/public/index.html
CHANGED
|
@@ -27,21 +27,27 @@
|
|
|
27
27
|
<span id="status-text">Connecting...</span>
|
|
28
28
|
</div>
|
|
29
29
|
<button class="btn-logout" id="logout-btn" style="display: none;" onclick="logout()">Logout</button>
|
|
30
|
+
<button class="mobile-sidebar-toggle" id="sidebar-toggle" onclick="toggleMobileSidebar()">☰</button>
|
|
30
31
|
</div>
|
|
31
32
|
</div>
|
|
32
33
|
|
|
33
|
-
<!--
|
|
34
|
-
<div class="
|
|
35
|
-
<
|
|
36
|
-
<span class="
|
|
34
|
+
<!-- View Controls -->
|
|
35
|
+
<div class="view-controls">
|
|
36
|
+
<button class="view-btn" data-view="terminal" onclick="setViewMode('terminal')">
|
|
37
|
+
<span class="view-icon">></span>
|
|
37
38
|
Terminal
|
|
38
|
-
</
|
|
39
|
-
<
|
|
40
|
-
<span class="
|
|
39
|
+
</button>
|
|
40
|
+
<button class="view-btn active" data-view="split" onclick="setViewMode('split')">
|
|
41
|
+
<span class="view-icon">||</span>
|
|
42
|
+
Split
|
|
43
|
+
</button>
|
|
44
|
+
<button class="view-btn" data-view="editor" onclick="setViewMode('editor')">
|
|
45
|
+
<span class="view-icon">{}</span>
|
|
41
46
|
Editor
|
|
42
|
-
</
|
|
47
|
+
</button>
|
|
43
48
|
</div>
|
|
44
49
|
|
|
50
|
+
<div class="sidebar-overlay" id="sidebar-overlay" onclick="toggleMobileSidebar()"></div>
|
|
45
51
|
<div class="main-container">
|
|
46
52
|
<div id="terminal-container">
|
|
47
53
|
<div class="terminal-tabs" id="terminal-tabs">
|
|
@@ -50,6 +56,8 @@
|
|
|
50
56
|
<div id="terminal"></div>
|
|
51
57
|
</div>
|
|
52
58
|
|
|
59
|
+
<div class="panel-resizer" id="panel-resizer"></div>
|
|
60
|
+
|
|
53
61
|
<div id="editor-container">
|
|
54
62
|
<div class="editor-tabs" id="editor-tabs"></div>
|
|
55
63
|
<div id="monaco-editor"></div>
|
|
@@ -63,6 +71,7 @@
|
|
|
63
71
|
</div>
|
|
64
72
|
|
|
65
73
|
<div class="sidebar">
|
|
74
|
+
<button class="sidebar-close" onclick="toggleMobileSidebar()">×</button>
|
|
66
75
|
<!-- Files Section -->
|
|
67
76
|
<div class="sidebar-section">
|
|
68
77
|
<div class="sidebar-header" onclick="toggleSection('files')">
|
package/public/login.html
CHANGED
|
@@ -141,6 +141,38 @@
|
|
|
141
141
|
@keyframes spin {
|
|
142
142
|
to { transform: rotate(360deg); }
|
|
143
143
|
}
|
|
144
|
+
|
|
145
|
+
@media (max-width: 480px) {
|
|
146
|
+
.login-container {
|
|
147
|
+
margin: 16px;
|
|
148
|
+
padding: 24px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.logo {
|
|
152
|
+
width: 48px;
|
|
153
|
+
height: 48px;
|
|
154
|
+
margin-bottom: 16px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
h1 {
|
|
158
|
+
font-size: 20px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.subtitle {
|
|
162
|
+
font-size: 13px;
|
|
163
|
+
margin-bottom: 24px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.form-group input {
|
|
167
|
+
padding: 14px 12px;
|
|
168
|
+
font-size: 18px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.btn {
|
|
172
|
+
padding: 14px 24px;
|
|
173
|
+
font-size: 16px;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
144
176
|
</style>
|
|
145
177
|
</head>
|
|
146
178
|
<body>
|
package/public/styles.css
CHANGED
|
@@ -143,6 +143,7 @@ body {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
.sidebar {
|
|
146
|
+
position: relative;
|
|
146
147
|
width: 320px;
|
|
147
148
|
background-color: #161b22;
|
|
148
149
|
border-left: 1px solid #30363d;
|
|
@@ -443,18 +444,21 @@ body {
|
|
|
443
444
|
border-color: #f85149;
|
|
444
445
|
}
|
|
445
446
|
|
|
446
|
-
/*
|
|
447
|
-
.
|
|
447
|
+
/* View Controls */
|
|
448
|
+
.view-controls {
|
|
448
449
|
display: flex;
|
|
449
450
|
background-color: #161b22;
|
|
450
451
|
border-bottom: 1px solid #30363d;
|
|
451
452
|
padding: 0 12px;
|
|
453
|
+
gap: 4px;
|
|
452
454
|
}
|
|
453
455
|
|
|
454
|
-
.
|
|
455
|
-
padding:
|
|
456
|
+
.view-btn {
|
|
457
|
+
padding: 8px 16px;
|
|
456
458
|
font-size: 13px;
|
|
457
459
|
color: #8b949e;
|
|
460
|
+
background-color: transparent;
|
|
461
|
+
border: none;
|
|
458
462
|
cursor: pointer;
|
|
459
463
|
border-bottom: 2px solid transparent;
|
|
460
464
|
transition: all 0.15s ease;
|
|
@@ -463,18 +467,55 @@ body {
|
|
|
463
467
|
gap: 8px;
|
|
464
468
|
}
|
|
465
469
|
|
|
466
|
-
.
|
|
470
|
+
.view-btn:hover {
|
|
467
471
|
color: #c9d1d9;
|
|
468
472
|
background-color: #21262d;
|
|
469
473
|
}
|
|
470
474
|
|
|
471
|
-
.
|
|
475
|
+
.view-btn.active {
|
|
472
476
|
color: #c9d1d9;
|
|
473
477
|
border-bottom-color: #58a6ff;
|
|
474
478
|
}
|
|
475
479
|
|
|
476
|
-
.
|
|
480
|
+
.view-icon {
|
|
477
481
|
font-size: 14px;
|
|
482
|
+
font-family: monospace;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Panel Resizer */
|
|
486
|
+
.panel-resizer {
|
|
487
|
+
width: 4px;
|
|
488
|
+
background-color: #30363d;
|
|
489
|
+
cursor: col-resize;
|
|
490
|
+
flex-shrink: 0;
|
|
491
|
+
display: none;
|
|
492
|
+
transition: background-color 0.15s ease;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.panel-resizer:hover,
|
|
496
|
+
.panel-resizer.dragging {
|
|
497
|
+
background-color: #58a6ff;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
body.resizing {
|
|
501
|
+
cursor: col-resize !important;
|
|
502
|
+
user-select: none;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
body.resizing * {
|
|
506
|
+
cursor: col-resize !important;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* Split View Mode */
|
|
510
|
+
.main-container.split-view .panel-resizer {
|
|
511
|
+
display: block;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.main-container.split-view #terminal-container,
|
|
515
|
+
.main-container.split-view #editor-container {
|
|
516
|
+
flex: 1;
|
|
517
|
+
display: flex;
|
|
518
|
+
flex-direction: column;
|
|
478
519
|
}
|
|
479
520
|
|
|
480
521
|
/* Editor Container */
|
|
@@ -839,19 +880,386 @@ body {
|
|
|
839
880
|
}
|
|
840
881
|
}
|
|
841
882
|
|
|
883
|
+
/* Mobile Toggle Button */
|
|
884
|
+
.mobile-sidebar-toggle {
|
|
885
|
+
display: none;
|
|
886
|
+
width: 44px;
|
|
887
|
+
height: 44px;
|
|
888
|
+
background-color: #21262d;
|
|
889
|
+
border: 1px solid #30363d;
|
|
890
|
+
border-radius: 6px;
|
|
891
|
+
color: #8b949e;
|
|
892
|
+
font-size: 20px;
|
|
893
|
+
cursor: pointer;
|
|
894
|
+
align-items: center;
|
|
895
|
+
justify-content: center;
|
|
896
|
+
-webkit-tap-highlight-color: transparent;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
.mobile-sidebar-toggle:hover,
|
|
900
|
+
.mobile-sidebar-toggle:active {
|
|
901
|
+
background-color: #30363d;
|
|
902
|
+
color: #c9d1d9;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/* Mobile Sidebar Close */
|
|
906
|
+
.sidebar-close {
|
|
907
|
+
display: none;
|
|
908
|
+
position: absolute;
|
|
909
|
+
top: 12px;
|
|
910
|
+
right: 12px;
|
|
911
|
+
width: 32px;
|
|
912
|
+
height: 32px;
|
|
913
|
+
background-color: transparent;
|
|
914
|
+
border: none;
|
|
915
|
+
border-radius: 6px;
|
|
916
|
+
color: #8b949e;
|
|
917
|
+
font-size: 20px;
|
|
918
|
+
cursor: pointer;
|
|
919
|
+
align-items: center;
|
|
920
|
+
justify-content: center;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.sidebar-close:hover {
|
|
924
|
+
background-color: #21262d;
|
|
925
|
+
color: #c9d1d9;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/* Mobile Styles */
|
|
842
929
|
@media (max-width: 768px) {
|
|
930
|
+
.header {
|
|
931
|
+
padding: 10px 12px;
|
|
932
|
+
min-height: 56px;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.header h1 {
|
|
936
|
+
font-size: 16px;
|
|
937
|
+
gap: 8px;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
.header h1 .logo {
|
|
941
|
+
width: 24px;
|
|
942
|
+
height: 24px;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
.header-right {
|
|
946
|
+
gap: 10px;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
.system-info {
|
|
950
|
+
display: none;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.session-indicator {
|
|
954
|
+
display: none !important;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.status {
|
|
958
|
+
gap: 6px;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.status span {
|
|
962
|
+
display: none;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.status-dot {
|
|
966
|
+
width: 10px;
|
|
967
|
+
height: 10px;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.btn-logout {
|
|
971
|
+
padding: 8px 12px;
|
|
972
|
+
font-size: 12px;
|
|
973
|
+
min-height: 36px;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.mobile-sidebar-toggle {
|
|
977
|
+
display: flex;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
.view-controls {
|
|
981
|
+
padding: 0;
|
|
982
|
+
overflow-x: auto;
|
|
983
|
+
-webkit-overflow-scrolling: touch;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
.view-btn {
|
|
987
|
+
flex: 1;
|
|
988
|
+
padding: 12px 16px;
|
|
989
|
+
font-size: 13px;
|
|
990
|
+
justify-content: center;
|
|
991
|
+
min-height: 44px;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/* Disable split view on mobile */
|
|
995
|
+
.main-container.split-view .panel-resizer {
|
|
996
|
+
display: none;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
.main-container.split-view #editor-container {
|
|
1000
|
+
display: none;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.main-container.split-view #terminal-container {
|
|
1004
|
+
flex: 1;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
843
1007
|
.main-container {
|
|
844
1008
|
flex-direction: column;
|
|
1009
|
+
position: relative;
|
|
1010
|
+
flex: 1;
|
|
1011
|
+
overflow: hidden;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
#terminal-container {
|
|
1015
|
+
padding: 4px;
|
|
1016
|
+
flex: 1;
|
|
1017
|
+
display: flex;
|
|
1018
|
+
flex-direction: column;
|
|
1019
|
+
overflow: hidden;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
#terminal-container #terminal {
|
|
1023
|
+
flex: 1;
|
|
1024
|
+
min-height: 0;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#editor-container {
|
|
1028
|
+
flex: 1;
|
|
1029
|
+
overflow: hidden;
|
|
845
1030
|
}
|
|
846
1031
|
|
|
1032
|
+
.terminal-tabs {
|
|
1033
|
+
min-height: 40px;
|
|
1034
|
+
flex-shrink: 0;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.terminal-tab {
|
|
1038
|
+
padding: 10px 14px;
|
|
1039
|
+
font-size: 12px;
|
|
1040
|
+
min-height: 40px;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.terminal-tab-new {
|
|
1044
|
+
width: 36px;
|
|
1045
|
+
height: 36px;
|
|
1046
|
+
font-size: 18px;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.terminal-tab-close {
|
|
1050
|
+
width: 20px;
|
|
1051
|
+
height: 20px;
|
|
1052
|
+
font-size: 16px;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.editor-tabs {
|
|
1056
|
+
min-height: 40px;
|
|
1057
|
+
overflow-x: auto;
|
|
1058
|
+
-webkit-overflow-scrolling: touch;
|
|
1059
|
+
flex-shrink: 0;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.editor-tab {
|
|
1063
|
+
padding: 10px 14px;
|
|
1064
|
+
font-size: 12px;
|
|
1065
|
+
min-height: 40px;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.editor-tab-close {
|
|
1069
|
+
width: 20px;
|
|
1070
|
+
height: 20px;
|
|
1071
|
+
font-size: 16px;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/* Sidebar as overlay on mobile */
|
|
847
1075
|
.sidebar {
|
|
1076
|
+
position: fixed;
|
|
1077
|
+
top: 0;
|
|
1078
|
+
right: -100%;
|
|
1079
|
+
width: 85%;
|
|
1080
|
+
max-width: 320px;
|
|
1081
|
+
height: 100%;
|
|
1082
|
+
height: 100dvh;
|
|
1083
|
+
border-left: 1px solid #30363d;
|
|
1084
|
+
transition: right 0.25s ease-out;
|
|
1085
|
+
z-index: 100;
|
|
1086
|
+
overflow-y: auto;
|
|
1087
|
+
-webkit-overflow-scrolling: touch;
|
|
1088
|
+
padding-top: 50px;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
.sidebar.open {
|
|
1092
|
+
right: 0;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.sidebar-close {
|
|
1096
|
+
display: flex;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
.sidebar-overlay {
|
|
1100
|
+
display: none;
|
|
1101
|
+
position: fixed;
|
|
1102
|
+
top: 0;
|
|
1103
|
+
left: 0;
|
|
1104
|
+
right: 0;
|
|
1105
|
+
bottom: 0;
|
|
1106
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
1107
|
+
z-index: 99;
|
|
1108
|
+
opacity: 0;
|
|
1109
|
+
transition: opacity 0.25s ease-out;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.sidebar-overlay.show {
|
|
1113
|
+
display: block;
|
|
1114
|
+
opacity: 1;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
.sidebar-header {
|
|
1118
|
+
padding: 14px 16px;
|
|
1119
|
+
min-height: 48px;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.sidebar-content {
|
|
1123
|
+
max-height: none;
|
|
1124
|
+
padding: 8px 12px;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
#files-content {
|
|
1128
|
+
max-height: 40vh !important;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.tunnel-form {
|
|
1132
|
+
padding: 12px;
|
|
1133
|
+
flex-direction: column;
|
|
1134
|
+
gap: 10px;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.tunnel-form input {
|
|
1138
|
+
width: 100%;
|
|
1139
|
+
padding: 12px;
|
|
1140
|
+
font-size: 16px;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.tunnel-form .btn {
|
|
848
1144
|
width: 100%;
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
border-top: 1px solid #30363d;
|
|
1145
|
+
padding: 12px;
|
|
1146
|
+
min-height: 44px;
|
|
852
1147
|
}
|
|
853
1148
|
|
|
854
1149
|
.toast {
|
|
855
|
-
right:
|
|
1150
|
+
right: 12px;
|
|
1151
|
+
left: 12px;
|
|
1152
|
+
bottom: 12px;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
.reconnect-box {
|
|
1156
|
+
margin: 20px;
|
|
1157
|
+
padding: 24px;
|
|
1158
|
+
width: calc(100% - 40px);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.reconnect-btn {
|
|
1162
|
+
padding: 14px 24px;
|
|
1163
|
+
min-height: 48px;
|
|
1164
|
+
width: 100%;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.breadcrumb {
|
|
1168
|
+
padding: 10px 12px;
|
|
1169
|
+
font-size: 12px;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.breadcrumb-item {
|
|
1173
|
+
padding: 4px 8px;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.file-search {
|
|
1177
|
+
padding: 10px 12px;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.file-search input {
|
|
1181
|
+
padding: 10px 12px;
|
|
1182
|
+
font-size: 16px;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.file-item {
|
|
1186
|
+
padding: 10px 12px;
|
|
1187
|
+
min-height: 44px;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
.item-card {
|
|
1191
|
+
padding: 12px;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
.item-url {
|
|
1195
|
+
flex-direction: column;
|
|
1196
|
+
gap: 8px;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
.item-url input {
|
|
1200
|
+
width: 100%;
|
|
1201
|
+
padding: 10px;
|
|
1202
|
+
font-size: 13px;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.btn-copy {
|
|
1206
|
+
width: 100%;
|
|
1207
|
+
text-align: center;
|
|
1208
|
+
padding: 10px;
|
|
1209
|
+
min-height: 40px;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
.context-menu {
|
|
1213
|
+
min-width: 160px;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.context-menu-item {
|
|
1217
|
+
padding: 12px 16px;
|
|
1218
|
+
min-height: 44px;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
.modal {
|
|
1222
|
+
width: calc(100% - 32px);
|
|
1223
|
+
margin: 16px;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
.modal input {
|
|
1227
|
+
padding: 12px;
|
|
1228
|
+
font-size: 16px;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.modal-actions .btn {
|
|
1232
|
+
padding: 10px 16px;
|
|
1233
|
+
min-height: 40px;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/* Small mobile */
|
|
1238
|
+
@media (max-width: 480px) {
|
|
1239
|
+
.header h1 span {
|
|
1240
|
+
display: none;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.header h1 .logo {
|
|
1244
|
+
width: 28px;
|
|
1245
|
+
height: 28px;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
.btn-logout {
|
|
1249
|
+
padding: 6px 10px;
|
|
1250
|
+
font-size: 11px;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
.view-icon {
|
|
1254
|
+
display: none;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.view-btn {
|
|
1258
|
+
font-size: 12px;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.sidebar {
|
|
1262
|
+
width: 100%;
|
|
1263
|
+
max-width: none;
|
|
856
1264
|
}
|
|
857
1265
|
}
|
package/server.js
CHANGED
|
@@ -1104,7 +1104,7 @@ app.get('/health', (req, res) => {
|
|
|
1104
1104
|
});
|
|
1105
1105
|
});
|
|
1106
1106
|
|
|
1107
|
-
const DEFAULT_PORT = process.env.PORT || 3000;
|
|
1107
|
+
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
|
|
1108
1108
|
|
|
1109
1109
|
function findAvailablePort(startPort, maxAttempts = 10) {
|
|
1110
1110
|
return new Promise((resolve, reject) => {
|