fix(desktop): toggle browser icon and preserve webview state on coll… (#1424)
* fix(desktop): toggle browser icon and preserve webview state on collapse * fix(desktop): stabilize context panel collapse behavior --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
51901b980e
commit
6a173211b0
@@ -23,7 +23,7 @@ import { Icon } from "@/components/icon/Icon";
|
|||||||
import { OpenChamberLogo } from "@/components/ui/OpenChamberLogo";
|
import { OpenChamberLogo } from "@/components/ui/OpenChamberLogo";
|
||||||
import { invokeDesktopCommand } from '@/lib/desktopNative';
|
import { invokeDesktopCommand } from '@/lib/desktopNative';
|
||||||
|
|
||||||
const CONTEXT_PANEL_MIN_WIDTH = 360;
|
const CONTEXT_PANEL_MIN_WIDTH = 380;
|
||||||
const CONTEXT_PANEL_MAX_WIDTH = 1400;
|
const CONTEXT_PANEL_MAX_WIDTH = 1400;
|
||||||
const CONTEXT_PANEL_DEFAULT_WIDTH = 600;
|
const CONTEXT_PANEL_DEFAULT_WIDTH = 600;
|
||||||
const CONTEXT_TAB_LABEL_MAX_CHARS = 24;
|
const CONTEXT_TAB_LABEL_MAX_CHARS = 24;
|
||||||
@@ -1588,6 +1588,7 @@ export const ContextPanel: React.FC = () => {
|
|||||||
const width = clampWidth(panelState?.width ?? CONTEXT_PANEL_DEFAULT_WIDTH);
|
const width = clampWidth(panelState?.width ?? CONTEXT_PANEL_DEFAULT_WIDTH);
|
||||||
|
|
||||||
const [isResizing, setIsResizing] = React.useState(false);
|
const [isResizing, setIsResizing] = React.useState(false);
|
||||||
|
const [suppressWidthTransition, setSuppressWidthTransition] = React.useState(false);
|
||||||
const startXRef = React.useRef(0);
|
const startXRef = React.useRef(0);
|
||||||
const startWidthRef = React.useRef(width);
|
const startWidthRef = React.useRef(width);
|
||||||
const resizingWidthRef = React.useRef<number | null>(null);
|
const resizingWidthRef = React.useRef<number | null>(null);
|
||||||
@@ -1595,6 +1596,41 @@ export const ContextPanel: React.FC = () => {
|
|||||||
const panelRef = React.useRef<HTMLElement | null>(null);
|
const panelRef = React.useRef<HTMLElement | null>(null);
|
||||||
const chatFrameRefs = React.useRef<Map<string, HTMLIFrameElement>>(new Map());
|
const chatFrameRefs = React.useRef<Map<string, HTMLIFrameElement>>(new Map());
|
||||||
const wasOpenRef = React.useRef(false);
|
const wasOpenRef = React.useRef(false);
|
||||||
|
const previousIsOpenRef = React.useRef(isOpen);
|
||||||
|
const suppressWidthTransitionFrameRef = React.useRef<number | null>(null);
|
||||||
|
|
||||||
|
const suppressWidthTransitionForFrame = React.useCallback(() => {
|
||||||
|
setSuppressWidthTransition(true);
|
||||||
|
if (suppressWidthTransitionFrameRef.current !== null) {
|
||||||
|
window.cancelAnimationFrame(suppressWidthTransitionFrameRef.current);
|
||||||
|
}
|
||||||
|
suppressWidthTransitionFrameRef.current = window.requestAnimationFrame(() => {
|
||||||
|
suppressWidthTransitionFrameRef.current = null;
|
||||||
|
setSuppressWidthTransition(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => () => {
|
||||||
|
if (suppressWidthTransitionFrameRef.current !== null) {
|
||||||
|
window.cancelAnimationFrame(suppressWidthTransitionFrameRef.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
const wasOpen = previousIsOpenRef.current;
|
||||||
|
previousIsOpenRef.current = isOpen;
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
setSuppressWidthTransition(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
suppressWidthTransitionForFrame();
|
||||||
|
}, [isOpen, suppressWidthTransitionForFrame]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isOpen || wasOpenRef.current) {
|
if (!isOpen || wasOpenRef.current) {
|
||||||
@@ -1666,11 +1702,13 @@ export const ContextPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const finalWidth = clampWidthToAvailableSpace(resizingWidthRef.current ?? width, panelRef.current);
|
const finalWidth = clampWidthToAvailableSpace(resizingWidthRef.current ?? width, panelRef.current);
|
||||||
|
suppressWidthTransitionForFrame();
|
||||||
|
applyLiveWidth(finalWidth);
|
||||||
|
resizingWidthRef.current = finalWidth;
|
||||||
|
setContextPanelWidth(directoryKey, finalWidth);
|
||||||
setIsResizing(false);
|
setIsResizing(false);
|
||||||
activeResizePointerIDRef.current = null;
|
activeResizePointerIDRef.current = null;
|
||||||
resizingWidthRef.current = null;
|
}, [applyLiveWidth, directoryKey, setContextPanelWidth, suppressWidthTransitionForFrame, width]);
|
||||||
setContextPanelWidth(directoryKey, finalWidth);
|
|
||||||
}, [directoryKey, setContextPanelWidth, width]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isResizing) {
|
if (!isResizing) {
|
||||||
@@ -1910,36 +1948,44 @@ export const ContextPanel: React.FC = () => {
|
|||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isOpen) {
|
const panelStyle: React.CSSProperties = !isOpen
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const panelStyle: React.CSSProperties = isExpanded
|
|
||||||
? {
|
? {
|
||||||
['--oc-context-panel-width' as string]: '100%',
|
|
||||||
width: '100%',
|
|
||||||
minWidth: '100%',
|
|
||||||
maxWidth: '100%',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
width: 'min(var(--oc-context-panel-width), 100%)',
|
|
||||||
minWidth: `min(${CONTEXT_PANEL_MIN_WIDTH}px, 100%)`,
|
|
||||||
maxWidth: '100%',
|
|
||||||
['--oc-context-panel-width' as string]: `${isResizing ? (resizingWidthRef.current ?? width) : width}px`,
|
['--oc-context-panel-width' as string]: `${isResizing ? (resizingWidthRef.current ?? width) : width}px`,
|
||||||
};
|
width: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: 0,
|
||||||
|
opacity: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
visibility: 'hidden',
|
||||||
|
}
|
||||||
|
: isExpanded
|
||||||
|
? {
|
||||||
|
['--oc-context-panel-width' as string]: '100%',
|
||||||
|
width: '100%',
|
||||||
|
minWidth: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
width: 'min(var(--oc-context-panel-width), 100%)',
|
||||||
|
minWidth: `min(${CONTEXT_PANEL_MIN_WIDTH}px, 100%)`,
|
||||||
|
maxWidth: '100%',
|
||||||
|
['--oc-context-panel-width' as string]: `${isResizing ? (resizingWidthRef.current ?? width) : width}px`,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
ref={panelRef}
|
ref={panelRef}
|
||||||
data-context-panel="true"
|
data-context-panel="true"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
inert={!isOpen || undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex min-h-0 flex-col overflow-hidden bg-background',
|
'flex min-h-0 flex-col overflow-hidden bg-background',
|
||||||
!isExpanded && 'border-l border-border/40',
|
!isExpanded && 'border-l border-border/40',
|
||||||
isExpanded
|
isExpanded
|
||||||
? 'absolute inset-0 z-20 min-w-0'
|
? 'absolute inset-0 z-20 min-w-0'
|
||||||
: 'relative h-full flex-shrink-0',
|
: 'relative h-full flex-shrink-0',
|
||||||
isResizing ? 'transition-none' : 'transition-[width] duration-200 ease-in-out'
|
!isOpen && 'pointer-events-none',
|
||||||
|
isResizing || !isOpen || suppressWidthTransition ? 'transition-none' : 'transition-[width] duration-200 ease-in-out'
|
||||||
)}
|
)}
|
||||||
onKeyDownCapture={handlePanelKeyDownCapture}
|
onKeyDownCapture={handlePanelKeyDownCapture}
|
||||||
style={panelStyle}
|
style={panelStyle}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ type HeaderIconActionButtonProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
Icon: IconName;
|
Icon: IconName;
|
||||||
iconClassName?: string;
|
iconClassName?: string;
|
||||||
|
pressed?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
|
const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
|
||||||
@@ -91,6 +92,7 @@ const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
|
|||||||
className,
|
className,
|
||||||
Icon: iconName,
|
Icon: iconName,
|
||||||
iconClassName,
|
iconClassName,
|
||||||
|
pressed = false,
|
||||||
}: HeaderIconActionButtonProps) {
|
}: HeaderIconActionButtonProps) {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return null;
|
return null;
|
||||||
@@ -103,7 +105,11 @@ const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
className={className ?? DESKTOP_HEADER_ICON_BUTTON_CLASS}
|
aria-pressed={pressed}
|
||||||
|
className={cn(
|
||||||
|
className ?? DESKTOP_HEADER_ICON_BUTTON_CLASS,
|
||||||
|
pressed && 'bg-interactive-selection text-interactive-selection-foreground'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Icon name={iconName} className={iconClassName ?? 'h-[18px] w-[18px]'} />
|
<Icon name={iconName} className={iconClassName ?? 'h-[18px] w-[18px]'} />
|
||||||
</button>
|
</button>
|
||||||
@@ -1426,8 +1432,15 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
if (!directory) {
|
if (!directory) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const panelState = contextPanelByDirectory[directory];
|
||||||
|
if (getActiveContextMode(panelState) === 'browser') {
|
||||||
|
closeContextPanel(directory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
openContextBrowser(directory);
|
openContextBrowser(directory);
|
||||||
}, [openContextBrowser, openDirectory]);
|
}, [closeContextPanel, contextPanelByDirectory, openContextBrowser, openDirectory]);
|
||||||
|
|
||||||
const isContextPlanActive = React.useMemo(() => {
|
const isContextPlanActive = React.useMemo(() => {
|
||||||
const directory = normalize(openDirectory || '');
|
const directory = normalize(openDirectory || '');
|
||||||
@@ -1438,6 +1451,15 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
return getActiveContextMode(panelState) === 'plan';
|
return getActiveContextMode(panelState) === 'plan';
|
||||||
}, [contextPanelByDirectory, openDirectory]);
|
}, [contextPanelByDirectory, openDirectory]);
|
||||||
|
|
||||||
|
const isContextBrowserActive = React.useMemo(() => {
|
||||||
|
const directory = normalize(openDirectory || '');
|
||||||
|
if (!directory) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const panelState = contextPanelByDirectory[directory];
|
||||||
|
return getActiveContextMode(panelState) === 'browser';
|
||||||
|
}, [contextPanelByDirectory, openDirectory]);
|
||||||
|
|
||||||
const desktopHeaderIconButtonClass = DESKTOP_HEADER_ICON_BUTTON_CLASS;
|
const desktopHeaderIconButtonClass = DESKTOP_HEADER_ICON_BUTTON_CLASS;
|
||||||
const mobileHeaderIconButtonClass = MOBILE_HEADER_ICON_BUTTON_CLASS;
|
const mobileHeaderIconButtonClass = MOBILE_HEADER_ICON_BUTTON_CLASS;
|
||||||
const mobileActiveHeaderItem = React.useMemo(() => {
|
const mobileActiveHeaderItem = React.useMemo(() => {
|
||||||
@@ -1943,6 +1965,7 @@ export const Header: React.FC<HeaderProps> = ({
|
|||||||
title={t('contextPanel.browser.open')}
|
title={t('contextPanel.browser.open')}
|
||||||
ariaLabel={t('contextPanel.browser.open')}
|
ariaLabel={t('contextPanel.browser.open')}
|
||||||
onClick={handleOpenContextBrowser}
|
onClick={handleOpenContextBrowser}
|
||||||
|
pressed={isContextBrowserActive}
|
||||||
Icon={'global'}
|
Icon={'global'}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ const upsertContextPanelTab = (
|
|||||||
? {
|
? {
|
||||||
...tab,
|
...tab,
|
||||||
mode: nextTab.mode,
|
mode: nextTab.mode,
|
||||||
targetPath: nextTab.targetPath,
|
targetPath: nextTab.targetPath || tab.targetPath,
|
||||||
dedupeKey: nextTab.dedupeKey,
|
dedupeKey: nextTab.dedupeKey,
|
||||||
label: nextTab.label,
|
label: nextTab.label,
|
||||||
stagedDiff: nextTab.stagedDiff,
|
stagedDiff: nextTab.stagedDiff,
|
||||||
|
|||||||
Reference in New Issue
Block a user