feat: Notion-style block editor v6 + sidebar refactoring
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:
+89
-105
@@ -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: {},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user