diff --git a/frontend/src/components/MailApp.jsx b/frontend/src/components/MailApp.jsx
index 8e2df0e8..5d8dca4b 100644
--- a/frontend/src/components/MailApp.jsx
+++ b/frontend/src/components/MailApp.jsx
@@ -9,6 +9,7 @@ import { updateFaviconBadge } from '../themes.js';
import { shortcutBus } from '../utils/shortcutBus.js';
import { setPending, pendingMarkReadMap, completedMarkReadMap } from '../utils/pendingReads.js';
import { buildKeyMap, buildModKeyMap, getEffectiveShortcuts, getGroupedActions, parseModKey, modLabel, SPECIAL_KEYS, SPECIAL_KEY_LABELS } from '../utils/defaultShortcuts.js';
+import { sidebarTransition } from '../utils/sidebarTransition.js';
import Sidebar from './Sidebar.jsx';
import MessageList from './MessageList.jsx';
import MessagePane from './MessagePane.jsx';
@@ -61,7 +62,7 @@ export default function MailApp() {
const { t } = useTranslation();
const {
setAccounts, setUnreadCounts, showAdmin,
- setShowAdmin, setAdminTab, composing, sidebarCollapsed, layout,
+ setShowAdmin, setAdminTab, composing, sidebarCollapsed, toggleSidebar, layout,
unreadCounts, selectedAccountId, openCompose, setSelectedAccount,
shortcuts, selectedMessageId, setSelectedMessage,
mobileSidebarOpen, setMobileSidebarOpen, addNotification,
@@ -643,6 +644,13 @@ export default function MailApp() {
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
+ // Whole left-sidebar collapse toggle (cmd+\). Mobile keeps its drawer-specific controls.
+ useEffect(() => {
+ const onToggleLeftSidebar = () => { if (!isMobile) toggleSidebar(); };
+ shortcutBus.on('toggleLeftSidebar', onToggleLeftSidebar);
+ return () => shortcutBus.off('toggleLeftSidebar', onToggleLeftSidebar);
+ }, [isMobile, toggleSidebar]);
+
// Whole right-sidebar collapse toggle (cmd+/). Re-subscribed when applicability
// flips so the handler never toggles a sidebar that is not rendered.
useEffect(() => {
@@ -832,12 +840,12 @@ export default function MailApp() {
// Disabled while dragging (mirrors Sidebar's isSidebarResizing guard):
// otherwise every mousemove's CSS-var write would animate toward the new
// width instead of tracking the cursor.
- transition: isRightSidebarResizing ? 'none' : 'width 0.2s ease',
+ transition: isRightSidebarResizing ? 'none' : sidebarTransition('width'),
}}>
{rightSidebarContent}
diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx
index aac0fe62..80f0e358 100644
--- a/frontend/src/components/Sidebar.jsx
+++ b/frontend/src/components/Sidebar.jsx
@@ -10,6 +10,7 @@ import {
folderDropPosition,
resolveFolderOrderDrop,
} from '../utils/sidebar.js';
+import { sidebarTransition } from '../utils/sidebarTransition.js';
import { useMobile } from '../hooks/useMobile.js';
import LogoMark from './LogoMark.jsx';
import ProfileModal from './ProfileModal.jsx';
@@ -794,7 +795,7 @@ export default function Sidebar() {
borderRight: '1px solid var(--border-subtle)',
display: 'flex',
flexDirection: 'column',
- transition: isSidebarResizing ? 'none' : 'width 0.2s ease, min-width 0.2s ease',
+ transition: isSidebarResizing ? 'none' : sidebarTransition('width', 'min-width'),
overflow: 'hidden',
}}>
{/* Header */}
diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json
index 82f671bd..0ab4b6a6 100644
--- a/frontend/src/locales/de.json
+++ b/frontend/src/locales/de.json
@@ -551,6 +551,10 @@
"label": "Als Delegiert markieren",
"description": "Die ausgewählte Nachricht als Delegiert einordnen"
},
+ "toggleLeftSidebar": {
+ "label": "Linke Seitenleiste umschalten",
+ "description": "Linke Seitenleiste aus- oder einblenden"
+ },
"toggleRightSidebar": {
"label": "Rechte Seitenleiste umschalten",
"description": "Rechte Seitenleiste aus- oder einblenden"
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index a38e68cf..72b32e19 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -551,6 +551,10 @@
"label": "Mark Delegated",
"description": "Classify the selected message as Delegated"
},
+ "toggleLeftSidebar": {
+ "label": "Toggle left sidebar",
+ "description": "Collapse or expand the left sidebar"
+ },
"toggleRightSidebar": {
"label": "Toggle right sidebar",
"description": "Hide or show the right sidebar"
diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json
index 3f272fa3..56414d53 100644
--- a/frontend/src/locales/es.json
+++ b/frontend/src/locales/es.json
@@ -551,6 +551,10 @@
"label": "Marcar Delegado",
"description": "Clasificar el mensaje seleccionado como Delegado"
},
+ "toggleLeftSidebar": {
+ "label": "Alternar la barra lateral izquierda",
+ "description": "Contraer o expandir la barra lateral izquierda"
+ },
"toggleRightSidebar": {
"label": "Alternar la barra lateral derecha",
"description": "Ocultar o mostrar la barra lateral derecha"
diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json
index edfb7754..c80fb772 100644
--- a/frontend/src/locales/fr.json
+++ b/frontend/src/locales/fr.json
@@ -551,6 +551,10 @@
"label": "Marquer Délégué",
"description": "Classer le message sélectionné comme Délégué"
},
+ "toggleLeftSidebar": {
+ "label": "Afficher ou masquer la barre latérale gauche",
+ "description": "Réduire ou développer la barre latérale gauche"
+ },
"toggleRightSidebar": {
"label": "Afficher ou masquer la barre latérale droite",
"description": "Masquer ou afficher la barre latérale droite"
diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json
index 0e785726..9e431cc3 100644
--- a/frontend/src/locales/it.json
+++ b/frontend/src/locales/it.json
@@ -551,6 +551,10 @@
"label": "Segna Delegato",
"description": "Classifica il messaggio selezionato come Delegato"
},
+ "toggleLeftSidebar": {
+ "label": "Mostra o nascondi la barra laterale sinistra",
+ "description": "Comprimi o espandi la barra laterale sinistra"
+ },
"toggleRightSidebar": {
"label": "Mostra o nascondi la barra laterale destra",
"description": "Nascondi o mostra la barra laterale destra"
diff --git a/frontend/src/locales/ru.json b/frontend/src/locales/ru.json
index 237b417e..eb638dab 100644
--- a/frontend/src/locales/ru.json
+++ b/frontend/src/locales/ru.json
@@ -551,6 +551,10 @@
"label": "Отметить как «Делегировано»",
"description": "Отнести выбранное сообщение к «Делегировано»"
},
+ "toggleLeftSidebar": {
+ "label": "Переключить левую боковую панель",
+ "description": "Свернуть или развернуть левую боковую панель"
+ },
"toggleRightSidebar": {
"label": "Переключить правую боковую панель",
"description": "Скрыть или показать правую боковую панель"
diff --git a/frontend/src/locales/zhCN.json b/frontend/src/locales/zhCN.json
index ecfa7827..3b7688de 100644
--- a/frontend/src/locales/zhCN.json
+++ b/frontend/src/locales/zhCN.json
@@ -551,6 +551,10 @@
"label": "标记为已委派",
"description": "将所选邮件归类为已委派"
},
+ "toggleLeftSidebar": {
+ "label": "切换左侧边栏",
+ "description": "折叠或展开左侧边栏"
+ },
"toggleRightSidebar": {
"label": "切换右侧边栏",
"description": "隐藏或显示右侧边栏"
diff --git a/frontend/src/utils/defaultShortcuts.js b/frontend/src/utils/defaultShortcuts.js
index 3365f7f3..27cd79ba 100644
--- a/frontend/src/utils/defaultShortcuts.js
+++ b/frontend/src/utils/defaultShortcuts.js
@@ -30,6 +30,7 @@ export const ACTION_DEFS = {
prevMessage: { groupKey: 'shortcuts.groups.navigation', labelKey: 'shortcuts.actions.prevMessage.label', descriptionKey: 'shortcuts.actions.prevMessage.description', defaultKey: 'k' },
openMessage: { groupKey: 'shortcuts.groups.navigation', labelKey: 'shortcuts.actions.openMessage.label', descriptionKey: 'shortcuts.actions.openMessage.description', defaultKey: 'o' },
goInbox: { groupKey: 'shortcuts.groups.navigation', labelKey: 'shortcuts.actions.goInbox.label', descriptionKey: 'shortcuts.actions.goInbox.description', defaultKey: 'gi' },
+ toggleLeftSidebar: { groupKey: 'shortcuts.groups.navigation', labelKey: 'shortcuts.actions.toggleLeftSidebar.label', descriptionKey: 'shortcuts.actions.toggleLeftSidebar.description', defaultKey: 'ctrl+\\' },
toggleRightSidebar: { groupKey: 'shortcuts.groups.navigation', labelKey: 'shortcuts.actions.toggleRightSidebar.label', descriptionKey: 'shortcuts.actions.toggleRightSidebar.description', defaultKey: 'ctrl+/' },
// ── Message actions ────────────────────────────────────────────────────────
diff --git a/frontend/src/utils/defaultShortcuts.test.js b/frontend/src/utils/defaultShortcuts.test.js
index 618b0b52..9d39021c 100644
--- a/frontend/src/utils/defaultShortcuts.test.js
+++ b/frontend/src/utils/defaultShortcuts.test.js
@@ -63,4 +63,11 @@ describe('buildModKeyMap', () => {
assert.equal(keyMap['/'], 'focusSearch', 'bare / stays the search key (separate map)');
assert.equal(warn.mock.callCount(), 0);
});
+
+ it('binds toggleLeftSidebar to ctrl+backslash without colliding with defaults', (t) => {
+ const warn = t.mock.method(console, 'warn', () => {});
+ const modMap = buildModKeyMap();
+ assert.equal(modMap['\\'], 'toggleLeftSidebar');
+ assert.equal(warn.mock.callCount(), 0);
+ });
});
diff --git a/frontend/src/utils/sidebarTransition.js b/frontend/src/utils/sidebarTransition.js
new file mode 100644
index 00000000..095e6881
--- /dev/null
+++ b/frontend/src/utils/sidebarTransition.js
@@ -0,0 +1,5 @@
+const SIDEBAR_TRANSITION_TIMING = '0.2s ease';
+
+export function sidebarTransition(...properties) {
+ return properties.map(property => `${property} ${SIDEBAR_TRANSITION_TIMING}`).join(', ');
+}
diff --git a/frontend/src/utils/sidebarTransition.test.js b/frontend/src/utils/sidebarTransition.test.js
new file mode 100644
index 00000000..674ed7e3
--- /dev/null
+++ b/frontend/src/utils/sidebarTransition.test.js
@@ -0,0 +1,16 @@
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import { sidebarTransition } from './sidebarTransition.js';
+
+describe('sidebarTransition', () => {
+ it('uses the shared GTD timing and easing for one animated property', () => {
+ assert.equal(sidebarTransition('width'), 'width 0.2s ease');
+ });
+
+ it('applies the shared animation to every supplied property', () => {
+ assert.equal(
+ sidebarTransition('width', 'min-width'),
+ 'width 0.2s ease, min-width 0.2s ease',
+ );
+ });
+});