fix: redesign settings dialog layout to prevent content overflow and visual jitter

Fixed multiple layout issues in the settings dialog:

1. Dialog structure: Changed from grid to flexbox layout
   - Removed global padding from DialogContent
   - Added individual padding to DialogHeader (px-6 pt-6) and DialogFooter (px-6 pb-6 pt-4)
   - Added max-h-[90vh] constraint to prevent dialog from exceeding viewport

2. Content area improvements:
   - Replaced max-h-[70vh] with flex-1 for better space utilization
   - Set min-h-[480px] on content wrapper to maintain consistent dialog height
   - Applied min-h-[400px] to all TabsContent components to prevent height jumps

3. Scrollbar optimization:
   - Changed overflow-y-auto to overflow-y-scroll to force scrollbar gutter
   - Eliminates horizontal shift when switching between tabs with different content heights
   - Consistent with main app layout approach (App.tsx)

4. Footer enhancement:
   - Added border-t and bg-muted/20 for visual separation
   - Fixed footer overlapping content in advanced tab

Result: Settings dialog now displays all content properly without requiring
fullscreen, maintains consistent height across tabs, and eliminates layout shift
when switching tabs.
This commit is contained in:
Jason
2025-10-17 23:09:21 +08:00
parent f5c6363dee
commit c04f636bbe
2 changed files with 66 additions and 66 deletions

View File

@@ -171,21 +171,21 @@ export function SettingsDialog({
return ( return (
<Dialog open={open} onOpenChange={handleDialogChange}> <Dialog open={open} onOpenChange={handleDialogChange}>
<DialogContent className="max-w-3xl gap-6"> <DialogContent className="max-w-3xl">
<DialogHeader> <DialogHeader>
<DialogTitle>{t("settings.title")}</DialogTitle> <DialogTitle>{t("settings.title")}</DialogTitle>
</DialogHeader> </DialogHeader>
{isBusy ? ( {isBusy ? (
<div className="flex min-h-[320px] items-center justify-center"> <div className="flex min-h-[320px] items-center justify-center px-6">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
) : ( ) : (
<div className="flex max-h-[70vh] flex-col gap-6 overflow-hidden"> <div className="flex-1 overflow-y-scroll px-6 min-h-[480px]">
<Tabs <Tabs
value={activeTab} value={activeTab}
onValueChange={setActiveTab} onValueChange={setActiveTab}
className="flex flex-1 flex-col" className="flex flex-col h-full"
> >
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="general"> <TabsTrigger value="general">
@@ -197,65 +197,63 @@ export function SettingsDialog({
<TabsTrigger value="about">{t("common.about")}</TabsTrigger> <TabsTrigger value="about">{t("common.about")}</TabsTrigger>
</TabsList> </TabsList>
<div className="flex-1 overflow-y-auto pr-1"> <TabsContent value="general" className="space-y-6 mt-6 min-h-[400px]">
<TabsContent value="general" className="space-y-6 pt-4"> {settings ? (
{settings ? ( <>
<> <LanguageSettings
<LanguageSettings value={settings.language}
value={settings.language} onChange={(lang) => updateSettings({ language: lang })}
onChange={(lang) => updateSettings({ language: lang })} />
/> <WindowSettings
<WindowSettings settings={settings}
settings={settings} onChange={updateSettings}
onChange={updateSettings} />
/> <ConfigPathDisplay
<ConfigPathDisplay path={configPath}
path={configPath} onOpen={openConfigFolder}
onOpen={openConfigFolder} />
/> </>
</> ) : null}
) : null} </TabsContent>
</TabsContent>
<TabsContent value="advanced" className="space-y-6 pt-4"> <TabsContent value="advanced" className="space-y-6 mt-6 min-h-[400px]">
{settings ? ( {settings ? (
<> <>
<DirectorySettings <DirectorySettings
appConfigDir={appConfigDir} appConfigDir={appConfigDir}
resolvedDirs={resolvedDirs} resolvedDirs={resolvedDirs}
onAppConfigChange={updateAppConfigDir} onAppConfigChange={updateAppConfigDir}
onBrowseAppConfig={browseAppConfigDir} onBrowseAppConfig={browseAppConfigDir}
onResetAppConfig={resetAppConfigDir} onResetAppConfig={resetAppConfigDir}
claudeDir={settings.claudeConfigDir} claudeDir={settings.claudeConfigDir}
codexDir={settings.codexConfigDir} codexDir={settings.codexConfigDir}
onDirectoryChange={updateDirectory} onDirectoryChange={updateDirectory}
onBrowseDirectory={browseDirectory} onBrowseDirectory={browseDirectory}
onResetDirectory={resetDirectory} onResetDirectory={resetDirectory}
/> />
<ImportExportSection <ImportExportSection
status={importStatus} status={importStatus}
selectedFile={selectedFile} selectedFile={selectedFile}
errorMessage={errorMessage} errorMessage={errorMessage}
backupId={backupId} backupId={backupId}
isImporting={isImporting} isImporting={isImporting}
onSelectFile={selectImportFile} onSelectFile={selectImportFile}
onImport={importConfig} onImport={importConfig}
onExport={exportConfig} onExport={exportConfig}
onClear={clearSelection} onClear={clearSelection}
/> />
</> </>
) : null} ) : null}
</TabsContent> </TabsContent>
<TabsContent value="about" className="pt-4"> <TabsContent value="about" className="mt-6 min-h-[400px]">
<AboutSection isPortable={isPortable} /> <AboutSection isPortable={isPortable} />
</TabsContent> </TabsContent>
</div>
</Tabs> </Tabs>
</div> </div>
)} )}
<DialogFooter className="gap-2"> <DialogFooter>
<Button variant="outline" onClick={handleCancel}> <Button variant="outline" onClick={handleCancel}>
{t("common.cancel")} {t("common.cancel")}
</Button> </Button>
@@ -280,12 +278,14 @@ export function SettingsDialog({
<DialogHeader> <DialogHeader>
<DialogTitle>{t("settings.restartRequired")}</DialogTitle> <DialogTitle>{t("settings.restartRequired")}</DialogTitle>
</DialogHeader> </DialogHeader>
<p className="text-sm text-muted-foreground"> <div className="px-6">
{t("settings.restartRequiredMessage", { <p className="text-sm text-muted-foreground">
defaultValue: "配置目录已变更,需要重启应用生效。", {t("settings.restartRequiredMessage", {
})} defaultValue: "配置目录已变更,需要重启应用生效。",
</p> })}
<DialogFooter className="gap-2"> </p>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleRestartLater}> <Button variant="outline" onClick={handleRestartLater}>
{t("settings.restartLater")} {t("settings.restartLater")}
</Button> </Button>

View File

@@ -55,7 +55,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-1/2 top-1/2 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white dark:bg-gray-900 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-1/2 top-1/2 flex flex-col w-full max-w-lg max-h-[90vh] translate-x-[-50%] translate-y-[-50%] border bg-white dark:bg-gray-900 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
zIndexMap[zIndex], zIndexMap[zIndex],
className, className,
)} )}
@@ -78,7 +78,7 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left", "flex flex-col space-y-1.5 text-center sm:text-left px-6 pt-6",
className, className,
)} )}
{...props} {...props}
@@ -92,7 +92,7 @@ const DialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 px-6 pb-6 pt-4 border-t border-border bg-muted/20",
className, className,
)} )}
{...props} {...props}