@mjasano/devtunnel 1.5.0 → 1.5.2
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 +4 -1
- package/CHANGELOG.md +10 -0
- package/bin/cli.js +16 -1
- package/package.json +1 -1
- package/public/app.js +208 -9
- package/public/index.html +14 -8
- package/public/styles.css +64 -11
- package/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.2] - 2026-01-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Korean (Hangul) IME input on mobile - use dedicated input field for proper composition handling
|
|
12
|
+
|
|
13
|
+
## [1.5.1] - 2026-01-02
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Korean (Hangul) IME input on mobile devices - characters no longer split into individual jamo
|
|
17
|
+
|
|
8
18
|
## [1.5.0] - 2026-01-02
|
|
9
19
|
|
|
10
20
|
### 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,100 @@ term.loadAddon(webLinksAddon);
|
|
|
539
629
|
term.open(terminalContainer);
|
|
540
630
|
fitAddon.fit();
|
|
541
631
|
|
|
632
|
+
// 모바일 IME 한글 입력 처리
|
|
633
|
+
let isComposing = false;
|
|
634
|
+
let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
635
|
+
|
|
636
|
+
// 모바일용 숨겨진 입력 필드 생성
|
|
637
|
+
const mobileInput = document.createElement('input');
|
|
638
|
+
mobileInput.type = 'text';
|
|
639
|
+
mobileInput.autocapitalize = 'off';
|
|
640
|
+
mobileInput.autocomplete = 'off';
|
|
641
|
+
mobileInput.autocorrect = 'off';
|
|
642
|
+
mobileInput.spellcheck = false;
|
|
643
|
+
mobileInput.style.cssText = `
|
|
644
|
+
position: absolute;
|
|
645
|
+
left: -9999px;
|
|
646
|
+
top: 0;
|
|
647
|
+
width: 1px;
|
|
648
|
+
height: 1px;
|
|
649
|
+
opacity: 0;
|
|
650
|
+
z-index: -1;
|
|
651
|
+
`;
|
|
652
|
+
terminalContainer.appendChild(mobileInput);
|
|
653
|
+
|
|
654
|
+
mobileInput.addEventListener('compositionstart', () => {
|
|
655
|
+
isComposing = true;
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
mobileInput.addEventListener('compositionend', (e) => {
|
|
659
|
+
isComposing = false;
|
|
660
|
+
if (e.data && ws && ws.readyState === WebSocket.OPEN) {
|
|
661
|
+
ws.send(JSON.stringify({ type: 'input', data: e.data }));
|
|
662
|
+
}
|
|
663
|
+
mobileInput.value = '';
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
mobileInput.addEventListener('input', (e) => {
|
|
667
|
+
// 조합 중이 아닐 때만 전송 (영문, 숫자 등)
|
|
668
|
+
if (!isComposing && e.data && ws && ws.readyState === WebSocket.OPEN) {
|
|
669
|
+
ws.send(JSON.stringify({ type: 'input', data: e.data }));
|
|
670
|
+
mobileInput.value = '';
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
mobileInput.addEventListener('keydown', (e) => {
|
|
675
|
+
// 특수키 처리 (Enter, Backspace, Arrow 등)
|
|
676
|
+
if (!isComposing) {
|
|
677
|
+
let data = null;
|
|
678
|
+
switch (e.key) {
|
|
679
|
+
case 'Enter': data = '\r'; break;
|
|
680
|
+
case 'Backspace': data = '\x7f'; break;
|
|
681
|
+
case 'Tab': data = '\t'; e.preventDefault(); break;
|
|
682
|
+
case 'Escape': data = '\x1b'; break;
|
|
683
|
+
case 'ArrowUp': data = '\x1b[A'; break;
|
|
684
|
+
case 'ArrowDown': data = '\x1b[B'; break;
|
|
685
|
+
case 'ArrowRight': data = '\x1b[C'; break;
|
|
686
|
+
case 'ArrowLeft': data = '\x1b[D'; break;
|
|
687
|
+
}
|
|
688
|
+
if (data && ws && ws.readyState === WebSocket.OPEN) {
|
|
689
|
+
ws.send(JSON.stringify({ type: 'input', data }));
|
|
690
|
+
if (e.key !== 'Tab') mobileInput.value = '';
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// 모바일에서 터미널 클릭 시 숨겨진 입력 필드에 포커스
|
|
696
|
+
if (isMobile) {
|
|
697
|
+
terminalContainer.addEventListener('click', () => {
|
|
698
|
+
mobileInput.focus();
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Initialize panel resizer
|
|
703
|
+
initPanelResizer();
|
|
704
|
+
|
|
705
|
+
// Initialize view mode (default to split, or restore from localStorage)
|
|
706
|
+
const savedViewMode = localStorage.getItem('devtunnel-view-mode') || 'split';
|
|
707
|
+
setViewMode(savedViewMode);
|
|
708
|
+
|
|
709
|
+
// Restore panel ratio if saved
|
|
710
|
+
const savedRatio = localStorage.getItem('devtunnel-panel-ratio');
|
|
711
|
+
if (savedRatio && savedViewMode === 'split') {
|
|
712
|
+
setTimeout(() => {
|
|
713
|
+
const terminalContainer = document.getElementById('terminal-container');
|
|
714
|
+
const editorContainer = document.getElementById('editor-container');
|
|
715
|
+
const sidebar = document.querySelector('.sidebar');
|
|
716
|
+
const mainContainer = document.querySelector('.main-container');
|
|
717
|
+
const totalWidth = mainContainer.clientWidth - sidebar.clientWidth;
|
|
718
|
+
const terminalWidth = totalWidth * parseFloat(savedRatio);
|
|
719
|
+
const editorWidth = totalWidth - terminalWidth;
|
|
720
|
+
terminalContainer.style.flex = `0 0 ${terminalWidth}px`;
|
|
721
|
+
editorContainer.style.flex = `0 0 ${editorWidth}px`;
|
|
722
|
+
fitAddon.fit();
|
|
723
|
+
}, 100);
|
|
724
|
+
}
|
|
725
|
+
|
|
542
726
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
543
727
|
const wsUrl = `${protocol}//${window.location.host}`;
|
|
544
728
|
let ws;
|
|
@@ -613,7 +797,11 @@ function attachToTmux(sessionName) {
|
|
|
613
797
|
localStorage.removeItem('devtunnel-session-id');
|
|
614
798
|
term.clear();
|
|
615
799
|
ws.send(JSON.stringify({ type: 'attach-tmux', tmuxSession: sessionName }));
|
|
616
|
-
|
|
800
|
+
// In editor-only mode, switch to split view to show terminal
|
|
801
|
+
if (currentViewMode === 'editor') {
|
|
802
|
+
setViewMode('split');
|
|
803
|
+
}
|
|
804
|
+
term.focus();
|
|
617
805
|
}
|
|
618
806
|
|
|
619
807
|
function refreshTmuxSessions() {
|
|
@@ -688,7 +876,11 @@ function createNewSession() {
|
|
|
688
876
|
localStorage.removeItem('devtunnel-session-id');
|
|
689
877
|
term.clear();
|
|
690
878
|
ws.send(JSON.stringify({ type: 'attach', sessionId: null }));
|
|
691
|
-
|
|
879
|
+
// In editor-only mode, switch to split view to show terminal
|
|
880
|
+
if (currentViewMode === 'editor') {
|
|
881
|
+
setViewMode('split');
|
|
882
|
+
}
|
|
883
|
+
term.focus();
|
|
692
884
|
}
|
|
693
885
|
|
|
694
886
|
function attachToSession(sessionId) {
|
|
@@ -700,7 +892,11 @@ function attachToSession(sessionId) {
|
|
|
700
892
|
localStorage.setItem('devtunnel-session-id', sessionId);
|
|
701
893
|
term.clear();
|
|
702
894
|
ws.send(JSON.stringify({ type: 'attach', sessionId }));
|
|
703
|
-
|
|
895
|
+
// In editor-only mode, switch to split view to show terminal
|
|
896
|
+
if (currentViewMode === 'editor') {
|
|
897
|
+
setViewMode('split');
|
|
898
|
+
}
|
|
899
|
+
term.focus();
|
|
704
900
|
}
|
|
705
901
|
|
|
706
902
|
function killSession(sessionId) {
|
|
@@ -819,6 +1015,9 @@ function connect() {
|
|
|
819
1015
|
}
|
|
820
1016
|
|
|
821
1017
|
term.onData((data) => {
|
|
1018
|
+
// 모바일에서는 mobileInput으로 처리
|
|
1019
|
+
if (isMobile) return;
|
|
1020
|
+
|
|
822
1021
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
823
1022
|
ws.send(JSON.stringify({ type: 'input', data }));
|
|
824
1023
|
}
|
package/public/index.html
CHANGED
|
@@ -31,16 +31,20 @@
|
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
-
<!--
|
|
35
|
-
<div class="
|
|
36
|
-
<
|
|
37
|
-
<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>
|
|
38
38
|
Terminal
|
|
39
|
-
</
|
|
40
|
-
<
|
|
41
|
-
<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>
|
|
42
46
|
Editor
|
|
43
|
-
</
|
|
47
|
+
</button>
|
|
44
48
|
</div>
|
|
45
49
|
|
|
46
50
|
<div class="sidebar-overlay" id="sidebar-overlay" onclick="toggleMobileSidebar()"></div>
|
|
@@ -52,6 +56,8 @@
|
|
|
52
56
|
<div id="terminal"></div>
|
|
53
57
|
</div>
|
|
54
58
|
|
|
59
|
+
<div class="panel-resizer" id="panel-resizer"></div>
|
|
60
|
+
|
|
55
61
|
<div id="editor-container">
|
|
56
62
|
<div class="editor-tabs" id="editor-tabs"></div>
|
|
57
63
|
<div id="monaco-editor"></div>
|
package/public/styles.css
CHANGED
|
@@ -444,18 +444,21 @@ body {
|
|
|
444
444
|
border-color: #f85149;
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
/*
|
|
448
|
-
.
|
|
447
|
+
/* View Controls */
|
|
448
|
+
.view-controls {
|
|
449
449
|
display: flex;
|
|
450
450
|
background-color: #161b22;
|
|
451
451
|
border-bottom: 1px solid #30363d;
|
|
452
452
|
padding: 0 12px;
|
|
453
|
+
gap: 4px;
|
|
453
454
|
}
|
|
454
455
|
|
|
455
|
-
.
|
|
456
|
-
padding:
|
|
456
|
+
.view-btn {
|
|
457
|
+
padding: 8px 16px;
|
|
457
458
|
font-size: 13px;
|
|
458
459
|
color: #8b949e;
|
|
460
|
+
background-color: transparent;
|
|
461
|
+
border: none;
|
|
459
462
|
cursor: pointer;
|
|
460
463
|
border-bottom: 2px solid transparent;
|
|
461
464
|
transition: all 0.15s ease;
|
|
@@ -464,18 +467,55 @@ body {
|
|
|
464
467
|
gap: 8px;
|
|
465
468
|
}
|
|
466
469
|
|
|
467
|
-
.
|
|
470
|
+
.view-btn:hover {
|
|
468
471
|
color: #c9d1d9;
|
|
469
472
|
background-color: #21262d;
|
|
470
473
|
}
|
|
471
474
|
|
|
472
|
-
.
|
|
475
|
+
.view-btn.active {
|
|
473
476
|
color: #c9d1d9;
|
|
474
477
|
border-bottom-color: #58a6ff;
|
|
475
478
|
}
|
|
476
479
|
|
|
477
|
-
.
|
|
480
|
+
.view-icon {
|
|
478
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;
|
|
479
519
|
}
|
|
480
520
|
|
|
481
521
|
/* Editor Container */
|
|
@@ -937,13 +977,13 @@ body {
|
|
|
937
977
|
display: flex;
|
|
938
978
|
}
|
|
939
979
|
|
|
940
|
-
.
|
|
980
|
+
.view-controls {
|
|
941
981
|
padding: 0;
|
|
942
982
|
overflow-x: auto;
|
|
943
983
|
-webkit-overflow-scrolling: touch;
|
|
944
984
|
}
|
|
945
985
|
|
|
946
|
-
.
|
|
986
|
+
.view-btn {
|
|
947
987
|
flex: 1;
|
|
948
988
|
padding: 12px 16px;
|
|
949
989
|
font-size: 13px;
|
|
@@ -951,6 +991,19 @@ body {
|
|
|
951
991
|
min-height: 44px;
|
|
952
992
|
}
|
|
953
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
|
+
|
|
954
1007
|
.main-container {
|
|
955
1008
|
flex-direction: column;
|
|
956
1009
|
position: relative;
|
|
@@ -1197,11 +1250,11 @@ body {
|
|
|
1197
1250
|
font-size: 11px;
|
|
1198
1251
|
}
|
|
1199
1252
|
|
|
1200
|
-
.
|
|
1253
|
+
.view-icon {
|
|
1201
1254
|
display: none;
|
|
1202
1255
|
}
|
|
1203
1256
|
|
|
1204
|
-
.
|
|
1257
|
+
.view-btn {
|
|
1205
1258
|
font-size: 12px;
|
|
1206
1259
|
}
|
|
1207
1260
|
|
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) => {
|