@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.
@@ -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
- await waitForServer(port);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasano/devtunnel",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Web terminal with code editor and tunnel manager - access your dev environment from anywhere",
5
5
  "main": "server.js",
6
6
  "bin": {
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
- // Tab switching
114
- function switchTab(tab) {
115
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
116
- document.querySelector(`.tab[data-tab="${tab}"]`).classList.add('active');
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
- if (tab === 'terminal') {
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
- switchTab('editor');
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
- switchTab('terminal');
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
- switchTab('terminal');
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
- switchTab('terminal');
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()">&#9776;</button>
30
31
  </div>
31
32
  </div>
32
33
 
33
- <!-- Tab Bar -->
34
- <div class="tab-bar">
35
- <div class="tab active" data-tab="terminal" onclick="switchTab('terminal')">
36
- <span class="tab-icon">></span>
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
- </div>
39
- <div class="tab" data-tab="editor" onclick="switchTab('editor')">
40
- <span class="tab-icon">{}</span>
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
- </div>
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()">&times;</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
- /* Tab Bar */
447
- .tab-bar {
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
- .tab {
455
- padding: 10px 20px;
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
- .tab:hover {
470
+ .view-btn:hover {
467
471
  color: #c9d1d9;
468
472
  background-color: #21262d;
469
473
  }
470
474
 
471
- .tab.active {
475
+ .view-btn.active {
472
476
  color: #c9d1d9;
473
477
  border-bottom-color: #58a6ff;
474
478
  }
475
479
 
476
- .tab-icon {
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
- max-height: 250px;
850
- border-left: none;
851
- border-top: 1px solid #30363d;
1145
+ padding: 12px;
1146
+ min-height: 44px;
852
1147
  }
853
1148
 
854
1149
  .toast {
855
- right: 20px;
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) => {