mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
fix(app): yield navigation to narrow workspace explorer
This commit is contained in:
+1
-1
@@ -131,7 +131,7 @@ The branching is one `useIsCompactFormFactor()` check at the top of the screen c
|
||||
|
||||
The workspace screen (`packages/app/src/screens/workspace/workspace-screen.tsx`) follows a different but parallel rule: tabs collapse on compact, panes split on desktop. The sidebar (`packages/app/src/components/left-sidebar.tsx`) is overlaid on compact and pinned on desktop.
|
||||
|
||||
On a narrow desktop settings route, the app navigation yields to Settings' own 320px list + detail split when the remaining width cannot preserve its 400px detail target. That is a topology decision at the app container, not a second compact breakpoint.
|
||||
On a narrow desktop route, app navigation yields to the rendered content topology when the remaining width cannot preserve its center target: Settings keeps its 320px list + 400px detail split, and a workspace Explorer keeps its current visible width plus a 400px center pane. That is a topology decision at the app container, not a second compact breakpoint. Temporary width clamps are render-only; widening restores the user's saved sidebar widths.
|
||||
|
||||
Electron window controls are top-corner obstructions, not a compact-layout condition. Rendered surfaces declare which top corners they physically occupy; only those corners receive clearance. Full-window overlays redeclare both corners. A focused split pane owns both corners; if focus restoration temporarily exposes the full split tree, the split boundary reserves one top strip instead of assigning a control rectangle to an arbitrarily narrow leaf. The 720px desktop breakpoint preserves the default 320px sidebar and target 400px center width when the Explorer is closed; it is product policy, not an obstruction gate.
|
||||
|
||||
|
||||
@@ -183,4 +183,28 @@ test.describe("Half-screen desktop layout", () => {
|
||||
await expect(page.getByTestId("settings-detail-pane")).toBeVisible();
|
||||
await expect(page.getByTestId("sidebar-settings")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("yields app navigation to the Explorer", async ({ page }) => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "sidebar-half-screen-explorer-" });
|
||||
|
||||
try {
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarProject(page, path.basename(workspace.repoPath));
|
||||
await openWorkspaceFromSidebar(page, workspace.workspaceId);
|
||||
|
||||
await page.getByTestId("workspace-explorer-toggle").first().click();
|
||||
await expect(
|
||||
page.getByTestId("explorer-tab-files").filter({ visible: true }).first(),
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId("sidebar-global-new-workspace")).not.toBeVisible();
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await page.getByTestId("workspace-tabs-row").first().boundingBox())?.width ?? 0,
|
||||
)
|
||||
.toBeGreaterThanOrEqual(400);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,12 +37,11 @@ import { WorkspaceSetupDialog } from "@/components/workspace-setup-dialog";
|
||||
import { WorkspaceShortcutTargetsSubscriber } from "@/components/workspace-shortcut-targets-subscriber";
|
||||
import { FloatingPanelPortalHost } from "@/components/ui/floating-panel-portal";
|
||||
import { HostChooserModal, useHostChooser } from "@/hosts/host-chooser";
|
||||
import { getIsElectronRuntime, useIsCompactFormFactor } from "@/constants/layout";
|
||||
import {
|
||||
getIsElectronRuntime,
|
||||
SETTINGS_DESKTOP_SPLIT_MIN_WIDTH,
|
||||
useIsCompactFormFactor,
|
||||
} from "@/constants/layout";
|
||||
import { resolveDesktopSidebarWidth } from "@/components/left-sidebar-width";
|
||||
canDesktopAppSidebarShare,
|
||||
resolveDesktopAppContentMinimum,
|
||||
} from "@/components/desktop-sidebar-layout";
|
||||
import { isNative, isWeb } from "@/constants/platform";
|
||||
import { HorizontalScrollProvider } from "@/contexts/horizontal-scroll-context";
|
||||
import { SessionProvider } from "@/contexts/session-context";
|
||||
@@ -416,7 +415,9 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
|
||||
const toggleFocusMode = usePanelStore((state) => state.toggleFocusMode);
|
||||
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
||||
const isDesktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
||||
const isDesktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||
const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
|
||||
const explorerWidth = usePanelStore((state) => state.explorerWidth);
|
||||
const { width: viewportWidth } = useWindowDimensions();
|
||||
|
||||
const cycleTheme = useCallback(() => {
|
||||
@@ -470,16 +471,22 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
|
||||
/>
|
||||
);
|
||||
|
||||
const settingsCanShareAppSidebar =
|
||||
!pathname.includes("/settings") ||
|
||||
viewportWidth - resolveDesktopSidebarWidth({ requestedWidth: sidebarWidth, viewportWidth }) >=
|
||||
SETTINGS_DESKTOP_SPLIT_MIN_WIDTH;
|
||||
const appContentMinimumWidth = resolveDesktopAppContentMinimum({
|
||||
isSettingsRoute: pathname.includes("/settings"),
|
||||
isWorkspaceExplorerOpen: pathname.includes("/workspace/") && isDesktopFileExplorerOpen,
|
||||
requestedExplorerWidth: explorerWidth,
|
||||
viewportWidth,
|
||||
});
|
||||
const desktopSidebarRendered =
|
||||
!isCompactLayout &&
|
||||
chromeEnabled &&
|
||||
!isFocusModeEnabled &&
|
||||
isDesktopAgentListOpen &&
|
||||
settingsCanShareAppSidebar;
|
||||
canDesktopAppSidebarShare({
|
||||
contentMinimumWidth: appContentMinimumWidth,
|
||||
requestedSidebarWidth: sidebarWidth,
|
||||
viewportWidth,
|
||||
});
|
||||
const contentWindowChromeCorners = desktopSidebarRendered ? "top-right" : "both";
|
||||
const workspaceChrome = (
|
||||
<View style={rowStyle}>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
canDesktopAppSidebarShare,
|
||||
resolveDesktopAppContentMinimum,
|
||||
resolveDesktopExplorerWidth,
|
||||
resolveDesktopSidebarWidth,
|
||||
} from "@/components/desktop-sidebar-layout";
|
||||
|
||||
describe("desktop sidebar layout", () => {
|
||||
it("clamps a persisted wide sidebar to preserve the center pane", () => {
|
||||
const atHalfScreen = resolveDesktopSidebarWidth({ requestedWidth: 600, viewportWidth: 751 });
|
||||
expect(atHalfScreen).toBe(351);
|
||||
expect(751 - atHalfScreen).toBe(400);
|
||||
|
||||
const atBreakpoint = resolveDesktopSidebarWidth({ requestedWidth: 600, viewportWidth: 720 });
|
||||
expect(atBreakpoint).toBe(320);
|
||||
expect(720 - atBreakpoint).toBe(400);
|
||||
|
||||
expect(resolveDesktopSidebarWidth({ requestedWidth: 600, viewportWidth: 1440 })).toBe(600);
|
||||
});
|
||||
|
||||
it("keeps a temporarily narrow explorer render-only", () => {
|
||||
expect(resolveDesktopExplorerWidth({ requestedWidth: 400, viewportWidth: 751 })).toBe(351);
|
||||
expect(resolveDesktopExplorerWidth({ requestedWidth: 400, viewportWidth: 1440 })).toBe(400);
|
||||
});
|
||||
|
||||
it("yields app navigation when settings or Explorer need the shell width", () => {
|
||||
const settingsMinimum = resolveDesktopAppContentMinimum({
|
||||
isSettingsRoute: true,
|
||||
isWorkspaceExplorerOpen: false,
|
||||
requestedExplorerWidth: 400,
|
||||
viewportWidth: 751,
|
||||
});
|
||||
expect(settingsMinimum).toBe(720);
|
||||
expect(
|
||||
canDesktopAppSidebarShare({
|
||||
contentMinimumWidth: settingsMinimum,
|
||||
requestedSidebarWidth: 320,
|
||||
viewportWidth: 751,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
const explorerMinimum = resolveDesktopAppContentMinimum({
|
||||
isSettingsRoute: false,
|
||||
isWorkspaceExplorerOpen: true,
|
||||
requestedExplorerWidth: 400,
|
||||
viewportWidth: 751,
|
||||
});
|
||||
expect(explorerMinimum).toBe(751);
|
||||
expect(
|
||||
canDesktopAppSidebarShare({
|
||||
contentMinimumWidth: explorerMinimum,
|
||||
requestedSidebarWidth: 320,
|
||||
viewportWidth: 751,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
canDesktopAppSidebarShare({
|
||||
contentMinimumWidth: resolveDesktopAppContentMinimum({
|
||||
isSettingsRoute: false,
|
||||
isWorkspaceExplorerOpen: true,
|
||||
requestedExplorerWidth: 400,
|
||||
viewportWidth: 1120,
|
||||
}),
|
||||
requestedSidebarWidth: 320,
|
||||
viewportWidth: 1120,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { SETTINGS_DESKTOP_SPLIT_MIN_WIDTH } from "@/constants/layout";
|
||||
import {
|
||||
MAX_EXPLORER_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
} from "@/stores/panel-store";
|
||||
|
||||
export const MIN_DESKTOP_CENTER_WIDTH = 400;
|
||||
|
||||
function resolveDesktopPanelWidth(input: {
|
||||
requestedWidth: number;
|
||||
viewportWidth: number;
|
||||
minimumWidth: number;
|
||||
maximumWidth: number;
|
||||
}): number {
|
||||
"worklet";
|
||||
const maximumVisibleWidth = Math.max(
|
||||
input.minimumWidth,
|
||||
Math.min(input.maximumWidth, input.viewportWidth - MIN_DESKTOP_CENTER_WIDTH),
|
||||
);
|
||||
return Math.max(input.minimumWidth, Math.min(maximumVisibleWidth, input.requestedWidth));
|
||||
}
|
||||
|
||||
export function resolveDesktopSidebarWidth(input: {
|
||||
requestedWidth: number;
|
||||
viewportWidth: number;
|
||||
}): number {
|
||||
return resolveDesktopPanelWidth({
|
||||
...input,
|
||||
minimumWidth: MIN_SIDEBAR_WIDTH,
|
||||
maximumWidth: MAX_SIDEBAR_WIDTH,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveDesktopExplorerWidth(input: {
|
||||
requestedWidth: number;
|
||||
viewportWidth: number;
|
||||
}): number {
|
||||
return resolveDesktopPanelWidth({
|
||||
...input,
|
||||
minimumWidth: MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
maximumWidth: MAX_EXPLORER_SIDEBAR_WIDTH,
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveDesktopAppContentMinimum(input: {
|
||||
isSettingsRoute: boolean;
|
||||
isWorkspaceExplorerOpen: boolean;
|
||||
requestedExplorerWidth: number;
|
||||
viewportWidth: number;
|
||||
}): number {
|
||||
const workspaceMinimum = input.isWorkspaceExplorerOpen
|
||||
? MIN_DESKTOP_CENTER_WIDTH +
|
||||
resolveDesktopExplorerWidth({
|
||||
requestedWidth: input.requestedExplorerWidth,
|
||||
viewportWidth: input.viewportWidth,
|
||||
})
|
||||
: 0;
|
||||
return Math.max(input.isSettingsRoute ? SETTINGS_DESKTOP_SPLIT_MIN_WIDTH : 0, workspaceMinimum);
|
||||
}
|
||||
|
||||
export function canDesktopAppSidebarShare(input: {
|
||||
contentMinimumWidth: number;
|
||||
requestedSidebarWidth: number;
|
||||
viewportWidth: number;
|
||||
}): boolean {
|
||||
return (
|
||||
input.viewportWidth -
|
||||
resolveDesktopSidebarWidth({
|
||||
requestedWidth: input.requestedSidebarWidth,
|
||||
viewportWidth: input.viewportWidth,
|
||||
}) >=
|
||||
input.contentMinimumWidth
|
||||
);
|
||||
}
|
||||
@@ -22,13 +22,7 @@ import {
|
||||
} from "@/git/pull-request-panel";
|
||||
import { useCheckoutGitActionsStore } from "@/git/actions-store";
|
||||
import type { UsePrPaneDataResult } from "@/git/pull-request-panel/use-data";
|
||||
import {
|
||||
usePanelStore,
|
||||
selectIsFileExplorerOpen,
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
MAX_EXPLORER_SIDEBAR_WIDTH,
|
||||
type ExplorerTab,
|
||||
} from "@/stores/panel-store";
|
||||
import { usePanelStore, selectIsFileExplorerOpen, type ExplorerTab } from "@/stores/panel-store";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useCloseFileExplorerGesture } from "@/mobile-panels/gestures";
|
||||
import { MobilePanelOverlay } from "@/mobile-panels/presentation";
|
||||
@@ -41,8 +35,8 @@ import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||
import { RetainedPanelActivity } from "@/components/retained-panel";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { buildWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store";
|
||||
import { resolveDesktopExplorerWidth } from "@/components/desktop-sidebar-layout";
|
||||
|
||||
const MIN_CHAT_WIDTH = 400;
|
||||
function logExplorerSidebar(_event: string, _details: Record<string, unknown>): void {}
|
||||
|
||||
interface ExplorerSidebarProps {
|
||||
@@ -163,18 +157,16 @@ export function ExplorerSidebar({
|
||||
isGit,
|
||||
});
|
||||
const { width: viewportWidth } = useWindowDimensions();
|
||||
const startWidthRef = useRef(explorerWidth);
|
||||
const resizeWidth = useSharedValue(explorerWidth);
|
||||
const visibleExplorerWidth = resolveDesktopExplorerWidth({
|
||||
requestedWidth: explorerWidth,
|
||||
viewportWidth,
|
||||
});
|
||||
const startWidthRef = useRef(visibleExplorerWidth);
|
||||
const resizeWidth = useSharedValue(visibleExplorerWidth);
|
||||
|
||||
useEffect(() => {
|
||||
const maxWidth = Math.max(
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_EXPLORER_SIDEBAR_WIDTH, viewportWidth - MIN_CHAT_WIDTH),
|
||||
);
|
||||
if (explorerWidth > maxWidth) {
|
||||
setExplorerWidth(maxWidth);
|
||||
}
|
||||
}, [explorerWidth, setExplorerWidth, viewportWidth]);
|
||||
resizeWidth.value = visibleExplorerWidth;
|
||||
}, [resizeWidth, visibleExplorerWidth]);
|
||||
|
||||
const handleDesktopClose = useCallback(() => {
|
||||
logExplorerSidebar("handleClose", {
|
||||
@@ -190,22 +182,20 @@ export function ExplorerSidebar({
|
||||
.enabled(true)
|
||||
.hitSlop({ left: 8, right: 8, top: 0, bottom: 0 })
|
||||
.onStart(() => {
|
||||
startWidthRef.current = explorerWidth;
|
||||
resizeWidth.value = explorerWidth;
|
||||
startWidthRef.current = visibleExplorerWidth;
|
||||
resizeWidth.value = visibleExplorerWidth;
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
const newWidth = startWidthRef.current - event.translationX;
|
||||
const maxWidth = Math.max(
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_EXPLORER_SIDEBAR_WIDTH, viewportWidth - MIN_CHAT_WIDTH),
|
||||
);
|
||||
const clampedWidth = Math.max(MIN_EXPLORER_SIDEBAR_WIDTH, Math.min(maxWidth, newWidth));
|
||||
resizeWidth.value = clampedWidth;
|
||||
resizeWidth.value = resolveDesktopExplorerWidth({
|
||||
requestedWidth: newWidth,
|
||||
viewportWidth,
|
||||
});
|
||||
})
|
||||
.onEnd(() => {
|
||||
runOnJS(setExplorerWidth)(resizeWidth.value);
|
||||
}),
|
||||
[explorerWidth, resizeWidth, setExplorerWidth, viewportWidth],
|
||||
[resizeWidth, setExplorerWidth, viewportWidth, visibleExplorerWidth],
|
||||
);
|
||||
|
||||
const resizeAnimatedStyle = useAnimatedStyle(() => ({
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveDesktopSidebarWidth } from "@/components/left-sidebar-width";
|
||||
|
||||
describe("desktop sidebar width", () => {
|
||||
it("clamps a persisted wide sidebar to preserve 400px of content", () => {
|
||||
const atHalfScreen = resolveDesktopSidebarWidth({ requestedWidth: 600, viewportWidth: 751 });
|
||||
expect(atHalfScreen).toBe(351);
|
||||
expect(751 - atHalfScreen).toBe(400);
|
||||
|
||||
const atBreakpoint = resolveDesktopSidebarWidth({ requestedWidth: 600, viewportWidth: 720 });
|
||||
expect(atBreakpoint).toBe(320);
|
||||
expect(720 - atBreakpoint).toBe(400);
|
||||
|
||||
expect(resolveDesktopSidebarWidth({ requestedWidth: 600, viewportWidth: 1440 })).toBe(600);
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { MAX_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH } from "@/stores/panel-store";
|
||||
|
||||
const MIN_CHAT_WIDTH = 400;
|
||||
|
||||
export function resolveDesktopSidebarWidth(input: {
|
||||
requestedWidth: number;
|
||||
viewportWidth: number;
|
||||
}): number {
|
||||
"worklet";
|
||||
const maxWidth = Math.max(
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_SIDEBAR_WIDTH, input.viewportWidth - MIN_CHAT_WIDTH),
|
||||
);
|
||||
return Math.max(MIN_SIDEBAR_WIDTH, Math.min(maxWidth, input.requestedWidth));
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-nativ
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||
import { resolveDesktopSidebarWidth } from "@/components/left-sidebar-width";
|
||||
import { resolveDesktopSidebarWidth } from "@/components/desktop-sidebar-layout";
|
||||
import { HostPicker } from "@/components/hosts/host-picker";
|
||||
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
|
||||
import { SidebarDisplayPreferencesMenu } from "@/components/sidebar/sidebar-display-preferences-menu";
|
||||
|
||||
Reference in New Issue
Block a user