mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
fix(app): yield app sidebar to narrow settings
This commit is contained in:
@@ -131,6 +131,8 @@ 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.
|
||||
|
||||
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.
|
||||
|
||||
A new list+detail feature copies the settings shell. A new workspace-shaped feature copies the workspace shell. Inventing a third shape happens in design review, not in a PR.
|
||||
|
||||
@@ -174,4 +174,13 @@ test.describe("Half-screen desktop layout", () => {
|
||||
await expect(page.getByTestId("sidebar-global-new-workspace")).toBeVisible();
|
||||
await expect(page.getByTestId("agent-list-backdrop")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("yields app navigation to the settings split", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await page.getByTestId("sidebar-settings").click();
|
||||
|
||||
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
|
||||
await expect(page.getByTestId("settings-detail-pane")).toBeVisible();
|
||||
await expect(page.getByTestId("sidebar-settings")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { View } from "react-native";
|
||||
import { useWindowDimensions, View } from "react-native";
|
||||
import { GestureDetector, GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { KeyboardProvider } from "react-native-keyboard-controller";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
@@ -37,7 +37,12 @@ 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";
|
||||
import { isNative, isWeb } from "@/constants/platform";
|
||||
import { HorizontalScrollProvider } from "@/contexts/horizontal-scroll-context";
|
||||
import { SessionProvider } from "@/contexts/session-context";
|
||||
@@ -411,6 +416,8 @@ 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 sidebarWidth = usePanelStore((state) => state.sidebarWidth);
|
||||
const { width: viewportWidth } = useWindowDimensions();
|
||||
|
||||
const cycleTheme = useCallback(() => {
|
||||
const currentIndex = THEME_CYCLE_ORDER.indexOf(settings.theme as ThemeName);
|
||||
@@ -463,8 +470,16 @@ function AppContainer({ children, chromeEnabled: chromeEnabledOverride }: AppCon
|
||||
/>
|
||||
);
|
||||
|
||||
const settingsCanShareAppSidebar =
|
||||
!pathname.includes("/settings") ||
|
||||
viewportWidth - resolveDesktopSidebarWidth({ requestedWidth: sidebarWidth, viewportWidth }) >=
|
||||
SETTINGS_DESKTOP_SPLIT_MIN_WIDTH;
|
||||
const desktopSidebarRendered =
|
||||
!isCompactLayout && chromeEnabled && !isFocusModeEnabled && isDesktopAgentListOpen;
|
||||
!isCompactLayout &&
|
||||
chromeEnabled &&
|
||||
!isFocusModeEnabled &&
|
||||
isDesktopAgentListOpen &&
|
||||
settingsCanShareAppSidebar;
|
||||
const contentWindowChromeCorners = desktopSidebarRendered ? "top-right" : "both";
|
||||
const workspaceChrome = (
|
||||
<View style={rowStyle}>
|
||||
|
||||
@@ -15,6 +15,13 @@ export const HEADER_TOP_PADDING_MOBILE = 8;
|
||||
export const MAX_CONTENT_WIDTH = 820;
|
||||
export const COMPACT_FORM_FACTOR_WIDTH = 500;
|
||||
|
||||
// Settings uses the canonical desktop list + detail layout. Its sidebar and
|
||||
// detail target must fit together before it can share width with app navigation.
|
||||
export const SETTINGS_DESKTOP_SIDEBAR_WIDTH = 320;
|
||||
export const SETTINGS_DESKTOP_DETAIL_MIN_WIDTH = 400;
|
||||
export const SETTINGS_DESKTOP_SPLIT_MIN_WIDTH =
|
||||
SETTINGS_DESKTOP_SIDEBAR_WIDTH + SETTINGS_DESKTOP_DETAIL_MIN_WIDTH;
|
||||
|
||||
// Desktop app constants for macOS traffic light buttons
|
||||
// These buttons (close/minimize/maximize) overlay the top-left corner
|
||||
export const DESKTOP_TRAFFIC_LIGHT_WIDTH = 78;
|
||||
|
||||
@@ -97,7 +97,7 @@ import {
|
||||
} from "@/screens/settings/host-page";
|
||||
import ProjectsScreen from "@/screens/projects-screen";
|
||||
import ProjectSettingsScreen from "@/screens/project-settings-screen";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { SETTINGS_DESKTOP_SIDEBAR_WIDTH, useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
||||
import {
|
||||
type EnableBuiltInDaemonOption,
|
||||
@@ -1650,7 +1650,7 @@ const desktopStyles = StyleSheet.create((theme) => ({
|
||||
|
||||
const sidebarStyles = StyleSheet.create((theme) => ({
|
||||
desktopContainer: {
|
||||
width: 320,
|
||||
width: SETTINGS_DESKTOP_SIDEBAR_WIDTH,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.surfaceSidebar,
|
||||
|
||||
@@ -90,6 +90,7 @@ async function inspectSettingsGeometry(page) {
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
sidebarRect: rect('[data-testid="settings-sidebar"]'),
|
||||
detailPaneRect: rect('[data-testid="settings-detail-pane"]'),
|
||||
outerAppSidebarSettingsRect: rect('[data-testid="sidebar-settings"]'),
|
||||
backButtonRect: rect('[data-testid="settings-back-to-workspace"]'),
|
||||
detailTitleRect: rect('[data-testid="settings-detail-header-title"]'),
|
||||
detailHeaderLeftRect: headerLeftBounds
|
||||
@@ -423,8 +424,10 @@ async function inspectHalfScreenSettingsLayout(page, platform) {
|
||||
|
||||
const sidebar = page.getByTestId("settings-sidebar");
|
||||
const detail = page.getByTestId("settings-detail-pane");
|
||||
const outerAppSidebarSettings = page.getByTestId("sidebar-settings");
|
||||
await sidebar.waitFor({ state: "visible", timeout: 10_000 });
|
||||
await detail.waitFor({ state: "visible", timeout: 10_000 });
|
||||
await outerAppSidebarSettings.waitFor({ state: "hidden", timeout: 10_000 });
|
||||
|
||||
const details = await inspectSettingsGeometry(page);
|
||||
const obstruction = getWindowChromeObstruction(platform, details.innerWidth);
|
||||
@@ -450,6 +453,7 @@ async function inspectHalfScreenSettingsLayout(page, platform) {
|
||||
details.sidebarRect.width >= 300 &&
|
||||
details.detailPaneRect !== null &&
|
||||
details.detailPaneRect.width >= 400 &&
|
||||
details.outerAppSidebarSettingsRect === null &&
|
||||
Math.abs(details.sidebarRect.left) <= 1 &&
|
||||
sidebarRight !== null &&
|
||||
Math.abs(sidebarRight - details.detailPaneRect.left) <= 1 &&
|
||||
@@ -732,6 +736,11 @@ async function main() {
|
||||
|
||||
await collectSettingsSplitResult(page, serverId, desktopStatus, results);
|
||||
|
||||
if (outerSidebarDismissed) {
|
||||
await restoreOuterAppSidebar(page, outerSidebarDismissed);
|
||||
outerSidebarDismissed = false;
|
||||
}
|
||||
|
||||
const halfScreenDetails = await inspectHalfScreenSettingsLayout(
|
||||
page,
|
||||
desktopDetection.platform,
|
||||
|
||||
Reference in New Issue
Block a user