kokokoasd commited on
Commit
fa5b587
·
verified ·
1 Parent(s): 70bd544

Upload 19 files

Browse files
__pycache__/app.cpython-311.pyc ADDED
Binary file (2.01 kB). View file
 
__pycache__/config.cpython-311.pyc ADDED
Binary file (951 Bytes). View file
 
routers/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (1.15 kB). View file
 
routers/__pycache__/terminal.cpython-311.pyc ADDED
Binary file (10.5 kB). View file
 
static/app.js CHANGED
@@ -14,7 +14,9 @@ let termDataDisposable = null;
14
  let termResizeDisposable = null;
15
  let termCurrentZone = null; // tracks which zone the terminal is connected to
16
  let promptResolve = null;
17
- let isMobile = window.matchMedia("(max-width: 768px)").matches;
 
 
18
  let currentMobileTab = "files";
19
 
20
  // ── Init ──────────────────────────────────────
@@ -24,10 +26,11 @@ document.addEventListener("DOMContentLoaded", () => {
24
  initResizers();
25
  bindEvents();
26
 
27
- // Track mobile state on resize
28
- const mq = window.matchMedia("(max-width: 768px)");
29
  mq.addEventListener("change", (e) => {
30
  isMobile = e.matches;
 
31
  if (!isMobile) {
32
  // Exiting mobile: reset panel visibility
33
  toggleSidebar(false);
@@ -40,6 +43,9 @@ document.addEventListener("DOMContentLoaded", () => {
40
  }
41
  });
42
 
 
 
 
43
  // Apply initial mobile tab if on mobile
44
  if (isMobile && currentZone) {
45
  switchMobileTab("files");
@@ -82,7 +88,7 @@ async function loadZones() {
82
  list.innerHTML = `<li class="empty-hint" style="color:var(--text-3);font-size:12px;padding:8px 10px;cursor:default;opacity:0.6">No zones yet</li>`;
83
  } else {
84
  list.innerHTML = zones.map(z => `
85
- <li data-zone="${escapeAttr(z.name)}" class="${currentZone === z.name ? 'active' : ''}" onclick="openZone('${escapeAttr(z.name)}')">
86
  <span class="zone-icon"><i data-lucide="box"></i></span>
87
  <span class="zone-name">${escapeHtml(z.name)}</span>
88
  </li>
@@ -97,7 +103,7 @@ async function loadZones() {
97
  welcomeZones.innerHTML = `<p class="welcome-hint">No zones yet — create one to get started</p>`;
98
  } else {
99
  welcomeZones.innerHTML = `<div class="zone-grid">${zones.map(z => `
100
- <button class="zone-card" onclick="openZone('${escapeAttr(z.name)}')">
101
  <i data-lucide="box"></i>
102
  <span>${escapeHtml(z.name)}</span>
103
  </button>
@@ -163,7 +169,8 @@ async function createZone() {
163
 
164
  async function deleteZone() {
165
  if (!currentZone) return;
166
- if (!confirm(`Delete zone "${currentZone}"? All data will be lost!`)) return;
 
167
  try {
168
  await api(`/api/zones/${currentZone}`, { method: "DELETE" });
169
  disconnectTerminal();
@@ -187,12 +194,12 @@ async function loadFiles() {
187
 
188
  function renderBreadcrumb() {
189
  const parts = currentPath ? currentPath.split("/").filter(Boolean) : [];
190
- let html = `<span onclick="navigateTo('')">~</span>`;
191
  let path = "";
192
  for (const part of parts) {
193
  path += (path ? "/" : "") + part;
194
  html += `<span class="sep">/</span>`;
195
- html += `<span onclick="navigateTo('${escapeAttr(path)}')">${escapeHtml(part)}</span>`;
196
  }
197
  // Update both desktop and mobile breadcrumbs
198
  document.getElementById("breadcrumb").innerHTML = html;
@@ -209,7 +216,7 @@ function renderFiles(files) {
209
  }
210
  let html = "";
211
  if (currentPath) {
212
- html += `<div class="file-item" onclick="navigateUp()">
213
  <span class="fi-icon fi-icon-back"><i data-lucide="corner-left-up"></i></span>
214
  <span class="fi-name">..</span>
215
  </div>`;
@@ -223,16 +230,16 @@ function renderFiles(files) {
223
  const iconClass = f.is_dir ? "fi-icon-folder" : fileIconClass(f.name);
224
  const iconName = f.is_dir ? "folder" : "file-text";
225
  const size = f.is_dir ? "" : formatSize(f.size);
226
- const action = f.is_dir ? `navigateTo('${escapeAttr(relPath)}')` : `editFile('${escapeAttr(relPath)}')`;
227
 
228
- html += `<div class="file-item" onclick="${action}">
229
  <span class="fi-icon ${iconClass}"><i data-lucide="${iconName}"></i></span>
230
  <span class="fi-name">${escapeHtml(f.name)}</span>
231
  <span class="fi-size">${size}</span>
232
  <span class="fi-actions">
233
- ${!f.is_dir ? `<button title="Download" onclick="event.stopPropagation();downloadFile('${escapeAttr(relPath)}')"><i data-lucide="download"></i></button>` : ''}
234
- <button title="Rename" onclick="event.stopPropagation();renameFile('${escapeAttr(relPath)}','${escapeAttr(f.name)}')"><i data-lucide="pencil"></i></button>
235
- <button class="fi-del" title="Delete" onclick="event.stopPropagation();deleteFile('${escapeAttr(relPath)}')"><i data-lucide="trash-2"></i></button>
236
  </span>
237
  </div>`;
238
  }
@@ -354,7 +361,8 @@ function downloadFile(relPath) {
354
  }
355
 
356
  async function deleteFile(relPath) {
357
- if (!confirm(`Delete "${relPath}"?`)) return;
 
358
  try {
359
  await api(`/api/zones/${currentZone}/files?path=${encodeURIComponent(relPath)}`, { method: "DELETE" });
360
  toast("Deleted", "info");
@@ -583,7 +591,7 @@ function initResizers() {
583
  }
584
  }
585
 
586
- // ── Modal / Prompt ───────────────────────────
587
  function showModal(id) {
588
  document.getElementById(id).classList.remove("hidden");
589
  lucide.createIcons({ nodes: [document.getElementById(id)] });
@@ -603,6 +611,20 @@ function promptUser(title, defaultValue) {
603
  });
604
  }
605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  // ── Port Management ──────────────────────────
607
  async function loadPorts() {
608
  if (!currentZone) return;
@@ -632,9 +654,9 @@ function renderPorts(ports) {
632
  <span class="pi-port">:${p.port}</span>
633
  <span class="pi-label">${escapeHtml(p.label)}</span>
634
  <span class="pi-actions">
635
- <button title="Open in new tab" onclick="openPort(${p.port})"><i data-lucide="external-link"></i></button>
636
- <button title="Copy URL" onclick="copyPortUrl(${p.port})"><i data-lucide="copy"></i></button>
637
- <button class="pi-del" title="Remove" onclick="removePort(${p.port})"><i data-lucide="trash-2"></i></button>
638
  </span>
639
  </div>
640
  `).join("");
@@ -679,7 +701,8 @@ async function addPort() {
679
  }
680
 
681
  async function removePort(port) {
682
- if (!confirm(`Remove port ${port} mapping?`)) return;
 
683
  try {
684
  await api(`/api/zones/${currentZone}/ports/${port}`, { method: "DELETE" });
685
  toast(`Port ${port} removed`, "info");
@@ -793,6 +816,7 @@ function switchMobileTab(tab) {
793
 
794
  // ── Event Binding ────────────────────────────
795
  function bindEvents() {
 
796
  document.getElementById("btn-add-zone").addEventListener("click", () => showModal("modal-overlay"));
797
  document.getElementById("form-create-zone").addEventListener("submit", (e) => { e.preventDefault(); createZone(); });
798
  document.getElementById("btn-delete-zone").addEventListener("click", deleteZone);
@@ -802,21 +826,32 @@ function bindEvents() {
802
  document.getElementById("file-upload-input").addEventListener("change", handleUpload);
803
  document.getElementById("btn-save-file").addEventListener("click", saveFile);
804
  document.getElementById("btn-reconnect").addEventListener("click", reconnectTerminal);
 
 
 
 
805
 
806
  // Port management
807
  document.getElementById("btn-toggle-ports").addEventListener("click", togglePortPanel);
808
  document.getElementById("btn-add-port").addEventListener("click", () => showModal("modal-add-port"));
809
  document.getElementById("form-add-port").addEventListener("submit", (e) => { e.preventDefault(); addPort(); });
810
 
811
- // Close port panel when clicking outside
812
- document.addEventListener("click", (e) => {
813
- const panel = document.getElementById("port-panel");
814
- const toggle = document.getElementById("btn-toggle-ports");
815
- if (!panel.classList.contains("hidden") && !panel.contains(e.target) && !toggle.contains(e.target)) {
816
- panel.classList.add("hidden");
817
- }
818
  });
 
 
819
 
 
 
 
 
 
 
820
  document.getElementById("btn-cancel-prompt").addEventListener("click", () => {
821
  closeModal("modal-prompt");
822
  if (promptResolve) { promptResolve(null); promptResolve = null; }
@@ -828,6 +863,80 @@ function bindEvents() {
828
  if (promptResolve) { promptResolve(val); promptResolve = null; }
829
  });
830
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
831
  document.addEventListener("keydown", (e) => {
832
  if ((e.ctrlKey || e.metaKey) && e.key === "s") {
833
  e.preventDefault();
 
14
  let termResizeDisposable = null;
15
  let termCurrentZone = null; // tracks which zone the terminal is connected to
16
  let promptResolve = null;
17
+ let confirmResolve = null;
18
+ const mobileQuery = "(max-width: 768px), (pointer: coarse)";
19
+ let isMobile = window.matchMedia(mobileQuery).matches;
20
  let currentMobileTab = "files";
21
 
22
  // ── Init ──────────────────────────────────────
 
26
  initResizers();
27
  bindEvents();
28
 
29
+ // Track mobile state on resize / input change
30
+ const mq = window.matchMedia(mobileQuery);
31
  mq.addEventListener("change", (e) => {
32
  isMobile = e.matches;
33
+ document.body.classList.toggle("is-mobile", isMobile);
34
  if (!isMobile) {
35
  // Exiting mobile: reset panel visibility
36
  toggleSidebar(false);
 
43
  }
44
  });
45
 
46
+ // Set initial class
47
+ document.body.classList.toggle("is-mobile", isMobile);
48
+
49
  // Apply initial mobile tab if on mobile
50
  if (isMobile && currentZone) {
51
  switchMobileTab("files");
 
88
  list.innerHTML = `<li class="empty-hint" style="color:var(--text-3);font-size:12px;padding:8px 10px;cursor:default;opacity:0.6">No zones yet</li>`;
89
  } else {
90
  list.innerHTML = zones.map(z => `
91
+ <li data-zone="${escapeAttr(z.name)}" class="${currentZone === z.name ? 'active' : ''}">
92
  <span class="zone-icon"><i data-lucide="box"></i></span>
93
  <span class="zone-name">${escapeHtml(z.name)}</span>
94
  </li>
 
103
  welcomeZones.innerHTML = `<p class="welcome-hint">No zones yet — create one to get started</p>`;
104
  } else {
105
  welcomeZones.innerHTML = `<div class="zone-grid">${zones.map(z => `
106
+ <button class="zone-card" data-zone="${escapeAttr(z.name)}">
107
  <i data-lucide="box"></i>
108
  <span>${escapeHtml(z.name)}</span>
109
  </button>
 
169
 
170
  async function deleteZone() {
171
  if (!currentZone) return;
172
+ const ok = await customConfirm(`Delete zone "${currentZone}"?`, "All data will be lost!");
173
+ if (!ok) return;
174
  try {
175
  await api(`/api/zones/${currentZone}`, { method: "DELETE" });
176
  disconnectTerminal();
 
194
 
195
  function renderBreadcrumb() {
196
  const parts = currentPath ? currentPath.split("/").filter(Boolean) : [];
197
+ let html = `<span data-nav="">~</span>`;
198
  let path = "";
199
  for (const part of parts) {
200
  path += (path ? "/" : "") + part;
201
  html += `<span class="sep">/</span>`;
202
+ html += `<span data-nav="${escapeAttr(path)}">${escapeHtml(part)}</span>`;
203
  }
204
  // Update both desktop and mobile breadcrumbs
205
  document.getElementById("breadcrumb").innerHTML = html;
 
216
  }
217
  let html = "";
218
  if (currentPath) {
219
+ html += `<div class="file-item" data-action="up">
220
  <span class="fi-icon fi-icon-back"><i data-lucide="corner-left-up"></i></span>
221
  <span class="fi-name">..</span>
222
  </div>`;
 
230
  const iconClass = f.is_dir ? "fi-icon-folder" : fileIconClass(f.name);
231
  const iconName = f.is_dir ? "folder" : "file-text";
232
  const size = f.is_dir ? "" : formatSize(f.size);
233
+ const action = f.is_dir ? "navigate" : "edit";
234
 
235
+ html += `<div class="file-item" data-action="${action}" data-path="${escapeAttr(relPath)}">
236
  <span class="fi-icon ${iconClass}"><i data-lucide="${iconName}"></i></span>
237
  <span class="fi-name">${escapeHtml(f.name)}</span>
238
  <span class="fi-size">${size}</span>
239
  <span class="fi-actions">
240
+ ${!f.is_dir ? `<button data-action="download" data-path="${escapeAttr(relPath)}" title="Download"><i data-lucide="download"></i></button>` : ''}
241
+ <button data-action="rename" data-path="${escapeAttr(relPath)}" data-name="${escapeAttr(f.name)}" title="Rename"><i data-lucide="pencil"></i></button>
242
+ <button data-action="delete" data-path="${escapeAttr(relPath)}" class="fi-del" title="Delete"><i data-lucide="trash-2"></i></button>
243
  </span>
244
  </div>`;
245
  }
 
361
  }
362
 
363
  async function deleteFile(relPath) {
364
+ const ok = await customConfirm(`Delete "${relPath}"?`);
365
+ if (!ok) return;
366
  try {
367
  await api(`/api/zones/${currentZone}/files?path=${encodeURIComponent(relPath)}`, { method: "DELETE" });
368
  toast("Deleted", "info");
 
591
  }
592
  }
593
 
594
+ // ── Modal / Prompt / Confirm ─────────────────
595
  function showModal(id) {
596
  document.getElementById(id).classList.remove("hidden");
597
  lucide.createIcons({ nodes: [document.getElementById(id)] });
 
611
  });
612
  }
613
 
614
+ function customConfirm(title, message) {
615
+ return new Promise((resolve) => {
616
+ document.getElementById("confirm-title").textContent = title || "Confirm";
617
+ document.getElementById("confirm-message").textContent = message || title || "Are you sure?";
618
+ showModal("modal-confirm");
619
+ confirmResolve = resolve;
620
+ });
621
+ }
622
+
623
+ function resolveConfirm(value) {
624
+ closeModal("modal-confirm");
625
+ if (confirmResolve) { confirmResolve(value); confirmResolve = null; }
626
+ }
627
+
628
  // ── Port Management ──────────────────────────
629
  async function loadPorts() {
630
  if (!currentZone) return;
 
654
  <span class="pi-port">:${p.port}</span>
655
  <span class="pi-label">${escapeHtml(p.label)}</span>
656
  <span class="pi-actions">
657
+ <button title="Open in new tab" data-action="open-port" data-port="${p.port}"><i data-lucide="external-link"></i></button>
658
+ <button title="Copy URL" data-action="copy-port" data-port="${p.port}"><i data-lucide="copy"></i></button>
659
+ <button class="pi-del" title="Remove" data-action="remove-port" data-port="${p.port}"><i data-lucide="trash-2"></i></button>
660
  </span>
661
  </div>
662
  `).join("");
 
701
  }
702
 
703
  async function removePort(port) {
704
+ const ok = await customConfirm(`Remove port ${port} mapping?`);
705
+ if (!ok) return;
706
  try {
707
  await api(`/api/zones/${currentZone}/ports/${port}`, { method: "DELETE" });
708
  toast(`Port ${port} removed`, "info");
 
816
 
817
  // ── Event Binding ────────────────────────────
818
  function bindEvents() {
819
+ // ── Static button bindings ────────────────
820
  document.getElementById("btn-add-zone").addEventListener("click", () => showModal("modal-overlay"));
821
  document.getElementById("form-create-zone").addEventListener("submit", (e) => { e.preventDefault(); createZone(); });
822
  document.getElementById("btn-delete-zone").addEventListener("click", deleteZone);
 
826
  document.getElementById("file-upload-input").addEventListener("change", handleUpload);
827
  document.getElementById("btn-save-file").addEventListener("click", saveFile);
828
  document.getElementById("btn-reconnect").addEventListener("click", reconnectTerminal);
829
+ document.getElementById("btn-hamburger").addEventListener("click", () => toggleSidebar(true));
830
+ document.getElementById("sidebar-backdrop").addEventListener("click", () => toggleSidebar(false));
831
+ document.getElementById("btn-close-sidebar").addEventListener("click", () => toggleSidebar(false));
832
+ document.getElementById("btn-create-zone-welcome").addEventListener("click", () => document.getElementById("btn-add-zone").click());
833
 
834
  // Port management
835
  document.getElementById("btn-toggle-ports").addEventListener("click", togglePortPanel);
836
  document.getElementById("btn-add-port").addEventListener("click", () => showModal("modal-add-port"));
837
  document.getElementById("form-add-port").addEventListener("submit", (e) => { e.preventDefault(); addPort(); });
838
 
839
+ // Modal close/cancel buttons
840
+ document.getElementById("btn-close-create-modal").addEventListener("click", () => closeModal("modal-overlay"));
841
+ document.getElementById("btn-cancel-create-modal").addEventListener("click", () => closeModal("modal-overlay"));
842
+ document.getElementById("btn-close-prompt").addEventListener("click", () => {
843
+ closeModal("modal-prompt");
844
+ if (promptResolve) { promptResolve(null); promptResolve = null; }
 
845
  });
846
+ document.getElementById("btn-close-port-modal").addEventListener("click", () => closeModal("modal-add-port"));
847
+ document.getElementById("btn-cancel-port-modal").addEventListener("click", () => closeModal("modal-add-port"));
848
 
849
+ // Confirm modal
850
+ document.getElementById("btn-close-confirm").addEventListener("click", () => resolveConfirm(false));
851
+ document.getElementById("btn-confirm-cancel").addEventListener("click", () => resolveConfirm(false));
852
+ document.getElementById("btn-confirm-ok").addEventListener("click", () => resolveConfirm(true));
853
+
854
+ // Prompt form
855
  document.getElementById("btn-cancel-prompt").addEventListener("click", () => {
856
  closeModal("modal-prompt");
857
  if (promptResolve) { promptResolve(null); promptResolve = null; }
 
863
  if (promptResolve) { promptResolve(val); promptResolve = null; }
864
  });
865
 
866
+ // Close port panel when clicking outside
867
+ document.addEventListener("click", (e) => {
868
+ const panel = document.getElementById("port-panel");
869
+ const toggle = document.getElementById("btn-toggle-ports");
870
+ if (!panel.classList.contains("hidden") && !panel.contains(e.target) && !toggle.contains(e.target)) {
871
+ panel.classList.add("hidden");
872
+ }
873
+ });
874
+
875
+ // ── Event delegation: file list ───────────
876
+ document.getElementById("file-list").addEventListener("click", (e) => {
877
+ // Check action buttons first (download, rename, delete)
878
+ const actionBtn = e.target.closest(".fi-actions [data-action]");
879
+ if (actionBtn) {
880
+ e.stopPropagation();
881
+ const action = actionBtn.dataset.action;
882
+ const path = actionBtn.dataset.path;
883
+ if (action === "download") downloadFile(path);
884
+ else if (action === "rename") renameFile(path, actionBtn.dataset.name);
885
+ else if (action === "delete") deleteFile(path);
886
+ return;
887
+ }
888
+ // File item click (open/navigate/up)
889
+ const item = e.target.closest(".file-item[data-action]");
890
+ if (!item) return;
891
+ const action = item.dataset.action;
892
+ const path = item.dataset.path;
893
+ if (action === "navigate") navigateTo(path);
894
+ else if (action === "edit") editFile(path);
895
+ else if (action === "up") navigateUp();
896
+ });
897
+
898
+ // ── Event delegation: zone list (sidebar) ─
899
+ document.getElementById("zone-list").addEventListener("click", (e) => {
900
+ const li = e.target.closest("li[data-zone]");
901
+ if (li) openZone(li.dataset.zone);
902
+ });
903
+
904
+ // ── Event delegation: welcome zone cards ──
905
+ document.getElementById("welcome-zones").addEventListener("click", (e) => {
906
+ const card = e.target.closest("[data-zone]");
907
+ if (card) openZone(card.dataset.zone);
908
+ });
909
+
910
+ // ── Event delegation: breadcrumbs ─────────
911
+ document.getElementById("breadcrumb").addEventListener("click", (e) => {
912
+ const span = e.target.closest("[data-nav]");
913
+ if (span) navigateTo(span.dataset.nav);
914
+ });
915
+ const mbc = document.getElementById("breadcrumb-mobile");
916
+ if (mbc) {
917
+ mbc.addEventListener("click", (e) => {
918
+ const span = e.target.closest("[data-nav]");
919
+ if (span) navigateTo(span.dataset.nav);
920
+ });
921
+ }
922
+
923
+ // ── Event delegation: mobile tabs ─────────
924
+ document.getElementById("mobile-tabs").addEventListener("click", (e) => {
925
+ const tab = e.target.closest("[data-tab]");
926
+ if (tab) switchMobileTab(tab.dataset.tab);
927
+ });
928
+
929
+ // ── Event delegation: port list ───────────
930
+ document.getElementById("port-list").addEventListener("click", (e) => {
931
+ const btn = e.target.closest("[data-action]");
932
+ if (!btn) return;
933
+ const port = parseInt(btn.dataset.port);
934
+ if (btn.dataset.action === "open-port") openPort(port);
935
+ else if (btn.dataset.action === "copy-port") copyPortUrl(port);
936
+ else if (btn.dataset.action === "remove-port") removePort(port);
937
+ });
938
+
939
+ // ── Keyboard shortcuts ────────────────────
940
  document.addEventListener("keydown", (e) => {
941
  if ((e.ctrlKey || e.metaKey) && e.key === "s") {
942
  e.preventDefault();
static/index.html CHANGED
@@ -39,14 +39,14 @@
39
  <body>
40
  <div id="app">
41
  <!-- Sidebar Backdrop (mobile) -->
42
- <div id="sidebar-backdrop" class="sidebar-backdrop" onclick="toggleSidebar(false)"></div>
43
 
44
  <!-- Sidebar -->
45
  <aside id="sidebar">
46
  <div class="sidebar-brand">
47
  <div class="brand-icon"><i data-lucide="layout-dashboard"></i></div>
48
  <span class="brand-text">HugPanel</span>
49
- <button id="btn-close-sidebar" class="icon-btn-sm sidebar-close-btn" onclick="toggleSidebar(false)"><i data-lucide="x"></i></button>
50
  </div>
51
 
52
  <nav class="sidebar-nav">
@@ -73,7 +73,7 @@
73
  <!-- Main -->
74
  <main id="main">
75
  <!-- Global hamburger (visible on mobile for both views) -->
76
- <button id="btn-hamburger" class="icon-btn-sm hamburger-global" onclick="toggleSidebar(true)" title="Menu">
77
  <i data-lucide="menu"></i>
78
  </button>
79
 
@@ -85,7 +85,7 @@
85
  <h1>HugPanel</h1>
86
  <p>Multi-zone workspace with file manager, code editor &amp; terminal.</p>
87
  <div id="welcome-zones" class="welcome-zones"></div>
88
- <button class="btn btn-accent btn-lg" onclick="document.getElementById('btn-add-zone').click()">
89
  <i data-lucide="plus"></i> Create Zone
90
  </button>
91
  </div>
@@ -169,15 +169,15 @@
169
 
170
  <!-- Mobile Bottom Tab Bar -->
171
  <nav id="mobile-tabs" class="mobile-tabs">
172
- <button class="mobile-tab active" data-tab="files" onclick="switchMobileTab('files')">
173
  <i data-lucide="folder"></i>
174
  <span>Files</span>
175
  </button>
176
- <button class="mobile-tab" data-tab="editor" onclick="switchMobileTab('editor')">
177
  <i data-lucide="code-2"></i>
178
  <span>Editor</span>
179
  </button>
180
- <button class="mobile-tab" data-tab="terminal" onclick="switchMobileTab('terminal')">
181
  <i data-lucide="terminal-square"></i>
182
  <span>Terminal</span>
183
  </button>
@@ -194,7 +194,7 @@
194
  <div class="modal">
195
  <div class="modal-header">
196
  <h3><i data-lucide="plus-circle"></i> Create Zone</h3>
197
- <button class="icon-btn-sm" onclick="closeModal('modal-overlay')"><i data-lucide="x"></i></button>
198
  </div>
199
  <form id="form-create-zone">
200
  <div class="form-group">
@@ -207,7 +207,7 @@
207
  <input type="text" id="input-zone-desc" placeholder="Short description...">
208
  </div>
209
  <div class="modal-footer">
210
- <button type="button" class="btn btn-ghost" onclick="closeModal('modal-overlay')">Cancel</button>
211
  <button type="submit" class="btn btn-accent"><i data-lucide="plus"></i> Create</button>
212
  </div>
213
  </form>
@@ -219,7 +219,7 @@
219
  <div class="modal modal-sm">
220
  <div class="modal-header">
221
  <h3 id="prompt-title">Enter name</h3>
222
- <button class="icon-btn-sm" onclick="closeModal('modal-prompt');if(promptResolve){promptResolve(null);promptResolve=null}"><i data-lucide="x"></i></button>
223
  </div>
224
  <form id="form-prompt">
225
  <div class="form-group">
@@ -245,12 +245,29 @@
245
  <div id="port-list" class="port-list"></div>
246
  </div>
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  <!-- Modal: Add Port -->
249
  <div id="modal-add-port" class="modal-overlay hidden">
250
  <div class="modal modal-sm">
251
  <div class="modal-header">
252
  <h3><i data-lucide="network"></i> Map Port</h3>
253
- <button class="icon-btn-sm" onclick="closeModal('modal-add-port')"><i data-lucide="x"></i></button>
254
  </div>
255
  <form id="form-add-port">
256
  <div class="form-group">
@@ -263,7 +280,7 @@
263
  <input type="text" id="input-port-label" placeholder="e.g. React Dev Server">
264
  </div>
265
  <div class="modal-footer">
266
- <button type="button" class="btn btn-ghost" onclick="closeModal('modal-add-port')">Cancel</button>
267
  <button type="submit" class="btn btn-accent"><i data-lucide="plus"></i> Map Port</button>
268
  </div>
269
  </form>
 
39
  <body>
40
  <div id="app">
41
  <!-- Sidebar Backdrop (mobile) -->
42
+ <div id="sidebar-backdrop" class="sidebar-backdrop"></div>
43
 
44
  <!-- Sidebar -->
45
  <aside id="sidebar">
46
  <div class="sidebar-brand">
47
  <div class="brand-icon"><i data-lucide="layout-dashboard"></i></div>
48
  <span class="brand-text">HugPanel</span>
49
+ <button id="btn-close-sidebar" class="icon-btn-sm sidebar-close-btn"><i data-lucide="x"></i></button>
50
  </div>
51
 
52
  <nav class="sidebar-nav">
 
73
  <!-- Main -->
74
  <main id="main">
75
  <!-- Global hamburger (visible on mobile for both views) -->
76
+ <button id="btn-hamburger" class="icon-btn-sm hamburger-global" title="Menu">
77
  <i data-lucide="menu"></i>
78
  </button>
79
 
 
85
  <h1>HugPanel</h1>
86
  <p>Multi-zone workspace with file manager, code editor &amp; terminal.</p>
87
  <div id="welcome-zones" class="welcome-zones"></div>
88
+ <button id="btn-create-zone-welcome" class="btn btn-accent btn-lg">
89
  <i data-lucide="plus"></i> Create Zone
90
  </button>
91
  </div>
 
169
 
170
  <!-- Mobile Bottom Tab Bar -->
171
  <nav id="mobile-tabs" class="mobile-tabs">
172
+ <button class="mobile-tab active" data-tab="files">
173
  <i data-lucide="folder"></i>
174
  <span>Files</span>
175
  </button>
176
+ <button class="mobile-tab" data-tab="editor">
177
  <i data-lucide="code-2"></i>
178
  <span>Editor</span>
179
  </button>
180
+ <button class="mobile-tab" data-tab="terminal">
181
  <i data-lucide="terminal-square"></i>
182
  <span>Terminal</span>
183
  </button>
 
194
  <div class="modal">
195
  <div class="modal-header">
196
  <h3><i data-lucide="plus-circle"></i> Create Zone</h3>
197
+ <button class="icon-btn-sm" id="btn-close-create-modal"><i data-lucide="x"></i></button>
198
  </div>
199
  <form id="form-create-zone">
200
  <div class="form-group">
 
207
  <input type="text" id="input-zone-desc" placeholder="Short description...">
208
  </div>
209
  <div class="modal-footer">
210
+ <button type="button" class="btn btn-ghost" id="btn-cancel-create-modal">Cancel</button>
211
  <button type="submit" class="btn btn-accent"><i data-lucide="plus"></i> Create</button>
212
  </div>
213
  </form>
 
219
  <div class="modal modal-sm">
220
  <div class="modal-header">
221
  <h3 id="prompt-title">Enter name</h3>
222
+ <button class="icon-btn-sm" id="btn-close-prompt"><i data-lucide="x"></i></button>
223
  </div>
224
  <form id="form-prompt">
225
  <div class="form-group">
 
245
  <div id="port-list" class="port-list"></div>
246
  </div>
247
 
248
+ <!-- Modal: Confirm -->
249
+ <div id="modal-confirm" class="modal-overlay hidden">
250
+ <div class="modal modal-sm">
251
+ <div class="modal-header">
252
+ <h3 id="confirm-title">Confirm</h3>
253
+ <button class="icon-btn-sm" id="btn-close-confirm"><i data-lucide="x"></i></button>
254
+ </div>
255
+ <div style="padding:16px">
256
+ <p id="confirm-message" style="font-size:14px;color:var(--text-2)"></p>
257
+ </div>
258
+ <div class="modal-footer" style="padding:0 16px 16px">
259
+ <button type="button" class="btn btn-ghost" id="btn-confirm-cancel">Cancel</button>
260
+ <button type="button" class="btn btn-danger" id="btn-confirm-ok">Delete</button>
261
+ </div>
262
+ </div>
263
+ </div>
264
+
265
  <!-- Modal: Add Port -->
266
  <div id="modal-add-port" class="modal-overlay hidden">
267
  <div class="modal modal-sm">
268
  <div class="modal-header">
269
  <h3><i data-lucide="network"></i> Map Port</h3>
270
+ <button class="icon-btn-sm" id="btn-close-port-modal"><i data-lucide="x"></i></button>
271
  </div>
272
  <form id="form-add-port">
273
  <div class="form-group">
 
280
  <input type="text" id="input-port-label" placeholder="e.g. React Dev Server">
281
  </div>
282
  <div class="modal-footer">
283
+ <button type="button" class="btn btn-ghost" id="btn-cancel-port-modal">Cancel</button>
284
  <button type="submit" class="btn btn-accent"><i data-lucide="plus"></i> Map Port</button>
285
  </div>
286
  </form>
static/style.css CHANGED
@@ -224,6 +224,13 @@ body {
224
 
225
  .btn-ghost:hover { background: var(--bg-hover); color: var(--text); }
226
 
 
 
 
 
 
 
 
227
  .btn-lg { padding: 12px 24px; font-size: 14px; border-radius: var(--radius-lg); }
228
  .btn-lg svg { width: 16px; height: 16px; }
229
 
@@ -501,7 +508,7 @@ body {
501
 
502
  .file-item .fi-actions {
503
  display: flex; gap: 1px;
504
- opacity: 0;
505
  transition: opacity var(--transition);
506
  }
507
 
@@ -907,8 +914,8 @@ body {
907
  .mobile-only { display: none !important; }
908
  .mobile-tabs { display: none; }
909
 
910
- /* ── Mobile Breakpoint ─────── */
911
- @media (max-width: 768px) {
912
  :root {
913
  --sidebar-w: 280px;
914
  --topbar-h: 48px;
@@ -1278,26 +1285,6 @@ body {
1278
 
1279
  .form-group input[type="number"]:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
1280
 
1281
- /* ── Touch optimizations ───── */
1282
- @media (pointer: coarse) {
1283
- .file-item .fi-actions { opacity: 1; }
1284
-
1285
- .file-item {
1286
- min-height: 48px;
1287
- -webkit-tap-highlight-color: transparent;
1288
- }
1289
-
1290
- .zone-list li {
1291
- min-height: 44px;
1292
- -webkit-tap-highlight-color: transparent;
1293
- }
1294
-
1295
- .icon-btn-sm {
1296
- min-width: 36px;
1297
- min-height: 36px;
1298
- }
1299
- }
1300
-
1301
  /* Safe area for notch devices */
1302
  @supports (padding: env(safe-area-inset-bottom)) {
1303
  .mobile-tabs {
 
224
 
225
  .btn-ghost:hover { background: var(--bg-hover); color: var(--text); }
226
 
227
+ .btn-danger {
228
+ background: var(--danger);
229
+ color: #fff;
230
+ border: none;
231
+ }
232
+ .btn-danger:hover { background: #dc2626; }
233
+
234
  .btn-lg { padding: 12px 24px; font-size: 14px; border-radius: var(--radius-lg); }
235
  .btn-lg svg { width: 16px; height: 16px; }
236
 
 
508
 
509
  .file-item .fi-actions {
510
  display: flex; gap: 1px;
511
+ opacity: 0.4;
512
  transition: opacity var(--transition);
513
  }
514
 
 
914
  .mobile-only { display: none !important; }
915
  .mobile-tabs { display: none; }
916
 
917
+ /* ── Mobile Breakpoint (width OR touch device) ── */
918
+ @media (max-width: 768px), (pointer: coarse) {
919
  :root {
920
  --sidebar-w: 280px;
921
  --topbar-h: 48px;
 
1285
 
1286
  .form-group input[type="number"]:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); }
1287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1288
  /* Safe area for notch devices */
1289
  @supports (padding: env(safe-area-inset-bottom)) {
1290
  .mobile-tabs {