diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 3a2f3e4ff5..8f3255ad9a 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -88,6 +88,7 @@ import { isSwitchToPreviousTabShortcut, isToggleResultsPaneShortcut, isToggleSidebarShortcut, + isToggleZenModeShortcut, isZoomInShortcut, isZoomOutShortcut, switchToTabIndexFromShortcut, @@ -254,6 +255,7 @@ const agentDriverUpdateCount = ref(0); const showHistory = ref(false); const showAiPanel = ref(safeLocalStorageGet("dbx-ai-panel-open") === "true"); const isAiPanelMaximized = ref(false); +const isZenMode = ref(false); const showSqlLibraryPanel = ref(safeLocalStorageGet("dbx-sql-library-open") === "true"); const showSqlFilePanel = ref(safeLocalStorageGet("dbx-sql-file-panel-open") === "true"); const rightSidebarPanelRefs: Record = { @@ -323,6 +325,18 @@ const activeTab = computed(() => queryStore.tabs.find((t) => t.id === queryStore const tabNavigationHistory = ref(createTabNavigationHistory()); let pendingTabHistoryNavigationId: string | null = null; +watch( + () => activeTab.value?.mode, + (mode) => { + if (mode !== "data") isZenMode.value = false; + }, +); + +function toggleZenMode() { + if (activeTab.value?.mode !== "data") return; + isZenMode.value = !isZenMode.value; +} + const externalSqlFileChanges = useExternalSqlFileChanges({ activeTab, recreateFile: async (tab) => { @@ -2610,6 +2624,12 @@ async function handleKeydown(e: KeyboardEvent) { setSidebarOpen(!sidebarOpen.value); return; } + if (isToggleZenModeShortcut(e, shortcuts) && activeTab.value?.mode === "data") { + e.preventDefault(); + e.stopPropagation(); + toggleZenMode(); + return; + } if (switchTabIndex != null) { if (activateTabByIndex(switchTabIndex)) { e.preventDefault(); @@ -2977,7 +2997,7 @@ onUnmounted(() => {
{ @open-settings="(initialTab) => openSettings(initialTab ?? 'appearance')" @add-to-ai="addToAi" /> -
+
-
+
{ :settings-page-open="settingsPageTabOpen" :settings-page-active="settingsStore.settingsPageActive" :agent-driver-update-count="toolbarAgentDriverUpdateCount" + @toggle-zen-mode="toggleZenMode" @activate-driver-store="openDriverStorePage" @activate-settings-page="activateSettingsPage" @locate-tab="locateTabInSidebar" @@ -3168,6 +3189,7 @@ onUnmounted(() => {
@@ -3192,21 +3214,21 @@ onUnmounted(() => {
-
+
-
+
-
+
diff --git a/apps/desktop/src/components/layout/AppTabBar.vue b/apps/desktop/src/components/layout/AppTabBar.vue index 08649b10c1..8797fa9e3d 100644 --- a/apps/desktop/src/components/layout/AppTabBar.vue +++ b/apps/desktop/src/components/layout/AppTabBar.vue @@ -34,6 +34,7 @@ const props = defineProps<{ const emit = defineEmits<{ "activate-tab": []; + "toggle-zen-mode": []; "locate-tab": [tab: QueryTab]; "activate-driver-store": []; "close-driver-store": []; @@ -577,6 +578,17 @@ function handleTabClick(tab: QueryTab) { activateTab(tab.id); } +function handleTabDoubleClick(tab: QueryTab, event: MouseEvent) { + event.stopPropagation(); + if (tabDrag.state.suppressClick || (event.target instanceof Element && event.target.closest("button, input, [role='button']"))) return; + if (tab.mode === "data") { + if (tab.id !== queryStore.activeTabId) activateTab(tab.id); + emit("toggle-zen-mode"); + return; + } + startRenameTab(tab); +} + function handleTabMouseDown(event: PointerEvent, tabId: string) { if (event.button === 0) { dispatchBeforeTabSwitch(tabId); @@ -697,7 +709,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul :style="[tabColorStyle(tab), tabDropStyle(tab.id)]" :data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive" @click="handleTabClick(tab)" - @dblclick.stop="startRenameTab(tab)" + @dblclick="handleTabDoubleClick(tab, $event)" @mousedown.middle.prevent="queryStore.closeTab(tab.id)" @pointerdown="handleTabMouseDown($event, tab.id)" @mouseenter="handleTabDragTarget($event, tab)" @@ -902,7 +914,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul :style="[tabColorStyle(tab), tabDropStyle(tab.id)]" :data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive" @click="handleTabClick(tab)" - @dblclick.stop="startRenameTab(tab)" + @dblclick="handleTabDoubleClick(tab, $event)" @mousedown.middle.prevent="queryStore.closeTab(tab.id)" @pointerdown="handleTabMouseDown($event, tab.id)" @mouseenter="handleTabDragTarget($event, tab)" diff --git a/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts b/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts index 6d1468ff22..2387c74e77 100644 --- a/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts @@ -66,6 +66,19 @@ describe("AppTabBar locate-in-sidebar action", () => { }); }); +describe("AppTabBar Zen mode interaction", () => { + it("switches Zen mode for data tabs while preserving query-tab renaming", () => { + const handler = sourceBetween("function handleTabDoubleClick", "function handleTabMouseDown"); + + expect(tabBarSource).toContain('"toggle-zen-mode": [];'); + expect(handler).toContain('if (tab.mode === "data") {'); + expect(handler).toContain('emit("toggle-zen-mode");'); + expect(handler).toContain("startRenameTab(tab);"); + expect(handler).toContain("event.target instanceof Element && event.target.closest(\"button, input, [role='button']\")"); + expect(tabBarSource.match(/@dblclick="handleTabDoubleClick\(tab, \$event\)"/g)).toHaveLength(2); + }); +}); + describe("AppTabBar right-side close action", () => { it("places the action after close-other and disables it when the target has no tabs to its right", () => { expect(tabBarSource).toContain('label: t("contextMenu.closeRightTabs")'); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 510a2aac66..e55e44fc94 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -6674,6 +6674,7 @@ export default { shortcutToggleTranspose: "Toggle transpose view", shortcutCancelSearch: "Cancel search", shortcutToggleSidebar: "Toggle sidebar", + shortcutToggleZenMode: "Toggle Zen mode", shortcutCopySidebarSelection: "Copy sidebar selection", shortcutPasteSidebarSelection: "Paste into sidebar", shortcutEditSidebarConnection: "Edit sidebar connection", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index e5f03244e9..b031f3bf78 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -6304,6 +6304,7 @@ export default withEnglishFallback({ shortcutOpenSettings: "Abrir configuración", shortcutCloseTab: "Cerrar pestaña", shortcutToggleSidebar: "Alternar barra lateral", + shortcutToggleZenMode: "Alternar modo Zen", shortcutFocusSearch: "Enfocar búsqueda de la vista actual", shortcutQuickOpen: "Abrir rápido (buscar todos los objetos de base de datos)", shortcutNavigateTabHistoryBack: "Volver a la pestaña vista anteriormente", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index cf5d9975af..548f6c8334 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -6327,6 +6327,7 @@ export default withEnglishFallback({ shortcutToggleTranspose: "Attiva/disattiva vista trasposta", shortcutCancelSearch: "Annulla ricerca", shortcutToggleSidebar: "Attiva/disattiva barra laterale", + shortcutToggleZenMode: "Attiva/disattiva modalità Zen", shortcutCopySidebarSelection: "Copia selezione barra laterale", shortcutPasteSidebarSelection: "Incolla nella barra laterale", shortcutEditSidebarConnection: "Modifica connessione barra laterale", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 7495501e0c..980f592124 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -6335,6 +6335,7 @@ export default withEnglishFallback({ shortcutToggleTranspose: "転置ビューを切替", shortcutCancelSearch: "検索をキャンセル", shortcutToggleSidebar: "サイドバーを切替", + shortcutToggleZenMode: "Zenモードを切替", shortcutCopySidebarSelection: "サイドバーの選択をコピー", shortcutPasteSidebarSelection: "サイドバーに貼り付け", shortcutEditSidebarConnection: "サイドバー接続を編集", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 782a4a7544..f700667864 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -6050,6 +6050,7 @@ export default withEnglishFallback({ shortcutToggleTranspose: "전치 보기 전환", shortcutCancelSearch: "검색 취소", shortcutToggleSidebar: "사이드바 전환", + shortcutToggleZenMode: "Zen 모드 전환", shortcutCopySidebarSelection: "사이드바 선택 복사", shortcutPasteSidebarSelection: "사이드바에 붙여넣기", shortcutEditSidebarConnection: "사이드바 연결 편집", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 1fa1c4b947..715e95c1d1 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -6306,6 +6306,7 @@ export default withEnglishFallback({ shortcutOpenSettings: "Abrir configurações", shortcutCloseTab: "Fechar aba", shortcutToggleSidebar: "Alternar barra lateral", + shortcutToggleZenMode: "Alternar modo Zen", shortcutFocusSearch: "Focar na pesquisa da tela atual", shortcutQuickOpen: "Abrir rapidamente (pesquisar todos os objetos do banco de dados)", shortcutNavigateTabHistoryBack: "Voltar para a aba visualizada anteriormente", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index c0addccd38..fc19efe467 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -6637,6 +6637,7 @@ export default withEnglishFallback({ shortcutOpenSettings: "打开设置", shortcutCloseTab: "关闭标签页", shortcutToggleSidebar: "切换侧边栏", + shortcutToggleZenMode: "切换禅模式", shortcutFocusSearch: "聚焦当前页面搜索", shortcutQuickOpen: "快速打开 (搜索所有数据库对象)", shortcutNavigateTabHistoryBack: "返回到上一个查看的标签页", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 9466898838..c5a533d441 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -5611,6 +5611,7 @@ export default withEnglishFallback({ shortcutOpenSettings: "開啟設定", shortcutCloseTab: "關閉分頁", shortcutToggleSidebar: "切換側邊欄", + shortcutToggleZenMode: "切換禪模式", shortcutFocusSearch: "聚焦目前頁面搜尋", shortcutQuickOpen: "快速開啟 (搜尋所有資料庫物件)", shortcutNavigateTabHistoryBack: "返回到上一個檢視的分頁", diff --git a/apps/desktop/src/lib/__tests__/app/rightSidebarPanels.spec.ts b/apps/desktop/src/lib/__tests__/app/rightSidebarPanels.spec.ts index b7fc4acee6..0baf7ff53a 100644 --- a/apps/desktop/src/lib/__tests__/app/rightSidebarPanels.spec.ts +++ b/apps/desktop/src/lib/__tests__/app/rightSidebarPanels.spec.ts @@ -55,7 +55,20 @@ describe("right sidebar panel entry points", () => { expect(appSource).toContain(':maximized="isAiPanelMaximized"'); expect(appSource).toContain('@toggle-maximize="toggleAiPanelMaximized"'); expect(appSource).toContain("isAiPanelMaximized ? 'min-w-0 flex-1' : 'min-w-[180px] max-w-full'"); - expect(appSource).toContain('v-show="!isAiPanelMaximized"'); + expect(appSource).toContain('v-show="!isAiPanelMaximized && !isZenMode"'); expect(functionSource("setRightSidebarPanelOpen", "toggleRightSidebarPanel")).toContain("isAiPanelMaximized.value = false;"); }); + + it("keeps Zen mode as a temporary data-tab layout", () => { + expect(appSource).toContain("const isZenMode = ref(false);"); + expect(appSource).toContain('if (mode !== "data") isZenMode.value = false;'); + expect(appSource).toContain("function toggleZenMode() {"); + expect(appSource).toContain('if (activeTab.value?.mode !== "data") return;'); + expect(appSource).toContain('isToggleZenModeShortcut(e, shortcuts) && activeTab.value?.mode === "data"'); + expect(appSource).toContain('@toggle-zen-mode="toggleZenMode"'); + expect(appSource).toContain('v-show="sidebarOpen && !isZenMode"'); + expect(appSource).toContain('v-show="!sidebarOpen && !isZenMode"'); + expect(appSource).toContain('v-show="!isAiPanelMaximized || isZenMode"'); + expect(appSource).toContain('v-show="!isZenMode"'); + }); }); diff --git a/apps/desktop/src/lib/__tests__/editor/keyboardShortcuts.spec.ts b/apps/desktop/src/lib/__tests__/editor/keyboardShortcuts.spec.ts index 55cfb5d019..67b6b7412c 100644 --- a/apps/desktop/src/lib/__tests__/editor/keyboardShortcuts.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/keyboardShortcuts.spec.ts @@ -9,6 +9,7 @@ import { isGoToLastPageShortcut, isGoToNextPageShortcut, isGoToPreviousPageShortcut, + isToggleZenModeShortcut, matchesModifierOnlyShortcut, matchesShortcut, tabSwitcherDirectionFromShortcut, @@ -98,6 +99,14 @@ describe("keyboard shortcut matching", () => { expect(isExecuteSqlInNewResultTabShortcut({ key: "\\", metaKey: true }, { executeSqlInNewResultTab: "Mod+\\" })).toBe(true); }); + it("matches the configurable Zen mode shortcut", () => { + const platformModEvent = isMacShortcutPlatform() ? { key: "F12", metaKey: true, shiftKey: true } : { key: "F12", ctrlKey: true, shiftKey: true }; + + expect(isToggleZenModeShortcut(platformModEvent, { toggleZenMode: "Shift+Mod+F12" })).toBe(true); + expect(isToggleZenModeShortcut({ ...platformModEvent, shiftKey: false }, { toggleZenMode: "Shift+Mod+F12" })).toBe(false); + expect(isToggleZenModeShortcut(platformModEvent, { toggleZenMode: "" })).toBe(false); + }); + it("matches legacy plus-key shortcuts saved with plus as a separator", () => { expect(matchesShortcut({ key: "+", ctrlKey: true }, "Mod++", "Win32")).toBe(true); expect(matchesShortcut({ key: "+", ctrlKey: true, shiftKey: true }, "Shift+Mod++", "Win32")).toBe(true); diff --git a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts index b8a006e143..751fd01c1d 100644 --- a/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/shortcutRegistry.spec.ts @@ -146,6 +146,14 @@ describe("shortcutRegistry editor actions", () => { expect(findShortcutConflict("find", DEFAULT_SHORTCUT_SETTINGS.find, DEFAULT_SHORTCUT_SETTINGS)).toBeNull(); }); + it("registers a conflict-free global shortcut for Zen mode", () => { + const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "toggleZenMode"); + + expect(definition).toMatchObject({ labelKey: "settings.shortcutToggleZenMode", scope: "global", defaultShortcut: "Shift+Mod+F12" }); + expect(DEFAULT_SHORTCUT_SETTINGS.toggleZenMode).toBe("Shift+Mod+F12"); + expect(findShortcutConflict("toggleZenMode", DEFAULT_SHORTCUT_SETTINGS.toggleZenMode, DEFAULT_SHORTCUT_SETTINGS)).toBeNull(); + }); + it("uses Shift+Enter for inserting a complete line below", () => { const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "insertLineBelow"); diff --git a/apps/desktop/src/lib/editor/keyboardShortcuts.ts b/apps/desktop/src/lib/editor/keyboardShortcuts.ts index 0031f6f64f..8a59720fa4 100644 --- a/apps/desktop/src/lib/editor/keyboardShortcuts.ts +++ b/apps/desktop/src/lib/editor/keyboardShortcuts.ts @@ -246,6 +246,10 @@ export function isToggleSidebarShortcut(event: ShortcutLikeEvent, shortcuts?: Pa return matchesShortcut(event, actionShortcut("toggleSidebar", shortcuts)); } +export function isToggleZenModeShortcut(event: ShortcutLikeEvent, shortcuts?: Partial): boolean { + return matchesShortcut(event, actionShortcut("toggleZenMode", shortcuts)); +} + export function isCopySidebarSelectionShortcut(event: ShortcutLikeEvent, shortcuts?: Partial): boolean { return matchesShortcut(event, actionShortcut("copySidebarSelection", shortcuts)); } diff --git a/apps/desktop/src/lib/editor/shortcutRegistry.ts b/apps/desktop/src/lib/editor/shortcutRegistry.ts index a6e1c600d1..0118460aa3 100644 --- a/apps/desktop/src/lib/editor/shortcutRegistry.ts +++ b/apps/desktop/src/lib/editor/shortcutRegistry.ts @@ -66,6 +66,7 @@ export type ShortcutActionId = | "toggleTranspose" | "cancelSearch" | "toggleSidebar" + | "toggleZenMode" | "copySidebarSelection" | "pasteSidebarSelection" | "editSidebarConnection" @@ -503,6 +504,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ scope: "global", defaultShortcut: "Mod+B", }, + { + id: "toggleZenMode", + labelKey: "settings.shortcutToggleZenMode", + scope: "global", + defaultShortcut: "Shift+Mod+F12", + }, { id: "copySidebarSelection", labelKey: "settings.shortcutCopySidebarSelection",