Merge pull request #225 from gamosoft/features/sidebar-toggle
added collapsible sidebar
This commit is contained in:
commit
98a1c18dbf
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -940,8 +940,28 @@
|
|||
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 {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
|
|
@ -1235,6 +1255,18 @@
|
|||
.mobile-sidebar-open {
|
||||
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 {
|
||||
|
|
@ -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')"
|
||||
>
|
||||
|
|
@ -1543,7 +1577,27 @@
|
|||
</button>
|
||||
|
||||
<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 -->
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@
|
|||
"sort_smallest": "小さい順(ファイルのみ)",
|
||||
"toggle_sidebar": "サイドバー切替",
|
||||
"go_to_homepage": "ホームへ",
|
||||
"hide_panel": "サイドバーを非表示",
|
||||
"show_panel": "サイドバーを表示",
|
||||
"files": "ファイル",
|
||||
"search": "検索",
|
||||
"search_title": "検索",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@
|
|||
"sort_smallest": "Сначала маленькие (только файлы)",
|
||||
"toggle_sidebar": "Показать/скрыть боковую панель",
|
||||
"go_to_homepage": "На главную",
|
||||
"hide_panel": "Скрыть боковую панель",
|
||||
"show_panel": "Показать боковую панель",
|
||||
"files": "Файлы",
|
||||
"search": "Поиск",
|
||||
"search_title": "ПОИСК",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@
|
|||
"sort_smallest": "按最小排序(仅文件)",
|
||||
"toggle_sidebar": "切换侧边栏",
|
||||
"go_to_homepage": "前往主页",
|
||||
"hide_panel": "隐藏侧边栏",
|
||||
"show_panel": "显示侧边栏",
|
||||
"files": "文件",
|
||||
"search": "搜索",
|
||||
"search_title": "搜索",
|
||||
|
|
|
|||
Loading…
Reference in New Issue