fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped

Corrige deux régressions de /local-workspace :

- le chemin du header restait bloqué sur « Home / <workspace> » quel que
  soit le dossier affiché : la route rend désormais breadcrumb_items
  (Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
  collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
  le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
  fois : Alpine.data('wsInitData') retournait le même objet singleton, le
  2e montage (navigation partielle) levait « Cannot redefine property:
  \ » et initTree abandonnait, laissant tout le contenu au state
  brut. La factory retourne désormais une enveloppe fraîche par montage
  qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
  exécution (le 2e montage gardait le folder_id du 1er chargement).

Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
This commit is contained in:
2026-10-09 16:58:04 -04:00
parent 7468e497c6
commit 0218d8f5e5
44 changed files with 12060 additions and 1729 deletions
+10
View File
@@ -181,6 +181,16 @@ document.addEventListener('alpine:init', function () {
return String(c.id) === id ? Object.assign({}, c, { label: d.title || 'Untitled' }) : c;
});
});
// Navigation in-place (ex. /local-workspace qui descend dans ses
// dossiers sans recharger) : le chemin du header est recalé sur la
// pile de dossiers courante. Même contrat que le rendu serveur.
window.addEventListener('flowdeck:breadcrumb-changed', function (e) {
var d = e && e.detail;
if (!d || !d.crumbs || !d.crumbs.length) return;
self.crumbs = d.crumbs.map(function (c, i) { c.origIndex = i; return c; });
if (d.workspace_id) self.wsId = d.workspace_id;
self.closeAll();
});
},
get display() {
+6
View File
@@ -1,4 +1,10 @@
<script type="application/json" id="page-data" nonce="{{ csp_nonce() }}">
{{ page_data | tojson }}
</script>
<link rel="stylesheet" href="/static/css/highlight.css?v={{ asset_version }}">
<script data-cfasync="false" src="/static/js/highlight.min.js?v={{ asset_version }}"></script>
<script data-cfasync="false" src="/static/js/highlight-extra.min.js?v={{ asset_version }}"></script>
<script data-cfasync="false" src="/static/js/code-highlight.js?v={{ asset_version }}"></script>
<script data-cfasync="false" src="/static/js/page_editor_scripts.js?v={{ asset_version }}"></script>
<link rel="stylesheet" href="/static/css/meeting.css?v={{ asset_version }}">
<script data-cfasync="false" src="/static/js/meeting_block.js?v={{ asset_version }}"></script>
+221 -25
View File
@@ -361,13 +361,28 @@
</div>
<div class="sidebar-section-actions">
{% if has_active_workspace %}
<button class="section-action-btn" title="Add" @click.stop="fdCreatePage()">+</button>
<button class="section-action-btn" title="New meeting note" @click.stop="fdCreateMeeting()">+</button>
{% endif %}
<button class="section-action-btn section-menu-btn" title="Section options" @click.stop="openSectionMenu($event, 'meetings')">⋮</button>
</div>
</div>
<div class="sidebar-section-items" x-show="sectionsOpen.meetings" x-transition>
<ul class="sidebar-items"><li class="sidebar-item empty-hint"><span class="page-icon page-icon-svg">{{ fd_icon("calendar",16) }}</span><span class="page-name text-dim">No meetings</span></li></ul>
<ul class="sidebar-items" id="sidebar-meetings-items">
<template x-for="n in meetingNotes" :key="'mn-' + n.id">
<li class="sidebar-item" @click="meetingNoteOpen(n)"
@contextmenu.prevent="showWsContext($event, n.id, n.title, false)">
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",14) }}</span>
<span style="flex:1;min-width:0;">
<span class="page-name" style="display:block;" x-text="n.title"></span>
<span class="sb-item-sub" x-text="n.has_summary ? 'Summary ready' : (n.status && n.status !== 'idle' ? n.status : 'Ready to record')"></span>
</span>
</li>
</template>
<li class="sidebar-item empty-hint" x-show="!meetingNotes.length">
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",16) }}</span>
<span class="page-name text-dim">No meetings</span>
</li>
</ul>
</div>
</div>
@@ -662,6 +677,10 @@
<div class="sb-filter-row">
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'upcoming' }" @click="meetingFilter = 'upcoming'">Upcoming</button>
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'today' }" @click="meetingFilter = 'today'">Today</button>
<button type="button" class="sb-filter-chip" :class="{ active: meetingFilter === 'past' }" @click="meetingFilter = 'past'; loadMeetingHistory()">Past</button>
</div>
<div class="sb-search-row">
<input type="search" class="sb-search-input" placeholder="Search meetings…" x-model="meetingSearch" @input="if (meetingFilter === 'past') loadMeetingHistory()" aria-label="Search meetings">
</div>
<div class="sb-quick-actions">
@@ -671,15 +690,18 @@
</div>
<ul class="sidebar-items">
<template x-for="ev in meetingVisible" :key="ev.collection_id + '-' + ev.id">
<template x-for="ev in meetingVisible" :key="(ev.collection_id || 'h') + '-' + ev.id">
<li class="sidebar-item" @click="meetingOpen(ev)">
<span class="page-icon page-icon-svg">{{ fd_icon("calendar",14) }}</span>
<span class="page-name" x-text="ev.title"></span>
<span class="sb-item-meta" x-text="ev.today ? 'Today' : ev.date"></span>
<span style="flex:1;min-width:0;">
<span class="page-name" style="display:block;" x-text="ev.title"></span>
<span class="sb-item-sub" x-text="meetingCta(ev)"></span>
</span>
<span class="sb-item-meta" x-text="ev.today ? 'Today' : (ev.date || '')"></span>
</li>
</template>
<li class="sidebar-item empty-hint" x-show="!meetingsLoading && !meetingVisible.length">
<span class="page-name text-dim" x-text="meetingFilter === 'today' ? 'Nothing scheduled today' : 'No upcoming meetings'"></span>
<span class="page-name text-dim" x-text="meetingEmptyHint()"></span>
</li>
<li class="sidebar-item empty-hint" x-show="meetingsLoading">
<span class="page-name text-dim">Loading…</span>
@@ -719,18 +741,35 @@
</ul>
</div>
<!-- Footer -->
<!-- Footer : bouton "New" + popover "Create new" (vers le haut) -->
<div class="sidebar-footer">
<div style="display:flex; gap:6px;">
<button class="sidebar-footer-btn" style="flex:1; justify-content:center; background:var(--bg-hover); border-radius:20px;">
<span style="margin-right:4px;">{{ fd_icon("message-square",14) }}</span>New chat
<span class="shortcut">Ctrl+O</span>
<div class="fd-new-wrap" @click.outside="closeNewMenu()">
<div class="fd-new-popover" x-show="newMenuOpen" x-cloak
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
role="menu" aria-label="Create new">
<div class="fd-new-pop-header">Create new</div>
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('page')">
<span>{{ fd_icon("file",16) }}</span><span>Page</span>
</button>
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('chat')">
<span>{{ fd_icon("message-square",16) }}</span><span>Chat</span>
</button>
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('meeting')">
<span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="17" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg></span><span>AI Meeting Notes</span>
</button>
<button type="button" class="fd-new-pop-item" role="menuitem" @click="newMenuAction('database')">
<span>{{ fd_icon("grid",16) }}</span><span>Database</span>
</button>
</div>
<button type="button" class="fd-new-btn" :class="{ open: newMenuOpen }"
@click="toggleNewMenu()" :aria-expanded="newMenuOpen" title="Create new">
<span>{{ fd_icon("edit",16) }}</span><span>New</span>
</button>
{% if has_active_workspace %}
<button class="sidebar-footer-btn" style="width:36px; justify-content:center; background:var(--bg-hover); border-radius:8px;" onclick="window.FlowDeck.createPage()" title="New page">
<svg width="18" height="18" 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>
{% endif %}
</div>
</div>
</div><!-- /sidebar-scroll -->
@@ -899,6 +938,9 @@
</div>
</template>
<div class="context-menu-section">
<div class="context-menu-item" @click="wsCtxAction('open')">
<span class="menu-icon">{{ fd_icon("external-link",14) }}</span> Open
</div>
<div class="context-menu-item" @click="wsCtxAction('rename')">
<span class="menu-icon">{{ fd_icon("edit",14) }}</span> Rename
</div>
@@ -1294,6 +1336,8 @@
this.loadTeamspaces();
// Badge counts on the collapsed tabs (lazy fetch, no blocking).
this.loadInbox();
// v7.1.1 : notes de réunion (section Meetings, pas Workspace).
this.loadMeetingNotes();
this.initSidebarWidth();
document.addEventListener('fd-toggle-sidebar', () => this.toggleSidebar());
this.startClipAutoRefresh();
@@ -1310,7 +1354,7 @@
});
document.addEventListener('flowdeck:workspace-changed', () => {
clearTimeout(self._clipRefreshTimer);
self._clipRefreshTimer = setTimeout(() => self.refreshSidebarTree(), 400);
self._clipRefreshTimer = setTimeout(() => { self.refreshSidebarTree(); self.loadMeetingNotes(); }, 400);
});
// Also refresh when tab becomes visible (user returns to FlowDeck after clipping)
document.addEventListener('visibilitychange', () => {
@@ -1451,15 +1495,37 @@
// ── Meeting tab ──
meetings: [],
meetingPast: [],
meetingsLoading: false,
meetingFilter: 'upcoming',
meetingSearch: '',
meetingHistoryLoaded: false,
get meetingVisible() {
if (this.meetingFilter === 'today') {
return this.meetings.filter(function(e) { return e.today; });
var q = (this.meetingSearch || '').trim().toLowerCase();
function matches(e) {
if (!q) return true;
return ((e.title || '') + ' ' + (e.date || '')).toLowerCase().indexOf(q) >= 0;
}
return this.meetings.filter(function(e) { return !e.today; });
if (this.meetingFilter === 'today') {
return this.meetings.filter(function(e) { return e.today && matches(e); });
}
if (this.meetingFilter === 'past') {
return this.meetingPast.filter(matches);
}
return this.meetings.filter(function(e) { return !e.today && matches(e); });
},
get meetingToday() { return this.meetings.filter(function(e) { return e.today; }); },
meetingEmptyHint() {
if (this.meetingFilter === 'today') return 'Nothing scheduled today';
if (this.meetingFilter === 'past') return 'No past meeting notes yet';
return 'No upcoming meetings';
},
meetingCta(ev) {
if (ev.status === 'done' || ev.summary) return 'View summary';
if (ev.today) return 'Join & transcribe';
if (ev.page_id) return 'Open note';
return 'Prepare';
},
loadMeetings(force) {
var self = this;
if (!force && this.meetings.length) return Promise.resolve();
@@ -1472,8 +1538,38 @@
})
.catch(function() { self.meetings = []; self.meetingsLoading = false; });
},
loadMeetingHistory() {
var self = this;
this.meetingsLoading = true;
var q = encodeURIComponent((this.meetingSearch || '').trim());
return fetch('/api/v2/meetings/history?query=' + q + '&limit=50', { credentials: 'same-origin' })
.then(function(r) { return r.ok ? r.json() : { meetings: [] }; })
.then(function(d) {
self.meetingPast = (d && d.meetings) || [];
self.meetingHistoryLoaded = true;
self.meetingsLoading = false;
})
.catch(function() { self.meetingPast = []; self.meetingsLoading = false; });
},
meetingOpen(ev) {
var target = ev.url || ('/db/' + ev.collection_id);
var target = ev.url || (ev.page_id ? ('/pages/' + ev.page_id) : ('/db/' + ev.collection_id));
if (ev.today) {
try { this.toast('Opening today\u2019s meeting — press Start transcribing in the note.'); } catch (e) {}
}
if (window.fdNavigate) window.fdNavigate(target);
else window.location.href = target;
},
// ── v7.1.1 : notes avec bloc AI Meeting Notes → section Meetings ──
meetingNotes: [],
loadMeetingNotes() {
var self = this;
return fetch('/api/v2/meetings/notes?limit=50', { credentials: 'same-origin' })
.then(function(r) { return r.ok ? r.json() : { notes: [] }; })
.then(function(d) { self.meetingNotes = (d && d.notes) || []; })
.catch(function() { self.meetingNotes = []; });
},
meetingNoteOpen(n) {
var target = '/pages/' + n.id;
if (window.fdNavigate) window.fdNavigate(target);
else window.location.href = target;
},
@@ -1614,6 +1710,15 @@
this.wsCtx.visible = false;
if (!id) return;
switch(action) {
case 'open':
if (this.wsCtx.isFolder) {
if (typeof this.navigateToFolder === 'function') this.navigateToFolder(id);
} else {
var target = '/pages/' + id;
if (window.fdNavigate) window.fdNavigate(target);
else window.location.href = target;
}
break;
case 'new-file':
this.newPageInFolder(id);
break;
@@ -1632,6 +1737,7 @@
if (r.ok) {
if (window.FlowDeck && window.FlowDeck.syncPageTitle) window.FlowDeck.syncPageTitle(id, newName.trim());
self.refreshSidebarTree();
if (typeof self.loadMeetingNotes === 'function') self.loadMeetingNotes();
document.dispatchEvent(new CustomEvent('flowdeck:workspace-changed'));
if (window._wsData && window._wsData._reloadAfterAction) {
window._wsData._reloadAfterAction();
@@ -1644,6 +1750,7 @@
if (!id) break;
if (!confirm('Delete "' + name + '"?')) break;
this.deleteWorkspacePage(id, true);
if (typeof this.loadMeetingNotes === 'function') this.loadMeetingNotes();
break;
}
},
@@ -1685,20 +1792,58 @@
});
},
// ── Sections collapsible ──
// ── Sections collapsible (accordéon v7.70 : une seule ouverte à la fois) ──
sectionsOpen: (function() {
var def = { recents: true, private: true, meetings: true, favorites: true, agents: true, teamspaces: true, shared: true, published: true, apps: true, workspace: true, gitea: true };
var order = ['workspace','gitea','meetings','recents','favorites','agents','teamspaces','shared','published','private','apps'];
var def = { recents: false, private: false, meetings: false, favorites: false, agents: false, teamspaces: false, shared: false, published: false, apps: false, workspace: true, gitea: false };
try {
var saved = JSON.parse(localStorage.getItem('fd_sections') || '{}');
return Object.assign(def, saved);
var merged = Object.assign({}, def, saved);
// Normalise : si plusieurs sections ouvertes (ancien état), ne garder que la première.
var first = null;
for (var i = 0; i < order.length; i++) {
if (merged[order[i]]) { first = order[i]; break; }
}
if (first) {
for (var j = 0; j < order.length; j++) merged[order[j]] = (order[j] === first);
} else {
merged.workspace = true;
}
return merged;
} catch(e) { return def; }
})(),
toggleSection(key) {
this.sectionsOpen[key] = !this.sectionsOpen[key];
var isOpen = !!this.sectionsOpen[key];
var order = ['workspace','gitea','meetings','recents','favorites','agents','teamspaces','shared','published','private','apps'];
if (isOpen) {
this.sectionsOpen[key] = false;
} else {
for (var i = 0; i < order.length; i++) this.sectionsOpen[order[i]] = false;
this.sectionsOpen[key] = true;
}
this.sectionsOpen = Object.assign({}, this.sectionsOpen);
try { localStorage.setItem('fd_sections', JSON.stringify(this.sectionsOpen)); }
catch(e) {}
},
// ── Menu "New" (popover "Create new" vers le haut) ──
newMenuOpen: false,
toggleNewMenu() { this.newMenuOpen = !this.newMenuOpen; },
closeNewMenu() { this.newMenuOpen = false; },
newMenuAction(kind) {
this.newMenuOpen = false;
if (kind === 'page') {
if (window.FlowDeck && window.FlowDeck.createPage) window.FlowDeck.createPage();
} else if (kind === 'chat') {
if (typeof this.chatNewConversation === 'function') this.chatNewConversation();
else if (typeof this.agentNew === 'function') this.agentNew();
} else if (kind === 'meeting') {
if (typeof this.fdCreateMeeting === 'function') this.fdCreateMeeting();
else if (typeof this.meetingNote === 'function') this.meetingNote();
} else if (kind === 'database') {
if (typeof this.createDatabase === 'function') this.createDatabase();
else if (window.showToast) window.showToast('Database creation: use a Gitea repo as a database.', 'info');
}
},
// ── Sidebar customization ──
customizing: false,
sidebarConfig: {},
@@ -1933,6 +2078,50 @@
});
},
fdCreatePage() { window.FlowDeck.createPage(); },
fdCreateMeeting() {
// v7.1.1 : crée une note de réunion vide avec son bloc
// AI Meeting Notes déjà inséré (même action que +, New, /meet).
var self = this;
var iso = new Date().toISOString().slice(0, 10);
var title = 'Meeting @' + iso;
fetch('/api/local-workspace/items', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
body: JSON.stringify({ name: title, type: 'page' })
})
.then(function(r) { if (!r.ok) throw new Error('page creation (' + r.status + ')'); return r.json(); })
.then(function(page) {
var block = {
id: 'b1_' + Date.now().toString(36), type: 'meeting', content: '',
meeting: {
v: 2, title: title, date: iso, state: 'idle', mode: 'mic_only',
instruction: 'auto', lang: 'fr', consent: null, consentAudio: false,
tab: 'notes', channels: [], segments: [], segSeq: 0, notes: '',
manual: '', interim: '', startedAt: 0, elapsedBase: 0, pausedMs: 0,
steps: [], summaryMd: '', completeness: '', genTitle: '',
titleEdited: false, shareHidden: false, bannerHidden: false,
transcriptId: null, error: '', remoteSilentWarned: false,
menuOpen: false, settingsOpen: false
}
};
// Amorçage du bloc : en cas d'échec la page s'ouvre quand même
// (l'utilisateur ajoute le bloc via /meet).
return fetch('/board/api/pages/' + page.id + '/blocks', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': self.getCsrfToken() },
body: JSON.stringify({ title: title, blocks: [block] })
}).then(function() { return page; }).catch(function() { return page; });
})
.then(function(page) {
if (typeof self.loadMeetingNotes === 'function') self.loadMeetingNotes();
if (typeof self.refreshSidebarTree === 'function') self.refreshSidebarTree();
if (window.fdNavigate) window.fdNavigate('/pages/' + page.id);
else window.location.href = '/pages/' + page.id;
})
.catch(function(err) { self.toast('Meeting note creation failed: ' + err.message, 'error'); });
},
fdCreateFolder() { window.FlowDeck.showCreateFolderModal(); },
fdGwRefresh() { if (window._gwData) window._gwData.refreshTree(); },
sidebarSections() {
@@ -2296,6 +2485,12 @@
const manageLink = ul.querySelector('li:last-child');
ul.innerHTML = html;
if (manageLink) ul.appendChild(manageLink);
// Le HTML injecté porte des directives Alpine (@contextmenu,
// x-show des dossiers, drag&drop) : les monter explicitement,
// sinon l'arbre perd menu contextuel et toggles après refresh.
try {
if (window.Alpine && window.Alpine.initTree) window.Alpine.initTree(ul);
} catch (e) { console.error('Sidebar Alpine remount failed:', e); }
})
.catch(e => console.error('Sidebar refresh failed:', e));
},
@@ -2526,6 +2721,7 @@
if (data.contextMenu) data.contextMenu.visible = false;
if (data.wsCtx) data.wsCtx.visible = false;
if (data.showNewPageMenu) data.showNewPageMenu = false;
if (data.newMenuOpen) data.newMenuOpen = false;
}
}
else if (e.ctrlKey && e.key === 's') {
+10 -5
View File
@@ -11,9 +11,11 @@
{% endblock %}
{% block content %}
<!-- Prism.js syntax highlighting -->
<link rel="stylesheet" href="/static/css/prism.css">
<script src="/static/js/prism.min.js"></script>
<!-- highlight.js syntax highlighting (~230 file types via FDHighlight helper) -->
<link rel="stylesheet" href="/static/css/highlight.css?v={{ asset_version }}">
<script src="/static/js/highlight.min.js?v={{ asset_version }}"></script>
<script src="/static/js/highlight-extra.min.js?v={{ asset_version }}"></script>
<script src="/static/js/code-highlight.js?v={{ asset_version }}"></script>
<style>
.gw-main{display:flex;flex-direction:column;height:calc(100vh - 48px);overflow:hidden;}
.gw-content{padding:24px;overflow-y:auto;flex:1;}
@@ -31,7 +33,7 @@
.gw-commit-bar input{flex:1;min-width:180px;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;outline:none;}
.gw-commit-bar input:focus{border-color:var(--accent);}
/* Prism theme tweaks for dark UI */
/* highlight.js theme tweaks for dark UI */
.gw-content pre[class*="language-"]{background:var(--bg-tertiary)!important;border:1px solid var(--border)!important;border-radius:8px!important;padding:16px!important;}
</style>
@@ -62,7 +64,10 @@
<div class="skeleton" style="height:15px;width:50%;"></div>
</div>
<!-- Syntax-highlighted code -->
<pre x-show="!fileLoading"><code :class="'language-'+fileLanguage" x-ref="codeBlock" x-text="fileContent"></code></pre>
<div class="gw-code-head" x-show="!fileLoading" style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<span class="gw-code-lang" x-text="fileLanguageLabel" style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-tertiary);"></span>
</div>
<pre x-show="!fileLoading"><code class="hljs" :class="'language-'+fileLanguage" :data-hljs-lang="fileLanguage" x-ref="codeBlock" x-text="fileContent"></code></pre>
</div>
</div>
+37 -8
View File
@@ -60,20 +60,38 @@
/* ── 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{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;overflow:hidden;text-overflow:ellipsis;}
.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-author{width:150px;flex-shrink:0;}
.lib-col-editedby{width:150px;flex-shrink:0;}
.lib-col-source{width:120px;flex-shrink:0;}
.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;}
.lib-col-visited{width:130px;flex-shrink:0;text-align:right;justify-content:flex-end;}
/* Visibilité des colonnes pilotée par Show columns. Ces règles doivent
battre .lib-table-header .th et .lib-row .row-cell (spécificité 0,2,0) :
avant, seul :not(.show-editedtime) (0,3,0) gagnait, donc seule Last
edited time s'affichait/masquait — les autres options semblaient mortes. */
.lib-table div.lib-col-author,
.lib-table div.lib-col-editedby,
.lib-table div.lib-col-source,
.lib-table div.lib-col-visited{display:none;}
.lib-table.show-author div.lib-col-author,
.lib-table.show-editedby div.lib-col-editedby,
.lib-table.show-source div.lib-col-source,
.lib-table.show-visited div.lib-col-visited{display:flex;}
.lib-table:not(.show-editedtime) div.lib-col-edited{display:none;}
.row-source{display:inline-flex;align-items:center;gap:5px;}
.row-source svg{width:13px;height:13px;flex-shrink:0;color:var(--text-dim);}
/* ── Row ── */
.lib-row{display:flex;align-items:center;height:34px;transition:background 0.1s;position:relative;}
.lib-row:hover{background:var(--bg-hover);}
/* ── Date section headers (AI Meeting Notes tab) ── */
.lib-section-header{display:flex;align-items:center;font-size:12px;font-weight:600;color:var(--text-secondary);padding:10px 8px 4px;user-select:none;}
.lib-section-header:first-child{padding-top:2px;}
.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;}
@@ -189,7 +207,7 @@
/* ── Responsive ── */
@media(max-width:768px){
.lib-col-author,.lib-col-source{display:none!important;}
.lib-col-author,.lib-col-editedby,.lib-col-source,.lib-col-visited{display:none!important;}
.peek-overlay{width:100vw;max-width:100vw;}
}
</style>
@@ -212,6 +230,9 @@
<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 === 'meetings' }" @click="switchTab('meetings')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg> AI Meeting Notes
</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>
@@ -265,6 +286,9 @@
<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('editedby')"><span class="check" x-text="showEditedBy ? '✓' : ''"></span> Last edited by</div>
<div class="dd-item" @click="toggleColumn('editedtime')"><span class="check" x-text="showEditedTime ? '✓' : ''"></span> Last edited time</div>
<div class="dd-item" @click="toggleColumn('visited')"><span class="check" x-text="showVisited ? '✓' : ''"></span> Last visited time</div>
<div class="dd-item" @click="toggleColumn('source')"><span class="check" x-text="showSource ? '✓' : ''"></span> Source</div>
</div>
</div>
@@ -351,7 +375,7 @@
</div>
<!-- ── Table ── -->
<div class="lib-table" id="lib-table" :class="{'show-author': showAuthor, 'show-source': showSource}">
<div class="lib-table" id="lib-table" :class="{'show-author': showAuthor, 'show-editedby': showEditedBy, 'show-editedtime': showEditedTime, 'show-visited': showVisited, '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">
@@ -359,11 +383,16 @@
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-editedby">Last edited by</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 class="th lib-col-visited">
<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>
Last visited time
</div>
<div class="th lib-col-source">Source</div>
</div>
<div class="lib-table-body" id="lib-table-body">
</div>
+16 -5
View File
@@ -166,9 +166,12 @@
.bc-edit-go:hover{opacity:.85;}
/* ── Preserved styles ── */
/* NOTE : scopées à .ws-tree (arbre principal) — la variante globale
écrasait aussi les items .ws-tree-item de la sidebar (macro partagée),
d'où un espacement différent sur /local-workspace. */
.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 .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 .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;}
@@ -179,6 +182,8 @@
.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-fav-badge{flex-shrink:0;margin-right:4px;font-size:13px;line-height:1;color:#f5a623;}
.ws-pub-badge{flex-shrink:0;margin-right:4px;font-size:13px;line-height:1;}
.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;}
@@ -480,7 +485,11 @@
.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 type="application/json" id="lw-config" nonce="{{ csp_nonce() }}">{{ {"current_folder_id": current_folder_id, "workspace_id": workspace_id, "workspace_name": workspace_name} | tojson }}</script>
<link rel="stylesheet" href="/static/css/highlight.css?v={{ asset_version }}">
<script data-cfasync="false" src="/static/js/highlight.min.js?v={{ asset_version }}"></script>
<script data-cfasync="false" src="/static/js/highlight-extra.min.js?v={{ asset_version }}"></script>
<script data-cfasync="false" src="/static/js/code-highlight.js?v={{ asset_version }}"></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>
@@ -528,7 +537,6 @@
<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>
@@ -549,7 +557,6 @@
</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;">
@@ -729,6 +736,8 @@
<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="node.favorited" class="ws-fav-badge" title="Favorite">★</span>
<span x-show="node.published" class="ws-pub-badge" title="Published to web">🌐</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>
@@ -743,6 +752,8 @@
@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="node.favorited" class="ws-fav-badge" title="Favorite">★</span>
<span x-show="node.published" class="ws-pub-badge" title="Published to web">🌐</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()"
+9
View File
@@ -173,8 +173,17 @@
</footer>
<script src="/static/js/katex.min.js?v=0.16.11"></script>
<link rel="stylesheet" href="/static/css/highlight.css?v={{ asset_version }}">
<script src="/static/js/highlight.min.js?v={{ asset_version }}"></script>
<script src="/static/js/highlight-extra.min.js?v={{ asset_version }}"></script>
<script src="/static/js/code-highlight.js?v={{ asset_version }}"></script>
<script data-cfasync="false" nonce="{{ csp_nonce() }}">
document.addEventListener('DOMContentLoaded', function () {
// hljs — syntax-highlight code blocks (~230 file types via FDHighlight).
try {
if (window.FDHighlight) FDHighlight.highlightAllIn(document.querySelector('.pub-content') || document);
else if (window.hljs) document.querySelectorAll('.pub-content pre code').forEach(function (el) { try { window.hljs.highlightElement(el); } catch (e) {} });
} catch (e) {}
if (window.katex) {
document.querySelectorAll('div[data-katex]').forEach(function (el) {
var tex = el.getAttribute('data-katex') || '';