Added — Settings → Automations, pipeline visuel (API steps v7.0) :
- Cartes ordinees (trigger/condition/delay/action) : resume + edition
TYPÉE par kind/type (datalist evenements, 7 ops, 8 types d'action avec
leurs champs reels), ajout/edition/suppression/haut-bas via
POST/PUT/DELETE /workspace/automations[/steps]/...
- ✨ Convertir le JSON en pipeline (legacy → steps ordonnes) ; textareas
JSON masques des qu'un step existe.
- Gate E2E « editeur visuel de steps » : creation → edition → ajout →
carte « Action · webhook » sous CSP reel.
Fixed (trouve par le gate) — 51 expressions Alpine MULTI-INSTRUCTIONS
(`a=1; b()`) = interdites par le parseur CSP (une seule expression par
directive ; ';' = token inattendu) — INVISIBLE pour le scan par tokens :
- Conversion en methodes dans 11 fichiers : nav settings x8 (navTo),
menu section base x7 (closeAndSetCount/Move/...), parts+editeur x13
(setSharePerm, more*, markAndSave...), breadcrumb x5 (hover*/goClose),
library/local x6 (menus popup), board x3 (pickStatus/...), ctx-menu x2
(addTagAndClear), agent/card/gitea/workspaces x6.
- Scanner dedie scan_semi (inventaire ';' hors chaines) ajoute au lot.
Verifs : 39 templates Jinja parse OK · scan expressions = 0 incompatible
(4 faux positifs en chaines) · **E2E 8/8** · suite **1094/1094** ·
ruff OK · docs a jour
1118 lines
78 KiB
HTML
1118 lines
78 KiB
HTML
{% extends "base.html" %}
|
||
{% block page_title %}{{ workspace_name }}{% endblock %}
|
||
{% block title_prefix %}{{ workspace_name }}{% endblock %}
|
||
{% block page_icon %}{{ fd_icon("folder",18) }}{% endblock %}
|
||
|
||
{% block topbar %}
|
||
{% set page_icon = "folder" %}
|
||
{% set page_title = workspace_name %}
|
||
{% include '_header.html' %}
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<span id="fd-local-ws-id" hidden>{{ workspace_id }}</span>
|
||
<div x-data="wsInitData()"
|
||
@dragover.prevent="onDragOver($event)"
|
||
@dragleave="onDragLeave($event)"
|
||
@drop.prevent="onDrop($event)"
|
||
@keydown="onKeydown($event)">
|
||
<style>
|
||
/* ── 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;}
|
||
|
||
/* ── Multi-select "…" dropdown (Library-style) ── */
|
||
.ws-select-menu{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:300;padding:6px;}
|
||
.ws-select-menu .menu-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-select-menu .menu-item:hover{background:var(--bg-hover);}
|
||
.ws-select-menu .menu-item svg{width:14px;height:14px;flex-shrink:0;}
|
||
.ws-select-menu .menu-item .shortcut{margin-left:auto;font-size:11px;color:var(--text-dim);}
|
||
.ws-select-menu .menu-danger{color:var(--danger);}
|
||
.ws-select-menu .more-sep{height:1px;background:var(--border);margin:4px 8px;}
|
||
|
||
/* ── Move-to modal list items ── */
|
||
.move-modal-item{padding:7px 10px;font-size:13px;color:var(--text-primary);cursor:pointer;border-radius:6px;display:flex;align-items:center;gap:8px;}
|
||
.move-modal-item:hover{background:var(--bg-hover);}
|
||
|
||
/* ── 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;}
|
||
.ws-tree-item:hover .hover-only,.ws-tree-item.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:6px;}
|
||
.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);}
|
||
.ws-tree-item .icon{font-size:16px;width:20px;text-align:center;flex-shrink:0;}
|
||
.ws-tree-item .name{flex:1;font-size:14px;color:var(--text);}
|
||
.ws-tree-item .name.folder{font-weight:500;cursor:pointer;}
|
||
.ws-tree-item .name.folder:hover{color:var(--accent);}
|
||
.ws-tree-item .name.file{cursor:pointer;}
|
||
.ws-tree-item .name.file:hover{color:var(--accent);}
|
||
.ws-tree-item .name.file{display:inline-flex;align-items:center;max-width:100%;overflow:hidden;}
|
||
.ws-tree-item .name.file .inline-rename-input{flex:0 1 auto;}
|
||
.ws-tree-item .name.file .btn-open{flex:0 0 auto;}
|
||
.ws-shared-badge{flex-shrink:0;margin-right:4px;font-size:13px;line-height:1;opacity:.9;filter:grayscale(1) brightness(1.6);}
|
||
.ws-tree-item .actions{display:none;gap:2px;}
|
||
.ws-tree-item:hover .actions{display:flex;}
|
||
.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-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;}
|
||
.btn-primary:hover{background:var(--accent-hover);}
|
||
.btn-secondary{background:var(--bg-tertiary);color:var(--text);border:1px solid var(--border);}
|
||
.btn-secondary:hover{background:var(--bg-hover);}
|
||
.btn-danger{background:rgba(255,80,80,.1);color:var(--danger);}
|
||
.btn-danger:hover{background:rgba(255,80,80,.2);}
|
||
|
||
.btn-icon{width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:16px;color:var(--text-secondary);transition:all 150ms;}
|
||
.btn-icon:hover{background:var(--bg-hover);color:var(--text);border-color:var(--border-strong);}
|
||
.btn-icon:disabled{opacity:0.3;cursor:default;}
|
||
.btn-icon:disabled:hover{background:var(--bg-tertiary);color:var(--text-secondary);}
|
||
|
||
/* Recently modified dot */
|
||
.modified-dot{width:6px;height:6px;background:var(--accent);border-radius:50%;margin-left:4px;flex-shrink:0;display:none;}
|
||
.modified-dot.show{display:block;}
|
||
|
||
/* Offline banner */
|
||
.offline-banner{position:fixed;top:0;left:0;right:0;background:var(--danger);color:#fff;text-align:center;padding:6px 12px;font-size:12px;z-index:500;font-weight:500;display:flex;align-items:center;justify-content:center;gap:12px;}
|
||
.offline-badge{background:rgba(255,255,255,.22);border-radius:10px;padding:1px 8px;font-size:11px;font-weight:600;}
|
||
|
||
/* Pinned indicator */
|
||
.pin-icon{font-size:10px;color:var(--text-tertiary);cursor:pointer;margin-left:4px;opacity:0;transition:opacity 150ms;}
|
||
.ws-tree-item:hover .pin-icon{opacity:1;}
|
||
.pin-icon.pinned{opacity:1;color:var(--accent);}
|
||
|
||
/* "X more" toggle in tree view */
|
||
.ws-more-item{cursor:pointer;opacity:.7;transition:opacity 150ms;}
|
||
.ws-more-item:hover{opacity:1;background:var(--bg-tertiary);}
|
||
|
||
/* Multi-select */
|
||
.item-checkbox{width:15px;height:15px;flex-shrink:0;cursor:pointer;accent-color:var(--accent);margin:0 4px;}
|
||
.ws-tree-item.selected{background:rgba(35,131,226,.1);}
|
||
|
||
/* Bulk bar */
|
||
.bulk-bar{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--accent);color:#fff;border-radius:8px;margin-bottom:10px;font-size:13px;font-weight:500;}
|
||
.bulk-bar .btn-danger{background:rgba(255,255,255,.15);color:#fff;border:1px solid rgba(255,255,255,.3);}
|
||
.bulk-bar .btn-secondary{background:rgba(255,255,255,.1);color:#fff;border:1px solid rgba(255,255,255,.2);}
|
||
[data-theme="light"] .bulk-bar .btn-danger{background:rgba(0,0,0,.08);color:var(--text-primary);border:1px solid var(--border);}
|
||
[data-theme="light"] .bulk-bar .btn-secondary{background:rgba(0,0,0,.05);color:var(--text-primary);border:1px solid var(--border);}
|
||
|
||
/* Table view */
|
||
.ws-table{width:100%;border-collapse:collapse;font-size:13px;}
|
||
.ws-table th{text-align:left;padding:6px 12px;color:var(--text-tertiary);font-weight:500;border-bottom:1px solid var(--border);font-size:11px;text-transform:uppercase;letter-spacing:.5px;}
|
||
.ws-table td{padding:8px 12px;border-bottom:1px solid var(--border);}
|
||
.ws-table tr:hover td{background:var(--bg-hover);}
|
||
.ws-table .table-icon{font-size:16px;width:24px;}
|
||
.ws-table .table-name{font-weight:500;cursor:pointer;}
|
||
.ws-table .table-name:hover{color:var(--accent);}
|
||
.ws-table .table-meta{color:var(--text-tertiary);font-size:12px;}
|
||
|
||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-items:center;justify-content:center;z-index:999;backdrop-filter:blur(4px);}
|
||
.modal-box{background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-lg);padding:28px;width:420px;max-width:90vw;box-shadow:var(--shadow-modal);}
|
||
.modal-box h3{font-size:16px;margin-bottom:6px;display:flex;align-items:center;gap:8px;}
|
||
.modal-box .modal-sub{font-size:12px;color:var(--text-tertiary);margin-bottom:20px;}
|
||
.modal-input{width:100%;padding:12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:10px;color:var(--text);font-size:14px;outline:none;}
|
||
.modal-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(35,131,226,.15);}
|
||
.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:20px;}
|
||
.delete-confirm{padding:12px;background:rgba(255,80,80,.08);border-radius:8px;margin-top:12px;font-size:13px;color:var(--text-secondary);}
|
||
|
||
/* Breadcrumb in page */
|
||
.breadcrumb-bar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:20px;font-size:14px;color:var(--text-secondary);}
|
||
.breadcrumb-bar a{color:var(--text-secondary);text-decoration:none;padding:2px 6px;border-radius:4px;}
|
||
.breadcrumb-bar a:hover{color:var(--text);background:var(--bg-tertiary);}
|
||
.breadcrumb-bar .sep{color:var(--text-tertiary);}
|
||
.breadcrumb-bar .current{color:var(--text);font-weight:500;}
|
||
|
||
/* Drag & drop */
|
||
.ws-layout.drop-active { outline: 2px dashed var(--accent); outline-offset: -4px; border-radius: 8px; }
|
||
/* Drag handle — 6 dots */
|
||
.drag-handle{display:flex;flex-wrap:wrap;width:16px;height:16px;cursor:grab;align-content:space-between;justify-content:space-between;flex-shrink:0;opacity:0.45;margin:0 4px;}
|
||
.ws-tree-item:hover .drag-handle{opacity:1;}
|
||
.drag-handle .dot{width:3px;height:3px;background:var(--text-tertiary);border-radius:50%;}
|
||
.drop-overlay{display:none;position:fixed;inset:0;background:rgba(35,131,226,.08);z-index:100;pointer-events:none;align-items:center;justify-content:center;}
|
||
.drop-overlay.active{display:flex;}
|
||
.drop-overlay .drop-hint{background:var(--bg);border:2px dashed var(--accent);border-radius:16px;padding:40px 60px;text-align:center;color:var(--text);font-size:16px;}
|
||
.drop-overlay .drop-hint .icon{font-size:48px;display:block;margin-bottom:12px;}
|
||
.drop-target-highlight{outline:2px solid var(--accent)!important;outline-offset:-2px;border-radius:6px;background:rgba(35,131,226,.1)!important;}
|
||
.upload-progress{position:fixed;bottom:24px;right:24px;background:var(--bg-modal);border:1px solid var(--border);border-radius:12px;padding:16px 20px;z-index:200;box-shadow:var(--shadow-popover);min-width:200px;}
|
||
.upload-progress .up-bar{height:4px;background:var(--bg-tertiary);border-radius:2px;margin-top:8px;overflow:hidden;}
|
||
.upload-progress .up-bar-fill{height:100%;background:var(--accent);border-radius:2px;transition:width 200ms;}
|
||
|
||
/* Search bar */
|
||
.search-bar{display:flex;align-items:center;gap:8px;margin-bottom:12px;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;}
|
||
.search-bar:focus-within{border-color:var(--accent);}
|
||
.search-icon{font-size:14px;color:var(--text-tertiary);flex-shrink:0;}
|
||
.search-input{flex:1;background:none;border:none;color:var(--text);font-size:13px;outline:none;}
|
||
.search-input::placeholder{color:var(--text-tertiary);}
|
||
.search-clear{background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:16px;padding:2px 4px;border-radius:3px;}
|
||
.search-clear:hover{color:var(--text);background:var(--bg-hover);}
|
||
|
||
/* Filter chips */
|
||
.filter-chips{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;}
|
||
.filter-chip{padding:4px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--text-secondary);cursor:pointer;transition:all 150ms;}
|
||
.filter-chip:hover{background:var(--bg-hover);color:var(--text);}
|
||
.filter-chip.active{background:var(--accent);color:#fff;border-color:var(--accent);}
|
||
|
||
/* Child count badge */
|
||
.child-count{font-size:11px;color:var(--text-tertiary);margin-left:4px;}
|
||
|
||
/* Inline rename */
|
||
.inline-rename-input{background:var(--bg);border:1px solid var(--accent);border-radius:4px;color:var(--text);font-size:13px;padding:2px 6px;outline:none;min-width:60px;}
|
||
|
||
/* Drag count badge */
|
||
.drag-badge{position:fixed;pointer-events:none;z-index:999;background:var(--accent);color:#fff;font-size:12px;font-weight:700;padding:2px 8px;border-radius:10px;transform:translate(-50%,-50%);}
|
||
|
||
/* Drop indicator line */
|
||
.drop-indicator{position:absolute;left:0;right:0;height:2px;background:var(--accent);border-radius:1px;pointer-events:none;z-index:10;}
|
||
|
||
/* Sort select */
|
||
.sort-select{background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);font-size:12px;padding:4px 8px;border-radius:6px;cursor:pointer;outline:none;}
|
||
.sort-select:focus{border-color:var(--accent);}
|
||
|
||
/* Undo toast */
|
||
.undo-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--bg-modal);border:1px solid var(--border);border-radius:12px;padding:10px 20px;z-index:300;box-shadow:var(--shadow-popover);display:flex;align-items:center;gap:12px;font-size:13px;}
|
||
.undo-btn{background:var(--accent);color:#fff;border:none;padding:4px 12px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;}
|
||
.undo-btn:hover{opacity:.85;}
|
||
.undo-dismiss{background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:18px;padding:0 4px;}
|
||
|
||
/* Smooth expand animation */
|
||
.ws-tree[x-show] { transition: max-height 0.25s ease, opacity 0.2s ease; overflow:hidden; }
|
||
|
||
/* Path bar */
|
||
.path-bar{display:flex;align-items:center;gap:6px;margin-bottom:12px;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;}
|
||
.path-bar:focus-within{border-color:var(--accent);}
|
||
.path-icon{font-size:14px;flex-shrink:0;}
|
||
.path-input{flex:1;background:none;border:none;color:var(--text);font-size:13px;outline:none;font-family:'Fira Code',monospace;}
|
||
.path-input::placeholder{color:var(--text-tertiary);}
|
||
.path-go{background:var(--accent);color:#fff;border:none;padding:3px 8px;border-radius:4px;cursor:pointer;font-size:12px;}
|
||
.path-go:hover{opacity:.85;}
|
||
|
||
/* File preview tooltip */
|
||
.file-preview{position:fixed;z-index:300;background:var(--bg-modal);border:1px solid var(--border);border-radius:10px;box-shadow:var(--shadow-popover);max-width:400px;min-width:200px;pointer-events:auto;}
|
||
.file-preview-header{padding:8px 12px;font-size:12px;font-weight:600;color:var(--text-secondary);border-bottom:1px solid var(--border);}
|
||
.file-preview-body{padding:10px 12px;margin:0;font-size:12px;line-height:1.5;color:var(--text);max-height:200px;overflow:auto;font-family:'Fira Code','Cascadia Code',monospace;white-space:pre-wrap;word-wrap:break-word;background:var(--bg-secondary);}
|
||
|
||
/* Tag chips */
|
||
.tag-chip{display:inline-flex;align-items:center;gap:2px;padding:1px 8px;border-radius:10px;font-size:11px;color:#fff;white-space:nowrap;margin:1px 2px;transition:all 150ms;}
|
||
.tag-chip:hover{filter:brightness(1.15);}
|
||
.tag-chip.rm-btn{background:none !important;border:none;color:var(--text-tertiary);cursor:pointer;font-size:13px;line-height:1;padding:0 2px;border-radius:50%;margin-left:2px;}
|
||
.tag-chip.rm-btn:hover{color:var(--danger);background:rgba(255,80,80,.15) !important;}
|
||
.tag-chip.active{box-shadow:0 0 0 2px var(--accent);}
|
||
.tag-add-btn{display:inline-flex;align-items:center;gap:3px;padding:1px 7px;background:none;border:1px dashed var(--border);border-radius:10px;font-size:11px;color:var(--text-tertiary);cursor:pointer;white-space:nowrap;transition:all 150ms;}
|
||
.tag-add-btn:hover{border-color:var(--accent);color:var(--accent);}
|
||
.tag-input{background:var(--bg);border:1px solid var(--accent);border-radius:8px;color:var(--text);font-size:11px;padding:2px 6px;outline:none;min-width:80px;}
|
||
.tag-filter-bar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:8px;padding:4px 0;}
|
||
|
||
/* Enhanced table */
|
||
.ws-table-enhanced{width:100%;border-collapse:collapse;font-size:13px;margin-top:4px;}
|
||
.ws-table-enhanced th{text-align:left;padding:10px 12px;color:var(--text-tertiary);font-weight:500;border-bottom:2px solid var(--border);font-size:11px;text-transform:uppercase;letter-spacing:.5px;position:sticky;top:0;background:var(--bg);z-index:1;}
|
||
.ws-table-enhanced td{padding:8px 12px;border-bottom:1px solid var(--border);vertical-align:middle;}
|
||
.ws-table-enhanced tr:hover td{background:var(--bg-hover);}
|
||
.ws-table-enhanced .col-name{font-weight:500;cursor:pointer;min-width:150px;}
|
||
.ws-table-enhanced .col-name:hover{color:var(--accent);}
|
||
.ws-table-enhanced .col-path{color:var(--text-tertiary);font-size:12px;font-family:'Fira Code',monospace;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||
.ws-table-enhanced .col-size{color:var(--text-secondary);font-size:12px;white-space:nowrap;}
|
||
.ws-table-enhanced .col-date{color:var(--text-tertiary);font-size:12px;white-space:nowrap;}
|
||
.ws-table-enhanced .col-type{font-size:12px;white-space:nowrap;}
|
||
.ws-table-enhanced .col-author{font-size:12px;color:var(--text-secondary);white-space:nowrap;}
|
||
.ws-table-enhanced .col-tags{min-width:120px;}
|
||
.ws-table-enhanced .table-icon{font-size:16px;margin-right:6px;vertical-align:-2px;}
|
||
|
||
/* Table compact mode (inside filter/search) */
|
||
.ws-table-compact{width:100%;border-collapse:collapse;font-size:13px;}
|
||
.ws-table-compact th{text-align:left;padding:6px 10px;color:var(--text-tertiary);font-weight:500;border-bottom:2px solid var(--border);font-size:11px;text-transform:uppercase;}
|
||
.ws-table-compact td{padding:5px 10px;border-bottom:1px solid var(--border);}
|
||
.ws-table-compact tr:hover td{background:var(--bg-hover);}
|
||
|
||
/* Split layout */
|
||
.ws-split{display:flex;max-width:100%;margin:0 auto;padding:0 16px 0;gap:0;min-height:calc(100vh - var(--topbar-height));}
|
||
.ws-main{flex:1;min-width:0;overflow:auto;padding-right:0;}
|
||
.preview-panel + .ws-main{margin-right:0;}
|
||
@media(max-width:768px){.ws-split{flex-direction:column;padding:20px 12px 0;}.preview-panel{width:100%;max-width:100%;min-width:100%;}.ws-main{width:100%;}}
|
||
|
||
/* Breadcrumb row */
|
||
.breadcrumb-row{display:flex;align-items:center;gap:4px;padding:2px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px 8px 0 0;margin-bottom:0;flex-wrap:wrap;}
|
||
.breadcrumb-row .nav-arrow{width:22px;height:22px;padding:0;font-size:13px;}
|
||
.toolbar-row{display:flex;align-items:center;gap:4px;padding:2px 8px;background:var(--bg-secondary);border:1px solid var(--border);border-top:none;border-radius:0 0 8px 8px;margin-bottom:2px;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;} /* 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;}
|
||
.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 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-modal);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-popover);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-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-section{padding:5px 14px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;}
|
||
.ctx-tag-color{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
|
||
.color-swatch-ctx{width:100%;padding-bottom:100%;border-radius:5px;cursor:pointer;border:2px solid transparent;transition:all 100ms;box-sizing:border-box;position:relative;}
|
||
.color-swatch-ctx:hover{transform:scale(1.15);z-index:1;}
|
||
.color-swatch-ctx.selected{border-color:var(--text);box-shadow:0 0 0 2px var(--accent);}
|
||
.ctx-add-tag-item:hover{background:rgba(35,131,226,.08);}
|
||
.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;}
|
||
|
||
/* Title grid */
|
||
.title-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px;margin-top:8px;}
|
||
.title-card{display:flex;flex-direction:column;align-items:center;padding:16px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all 150ms;min-height:100px;justify-content:center;}
|
||
.title-card:hover{background:var(--bg-hover);border-color:var(--accent);}
|
||
.title-card.selected-card{background:rgba(35,131,226,.1);border-color:var(--accent);}
|
||
.title-card-icon{font-size:28px;margin-bottom:8px;}
|
||
.title-card-name{font-size:13px;font-weight:500;text-align:center;word-break:break-word;max-width:100%;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;}
|
||
.title-card-meta{display:flex;gap:8px;margin-top:6px;font-size:11px;color:var(--text-tertiary);}
|
||
|
||
/* Content list view */
|
||
.content-list{display:flex;flex-direction:column;gap:4px;margin-top:8px;}
|
||
.content-item{padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;cursor:pointer;transition:all 150ms;}
|
||
.content-item:hover{background:var(--bg-hover);border-color:var(--accent);}
|
||
.content-item.selected-row{background:rgba(35,131,226,.08);border-color:var(--accent);}
|
||
.content-item-header{display:flex;align-items:center;gap:8px;font-size:14px;}
|
||
.content-item-preview{font-size:12px;color:var(--text-secondary);margin-top:6px;padding-left:24px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:600px;}
|
||
.content-item-tags{display:flex;gap:4px;margin-top:6px;padding-left:24px;}
|
||
|
||
/* Selected row highlight */
|
||
.selected-row{background:rgba(35,131,226,.08)!important;}
|
||
|
||
/* Preview panel — side panel in split layout */
|
||
.preview-panel{width:380px;min-width:300px;max-width:45vw;background:var(--bg-secondary);border-left:1px solid var(--border-strong);display:flex;flex-direction:column;overflow:hidden;flex-shrink:0;}
|
||
.preview-panel-header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--border);flex-shrink:0;}
|
||
.preview-panel-title{font-size:15px;font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||
.preview-close{background:none;border:none;color:var(--text-tertiary);font-size:20px;cursor:pointer;padding:2px 6px;border-radius:4px;line-height:1;}
|
||
.preview-close:hover{color:var(--text);background:var(--bg-hover);}
|
||
.preview-panel-meta{display:flex;gap:12px;padding:8px 16px;font-size:12px;color:var(--text-tertiary);border-bottom:1px solid var(--border);flex-shrink:0;}
|
||
.preview-panel-tags{display:flex;gap:4px;flex-wrap:wrap;padding:8px 16px;border-bottom:1px solid var(--border);flex-shrink:0;}
|
||
.preview-panel-body{flex:1;overflow:auto;padding:16px;font-size:13px;line-height:1.6;color:var(--text);}
|
||
.preview-panel-body pre{background:var(--bg-tertiary);border-radius:8px;padding:12px;overflow:auto;font-size:12px;font-family:'Fira Code',monospace;white-space:pre-wrap;word-wrap:break-word;}
|
||
.preview-panel-body img{max-width:100%;border-radius:8px;}
|
||
.preview-panel-actions{display:flex;gap:8px;padding:12px 16px;border-top:1px solid var(--border);flex-shrink:0;}
|
||
|
||
/* Transitions */
|
||
.preview-enter{transition:transform .2s ease,opacity .2s ease;}
|
||
.preview-enter-start{transform:translateX(100%);opacity:0;}
|
||
.preview-enter-end{transform:translateX(0);opacity:1;}
|
||
.preview-leave{transition:transform .15s ease,opacity .15s ease;}
|
||
.preview-leave-start{transform:translateX(0);opacity:1;}
|
||
.preview-leave-end{transform:translateX(100%);opacity:0;}
|
||
</style>
|
||
|
||
<script type="application/json" id="lw-config" nonce="{{ csp_nonce() }}">{{ {"current_folder_id": current_folder_id, "workspace_id": workspace_id} | tojson }}</script>
|
||
<script data-cfasync="false" src="/static/js/local_workspace.js?v={{ asset_version }}"></script>
|
||
<script data-cfasync="false" nonce="{{ csp_nonce() }}">console.log('FLOWDECK v2.4.6 — ws-split about to render, _wsInitData exists:', typeof _wsInitData !== 'undefined');</script>
|
||
|
||
<div class="ws-split"
|
||
@contextmenu.prevent="onContextMenu($event)"
|
||
@touchstart="onTouchStart($event)"
|
||
@touchend="onTouchEnd($event)"
|
||
@touchmove="onTouchMove($event)"
|
||
tabindex="0"
|
||
x-ref="layout">
|
||
<div class="ws-main">
|
||
|
||
<!-- Drop overlay -->
|
||
<div class="drop-overlay" :class="{ active: isDragging }">
|
||
<div class="drop-hint">
|
||
{{ fd_icon("download",16) }}
|
||
<div>Drop files here</div>
|
||
<div style="font-size:12px;color:var(--text-tertiary);margin-top:6px;" x-show="dropTarget">
|
||
Into: <strong x-text="dropTargetName"></strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Upload progress -->
|
||
<div class="upload-progress" x-show="uploading" x-transition>
|
||
<div style="display:flex;align-items:center;gap:8px;">
|
||
<span x-text="uploading ? '⏳' : ''"></span>
|
||
<span x-text="uploadStatus"></span>
|
||
<span style="font-size:11px;color:var(--text-tertiary);" x-show="uploadSpeed" x-text="uploadSpeed"></span>
|
||
</div>
|
||
<div class="up-bar">
|
||
<div class="up-bar-fill" :style="{ width: uploadProgress + '%' }"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Undo toast -->
|
||
<div class="undo-toast" x-show="undoVisible" x-transition>
|
||
<span x-text="undoMessage"></span>
|
||
<button class="undo-btn" @click="undoDelete()">Undo</button>
|
||
<button class="undo-dismiss" @click="undoVisible=false">×</button>
|
||
</div>
|
||
|
||
<!-- Breadcrumb + toolbar row (Library-style) -->
|
||
<div class="breadcrumb-row">
|
||
<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",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;">
|
||
<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>
|
||
<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/...">
|
||
<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",11) }}</button>
|
||
</div>
|
||
{% endif %}
|
||
<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="toggleViewMenu()">
|
||
<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="toggleSortMenu()">
|
||
<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>
|
||
<!-- Filter -->
|
||
<div class="lib-toolbar-wrap" style="position:relative;">
|
||
<button class="ws-icon-btn" :class="{active: filterOpen || filterType}" title="Filter" @click.stop="toggleFilterMenu()">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
|
||
</button>
|
||
<div class="ws-dropdown" x-show="filterOpen" @click.outside="filterOpen=false" x-transition style="right:0;">
|
||
<div class="dd-label">File type</div>
|
||
<div class="dd-item" :class="{active: filterType==='folder'}" @click="filterByType('folder'), filterOpen=false"><span class="check" x-text="filterType==='folder'?'✓':''"></span>{{ fd_icon("folder",14) }} Folders</div>
|
||
<div class="dd-item" :class="{active: filterType==='page'}" @click="filterByType('page'), filterOpen=false"><span class="check" x-text="filterType==='page'?'✓':''"></span>{{ fd_icon("file",14) }} Pages</div>
|
||
<div class="dd-item" :class="{active: filterType==='pdf'}" @click="filterByType('pdf'), filterOpen=false"><span class="check" x-text="filterType==='pdf'?'✓':''"></span>{{ fd_icon("file",14) }} PDF</div>
|
||
<div class="dd-item" :class="{active: filterType==='image'}" @click="filterByType('image'), filterOpen=false"><span class="check" x-text="filterType==='image'?'✓':''"></span>{{ fd_icon("image",14) }} Images</div>
|
||
<div class="dd-item" :class="{active: filterType==='code'}" @click="filterByType('code'), filterOpen=false"><span class="check" x-text="filterType==='code'?'✓':''"></span>{{ fd_icon("file",14) }} Code</div>
|
||
<div class="dd-item" :class="{active: filterType==='text'}" @click="filterByType('text'), filterOpen=false"><span class="check" x-text="filterType==='text'?'✓':''"></span>{{ fd_icon("file",14) }} Text</div>
|
||
<div class="more-sep" x-show="filterType"></div>
|
||
<div class="dd-item" x-show="filterType" @click="filterType='', doFilter(), filterOpen=false" style="color:var(--text-dim);">{{ fd_icon("x",14) }} Clear filter</div>
|
||
</div>
|
||
</div>
|
||
<!-- Search toggle -->
|
||
<button class="ws-icon-btn" :class="{active: searchOpen}" title="Search" @click.stop="toggleSearch()">
|
||
<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-icon-btn" style="background:var(--bg-hover); border-radius:8px;" onclick="window.FlowDeck.createPage()" title="New page">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>
|
||
</button>
|
||
</div>
|
||
</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 (Library-style) -->
|
||
<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 selection">
|
||
<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="copySelectedLinks()" title="Copy links">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
|
||
</button>
|
||
<button @click="openMovePicker()" title="Move to">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
||
</button>
|
||
<div style="position:relative;margin-left:auto;">
|
||
<button @click.stop="moreOpen=!moreOpen" title="More actions">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
|
||
</button>
|
||
<div class="ws-select-menu" x-show="moreOpen" @click.outside="moreOpen=false" x-transition>
|
||
<div class="menu-item" @click="copySelectedLinks()">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
|
||
Copy links to all
|
||
</div>
|
||
<div class="menu-item" @click="openMovePicker()">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
|
||
Move to
|
||
</div>
|
||
<div class="more-sep"></div>
|
||
<div class="menu-item menu-danger" @click="bulkDelete()">
|
||
<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>
|
||
Move to Trash <span class="shortcut">Del</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filter chips (visual indicator when filter is active — shown in all views) -->
|
||
<div class="filter-row" x-show="filterType" style="padding:4px 0;margin-bottom:6px;">
|
||
<button class="filter-chip" :class="{ active: filterType === 'folder' }" @click="filterByType('folder')" title="Folders" x-init="bindSvg($el, 'folder', 14)"></button>
|
||
<button class="filter-chip" :class="{ active: filterType === 'page' }" @click="filterByType('page')" title="Pages" x-init="bindSvg($el, 'file', 14)"></button>
|
||
<button class="filter-chip" :class="{ active: filterType === 'pdf' }" @click="filterByType('pdf')" title="PDF" x-init="bindSvg($el, 'file', 14)"></button>
|
||
<button class="filter-chip" :class="{ active: filterType === 'image' }" @click="filterByType('image')" title="Images" x-init="bindSvg($el, 'image', 14)"></button>
|
||
<button class="filter-chip" :class="{ active: filterType === 'code' }" @click="filterByType('code')" title="Code" x-init="bindSvg($el, 'file', 14)"></button>
|
||
<button class="filter-chip" :class="{ active: filterType === 'text' }" @click="filterByType('text')" title="Text" x-init="bindSvg($el, 'file', 14)"></button>
|
||
<span class="filter-clear" @click="filterType='', doFilter()">clear filter</span>
|
||
</div>
|
||
|
||
<!-- Tag filter bar -->
|
||
<div class="tag-filter-bar" x-show="workspaceTags && workspaceTags.length > 0">
|
||
<span style="font-size:11px;color:var(--text-tertiary);margin-right:4px;">Tags:</span>
|
||
<template x-for="t in workspaceTags" :key="'tf'+t.id">
|
||
<button class="tag-chip" :class="{ active: tagFilter === t.name }"
|
||
:style="{background: t.color}"
|
||
@click="toggleTagFilter(t.name)">
|
||
<span x-text="t.name"></span>
|
||
<span style="font-size:10px;opacity:.7" x-text="'('+t.count+')'"></span>
|
||
</button>
|
||
</template>
|
||
<button x-show="tagFilter" class="tag-chip rm-btn" @click="tagFilter='', doFilter()" style="margin-left:4px;">clear</button>
|
||
</div>
|
||
|
||
<!-- Tree -->
|
||
<div x-show="viewMode==='tree'" style="display:contents;">
|
||
|
||
<!-- Select all toggle -->
|
||
<div x-show="viewMode==='tree' && displayTree.length>0"
|
||
style="display:flex;align-items:center;gap:6px;padding:4px 8px;margin-bottom:2px;">
|
||
<input type="checkbox" class="item-checkbox" :checked="allSelected" @click="selectAll($event)" title="Select / unselect all">
|
||
<span style="font-size:12px;color:var(--text-secondary);cursor:pointer;user-select:none;" @click="selectAll($event)"
|
||
x-text="allSelected ? 'Clear selection' : 'Select all'"></span>
|
||
<span x-show="selectedIdsCount > 0" style="font-size:12px;color:var(--text-dim);" x-text="'(' + selectedIdsCount + ' selected)'"></span>
|
||
</div>
|
||
|
||
<ul class="ws-tree" x-show="viewMode==='tree' && (displayTree.length>0 || (currentFolder > 0))" @click="handleTreeAction($event)">
|
||
<!-- Parent folder ".." -->
|
||
<li x-show="currentFolder > 0" style="list-style:none;">
|
||
<div class="ws-tree-item" @click="goToParent()" style="padding-left:4px;">
|
||
<span style="width:16px;flex-shrink:0;"></span>
|
||
<span class="icon">{{ fd_icon("folder",16) }}</span>
|
||
<span class="name folder" style="font-weight:600;">..</span>
|
||
</div>
|
||
</li>
|
||
<template x-for="node in (showAll ? displayTree : displayTree.slice(0, 5))" :key="node.id">
|
||
<li>
|
||
<div class="ws-tree-item" :style="{paddingLeft:(node.depth||0)*20+'px'}"
|
||
draggable="true"
|
||
:data-ws-id="node.id"
|
||
:data-folder-id="node.is_folder ? node.id : null"
|
||
@dragstart="onInternalDragStart($event, node.id)"
|
||
@dragend="draggedItemId = null, $el.style.opacity = ''"
|
||
:class="{ selected: !!selectedIds[node.id] }">
|
||
<input type="checkbox" class="item-checkbox"
|
||
:checked="!!selectedIds[node.id]"
|
||
@click.stop="toggleSelect(node, $event)"
|
||
title="Select item">
|
||
<!-- drag handle -->
|
||
<div class="drag-handle" style="margin-right:2px;" title="Drag to move">
|
||
<span class="dot"></span><span class="dot"></span>
|
||
<span class="dot"></span><span class="dot"></span>
|
||
<span class="dot"></span><span class="dot"></span>
|
||
</div>
|
||
<!-- chevron -->
|
||
<template x-if="node.is_folder && node.children && node.children.length > 0">
|
||
<button class="ws-chevron"
|
||
:class="{ open: expanded[node.id] }"
|
||
@click.stop="toggleExpand(node.id)">▶</button>
|
||
</template>
|
||
<template x-if="!(node.is_folder && node.children && node.children.length > 0)">
|
||
<span style="width:16px;flex-shrink:0;"></span>
|
||
</template>
|
||
|
||
<span class="icon" x-init="bindFileIcon($el, node)"></span>
|
||
|
||
<template x-if="node.is_folder">
|
||
<span class="name folder" @click.stop="navigateToFolder(node.id)"
|
||
@dblclick.prevent.stop="startInlineRename(node, $event)">
|
||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||
<span x-show="renamingId !== node.id" x-text="node.name"></span>
|
||
<span class="child-count" x-show="node.child_count > 0 && renamingId !== node.id"
|
||
x-text="'(' + node.child_count + ')'"></span>
|
||
<input x-show="renamingId === node.id" class="inline-rename-input" x-model="renamingName"
|
||
@keydown.enter="commitRename(node)" @keydown.escape="cancelRename()"
|
||
@blur="commitRename(node)" @click.stop>
|
||
</span>
|
||
</template>
|
||
<template x-if="!node.is_folder">
|
||
<span class="name file" @click.stop="startInlineRename(node, $event)"
|
||
@mouseenter="showPreview($event, node)"
|
||
@mouseleave="hidePreview()"
|
||
title="Click to rename, double-click to open">
|
||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||
<span x-show="renamingId !== node.id" x-text="node.name"></span>
|
||
<input x-show="renamingId === node.id" class="inline-rename-input" x-model="renamingName"
|
||
@keydown.enter="commitRename(node)" @keydown.escape="cancelRename()"
|
||
@blur="commitRename(node)" @click.stop>
|
||
<a href="#" class="btn-open hover-only" @click.prevent.stop="selectForPreview(node)" title="Open in side preview">↢ Open</a>
|
||
</span>
|
||
</template>
|
||
|
||
<div class="actions">
|
||
<template x-if="node.is_folder">
|
||
<span style="display:flex;gap:2px">
|
||
<button class="ws-act" @click.stop="createPageAt(node)" title="New file">{{ fd_icon("file",14) }}</button>
|
||
<button class="ws-act" @click.stop="createFolderAt(node)" title="New folder">{{ fd_icon("folder",14) }}</button>
|
||
<a class="ws-act" href="/local-workspace" title="Open workspace page" style="text-decoration:none;display:inline-flex;align-items:center;">{{ fd_icon("external-link",14) }}</a>
|
||
</span>
|
||
</template>
|
||
<button class="ws-act" @click.stop="startRename(node)" title="Rename">{{ fd_icon("edit",14) }}</button>
|
||
<button class="ws-act danger" @click.stop="deleteWithUndo(node)" title="Delete">{{ fd_icon("trash",14) }}</button>
|
||
</div>
|
||
</div>
|
||
<!-- Children -->
|
||
<ul class="ws-tree" x-show="expanded[node.id]" x-transition
|
||
x-init="bindChildren($el, node)">
|
||
</ul>
|
||
</li>
|
||
</template>
|
||
<!-- "X more" toggle -->
|
||
<li x-show="displayTree.length > 5 && !showAll" style="list-style:none;">
|
||
<div class="ws-tree-item ws-more-item" @click="showAll = true" style="padding-left:4px;cursor:pointer;">
|
||
<span style="width:16px;flex-shrink:0;"></span>
|
||
<span class="icon">⋯</span>
|
||
<span class="name folder" style="color:var(--text-secondary);font-style:italic;"
|
||
x-text="displayTree.length - 5 + ' more...'"></span>
|
||
</div>
|
||
</li>
|
||
<!-- "Show less" toggle -->
|
||
<li x-show="displayTree.length > 5 && showAll" style="list-style:none;">
|
||
<div class="ws-tree-item ws-more-item" @click="showAll = false" style="padding-left:4px;cursor:pointer;">
|
||
<span style="width:16px;flex-shrink:0;"></span>
|
||
<span class="icon" style="display:inline-flex;align-items:center;justify-content:center;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:14px;height:14px;color:var(--text-dim);"><polyline points="18 15 12 9 6 15"/></svg></span>
|
||
<span class="name folder" style="color:var(--text-secondary);">Show less</span>
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Filter table (shown when search/filter/tag is active) -->
|
||
<table class="ws-table-enhanced" x-show="(searchQuery || filterType || tagFilter) && filteredTableItems.length > 0">
|
||
<thead><tr>
|
||
<th style="width:30px"><input type="checkbox" :checked="allSelected" @click="selectAllFiltered($event)"></th>
|
||
<th>Name</th>
|
||
<th>Path</th>
|
||
<th>Size</th>
|
||
<th>Modified</th>
|
||
<th>Type</th>
|
||
<th>Author</th>
|
||
<th>Tags</th>
|
||
<th style="width:60px"></th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<template x-for="node in filteredTableItems" :key="'ft'+node.id">
|
||
<tr :class="{ selected: !!selectedIds[node.id] }">
|
||
<td><input type="checkbox" :checked="!!selectedIds[node.id]" @click.stop="toggleSelect(node, $event)"></td>
|
||
<td>
|
||
<span class="table-icon" x-init="bindFileIcon($el, node)"></span>
|
||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||
<span class="col-name" @click="node.is_folder ? navigateToFolder(node.id) : openPage(node.id)" x-text="node.name"></span>
|
||
</td>
|
||
<td class="col-path" x-text="node._path || '—'"></td>
|
||
<td class="col-size" x-text="node.size_display || '—'"></td>
|
||
<td class="col-date" x-text="_formatDate(node.updated_at)"></td>
|
||
<td class="col-type" x-text="node.is_folder ? 'Folder' : _fileTypeLabel(node.name, node.content_format)"></td>
|
||
<td class="col-author" x-text="node.author || '—'"></td>
|
||
<td class="col-tags">
|
||
<span style="display:flex;align-items:center;gap:2px;flex-wrap:wrap;">
|
||
<template x-for="t in (node.tags||[])" :key="'ftag'+t.id">
|
||
<span class="tag-chip" :style="{background: t.color}">
|
||
<span x-text="t.name"></span>
|
||
<button class="tag-chip rm-btn" @click.stop="removeTag(node.id, t.id)" title="Remove tag">×</button>
|
||
</span>
|
||
</template>
|
||
<button class="tag-add-btn" @click.stop="startTagInput(node)" title="Add tag">+</button>
|
||
<span x-show="tagInputId === node.id" style="display:contents;">
|
||
<input class="tag-input" x-ref="tagInputEl"
|
||
@keydown.enter="addTag(node.id, $event.target.value), $event.target.value=''"
|
||
@keydown.escape="tagInputId=null"
|
||
@blur="tagInputId=null"
|
||
@click.stop
|
||
placeholder="tag...">
|
||
</span>
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<button class="ws-act" @click.stop="startRename(node)" title="Rename">{{ fd_icon("edit",14) }}</button>
|
||
<button class="ws-act danger" @click.stop="deleteWithUndo(node)" title="Delete">{{ fd_icon("trash",14) }}</button>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- List view — compact flat list -->
|
||
<table class="ws-table-compact" x-show="viewMode==='list' && !searchQuery && !filterType && !tagFilter && displayTree.length>0">
|
||
<thead><tr>
|
||
<th style="width:24px"></th>
|
||
<th>Name</th>
|
||
<th style="width:80px">Type</th>
|
||
<th style="width:80px">Size</th>
|
||
<th style="width:100px">Modified</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<!-- Parent folder ".." -->
|
||
<tr x-show="currentFolder > 0" @click="goToParent()" style="cursor:pointer" class="parent-folder-row">
|
||
<td>{{ fd_icon("folder",16) }}</td>
|
||
<td class="col-name"><span style="font-weight:600;color:var(--text);">..</span></td>
|
||
<td style="font-size:12px;color:var(--text-tertiary)">Folder</td>
|
||
<td style="font-size:12px;color:var(--text-secondary)"></td>
|
||
<td style="font-size:12px;color:var(--text-tertiary)"></td>
|
||
</tr>
|
||
<template x-for="node in displayTree" :key="'l'+node.id">
|
||
<tr :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()" style="cursor:pointer">
|
||
<td><span x-init="bindFileIcon($el, node)"></span>
|
||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span></td>
|
||
<td class="col-name" x-text="node.name"></td>
|
||
<td style="font-size:12px;color:var(--text-tertiary)" x-text="node.is_folder ? 'Folder' : _fileTypeLabel(node.name, node.content_format)"></td>
|
||
<td style="font-size:12px;color:var(--text-secondary)" x-text="node.is_folder ? (node.child_count||0)+' items' : (node.size_display||'—')"></td>
|
||
<td style="font-size:12px;color:var(--text-tertiary)" x-text="_formatDate(node.updated_at)"></td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- Details view — full table with all columns -->
|
||
<table class="ws-table-enhanced" x-show="viewMode==='details' && !searchQuery && !filterType && !tagFilter && displayTree.length>0">
|
||
<thead><tr>
|
||
<th style="width:24px"></th>
|
||
<th>Name</th>
|
||
<th>Path</th>
|
||
<th>Size</th>
|
||
<th>Modified</th>
|
||
<th>Type</th>
|
||
<th>Author</th>
|
||
<th>Tags</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
<!-- Parent folder ".." -->
|
||
<tr x-show="currentFolder > 0" @click="goToParent()" style="cursor:pointer" class="parent-folder-row">
|
||
<td>{{ fd_icon("folder",16) }}</td>
|
||
<td class="col-name"><span style="font-weight:600;color:var(--text);">..</span></td>
|
||
<td class="col-path"></td>
|
||
<td class="col-size"></td>
|
||
<td class="col-date"></td>
|
||
<td class="col-type">Folder</td>
|
||
<td class="col-author"></td>
|
||
<td class="col-tags"></td>
|
||
</tr>
|
||
<template x-for="node in displayTree" :key="'d'+node.id">
|
||
<tr :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()" style="cursor:pointer"
|
||
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
|
||
<td><span class="table-icon" x-init="bindFileIcon($el, node)"></span>
|
||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span></td>
|
||
<td class="col-name" x-text="node.name"></td>
|
||
<td class="col-path" x-text="node._path || '—'"></td>
|
||
<td class="col-size" x-text="node.size_display || '—'"></td>
|
||
<td class="col-date" x-text="_formatDate(node.updated_at)"></td>
|
||
<td class="col-type" x-text="node.is_folder ? 'Folder' : _fileTypeLabel(node.name, node.content_format)"></td>
|
||
<td class="col-author" x-text="node.author || '—'"></td>
|
||
<td class="col-tags">
|
||
<span style="display:flex;align-items:center;gap:2px;flex-wrap:wrap;">
|
||
<template x-for="t in (node.tags||[])" :key="'dtag'+t.id">
|
||
<span class="tag-chip" :style="{background: t.color}"><span x-text="t.name"></span></span>
|
||
</template>
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
|
||
<!-- Title view — card grid -->
|
||
<div class="title-grid" x-show="viewMode==='title' && !searchQuery && !filterType && !tagFilter && displayTree.length>0">
|
||
<!-- Parent folder ".." -->
|
||
<div class="title-card parent-folder-card" x-show="currentFolder > 0" @click="goToParent()" style="cursor:pointer">
|
||
<div class="title-card-icon">{{ fd_icon("folder",24) }}</div>
|
||
<div class="title-card-name" style="font-weight:600;">..</div>
|
||
<div class="title-card-meta"><span>Parent folder</span></div>
|
||
</div>
|
||
<template x-for="node in displayTree" :key="'g'+node.id">
|
||
<div class="title-card" :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()"
|
||
:class="{ 'selected-card': previewItem && previewItem.id === node.id }">
|
||
<div class="title-card-icon" x-init="bindNodeIcon($el, node)"></div>
|
||
<div class="title-card-name">
|
||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||
<span x-text="node.name"></span>
|
||
</div>
|
||
<div class="title-card-meta">
|
||
<span x-text="_formatDate(node.updated_at)"></span>
|
||
<span x-text="node.size_display || '—'"></span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- Content view — list with inline preview -->
|
||
<div class="content-list" x-show="viewMode==='content' && !searchQuery && !filterType && !tagFilter && displayTree.length>0">
|
||
<!-- Parent folder ".." -->
|
||
<div class="content-item parent-folder-item" x-show="currentFolder > 0" @click="goToParent()" style="cursor:pointer">
|
||
<div class="content-item-header">
|
||
{{ fd_icon("folder",16) }}
|
||
<span class="col-name" style="font-weight:600;">..</span>
|
||
</div>
|
||
<div class="content-item-preview">Parent folder</div>
|
||
</div>
|
||
<template x-for="node in displayTree" :key="'c'+node.id">
|
||
<div class="content-item" :data-ws-id="node.id" @click="node.is_folder ? navigateToFolder(node.id) : selectForPreview(node)" @mouseenter="showPreview($event, node)" @mouseleave="hidePreview()"
|
||
:class="{ 'selected-row': previewItem && previewItem.id === node.id }">
|
||
<div class="content-item-header">
|
||
<span x-init="bindFileIcon($el, node)"></span>
|
||
<span x-show="node.is_shared" class="ws-shared-badge" title="Partagé" x-text="'👥'"></span>
|
||
<span class="col-name" x-text="node.name"></span>
|
||
<span style="font-size:11px;color:var(--text-tertiary);margin-left:auto" x-text="_formatDate(node.updated_at)"></span>
|
||
</div>
|
||
<div class="content-item-preview" x-text="node.is_folder ? (node.child_count||0)+' items' : (node.size_display||'—')"></div>
|
||
<div class="content-item-tags" x-show="(node.tags||[]).length > 0">
|
||
<template x-for="t in (node.tags||[])" :key="'ctag'+t.id">
|
||
<span class="tag-chip" :style="{background: t.color}"><span x-text="t.name"></span></span>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Old table view kept for backward compat, hidden -->
|
||
<table class="ws-table" x-show="false" style="display:none">
|
||
<thead><tr><th>Name</th><th>Type</th><th>Size</th></tr></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
|
||
<!-- Empty -->
|
||
<div class="empty-state" x-show="tree.length===0&&loaded">
|
||
<div class="empty-icon">{{ fd_icon("folder",48) }}</div>
|
||
<h2 x-text="currentFolder > 0 ? 'Empty folder' : 'Welcome to your Workspace'"></h2>
|
||
<p x-text="currentFolder > 0 ? 'Create a new file or folder to get started' : 'Drag & drop files from your computer, or create new ones.'"></p>
|
||
<div class="empty-actions">
|
||
<button class="btn-icon" onclick="window.FlowDeck.createPage()" title="New file">{{ fd_icon("file",16) }}</button>
|
||
<button class="btn-icon" onclick="window.FlowDeck.showCreateFolderModal()" title="New folder">{{ fd_icon("folder",16) }}</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Offline banner -->
|
||
<div class="offline-banner" x-show="!online" x-transition>
|
||
<span>{{ fd_icon('alert-triangle',14) }} You are offline. Changes will sync when connection is restored.</span>
|
||
<span class="offline-badge" x-show="pendingSyncCount > 0" x-text="pendingSyncCount + ' modification(s) en attente'"></span>
|
||
</div>
|
||
|
||
<!-- Move-to modal -->
|
||
<div class="modal-overlay" x-show="moveModalOpen" @click.self="moveModalOpen=false" @keydown.escape="moveModalOpen=false" style="display:none">
|
||
<div class="modal-box" style="width:440px;max-width:90vw;padding:0;overflow:hidden;display:flex;flex-direction:column;max-height:60vh;">
|
||
<div style="padding:14px 16px;border-bottom:1px solid var(--border);font-size:14px;font-weight:600;color:var(--text-primary);">Move to…</div>
|
||
<div style="padding:8px 12px;border-bottom:1px solid var(--border);">
|
||
<input class="modal-input" type="text" placeholder="Search folders..." x-model="moveSearchQuery" @input="doMoveSearch()" style="padding:7px 10px;font-size:13px;">
|
||
</div>
|
||
<div style="flex:1;overflow-y:auto;padding:4px;">
|
||
<div class="move-modal-item" @click="confirmBulkMove(0)">{{ fd_icon("folder",14) }} <span>Root (no parent)</span></div>
|
||
<template x-for="it in moveCandidates" :key="'mv'+it.id">
|
||
<div class="move-modal-item" :style="{paddingLeft: (8 + (it.depth||0)*20) + 'px'}" @click="confirmBulkMove(it.id)">
|
||
{{ fd_icon("folder",14) }} <span x-text="it.name"></span>
|
||
</div>
|
||
</template>
|
||
<div x-show="moveCandidates.length === 0" style="padding:20px;text-align:center;color:var(--text-tertiary);font-size:13px;">No folders found</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create Modal -->
|
||
<div class="modal-overlay" x-show="showCreate" @click.outside="showCreate=false" @keydown.escape="showCreate=false" style="display:none">
|
||
<div class="modal-box">
|
||
<h3><span x-show="createType==='folder'">{{ fd_icon('folder',16) }}</span><span x-show="createType!='folder'">{{ fd_icon('file',16) }}</span> <span x-text="createType==='folder'?'New Folder':'New File'"></span></h3>
|
||
<p class="modal-sub">
|
||
In <strong>{{ workspace_name }}</strong> <span x-show="folderStack.length>1">/ <span x-text="(folderStack[folderStack.length-1]||{}).name||''"></span></span>
|
||
<span x-show="parentFolder"> / <span x-text="parentFolder ? parentFolder.name : ''"></span></span>
|
||
</p>
|
||
<input class="modal-input" x-model="newName" :placeholder="createType==='folder'?'Folder name':'File name'" @keydown.enter="doCreate" x-ref="cinp">
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" @click="showCreate=false">Cancel</button>
|
||
<button class="btn btn-primary" @click="doCreate" :disabled="!newName.trim()">Create</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Rename Modal -->
|
||
<div class="modal-overlay" x-show="showRename" @click.outside="showRename=false" @keydown.escape="showRename=false" style="display:none">
|
||
<div class="modal-box">
|
||
<h3>{{ fd_icon("edit",14) }} Rename</h3>
|
||
<p class="modal-sub">Rename <strong x-text="target ? target.name : ''"></strong></p>
|
||
<input class="modal-input" x-model="newName" @keydown.enter="doRename" x-ref="rinp">
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" @click="showRename=false">Cancel</button>
|
||
<button class="btn btn-primary" @click="doRename">Rename</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Delete Modal -->
|
||
<div class="modal-overlay" x-show="showDelete" @click.outside="showDelete=false" @keydown.escape="showDelete=false" style="display:none">
|
||
<div class="modal-box">
|
||
<h3>{{ fd_icon("trash",16) }} Delete</h3>
|
||
<p class="modal-sub">Delete <strong x-text="target ? target.name : ''"></strong>?</p>
|
||
<div class="delete-confirm">This cannot be undone. Children will also be deleted.</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-secondary" @click="showDelete=false">Cancel</button>
|
||
<button class="btn btn-danger" @click="doDelete">Delete</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /ws-main -->
|
||
|
||
<!-- Context menu (Library-style) — MENU UNIQUE partagé (workspace ⇄ library) -->
|
||
{% include "_ctx_menu.html" %}
|
||
|
||
<!-- Preview tooltip -->
|
||
<!-- ponytail: le parseur referme la div racine avant ce bloc (structure
|
||
pré-existante, masquée par le fallback window d'Alpine standard) →
|
||
wsPreview = wrapper déléguant vers _wsInitData (voir local_workspace.js) -->
|
||
<div class="file-preview" x-data="wsPreview()" x-show="previewVisible"
|
||
:style="{ left: previewX + 'px', top: previewY + 'px' }"
|
||
@mouseenter="previewVisible = true" @mouseleave="previewVisible = false">
|
||
<div class="file-preview-header" x-text="previewName"></div>
|
||
<div class="file-preview-body" x-init="bindPreview($el)"></div>
|
||
</div>
|
||
|
||
<!-- Preview Panel — identical to Library peek-overlay -->
|
||
<div id="ws-preview-panel" style="position:fixed;top:0;right:0;width:560px;max-width:90vw;height:100vh;background:var(--bg-modal);border-left:1px solid var(--border);box-shadow:-8px 0 32px rgba(0,0,0,0.25);z-index:500;display:none;flex-direction:column;transform:translateX(100%);transition:transform 0.2s cubic-bezier(0.16,1,0.3,1);">
|
||
<div id="ws-preview-resize-handle" style="position:absolute;left:-4px;top:0;width:8px;height:100%;cursor:col-resize;z-index:501;" title="Drag to resize — click to close"></div>
|
||
<div class="peek-header" style="display:flex;align-items:center;gap:4px;padding:12px 16px;border-bottom:1px solid var(--border);">
|
||
<button onclick="window._wsData.closePreviewPanel()" title="Close" style="background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:6px;border-radius:4px;display:flex;align-items:center;">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px;height:15px;"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||
</button>
|
||
<div class="peek-title" style="flex:1;font-size:15px;font-weight:600;color:var(--text-primary);display:flex;align-items:center;gap:8px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">
|
||
<span id="ws-preview-icon"></span>
|
||
<span id="ws-preview-title"></span>
|
||
</div>
|
||
<button id="ws-preview-open-btn" title="Open full page" style="background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:6px;border-radius:4px;display:flex;align-items:center;">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px;height:15px;"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||
</button>
|
||
<button id="ws-preview-fav-btn" title="Favorite" style="background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:6px;border-radius:4px;display:flex;align-items:center;">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px;height:15px;"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||
</button>
|
||
<button id="ws-preview-copy-btn" title="Copy link" style="background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:6px;border-radius:4px;display:flex;align-items:center;">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px;height:15px;"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
|
||
</button>
|
||
<div style="position:relative;">
|
||
<button onclick="var m=document.getElementById('ws-preview-more-menu');m.style.display=m.style.display==='block'?'none':'block';" title="More" style="background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:6px;border-radius:4px;display:flex;align-items:center;">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px;height:15px;"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
|
||
</button>
|
||
<div id="ws-preview-more-menu" class="more-menu" style="display:none;right:0;top:100%;min-width:180px;">
|
||
<div class="menu-item" onclick="document.getElementById('ws-preview-open-btn').click();document.getElementById('ws-preview-more-menu').style.display='none';">{{ fd_icon("external-link",14) }} Open</div>
|
||
<div class="menu-item" onclick="document.getElementById('ws-preview-fav-btn').click();document.getElementById('ws-preview-more-menu').style.display='none';">{{ fd_icon("star",14) }} Favorite</div>
|
||
<div class="menu-item" onclick="document.getElementById('ws-preview-copy-btn').click();document.getElementById('ws-preview-more-menu').style.display='none';">{{ fd_icon("link",14) }} Copy link</div>
|
||
<div class="more-sep"></div>
|
||
<div class="menu-item menu-danger" onclick="window._wsData.closePreviewPanel();document.getElementById('ws-preview-more-menu').style.display='none';">{{ fd_icon("trash",14) }} Close</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<iframe id="ws-preview-iframe" src="" style="width:100%;height:100%;border:none;flex:1;"></iframe>
|
||
</div>
|
||
</div><!-- /ws-split -->
|
||
|
||
</div><!-- /_wsInitData wrapper -->
|
||
|
||
{% endblock %}
|