Merge pull request #225 from gamosoft/features/sidebar-toggle

added collapsible sidebar
This commit is contained in:
Guillermo Villar 2026-05-29 12:30:57 +02:00 committed by GitHub
commit 98a1c18dbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 134 additions and 11 deletions

View File

@ -119,6 +119,7 @@ To link to a heading, convert the heading text to a slug: **lowercase, spaces
### Layout
- **Resizable sidebar** - Drag to adjust width
- **Collapsible sidebar panel** - Show/hide the sidebar contents while keeping the icon rail in place
- **View mode memory** - Remembers Edit/Split/Preview preference
- **Responsive design** - Works on all screen sizes

View File

@ -42,6 +42,7 @@ const LOCAL_SETTINGS = {
tagsExpanded: { key: 'tagsExpanded', type: 'boolean', default: false },
hideUnderscoreFolders: { key: 'hideUnderscoreFolders', type: 'boolean', default: false },
tabInsertsTab: { key: 'tabInsertsTab', type: 'boolean', default: false },
sidebarPanelCollapsed: { key: 'sidebarPanelCollapsed', type: 'boolean', default: false },
// String settings
sortMode: { key: 'sortMode', type: 'string', default: 'a-z' },
// Number settings with validation
@ -273,6 +274,11 @@ function noteApp() {
lastSaved: false,
linkCopied: false,
zenMode: false,
// Hides only the .sidebar-panel content (files / search / tags / outline / etc.) while
// keeping the icon rail visible — a lighter "focus mode" than zenMode. Persisted via
// LOCAL_SETTINGS so it survives reload; hydrated by loadLocalSettings() at app init,
// which is why the initial value here matches the LOCAL_SETTINGS default.
sidebarPanelCollapsed: false,
previousViewMode: 'split',
favorites: [],
favoritesSet: new Set(), // For O(1) lookups
@ -1050,6 +1056,41 @@ function noteApp() {
localStorage.setItem('tabInsertsTab', this.tabInsertsTab);
},
// Hide / show only the sidebar PANEL (files, search, outline, etc.); the icon rail
// stays put so users can still switch panels with one click after re-expanding.
// Width is animated via CSS; sidebarWidth itself is intentionally NOT touched, so a
// user's custom drag-resized width is restored automatically on expand.
toggleSidebarPanel() {
this.sidebarPanelCollapsed = !this.sidebarPanelCollapsed;
localStorage.setItem('sidebarPanelCollapsed', this.sidebarPanelCollapsed);
},
// Switch the active sidebar panel (Files / Search / Tags / Outline / Backlinks / Shared
// / Settings) with smart toggle behaviour for the icon rail:
//
// collapsed + click any icon X → expand + switch to X
// expanded, on Y + click icon X → switch to X (no collapse change)
// expanded, on X + click icon X → collapse (symmetric "click active to close")
//
// That third rule matches the VS Code / Cursor / JetBrains / Obsidian convention where
// re-clicking the active sidebar icon hides the panel. Without it, the only way to
// collapse would be the dedicated toggle button, which is fine but leaves an asymmetry.
//
// On mobile (≤768px) the desktop collapse is CSS-overridden, so the panel is always
// visible; we deliberately skip toggling in that case so a one-off mobile tap doesn't
// silently clear the user's persisted desktop preference.
openSidebarPanel(panelName) {
const isDesktop = window.innerWidth > 768;
if (isDesktop && this.activePanel === panelName && !this.sidebarPanelCollapsed) {
this.toggleSidebarPanel();
return;
}
this.activePanel = panelName;
if (isDesktop && this.sidebarPanelCollapsed) {
this.toggleSidebarPanel();
}
},
// Handle Tab key in editor (inserts tab if setting enabled; Shift+Tab outdents matching lines)
handleTabKey(event) {
if (!this.tabInsertsTab) return;

View File

@ -940,6 +940,26 @@
flex-direction: column;
min-width: 0;
overflow: hidden;
transition: flex-basis 180ms ease, width 180ms ease, opacity 120ms ease;
}
/* Collapsed state: panel shrinks to 0 width so the editor can claim the freed space.
The icon rail (which is outside .sidebar-panel) stays visible — only the inner
file/search/outline area disappears. flex: 0 0 0 overrides the default flex: 1 so
the panel actually shrinks instead of competing for space with the rail. */
.sidebar-panel.sidebar-panel-collapsed {
flex: 0 0 0;
width: 0;
opacity: 0;
pointer-events: none;
}
/* Smooth the outer sidebar wrap so the editor gains the space gradually rather than
snapping. Mobile (≤768px) uses position: fixed + left transition for its slide-in
drawer pattern, so this width transition is desktop-only by virtue of being shadowed
there by !important rules. */
.mobile-sidebar {
transition: width 180ms ease, min-width 180ms ease, max-width 180ms ease;
}
.sidebar-panel-header {
@ -1236,6 +1256,18 @@
left: 0;
}
/* On mobile, the sidebar is an overlay drawer — the desktop "collapse panel"
feature must be disregarded. Otherwise a previously-persisted collapsed state
would carry over and the drawer would open empty (icon-rail buttons would do
nothing visible). The desktop toggle button itself is already hidden on mobile
(`hidden md:flex`), so the user has no way to undo it from here. */
.sidebar-panel.sidebar-panel-collapsed {
flex: 1;
width: auto;
opacity: 1;
pointer-events: auto;
}
/* Add bottom padding to scrollable sidebar content for tab bar */
.mobile-sidebar .overflow-y-auto {
padding-bottom: 70px !important;
@ -1406,7 +1438,9 @@
<div
class="flex mobile-sidebar zen-hide"
:class="{'mobile-sidebar-open': mobileSidebarOpen}"
:style="'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); min-width: 200px; max-width: 600px;'"
:style="sidebarPanelCollapsed
? 'width: auto; background-color: var(--bg-secondary); min-width: 0; max-width: none;'
: 'width: ' + sidebarWidth + 'px; background-color: var(--bg-secondary); min-width: 200px; max-width: 600px;'"
>
<!-- Icon Rail -->
<div class="icon-rail">
@ -1423,7 +1457,7 @@
<button
class="icon-rail-btn"
:class="{'active': activePanel === 'files'}"
@click="activePanel = 'files'"
@click="openSidebarPanel('files')"
:title="t('sidebar.files')"
:aria-label="t('sidebar.files')"
>
@ -1436,7 +1470,7 @@
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'search'}"
@click="activePanel = 'search'"
@click="openSidebarPanel('search')"
:title="t('sidebar.search')"
:aria-label="t('sidebar.search')"
>
@ -1457,7 +1491,7 @@
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'tags'}"
@click="activePanel = 'tags'"
@click="openSidebarPanel('tags')"
:title="t('tags.title')"
:aria-label="t('tags.title')"
>
@ -1478,7 +1512,7 @@
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'outline'}"
@click="activePanel = 'outline'"
@click="openSidebarPanel('outline')"
:title="t('outline.title')"
:aria-label="t('outline.title')"
>
@ -1499,7 +1533,7 @@
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'backlinks'}"
@click="activePanel = 'backlinks'"
@click="openSidebarPanel('backlinks')"
:title="t('backlinks.title')"
:aria-label="t('backlinks.title')"
>
@ -1526,7 +1560,7 @@
<button
class="icon-rail-btn relative"
:class="{'active': activePanel === 'shared'}"
@click="activePanel = 'shared'"
@click="openSidebarPanel('shared')"
:title="t('share.panel_title')"
:aria-label="t('share.panel_title')"
>
@ -1544,6 +1578,26 @@
<div class="icon-rail-spacer"></div>
<!-- Toggle sidebar panel (hides files/search/outline/etc.; icon rail stays).
Hidden on small screens where the sidebar is an overlay drawer instead. -->
<button
class="icon-rail-btn hidden md:flex"
@click="toggleSidebarPanel()"
:title="t(sidebarPanelCollapsed ? 'sidebar.show_panel' : 'sidebar.hide_panel')"
:aria-label="t(sidebarPanelCollapsed ? 'sidebar.show_panel' : 'sidebar.hide_panel')"
>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<rect x="3" y="4" width="18" height="16" rx="2" stroke-width="2"></rect>
<line x1="9" y1="4" x2="9" y2="20" stroke-width="2"></line>
<!-- Chevron flips: points LEFT when expanded ("click to collapse"),
RIGHT when collapsed ("click to expand"). -->
<path
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
:d="sidebarPanelCollapsed ? 'M13 9 L16 12 L13 15' : 'M16 9 L13 12 L16 15'"
></path>
</svg>
</button>
<!-- Graph -->
<button
class="icon-rail-btn"
@ -1561,7 +1615,7 @@
<button
class="icon-rail-btn"
:class="{'active': activePanel === 'settings'}"
@click="activePanel = 'settings'"
@click="openSidebarPanel('settings')"
:title="t('sidebar.settings')"
:aria-label="t('sidebar.settings')"
>
@ -1573,7 +1627,11 @@
</div>
<!-- Sidebar Panel -->
<div class="sidebar-panel" style="border-right: 1px solid var(--border-primary);">
<div
class="sidebar-panel"
:class="{'sidebar-panel-collapsed': sidebarPanelCollapsed}"
style="border-right: 1px solid var(--border-primary);"
>
<!-- ==================== FILES PANEL ==================== -->
<div x-show="activePanel === 'files'" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="flex flex-col h-full">
@ -2255,6 +2313,7 @@
style="width: 4px; cursor: col-resize; background-color: transparent; transition: background-color 0.2s;"
@mouseover="$el.style.backgroundColor='var(--accent-primary)'"
@mouseout="if(!isResizing) $el.style.backgroundColor='transparent'"
x-show="!sidebarPanelCollapsed"
></div>
<!-- Main Content Area -->

