Upload 19 files
Browse files- __pycache__/app.cpython-311.pyc +0 -0
- __pycache__/config.cpython-311.pyc +0 -0
- routers/__pycache__/__init__.cpython-311.pyc +0 -0
- routers/__pycache__/terminal.cpython-311.pyc +0 -0
- static/app.js +136 -27
- static/index.html +29 -12
- static/style.css +10 -23
__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
|
|
|
|
|
|
|
| 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(
|
| 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' : ''}"
|
| 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"
|
| 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 |
-
|
|
|
|
| 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
|
| 191 |
let path = "";
|
| 192 |
for (const part of parts) {
|
| 193 |
path += (path ? "/" : "") + part;
|
| 194 |
html += `<span class="sep">/</span>`;
|
| 195 |
-
html += `<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"
|
| 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 ?
|
| 227 |
|
| 228 |
-
html += `<div class="file-item"
|
| 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
|
| 234 |
-
<button
|
| 235 |
-
<button
|
| 236 |
</span>
|
| 237 |
</div>`;
|
| 238 |
}
|
|
@@ -354,7 +361,8 @@ function downloadFile(relPath) {
|
|
| 354 |
}
|
| 355 |
|
| 356 |
async function deleteFile(relPath) {
|
| 357 |
-
|
|
|
|
| 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"
|
| 636 |
-
<button title="Copy URL"
|
| 637 |
-
<button class="pi-del" title="Remove"
|
| 638 |
</span>
|
| 639 |
</div>
|
| 640 |
`).join("");
|
|
@@ -679,7 +701,8 @@ async function addPort() {
|
|
| 679 |
}
|
| 680 |
|
| 681 |
async function removePort(port) {
|
| 682 |
-
|
|
|
|
| 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 |
-
//
|
| 812 |
-
document.addEventListener("click", (
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 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"
|
| 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"
|
| 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"
|
| 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 & terminal.</p>
|
| 87 |
<div id="welcome-zones" class="welcome-zones"></div>
|
| 88 |
-
<button class="btn btn-accent btn-lg"
|
| 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"
|
| 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,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"
|
| 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"
|
| 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"
|
| 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"
|
| 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"
|
| 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 & 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 {
|