Extraction des 7 templates dont le JS n'est PAS interpolé Jinja → 9 fichiers
static/js/*.js (4 243 lignes, -30 % du JS inline : 13 904 → 9 661) :
- agent_panel_1/_2 (bloc de 1 788 L livré sur CHAQUE page), library (1 039),
gitea_workspace (626), _icon_picker_1/_2, _ctx_menu, import, workspaces
- UN fichier par bloc : ordre/timing identiques (pas de defer, attributs
conservés dont data-cfasync), cache-busting via ?v={{ asset_version }}
(source unique A40), scripts externes = 'self' en CSP (pas de nonce requis)
- garde-fou : le script refuse tout bloc contenant {{ ou {%
- vérifs : node --check vert sur les 9, 0 script inline restant dans les
cibles, suite complète 1089/1089
Lint (la moitié « ajouter les templates à eslint » de l'audit) :
- eslint.config.mjs existait (flat v9, sans dépendances npm) mais AUCUN
binaire eslint n'était installé → npm i -g eslint
- `eslint static/js` → 0 erreur, 120 warnings (no-unused-vars 69,
no-empty 36, no-undef 15) sur 8 fichiers = baseline à nettoyer
- les extraits sont couverts d'office par la config (static/js/**/*.js)
Reste A27 : blocs interpolés Jinja (page_editor 2 517, local_workspace 2 031,
base 1 523, database_table 1 323, settings 1 093, realtime 531 ≈ 9 661 L)
→ extraction en 2 temps (config JSON injectée + script statique).
suite **1089/1089** · ruff OK · docs à jour
461 lines
32 KiB
HTML
461 lines
32 KiB
HTML
{% extends "base.html" %}
|
|
{% block page_icon %}{{ fd_icon("book",18) }}{% endblock %}
|
|
{% block page_title %}Library{% endblock %}
|
|
{% block title_prefix %}Library{% endblock %}
|
|
|
|
{% block topbar %}
|
|
{% set page_icon = "book" %}
|
|
{% set page_title = "Library" %}
|
|
{% include '_header.html' %}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div x-data="libraryPage()" x-init="init()" @keydown.escape="closePeek()">
|
|
<div id="lib-workspace-id" style="display:none;">{{ active_workspace_id or 0 }}</div>
|
|
<div id="lib-gitea-active" style="display:none;">{{ '1' if is_gitea_workspace else '0' }}</div>
|
|
<div id="lib-gitea-owner" style="display:none;">{{ gitea_workspace_owner or '' }}</div>
|
|
<div id="lib-gitea-repo" style="display:none;">{{ gitea_workspace_repo or '' }}</div>
|
|
|
|
<style>
|
|
/* ── Library layout ── */
|
|
.lib-container{max-width:1100px;margin:0 auto;padding:20px 24px;}
|
|
.lib-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;}
|
|
.lib-header h1{font-size:24px;font-weight:700;color:var(--text-primary);margin:0;}
|
|
.lib-header-actions{display:flex;align-items:center;gap:8px;}
|
|
.lib-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;}
|
|
.lib-icon-btn:hover,.lib-icon-btn.active{background:var(--bg-hover);color:var(--text-primary);}
|
|
.lib-icon-btn svg{width:16px;height:16px;}
|
|
.lib-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;}
|
|
.lib-btn-primary:hover{background:var(--accent-hover);}
|
|
.lib-btn-primary svg{width:14px;height:14px;}
|
|
|
|
/* ── Tabs row (tabs left, toolbar icons right) ── */
|
|
.lib-tabs-row{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px;}
|
|
.lib-tabs{display:flex;gap:2px;align-items:center;overflow-x:auto;scrollbar-width:none;}
|
|
.lib-tabs::-webkit-scrollbar{display:none;}
|
|
.lib-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;}
|
|
.lib-tab:hover{color:var(--text-primary);background:var(--bg-hover);}
|
|
.lib-tab.active{color:var(--text-primary);background:var(--bg-active);}
|
|
.lib-tab svg{width:14px;height:14px;flex-shrink:0;}
|
|
.lib-toolbar{display:flex;align-items:center;gap:2px;flex-shrink:0;}
|
|
.lib-toolbar-wrap{position:relative;}
|
|
|
|
/* ── Dropdowns (filter/sort/view) ── */
|
|
.lib-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;}
|
|
.lib-dropdown .dd-label{padding:6px 8px 2px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.3px;}
|
|
.lib-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;}
|
|
.lib-dropdown .dd-item:hover{background:var(--bg-hover);}
|
|
.lib-dropdown .dd-item.active{color:var(--accent);}
|
|
.lib-dropdown .dd-item .check{width:14px;flex-shrink:0;color:var(--accent);}
|
|
.lib-search-bar{margin-bottom:10px;}
|
|
.lib-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;}
|
|
|
|
/* ── Multi-select bar ── */
|
|
.lib-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;}
|
|
.lib-select-bar .sel-count{font-weight:500;color:var(--text-primary);margin-right:8px;white-space:nowrap;}
|
|
.lib-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;}
|
|
.lib-select-bar button:hover{background:var(--bg-hover);color:var(--text-primary);}
|
|
.lib-select-bar button svg{width:14px;height:14px;}
|
|
|
|
/* ── Table (div-based for hover effects) ── */
|
|
.lib-table{width:100%;}
|
|
.lib-table-header{display:flex;align-items:center;border-bottom:1px solid var(--border);padding:0;}
|
|
.lib-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;}
|
|
.lib-table-header .th svg{width:13px;height:13px;flex-shrink:0;}
|
|
|
|
/* Column widths — Notion-style */
|
|
.lib-col-name{flex:1;min-width:0;}
|
|
.lib-col-author{width:150px;flex-shrink:0;display:none;}
|
|
.lib-col-source{width:120px;flex-shrink:0;display:none;}
|
|
.lib-col-edited{width:110px;flex-shrink:0;text-align:right;justify-content:flex-end;}
|
|
.lib-table.show-author .lib-col-author{display:flex;}
|
|
.lib-table.show-source .lib-col-source{display:flex;}
|
|
|
|
/* ── Row ── */
|
|
.lib-row{display:flex;align-items:center;height:34px;transition:background 0.1s;position:relative;}
|
|
.lib-row:hover{background:var(--bg-hover);}
|
|
.lib-row.selected{background:rgba(35,131,226,0.08);}
|
|
.lib-row.drag-over{outline:2px solid var(--accent);outline-offset:-2px;background:rgba(35,131,226,0.12);}
|
|
.lib-row.dragging{opacity:0.4;}
|
|
.lib-row .row-cell{display:flex;align-items:center;padding:0 8px;height:100%;overflow:hidden;}
|
|
.lib-row .row-cell.lib-col-edited{color:var(--text-secondary);font-size:13px;white-space:nowrap;}
|
|
|
|
/* ── Name cell — contains drag handle, checkbox, chevron, icon, name, OPEN ── */
|
|
.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;}
|
|
|
|
/* ── Tag chips (représentation + filtre) ── */
|
|
.row-tags{display:flex;align-items:center;gap:3px;flex-shrink:0;overflow:hidden;max-width:45%;}
|
|
.tag-chip{display:inline-flex;align-items:center;gap:3px;font-size:10px;line-height:1;padding:2px 6px;border-radius:999px;color:#fff;white-space:nowrap;border:none;cursor:pointer;font-family:inherit;}
|
|
.tag-chip.active{outline:2px solid var(--text-primary);outline-offset:1px;}
|
|
.lib-tag-filter{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:4px 16px 2px;}
|
|
.lib-tag-filter .tf-label{font-size:11px;color:var(--text-dim);margin-right:2px;}
|
|
|
|
/* ── Shared direction sub-filter pils ── */
|
|
.lib-share-directory{display:flex;align-items:center;gap:6px;padding:4px 16px;}
|
|
.lib-dir-pill{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);cursor:pointer;transition:background 0.15s,color 0.15s,border-color 0.15s;}
|
|
.lib-dir-pill:hover{background:var(--bg-hover);color:var(--text-primary);}
|
|
.lib-dir-pill.active{background:var(--accent);border-color:var(--accent);color:#fff;}
|
|
|
|
/* ── Hover-only elements (hidden until row hover, or when checked/dragging) ── */
|
|
.hover-only{opacity:0;transition:opacity 0.12s;flex-shrink:0;}
|
|
.lib-row:hover .hover-only,.lib-row.selected .hover-only{opacity:1;}
|
|
|
|
/* Drag handle — grip icon */
|
|
.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 */
|
|
.lib-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;}
|
|
.lib-checkbox:hover{border-color:var(--text-primary);}
|
|
.lib-checkbox.checked{background:var(--accent);border-color:var(--accent);opacity:1!important;}
|
|
.lib-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 levels (computed inline via CSS var for arbitrary depth) */
|
|
.row-indent{flex-shrink:0;}
|
|
|
|
/* ── Author cell ── */
|
|
.row-author{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text-secondary);}
|
|
.author-avatar{width:18px;height:18px;border-radius:50%;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:600;color:var(--text-secondary);flex-shrink:0;}
|
|
|
|
/* ── Source cell ── */
|
|
.row-source{font-size:13px;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
|
|
/* "+ Add new" under expanded parent */
|
|
.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;}
|
|
|
|
/* ── Side Peek Panel ── */
|
|
.peek-overlay{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:flex;flex-direction:column;transform:translateX(100%);transition:transform 0.2s cubic-bezier(0.16,1,0.3,1);}
|
|
.peek-overlay.open{transform:translateX(0);}
|
|
.peek-header{display:flex;align-items:center;gap:4px;padding:12px 16px;border-bottom:1px solid var(--border);}
|
|
.peek-header .peek-title{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;}
|
|
.peek-header button{background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:6px;border-radius:4px;font-size:13px;display:flex;align-items:center;}
|
|
.peek-header button:hover{background:var(--bg-hover);color:var(--text-primary);}
|
|
.peek-header button svg{width:15px;height:15px;}
|
|
.peek-body{flex:1;overflow-y:auto;padding:16px 20px;}
|
|
.peek-body .peek-empty{text-align:center;padding:60px 20px;color:var(--text-dim);}
|
|
.peek-loading{text-align:center;padding:40px;color:var(--text-dim);}
|
|
|
|
/* ── "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;}
|
|
|
|
/* ── More / context menu dropdown ── */
|
|
.more-menu{position:absolute;min-width:200px;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow-popover);z-index:300;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;}
|
|
|
|
/* ── Move-to modal ── */
|
|
.move-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:600;display:flex;align-items:center;justify-content:center;}
|
|
.move-modal{width:420px;max-height:60vh;background:var(--bg-modal);border-radius:10px;box-shadow:var(--shadow-modal);display:flex;flex-direction:column;overflow:hidden;}
|
|
.move-modal-header{padding:14px 16px;border-bottom:1px solid var(--border);font-size:14px;font-weight:600;color:var(--text-primary);}
|
|
.move-modal-search{padding:8px 12px;border-bottom:1px solid var(--border);}
|
|
.move-modal-search input{width:100%;padding:7px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:13px;outline:none;}
|
|
.move-modal-list{flex:1;overflow-y:auto;padding:4px;}
|
|
.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);}
|
|
|
|
/* ── Empty state ── */
|
|
.lib-empty{text-align:center;padding:80px 20px;color:var(--text-dim);}
|
|
.lib-empty .empty-icon{font-size:48px;margin-bottom:12px;opacity:0.5;}
|
|
.lib-empty h3{font-size:16px;color:var(--text-secondary);margin:0 0 4px;}
|
|
.lib-empty p{font-size:13px;margin:0;}
|
|
|
|
/* ── Loading ── */
|
|
.lib-loading{text-align:center;padding:60px 20px;color:var(--text-dim);}
|
|
|
|
/* ── Responsive ── */
|
|
@media(max-width:768px){
|
|
.lib-col-author,.lib-col-source{display:none!important;}
|
|
.peek-overlay{width:100vw;max-width:100vw;}
|
|
}
|
|
</style>
|
|
|
|
<!-- ── Header ── -->
|
|
<div class="lib-container">
|
|
<div class="lib-header">
|
|
<h1>Library</h1>
|
|
<div class="lib-header-actions">
|
|
<button class="lib-btn-primary" x-show="!noWorkspace" onclick="window.FlowDeck.createPage()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
New page
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Tabs + toolbar row ── -->
|
|
<div class="lib-tabs-row">
|
|
<div class="lib-tabs">
|
|
<button class="lib-tab" :class="{ active: tab === 'recents' }" @click="switchTab('recents')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 16 14"/></svg> Recents
|
|
</button>
|
|
<button class="lib-tab" :class="{ active: tab === 'favorites' }" @click="switchTab('favorites')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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> Favorites
|
|
</button>
|
|
<button class="lib-tab" :class="{ active: tab === 'shared' }" @click="switchTab('shared')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg> Shared
|
|
</button>
|
|
<button class="lib-tab" :class="{ active: tab === 'private' }" @click="switchTab('private')" x-show="!workspaceId && !isGiteaActive">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg> Private
|
|
</button>
|
|
<button class="lib-tab" :class="{ active: tab === 'published' }" @click="switchTab('published')">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><line x1="3" y1="12" x2="21" y2="12"/><path d="M12 3a15 15 0 010 18 15 15 0 010-18"/></svg> Published
|
|
</button>
|
|
<button class="lib-tab" :class="{ active: tab === 'workspace' }" @click="switchTab('workspace')">
|
|
<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> Workspace
|
|
</button>
|
|
<button class="lib-tab" :class="{ active: tab === 'repository' }" @click="switchTab('repository')" x-show="isGiteaActive">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4a2 2 0 001-1.73z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg> Repository
|
|
</button>
|
|
</div>
|
|
<div class="lib-toolbar">
|
|
<div class="lib-toolbar-wrap" x-show="tab !== 'repository'">
|
|
<button class="lib-icon-btn" :class="{active: filterOpen}" title="Filter" @click.stop="filterOpen=!filterOpen; sortOpen=false; viewOpen=false">
|
|
<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="lib-dropdown" x-show="filterOpen" @click.outside="filterOpen=false" x-transition>
|
|
<div class="dd-label">Source</div>
|
|
<div class="dd-item" :class="{active: sourceFilter==='all'}" @click="setSourceFilter('all')"><span class="check" x-text="sourceFilter==='all' ? '✓' : ''"></span> All sources</div>
|
|
<div class="dd-item" :class="{active: sourceFilter==='local'}" @click="setSourceFilter('local')"><span class="check" x-text="sourceFilter==='local' ? '✓' : ''"></span> Local</div>
|
|
<div class="dd-item" :class="{active: sourceFilter==='gitea'}" @click="setSourceFilter('gitea')"><span class="check" x-text="sourceFilter==='gitea' ? '✓' : ''"></span> Gitea</div>
|
|
<div class="dd-item" :class="{active: sourceFilter==='github'}" @click="setSourceFilter('github')"><span class="check" x-text="sourceFilter==='github' ? '✓' : ''"></span> GitHub</div>
|
|
</div>
|
|
</div>
|
|
<div class="lib-toolbar-wrap">
|
|
<button class="lib-icon-btn" :class="{active: sortOpen}" title="Sort" @click.stop="sortOpen=!sortOpen; filterOpen=false; viewOpen=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="lib-dropdown" x-show="sortOpen" @click.outside="sortOpen=false" x-transition>
|
|
<div class="dd-label">Sort by</div>
|
|
<div class="dd-item" :class="{active: sortBy==='name'}" @click="setSort('name')"><span class="check" x-text="sortBy==='name' ? '✓' : ''"></span> Name</div>
|
|
<div class="dd-item" :class="{active: sortBy==='edited'}" @click="setSort('edited')"><span class="check" x-text="sortBy==='edited' ? '✓' : ''"></span> Last edited</div>
|
|
<div class="dd-item" :class="{active: sortBy==='author'}" @click="setSort('author')"><span class="check" x-text="sortBy==='author' ? '✓' : ''"></span> Created by</div>
|
|
</div>
|
|
</div>
|
|
<button class="lib-icon-btn" :class="{active: searchOpen}" title="Search" @click.stop="searchOpen=!searchOpen; $nextTick(()=>{ if(searchOpen) document.getElementById('lib-search-input').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>
|
|
<div class="lib-toolbar-wrap">
|
|
<button class="lib-icon-btn" :class="{active: viewOpen}" title="View options" @click.stop="viewOpen=!viewOpen; filterOpen=false; sortOpen=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"/><circle cx="9" cy="6" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="12" r="1.5" fill="currentColor" stroke="none"/><circle cx="9" cy="18" r="1.5" fill="currentColor" stroke="none"/></svg>
|
|
</button>
|
|
<div class="lib-dropdown" x-show="viewOpen" @click.outside="viewOpen=false" x-transition>
|
|
<div class="dd-label">Show columns</div>
|
|
<div class="dd-item" @click="toggleColumn('author')"><span class="check" x-text="showAuthor ? '✓' : ''"></span> Created by</div>
|
|
<div class="dd-item" @click="toggleColumn('source')"><span class="check" x-text="showSource ? '✓' : ''"></span> Source</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Shared direction sub-filter (Par moi / Avec moi) -->
|
|
<div class="lib-share-directory" x-show="tab === 'shared'">
|
|
<button class="lib-dir-pill" :class="{ active: shareDir === 'all' }" @click="setShareDir('all')">Tous</button>
|
|
<button class="lib-dir-pill" :class="{ active: shareDir === 'made' }" @click="setShareDir('made')">Par moi</button>
|
|
<button class="lib-dir-pill" :class="{ active: shareDir === 'received' }" @click="setShareDir('received')">Avec moi</button>
|
|
</div>
|
|
|
|
<!-- Tag filter bar -->
|
|
<div class="lib-tag-filter" x-show="workspaceTags && workspaceTags.length > 0" x-cloak>
|
|
<span class="tf-label">Tags:</span>
|
|
<template x-for="t in workspaceTags" :key="'ltf'+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||0)+')'"></span>
|
|
</button>
|
|
</template>
|
|
<button x-show="tagFilter" class="tag-chip" style="background:var(--bg-tertiary);color:var(--text-secondary);" @click="tagFilter='', _recomputeFlatItems()">clear</button>
|
|
</div>
|
|
|
|
<!-- Search bar (toggleable) -->
|
|
<div class="lib-search-bar" x-show="searchOpen" x-transition>
|
|
<input id="lib-search-input" type="text" placeholder="Search in this list..." x-model="searchQuery" @input="doSearch()">
|
|
</div>
|
|
|
|
<!-- ── Multi-select bar ── -->
|
|
<div class="lib-select-bar" x-show="selectedCount > 0" x-transition>
|
|
<span class="sel-count" x-text="selectedCount + ' selected'"></span>
|
|
<button @click="toggleSelectAll()" :title="allSelected ? 'Clear selection' : 'Select all'">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" x-show="!allSelected"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" x-show="allSelected" x-cloak><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
</button>
|
|
<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="copyLinks()" 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(Object.keys(selected).map(Number))" 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="more-menu" x-show="moreOpen" @click.outside="moreOpen=false" x-transition style="right:0;top:100%;">
|
|
<div class="menu-item" x-show="tab==='recents'" @click="removeFromRecents()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
|
Remove from Recents
|
|
</div>
|
|
<div class="menu-item" @click="copyLinks()">
|
|
<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(Object.keys(selected).map(Number))">
|
|
<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="deleteSelected()">
|
|
<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>
|
|
|
|
<!-- ── Loading ── -->
|
|
<div class="lib-loading" id="lib-loading" x-show="loading">
|
|
<p x-show="!noWorkspace">Loading…</p>
|
|
<div x-show="noWorkspace" style="padding:40px;text-align:center;">
|
|
<div style="margin-bottom:16px;">{{ fd_icon("folder",32) }}</div>
|
|
<h3 style="font-size:18px;margin:0 0 8px;">No workspace open</h3>
|
|
<p style="color:var(--text-dim);font-size:14px;margin:0 0 20px;">Open a workspace to browse and manage your library items.</p>
|
|
<a href="/workspaces" class="btn btn-primary" style="display:inline-block;text-decoration:none;padding:8px 20px;">Go to Workspaces</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Table ── -->
|
|
<div class="lib-table" id="lib-table" :class="{'show-author': showAuthor, 'show-source': showSource}">
|
|
<div class="lib-table-header">
|
|
<div class="th lib-col-name">
|
|
<input type="checkbox" style="width:14px;height:14px;accent-color:var(--accent);cursor:pointer;margin-left:2px;margin-right:6px;" :checked="allSelected" @click="toggleSelectAll()" title="Select / unselect all">
|
|
<svg 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"/></svg>
|
|
Page name
|
|
</div>
|
|
<div class="th lib-col-author">Created by</div>
|
|
<div class="th lib-col-source">Source</div>
|
|
<div class="th lib-col-edited">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12 16 14"/></svg>
|
|
Last edited time
|
|
</div>
|
|
</div>
|
|
<div class="lib-table-body" id="lib-table-body">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- "+ Add new" (root level) -->
|
|
<div class="add-new-row" x-show="!loading && !noWorkspace && tab !== 'favorites' && tab !== 'shared' && tab !== 'published' && tab !== 'repository'"
|
|
onclick="window.FlowDeck.createPage()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
Add new
|
|
</div>
|
|
|
|
<!-- ── Empty state ── -->
|
|
<div class="lib-empty" id="lib-empty">
|
|
<div class="empty-icon" x-html="getSvgIcon(emptyIcon,48)"></div>
|
|
<h3 x-text="emptyTitle"></h3>
|
|
<p x-text="emptyText"></p>
|
|
</div>
|
|
</div>
|
|
|
|
{% include "_ctx_menu.html" %}
|
|
|
|
<!-- ── Move-to modal ── -->
|
|
<div class="move-modal-overlay" x-show="moveModalOpen" @click.self="moveModalOpen=false" x-transition>
|
|
<div class="move-modal" @click.outside="moveModalOpen=false" x-show="moveModalOpen">
|
|
<div class="move-modal-header">Move to…</div>
|
|
<div class="move-modal-search">
|
|
<input type="text" placeholder="Search pages..." x-model="moveSearchQuery" @input="doMoveSearch()">
|
|
</div>
|
|
<div class="move-modal-list">
|
|
<div class="move-modal-item" @click="confirmMove(0)">{{ fd_icon("file",14) }} <span>Root (no parent)</span></div>
|
|
<template x-for="it in moveCandidates" :key="it.id">
|
|
<div class="move-modal-item" @click="confirmMove(it.id)">
|
|
<span x-html="_renderIcon(it)"></span> <span x-text="it.title"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Side Peek Panel ── -->
|
|
<div class="peek-overlay" :class="{ open: peekItem !== null }" @click.outside="closePeek()">
|
|
<div id="lib-peek-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>
|
|
<template x-if="peekItem">
|
|
<div style="display:flex;flex-direction:column;height:100%;">
|
|
<div class="peek-header">
|
|
<button @click="closePeek()" title="Close">
|
|
<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>
|
|
<div class="peek-title">
|
|
<span x-html="_renderIcon(peekItem)"></span>
|
|
<span x-text="peekItem.title"></span>
|
|
</div>
|
|
<button @click="openItem(peekItem)" title="Open full page">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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 @click="toggleFavoritePeek()" title="Favorite">
|
|
<svg viewBox="0 0 24 24" :fill="peekFavorited ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2"><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 @click="copyPeekLink()" title="Copy link">
|
|
<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.stop="peekMoreOpen=!peekMoreOpen" title="More">
|
|
<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="more-menu" x-show="peekMoreOpen" @click.outside="peekMoreOpen=false" style="position:absolute;top:100%;right:8px;">
|
|
<div class="menu-item" @click="duplicatePeek()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
|
|
Duplicate
|
|
</div>
|
|
<div class="menu-item" @click="movePeekTo()">
|
|
<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="deletePeek()">
|
|
<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
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="peek-body" style="flex:1;display:flex;flex-direction:column;">
|
|
<iframe id="lib-peek-iframe" :src="peekItem ? '/pages/' + peekItem.id + '?embed=1' : ''" style="width:100%;height:100%;border:none;flex:1;"></iframe>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
</div><!-- /x-data -->
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/library.js?v={{ asset_version }}"></script>
|
|
{% endblock %}
|