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 ### Layout
- **Resizable sidebar** - Drag to adjust width - **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 - **View mode memory** - Remembers Edit/Split/Preview preference
- **Responsive design** - Works on all screen sizes - **Responsive design** - Works on all screen sizes

View File

@ -42,6 +42,7 @@ const LOCAL_SETTINGS = {
tagsExpanded: { key: 'tagsExpanded', type: 'boolean', default: false }, tagsExpanded: { key: 'tagsExpanded', type: 'boolean', default: false },
hideUnderscoreFolders: { key: 'hideUnderscoreFolders', type: 'boolean', default: false }, hideUnderscoreFolders: { key: 'hideUnderscoreFolders', type: 'boolean', default: false },
tabInsertsTab: { key: 'tabInsertsTab', type: 'boolean', default: false }, tabInsertsTab: { key: 'tabInsertsTab', type: 'boolean', default: false },
sidebarPanelCollapsed: { key: 'sidebarPanelCollapsed', type: 'boolean', default: false },
// String settings // String settings
sortMode: { key: 'sortMode', type: 'string', default: 'a-z' }, sortMode: { key: 'sortMode', type: 'string', default: 'a-z' },
// Number settings with validation // Number settings with validation
@ -273,6 +274,11 @@ function noteApp() {
lastSaved: false, lastSaved: false,
linkCopied: false, linkCopied: false,
zenMode: 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', previousViewMode: 'split',
favorites: [], favorites: [],
favoritesSet: new Set(), // For O(1) lookups favoritesSet: new Set(), // For O(1) lookups
@ -1050,6 +1056,41 @@ function noteApp() {
localStorage.setItem('tabInsertsTab', this.tabInsertsTab); 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) // Handle Tab key in editor (inserts tab if setting enabled; Shift+Tab outdents matching lines)
handleTabKey(event) { handleTabKey(event) {
if (!this.tabInsertsTab) return; if (!this.tabInsertsTab) return;

View File

@ -940,6 +940,26 @@
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
overflow: hidden; 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 { .sidebar-panel-header {
@ -1236,6 +1256,18 @@
left: 0; 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 */ /* Add bottom padding to scrollable sidebar content for tab bar */
.mobile-sidebar .overflow-y-auto { .mobile-sidebar .overflow-y-auto {
padding-bottom: 70px !important; padding-bottom: 70px !important;
@ -1406,7 +1438,9 @@
<div <div
class="flex mobile-sidebar zen-hide" class="flex mobile-sidebar zen-hide"
:class="{'mobile-sidebar-open': mobileSidebarOpen}" :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 --> <!-- Icon Rail -->
<div class="icon-rail"> <div class="icon-rail">
@ -1423,7 +1457,7 @@
<button <button
class="icon-rail-btn" class="icon-rail-btn"
:class="{'active': activePanel === 'files'}" :class="{'active': activePanel === 'files'}"
@click="activePanel = 'files'" @click="openSidebarPanel('files')"
:title="t('sidebar.files')" :title="t('sidebar.files')"
:aria-label="t('sidebar.files')" :aria-label="t('sidebar.files')"
> >
@ -1436,7 +1470,7 @@
<button <button
class="icon-rail-btn relative" class="icon-rail-btn relative"
:class="{'active': activePanel === 'search'}" :class="{'active': activePanel === 'search'}"
@click="activePanel = 'search'" @click="openSidebarPanel('search')"
:title="t('sidebar.search')" :title="t('sidebar.search')"
:aria-label="t('sidebar.search')" :aria-label="t('sidebar.search')"
> >
@ -1457,7 +1491,7 @@
<button <button
class="icon-rail-btn relative" class="icon-rail-btn relative"
:class="{'active': activePanel === 'tags'}" :class="{'active': activePanel === 'tags'}"
@click="activePanel = 'tags'" @click="openSidebarPanel('tags')"
:title="t('tags.title')" :title="t('tags.title')"
:aria-label="t('tags.title')" :aria-label="t('tags.title')"
> >
@ -1478,7 +1512,7 @@
<button <button
class="icon-rail-btn relative" class="icon-rail-btn relative"
:class="{'active': activePanel === 'outline'}" :class="{'active': activePanel === 'outline'}"
@click="activePanel = 'outline'" @click="openSidebarPanel('outline')"
:title="t('outline.title')" :title="t('outline.title')"
:aria-label="t('outline.title')" :aria-label="t('outline.title')"
> >
@ -1499,7 +1533,7 @@
<button <button
class="icon-rail-btn relative" class="icon-rail-btn relative"
:class="{'active': activePanel === 'backlinks'}" :class="{'active': activePanel === 'backlinks'}"
@click="activePanel = 'backlinks'" @click="openSidebarPanel('backlinks')"
:title="t('backlinks.title')" :title="t('backlinks.title')"
:aria-label="t('backlinks.title')" :aria-label="t('backlinks.title')"
> >
@ -1526,7 +1560,7 @@
<button <button
class="icon-rail-btn relative" class="icon-rail-btn relative"
:class="{'active': activePanel === 'shared'}" :class="{'active': activePanel === 'shared'}"
@click="activePanel = 'shared'" @click="openSidebarPanel('shared')"
:title="t('share.panel_title')" :title="t('share.panel_title')"
:aria-label="t('share.panel_title')" :aria-label="t('share.panel_title')"
> >
@ -1544,6 +1578,26 @@
<div class="icon-rail-spacer"></div> <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 --> <!-- Graph -->
<button <button
class="icon-rail-btn" class="icon-rail-btn"
@ -1561,7 +1615,7 @@
<button <button
class="icon-rail-btn" class="icon-rail-btn"
:class="{'active': activePanel === 'settings'}" :class="{'active': activePanel === 'settings'}"
@click="activePanel = 'settings'" @click="openSidebarPanel('settings')"
:title="t('sidebar.settings')" :title="t('sidebar.settings')"
:aria-label="t('sidebar.settings')" :aria-label="t('sidebar.settings')"
> >
@ -1573,7 +1627,11 @@
</div> </div>
<!-- Sidebar Panel --> <!-- 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 ==================== --> <!-- ==================== 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"> <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;" style="width: 4px; cursor: col-resize; background-color: transparent; transition: background-color 0.2s;"
@mouseover="$el.style.backgroundColor='var(--accent-primary)'" @mouseover="$el.style.backgroundColor='var(--accent-primary)'"
@mouseout="if(!isResizing) $el.style.backgroundColor='transparent'" @mouseout="if(!isResizing) $el.style.backgroundColor='transparent'"
x-show="!sidebarPanelCollapsed"
></div> ></div>
<!-- Main Content Area --> <!-- Main Content Area -->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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