Replace file manager toolbar buttons with hamburger menu

Removed individual delete, upload, hidden files, and refresh buttons.
Added a hamburger (☰) menu with:
- Select: toggles selection mode with checkboxes for multi-select
- Delete: enabled only when files are selected
- Upload: file upload
- Refresh: reload current directory
- Show Hidden Files: toggles dotfile visibility, persisted via localStorage

Toolbar now only has New File, New Folder, and the hamburger menu for a
cleaner interface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
roberts 2026-03-15 00:48:33 -05:00
parent 30f0536b00
commit 3c295ba302
2 changed files with 180 additions and 68 deletions

View file

@ -76,9 +76,8 @@
color: var(--text-primary);
}
.files-action-btn.active {
background: var(--accent-hover);
color: var(--accent);
.files-menu-btn {
font-size: 18px;
}
/* File body — dual pane */
@ -264,3 +263,27 @@
background: var(--border-structural);
margin: 4px 8px;
}
.context-item.disabled {
color: var(--text-muted);
cursor: not-allowed;
opacity: 0.5;
}
.context-item.disabled:hover {
background: none;
}
/* Selection mode checkbox */
.file-select-cb {
font-size: 14px;
width: 20px;
text-align: center;
flex-shrink: 0;
color: var(--accent);
}
/* Toolbar menu — right-aligned dropdown */
.files-toolbar-menu {
min-width: 200px;
}

View file

