From: Roberto Morado Date: Thu, 21 May 2026 05:43:49 +0000 (-0400) Subject: File bug fix X-Git-Url: https://git.morado.dev/?a=commitdiff_plain;ds=inline;p=env.morado.dev File bug fix --- diff --git a/main.ts b/main.ts index 760709b..4660686 100644 --- a/main.ts +++ b/main.ts @@ -504,8 +504,10 @@ const HTML = ` .pi-val { color: var(--text); letter-spacing: 0.04em; } /* Panels */ - .panel-chat, .panel-files, .panel-ports { display: none; flex: 1; flex-direction: column; min-height: 0; overflow: hidden; } - .panel-chat.active, .panel-files.active, .panel-ports.active { display: flex; } + .panel-chat, .panel-ports { display: none; flex: 1; flex-direction: column; min-height: 0; overflow: hidden; } + .panel-chat.active, .panel-ports.active { display: flex; } + .panel-files { display: none; flex: 1; flex-direction: row; min-height: 0; overflow: hidden; } + .panel-files.active { display: flex; } /* Messages */ .messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 2px; scroll-behavior: smooth; min-height: 0; } @@ -572,27 +574,32 @@ const HTML = ` .input-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; padding-bottom: 2px; } #file-input { display: none; } - /* Files panel */ - .files-toolbar { padding: 12px 16px; border-bottom: 1px solid var(--border-soft); flex-shrink: 0; display: flex; align-items: center; gap: 10px; } - .files-toolbar-hint { font-family: var(--mono); font-size: 10px; color: var(--text-muted); letter-spacing: 0.06em; } - .files-list { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; min-height: 0; } - .files-list::-webkit-scrollbar { width: 3px; } - .files-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } - .files-empty { font-family: var(--mono); font-size: 11px; color: var(--text-muted); letter-spacing: 0.06em; text-align: center; padding: 48px 20px; } - .sfile-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; } - .sfile-header { display: flex; align-items: center; padding: 8px 10px; gap: 8px; border-bottom: 1px solid var(--border-soft); } - .sfile-name { flex: 1; font-family: var(--mono); font-size: 12px; color: var(--text); background: none; border: 1px solid transparent; border-radius: 3px; outline: none; min-width: 0; padding: 1px 4px; cursor: default; transition: border-color var(--transition), background var(--transition); } - .sfile-name:hover { border-color: var(--border); background: var(--surface2); cursor: text; } - .sfile-name:focus { border-color: var(--accent-dim); background: var(--surface2); cursor: text; } + /* Files panel — sidebar + editor */ + .files-sidebar { width: 190px; min-width: 120px; flex-shrink: 0; border-right: 1px solid var(--border-soft); display: flex; flex-direction: column; overflow: hidden; } + .files-sidebar-toolbar { padding: 8px 10px; border-bottom: 1px solid var(--border-soft); flex-shrink: 0; } + .files-tree { flex: 1; overflow-y: auto; padding: 4px 0; user-select: none; } + .files-tree::-webkit-scrollbar { width: 3px; } + .files-tree::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } + .files-empty { font-family: var(--mono); font-size: 10px; color: var(--text-muted); letter-spacing: 0.06em; text-align: center; padding: 24px 12px; } + .ft-folder { display: flex; align-items: center; gap: 4px; padding: 4px 10px; cursor: pointer; font-family: var(--mono); font-size: 11px; color: var(--text-muted); white-space: nowrap; } + .ft-folder:hover { background: var(--surface2); color: var(--text-dim); } + .ft-folder-arrow { font-size: 9px; flex-shrink: 0; line-height: 1; } + .ft-file { padding: 4px 10px; cursor: pointer; font-family: var(--mono); font-size: 11px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background var(--transition), color var(--transition); } + .ft-file:hover { background: var(--surface2); color: var(--text); } + .ft-file.selected { background: var(--surface2); color: var(--text); } + .ft-file.nested { padding-left: 24px; } + .files-editor-area { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; } + .files-editor-empty { flex: 1; display: flex; align-items: center; justify-content: center; font-family: var(--mono); font-size: 11px; color: var(--text-muted); letter-spacing: 0.06em; } + .files-editor-hdr { display: flex; align-items: center; padding: 6px 12px; border-bottom: 1px solid var(--border-soft); gap: 8px; flex-shrink: 0; } + .files-editor-name { flex: 1; font-family: var(--mono); font-size: 12px; color: var(--text); background: none; border: 1px solid transparent; border-radius: 3px; outline: none; padding: 1px 4px; min-width: 0; cursor: default; transition: border-color var(--transition), background var(--transition); } + .files-editor-name:hover { border-color: var(--border); background: var(--surface2); cursor: text; } + .files-editor-name:focus { border-color: var(--accent-dim); background: var(--surface2); cursor: text; } + .files-editor-ta { flex: 1; resize: none; background: none; border: none; outline: none; font-family: var(--mono); font-size: 12px; color: var(--text); padding: 12px; line-height: 1.65; min-height: 0; } + .files-editor-ta::placeholder { color: var(--text-muted); } .sfile-actions { display: flex; gap: 4px; flex-shrink: 0; } .sfile-btn { font-family: var(--mono); font-size: 10px; color: var(--text-muted); background: none; border: 1px solid var(--border); cursor: pointer; padding: 2px 8px; border-radius: 3px; transition: all var(--transition); letter-spacing: 0.04em; } .sfile-btn:hover { color: var(--text); border-color: var(--accent-dim); } .sfile-btn.del:hover { color: var(--danger); border-color: var(--danger); } - .sfile-toggle { background: none; border: none; cursor: pointer; color: var(--text-muted); font-family: var(--mono); font-size: 11px; padding: 0 5px 0 0; line-height: 1; transition: color var(--transition); flex-shrink: 0; } - .sfile-toggle:hover { color: var(--text); } - .sfile-editor { display: none; width: 100%; min-height: 90px; max-height: 280px; resize: vertical; background: none; border: none; outline: none; font-family: var(--mono); font-size: 12px; color: var(--text); padding: 10px 12px; line-height: 1.65; } - .sfile-editor::placeholder { color: var(--text-muted); } - .sfile-card.expanded .sfile-editor { display: block; } /* Port forwards panel */ .pf-toolbar { padding: 12px 16px; border-bottom: 1px solid var(--border-soft); flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; } @@ -736,12 +743,16 @@ const HTML = `
-
- - files vanish when the session ends +
+
+ +
+
+
no files yet
+
-
-
no shared files yet · click "+ new file" to create one
+
+
select a file to edit
@@ -795,7 +806,8 @@ const App = (() => { let sharedFiles = new Map(); // id -> { name, content, _debounce } let clipStore = new Map(); // clip-card-id -> full text let activeFwds = new Map(); // forwardId -> { localPort, targetHost, targetPort } - let expandedSfileId = null; // only one shared-file card open at a time + let selectedSfileId = null; // currently open file in the editor + const collapsedFolders = new Set(); // folder paths that are collapsed const STUN_CFG = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }; @@ -1118,8 +1130,9 @@ const App = (() => { const name = 'file-' + new Date().toISOString().slice(11,19).replace(/:/g,'-') + '.txt'; sharedFiles.set(id, { name, content: '', _debounce: null }); dc.send(JSON.stringify({ type: 'sfile-create', id, name, content: '' })); - expandedSfileId = id; // auto-open the new card - renderSfiles(); + selectedSfileId = id; + renderFileTree(); + renderSfileEditor(id); updateFilesBadge(); switchTab('files'); } @@ -1131,6 +1144,7 @@ const App = (() => { if (dc && dc.readyState === 'open') { dc.send(JSON.stringify({ type: 'sfile-update', id, name, content: sf.content })); } + renderFileTree(); } function sfileInput(id, value) { @@ -1159,103 +1173,137 @@ const App = (() => { const sf = sharedFiles.get(id); if (sf && sf._debounce) clearTimeout(sf._debounce); sharedFiles.delete(id); - if (expandedSfileId === id) expandedSfileId = null; + if (selectedSfileId === id) { selectedSfileId = null; renderSfileEditor(null); } if (dc && dc.readyState === 'open') dc.send(JSON.stringify({ type: 'sfile-delete', id })); - renderSfiles(); + renderFileTree(); updateFilesBadge(); } - function renderSfiles() { - const list = document.getElementById('files-list'); - list.innerHTML = ''; - // Discard expandedSfileId if that file no longer exists - if (expandedSfileId && !sharedFiles.has(expandedSfileId)) expandedSfileId = null; - + function renderFileTree() { + const tree = document.getElementById('files-tree'); + if (!tree) return; + tree.innerHTML = ''; if (sharedFiles.size === 0) { const empty = document.createElement('div'); empty.className = 'files-empty'; - empty.textContent = 'no shared files yet \xb7 click "+ new file" to create one'; - list.appendChild(empty); + empty.textContent = 'no files yet'; + tree.appendChild(empty); return; } + // Group files by folder — folder is everything before the last '/' + const folders = new Map(); // folderName -> [{id, fname}] + const rootFiles = []; for (const [id, sf] of sharedFiles) { - const isExpanded = id === expandedSfileId; - const card = document.createElement('div'); - card.className = 'sfile-card' + (isExpanded ? ' expanded' : ''); - card.id = 'sfc-' + id; - - // Header row - const hdr = document.createElement('div'); - hdr.className = 'sfile-header'; - - // Expand/collapse toggle - const toggleBtn = document.createElement('button'); - toggleBtn.className = 'sfile-toggle'; - toggleBtn.id = 'sft-' + id; - toggleBtn.textContent = isExpanded ? '▾' : '▸'; // ▾ / ▸ - toggleBtn.title = isExpanded ? 'collapse' : 'expand'; - toggleBtn.addEventListener('click', () => toggleSfile(id)); - - const nameEl = document.createElement('input'); - nameEl.className = 'sfile-name'; - nameEl.id = 'sfn-' + id; - nameEl.value = sf.name; - nameEl.addEventListener('change', () => renameSfile(id, nameEl.value)); - - const acts = document.createElement('div'); - acts.className = 'sfile-actions'; - - const dlBtn = document.createElement('button'); - dlBtn.className = 'sfile-btn'; - dlBtn.textContent = '↓ dl'; - dlBtn.addEventListener('click', () => downloadSfile(id)); - - const delBtn = document.createElement('button'); - delBtn.className = 'sfile-btn del'; - delBtn.textContent = '\xd7 del'; - delBtn.addEventListener('click', () => deleteSfile(id)); - - acts.appendChild(dlBtn); - acts.appendChild(delBtn); - hdr.appendChild(toggleBtn); - hdr.appendChild(nameEl); - hdr.appendChild(acts); - - // Editor (hidden unless card has .expanded) - const editor = document.createElement('textarea'); - editor.className = 'sfile-editor'; - editor.id = 'sfe-' + id; - editor.placeholder = 'start typing…'; - editor.value = sf.content; - editor.addEventListener('input', () => sfileInput(id, editor.value)); - - card.appendChild(hdr); - card.appendChild(editor); - list.appendChild(card); + const slash = sf.name.lastIndexOf('/'); + if (slash >= 0) { + const folder = sf.name.slice(0, slash); + const fname = sf.name.slice(slash + 1); + if (!folders.has(folder)) folders.set(folder, []); + folders.get(folder).push({ id, fname }); + } else { + rootFiles.push({ id }); + } } - } - - function toggleSfile(id) { - const wasExpanded = expandedSfileId === id; - // Collapse the previously open card - if (expandedSfileId) { - const prev = document.getElementById('sfc-' + expandedSfileId); - if (prev) prev.classList.remove('expanded'); - const prevBtn = document.getElementById('sft-' + expandedSfileId); - if (prevBtn) prevBtn.textContent = '▸'; // ▸ + // Root-level files first + for (const { id } of rootFiles) { + const sf = sharedFiles.get(id); + const item = document.createElement('div'); + item.className = 'ft-file' + (id === selectedSfileId ? ' selected' : ''); + item.dataset.id = id; + item.textContent = sf.name; + item.title = sf.name; + item.addEventListener('click', () => selectSfile(id)); + tree.appendChild(item); } - expandedSfileId = wasExpanded ? null : id; - if (expandedSfileId) { - const card = document.getElementById('sfc-' + expandedSfileId); - if (card) { - card.classList.add('expanded'); - card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + // Folders + for (const [folder, files] of folders) { + const collapsed = collapsedFolders.has(folder); + const folderEl = document.createElement('div'); + folderEl.className = 'ft-folder'; + const arrow = document.createElement('span'); + arrow.className = 'ft-folder-arrow'; + arrow.textContent = collapsed ? '▸' : '▾'; // ▸ / ▾ + const nameSpan = document.createElement('span'); + nameSpan.textContent = folder + '/'; + nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis'; + folderEl.appendChild(arrow); + folderEl.appendChild(nameSpan); + folderEl.addEventListener('click', () => { + if (collapsedFolders.has(folder)) collapsedFolders.delete(folder); + else collapsedFolders.add(folder); + renderFileTree(); + }); + tree.appendChild(folderEl); + if (!collapsed) { + for (const { id, fname } of files) { + const item = document.createElement('div'); + item.className = 'ft-file nested' + (id === selectedSfileId ? ' selected' : ''); + item.dataset.id = id; + item.textContent = fname; + item.title = sharedFiles.get(id)?.name || fname; + item.addEventListener('click', () => selectSfile(id)); + tree.appendChild(item); + } } - const btn = document.getElementById('sft-' + expandedSfileId); - if (btn) btn.textContent = '▾'; // ▾ } } + function selectSfile(id) { + selectedSfileId = id; + document.querySelectorAll('.ft-file').forEach(el => { + el.classList.toggle('selected', el.dataset.id === id); + }); + renderSfileEditor(id); + } + + function renderSfileEditor(id) { + const area = document.getElementById('files-editor-area'); + if (!area) return; + area.innerHTML = ''; + if (!id || !sharedFiles.has(id)) { + const empty = document.createElement('div'); + empty.className = 'files-editor-empty'; + empty.id = 'files-editor-empty'; + empty.textContent = 'select a file to edit'; + area.appendChild(empty); + return; + } + const sf = sharedFiles.get(id); + // Header + const hdr = document.createElement('div'); + hdr.className = 'files-editor-hdr'; + const nameInput = document.createElement('input'); + nameInput.className = 'files-editor-name'; + nameInput.id = 'sfe-name-' + id; + nameInput.value = sf.name; + nameInput.spellcheck = false; + nameInput.addEventListener('change', () => renameSfile(id, nameInput.value)); + const acts = document.createElement('div'); + acts.className = 'sfile-actions'; + const dlBtn = document.createElement('button'); + dlBtn.className = 'sfile-btn'; + dlBtn.textContent = '↓ dl'; + dlBtn.addEventListener('click', () => downloadSfile(id)); + const delBtn = document.createElement('button'); + delBtn.className = 'sfile-btn del'; + delBtn.textContent = '\xd7 del'; + delBtn.addEventListener('click', () => deleteSfile(id)); + acts.appendChild(dlBtn); + acts.appendChild(delBtn); + hdr.appendChild(nameInput); + hdr.appendChild(acts); + // Textarea + const ta = document.createElement('textarea'); + ta.className = 'files-editor-ta'; + ta.id = 'sfe-' + id; + ta.placeholder = 'start typing…'; + ta.value = sf.content; + ta.addEventListener('input', () => sfileInput(id, ta.value)); + area.appendChild(hdr); + area.appendChild(ta); + ta.focus(); + } + function updateFilesBadge() { const badge = document.getElementById('files-badge'); const n = sharedFiles.size; @@ -1401,7 +1449,8 @@ const App = (() => { sharedFiles.clear(); clipStore.clear(); activeFwds.clear(); - expandedSfileId = null; + selectedSfileId = null; + collapsedFolders.clear(); if (dc) { try { dc.close(); } catch (_) {} dc = null; } if (pc) { try { pc.close(); } catch (_) {} pc = null; } if (sseSource) { sseSource.close(); sseSource = null; } @@ -1417,7 +1466,8 @@ const App = (() => { document.getElementById('btn-burn').classList.remove('burn-on'); document.getElementById('btn-info').classList.remove('active-btn'); document.getElementById('peer-info-panel').classList.remove('open'); - renderSfiles(); + renderFileTree(); + renderSfileEditor(null); updateFilesBadge(); renderPortForwards(); updatePortsBadge(); @@ -1616,16 +1666,18 @@ const App = (() => { } else if (msg.type === 'sfile-create') { if (!sharedFiles.has(msg.id)) { sharedFiles.set(msg.id, { name: msg.name, content: msg.content || '', _debounce: null }); - renderSfiles(); + renderFileTree(); updateFilesBadge(); addSystem('shared file created: ' + escHtml(msg.name)); } } else if (msg.type === 'sfile-update') { const sf = sharedFiles.get(msg.id); if (sf) { - if (msg.name !== undefined) { + let treeChanged = false; + if (msg.name !== undefined && sf.name !== msg.name) { sf.name = msg.name; - const nameEl = document.getElementById('sfn-' + msg.id); + treeChanged = true; + const nameEl = document.getElementById('sfe-name-' + msg.id); if (nameEl) nameEl.value = msg.name; } if (msg.content !== undefined) { @@ -1633,15 +1685,14 @@ const App = (() => { const edEl = document.getElementById('sfe-' + msg.id); if (edEl && document.activeElement !== edEl) edEl.value = msg.content; } + if (treeChanged) renderFileTree(); } } else if (msg.type === 'sfile-delete') { const sf = sharedFiles.get(msg.id); if (sf && sf._debounce) clearTimeout(sf._debounce); sharedFiles.delete(msg.id); - if (expandedSfileId === msg.id) expandedSfileId = null; - const card = document.getElementById('sfc-' + msg.id); - if (card) card.remove(); - if (sharedFiles.size === 0) renderSfiles(); + if (selectedSfileId === msg.id) { selectedSfileId = null; renderSfileEditor(null); } + renderFileTree(); updateFilesBadge(); }