feat: dropdown view + breadcrumb au-dessus + menu contextuel clic-droit
- Sélecteur de vue en dropdown (🌳 Tree ▾) au lieu des 5 boutons
- Layout split: breadcrumb row (chemin + ←→) au-dessus, toolbar row en dessous
- Menu contextuel clic-droit/long-press sur fichiers et dossiers:
Open, Open folder, Rename, Duplicate, Tags (add/remove inline), Delete
- Gestion des tags depuis le menu contextuel avec sous-menu Tags
This commit is contained in:
@@ -222,8 +222,10 @@
|
||||
.ws-table-compact td{padding:5px 10px;border-bottom:1px solid var(--border);}
|
||||
.ws-table-compact tr:hover td{background:var(--bg-hover);}
|
||||
|
||||
/* Unified navigation bar */
|
||||
.unified-nav{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:10px;margin-bottom:6px;flex-wrap:wrap;}
|
||||
/* Breadcrumb row */
|
||||
.breadcrumb-row{display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:10px 10px 0 0;margin-bottom:0;flex-wrap:wrap;}
|
||||
/* Toolbar row */
|
||||
.toolbar-row{display:flex;align-items:center;gap:6px;padding:4px 8px;background:var(--bg-secondary);border:1px solid var(--border);border-top:none;border-radius:0 0 10px 10px;margin-bottom:8px;flex-wrap:wrap;}
|
||||
.nav-arrow{width:28px;height:28px;font-size:13px;flex-shrink:0;}
|
||||
.unified-breadcrumb{flex:1;display:flex;align-items:center;gap:2px;min-width:0;font-size:13px;overflow:hidden;}
|
||||
.unified-breadcrumb .bc-icon{font-size:15px;flex-shrink:0;}
|
||||
@@ -239,7 +241,7 @@
|
||||
.bc-edit-input{width:200px;background:var(--bg);border:1px solid var(--accent);border-radius:5px;color:var(--text);font-size:12px;padding:3px 8px;outline:none;font-family:'Fira Code',monospace;}
|
||||
.bc-edit-go{background:var(--accent);color:#fff;border:none;padding:3px 8px;border-radius:4px;cursor:pointer;font-size:12px;}
|
||||
.bc-edit-go:hover{opacity:.85;}
|
||||
.unified-actions{display:flex;align-items:center;gap:6px;flex-shrink:0;flex-wrap:wrap;}
|
||||
.unified-actions{display:flex;align-items:center;gap:6px;flex-shrink:0;flex-wrap:wrap;} /* unused, superseded by toolbar-row */
|
||||
|
||||
/* Search bar inline (in unified nav) */
|
||||
.search-bar-inline{display:flex;align-items:center;gap:4px;padding:2px 8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;min-width:140px;}
|
||||
@@ -256,8 +258,30 @@
|
||||
.filter-clear{font-size:11px;color:var(--text-tertiary);cursor:pointer;padding:2px 6px;border-radius:4px;}
|
||||
.filter-clear:hover{color:var(--danger);background:var(--bg-hover);}
|
||||
|
||||
/* View selector */
|
||||
.view-selector{display:flex;gap:2px;background:var(--bg-tertiary);border-radius:8px;padding:2px;}
|
||||
/* View dropdown */
|
||||
.view-dropdown{position:relative;}
|
||||
.view-dropdown-btn{display:flex;align-items:center;gap:6px;padding:5px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;cursor:pointer;font-size:12px;color:var(--text);white-space:nowrap;}
|
||||
.view-dropdown-btn:hover{background:var(--bg-hover);border-color:var(--accent);}
|
||||
.dropdown-arrow{font-size:10px;color:var(--text-tertiary);}
|
||||
.view-dropdown-menu{position:absolute;top:100%;left:0;margin-top:4px;background:var(--bg);border:1px solid var(--border-strong);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.4);z-index:250;min-width:140px;overflow:hidden;}
|
||||
.view-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 12px;background:none;border:none;color:var(--text);font-size:13px;cursor:pointer;text-align:left;}
|
||||
.view-menu-item:hover{background:var(--bg-hover);}
|
||||
.view-menu-item.active{background:rgba(35,131,226,.12);color:var(--accent);font-weight:500;}
|
||||
|
||||
/* Context menu */
|
||||
.ctx-menu{position:fixed;z-index:300;background:var(--bg);border:1px solid var(--border-strong);border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.5);min-width:180px;overflow:hidden;padding:4px 0;}
|
||||
.ctx-item{display:flex;align-items:center;gap:8px;padding:7px 14px;font-size:13px;color:var(--text);cursor:pointer;transition:background 100ms;}
|
||||
.ctx-item:hover{background:var(--bg-hover);}
|
||||
.ctx-item.ctx-danger{color:var(--danger);}
|
||||
.ctx-item.ctx-danger:hover{background:rgba(255,80,80,.1);}
|
||||
.ctx-sep{height:1px;background:var(--border);margin:3px 0;}
|
||||
.ctx-tag-item{justify-content:space-between;}
|
||||
.ctx-tag-add{color:var(--text-tertiary);font-size:12px;padding:4px 14px;cursor:pointer;}
|
||||
.ctx-tag-add:hover{color:var(--accent);}
|
||||
.ctx-has-sub{justify-content:space-between;}
|
||||
|
||||
/* Old view-selector kept for no conflict, but hidden */
|
||||
.view-selector{display:none;}
|
||||
.view-btn{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;background:none;border:none;border-radius:6px;cursor:pointer;font-size:14px;color:var(--text-tertiary);transition:all 150ms;}
|
||||
.view-btn:hover{background:var(--bg-hover);color:var(--text);}
|
||||
.view-btn.active{background:var(--accent);color:#fff;}
|
||||
@@ -311,6 +335,7 @@
|
||||
@dragleave="onDragLeave($event)"
|
||||
@drop.prevent="onDrop($event)"
|
||||
@keydown="onKeydown($event)"
|
||||
@contextmenu.prevent="onContextMenu($event)"
|
||||
tabindex="0"
|
||||
x-ref="layout">
|
||||
|
||||
@@ -344,8 +369,8 @@
|
||||
<button class="undo-dismiss" @click="undoVisible=false">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Unified navigation bar: breadcrumb + inline path edit + back/forward -->
|
||||
<div class="unified-nav">
|
||||
<!-- Breadcrumb row -->
|
||||
<div class="breadcrumb-row">
|
||||
<button class="btn-icon nav-arrow" @click="goBack()" title="Back" :disabled="!canGoBack">←</button>
|
||||
<button class="btn-icon nav-arrow" @click="goForward()" title="Forward" :disabled="!canGoForward">→</button>
|
||||
<div class="unified-breadcrumb">
|
||||
@@ -354,14 +379,13 @@
|
||||
<template x-for="(b, i) in folderStack" :key="'bc'+b.id">
|
||||
<span style="display:contents;">
|
||||
<span class="bc-sep">/</span>
|
||||
<a href="#" @click.prevent="navigateToBreadcrumb(i)" x-text="b.name"
|
||||
<a href="#" @click.prevent="navigateToBreadcrumb(i)" x-text="b.name"
|
||||
:class="{ 'bc-current': i === folderStack.length - 1 }"
|
||||
@dblclick.prevent="startInlineRename(b, $event)"></a>
|
||||
</span>
|
||||
</template>
|
||||
<!-- Inline path editor (shown on click) -->
|
||||
<div class="bc-edit-wrap" x-show="editingPath" @click.outside="editingPath=false">
|
||||
<input class="bc-edit-input" x-ref="pathEdit" :value="pathValue"
|
||||
<div class="bc-edit-wrap" x-show="editingPath" @click.outside="editingPath=false" style="display:contents;">
|
||||
<input class="bc-edit-input" x-ref="pathEdit" :value="pathValue"
|
||||
@keydown.enter="navigateToPath(); editingPath=false"
|
||||
@keydown.escape="editingPath=false"
|
||||
placeholder="/workspace/...">
|
||||
@@ -369,33 +393,42 @@
|
||||
</div>
|
||||
<button class="bc-edit-trigger" x-show="!editingPath" @click="startPathEdit()" title="Edit path">✎</button>
|
||||
</div>
|
||||
<div class="unified-actions">
|
||||
<div class="view-selector">
|
||||
<button class="view-btn" :class="{ active: viewMode==='tree' }" @click="viewMode='tree'; previewItem=null" title="Tree">🌳</button>
|
||||
<button class="view-btn" :class="{ active: viewMode==='list' }" @click="viewMode='list'; previewItem=null" title="List">📋</button>
|
||||
<button class="view-btn" :class="{ active: viewMode==='details' }" @click="viewMode='details'; previewItem=null" title="Details">📊</button>
|
||||
<button class="view-btn" :class="{ active: viewMode==='title' }" @click="viewMode='title'; previewItem=null" title="Cards">🏷️</button>
|
||||
<button class="view-btn" :class="{ active: viewMode==='content' }" @click="viewMode='content'; previewItem=null" title="Content">📝</button>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar row -->
|
||||
<div class="toolbar-row">
|
||||
<div class="view-dropdown" @click.outside="viewMenuOpen=false" @keydown.escape="viewMenuOpen=false">
|
||||
<button class="view-dropdown-btn" @click="viewMenuOpen=!viewMenuOpen" title="View">
|
||||
<span x-text="viewIcons[viewMode] || '🌳'"></span>
|
||||
<span x-text="viewLabels[viewMode] || 'Tree'"></span>
|
||||
<span class="dropdown-arrow">▾</span>
|
||||
</button>
|
||||
<div class="view-dropdown-menu" x-show="viewMenuOpen" x-transition @click="viewMenuOpen=false">
|
||||
<button class="view-menu-item" :class="{ active: viewMode==='tree' }" @click="viewMode='tree'; previewItem=null">🌳 Tree</button>
|
||||
<button class="view-menu-item" :class="{ active: viewMode==='list' }" @click="viewMode='list'; previewItem=null">📋 List</button>
|
||||
<button class="view-menu-item" :class="{ active: viewMode==='details' }" @click="viewMode='details'; previewItem=null">📊 Details</button>
|
||||
<button class="view-menu-item" :class="{ active: viewMode==='title' }" @click="viewMode='title'; previewItem=null">🏷️ Cards</button>
|
||||
<button class="view-menu-item" :class="{ active: viewMode==='content' }" @click="viewMode='content'; previewItem=null">📝 Content</button>
|
||||
</div>
|
||||
<div class="search-bar-inline">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input class="search-input" type="text" placeholder="Filter..."
|
||||
x-model="searchQuery" @input="doFilter" autocomplete="off">
|
||||
<button class="search-clear" x-show="searchQuery" @click="searchQuery='';doFilter()" title="Clear">×</button>
|
||||
</div>
|
||||
<select class="sort-select" x-model="sortBy" @change="doSort()">
|
||||
<option value="">Sort</option>
|
||||
<option value="name-asc">A→Z</option>
|
||||
<option value="name-desc">Z→A</option>
|
||||
<option value="type">Type</option>
|
||||
<option value="date-asc">Oldest</option>
|
||||
<option value="date-desc">Newest</option>
|
||||
</select>
|
||||
<button class="btn-icon" @click="expandAll()" title="Expand" x-show="viewMode==='tree'">⊞</button>
|
||||
<button class="btn-icon" @click="collapseAll()" title="Collapse" x-show="viewMode==='tree'">⊟</button>
|
||||
<button class="btn-icon btn-create" @click="openModal('file')" title="New file">📄</button>
|
||||
<button class="btn-icon btn-create" @click="openModal('folder')" title="New folder">📁</button>
|
||||
</div>
|
||||
<div class="search-bar-inline">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input class="search-input" type="text" placeholder="Filter..."
|
||||
x-model="searchQuery" @input="doFilter" autocomplete="off">
|
||||
<button class="search-clear" x-show="searchQuery" @click="searchQuery='';doFilter()" title="Clear">×</button>
|
||||
</div>
|
||||
<select class="sort-select" x-model="sortBy" @change="doSort()">
|
||||
<option value="">Sort</option>
|
||||
<option value="name-asc">A→Z</option>
|
||||
<option value="name-desc">Z→A</option>
|
||||
<option value="type">Type</option>
|
||||
<option value="date-asc">Oldest</option>
|
||||
<option value="date-desc">Newest</option>
|
||||
</select>
|
||||
<button class="btn-icon" @click="expandAll()" title="Expand" x-show="viewMode==='tree'">⊞</button>
|
||||
<button class="btn-icon" @click="collapseAll()" title="Collapse" x-show="viewMode==='tree'">⊟</button>
|
||||
<button class="btn-icon btn-create" @click="openModal('file')" title="New file">📄</button>
|
||||
<button class="btn-icon btn-create" @click="openModal('folder')" title="New folder">📁</button>
|
||||
</div>
|
||||
|
||||
<!-- Bulk actions bar -->
|
||||
@@ -720,6 +753,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Context menu -->
|
||||
<div class="ctx-menu" x-show="ctxMenu.visible"
|
||||
:style="{ left: ctxMenu.x + 'px', top: ctxMenu.y + 'px' }"
|
||||
@click.outside="ctxMenu.visible=false"
|
||||
@keydown.escape="ctxMenu.visible=false"
|
||||
x-transition>
|
||||
<div class="ctx-item" @click="ctxMenuOpenPage()">📄 Open</div>
|
||||
<div class="ctx-item" x-show="ctxMenu.node.is_folder" @click="ctxMenuOpenFolder()">📂 Open folder</div>
|
||||
<div class="ctx-item" @click="ctxMenuRename()">✏️ Rename</div>
|
||||
<div class="ctx-item" @click="ctxMenuDuplicate()">📋 Duplicate</div>
|
||||
<div class="ctx-sep"></div>
|
||||
<div class="ctx-item ctx-has-sub" @click.stop="ctxTagSub=!ctxTagSub">
|
||||
🏷️ Tags
|
||||
<span style="margin-left:auto">▸</span>
|
||||
</div>
|
||||
<div x-show="ctxTagSub" style="padding:4px 0;">
|
||||
<template x-for="t in (ctxMenu.tags||[])" :key="'ctx-tag'+t.id">
|
||||
<div class="ctx-item ctx-tag-item">
|
||||
<span class="tag-chip" style="margin-right:6px;"><span x-text="t.name"></span></span>
|
||||
<button class="tag-chip rm-btn" @click.stop="ctxRemoveTag(t.id)" title="Remove tag">×</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ctx-tag-add" x-show="!ctxTagAdding" @click.stop="ctxTagAdding=true">+ Add tag</div>
|
||||
<div x-show="ctxTagAdding" style="display:flex;gap:4px;padding:4px 12px;">
|
||||
<input class="tag-input" x-ref="ctxTagInput"
|
||||
@keydown.enter="ctxAddTag($event.target.value); $event.target.value=''"
|
||||
@keydown.escape="ctxTagAdding=false; ctxTagSub=false"
|
||||
@click.stop
|
||||
placeholder="tag name...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="ctx-sep"></div>
|
||||
<div class="ctx-item ctx-danger" @click="ctxMenuDelete()">🗑 Delete</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview tooltip -->
|
||||
<div class="file-preview" x-show="previewVisible"
|
||||
:style="{ left: previewX + 'px', top: previewY + 'px' }"
|
||||
@@ -794,6 +862,14 @@ document.addEventListener('alpine:init', function() {
|
||||
// Sort
|
||||
sortBy: '',
|
||||
viewMode: 'tree',
|
||||
viewMenuOpen: false,
|
||||
viewIcons: {tree:'🌳',list:'📋',details:'📊',title:'🏷️',content:'📝'},
|
||||
viewLabels: {tree:'Tree',list:'List',details:'Details',title:'Cards',content:'Content'},
|
||||
|
||||
// Context menu
|
||||
ctxMenu: {visible:false,x:0,y:0,node:null,tags:[]},
|
||||
ctxTagSub: false,
|
||||
ctxTagAdding: false,
|
||||
|
||||
// Tags
|
||||
workspaceTags: [],
|
||||
@@ -1112,6 +1188,87 @@ document.addEventListener('alpine:init', function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
// ── Context menu ──
|
||||
onContextMenu(ev) {
|
||||
// Find the closest item element with data-ws-id or a clickable row
|
||||
var el = ev.target.closest('[data-ws-id], .ws-tree-item, .title-card, .content-item, tr[style*="cursor:pointer"]');
|
||||
if (!el) { this.ctxMenu.visible = false; return; }
|
||||
// Get the node id
|
||||
var id = parseInt(el.getAttribute('data-ws-id') || el.getAttribute('data-folder-id'));
|
||||
if (!id) {
|
||||
// Try to find the node from displayTree by index match
|
||||
var rows = this.$el.querySelectorAll('tr[style*="cursor:pointer"], .title-card, .content-item');
|
||||
for (var i=0; i<rows.length; i++) {
|
||||
if (rows[i] === el || rows[i].contains(el)) {
|
||||
if (this.displayTree[i]) { id = this.displayTree[i].id; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!id) { this.ctxMenu.visible = false; return; }
|
||||
// Find node in tree
|
||||
var node = this._findNodeById(this.displayTree, id);
|
||||
if (!node) { this.ctxMenu.visible = false; return; }
|
||||
// Set context menu
|
||||
this.ctxMenu = {
|
||||
visible: true,
|
||||
x: Math.min(ev.clientX, window.innerWidth - 200),
|
||||
y: Math.min(ev.clientY, window.innerHeight - 300),
|
||||
node: node,
|
||||
tags: [...(node.tags||[])]
|
||||
};
|
||||
this.ctxTagSub = false;
|
||||
this.ctxTagAdding = false;
|
||||
},
|
||||
|
||||
ctxMenuOpenPage() {
|
||||
if (this.ctxMenu.node) this.openPage(this.ctxMenu.node.id);
|
||||
this.ctxMenu.visible = false;
|
||||
},
|
||||
|
||||
ctxMenuOpenFolder() {
|
||||
if (this.ctxMenu.node) this.navigateToFolder(this.ctxMenu.node.id);
|
||||
this.ctxMenu.visible = false;
|
||||
},
|
||||
|
||||
ctxMenuRename() {
|
||||
if (this.ctxMenu.node) { this.startRename(this.ctxMenu.node); this.ctxMenu.visible = false; }
|
||||
},
|
||||
|
||||
ctxMenuDuplicate() {
|
||||
if (this.ctxMenu.node) { this.duplicateItem(this.ctxMenu.node); this.ctxMenu.visible = false; }
|
||||
},
|
||||
|
||||
ctxMenuDelete() {
|
||||
if (this.ctxMenu.node) { this.deleteWithUndo(this.ctxMenu.node); this.ctxMenu.visible = false; }
|
||||
},
|
||||
|
||||
ctxRemoveTag(tagId) {
|
||||
var node = this.ctxMenu.node;
|
||||
if (!node) return;
|
||||
this.removeTag(node.id, tagId);
|
||||
// Update ctxMenu tags
|
||||
this.ctxMenu.tags = this.ctxMenu.tags.filter(function(t) { return t.id !== tagId; });
|
||||
this.ctxTagSub = true; // keep sub open
|
||||
},
|
||||
|
||||
async ctxAddTag(tagName) {
|
||||
var node = this.ctxMenu.node;
|
||||
if (!node || !tagName) return;
|
||||
var r = await fetch('/api/local-workspace/items/' + node.id + '/tags', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({name: tagName.trim()})
|
||||
});
|
||||
if (r.ok) {
|
||||
var d = await r.json();
|
||||
this.ctxMenu.tags.push(d.tag);
|
||||
this._updateNodeTags(this.tree, node.id, d.tag, 'add');
|
||||
this.displayTree = [...this.tree];
|
||||
this.loadWorkspaceTags();
|
||||
}
|
||||
this.ctxTagAdding = false;
|
||||
this.ctxTagSub = true;
|
||||
},
|
||||
|
||||
_markTree(nodes, depth) {
|
||||
for (var i=0; i<nodes.length; i++) {
|
||||
var n = nodes[i];
|
||||
|
||||
Reference in New Issue
Block a user