Keep desktop sidebar controls stable across panel changes (#2078)
* fix(app): stabilize desktop sidebar controls Keep the left-window-controls toggle mounted across sidebar state changes so its position and tooltip state remain stable. Preserve browser placement and show an explicit Explorer close control only where native right-side controls do not provide one. * fix(app): suppress inactive sidebar chrome
This commit is contained in:
@@ -5,10 +5,11 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { PanelLeft } from "lucide-react-native";
|
||||
import { ScreenHeader } from "./screen-header";
|
||||
import { ScreenTitle } from "./screen-title";
|
||||
import { HeaderToggleButton } from "./header-toggle-button";
|
||||
import { HeaderToggleButton, headerIconSlotStyle } from "./header-toggle-button";
|
||||
import { selectIsAgentListOpen, usePanelStore } from "@/stores/panel-store";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||
import { useHasWindowChromeObstruction, useOwnsWindowChromeCorner } from "@/utils/desktop-window";
|
||||
|
||||
interface MenuHeaderProps {
|
||||
title?: string;
|
||||
@@ -42,15 +43,18 @@ function MobileMenuIcon({ color }: { color: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarMenuToggle({
|
||||
style,
|
||||
function SidebarMenuToggleButton({
|
||||
isMobile,
|
||||
resolvedStyle,
|
||||
tooltipSide = "right",
|
||||
testID = "menu-button",
|
||||
nativeID = "menu-button",
|
||||
}: SidebarMenuToggleProps = {}) {
|
||||
}: Omit<SidebarMenuToggleProps, "style"> & {
|
||||
isMobile: boolean;
|
||||
resolvedStyle: StyleProp<ViewStyle>;
|
||||
}) {
|
||||
const { theme } = useUnistyles();
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useIsCompactFormFactor();
|
||||
const isOpen = usePanelStore((state) => selectIsAgentListOpen(state, { isCompact: isMobile }));
|
||||
const toggleAgentListForLayout = usePanelStore((state) => state.toggleAgentListForLayout);
|
||||
const toggleShortcutKeys = useMemo(
|
||||
@@ -58,9 +62,6 @@ export function SidebarMenuToggle({
|
||||
[],
|
||||
);
|
||||
|
||||
const menuIconColor =
|
||||
!isMobile && isOpen ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
toggleAgentListForLayout({ isCompact: isMobile });
|
||||
}, [toggleAgentListForLayout, isMobile]);
|
||||
@@ -75,21 +76,54 @@ export function SidebarMenuToggle({
|
||||
tooltipSide={tooltipSide}
|
||||
testID={testID}
|
||||
nativeID={nativeID}
|
||||
style={style}
|
||||
style={resolvedStyle}
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={isOpen ? t("shell.menu.close") : t("shell.menu.open")}
|
||||
accessibilityState={accessibilityState}
|
||||
>
|
||||
{isMobile ? (
|
||||
<MobileMenuIcon color={menuIconColor} />
|
||||
) : (
|
||||
<PanelLeft size={theme.iconSize.md} color={menuIconColor} />
|
||||
)}
|
||||
{({ hovered, pressed }) => {
|
||||
const color = hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
return isMobile ? (
|
||||
<MobileMenuIcon color={color} />
|
||||
) : (
|
||||
<PanelLeft size={theme.iconSize.md} color={color} />
|
||||
);
|
||||
}}
|
||||
</HeaderToggleButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarMenuToggle({ style, ...props }: SidebarMenuToggleProps = {}) {
|
||||
const isMobile = useIsCompactFormFactor();
|
||||
const ownsTopLeft = useOwnsWindowChromeCorner("top-left");
|
||||
const hasTopLeftWindowControls = useHasWindowChromeObstruction("top-left");
|
||||
const resolvedStyle = useMemo(() => [styles.leadingToggle, style], [style]);
|
||||
const placeholderStyle = useMemo(
|
||||
() => [headerIconSlotStyle.slot, resolvedStyle],
|
||||
[resolvedStyle],
|
||||
);
|
||||
|
||||
if (!isMobile && !ownsTopLeft) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isMobile && hasTopLeftWindowControls) {
|
||||
return (
|
||||
<View pointerEvents="none" style={placeholderStyle}>
|
||||
<View style={styles.desktopMenuIconSpace} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <SidebarMenuToggleButton {...props} isMobile={isMobile} resolvedStyle={resolvedStyle} />;
|
||||
}
|
||||
|
||||
export function WindowSidebarMenuToggle({ style, ...props }: SidebarMenuToggleProps = {}) {
|
||||
const resolvedStyle = useMemo(() => [styles.leadingToggle, style], [style]);
|
||||
return <SidebarMenuToggleButton {...props} isMobile={false} resolvedStyle={resolvedStyle} />;
|
||||
}
|
||||
|
||||
export function MenuHeader({ title, rightContent, borderless }: MenuHeaderProps) {
|
||||
return (
|
||||
<ScreenHeader
|
||||
@@ -107,6 +141,12 @@ export function MenuHeader({ title, rightContent, borderless }: MenuHeaderProps)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
leadingToggle: {
|
||||
marginLeft: {
|
||||
xs: 0,
|
||||
md: -theme.spacing[2],
|
||||
},
|
||||
},
|
||||
left: {
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
@@ -116,6 +156,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
desktopMenuIconSpace: {
|
||||
width: theme.iconSize.md,
|
||||
height: theme.iconSize.md,
|
||||
},
|
||||
mobileMenuLine: {
|
||||
width: MOBILE_MENU_LINE_WIDTH,
|
||||
height: MOBILE_MENU_LINE_HEIGHT,
|
||||
|
||||
@@ -38,7 +38,7 @@ export function ScreenHeader({
|
||||
const isMobile = useIsCompactFormFactor();
|
||||
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
|
||||
const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
|
||||
const baseHorizontalPadding = theme.spacing[2];
|
||||
const baseHorizontalPadding = isMobile ? theme.spacing[2] : theme.spacing[3];
|
||||
|
||||
const innerStyle = useMemo(
|
||||
() => [styles.inner, { paddingTop: insets.top + topPadding }],
|
||||
|
||||
Reference in New Issue
Block a user