refactor: local-workspace UI → Library-style
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

CSS:
- Ajouté: ws-container, ws-tabs-row, ws-tab, ws-icon-btn, ws-btn-primary
- Ajouté: ws-dropdown, ws-search-bar, ws-select-bar, ws-row
- Ajouté: row-name, row-icon, row-title, hover-only, ws-checkbox
- Ajouté: tree-chevron, btn-open, more-menu (Library-style)
- Conservé: ws-tree, ws-tree-item, ws-act, breadcrumb-row pour compatibilité

Navbar:
- Breadcrumb + toolbar fusionnés en une seule row propre
- Dropdowns style Library (View, Sort)
- Search toggle avec barre coulissante
- Bulk bar → ws-select-bar avec icônes SVG

Context menu:
- .ctx-menu → .more-menu (style Library)
- Toutes les refs JS mises à jour

143 tests passent
This commit is contained in:
2026-07-21 15:53:36 -04:00
parent ef8d4e6bca
commit 85b4d624b8
+231 -98
View File
@@ -17,12 +17,141 @@
@drop.prevent="onDrop($event)"
@keydown="onKeydown($event)">
<style>
.ws-layout{max-width:800px;margin:0 auto;padding:40px 24px;}/* deprecated by ws-split */
@media (max-width: 768px) { .ws-layout { padding: 20px 12px; } }
.ws-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;flex-wrap:wrap;gap:12px;}
.ws-header h1{font-size:24px;}
.ws-toolbar{display:flex;gap:8px;}
/* ── Library-style layout ── */
.ws-container{max-width:1100px;margin:0 auto;padding:20px 24px;}
.ws-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;}
.ws-header h1{font-size:24px;font-weight:700;color:var(--text-primary);margin:0;}
/* ── Tabs row ── */
.ws-tabs-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px;padding:4px 0;}
.ws-tabs{display:flex;gap:2px;align-items:center;overflow-x:auto;scrollbar-width:none;}
.ws-tabs::-webkit-scrollbar{display:none;}
.ws-tab{display:flex;align-items:center;gap:6px;padding:6px 12px;font-size:13px;font-weight:500;color:var(--text-secondary);background:transparent;border:none;border-radius:6px;cursor:pointer;transition:all 0.12s;white-space:nowrap;}
.ws-tab:hover{color:var(--text-primary);background:var(--bg-hover);}
.ws-tab.active{color:var(--text-primary);background:var(--bg-active);}
.ws-tab svg{width:14px;height:14px;flex-shrink:0;}
.ws-toolbar{display:flex;align-items:center;gap:2px;flex-shrink:0;}
.ws-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;background:transparent;border:none;color:var(--text-secondary);cursor:pointer;transition:background 0.1s,color 0.1s;}
.ws-icon-btn:hover,.ws-icon-btn.active{background:var(--bg-hover);color:var(--text-primary);}
.ws-icon-btn svg{width:16px;height:16px;}
.ws-btn-primary{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;font-size:13px;font-weight:500;color:#fff;background:var(--accent);border:none;border-radius:6px;cursor:pointer;transition:background 0.1s;}
.ws-btn-primary:hover{background:var(--accent-hover);}
.ws-btn-primary svg{width:14px;height:14px;}
/* ── Dropdowns ── */
.ws-dropdown{position:absolute;top:calc(100% + 4px);right:0;min-width:210px;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow-popover);z-index:200;padding:6px;}
.ws-dropdown .dd-label{padding:6px 8px 2px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.3px;}
.ws-dropdown .dd-item{padding:6px 8px;font-size:13px;color:var(--text-primary);cursor:pointer;border-radius:4px;display:flex;align-items:center;gap:8px;transition:background 0.1s;}
.ws-dropdown .dd-item:hover{background:var(--bg-hover);}
.ws-dropdown .dd-item.active{color:var(--accent);}
.ws-dropdown .dd-item .check{width:14px;flex-shrink:0;color:var(--accent);}
.lib-toolbar-wrap{position:relative;}
/* ── Search bar ── */
.ws-search-bar{margin-bottom:10px;}
.ws-search-bar input{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);color:var(--text-primary);font-size:13px;outline:none;}
.ws-search-bar input:focus{border-color:var(--accent);}
/* ── Multi-select bar ── */
.ws-select-bar{display:flex;align-items:center;gap:4px;padding:6px 10px;background:var(--bg-secondary);border-radius:8px;margin-bottom:8px;font-size:13px;color:var(--text-secondary);min-height:38px;}
.ws-select-bar .sel-count{font-weight:500;color:var(--text-primary);margin-right:8px;white-space:nowrap;}
.ws-select-bar button{background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:6px 8px;border-radius:6px;font-size:13px;display:flex;align-items:center;gap:6px;transition:all 0.12s;}
.ws-select-bar button:hover{background:var(--bg-hover);color:var(--text-primary);}
.ws-select-bar button svg{width:14px;height:14px;}
/* ── Table (Notion-style rows) ── */
.ws-table{width:100%;}
.ws-table-header{display:flex;align-items:center;border-bottom:1px solid var(--border);padding:0 4px;}
.ws-table-header .th{display:flex;align-items:center;gap:5px;padding:6px 8px;font-size:12px;font-weight:500;color:var(--text-dim);user-select:none;white-space:nowrap;}
.ws-table-header .th svg{width:13px;height:13px;flex-shrink:0;}
/* Column widths */
.ws-col-name{flex:1;min-width:0;}
.ws-col-type{width:80px;flex-shrink:0;}
.ws-col-date{width:110px;flex-shrink:0;justify-content:flex-end;}
/* ── Row ── */
.ws-row{display:flex;align-items:center;height:34px;transition:background 0.1s;position:relative;}
.ws-row:hover{background:var(--bg-hover);}
.ws-row.selected{background:rgba(35,131,226,0.08);}
.ws-row.drag-over{outline:2px solid var(--accent);outline-offset:-2px;background:rgba(35,131,226,0.12);}
.ws-row.dragging{opacity:0.4;}
.ws-row .row-cell{display:flex;align-items:center;padding:0 8px;height:100%;overflow:hidden;}
.ws-row .row-cell.ws-col-date{color:var(--text-secondary);font-size:13px;white-space:nowrap;}
/* ── Name cell ── */
.row-name{display:flex;align-items:center;gap:6px;min-width:0;flex:1;position:relative;}
.row-icon{font-size:15px;flex-shrink:0;width:20px;text-align:center;display:inline-flex;align-items:center;justify-content:center;}
.row-icon svg{width:16px;height:16px;color:var(--text-dim);}
.row-title{font-size:13px;font-weight:400;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;cursor:pointer;}
.row-title:hover{text-decoration:underline;}
.row-title input{width:100%;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;}
/* ── Hover-only elements ── */
.hover-only{opacity:0;transition:opacity 0.12s;flex-shrink:0;}
.ws-row:hover .hover-only,.ws-row.selected .hover-only{opacity:1;}
/* ── Drag handle ── */
.drag-handle{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;cursor:grab;flex-shrink:0;margin-right:2px;}
.drag-handle:active{cursor:grabbing;}
.drag-handle svg{width:14px;height:14px;color:var(--text-dim);opacity:0.75;}
/* ── Checkbox ── */
.ws-checkbox{width:15px;height:15px;border:1.5px solid var(--text-dim);border-radius:3px;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;transition:border-color 0.1s;}
.ws-checkbox:hover{border-color:var(--text-primary);}
.ws-checkbox.checked{background:var(--accent);border-color:var(--accent);opacity:1!important;}
.ws-checkbox.checked svg{width:10px;height:10px;color:#fff;}
/* ── OPEN button ── */
.btn-open{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border:1px solid var(--border);border-radius:4px;font-size:11px;font-weight:500;color:var(--text-primary);background:var(--bg-tertiary);cursor:pointer;white-space:nowrap;transition:all 0.1s;flex-shrink:0;text-decoration:none;margin-left:auto;}
.btn-open:hover{background:var(--bg-active);}
.btn-open svg{width:12px;height:12px;}
/* ── Expand chevron ── */
.tree-chevron{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;background:none;border:none;color:var(--text-dim);cursor:pointer;padding:0;border-radius:3px;transition:transform 0.15s,background 0.1s;}
.tree-chevron:hover{color:var(--text-primary);background:var(--bg-active);}
.tree-chevron svg{width:11px;height:11px;transition:transform 0.15s;}
.tree-chevron.open svg{transform:rotate(90deg);}
/* Indent */
.row-indent{flex-shrink:0;}
/* ── "Add new" row ── */
.add-new-row{padding:8px 12px;color:var(--text-dim);font-size:13px;cursor:pointer;transition:color 0.15s;display:flex;align-items:center;gap:4px;}
.add-new-row:hover{color:var(--text-primary);}
.add-new-row svg{width:13px;height:13px;}
.add-new-under{padding:5px 8px;color:var(--text-dim);font-size:12px;cursor:pointer;transition:color 0.15s;display:flex;align-items:center;gap:4px;}
.add-new-under:hover{color:var(--text-primary);}
.add-new-under svg{width:12px;height:12px;}
/* ── Context menu ── */
.more-menu{position:fixed;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow-popover);z-index:600;padding:4px;}
.more-menu .menu-item{padding:6px 10px;font-size:13px;color:var(--text-primary);cursor:pointer;border-radius:4px;display:flex;align-items:center;gap:8px;transition:background 0.1s;}
.more-menu .menu-item:hover{background:var(--bg-hover);}
.more-menu .menu-item svg{width:14px;height:14px;flex-shrink:0;}
.more-menu .menu-item .shortcut{margin-left:auto;font-size:11px;color:var(--text-dim);}
.more-menu .menu-danger{color:var(--danger);}
.more-sep{height:1px;background:var(--border);margin:4px 8px;}
/* ── Keep existing functional styles ── */
.breadcrumb-row{display:flex;align-items:center;gap:4px;padding:6px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;margin-bottom:10px;flex-wrap:wrap;}
.breadcrumb-row .nav-arrow{width:26px;height:26px;padding:0;font-size:13px;}
.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;}
/* ── Preserved styles ── */
.ws-tree{list-style:none;margin:0;padding:0;}
.ws-tree-item{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:6px;cursor:pointer;transition:background 150ms;border:1px solid transparent;margin-bottom:2px;}
.ws-tree-item:hover{background:var(--bg-tertiary);border-color:var(--border);}
@@ -34,21 +163,18 @@
.ws-tree-item .name.file:hover{color:var(--accent);}
.ws-tree-item .actions{display:none;gap:2px;}
.ws-tree-item:hover .actions{display:flex;}
/* Chevron in main tree */
.ws-chevron{width:16px;height:16px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:8px;padding:0;border-radius:3px;transition:transform 150ms,color 100ms;}
.ws-chevron:hover{color:var(--text);background:var(--bg-hover);}
.ws-chevron.open{transform:rotate(90deg);}
.ws-act{background:none;border:none;color:var(--text-tertiary);cursor:pointer;padding:3px 6px;border-radius:4px;font-size:11px;}
.ws-act:hover{background:var(--bg-hover);color:var(--text);}
.ws-act.danger:hover{color:var(--danger);}
.empty-state{text-align:center;padding:80px 20px;color:var(--text-tertiary);}
.empty-state .empty-icon{font-size:64px;margin-bottom:16px;opacity:.5;}
.empty-state h2{font-size:18px;color:var(--text-secondary);margin-bottom:8px;}
.empty-state p{font-size:13px;margin-bottom:24px;max-width:400px;margin-left:auto;margin-right:auto;}
.empty-actions{display:flex;gap:10px;justify-content:center;}
.empty-state{text-align:center;padding:80px 20px;color:var(--text-dim);}
.empty-state .empty-icon{font-size:48px;margin-bottom:12px;opacity:0.5;}
.empty-state h3{font-size:16px;color:var(--text-secondary);margin:0 0 4px;}
.empty-state p{font-size:13px;margin:0;}
.empty-actions{display:flex;gap:10px;justify-content:center;margin-top:16px;}
.btn{padding:10px 20px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;}
.btn-primary{background:var(--accent);color:#fff;}
@@ -270,7 +396,6 @@
.view-menu-item.active{background:rgba(35,131,226,.12);color:var(--accent);font-weight:500;}
/* Context menu */
.ctx-menu{position:fixed;background:var(--bg-modal);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-popover);min-width:200px;z-index:600;padding:4px 0;max-height:80vh;overflow-y:auto;}
.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);}
@@ -377,6 +502,8 @@ window._wsInitData = (function() {
sortBy: '',
viewMode: localStorage.getItem('fd_default_view') || 'tree',
viewMenuOpen: false,
sortOpen: false,
searchOpen: false,
viewIcons: {tree:'',list:'',details:'',title:'',content:''},
viewLabels: {tree:'Tree',list:'List',details:'Details',title:'Cards',content:'Content'},
@@ -750,7 +877,7 @@ window._wsInitData = (function() {
// ── Context menu ──
onContextMenu(ev) {
var el = ev.target.closest('[data-ws-id]');
var menu = document.querySelector('.ctx-menu');
var menu = document.querySelector('.more-menu');
if (!el || !menu) { if (menu) menu.style.display = 'none'; return; }
var id = parseInt(el.getAttribute('data-ws-id'));
if (!id) { menu.style.display = 'none'; return; }
@@ -791,7 +918,7 @@ window._wsInitData = (function() {
c.ctxAvailTags = this.ctxAvailTags;
c.ctxNewTagColor = this.ctxNewTagColor;
// Also force Alpine update on the menu element
var menuEl = document.querySelector('.ctx-menu');
var menuEl = document.querySelector('.more-menu');
if (menuEl && menuEl._x_dataStack && menuEl._x_dataStack[0]) {
var d = menuEl._x_dataStack[0];
d.ctxMenu = this.ctxMenu;
@@ -848,24 +975,24 @@ window._wsInitData = (function() {
ctxMenuOpenPage() {
if (this.ctxMenu.node) this.openPage(this.ctxMenu.node.id);
document.querySelector('.ctx-menu').style.display = 'none';
document.querySelector('.more-menu').style.display = 'none';
},
ctxMenuOpenFolder() {
if (this.ctxMenu.node) this.navigateToFolder(this.ctxMenu.node.id);
document.querySelector('.ctx-menu').style.display = 'none';
document.querySelector('.more-menu').style.display = 'none';
},
ctxMenuRename() {
if (this.ctxMenu.node) { this.startRename(this.ctxMenu.node); document.querySelector('.ctx-menu').style.display = 'none'; }
if (this.ctxMenu.node) { this.startRename(this.ctxMenu.node); document.querySelector('.more-menu').style.display = 'none'; }
},
ctxMenuDuplicate() {
if (this.ctxMenu.node) { this.duplicateItem(this.ctxMenu.node); document.querySelector('.ctx-menu').style.display = 'none'; }
if (this.ctxMenu.node) { this.duplicateItem(this.ctxMenu.node); document.querySelector('.more-menu').style.display = 'none'; }
},
ctxMenuDelete() {
if (this.ctxMenu.node) { this.deleteWithUndo(this.ctxMenu.node); document.querySelector('.ctx-menu').style.display = 'none'; }
if (this.ctxMenu.node) { this.deleteWithUndo(this.ctxMenu.node); document.querySelector('.more-menu').style.display = 'none'; }
},
ctxRemoveTag(tagId) {
@@ -881,7 +1008,7 @@ window._wsInitData = (function() {
window._ctxMenuData.ctxAvailTags = this.ctxAvailTags;
window._ctxMenuData.ctxMenu = this.ctxMenu;
// Also force Alpine update
var menuEl = document.querySelector('.ctx-menu');
var menuEl = document.querySelector('.more-menu');
if (menuEl && menuEl._x_dataStack && menuEl._x_dataStack[0]) {
menuEl._x_dataStack[0].ctxAvailTags = [...this.ctxAvailTags];
menuEl._x_dataStack[0].ctxMenu = this.ctxMenu;
@@ -914,13 +1041,13 @@ window._wsInitData = (function() {
window._ctxMenuData.ctxAvailTags = self.ctxAvailTags;
window._ctxMenuData.ctxMenu = self.ctxMenu;
// Also force Alpine update
var menuEl = document.querySelector('.ctx-menu');
var menuEl = document.querySelector('.more-menu');
if (menuEl && menuEl._x_dataStack && menuEl._x_dataStack[0]) {
menuEl._x_dataStack[0].ctxAvailTags = [...self.ctxAvailTags];
menuEl._x_dataStack[0].ctxMenu = self.ctxMenu;
}
// Close menu after successful add
document.querySelector('.ctx-menu').style.display = 'none';
document.querySelector('.more-menu').style.display = 'none';
}); }, 200);
}
this.ctxTagAdding = false;
@@ -2125,15 +2252,15 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<button class="undo-dismiss" @click="undoVisible=false">×</button>
</div>
<!-- Breadcrumb row -->
<!-- Breadcrumb + toolbar row (Library-style) -->
<div class="breadcrumb-row">
<button class="btn-icon nav-arrow" x-show="sidebarCollapsed" x-cloak @click="sidebarCollapsed = false" title="Show sidebar" style="margin-right:2px;">{{ fd_icon("chevron-right",16) }}</button>
<a href="/workspaces" class="btn-icon nav-arrow" title="All workspaces">{{ fd_icon("home",16) }}</a>
<button class="btn-icon nav-arrow" @click="goBack()" title="Back" :disabled="!canGoBack">{{ fd_icon("chevron-left",16) }}</button>
<button class="btn-icon nav-arrow" @click="goForward()" title="Forward" :disabled="!canGoForward">{{ fd_icon("chevron-right",16) }}</button>
<button class="btn-icon nav-arrow" x-show="sidebarCollapsed" x-cloak @click="sidebarCollapsed = false" title="Show sidebar">{{ fd_icon("chevron-right",14) }}</button>
<a href="/workspaces" class="btn-icon nav-arrow" title="All workspaces">{{ fd_icon("home",14) }}</a>
<button class="btn-icon nav-arrow" @click="goBack()" title="Back" :disabled="!canGoBack">{{ fd_icon("chevron-left",14) }}</button>
<button class="btn-icon nav-arrow" @click="goForward()" title="Forward" :disabled="!canGoForward">{{ fd_icon("chevron-right",14) }}</button>
{% if breadcrumbs %}
<div class="unified-breadcrumb">
<span class="bc-icon">{{ fd_icon("folder",14) }}</span>
<span class="bc-icon">{{ fd_icon("folder",13) }}</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;">
@@ -2148,53 +2275,64 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
@keydown.enter="navigateToPath(), editingPath=false"
@keydown.escape="editingPath=false"
placeholder="/workspace/...">
<button class="bc-edit-go" @click="navigateToPath(), editingPath=false">{{ fd_icon("corner-down-right",14) }}</button>
<button class="bc-edit-go" @click="navigateToPath(), editingPath=false">{{ fd_icon("corner-down-right",13) }}</button>
</div>
<button class="bc-edit-trigger" x-show="!editingPath" @click="startPathEdit()" title="Edit path">{{ fd_icon("edit",13) }}</button>
<button class="bc-edit-trigger" x-show="!editingPath" @click="startPathEdit()" title="Edit path">{{ fd_icon("edit",11) }}</button>
</div>
{% endif %}
</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="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">{{ fd_icon("folder",14) }} Tree</button>
<button class="view-menu-item" :class="{ active: viewMode==='list' }" @click="viewMode='list', previewItem=null">{{ fd_icon("list",14) }} List</button>
<button class="view-menu-item" :class="{ active: viewMode==='details' }" @click="viewMode='details', previewItem=null">{{ fd_icon("bar-chart",14) }} Details</button>
<button class="view-menu-item" :class="{ active: viewMode==='title' }" @click="viewMode='title', previewItem=null">{{ fd_icon("grid",14) }} Cards</button>
<button class="view-menu-item" :class="{ active: viewMode==='content' }" @click="viewMode='content', previewItem=null">{{ fd_icon("align-left",14) }} Content</button>
<div class="ws-toolbar" style="margin-left:auto;">
<!-- View dropdown -->
<div class="lib-toolbar-wrap" style="position:relative;">
<button class="ws-icon-btn" :class="{active: viewMenuOpen}" title="View" @click.stop="viewMenuOpen=!viewMenuOpen; sortOpen=false; searchOpen=false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg>
</button>
<div class="ws-dropdown" x-show="viewMenuOpen" @click.outside="viewMenuOpen=false" x-transition style="right:0;">
<div class="dd-label">View</div>
<div class="dd-item" :class="{active: viewMode==='tree'}" @click="viewMode='tree', previewItem=null, viewMenuOpen=false"><span class="check" x-text="viewMode==='tree'?'✓':''"></span>{{ fd_icon("folder",14) }} Tree</div>
<div class="dd-item" :class="{active: viewMode==='list'}" @click="viewMode='list', previewItem=null, viewMenuOpen=false"><span class="check" x-text="viewMode==='list'?'✓':''"></span>{{ fd_icon("list",14) }} List</div>
<div class="dd-item" :class="{active: viewMode==='details'}" @click="viewMode='details', previewItem=null, viewMenuOpen=false"><span class="check" x-text="viewMode==='details'?'✓':''"></span>{{ fd_icon("bar-chart",14) }} Details</div>
<div class="dd-item" :class="{active: viewMode==='title'}" @click="viewMode='title', previewItem=null, viewMenuOpen=false"><span class="check" x-text="viewMode==='title'?'✓':''"></span>{{ fd_icon("grid",14) }} Cards</div>
<div class="dd-item" :class="{active: viewMode==='content'}" @click="viewMode='content', previewItem=null, viewMenuOpen=false"><span class="check" x-text="viewMode==='content'?'✓':''"></span>{{ fd_icon("align-left",14) }} Content</div>
</div>
</div>
<!-- Sort -->
<div class="lib-toolbar-wrap" style="position:relative;">
<button class="ws-icon-btn" :class="{active: sortOpen}" title="Sort" @click.stop="sortOpen=!sortOpen; viewMenuOpen=false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="9" x2="14" y2="9"/><line x1="4" y1="15" x2="10" y2="15"/><polyline points="17 5 17 19"/><polyline points="13 16 17 20 21 16"/></svg>
</button>
<div class="ws-dropdown" x-show="sortOpen" @click.outside="sortOpen=false" x-transition style="right:0;">
<div class="dd-label">Sort by</div>
<div class="dd-item" :class="{active: sortBy==='name-asc'}" @click="sortBy='name-asc', doSort(), sortOpen=false"><span class="check" x-text="sortBy==='name-asc'?'✓':''"></span> Name A→Z</div>
<div class="dd-item" :class="{active: sortBy==='name-desc'}" @click="sortBy='name-desc', doSort(), sortOpen=false"><span class="check" x-text="sortBy==='name-desc'?'✓':''"></span> Name Z→A</div>
<div class="dd-item" :class="{active: sortBy==='type'}" @click="sortBy='type', doSort(), sortOpen=false"><span class="check" x-text="sortBy==='type'?'✓':''"></span> Type</div>
<div class="dd-item" :class="{active: sortBy==='date-asc'}" @click="sortBy='date-asc', doSort(), sortOpen=false"><span class="check" x-text="sortBy==='date-asc'?'✓':''"></span> Oldest</div>
<div class="dd-item" :class="{active: sortBy==='date-desc'}" @click="sortBy='date-desc', doSort(), sortOpen=false"><span class="check" x-text="sortBy==='date-desc'?'✓':''"></span> Newest</div>
</div>
</div>
<!-- Search toggle -->
<button class="ws-icon-btn" :class="{active: searchOpen}" title="Search" @click.stop="searchOpen=!searchOpen; if(searchOpen) $nextTick(()=>$refs.searchInput?.focus())">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</button>
<!-- Expand/Collapse (tree only) -->
<button class="ws-icon-btn" @click="expandAll()" title="Expand all" x-show="viewMode==='tree'"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></button>
<button class="ws-icon-btn" @click="collapseAll()" title="Collapse all" x-show="viewMode==='tree'"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 15 12 9 18 15"/></svg></button>
<!-- Create buttons -->
<button class="ws-btn-primary" onclick="window.FlowDeck.createPage()" title="New page">{{ fd_icon("file",14) }} New</button>
</div>
<div class="search-bar-inline">
<span class="search-icon" x-html="getSvgIcon('search',16)"></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" onclick="window.FlowDeck.createPage()" title="New file" x-html="getSvgIcon('file',16)"></button>
<button class="btn-icon btn-create" onclick="window.FlowDeck.showCreateFolderModal()" title="New folder" x-html="getSvgIcon('folder',16)"></button>
</div>
<!-- Search bar (togglable) -->
<div class="ws-search-bar" x-show="searchOpen" x-transition>
<input x-ref="searchInput" type="text" placeholder="Filter files and folders..." x-model="searchQuery" @input="doFilter" autocomplete="off">
</div>
<!-- Bulk actions bar -->
<div class="bulk-bar" x-show="selectedIdsCount > 0" x-transition>
<span x-text="selectedIdsCount + ' selected'"></span>
<button class="btn btn-danger" @click="bulkDelete()">Delete</button>
<button class="btn btn-secondary" @click="clearSelection()">{{ fd_icon("x",14) }} Clear</button>
<div class="ws-select-bar" x-show="selectedIdsCount > 0" x-transition>
<span class="sel-count" x-text="selectedIdsCount + ' selected'"></span>
<button @click="clearSelection()" title="Clear"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
<button @click="bulkDelete()" style="margin-left:auto;color:var(--danger);" title="Delete">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6M14 11v6"/></svg>
Delete
</button>
</div>
<!-- Tree -->
@@ -2571,45 +2709,42 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
</div><!-- /ws-main -->
<!-- Context menu -->
<div class="ctx-menu" x-data="_ctxMenuData" style="display:none"
@click.outside="document.querySelector('.ctx-menu').style.display='none'"
@keydown.escape="document.querySelector('.ctx-menu').style.display='none'"
<!-- Context menu (Library-style) -->
<div class="more-menu" x-data="_ctxMenuData" style="display:none"
@click.outside="document.querySelector('.more-menu').style.display='none'"
@keydown.escape="document.querySelector('.more-menu').style.display='none'"
x-transition>
<div class="ctx-item" @click="ctxMenuOpenPage()">{{ fd_icon("file",14) }} Open</div>
<div class="ctx-item" x-show="ctxMenu.node && ctxMenu.node.is_folder" @click="ctxMenuOpenFolder()">{{ fd_icon("folder",14) }} Open folder</div>
<div class="ctx-item" @click="ctxMenuRename()">{{ fd_icon("edit",14) }} Rename</div>
<div class="ctx-item" @click="ctxMenuDuplicate()">{{ fd_icon("copy",14) }} Duplicate</div>
<div class="ctx-sep"></div>
<div class="ctx-section">{{ fd_icon("tag",14) }} Tags</div>
<!-- Current tags with remove button -->
<div class="menu-item" @click="ctxMenuOpenPage()">{{ fd_icon("file",14) }} Open</div>
<div class="menu-item" x-show="ctxMenu.node && ctxMenu.node.is_folder" @click="ctxMenuOpenFolder()">{{ fd_icon("folder",14) }} Open folder</div>
<div class="menu-item" @click="ctxMenuRename()">{{ fd_icon("edit",14) }} Rename</div>
<div class="menu-item" @click="ctxMenuDuplicate()">{{ fd_icon("copy",14) }} Duplicate</div>
<div class="more-sep"></div>
<!-- Current tags -->
<template x-for="t in (ctxMenu.tags||[])" :key="'ctx-tag'+t.id">
<div class="ctx-item ctx-tag-item">
<span class="ctx-tag-color" :style="{background: t.color}"></span>
<span style="flex:1;font-size:13px;" x-text="t.name"></span>
<div class="menu-item" style="justify-content:space-between;">
<span style="display:flex;align-items:center;gap:8px;"><span class="ctx-tag-color" :style="{background: t.color}"></span><span style="font-size:13px;" x-text="t.name"></span></span>
<button class="tag-chip rm-btn" @click.stop="ctxRemoveTag(t.id)" title="Remove">×</button>
</div>
</template>
<div x-show="(ctxMenu.tags||[]).length === 0" style="padding:2px 14px;font-size:11px;color:var(--text-dim);">No tags</div>
<div class="ctx-sep"></div>
<!-- Add existing tag → sub-menu -->
<div class="ctx-item ctx-has-sub" @click.stop="ctxTagExistingOpen=!ctxTagExistingOpen" x-show="ctxAvailTags.length > 0">
<div x-show="(ctxMenu.tags||[]).length === 0" style="padding:4px 10px;font-size:11px;color:var(--text-dim);">No tags</div>
<div class="more-sep"></div>
<!-- Add existing tag -->
<div class="menu-item" @click.stop="ctxTagExistingOpen=!ctxTagExistingOpen" x-show="ctxAvailTags.length > 0">
<span style="flex:1;">Add existing tag</span>
<span x-text="ctxTagExistingOpen ? '▾' : '▸'"></span>
<span x-text="ctxTagExistingOpen ? '▾' : '▸'" style="font-size:11px;"></span>
</div>
<div x-show="ctxTagExistingOpen && ctxAvailTags.length > 0" style="padding:0 0 4px;background:var(--bg-tertiary);border-radius:6px;margin:2px 6px;">
<template x-for="t in ctxAvailTags" :key="'ctx-avail'+t.id">
<div class="ctx-item ctx-add-tag-item" @click.stop="ctxAddExistingTag(t), ctxTagExistingOpen=false">
<span class="ctx-tag-color" :style="{background: t.color}"></span>
<span style="flex:1;font-size:13px;" x-text="t.name"></span>
<div class="menu-item" @click.stop="ctxAddExistingTag(t), ctxTagExistingOpen=false" style="justify-content:space-between;">
<span style="display:flex;align-items:center;gap:8px;"><span class="ctx-tag-color" :style="{background: t.color}"></span><span style="font-size:13px;" x-text="t.name"></span></span>
<span style="font-size:10px;color:var(--text-dim);" x-text="t.count||''"></span>
</div>
</template>
</div>
<!-- Create new tag -->
<div class="ctx-item ctx-has-sub" @click.stop="ctxTagAdding=!ctxTagAdding">
<div class="menu-item" @click.stop="ctxTagAdding=!ctxTagAdding">
<span style="flex:1;">{{ fd_icon("plus",14) }} New tag</span>
<span x-text="ctxTagAdding ? '▾' : '▸'"></span>
<span x-text="ctxTagAdding ? '▾' : '▸'" style="font-size:11px;"></span>
</div>
<div x-show="ctxTagAdding" style="padding:6px 10px;background:var(--bg-tertiary);border-radius:6px;margin:2px 6px;">
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:4px;margin-bottom:6px;">
@@ -2622,14 +2757,12 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
<input class="tag-input" x-ref="ctxTagInput"
@keydown.enter="ctxAddNewTag($event.target.value), $event.target.value=''"
@keydown.escape="ctxTagAdding=false"
@click.stop
placeholder="Tag name..."
style="flex:1;">
@click.stop placeholder="Tag name..." style="flex:1;">
<button class="btn btn-primary" @click.stop="ctxAddNewTag($refs.ctxTagInput.value), $refs.ctxTagInput.value=''" style="padding:2px 10px;font-size:12px;">Add</button>
</div>
</div>
<div class="ctx-sep"></div>
<div class="ctx-item ctx-danger" @click="ctxMenuDelete()">{{ fd_icon("trash",14) }} Delete</div>
<div class="more-sep"></div>
<div class="menu-item menu-danger" @click="ctxMenuDelete()">{{ fd_icon("trash",14) }} Delete</div>
</div>
<!-- Preview tooltip -->