feat: Notion-style block editor v6 + sidebar refactoring
FlowDeck CI / test (push) Failing after 6s
FlowDeck CI / docker (push) Has been skipped

Block editor:
- Vanilla JS rendering (renderBlock) — zero Alpine x-for on blocks
- 15 block types with Notion placeholders (Heading 1-4, Toggle list, Empty quote, etc.)
- Event delegation keydown handler on container
- sync() before every render() — content preserved across Enter/Backspace/type changes
- Slash menu with event delegation, single innerHTML set, window.E for applySlash
- Markdown shortcuts use idx (not bid) to avoid stale references after render
- data-bid only on contentEditable (not .block-wrapper) — fixes '+' artifact
- .empty class + outline:none on block-content

Sidebar:
- Notion-style collapsible sections (Recents/Private/Shared/Notion apps)
- Workspace header with chevron, Search tab, Home tab
- Sticky footer with '+ New page' button
- sectionsOpen/toggleSection/toggleWorkspaceMenu/openQuickFind in appState
- CSS: uppercase section headers, 240px width, collapsed 56px mode, border-right
This commit is contained in:
2026-07-09 15:47:43 -04:00
parent 8aa1a703b5
commit a585587e4c
3 changed files with 247 additions and 378 deletions
+89 -105
View File
@@ -48,140 +48,124 @@
{% endif %}
{% endmacro %}
<!-- ═══════════ SIDEBAR ═══════════ -->
<!-- ═══════════ SIDEBAR Notion-style ═══════════ -->
<aside class="sidebar" :class="{ collapsed: sidebarCollapsed }" id="sidebar"
x-init="$nextTick(() => initTreeSortable())">
x-data x-init="$nextTick(() => { if(typeof initTreeSortable==='function') initTreeSortable(); })">
<!-- Workspace header -->
<div class="sidebar-workspace">
<div class="sidebar-workspace" @click="toggleWorkspaceMenu">
<div class="workspace-info">
<div class="workspace-avatar">{{ workspaceInitial }}</div>
<span class="workspace-name">{{ workspaceName }}</span>
</div>
<div class="sidebar-actions">
<button class="sidebar-action-btn" @click="sidebarCollapsed = !sidebarCollapsed" title="Toggle sidebar">
«
</button>
<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>
</div>
<!-- Quick actions -->
<!-- Settings -->
<div class="sidebar-nav">
<a href="/" class="sidebar-nav-item" :class="{ active: currentView === 'home' }">
<span class="nav-icon">🏠</span> Home
</a>
</div>
<div class="sidebar-nav" style="gap: 2px; padding-right: 6px;">
<button class="sidebar-action-btn" style="width:auto; padding: 0 6px;">💬</button>
<button class="sidebar-action-btn" style="width:auto; padding: 0 6px;">🎤</button>
<button class="sidebar-action-btn" style="width:auto; padding: 0 6px;">📥</button>
<button class="sidebar-action-btn" style="width:auto; padding: 0 6px;">🔍</button>
</div>
<!-- Recents -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<div class="sidebar-section-header" @click="toggleSection('recents')">
<div class="sidebar-section-title">
<span class="chevron open">▶</span> Recents
<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="Open in Library" @click="openLibrary('Recents')">📚</button>
<button class="section-action-btn" title="Add page" @click="newPage('Recents')">+</button>
<button class="section-action-btn" title="Add page" @click.stop="newPage('Recents')">+</button>
</div>
</div>
<ul class="sidebar-items" data-section="recents">
{% for page in recent_pages %}
{{ 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>
{% endif %}
</ul>
<div class="sidebar-section-items" x-show="sectionsOpen.recents" x-transition>
<ul class="sidebar-items" data-section="recents">
{% for page in recent_pages %}
{{ 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>
{% endif %}
</ul>
</div>
</div>
<!-- Favorites: Private -->
<!-- Favorites section is optionnel ici -->
<!-- Private -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<div class="sidebar-section-header" @click="toggleSection('private')">
<div class="sidebar-section-title">
<span class="chevron open">▶</span> Private
<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="Open in Library" @click="openLibrary('Private')">📚</button>
<button class="section-action-btn" title="Add page" @click="newPage('Private')">+</button>
<button class="section-action-btn" title="Add page" @click.stop="newPage('Private')">+</button>
</div>
</div>
<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">
<span class="tree-toggle-placeholder"></span>
<span class="page-icon">📄</span>
<span class="page-name text-dim">No pages yet</span>
</li>
{% endif %}
</ul>
</div>
<!-- Notion apps -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<div class="sidebar-section-title">
<span class="chevron open">▶</span> Notion apps
</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">
<span class="tree-toggle-placeholder"></span>
<span class="page-icon">📄</span>
<span class="page-name text-dim">No pages yet</span>
</li>
{% endif %}
</ul>
</div>
<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>
</ul>
</div>
<!-- Favorites -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<div class="sidebar-section-title">
<span class="chevron open">▶</span> Favorites
</div>
<div class="sidebar-section-actions">
<button class="section-action-btn" title="Add page" @click="newPage('Favorites')">+</button>
</div>
</div>
<ul class="sidebar-items" data-section="favorites">
{% for page in favorite_pages %}
{{ render_tree_item(page) }}
{% endfor %}
{% if not favorite_pages %}
<li class="sidebar-item">
<span class="tree-toggle-placeholder"></span>
<span class="page-icon">⭐</span>
<span class="page-name text-dim">No favorites yet</span>
</li>
{% endif %}
</ul>
</div>
<!-- Shared -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<div class="sidebar-section-header" @click="toggleSection('shared')">
<div class="sidebar-section-title">
<span class="chevron">▶</span> Shared
<span class="chevron" :class="{ open: sectionsOpen.shared }" x-text="sectionsOpen.shared ? '▼' : '▶'"></span>
<span>Shared</span>
</div>
</div>
<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>
<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>
</div>
</div>
<!-- Library button -->
<div class="sidebar-nav" style="margin-top: 8px;">
<!-- Notion apps -->
<div class="sidebar-section">
<div class="sidebar-section-header" @click="toggleSection('apps')">
<div class="sidebar-section-title">
<span class="chevron" :class="{ open: sectionsOpen.apps }" x-text="sectionsOpen.apps ? '▼' : '▶'"></span>
<span>Notion apps</span>
</div>
</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>
</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>
@@ -189,22 +173,16 @@
<!-- Footer -->
<div class="sidebar-footer">
<div class="sidebar-invite" x-show="showInvite" x-transition>
<div class="sidebar-invite-title">👥 Invite members</div>
<div class="sidebar-invite-desc">Collaborate with your team.</div>
<button class="sidebar-invite-close" @click="showInvite = false">✕</button>
</div>
<button class="sidebar-new-chat">
<span>💬 New chat</span>
<span class="shortcut">Ctrl+O</span>
<button class="sidebar-footer-btn" @click="newPage('Private')">
<span style="font-size:16px;">+</span> New page
</button>
<div style="display: flex; gap: 4px; margin-top: 4px;">
{% if user %}
<a href="/auth/logout" class="sidebar-new-chat" style="flex:1; justify-content:center;">🚪</a>
<a href="/auth/logout" class="sidebar-footer-btn" style="flex:1; justify-content:center;">🚪</a>
{% else %}
<a href="/auth/login" class="sidebar-new-chat" style="flex:1; justify-content:center;">🔑</a>
<a href="/auth/login" class="sidebar-footer-btn" style="flex:1; justify-content:center;">🔑</a>
{% endif %}
<button class="sidebar-new-chat" style="width:36px; justify-content:center;" @click="toggleTheme()">🌓</button>
<button class="sidebar-footer-btn" style="width:36px; justify-content:center;" @click="toggleTheme()">🌓</button>
</div>
</div>
</aside>
@@ -362,6 +340,12 @@
contextActions: {},
showNewPageMenu: false,
// ── Sections collapsible ──
sectionsOpen: { recents: true, private: 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'); },
// ── Tree state ──
expandedNodes: {},