feat (ui): compacte IDE-sidebar tree view voor Files tabblad

- Vervang kaart-stijl folder-rijen door compacte, platte rijen (2px padding, geen border)
- Verwijder badge-tellers (📁 N, 📄 N) uit folder-rijen
- Voeg .btn.tiny toe voor kleine actieknoppen (+/✕) in boom
- Alle mappen standaard ingeklapt; localStorage behoudt uitgeklapte staat
- file-entry hover highlight; verwijder bottom-border per rij

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 18:19:04 +01:00
parent 5f6719464d
commit 2dfe53895b
2 changed files with 58 additions and 73 deletions
+19 -17
View File
@@ -250,6 +250,7 @@ header{
.btn:hover{background: var(--btn2)} .btn:hover{background: var(--btn2)}
.btn:active{transform: translateY(1px)} .btn:active{transform: translateY(1px)}
.btn.small{padding:7px 9px; border-radius: 10px} .btn.small{padding:7px 9px; border-radius: 10px}
.btn.tiny{padding:1px 5px; border-radius: 4px; font-size:11px; line-height:16px;}
.btn.ghost{background: transparent} .btn.ghost{background: transparent}
.btn.ok{border-color: rgba(45,212,191,.6)} .btn.ok{border-color: rgba(45,212,191,.6)}
.btn.bad{border-color: rgba(251,113,133,.6)} .btn.bad{border-color: rgba(251,113,133,.6)}
@@ -643,29 +644,30 @@ pre{
.sidebar .navLabel { display: none; } .sidebar .navLabel { display: none; }
.sidebar .tab { justify-content: center; } .sidebar .tab { justify-content: center; }
} }
/* Files tree (Portainer-ish) */ /* Files tree (IDE sidebar stijl) */
.file-folder-row{ .file-folder-row{
display:flex; display:flex;
align-items:center; align-items:center;
justify-content:space-between; justify-content:space-between;
gap:10px; gap:6px;
cursor:pointer; cursor:pointer;
user-select:none; user-select:none;
padding: 10px 12px; padding: 2px 6px;
border: 1px solid var(--card-border); border: none;
border-radius: 12px; border-radius: 4px;
background: var(--folder-bg); background: transparent;
transition: background .12s ease, border-color .12s ease, transform .06s ease; transition: background .1s ease;
line-height: 22px;
font-weight: 600;
} }
.file-folder-row:hover{ .file-folder-row:hover{
background: var(--folder-hover); background: var(--folder-hover);
border-color: rgba(96,165,250,.35);
} }
.file-folder-row:active{ .file-entry:hover{
transform: translateY(1px); background: var(--folder-hover);
} }
.file-folder-left{ .file-folder-left{
@@ -697,19 +699,19 @@ pre{
.file-folder-files{ .file-folder-files{
margin-left: 0; margin-left: 0;
margin-top: 6px; margin-top: 0;
padding-left: 12px; padding-left: 0;
border-left: 1px dashed var(--soft-line); border-left: none;
} }
.file-entry{ .file-entry{
display:flex; display:flex;
align-items:center; align-items:center;
justify-content:space-between; justify-content:space-between;
gap:10px; gap:6px;
padding:4px 0; padding: 2px 6px;
border-bottom:1px dashed var(--soft-line); border-radius: 4px;
border-radius: 8px; line-height: 22px;
} }
.file-entry-name{ .file-entry-name{
cursor:pointer; cursor:pointer;
+22 -39
View File
@@ -21,9 +21,10 @@ function filesSetEditorTheme(themeName) {
window.filesSetEditorTheme = filesSetEditorTheme; window.filesSetEditorTheme = filesSetEditorTheme;
function _isFolderCollapsed(folderKey) { function _isFolderCollapsed(folderKey, level) {
return localStorage.getItem('files_folder_collapsed:' + folderKey) !== '0'; const stored = localStorage.getItem('files_folder_collapsed:' + folderKey);
// default collapsed = true if (stored !== null) return stored !== '0';
return true; // standaard alles ingeklapt
} }
function _setFolderCollapsed(folderKey, v) { function _setFolderCollapsed(folderKey, v) {
@@ -246,37 +247,26 @@ async function filesRefresh() {
function renderNode(node, level) { function renderNode(node, level) {
const folderKey = node.apiPath; const folderKey = node.apiPath;
const collapsed = _isFolderCollapsed(folderKey); const collapsed = _isFolderCollapsed(folderKey, level);
const label = node.uiPath || 'root'; const label = node.uiPath || 'root';
const indent = Math.max(0, level) * 14; const indent = Math.max(0, level) * 14;
const folder = folderByPath.get(folderKey); const folder = folderByPath.get(folderKey);
const files = (folder && folder.files) ? folder.files : []; const files = (folder && folder.files) ? folder.files : [];
// subfolders (NU AL beschikbaar voor de badges)
const childNames = Array.from(node.children.keys()).sort((a,b) => a.localeCompare(b)); const childNames = Array.from(node.children.keys()).sort((a,b) => a.localeCompare(b));
// files (NU AL beschikbaar voor de badges)
const sortedFiles = (files || []).slice().sort((a,b) => a.localeCompare(b)); const sortedFiles = (files || []).slice().sort((a,b) => a.localeCompare(b));
const out = []; const out = [];
out.push(` out.push(`<div class="mono file-folder-row" data-folder="${esc(folderKey)}" style="padding-left:${indent}px;">
<div class="mono file-folder-row" data-folder="${esc(folderKey)}" style="margin:${level === 0 ? '8px' : '6px'} 0 6px 0; padding-left:${indent}px; font-weight:600;">
<span class="file-folder-left"> <span class="file-folder-left">
<span class="folder-toggle">${collapsed ? '▶' : '▼'}</span> <span class="folder-toggle">${collapsed ? '▶' : '▼'}</span>
<span>📂 ${esc(label)}</span> <span>📂 ${esc(label)}</span>
</span> </span>
<span class="file-folder-meta" onclick="event.stopPropagation();"> <span class="file-folder-actions" onclick="event.stopPropagation();">
<span class="file-badge" title="Subfolders in deze map">📁 ${childNames.length}</span> <button class="btn tiny ok" title="Nieuw bestand in ${esc(label)}" onclick="filesNewFileInFolder(decodeURIComponent('${encodeURIComponent(node.uiPath)}'))">+</button>
<span class="file-badge" title="Bestanden in deze map">📄 ${sortedFiles.length}</span> <button class="btn tiny bad" title="Verwijder map (alleen als leeg)" onclick="filesDeleteFolder(decodeURIComponent('${encodeURIComponent(node.uiPath)}'))">✕</button>
<span class="flex file-folder-actions">
<button class="btn small ok" title="Nieuw bestand in ${esc(label)}" onclick="filesNewFileInFolder(decodeURIComponent('${encodeURIComponent(node.uiPath)}'))">+</button>
<button class="btn small bad" title="Verwijder map (alleen als leeg)" onclick="filesDeleteFolder(decodeURIComponent('${encodeURIComponent(node.uiPath)}'))">🗑️</button>
</span> </span>
</span> </div>`);
</div>
`);
out.push(`<div class="file-folder-files" data-folder-files="${esc(folderKey)}" style="${collapsed ? 'display:none;' : ''}">`); out.push(`<div class="file-folder-files" data-folder-files="${esc(folderKey)}" style="${collapsed ? 'display:none;' : ''}">`);
@@ -287,16 +277,14 @@ async function filesRefresh() {
for (const f of sortedFiles) { for (const f of sortedFiles) {
const fullUi = node.uiPath ? `${node.uiPath}/${f}` : f; const fullUi = node.uiPath ? `${node.uiPath}/${f}` : f;
const fileKey = encodeURIComponent(fullUi); const fileKey = encodeURIComponent(fullUi);
out.push(` out.push(`<div class="file-entry" data-file="${fileKey}" style="padding-left:${indent + 16}px;">
<div class="file-entry" data-file="${fileKey}" style="padding-left:${indent + 18}px;">
<span class="mono file-entry-name" onclick="filesOpen(decodeURIComponent('${fileKey}'))">📄 ${esc(f)}</span> <span class="mono file-entry-name" onclick="filesOpen(decodeURIComponent('${fileKey}'))">📄 ${esc(f)}</span>
<span class="file-entry-state"></span> <span class="file-entry-state"></span>
</div> </div>`);
`);
} }
if (!childNames.length && !sortedFiles.length) { if (!childNames.length && !sortedFiles.length) {
out.push(`<div class="muted" style="padding-left:${indent + 18}px;">(leeg)</div>`); out.push(`<div class="muted" style="padding-left:${indent + 16}px; font-size:0.85em;">(leeg)</div>`);
} }
out.push(`</div>`); out.push(`</div>`);
@@ -311,30 +299,25 @@ async function filesRefresh() {
const rootFolder = folderByPath.get(FILES_ROOT); const rootFolder = folderByPath.get(FILES_ROOT);
if (rootFolder && (rootFolder.files || []).length) { if (rootFolder && (rootFolder.files || []).length) {
const folderKey = FILES_ROOT; const folderKey = FILES_ROOT;
const collapsed = _isFolderCollapsed(folderKey); const collapsed = _isFolderCollapsed(folderKey, 0);
parts.unshift(` parts.unshift(`<div class="mono file-folder-row" data-folder="${esc(folderKey)}">
<div class="mono file-folder-row" data-folder="${esc(folderKey)}" style="margin:8px 0 6px 0; font-weight:600;">
<span class="file-folder-left"> <span class="file-folder-left">
<span class="folder-toggle">${collapsed ? '▶' : '▼'}</span> <span class="folder-toggle">${collapsed ? '▶' : '▼'}</span>
<span>📂 root</span> <span>📂 root</span>
</span> </span>
<span class="flex" onclick="event.stopPropagation();"> <span class="file-folder-actions" onclick="event.stopPropagation();">
<button class="btn small ok" title="Nieuw bestand in root" onclick="filesNewFileInFolder('')">+</button> <button class="btn tiny ok" title="Nieuw bestand in root" onclick="filesNewFileInFolder('')">+</button>
</span> </span>
</div> </div>
<div class="file-folder-files" data-folder-files="${esc(folderKey)}" style="${collapsed ? 'display:none;' : ''}"> <div class="file-folder-files" data-folder-files="${esc(folderKey)}" style="${collapsed ? 'display:none;' : ''}">
${(rootFolder.files || []).slice().sort((a,b)=>a.localeCompare(b)).map(f => { ${(rootFolder.files || []).slice().sort((a,b)=>a.localeCompare(b)).map(f => {
const fullUi = f; const fileKey = encodeURIComponent(f);
const fileKey = encodeURIComponent(fullUi); return `<div class="file-entry" data-file="${fileKey}" style="padding-left:16px;">
return ` <span class="mono file-entry-name" onclick="filesOpen(decodeURIComponent('${fileKey}'))">📄 ${esc(f)}</span>
<div class="file-entry" data-file="${fileKey}" style="padding-left:18px;">
<span class="mono file-entry-name" style="cursor:pointer" onclick="filesOpen(decodeURIComponent('${fileKey}'))">📄 ${esc(f)}</span>
<span class="file-entry-state"></span> <span class="file-entry-state"></span>
</div> </div>`;
`;
}).join('')} }).join('')}
</div> </div>`);
`);
} }
treeEl.innerHTML = parts.join(''); treeEl.innerHTML = parts.join('');