View File

@ -62,6 +62,8 @@
"sort_smallest": "Nach kleinsten sortieren (nur Dateien)",
"toggle_sidebar": "Seitenleiste umschalten",
"go_to_homepage": "Zur Startseite",
"hide_panel": "Seitenleiste ausblenden",
"show_panel": "Seitenleiste einblenden",
"files": "Dateien",
"search": "Suchen",
"search_title": "SUCHE",

View File

@ -61,6 +61,8 @@
"sort_smallest": "Sort by smallest (files only)",
"toggle_sidebar": "Toggle sidebar",
"go_to_homepage": "Go to homepage",
"hide_panel": "Hide sidebar panel",
"show_panel": "Show sidebar panel",
"files": "Files",
"search": "Search",
"search_title": "SEARCH",

View File

@ -62,6 +62,8 @@
"sort_smallest": "Sort by smallest (files only)",
"toggle_sidebar": "Toggle sidebar",
"go_to_homepage": "Go to homepage",
"hide_panel": "Hide sidebar panel",
"show_panel": "Show sidebar panel",
"files": "Files",
"search": "Search",
"search_title": "SEARCH",

View File

@ -62,6 +62,8 @@
"sort_smallest": "Ordenar por más pequeño (solo archivos)",
"toggle_sidebar": "Alternar barra lateral",
"go_to_homepage": "Ir al inicio",
"hide_panel": "Ocultar el panel lateral",
"show_panel": "Mostrar el panel lateral",
"files": "Archivos",
"search": "Buscar",
"search_title": "BÚSQUEDA",

