fix(ui): keep settings sidebar open, center content, and fix worktree refresh (#1058)
* fix(ui): keep settings sidebar open, center content, and fix worktree refresh - Prevent settings nav from collapsing on split pages (Magic Prompts, Agents, etc.) - Center settings content with max-w-3xl for better layout on wide screens - Widen settings window from 960px to 1200px max-width - Fix source branch dropdown overflow with max-height and scrolling - Add worktreeRefreshNonce to trigger immediate sidebar refresh after worktree creation * ref(ui): address Greptile review — remove dead isNavCollapsed code and split worktree effect - Remove isNavCollapsed variable and all dead branches (resize handle, renderSettingsNav param, SETTINGS_NAV_RAIL_WIDTH) - Split SessionSidebar effect so worktreeRefreshNonce only triggers discoverWorktrees, not refreshGlobalSessions - Both type-check and lint pass * fix(ui): refine settings and worktree dropdown behavior --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
4f51abddf4
commit
6ab363f8df
@@ -1593,7 +1593,7 @@ export function NewWorktreeDialog({
|
|||||||
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" sideOffset={6} className="w-[320px] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden" ref={existingBranchDropdownContentRef}>
|
<DropdownMenuContent align="start" sideOffset={6} portalToBody className="w-[min(42rem,calc(100vw-2rem))] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden" ref={existingBranchDropdownContentRef}>
|
||||||
<Command shouldFilter={false}>
|
<Command shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={t('session.newWorktree.searchBranches')}
|
placeholder={t('session.newWorktree.searchBranches')}
|
||||||
@@ -1833,7 +1833,7 @@ export function NewWorktreeDialog({
|
|||||||
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
|
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="w-[320px] p-0" ref={sourceBranchDropdownContentRef}>
|
<DropdownMenuContent align="start" portalToBody className="w-[min(42rem,calc(100vw-2rem))] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden" ref={sourceBranchDropdownContentRef}>
|
||||||
<Command shouldFilter={false}>
|
<Command shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={t('session.newWorktree.searchBranches')}
|
placeholder={t('session.newWorktree.searchBranches')}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ type ContentProps = {
|
|||||||
align?: "start" | "center" | "end";
|
align?: "start" | "center" | "end";
|
||||||
side?: "top" | "right" | "bottom" | "left";
|
side?: "top" | "right" | "bottom" | "left";
|
||||||
alignOffset?: number;
|
alignOffset?: number;
|
||||||
|
portalToBody?: boolean;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -105,6 +106,7 @@ function DropdownMenuContent({
|
|||||||
align,
|
align,
|
||||||
side,
|
side,
|
||||||
alignOffset,
|
alignOffset,
|
||||||
|
portalToBody = false,
|
||||||
style,
|
style,
|
||||||
children,
|
children,
|
||||||
onCloseAutoFocus,
|
onCloseAutoFocus,
|
||||||
@@ -114,7 +116,7 @@ function DropdownMenuContent({
|
|||||||
void onCloseAutoFocus
|
void onCloseAutoFocus
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseMenu.Portal container={portalContext?.portalContainer || undefined}>
|
<BaseMenu.Portal container={portalToBody ? undefined : portalContext?.portalContainer || undefined}>
|
||||||
<BaseMenu.Positioner
|
<BaseMenu.Positioner
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
align={align}
|
align={align}
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ import {
|
|||||||
// Same constraints as main sidebar
|
// Same constraints as main sidebar
|
||||||
const SETTINGS_NAV_MIN_WIDTH = 176;
|
const SETTINGS_NAV_MIN_WIDTH = 176;
|
||||||
const SETTINGS_NAV_MAX_WIDTH = 280;
|
const SETTINGS_NAV_MAX_WIDTH = 280;
|
||||||
const SETTINGS_NAV_RAIL_WIDTH = 48;
|
|
||||||
|
|
||||||
type MobileStage = 'nav' | 'page-sidebar' | 'page-content';
|
type MobileStage = 'nav' | 'page-sidebar' | 'page-content';
|
||||||
|
|
||||||
@@ -372,8 +371,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
return getSettingsPageMeta(settingsSlug);
|
return getSettingsPageMeta(settingsSlug);
|
||||||
}, [settingsSlug]);
|
}, [settingsSlug]);
|
||||||
|
|
||||||
// Collapse main nav to icon rail when active page has its own sidebar
|
// Nav is always open (collapsed state removed)
|
||||||
const isNavCollapsed = !isMobile && activePageMeta?.kind === 'split';
|
|
||||||
|
|
||||||
const openChamberSectionBySlug: Partial<Record<SettingsPageSlug, OpenChamberSection>> = React.useMemo(() => ({
|
const openChamberSectionBySlug: Partial<Record<SettingsPageSlug, OpenChamberSection>> = React.useMemo(() => ({
|
||||||
appearance: 'visual',
|
appearance: 'visual',
|
||||||
@@ -544,7 +542,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
setMobileStage('page-sidebar');
|
setMobileStage('page-sidebar');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderSettingsNav = (collapsed: boolean) => {
|
const renderSettingsNav = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col overflow-hidden">
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
{/* Scrollable nav items */}
|
{/* Scrollable nav items */}
|
||||||
@@ -556,7 +554,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
if (!Icon) return null;
|
if (!Icon) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip key={page.slug} delayDuration={collapsed ? 100 : 600}>
|
<Tooltip key={page.slug} delayDuration={600}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -569,12 +567,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4 shrink-0" />
|
<Icon className="h-4 w-4 shrink-0" />
|
||||||
<span
|
<span className="flex items-center gap-1.5 whitespace-nowrap overflow-hidden transition-opacity duration-150 opacity-100">
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-1.5 whitespace-nowrap overflow-hidden transition-opacity duration-150',
|
|
||||||
collapsed ? 'opacity-0' : 'opacity-100'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="typography-ui-label font-normal truncate">{getPageTitle(page.slug)}</span>
|
<span className="typography-ui-label font-normal truncate">{getPageTitle(page.slug)}</span>
|
||||||
{(page.slug === 'voice' || page.slug === 'tunnel') && (
|
{(page.slug === 'voice' || page.slug === 'tunnel') && (
|
||||||
<span className="shrink-0 typography-micro px-1 rounded leading-none pb-px text-[var(--status-warning)] bg-[var(--status-warning)]/10">
|
<span className="shrink-0 typography-micro px-1 rounded leading-none pb-px text-[var(--status-warning)] bg-[var(--status-warning)]/10">
|
||||||
@@ -584,24 +577,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{collapsed && (
|
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
|
||||||
{getPageTitle(page.slug)}
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer — hidden when collapsed via overflow on parent */}
|
{/* Footer */}
|
||||||
<div
|
<div className="overflow-hidden transition-opacity duration-150 opacity-100">
|
||||||
className={cn(
|
|
||||||
'overflow-hidden transition-opacity duration-150',
|
|
||||||
collapsed ? 'opacity-0 pointer-events-none' : 'opacity-100'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="border-t border-border bg-sidebar px-2 py-1 space-y-0.5">
|
<div className="border-t border-border bg-sidebar px-2 py-1 space-y-0.5">
|
||||||
{!runtimeCtx.isVSCode && (
|
{!runtimeCtx.isVSCode && (
|
||||||
<Tooltip delayDuration={300}>
|
<Tooltip delayDuration={300}>
|
||||||
@@ -636,7 +619,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
return (
|
return (
|
||||||
<div className={cn('flex-1 min-h-0 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
|
<div className={cn('flex-1 min-h-0 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<div className="flex h-full min-h-0 flex-col">
|
||||||
<ErrorBoundary>{renderSettingsNav(false)}</ErrorBoundary>
|
<ErrorBoundary>{renderSettingsNav()}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -686,16 +669,20 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
<div className={cn('w-[264px] min-w-[264px] border-r', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')} style={{ borderColor: 'var(--interactive-border)' }}>
|
<div className={cn('w-[264px] min-w-[264px] border-r', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')} style={{ borderColor: 'var(--interactive-border)' }}>
|
||||||
<ErrorBoundary>{renderPageSidebar(settingsSlug, {})}</ErrorBoundary>
|
<ErrorBoundary>{renderPageSidebar(settingsSlug, {})}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-hidden bg-background">
|
<div className="flex-1 overflow-auto scrollbar-none bg-background">
|
||||||
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary>
|
<div className="mx-auto w-full max-w-3xl">
|
||||||
|
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-hidden bg-background">
|
<div className="h-full overflow-auto scrollbar-none bg-background">
|
||||||
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary>
|
<div className="mx-auto w-full max-w-3xl">
|
||||||
|
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -792,28 +779,26 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
|||||||
: runtimeCtx.isVSCode
|
: runtimeCtx.isVSCode
|
||||||
? 'bg-background'
|
? 'bg-background'
|
||||||
: 'bg-sidebar',
|
: 'bg-sidebar',
|
||||||
isResizing && !isNavCollapsed ? '' : 'transition-[width,min-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]'
|
isResizing ? '' : 'transition-[width,min-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]'
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
width: isNavCollapsed ? `${SETTINGS_NAV_RAIL_WIDTH}px` : `${navWidth}px`,
|
width: `${navWidth}px`,
|
||||||
minWidth: isNavCollapsed ? `${SETTINGS_NAV_RAIL_WIDTH}px` : `${navWidth}px`,
|
minWidth: `${navWidth}px`,
|
||||||
borderColor: 'var(--interactive-border)',
|
borderColor: 'var(--interactive-border)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isNavCollapsed && (
|
<div
|
||||||
<div
|
className={cn(
|
||||||
className={cn(
|
'absolute right-0 top-0 z-20 h-full w-[6px] -mr-[3px] cursor-col-resize',
|
||||||
'absolute right-0 top-0 z-20 h-full w-[6px] -mr-[3px] cursor-col-resize',
|
isResizing ? 'bg-primary/30' : 'bg-transparent hover:bg-primary/20'
|
||||||
isResizing ? 'bg-primary/30' : 'bg-transparent hover:bg-primary/20'
|
)}
|
||||||
)}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerDown={handlePointerDown}
|
role="separator"
|
||||||
role="separator"
|
aria-orientation="vertical"
|
||||||
aria-orientation="vertical"
|
aria-label={t('settings.view.actions.resizeNavigation')}
|
||||||
aria-label={t('settings.view.actions.resizeNavigation')}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{renderSettingsNav(isNavCollapsed)}
|
{renderSettingsNav()}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChan
|
|||||||
aria-describedby={descriptionId}
|
aria-describedby={descriptionId}
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||||
'w-[90vw] max-w-[960px] h-[85vh] max-h-[900px]',
|
'w-[90vw] max-w-[1200px] h-[85vh] max-h-[900px]',
|
||||||
'rounded-xl border shadow-none overflow-hidden',
|
'rounded-xl border shadow-none overflow-hidden',
|
||||||
'bg-background'
|
'bg-background'
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user