rms-devremote 3.1.0 → 3.2.0

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/dist/index.js CHANGED
@@ -4,7 +4,7 @@ const program = new Command();
4
4
  program
5
5
  .name('rms-devremote')
6
6
  .description('Share your local terminal remotely with push notifications')
7
- .version('2.0.0');
7
+ .version('3.2.0');
8
8
  // Helper: lazy-load a command module by path (relative to dist/)
9
9
  async function runCommand(modulePath) {
10
10
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -32,6 +32,8 @@ export function buildFrontendHTML() {
32
32
  <span class="h-status" id="status-text">connecting</span>
33
33
  </div>
34
34
  <div class="h-right">
35
+ <button class="h-btn" id="btn-wins" title="Windows"><span id="win-label">1</span></button>
36
+ <button class="h-btn" id="btn-new-win" title="New window">+</button>
35
37
  <button class="h-btn" id="btn-kb" title="Keyboard">&#x2328;</button>
36
38
  <button class="h-btn" id="btn-toggle" title="Toolbar">&#x2699;</button>
37
39
  </div>
@@ -46,12 +48,9 @@ export function buildFrontendHTML() {
46
48
  <div class="info-row"><span class="info-label">Uptime</span><span class="info-value" id="info-uptime">--</span></div>
47
49
  </div>
48
50
 
49
- <!-- Tab bar (tmux windows) -->
50
- <div id="tab-bar" class="hidden">
51
- <div id="tabs-scroll">
52
- <div id="tabs-list"></div>
53
- <button id="tab-add" title="New window">+</button>
54
- </div>
51
+ <!-- Window selector (dropdown from header) -->
52
+ <div id="win-dropdown" class="hidden">
53
+ <div id="win-list"></div>
55
54
  </div>
56
55
 
57
56
  <!-- Terminal -->
@@ -133,7 +132,7 @@ const CSS = `
133
132
  --muted: #888888;
134
133
  --border: #333333;
135
134
  --header-h: 44px;
136
- --tabbar-h: 38px;
135
+ --tabbar-h: 0px;
137
136
  --input-h: 48px;
138
137
  --toolbar-h: 104px;
139
138
  --safe-t: env(safe-area-inset-top, 0px);
@@ -209,7 +208,7 @@ body { touch-action: none; }
209
208
  /* ── Info panel ──────────────────────────── */
210
209
  #info-panel {
211
210
  position: fixed;
212
- top: calc(var(--header-h) + var(--tabbar-h) + var(--safe-t));
211
+ top: calc(var(--header-h) + var(--safe-t));
213
212
  left: 0; right: 0;
214
213
  background: var(--surface);
215
214
  border-bottom: 2px solid var(--border);
@@ -230,76 +229,53 @@ body { touch-action: none; }
230
229
  .info-value.err { color: var(--danger); }
231
230
  .info-value.info-path { font-size: 12px; max-width: 60%; text-align: right; word-break: break-all; }
232
231
 
233
- /* ── Tab bar ────────────────────────────── */
234
- #tab-bar {
232
+ /* ── Window selector dropdown ───────────── */
233
+ #btn-wins {
234
+ font-size: 14px; font-weight: 800; min-width: 36px;
235
+ }
236
+ #btn-wins.active { border-color: var(--accent); color: var(--accent); background: rgba(0,255,170,0.08); }
237
+ #btn-new-win { font-size: 22px; font-weight: 400; }
238
+ #win-dropdown {
235
239
  position: fixed;
236
240
  top: calc(var(--header-h) + var(--safe-t));
237
- left: 0; right: 0;
238
- height: var(--tabbar-h);
241
+ right: 0; left: 0;
239
242
  background: var(--surface);
240
- border-bottom: 1px solid var(--border);
241
- z-index: 95;
242
- display: flex; align-items: center;
243
- }
244
- #tab-bar.hidden { display: none !important; }
245
- #tabs-scroll {
246
- display: flex; align-items: center;
247
- overflow-x: auto; overflow-y: hidden;
248
- width: 100%; height: 100%;
249
- padding: 0 calc(6px + var(--safe-l)) 0 calc(6px + var(--safe-r));
250
- gap: 4px;
251
- scrollbar-width: none;
252
- -webkit-overflow-scrolling: touch;
253
- }
254
- #tabs-scroll::-webkit-scrollbar { display: none; }
255
- #tabs-list {
256
- display: flex; align-items: center; gap: 4px;
257
- flex-shrink: 0;
243
+ border-bottom: 2px solid var(--border);
244
+ padding: 8px calc(14px + var(--safe-l)) 10px calc(14px + var(--safe-r));
245
+ z-index: 92;
246
+ animation: slideDown 0.15s ease-out;
247
+ display: flex; flex-direction: column; gap: 4px;
258
248
  }
259
- .tab-item {
260
- display: flex; align-items: center; gap: 6px;
261
- flex-shrink: 0;
262
- height: 28px; padding: 0 10px;
263
- background: var(--surface2); border: 1px solid var(--border);
264
- border-radius: 6px;
265
- font-size: 12px; font-weight: 600;
266
- color: var(--muted); cursor: pointer;
249
+ #win-dropdown.hidden { display: none !important; }
250
+ .win-row {
251
+ display: flex; align-items: center; justify-content: space-between;
252
+ padding: 10px 12px;
253
+ background: var(--surface2); border: 2px solid var(--border);
254
+ border-radius: 8px; cursor: pointer;
267
255
  -webkit-tap-highlight-color: transparent;
268
- transition: background 0.12s, border-color 0.12s, color 0.12s;
269
- white-space: nowrap;
256
+ transition: border-color 0.12s, background 0.12s;
270
257
  }
271
- .tab-item:active { background: #222; }
272
- .tab-item.active {
273
- background: rgba(0,255,170,0.1);
274
- border-color: var(--accent);
275
- color: var(--accent);
258
+ .win-row:active { background: #222; }
259
+ .win-row.active { border-color: var(--accent); background: rgba(0,255,170,0.06); }
260
+ .win-name {
261
+ font-size: 14px; font-weight: 600; color: var(--text);
262
+ font-family: 'JetBrains Mono', monospace;
276
263
  }
277
- .tab-close {
264
+ .win-row.active .win-name { color: var(--accent); }
265
+ .win-close {
266
+ width: 24px; height: 24px;
278
267
  display: flex; align-items: center; justify-content: center;
279
- width: 16px; height: 16px;
280
- font-size: 13px; font-weight: 700;
281
- color: var(--muted); border: none; background: none;
282
- border-radius: 3px; cursor: pointer; padding: 0;
268
+ font-size: 16px; font-weight: 700;
269
+ color: var(--muted); background: none; border: none;
270
+ border-radius: 4px; cursor: pointer;
283
271
  -webkit-tap-highlight-color: transparent;
284
- line-height: 1;
285
272
  }
286
- .tab-close:active { background: rgba(255,51,85,0.3); color: var(--danger); }
287
- #tab-add {
288
- flex-shrink: 0;
289
- width: 28px; height: 28px;
290
- background: var(--surface2); border: 1px dashed var(--border);
291
- border-radius: 6px;
292
- color: var(--muted); font-size: 18px; font-weight: 400;
293
- cursor: pointer; display: flex; align-items: center; justify-content: center;
294
- -webkit-tap-highlight-color: transparent;
295
- transition: border-color 0.12s, color 0.12s;
296
- }
297
- #tab-add:active { border-color: var(--accent); color: var(--accent); }
273
+ .win-close:active { background: rgba(255,51,85,0.3); color: var(--danger); }
298
274
 
299
275
  /* ── Terminal ───────────────────────────── */
300
276
  #terminal-container {
301
277
  position: fixed;
302
- top: calc(var(--header-h) + var(--tabbar-h) + var(--safe-t)); left: 0; right: 0;
278
+ top: calc(var(--header-h) + var(--safe-t)); left: 0; right: 0;
303
279
  bottom: calc(var(--input-h) + var(--toolbar-h) + var(--safe-b));
304
280
  background: var(--bg);
305
281
  opacity: 0; animation: fadeIn 0.3s 0.2s forwards;
@@ -441,7 +417,6 @@ body.keyboard-open #terminal-container { bottom: calc(var(--input-h) + var(--saf
441
417
  @media (orientation: landscape) {
442
418
  :root {
443
419
  --header-h: 36px;
444
- --tabbar-h: 32px;
445
420
  --input-h: 40px;
446
421
  --toolbar-h: 52px;
447
422
  }
@@ -557,7 +532,7 @@ const JS = `
557
532
  container.classList.add('hidden');
558
533
  inputBar.classList.add('hidden');
559
534
  toolbar.classList.add('hidden');
560
- tabBar.classList.add('hidden');
535
+ toggleWinDropdown(false);
561
536
  disconnect.classList.remove('hidden');
562
537
  dcTitle.textContent = title;
563
538
  dcReason.textContent = reason;
@@ -909,48 +884,54 @@ const JS = `
909
884
  navigator.serviceWorker.register('/sw.js').catch(function() {});
910
885
  }
911
886
 
912
- // ── Tab bar (tmux windows) ──────────────────────────────
913
- var tabBar = document.getElementById('tab-bar');
914
- var tabsList = document.getElementById('tabs-list');
915
- var tabAdd = document.getElementById('tab-add');
887
+ // ── Window management (header buttons + dropdown) ───────
888
+ var btnWins = document.getElementById('btn-wins');
889
+ var btnNewWin = document.getElementById('btn-new-win');
890
+ var winLabel = document.getElementById('win-label');
891
+ var winDropdown = document.getElementById('win-dropdown');
892
+ var winList = document.getElementById('win-list');
916
893
  var windowsCache = [];
917
894
  var windowsPollTimer = null;
895
+ var winDropdownOpen = false;
918
896
 
919
897
  function fetchWindows() {
920
898
  fetch('/windows', { credentials: 'same-origin' })
921
899
  .then(function(r) { return r.json(); })
922
900
  .then(function(wins) {
923
901
  windowsCache = wins;
924
- renderTabs(wins);
902
+ updateWinLabel(wins);
903
+ if (winDropdownOpen) renderWinDropdown(wins);
925
904
  })
926
905
  .catch(function() {});
927
906
  }
928
907
 
929
- function renderTabs(wins) {
930
- // Clear tabs safely (no innerHTML)
931
- while (tabsList.firstChild) tabsList.removeChild(tabsList.firstChild);
932
- if (!wins || wins.length === 0) return;
908
+ function updateWinLabel(wins) {
909
+ if (!wins || wins.length === 0) { winLabel.textContent = '0'; return; }
910
+ var active = wins.find(function(w) { return w.active; });
911
+ winLabel.textContent = active ? active.index : wins.length;
912
+ }
933
913
 
934
- // Show tab bar only when connected
935
- if (connected) tabBar.classList.remove('hidden');
914
+ function renderWinDropdown(wins) {
915
+ while (winList.firstChild) winList.removeChild(winList.firstChild);
916
+ if (!wins) return;
936
917
 
937
918
  wins.forEach(function(w) {
938
- var tab = document.createElement('div');
939
- tab.className = 'tab-item' + (w.active ? ' active' : '');
919
+ var row = document.createElement('div');
920
+ row.className = 'win-row' + (w.active ? ' active' : '');
940
921
 
941
- var label = document.createElement('span');
942
- label.textContent = w.index + ':' + (w.name || 'bash');
943
- tab.appendChild(label);
922
+ var name = document.createElement('span');
923
+ name.className = 'win-name';
924
+ name.textContent = w.index + ' : ' + (w.name || 'bash');
925
+ row.appendChild(name);
944
926
 
945
927
  // Close button (only if more than 1 window)
946
928
  if (wins.length > 1) {
947
929
  var closeBtn = document.createElement('button');
948
- closeBtn.className = 'tab-close';
930
+ closeBtn.className = 'win-close';
949
931
  closeBtn.textContent = '\\u00d7';
950
932
  closeBtn.setAttribute('tabindex', '-1');
951
933
  closeBtn.addEventListener('touchstart', function(e) {
952
- e.stopPropagation();
953
- e.preventDefault();
934
+ e.stopPropagation(); e.preventDefault();
954
935
  if (navigator.vibrate) navigator.vibrate(VIBRATE);
955
936
  sendWsMsg({ type: 'window-close', index: w.index });
956
937
  }, { passive: false });
@@ -959,31 +940,44 @@ const JS = `
959
940
  if (navigator.vibrate) navigator.vibrate(VIBRATE);
960
941
  sendWsMsg({ type: 'window-close', index: w.index });
961
942
  });
962
- tab.appendChild(closeBtn);
943
+ row.appendChild(closeBtn);
963
944
  }
