diff --git a/apps/desktop/src/renderer/hotkeys/registry.ts b/apps/desktop/src/renderer/hotkeys/registry.ts index e04d7c4b3a1..24db8d60b85 100644 --- a/apps/desktop/src/renderer/hotkeys/registry.ts +++ b/apps/desktop/src/renderer/hotkeys/registry.ts @@ -101,24 +101,6 @@ export const HOTKEYS_REGISTRY = { label: "Switch to Workspace 9", category: "Workspace", }, - PREV_WORKSPACE: { - key: { - mac: "meta+alt+up", - windows: "ctrl+shift+alt+up", - linux: "ctrl+shift+alt+up", - }, - label: "Previous Workspace", - category: "Workspace", - }, - NEXT_WORKSPACE: { - key: { - mac: "meta+alt+down", - windows: "ctrl+shift+alt+down", - linux: "ctrl+shift+alt+down", - }, - label: "Next Workspace", - category: "Workspace", - }, CLOSE_WORKSPACE: { key: { mac: "meta+shift+backspace", @@ -334,24 +316,6 @@ export const HOTKEYS_REGISTRY = { category: "Terminal", description: "Scroll the active terminal to the bottom", }, - PREV_TAB: { - key: { - mac: "meta+alt+left", - windows: "ctrl+shift+alt+left", - linux: "ctrl+shift+alt+left", - }, - label: "Previous Tab", - category: "Terminal", - }, - NEXT_TAB: { - key: { - mac: "meta+alt+right", - windows: "ctrl+shift+alt+right", - linux: "ctrl+shift+alt+right", - }, - label: "Next Tab", - category: "Terminal", - }, PREV_TAB_ALT: { key: { mac: "ctrl+shift+tab", @@ -366,25 +330,45 @@ export const HOTKEYS_REGISTRY = { label: "Next Tab (Alt)", category: "Terminal", }, - PREV_PANE: { + FOCUS_PANE_LEFT: { key: { - mac: "meta+shift+left", + mac: "meta+alt+left", windows: "ctrl+shift+alt+left", linux: "ctrl+shift+alt+left", }, - label: "Previous Pane", + label: "Focus Pane Left", category: "Terminal", - description: "Focus the previous pane in the current tab", + description: "Focus the pane to the left of the active pane", }, - NEXT_PANE: { + FOCUS_PANE_RIGHT: { key: { - mac: "meta+shift+right", + mac: "meta+alt+right", windows: "ctrl+shift+alt+right", linux: "ctrl+shift+alt+right", }, - label: "Next Pane", + label: "Focus Pane Right", + category: "Terminal", + description: "Focus the pane to the right of the active pane", + }, + FOCUS_PANE_UP: { + key: { + mac: "meta+alt+up", + windows: "ctrl+shift+alt+up", + linux: "ctrl+shift+alt+up", + }, + label: "Focus Pane Up", + category: "Terminal", + description: "Focus the pane above the active pane", + }, + FOCUS_PANE_DOWN: { + key: { + mac: "meta+alt+down", + windows: "ctrl+shift+alt+down", + linux: "ctrl+shift+alt+down", + }, + label: "Focus Pane Down", category: "Terminal", - description: "Focus the next pane in the current tab", + description: "Focus the pane below the active pane", }, JUMP_TO_TAB_1: { key: { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts index eaedf913f8a..dbe6240106d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts @@ -1,4 +1,4 @@ -import { useMatchRoute, useNavigate } from "@tanstack/react-router"; +import { useNavigate } from "@tanstack/react-router"; import { useCallback, useMemo } from "react"; import { useHotkey } from "renderer/hotkeys"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; @@ -48,33 +48,5 @@ export function useDashboardSidebarShortcuts( useHotkey("JUMP_TO_WORKSPACE_8", () => switchToWorkspace(7)); useHotkey("JUMP_TO_WORKSPACE_9", () => switchToWorkspace(8)); - // Prev/next workspace navigation (cycles) - const matchRoute = useMatchRoute(); - const currentWorkspaceMatch = matchRoute({ - to: "/v2-workspace/$workspaceId", - fuzzy: true, - }); - const currentWorkspaceId = - currentWorkspaceMatch !== false ? currentWorkspaceMatch.workspaceId : null; - - useHotkey("PREV_WORKSPACE", () => { - if (!currentWorkspaceId || flattenedWorkspaces.length === 0) return; - const index = flattenedWorkspaces.findIndex( - (w) => w.id === currentWorkspaceId, - ); - const prevIndex = index <= 0 ? flattenedWorkspaces.length - 1 : index - 1; - navigateToV2Workspace(flattenedWorkspaces[prevIndex].id, navigate); - }); - - useHotkey("NEXT_WORKSPACE", () => { - if (!currentWorkspaceId || flattenedWorkspaces.length === 0) return; - const index = flattenedWorkspaces.findIndex( - (w) => w.id === currentWorkspaceId, - ); - const nextIndex = - index >= flattenedWorkspaces.length - 1 || index === -1 ? 0 : index + 1; - navigateToV2Workspace(flattenedWorkspaces[nextIndex].id, navigate); - }); - return workspaceShortcutLabels; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts index ef5bf804a13..c791446a404 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts @@ -1,4 +1,8 @@ -import type { WorkspaceStore } from "@superset/panes"; +import { + type FocusDirection, + getSpatialNeighborPaneId, + type WorkspaceStore, +} from "@superset/panes"; import { useCallback } from "react"; import { useHotkey } from "renderer/hotkeys"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; @@ -83,23 +87,6 @@ export function useWorkspaceHotkeys({ } }); - useHotkey("PREV_TAB", () => { - const state = store.getState(); - if (!state.activeTabId || state.tabs.length === 0) return; - const index = state.tabs.findIndex((t) => t.id === state.activeTabId); - const prevIndex = index <= 0 ? state.tabs.length - 1 : index - 1; - state.setActiveTab(state.tabs[prevIndex].id); - }); - - useHotkey("NEXT_TAB", () => { - const state = store.getState(); - if (!state.activeTabId || state.tabs.length === 0) return; - const index = state.tabs.findIndex((t) => t.id === state.activeTabId); - const nextIndex = - index >= state.tabs.length - 1 || index === -1 ? 0 : index + 1; - state.setActiveTab(state.tabs[nextIndex].id); - }); - useHotkey("PREV_TAB_ALT", () => { const state = store.getState(); if (!state.activeTabId || state.tabs.length === 0) return; @@ -138,25 +125,25 @@ export function useWorkspaceHotkeys({ // --- Pane management --- - useHotkey("PREV_PANE", () => { - const state = store.getState(); - const tab = state.getActiveTab(); - if (!tab || !tab.activePaneId) return; - const paneIds = Object.keys(tab.panes); - const index = paneIds.indexOf(tab.activePaneId); - const prevIndex = index <= 0 ? paneIds.length - 1 : index - 1; - state.setActivePane({ tabId: tab.id, paneId: paneIds[prevIndex] }); - }); + const moveFocusDirectional = useCallback( + (dir: FocusDirection) => { + const state = store.getState(); + const tab = state.getActiveTab(); + if (!tab || !tab.activePaneId) return; + const neighbor = getSpatialNeighborPaneId( + tab.layout, + tab.activePaneId, + dir, + ); + if (neighbor) state.setActivePane({ tabId: tab.id, paneId: neighbor }); + }, + [store], + ); - useHotkey("NEXT_PANE", () => { - const state = store.getState(); - const tab = state.getActiveTab(); - if (!tab || !tab.activePaneId) return; - const paneIds = Object.keys(tab.panes); - const index = paneIds.indexOf(tab.activePaneId); - const nextIndex = index >= paneIds.length - 1 ? 0 : index + 1; - state.setActivePane({ tabId: tab.id, paneId: paneIds[nextIndex] }); - }); + useHotkey("FOCUS_PANE_LEFT", () => moveFocusDirectional("left")); + useHotkey("FOCUS_PANE_RIGHT", () => moveFocusDirectional("right")); + useHotkey("FOCUS_PANE_UP", () => moveFocusDirectional("up")); + useHotkey("FOCUS_PANE_DOWN", () => moveFocusDirectional("down")); useHotkey("SPLIT_AUTO", () => { const state = store.getState(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx index 254916f1cb2..5ca024491e9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/page.tsx @@ -1,5 +1,5 @@ import type { ExternalApp } from "@superset/local-db"; -import { createFileRoute, notFound, useNavigate } from "@tanstack/react-router"; +import { createFileRoute, notFound } from "@tanstack/react-router"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { useFileOpenMode } from "renderer/hooks/useFileOpenMode"; @@ -8,7 +8,6 @@ import { electronTrpc } from "renderer/lib/electron-trpc"; import { electronTrpcClient as trpcClient } from "renderer/lib/trpc-client"; import { usePresets } from "renderer/react-query/presets"; import type { WorkspaceSearchParams } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; -import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { usePresetHotkeys } from "renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/usePresetHotkeys"; import { useWorkspaceRunCommand } from "renderer/routes/_authenticated/_dashboard/workspace/$workspaceId/hooks/useWorkspaceRunCommand"; import { NotFound } from "renderer/routes/not-found"; @@ -35,8 +34,6 @@ import { useTabsWithPresets } from "renderer/stores/tabs/useTabsWithPresets"; import { findPanePath, getFirstPaneId, - getNextPaneId, - getPreviousPaneId, resolveActiveTabIdForWorkspace, } from "renderer/stores/tabs/utils"; import { @@ -93,7 +90,6 @@ function WorkspacePage() { worktreePath: workspace?.worktreePath, enabled: Boolean(workspace?.worktreePath), }); - const navigate = useNavigate(); const routeNavigate = Route.useNavigate(); const { tabId: searchTabId, paneId: searchPaneId } = Route.useSearch(); @@ -228,20 +224,6 @@ function WorkspacePage() { } }); - useHotkey("PREV_TAB", () => { - if (!activeTabId || tabs.length === 0) return; - const index = tabs.findIndex((t) => t.id === activeTabId); - const prevIndex = index <= 0 ? tabs.length - 1 : index - 1; - setActiveTab(workspaceId, tabs[prevIndex].id); - }); - - useHotkey("NEXT_TAB", () => { - if (!activeTabId || tabs.length === 0) return; - const index = tabs.findIndex((t) => t.id === activeTabId); - const nextIndex = index >= tabs.length - 1 || index === -1 ? 0 : index + 1; - setActiveTab(workspaceId, tabs[nextIndex].id); - }); - useHotkey("PREV_TAB_ALT", () => { if (!activeTabId || tabs.length === 0) return; const index = tabs.findIndex((t) => t.id === activeTabId); @@ -276,22 +258,6 @@ function WorkspacePage() { useHotkey("JUMP_TO_TAB_8", () => switchToTab(7)); useHotkey("JUMP_TO_TAB_9", () => switchToTab(8)); - useHotkey("PREV_PANE", () => { - if (!activeTabId || !activeTab?.layout || !focusedPaneId) return; - const prevPaneId = getPreviousPaneId(activeTab.layout, focusedPaneId); - if (prevPaneId) { - setFocusedPane(activeTabId, prevPaneId); - } - }); - - useHotkey("NEXT_PANE", () => { - if (!activeTabId || !activeTab?.layout || !focusedPaneId) return; - const nextPaneId = getNextPaneId(activeTab.layout, focusedPaneId); - if (nextPaneId) { - setFocusedPane(activeTabId, nextPaneId); - } - }); - // Open in last used app shortcut const projectId = workspace?.projectId; const { data: defaultApp } = electronTrpc.projects.getDefaultApp.useQuery( @@ -427,31 +393,6 @@ function WorkspacePage() { } }); - // Navigate to previous workspace (⌘↑) - const getPreviousWorkspace = - electronTrpc.workspaces.getPreviousWorkspace.useQuery( - { id: workspaceId }, - { enabled: !!workspaceId }, - ); - useHotkey("PREV_WORKSPACE", () => { - const prevWorkspaceId = getPreviousWorkspace.data; - if (prevWorkspaceId) { - navigateToWorkspace(prevWorkspaceId, navigate); - } - }); - - // Navigate to next workspace (⌘↓) - const getNextWorkspace = electronTrpc.workspaces.getNextWorkspace.useQuery( - { id: workspaceId }, - { enabled: !!workspaceId }, - ); - useHotkey("NEXT_WORKSPACE", () => { - const nextWorkspaceId = getNextWorkspace.data; - if (nextWorkspaceId) { - navigateToWorkspace(nextWorkspaceId, navigate); - } - }); - return (
diff --git a/apps/desktop/src/renderer/stores/tabs/utils.ts b/apps/desktop/src/renderer/stores/tabs/utils.ts index 8217f208ef4..f784e3a9428 100644 --- a/apps/desktop/src/renderer/stores/tabs/utils.ts +++ b/apps/desktop/src/renderer/stores/tabs/utils.ts @@ -506,42 +506,6 @@ export const getFirstPaneId = (layout: MosaicNode): string => { return getFirstPaneId(layout.first); }; -/** - * Gets the next pane ID in visual order (left-to-right, top-to-bottom), - * wrapping around to the first if at the end. - */ -export const getNextPaneId = ( - layout: MosaicNode, - currentPaneId: string, -): string | null => { - const paneIds = getPaneIdsInVisualOrder(layout); - if (paneIds.length <= 1) return null; - - const currentIndex = paneIds.indexOf(currentPaneId); - if (currentIndex === -1) return paneIds[0]; - - const nextIndex = (currentIndex + 1) % paneIds.length; - return paneIds[nextIndex]; -}; - -/** - * Gets the previous pane ID in visual order (right-to-left, bottom-to-top), - * wrapping around to the last if at the beginning. - */ -export const getPreviousPaneId = ( - layout: MosaicNode, - currentPaneId: string, -): string | null => { - const paneIds = getPaneIdsInVisualOrder(layout); - if (paneIds.length <= 1) return null; - - const currentIndex = paneIds.indexOf(currentPaneId); - if (currentIndex === -1) return paneIds[paneIds.length - 1]; - - const prevIndex = (currentIndex - 1 + paneIds.length) % paneIds.length; - return paneIds[prevIndex]; -}; - /** * Gets the adjacent pane ID for focus fallback when a pane is closed. * Prefers the next pane in visual order, falls back to previous if at the end. diff --git a/packages/panes/src/core/store/utils/index.ts b/packages/panes/src/core/store/utils/index.ts index 3b4da77a2bd..46eb9cafa0b 100644 --- a/packages/panes/src/core/store/utils/index.ts +++ b/packages/panes/src/core/store/utils/index.ts @@ -1,11 +1,14 @@ +export type { FocusDirection } from "./utils"; export { equalizeAllSplits, findFirstPaneId, findPaneInLayout, + findPanePath, findSiblingPaneId, generateId, getNodeAtPath, getOtherBranch, + getSpatialNeighborPaneId, positionToDirection, removePaneFromLayout, replacePaneIdInLayout, diff --git a/packages/panes/src/core/store/utils/utils.test.ts b/packages/panes/src/core/store/utils/utils.test.ts index 22fc3c0f2f4..46d060e3eeb 100644 --- a/packages/panes/src/core/store/utils/utils.test.ts +++ b/packages/panes/src/core/store/utils/utils.test.ts @@ -6,6 +6,7 @@ import { findPaneInLayout, getNodeAtPath, getOtherBranch, + getSpatialNeighborPaneId, positionToDirection, removePaneFromLayout, replacePaneIdInLayout, @@ -320,3 +321,135 @@ describe("positionToDirection", () => { expect(positionToDirection("bottom")).toBe("vertical"); }); }); + +describe("getSpatialNeighborPaneId", () => { + // +---+ + // | a | + // +---+ + it("returns null when there is only one pane", () => { + const layout: LayoutNode = { type: "pane", paneId: "a" }; + expect(getSpatialNeighborPaneId(layout, "a", "left")).toBeNull(); + expect(getSpatialNeighborPaneId(layout, "a", "right")).toBeNull(); + expect(getSpatialNeighborPaneId(layout, "a", "up")).toBeNull(); + expect(getSpatialNeighborPaneId(layout, "a", "down")).toBeNull(); + }); + + // +---+---+ + // | a | b | + // +---+---+ + it("moves between siblings in a horizontal split", () => { + const layout: LayoutNode = { + type: "split", + direction: "horizontal", + first: { type: "pane", paneId: "a" }, + second: { type: "pane", paneId: "b" }, + }; + expect(getSpatialNeighborPaneId(layout, "a", "right")).toBe("b"); + expect(getSpatialNeighborPaneId(layout, "b", "left")).toBe("a"); + expect(getSpatialNeighborPaneId(layout, "a", "up")).toBeNull(); + expect(getSpatialNeighborPaneId(layout, "a", "down")).toBeNull(); + }); + + // +---+ + // | a | + // +---+ + // | b | + // +---+ + it("moves between siblings in a vertical split", () => { + const layout: LayoutNode = { + type: "split", + direction: "vertical", + first: { type: "pane", paneId: "a" }, + second: { type: "pane", paneId: "b" }, + }; + expect(getSpatialNeighborPaneId(layout, "a", "down")).toBe("b"); + expect(getSpatialNeighborPaneId(layout, "b", "up")).toBe("a"); + expect(getSpatialNeighborPaneId(layout, "a", "right")).toBeNull(); + }); + + // +---+---+ + // | a | b | + // +---+---+ + it("does not wrap around at the layout edge", () => { + const layout: LayoutNode = { + type: "split", + direction: "horizontal", + first: { type: "pane", paneId: "a" }, + second: { type: "pane", paneId: "b" }, + }; + expect(getSpatialNeighborPaneId(layout, "a", "left")).toBeNull(); + expect(getSpatialNeighborPaneId(layout, "b", "right")).toBeNull(); + }); + + // 2x2 grid built "rows first": + // outer = vertical split { top row / bot row } + // top row = horizontal split { a | b } + // bot row = horizontal split { c | d } + // + // +---+---+ + // | a | b | + // +---+---+ + // | c | d | + // +---+---+ + it("preserves column alignment in a rows-first 2x2", () => { + const layout: LayoutNode = { + type: "split", + direction: "vertical", + first: { + type: "split", + direction: "horizontal", + first: { type: "pane", paneId: "a" }, + second: { type: "pane", paneId: "b" }, + }, + second: { + type: "split", + direction: "horizontal", + first: { type: "pane", paneId: "c" }, + second: { type: "pane", paneId: "d" }, + }, + }; + expect(getSpatialNeighborPaneId(layout, "b", "down")).toBe("d"); + expect(getSpatialNeighborPaneId(layout, "d", "up")).toBe("b"); + expect(getSpatialNeighborPaneId(layout, "c", "up")).toBe("a"); + expect(getSpatialNeighborPaneId(layout, "a", "down")).toBe("c"); + expect(getSpatialNeighborPaneId(layout, "a", "right")).toBe("b"); + expect(getSpatialNeighborPaneId(layout, "d", "left")).toBe("c"); + expect(getSpatialNeighborPaneId(layout, "a", "left")).toBeNull(); + expect(getSpatialNeighborPaneId(layout, "b", "up")).toBeNull(); + }); + + // 2x2 grid built "columns first": + // outer = horizontal split { left col / right col } + // left col = vertical split { a / c } + // right col = vertical split { b / d } + // + // +---+---+ + // | a | b | + // +---+---+ + // | c | d | + // +---+---+ + it("preserves row alignment in a columns-first 2x2", () => { + const layout: LayoutNode = { + type: "split", + direction: "horizontal", + first: { + type: "split", + direction: "vertical", + first: { type: "pane", paneId: "a" }, + second: { type: "pane", paneId: "c" }, + }, + second: { + type: "split", + direction: "vertical", + first: { type: "pane", paneId: "b" }, + second: { type: "pane", paneId: "d" }, + }, + }; + expect(getSpatialNeighborPaneId(layout, "c", "right")).toBe("d"); + expect(getSpatialNeighborPaneId(layout, "d", "left")).toBe("c"); + expect(getSpatialNeighborPaneId(layout, "a", "right")).toBe("b"); + expect(getSpatialNeighborPaneId(layout, "b", "left")).toBe("a"); + expect(getSpatialNeighborPaneId(layout, "a", "down")).toBe("c"); + expect(getSpatialNeighborPaneId(layout, "b", "down")).toBe("d"); + }); +}); diff --git a/packages/panes/src/core/store/utils/utils.ts b/packages/panes/src/core/store/utils/utils.ts index 6bc4df9a64f..3c1a0c7a52e 100644 --- a/packages/panes/src/core/store/utils/utils.ts +++ b/packages/panes/src/core/store/utils/utils.ts @@ -169,3 +169,66 @@ export function positionToDirection(position: SplitPosition): SplitDirection { export function generateId(prefix: string): string { return `${prefix}-${crypto.randomUUID()}`; } + +export type FocusDirection = "left" | "right" | "up" | "down"; + +export function findPanePath( + node: LayoutNode, + paneId: string, + currentPath: SplitBranch[] = [], +): SplitPath | null { + if (node.type === "pane") { + return node.paneId === paneId ? currentPath : null; + } + const firstPath = findPanePath(node.first, paneId, [...currentPath, "first"]); + if (firstPath) return firstPath; + return findPanePath(node.second, paneId, [...currentPath, "second"]); +} + +// Descent into a sibling subtree once a pivot split has been chosen. +// - On splits matching the arrow axis: pick the near edge (first for +// right/down, second for left/up), preserving alignmentPath. +// - On perpendicular splits: consume one entry from alignmentPath to +// preserve the source pane's cross-axis position; if exhausted, fall +// back to `first`. +function findEdgePaneId( + node: LayoutNode, + dir: FocusDirection, + alignmentPath: SplitPath = [], +): string | null { + if (node.type === "pane") return node.paneId; + const axis: SplitDirection = + dir === "left" || dir === "right" ? "horizontal" : "vertical"; + if (node.direction === axis) { + const nearEdge: SplitBranch = + dir === "right" || dir === "down" ? "first" : "second"; + return findEdgePaneId(node[nearEdge], dir, alignmentPath); + } + const [alignedBranch = "first", ...rest] = alignmentPath; + return findEdgePaneId(node[alignedBranch], dir, rest); +} + +export function getSpatialNeighborPaneId( + root: LayoutNode, + paneId: string, + dir: FocusDirection, +): string | null { + const path = findPanePath(root, paneId); + if (!path) return null; + + const axis: SplitDirection = + dir === "left" || dir === "right" ? "horizontal" : "vertical"; + const wantSecond = dir === "right" || dir === "down"; + + for (let i = path.length - 1; i >= 0; i--) { + const ancestor = getNodeAtPath(root, path.slice(0, i)); + if (!ancestor || ancestor.type !== "split") continue; + if (ancestor.direction !== axis) continue; + const cameFrom = path[i]; + if (wantSecond && cameFrom !== "first") continue; + if (!wantSecond && cameFrom !== "second") continue; + const siblingBranch: SplitBranch = wantSecond ? "second" : "first"; + return findEdgePaneId(ancestor[siblingBranch], dir, path.slice(i + 1)); + } + return null; +} diff --git a/packages/panes/src/index.ts b/packages/panes/src/index.ts index 8f1844c72cb..4e08a915239 100644 --- a/packages/panes/src/index.ts +++ b/packages/panes/src/index.ts @@ -5,6 +5,8 @@ export type { WorkspaceStore, } from "./core/store"; export { createWorkspaceStore } from "./core/store"; +export type { FocusDirection } from "./core/store/utils"; +export { getSpatialNeighborPaneId } from "./core/store/utils"; export type { ContextMenuActionConfig, PaneActionConfig,