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', + ); + }); +});