@ -10,7 +10,9 @@
let selectedFiles = new Set();
let contextMenuEl = null;
let uploadInput = null;
let showHidden = false; // show dotfiles
let showHidden = localStorage.getItem('atlus_files_show_hidden') === '1';
let selectionMode = false;
let lastEntries = []; // cached for re-render without refetch
const FILE_ICONS = {
dir: '📁',
@ -66,8 +68,8 @@
try {
const res = await Atlus.apiFetch(`/api/files/list?path=${encodeURIComponent(path)}`);
if (!res.ok) throw new Error('Failed to load directory');
const entries = await res.json();
renderFileList(entries);
lastEntries = await res.json();
renderFileList(lastEntries);
} catch (e) {
fileListEl.innerHTML = `<div style="padding:16px;color:var(--status-red);font-family:var(--font-mono);font-size:13px;">Error: ${e.message}</div>`;
}
@ -108,6 +110,29 @@
row.dataset.path = entry.path;
const icon = getFileIcon(entry);
if (selectionMode) {
const checked = selectedFiles.has(entry.path);
row.classList.toggle('selected', checked);
row.innerHTML = `
<div class="file-name">
<span class="file-select-cb">${checked ? '☑' : '☐'}</span>
<span class="file-icon ${entry.is_dir ? 'dir' : ''}">${icon}</span>
<span>${entry.name}</span>
</div>
<span class="file-size">${entry.is_dir ? '--' : formatSize(entry.size)}</span>
<span class="file-modified">${formatDate(entry.modified)}</span>
<span class="file-perms">${entry.permissions}</span>
`;
row.addEventListener('click', () => {
if (selectedFiles.has(entry.path)) {
selectedFiles.delete(entry.path);
} else {
selectedFiles.add(entry.path);
}
renderFileList(lastEntries);
});
} else {
row.innerHTML = `
<div class="file-name">
<span class="file-icon ${entry.is_dir ? 'dir' : ''}">${icon}</span>
@ -123,7 +148,6 @@
if (entry.is_dir) {
loadDirectory(entry.path);
} else {
// Toggle selection
row.classList.toggle('selected');
if (selectedFiles.has(entry.path)) {
selectedFiles.delete(entry.path);
@ -140,6 +164,7 @@
openInEditor(entry);
}
});
}
// Long-press for context menu
let pressTimer;
@ -187,7 +212,7 @@
});
}
// ---- Context menu ----
// ---- Context menu (right-click on files) ----
function showContextMenu(e, entry) {
hideContextMenu();
@ -196,7 +221,7 @@
const items = [];
// If right-clicked on empty area or a specific entry
// If right-clicked on a specific entry
if (entry) {
if (entry.is_dir) {
items.push({ label: 'Open', action: () => loadDirectory(entry.path) });
@ -254,6 +279,92 @@
}
}
// ---- Hamburger toolbar menu ----
let toolbarMenuEl = null;
function showToolbarMenu(anchorBtn) {
hideToolbarMenu();
toolbarMenuEl = document.createElement('div');
toolbarMenuEl.className = 'file-context-menu files-toolbar-menu';
const items = [
{
label: selectionMode ? '✓ Select' : 'Select',
action: () => {
selectionMode = !selectionMode;
if (!selectionMode) selectedFiles.clear();
renderFileList(lastEntries);
},
},
{
label: 'Delete',
disabled: selectedFiles.size === 0,
danger: true,
action: () => deleteSelected(),
},
{ sep: true },
{
label: 'Upload',
action: () => uploadFile(),
},
{ sep: true },
{
label: 'Refresh',
action: () => loadDirectory(currentPath),
},
{
label: showHidden ? '✓ Show Hidden Files' : 'Show Hidden Files',
action: () => {
showHidden = !showHidden;
localStorage.setItem('atlus_files_show_hidden', showHidden ? '1' : '0');
renderFileList(lastEntries);
},
},
];
items.forEach(item => {
if (item.sep) {
const sep = document.createElement('div');
sep.className = 'context-sep';
toolbarMenuEl.appendChild(sep);
return;
}
const btn = document.createElement('button');
btn.className = 'context-item' + (item.danger ? ' danger' : '') + (item.disabled ? ' disabled' : '');
btn.textContent = item.label;
if (item.disabled) {
btn.disabled = true;
} else {
btn.addEventListener('click', () => {
hideToolbarMenu();
item.action();
});
}
toolbarMenuEl.appendChild(btn);
});
// Position below the anchor button, right-aligned
const rect = anchorBtn.getBoundingClientRect();
toolbarMenuEl.style.right = (window.innerWidth - rect.right) + 'px';
toolbarMenuEl.style.top = rect.bottom + 4 + 'px';
toolbarMenuEl.style.left = 'auto';
document.body.appendChild(toolbarMenuEl);
// Close on outside click
setTimeout(() => {
document.addEventListener('click', hideToolbarMenu, { once: true });
}, 10);
}
function hideToolbarMenu() {
if (toolbarMenuEl) {
toolbarMenuEl.remove();
toolbarMenuEl = null;
}
}
// ---- File operations ----
async function createNewFile() {
@ -302,6 +413,7 @@
});
}
selectedFiles.clear();
selectionMode = false;
loadDirectory(currentPath);
}
@ -395,7 +507,7 @@
newFolderBtn.addEventListener('click', createNewFolder);
btnGroup.appendChild(newFolderBtn);
// Upload
// Hidden upload input
uploadInput = document.createElement('input');
uploadInput.type = 'file';
uploadInput.style.display = 'none';
@ -407,41 +519,16 @@
});
container.appendChild(uploadInput);
const uploadBtn = document.createElement('button');
uploadBtn.className = 'files-action-btn';
uploadBtn.textContent = '⬆';
uploadBtn.title = 'Upload';
uploadBtn.addEventListener('click', uploadFile);
btnGroup.appendChild(uploadBtn);
// Delete selected
const deleteBtn = document.createElement('button');
deleteBtn.className = 'files-action-btn';
deleteBtn.textContent = '🗑';
deleteBtn.title = 'Delete Selected';
deleteBtn.addEventListener('click', deleteSelected);
btnGroup.appendChild(deleteBtn);
// Toggle hidden files
const hiddenBtn = document.createElement('button');
hiddenBtn.className = 'files-action-btn files-hidden-toggle' + (showHidden ? ' active' : '');
hiddenBtn.innerHTML = '<span style="font-size:20px;line-height:1;">.</span>';
hiddenBtn.title = 'Show Hidden Files';
hiddenBtn.addEventListener('click', () => {
showHidden = !showHidden;
hiddenBtn.classList.toggle('active', showHidden);
hiddenBtn.title = showHidden ? 'Hide Hidden Files' : 'Show Hidden Files';
loadDirectory(currentPath);
// Hamburger menu button
const menuBtn = document.createElement('button');
menuBtn.className = 'files-action-btn files-menu-btn';
menuBtn.innerHTML = '&#9776;';
menuBtn.title = 'Menu';
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
showToolbarMenu(menuBtn);
});
btnGroup.appendChild(hiddenBtn);
// Refresh
const refreshBtn = document.createElement('button');
refreshBtn.className = 'files-action-btn';
refreshBtn.textContent = '↻';
refreshBtn.title = 'Refresh';
refreshBtn.addEventListener('click', () => loadDirectory(currentPath));
btnGroup.appendChild(refreshBtn);
btnGroup.appendChild(menuBtn);
toolbar.appendChild(btnGroup);
container.appendChild(toolbar);
@ -508,6 +595,7 @@
destroy() {
hideContextMenu();
hideToolbarMenu();
container = null;
fileListEl = null;
breadcrumbEl = null;
@ -515,6 +603,7 @@
uploadInput = null;
currentPath = '/';
selectedFiles.clear();
selectionMode = false;
},
});
})();