refactor: barre de navigation unifiée — breadcrumb éditable + recherche intégrée
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- Barre unique remplaçant breadcrumb + path bar + header: back/forward + chemin + toolbar
- Chemin cliquable avec navigation, double-clic pour rename inline
- Édition du path intégrée: clic ✎ → input inline au même endroit
- Barre de recherche intégrée dans la toolbar (compacte, à côté du sélecteur de vue)
- Filtres compactés: icônes seules, ligne unique avec bouton clear
- Gain d'espace vertical: ~80px libérés pour le contenu
This commit is contained in:
2026-07-12 11:47:24 -04:00
parent 21806aa14e
commit 75dbdf2d4f
+97 -58
View File
@@ -222,6 +222,40 @@
.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;}
.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;}
.unified-breadcrumb .bc-root{color:var(--text-secondary);text-decoration:none;white-space:nowrap;font-weight:500;flex-shrink:0;}
.unified-breadcrumb .bc-root:hover{color:var(--accent);}
.unified-breadcrumb .bc-sep{color:var(--text-tertiary);margin:0 2px;flex-shrink:0;}
.unified-breadcrumb a{color:var(--text-secondary);text-decoration:none;white-space:nowrap;}
.unified-breadcrumb a:hover{color:var(--accent);}
.unified-breadcrumb .bc-current{color:var(--text);font-weight:600;}
.bc-edit-trigger{background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:11px;padding:2px 4px;border-radius:3px;flex-shrink:0;opacity:.5;transition:opacity 150ms;}
.bc-edit-trigger:hover{opacity:1;color:var(--accent);background:var(--bg-hover);}
.bc-edit-wrap{display:flex;align-items:center;gap:4px;flex-shrink:0;}
.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;}
/* 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;}
.search-bar-inline:focus-within{border-color:var(--accent);}
.search-bar-inline .search-icon{font-size:12px;color:var(--text-tertiary);}
.search-bar-inline .search-input{width:80px;background:none;border:none;color:var(--text);font-size:12px;outline:none;}
.search-bar-inline .search-input::placeholder{color:var(--text-tertiary);}
.search-bar-inline .search-clear{background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:14px;padding:0 2px;}
.search-bar-inline .search-clear:hover{color:var(--text);}
/* Compact filter row */
.filter-row{display:flex;align-items:center;gap:4px;padding:2px 0;margin-bottom:4px;flex-wrap:wrap;}
.filter-row .filter-chip{font-size:12px;padding:3px 8px;}
.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-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;}
@@ -310,56 +344,57 @@
<button class="undo-dismiss" @click="undoVisible=false">×</button>
</div>
<!-- Breadcrumb -->
<div class="breadcrumb-bar" x-show="folderStack.length > 0">
<a href="/local-workspace" @click.prevent="navigateToRoot()">📁 {{ workspace_name }}</a>
<template x-for="(b, i) in folderStack" :key="'bc'+b.id">
<span style="display:contents;">
<span class="sep">/</span>
<a href="#" @click.prevent="navigateToBreadcrumb(i)" x-text="b.name"
:class="{ current: i === folderStack.length - 1 }"
@dblclick.prevent="startInlineRename(b, $event)"></a>
</span>
</template>
</div>
<!-- Path bar -->
<div class="path-bar" x-show="folderStack.length > 0">
<span class="path-icon">📂</span>
<input class="path-input" :value="pathValue"
@keydown.enter="navigateToPath()"
@focus="$event.target.select()"
placeholder="/workspace/...">
<button class="path-go" @click="navigateToPath()" title="Go">→</button>
</div>
<div class="ws-header">
<h1 x-show="folderStack.length===0 || (folderStack.length===1 && folderStack[0].id===0)">📁 {{ workspace_name }}</h1>
<h1 x-show="folderStack.length>0 && !(folderStack.length===1 && folderStack[0].id===0)">
📁 <span x-text="folderStack[folderStack.length-1].name"></span>
</h1>
<div class="ws-toolbar">
<button class="btn-icon" @click="goBack()" title="Back" :disabled="!canGoBack">←</button>
<button class="btn-icon" @click="goForward()" title="Forward" :disabled="!canGoForward">→</button>
<!-- Unified navigation bar: breadcrumb + inline path edit + back/forward -->
<div class="unified-nav">
<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">
<span class="bc-icon">📁</span>
<a href="/local-workspace" @click.prevent="navigateToRoot()" class="bc-root">{{ workspace_name }}</a>
<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"
: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"
@keydown.enter="navigateToPath(); editingPath=false"
@keydown.escape="editingPath=false"
placeholder="/workspace/...">
<button class="bc-edit-go" @click="navigateToPath(); editingPath=false">→</button>
</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 view">🌳</button>
<button class="view-btn" :class="{ active: viewMode==='list' }" @click="viewMode='list'; previewItem=null" title="List view">📋</button>
<button class="view-btn" :class="{ active: viewMode==='details' }" @click="viewMode='details'; previewItem=null" title="Details view">📊</button>
<button class="view-btn" :class="{ active: viewMode==='title' }" @click="viewMode='title'; previewItem=null" title="Title cards">🏷️</button>
<button class="view-btn" :class="{ active: viewMode==='content' }" @click="viewMode='content'; previewItem=null" title="Content preview">📝</button>
<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>
<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 by...</option>
<option value="name-asc">Name A→Z</option>
<option value="name-desc">Name Z→A</option>
<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 all" x-show="viewMode==='tree'">⊞</button>
<button class="btn-icon" @click="collapseAll()" title="Collapse all" x-show="viewMode==='tree'">⊟</button>
<button class="btn-icon" @click="openModal('file')" title="New file">📄</button>
<button class="btn-icon" @click="openModal('folder')" title="New folder">📁</button>
<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>
@@ -372,22 +407,16 @@
<!-- Tree -->
<div x-show="viewMode==='tree'" style="display:contents;">
<!-- Search bar -->
<div class="search-bar" x-show="tree.length>0 || searchQuery">
<span class="search-icon">🔍</span>
<input class="search-input" type="text" placeholder="Filter files & folders..."
x-model="searchQuery" @input="doFilter" autocomplete="off">
<button class="search-clear" x-show="searchQuery" @click="searchQuery='';doFilter()" title="Clear">×</button>
</div>
<!-- Filter chips -->
<div class="filter-chips" x-show="tree.length>0">
<button class="filter-chip" :class="{ active: filterType === 'folder' }" @click="filterByType('folder')" title="Folders only">📁 Folders</button>
<button class="filter-chip" :class="{ active: filterType === 'page' }" @click="filterByType('page')" title="FlowDeck pages">📝 Pages</button>
<button class="filter-chip" :class="{ active: filterType === 'pdf' }" @click="filterByType('pdf')" title="PDF">📕 PDF</button>
<button class="filter-chip" :class="{ active: filterType === 'image' }" @click="filterByType('image')" title="Images">🖼️ Img</button>
<button class="filter-chip" :class="{ active: filterType === 'code' }" @click="filterByType('code')" title="Code">📜 Code</button>
<button class="filter-chip" :class="{ active: filterType === 'text' }" @click="filterByType('text')" title="Text">📄 Text</button>
<!-- Compact filter row -->
<div class="filter-row" x-show="tree.length>0 || searchQuery || filterType">
<button class="filter-chip" :class="{ active: filterType === 'folder' }" @click="filterByType('folder')" title="Folders">📁</button>
<button class="filter-chip" :class="{ active: filterType === 'page' }" @click="filterByType('page')" title="Pages">📝</button>
<button class="filter-chip" :class="{ active: filterType === 'pdf' }" @click="filterByType('pdf')" title="PDF">📕</button>
<button class="filter-chip" :class="{ active: filterType === 'image' }" @click="filterByType('image')" title="Images">🖼️</button>
<button class="filter-chip" :class="{ active: filterType === 'code' }" @click="filterByType('code')" title="Code">📜</button>
<button class="filter-chip" :class="{ active: filterType === 'text' }" @click="filterByType('text')" title="Text">📄</button>
<span x-show="filterType || searchQuery" class="filter-clear" @click="filterType='';searchQuery='';doFilter()">✕ clear</span>
</div>
<ul class="ws-tree" x-show="viewMode==='tree' && (displayTree.length>0 || (currentFolder > 0))" @click="handleTreeAction($event)">
@@ -748,6 +777,7 @@ document.addEventListener('alpine:init', function() {
currentFolder: {{ current_folder_id }},
searchQuery: '',
filterType: '',
editingPath: false,
folderStack: [],
// Inline rename
@@ -809,6 +839,15 @@ document.addEventListener('alpine:init', function() {
await this._loadFolder(entry ? entry.id : 0);
},
startPathEdit() {
this.editingPath = true;
var self = this;
this.$nextTick(function() {
var inp = self.$refs.pathEdit;
if (inp) { inp.focus(); inp.select(); }
});
},
// Drag indicators
dragCount: 0,
dragOverParentId: null,