964
945
 
965
946
  // Switch on tap
966
- tab.addEventListener('touchstart', function(e) {
947
+ row.addEventListener('touchstart', function(e) {
967
948
  e.preventDefault();
968
949
  if (navigator.vibrate) navigator.vibrate(VIBRATE);
969
950
  sendWsMsg({ type: 'window-switch', index: w.index });
951
+ toggleWinDropdown(false);
970
952
  }, { passive: false });
971
- tab.addEventListener('click', function() {
953
+ row.addEventListener('click', function() {
972
954
  if (navigator.vibrate) navigator.vibrate(VIBRATE);
973
955
  sendWsMsg({ type: 'window-switch', index: w.index });
956
+ toggleWinDropdown(false);
974
957
  });
975
958
 
976
- tabsList.appendChild(tab);
959
+ winList.appendChild(row);
977
960
  });
978
961
  }
979
962
 
963
+ function toggleWinDropdown(force) {
964
+ winDropdownOpen = force !== undefined ? force : !winDropdownOpen;
965
+ winDropdown.classList.toggle('hidden', !winDropdownOpen);
966
+ btnWins.classList.toggle('active', winDropdownOpen);
967
+ // Close info panel if open
968
+ if (winDropdownOpen && infoOpen) { infoOpen = false; infoPanel.classList.add('hidden'); }
969
+ if (winDropdownOpen) renderWinDropdown(windowsCache);
970
+ }
971
+
972
+ btnWins.addEventListener('click', function() { toggleWinDropdown(); });
973
+
980
974
  // New window button
981
- tabAdd.addEventListener('touchstart', function(e) {
975
+ btnNewWin.addEventListener('touchstart', function(e) {
982
976
  e.preventDefault();
983
977
  if (navigator.vibrate) navigator.vibrate(VIBRATE);
984
978
  sendWsMsg({ type: 'window-create' });
985
979
  }, { passive: false });
986
- tabAdd.addEventListener('click', function() {
980
+ btnNewWin.addEventListener('click', function() {
987
981
  if (navigator.vibrate) navigator.vibrate(VIBRATE);
988
982
  sendWsMsg({ type: 'window-create' });
989
983
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rms-devremote",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Control your terminal remotely from your phone — mobile PWA with push notifications and zero open ports",
5
5
  "type": "module",
6
6
  "bin": {