feat(sidebar): Notion-style collapse/peek, desktop reopen button, Ctrl+\\ toggle
Stop collapsing the sidebar on navigation (was hiding the nav when opening Shared pages). Show the hamburger in the top-left on desktop when collapsed, add a left-edge hover zone that floats the sidebar in and auto-collapses on mouse leave, and bind Ctrl/Cmd+\\ to toggle it.
This commit is contained in:
@@ -36,7 +36,8 @@
|
|||||||
<header class="topbar unified-header">
|
<header class="topbar unified-header">
|
||||||
{% if not hide_hamburger %}
|
{% if not hide_hamburger %}
|
||||||
<button class="hamburger-btn"
|
<button class="hamburger-btn"
|
||||||
@click="{{ hamburger_click|default('sidebarCollapsed ? (sidebarCollapsed = false) : (mobileSidebarOpen = true, sidebarCollapsed = false)') }}"
|
:class="{ 'hamburger-desktop-show': sidebarCollapsed }"
|
||||||
|
@click="{{ hamburger_click|default('sidebarCollapsed ? toggleSidebar() : (mobileSidebarOpen = true, sidebarCollapsed = false)') }}"
|
||||||
title="Toggle sidebar">
|
title="Toggle sidebar">
|
||||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||||
|
|||||||
+19
-6
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>FlowDeck — {% block title_prefix %}Home{% endblock %}</title>
|
<title>FlowDeck — {% block title_prefix %}Home{% endblock %}</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||||
<link rel="stylesheet" href="/static/css/app.css?v=5.0.0">
|
<link rel="stylesheet" href="/static/css/app.css?v=5.0.1">
|
||||||
<link rel="stylesheet" href="/static/css/design-tokens.css?v=5.2.0">
|
<link rel="stylesheet" href="/static/css/design-tokens.css?v=5.2.0">
|
||||||
<link rel="stylesheet" href="/static/css/components.css?v=5.2.0">
|
<link rel="stylesheet" href="/static/css/components.css?v=5.2.0">
|
||||||
<link rel="stylesheet" href="/static/css/katex.min.css?v=0.16.11">
|
<link rel="stylesheet" href="/static/css/katex.min.css?v=0.16.11">
|
||||||
@@ -118,9 +118,14 @@
|
|||||||
<!-- ═══════════ MOBILE SIDEBAR OVERLAY ═══════════ -->
|
<!-- ═══════════ MOBILE SIDEBAR OVERLAY ═══════════ -->
|
||||||
<div class="sidebar-overlay"
|
<div class="sidebar-overlay"
|
||||||
:class="{ visible: mobileSidebarOpen }"
|
:class="{ visible: mobileSidebarOpen }"
|
||||||
@click="mobileSidebarOpen = false, sidebarCollapsed = true">
|
@click="mobileSidebarOpen = false">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════ SIDEBAR PEEK ZONE (collapsed, desktop) ═══════════ -->
|
||||||
|
<div class="sidebar-peek-zone"
|
||||||
|
x-show="sidebarCollapsed" x-cloak
|
||||||
|
@mouseenter="sidebarPeek = true"></div>
|
||||||
|
|
||||||
{# ─── Recursive tree macro ─── #}
|
{# ─── Recursive tree macro ─── #}
|
||||||
{% macro render_tree_item(page, depth=0) %}
|
{% macro render_tree_item(page, depth=0) %}
|
||||||
{% set indent_px = depth * 16 %}
|
{% set indent_px = depth * 16 %}
|
||||||
@@ -163,8 +168,9 @@
|
|||||||
|
|
||||||
<!-- ═══════════ SIDEBAR Notion ═══════════ -->
|
<!-- ═══════════ SIDEBAR Notion ═══════════ -->
|
||||||
<aside class="sidebar"
|
<aside class="sidebar"
|
||||||
:class="{ collapsed: sidebarCollapsed, 'mobile-open': mobileSidebarOpen }"
|
:class="{ collapsed: sidebarCollapsed && !sidebarPeek, 'mobile-open': mobileSidebarOpen, peeking: sidebarPeek }"
|
||||||
id="sidebar"
|
id="sidebar"
|
||||||
|
@mouseleave="sidebarPeek = false"
|
||||||
x-init="$nextTick(() => { if(typeof initTreeSortable==='function') initTreeSortable(); })">
|
x-init="$nextTick(() => { if(typeof initTreeSortable==='function') initTreeSortable(); })">
|
||||||
<!-- Header + dropdown wrapper -->
|
<!-- Header + dropdown wrapper -->
|
||||||
<div style="position:relative;">
|
<div style="position:relative;">
|
||||||
@@ -180,7 +186,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<span class="workspace-chevron" :class="{ open: workspaceMenuOpen }">▾</span>
|
<span class="workspace-chevron" :class="{ open: workspaceMenuOpen }">▾</span>
|
||||||
<button class="sidebar-collapse-inline" @click.stop="sidebarCollapsed = !sidebarCollapsed" title="Collapse sidebar">
|
<button class="sidebar-collapse-inline" @click.stop="toggleSidebar()" title="Collapse sidebar">
|
||||||
<svg width="14" height="14" 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>
|
<svg width="14" height="14" 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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -963,6 +969,7 @@
|
|||||||
window.appState = this;
|
window.appState = this;
|
||||||
this.loadSidebarConfig();
|
this.loadSidebarConfig();
|
||||||
this.loadAgents();
|
this.loadAgents();
|
||||||
|
document.addEventListener('fd-toggle-sidebar', () => this.toggleSidebar());
|
||||||
},
|
},
|
||||||
agentList: [],
|
agentList: [],
|
||||||
loadAgents() {
|
loadAgents() {
|
||||||
@@ -980,6 +987,7 @@
|
|||||||
else { document.dispatchEvent(new CustomEvent('fd-agent-toggle')); }
|
else { document.dispatchEvent(new CustomEvent('fd-agent-toggle')); }
|
||||||
},
|
},
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
|
sidebarPeek: false,
|
||||||
mobileSidebarOpen: false,
|
mobileSidebarOpen: false,
|
||||||
showInvite: true,
|
showInvite: true,
|
||||||
// -- Toast notifications --
|
// -- Toast notifications --
|
||||||
@@ -1325,7 +1333,7 @@
|
|||||||
|
|
||||||
navigateTo(url) {
|
navigateTo(url) {
|
||||||
this.mobileSidebarOpen = false;
|
this.mobileSidebarOpen = false;
|
||||||
this.sidebarCollapsed = true;
|
this.sidebarPeek = false;
|
||||||
// Preserve workspace context when navigating to library/trash
|
// Preserve workspace context when navigating to library/trash
|
||||||
if ((url === '/library' || url === '/trash') && this.workspaceKey && this.workspaceKey.includes('/')) {
|
if ((url === '/library' || url === '/trash') && this.workspaceKey && this.workspaceKey.includes('/')) {
|
||||||
const parts = this.workspaceKey.split('/');
|
const parts = this.workspaceKey.split('/');
|
||||||
@@ -1335,6 +1343,11 @@
|
|||||||
else window.location.href = url;
|
else window.location.href = url;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleSidebar() {
|
||||||
|
this.sidebarPeek = false;
|
||||||
|
this.sidebarCollapsed = !this.sidebarCollapsed;
|
||||||
|
},
|
||||||
|
|
||||||
showContextMenu(event, pageId, pageName) {
|
showContextMenu(event, pageId, pageName) {
|
||||||
var x = event.clientX, y = event.clientY;
|
var x = event.clientX, y = event.clientY;
|
||||||
// Prevent menu from overflowing right edge
|
// Prevent menu from overflowing right edge
|
||||||
@@ -1842,7 +1855,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/js/app.js?v=2.4.7" defer></script>
|
<script src="/static/js/app.js?v=2.4.8" defer></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.flowdeck-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:2000;display:flex;align-items:center;justify-content:center;}
|
.flowdeck-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:2000;display:flex;align-items:center;justify-content:center;}
|
||||||
|
|||||||
@@ -166,6 +166,31 @@ input, select, textarea { font-family: inherit; color: inherit; }
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Notion-style collapsed peek (hover the left edge) ── */
|
||||||
|
.sidebar-peek-zone {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 8px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.sidebar.peeking {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
min-width: var(--sidebar-width);
|
||||||
|
z-index: 1001;
|
||||||
|
box-shadow: 4px 0 24px rgba(0, 0, 0, .35);
|
||||||
|
animation: fd-sidebar-peek-in .16s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes fd-sidebar-peek-in {
|
||||||
|
from { transform: translateX(-100%); }
|
||||||
|
to { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
/* Workspace header — Notion style */
|
/* Workspace header — Notion style */
|
||||||
.sidebar-workspace-header {
|
.sidebar-workspace-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -3530,6 +3555,12 @@ button.breadcrumb-current { max-width: 260px; }
|
|||||||
}
|
}
|
||||||
.hamburger-btn:hover { background: var(--bg-hover); }
|
.hamburger-btn:hover { background: var(--bg-hover); }
|
||||||
|
|
||||||
|
/* Show the hamburger on desktop only when the sidebar is collapsed,
|
||||||
|
so it can be reopened from the top-left corner. */
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.hamburger-btn.hamburger-desktop-show { display: flex; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Mobile Sidebar Overlay ── */
|
/* ── Mobile Sidebar Overlay ── */
|
||||||
.sidebar-overlay {
|
.sidebar-overlay {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -3611,6 +3642,12 @@ button.breadcrumb-current { max-width: 260px; }
|
|||||||
}
|
}
|
||||||
.sidebar-back-btn { display: flex; }
|
.sidebar-back-btn { display: flex; }
|
||||||
.sidebar-uncollapse-btn { display: none !important; }
|
.sidebar-uncollapse-btn { display: none !important; }
|
||||||
|
.sidebar-peek-zone { display: none; }
|
||||||
|
.sidebar.peeking {
|
||||||
|
width: 85vw;
|
||||||
|
max-width: 320px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Topbar ── */
|
/* ── Topbar ── */
|
||||||
.topbar {
|
.topbar {
|
||||||
|
|||||||
+9
-1
@@ -325,6 +325,14 @@
|
|||||||
fdLoad(window.location.href, false);
|
fdLoad(window.location.href, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ctrl/Cmd + \ : toggle the sidebar (Notion-style).
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && (e.key === '\\' || e.code === 'Backslash')) {
|
||||||
|
e.preventDefault();
|
||||||
|
document.dispatchEvent(new CustomEvent('fd-toggle-sidebar'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Programmatic navigation used by Alpine helpers.
|
// Programmatic navigation used by Alpine helpers.
|
||||||
window.fdNavigate = function(url, opts) {
|
window.fdNavigate = function(url, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
@@ -379,7 +387,7 @@
|
|||||||
var doc = new DOMParser().parseFromString(d.serverResponse, 'text/html');
|
var doc = new DOMParser().parseFromString(d.serverResponse, 'text/html');
|
||||||
var mw = doc.querySelector('.main-wrapper');
|
var mw = doc.querySelector('.main-wrapper');
|
||||||
if (mw) d.serverResponse = mw.outerHTML;
|
if (mw) d.serverResponse = mw.outerHTML;
|
||||||
} catch (e) { /* keep original response on parse failure */ }
|
} catch { /* keep original response on parse failure */ }
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||||
|
|||||||
Reference in New Issue
Block a user