feat: Notion sidebar complete — 7 sections, nav icons, auto-update title
- 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:
+97
-54
@@ -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'); },
|
||||
|
||||
@@ -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;},
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user