feat: Notion sidebar complete — 7 sections, nav icons, auto-update title
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- Sidebar: 7 collapsible sections (Meetings, Recents, Favorites, Agents, Shared, Private, Notion apps)
- Nav icon row: Home with text + icon, Chat/Inbox/Search icon-only, collapse on right
- Notion apps: Library, My Tasks, Trash, Help
- Footer: New chat Ctrl+O + New page buttons
- Auto-update: sidebar items reflect title changes immediately on save
- CSS: nav-icon-btn, home-btn, collapse-btn, empty-hint, outline none
This commit is contained in:
2026-07-09 16:39:22 -04:00
parent c02e00726e
commit 780cccf79f
3 changed files with 126 additions and 55 deletions
+97 -54
View File
@@ -48,7 +48,7 @@
{% endif %}
{% endmacro %}
<!-- ═══════════ SIDEBAR Notion-style ═══════════ -->
<!-- ═══════════ SIDEBAR Notion ═══════════ -->
<aside class="sidebar" :class="{ collapsed: sidebarCollapsed }" id="sidebar"
x-data x-init="$nextTick(() => { if(typeof initTreeSortable==='function') initTreeSortable(); })">
<!-- Workspace header -->
@@ -60,18 +60,40 @@
<span class="workspace-chevron">▾</span>
</div>
<!-- Search -->
<div class="sidebar-nav">
<div class="sidebar-nav-item" @click="openQuickFind">
<span class="nav-icon">🔍</span> Search
</div>
<!-- Navigation icons row -->
<div class="sidebar-nav-row">
<a href="/" class="nav-icon-btn home-btn" :class="{ active: currentView === 'home' }" title="Home">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
Home
</a>
<button class="nav-icon-btn" title="Chat">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
</button>
<button class="nav-icon-btn" title="Inbox">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/></svg>
</button>
<button class="nav-icon-btn" @click="openQuickFind" title="Search">
<svg width="18" height="18" 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>
<button class="nav-icon-btn collapse-btn" @click="sidebarCollapsed = !sidebarCollapsed" title="Collapse sidebar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="11 17 6 12 11 7"/><polyline points="18 17 13 12 18 7"/></svg>
</button>
</div>
<!-- Settings -->
<div class="sidebar-nav">
<a href="/" class="sidebar-nav-item" :class="{ active: currentView === 'home' }">
<span class="nav-icon">🏠</span> Home
</a>
<!-- Meetings -->
<div class="sidebar-section">
<div class="sidebar-section-header" @click="toggleSection('meetings')">
<div class="sidebar-section-title">
<span class="chevron" :class="{ open: sectionsOpen.meetings }" x-text="sectionsOpen.meetings ? '▼' : '▶'"></span>
<span>Meetings</span>
</div>
<div class="sidebar-section-actions">
<button class="section-action-btn" title="Add" @click.stop="newPage('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">📅</span><span class="page-name text-dim">No meetings</span></li></ul>
</div>
</div>
<!-- Recents -->
@@ -81,9 +103,6 @@
<span class="chevron" :class="{ open: sectionsOpen.recents }" x-text="sectionsOpen.recents ? '▼' : '▶'"></span>
<span>Recents</span>
</div>
<div class="sidebar-section-actions">
<button class="section-action-btn" title="Add page" @click.stop="newPage('Recents')">+</button>
</div>
</div>
<div class="sidebar-section-items" x-show="sectionsOpen.recents" x-transition>
<ul class="sidebar-items" data-section="recents">
@@ -91,45 +110,45 @@
{{ render_tree_item(page) }}
{% endfor %}
{% if not recent_pages %}
<li class="sidebar-item">
<span class="tree-toggle-placeholder"></span>
<span class="page-icon">📄</span>
<span class="page-name text-dim">No recent pages</span>
</li>
<li class="sidebar-item empty-hint"><span class="page-icon">📄</span><span class="page-name text-dim">No recent pages</span></li>
{% endif %}
</ul>
</div>
</div>
<!-- Favorites section is optionnel ici -->
<!-- Private -->
<!-- Favorites -->
<div class="sidebar-section">
<div class="sidebar-section-header" @click="toggleSection('private')">
<div class="sidebar-section-header" @click="toggleSection('favorites')">
<div class="sidebar-section-title">
<span class="chevron" :class="{ open: sectionsOpen.private }" x-text="sectionsOpen.private ? '▼' : '▶'"></span>
<span>Private</span>
<span class="chevron" :class="{ open: sectionsOpen.favorites }" x-text="sectionsOpen.favorites ? '▼' : '▶'"></span>
<span>Favorites</span>
</div>
<div class="sidebar-section-actions">
<button class="section-action-btn" title="Add page" @click.stop="newPage('Private')">+</button>
<button class="section-action-btn" title="Add" @click.stop="newPage('Favorites')">+</button>
</div>
</div>
<div class="sidebar-section-items" x-show="sectionsOpen.private" x-transition>
<ul class="sidebar-items" data-section="private">
{% for page in private_pages %}
<div class="sidebar-section-items" x-show="sectionsOpen.favorites" x-transition>
<ul class="sidebar-items" data-section="favorites">
{% for page in favorite_pages %}
{{ render_tree_item(page) }}
{% endfor %}
{% if not private_pages %}
<li class="sidebar-item">
<span class="tree-toggle-placeholder"></span>
<span class="page-icon">📄</span>
<span class="page-name text-dim">No pages yet</span>
</li>
{% if not favorite_pages %}
<li class="sidebar-item empty-hint"><span class="page-icon">⭐</span><span class="page-name text-dim">No favorites yet</span></li>
{% endif %}
</ul>
</div>
</div>
<!-- Agents -->
<div class="sidebar-section">
<div class="sidebar-section-header" @click="toggleSection('agents')">
<div class="sidebar-section-title"><span>Agents</span></div>
</div>
<div class="sidebar-section-items" x-show="sectionsOpen.agents" x-transition>
<ul class="sidebar-items"><li class="sidebar-item empty-hint"><span class="page-icon">🤖</span><span class="page-name text-dim">No agents configured</span></li></ul>
</div>
</div>
<!-- Shared -->
<div class="sidebar-section">
<div class="sidebar-section-header" @click="toggleSection('shared')">
@@ -137,18 +156,41 @@
<span class="chevron" :class="{ open: sectionsOpen.shared }" x-text="sectionsOpen.shared ? '▼' : '▶'"></span>
<span>Shared</span>
</div>
<div class="sidebar-section-actions">
<button class="section-action-btn" title="Add" @click.stop="newPage('Shared')">+</button>
</div>
</div>
<div class="sidebar-section-items" x-show="sectionsOpen.shared" x-transition>
<ul class="sidebar-items">
<li class="sidebar-item">
<span class="page-icon">👥</span>
<span class="page-name text-dim">No shared pages</span>
</li>
<ul class="sidebar-items"><li class="sidebar-item empty-hint"><span class="page-icon">👥</span><span class="page-name text-dim">No shared pages</span></li></ul>
</div>
</div>
<!-- Private -->
<div class="sidebar-section">
<div class="sidebar-section-header" @click="toggleSection('private')">
<div class="sidebar-section-title">
<span class="chevron" :class="{ open: sectionsOpen.private }" x-text="sectionsOpen.private ? '▼' : '▶'"></span>
<span>Private</span>
</div>
<div class="sidebar-section-actions">
<button class="section-action-btn" title="View" @click.stop="">⊞</button>
<button class="section-action-btn" title="More" @click.stop="">⋯</button>
<button class="section-action-btn" title="Add page" @click.stop="newPage('Private')">+</button>
</div>
</div>
<div class="sidebar-section-items" x-show="sectionsOpen.private" x-transition>
<ul class="sidebar-items" data-section="private">
{% for page in private_pages %}
{{ render_tree_item(page) }}
{% endfor %}
{% if not private_pages %}
<li class="sidebar-item empty-hint"><span class="page-icon">📄</span><span class="page-name text-dim">No pages yet</span></li>
{% endif %}
</ul>
</div>
</div>
<!-- Notion apps -->
<!-- Notion apps / Utilities -->
<div class="sidebar-section">
<div class="sidebar-section-header" @click="toggleSection('apps')">
<div class="sidebar-section-title">
@@ -158,24 +200,25 @@
</div>
<div class="sidebar-section-items" x-show="sectionsOpen.apps" x-transition>
<ul class="sidebar-items">
<li class="sidebar-item"><span class="page-icon">📅</span><span class="page-name">Notion Calendar</span></li>
<li class="sidebar-item"><span class="page-icon">🖥️</span><span class="page-name">Notion Desktop</span></li>
<li class="sidebar-item" @click="navigateTo('/library')"><span class="page-icon">📚</span><span class="page-name">Library</span></li>
<li class="sidebar-item"><span class="page-icon">✅</span><span class="page-name">My Tasks</span></li>
<li class="sidebar-item"><span class="page-icon">🗑️</span><span class="page-name">Trash</span></li>
<li class="sidebar-item"><span class="page-icon">❓</span><span class="page-name">Help</span></li>
</ul>
</div>
</div>
<!-- Utility links -->
<div class="sidebar-utility">
<a href="/library" class="sidebar-nav-item" @click.prevent="navigateTo('/library')">
<span class="nav-icon">📚</span> Library
</a>
</div>
<!-- Footer -->
<div class="sidebar-footer">
<button class="sidebar-footer-btn" @click="newPage('Private')">
<span style="font-size:16px;">+</span> New page
</button>
<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="font-size:14px;">💬</span> New chat
<span class="shortcut">Ctrl+O</span>
</button>
<button class="sidebar-footer-btn" style="width:36px; justify-content:center; background:var(--bg-hover); border-radius:8px;" @click="newPage('Private')" 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>
</div>
<div style="display: flex; gap: 4px; margin-top: 4px;">
{% if user %}
<a href="/auth/logout" class="sidebar-footer-btn" style="flex:1; justify-content:center;">🚪</a>
@@ -341,7 +384,7 @@
showNewPageMenu: false,
// ── Sections collapsible ──
sectionsOpen: { recents: true, private: true, shared: true, apps: true },
sectionsOpen: { recents: true, private: true, meetings: true, favorites: true, agents: true, shared: true, apps: true },
toggleSection(key) { this.sectionsOpen[key] = !this.sectionsOpen[key]; this.sectionsOpen = Object.assign({}, this.sectionsOpen); },
toggleWorkspaceMenu() { this.sidebarCollapsed = !this.sidebarCollapsed; },
openQuickFind() { alert('Quick Find — Ctrl+K'); },
+4 -1
View File
@@ -234,7 +234,10 @@ function editor(pid,title,fmt,raw){
this.saving=true;
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','alt','style'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;})})})
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();}).catch(()=>{this.saving=false;});
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();
// Update sidebar items with new title
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=this.pageTitle||'New page';});
}).catch(()=>{this.saving=false;});
},
md2b(md){const bl=[];let cb=null;for(const line of md.split('\n')){if(cb){if(line.trim()==='```'){bl.push(cb);cb=null;}else cb.content+=(cb.content?'\n':'')+line;continue;}const t=line.trim();if(!t)continue;if(t.startsWith('```')){cb=this.mkB('code','',{language:t.substring(3).trim()||'Plain Text'});continue;}if(t.startsWith('# ')&&!t.startsWith('## '))bl.push(this.mkB('heading_1',t.substring(2)));else if(t.startsWith('## ')&&!t.startsWith('### '))bl.push(this.mkB('heading_2',t.substring(3)));else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));else if(/^\d+\. /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+\. /,'')));else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));else bl.push(this.mkB('paragraph',t));}if(cb)bl.push(cb);return bl;},
+25
View File
@@ -221,6 +221,31 @@ input, select, textarea { font-family: inherit; color: inherit; }
flex-shrink: 0;
}
/* Navigation icon row (Notion top bar) */
.sidebar-nav-row {
display: flex; align-items: center; gap: 3px;
padding: 6px 6px; margin: 0 2px;
}
.nav-icon-btn {
display: flex; align-items: center; gap: 8px;
padding: 6px; border-radius: 6px;
color: var(--text-secondary); transition: background 0.15s, color 0.15s;
}
.nav-icon-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.nav-icon-btn.active { background: var(--bg-active); color: var(--text-primary); }
/* Home button: icon + text */
.nav-icon-btn.home-btn {
padding: 6px 8px; gap: 8px;
font-size: 14px; font-weight: 500;
}
/* Collapse button on the right */
.nav-icon-btn.collapse-btn { margin-left: auto; }
/* Empty hint */
.sidebar-item.empty-hint { color: var(--text-dim); font-size: 12px; cursor: default; }
.sidebar-item.empty-hint:hover { background: none; }
.sidebar-item.empty-hint .page-name { font-size: 12px; }
/* Sections */
.sidebar-section {
padding: var(--space-xs) var(--space-md);