View File

@ -62,6 +62,8 @@
"sort_smallest": "Trier par plus petit (fichiers uniquement)",
"toggle_sidebar": "Afficher/Masquer la barre latérale",
"go_to_homepage": "Aller à l'accueil",
"hide_panel": "Masquer le panneau latéral",
"show_panel": "Afficher le panneau latéral",
"files": "Fichiers",
"search": "Rechercher",
"search_title": "RECHERCHE",

View File

@ -62,6 +62,8 @@
"sort_smallest": "Legkisebb elöl (csak fájlok)",
"toggle_sidebar": "Oldalsáv megjelenítése",
"go_to_homepage": "Ugrás a kezdőlapra",
"hide_panel": "Oldalsáv elrejtése",
"show_panel": "Oldalsáv megjelenítése",
"files": "Fájlok",
"search": "Keresés",
"search_title": "KERESÉS",

View File

@ -61,6 +61,8 @@
"sort_smallest": "Ordina per più piccolo (solo file)",
"toggle_sidebar": "Mostra/nascondi barra laterale",
"go_to_homepage": "Vai alla home",
"hide_panel": "Nascondi il pannello laterale",
"show_panel": "Mostra il pannello laterale",
"files": "File",
"search": "Cerca",
"search_title": "CERCA",

View File

@ -61,6 +61,8 @@
"sort_smallest": "小さい順(ファイルのみ)",
"toggle_sidebar": "サイドバー切替",
"go_to_homepage": "ホームへ",
"hide_panel": "サイドバーを非表示",
"show_panel": "サイドバーを表示",
"files": "ファイル",
"search": "検索",
"search_title": "検索",

View File

@ -61,6 +61,8 @@
"sort_smallest": "Сначала маленькие (только файлы)",
"toggle_sidebar": "Показать/скрыть боковую панель",
"go_to_homepage": "На главную",
"hide_panel": "Скрыть боковую панель",
"show_panel": "Показать боковую панель",
"files": "Файлы",
"search": "Поиск",
"search_title": "ПОИСК",

View File

@ -61,6 +61,8 @@
"sort_smallest": "Razvrsti po najmanjših (samo datoteke)",
"toggle_sidebar": "Preklopi stransko vrstico",
"go_to_homepage": "Pojdi na začetno stran",
"hide_panel": "Skrij stranski pano",
"show_panel": "Pokaži stranski pano",
"files": "Datoteke",
"search": "Iskanje",
"search_title": "ISKANJE",

View File

@ -61,6 +61,8 @@
"sort_smallest": "按最小排序(仅文件)",
"toggle_sidebar": "切换侧边栏",
"go_to_homepage": "前往主页",
"hide_panel": "隐藏侧边栏",
"show_panel": "显示侧边栏",
"files": "文件",
"search": "搜索",
"search_title": "搜索",