feat: add settings dialog and header button for mobile view

This commit is contained in:
Bohdan Triapitsyn
2025-12-17 23:12:59 +02:00
parent 2c261308f2
commit 5186a4c9e2
4 changed files with 163 additions and 95 deletions
+23 -1
View File
@@ -5,7 +5,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { RiChat4Line, RiCodeLine, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react'; import { RiChat4Line, RiCodeLine, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
import { useUIStore, type MainTab } from '@/stores/useUIStore'; import { useUIStore, type MainTab } from '@/stores/useUIStore';
import { useConfigStore } from '@/stores/useConfigStore'; import { useConfigStore } from '@/stores/useConfigStore';
import { useSessionStore } from '@/stores/useSessionStore'; import { useSessionStore } from '@/stores/useSessionStore';
@@ -82,6 +82,7 @@ export const FixedSessionsButton: React.FC = () => {
export const Header: React.FC = () => { export const Header: React.FC = () => {
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
const toggleSidebar = useUIStore((state) => state.toggleSidebar); const toggleSidebar = useUIStore((state) => state.toggleSidebar);
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen); const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
const sidebarWidth = useUIStore((state) => state.sidebarWidth); const sidebarWidth = useUIStore((state) => state.sidebarWidth);
const activeMainTab = useUIStore((state) => state.activeMainTab); const activeMainTab = useUIStore((state) => state.activeMainTab);
@@ -134,6 +135,11 @@ export const Header: React.FC = () => {
toggleSidebar(); toggleSidebar();
}, [isMobile, isSessionSwitcherOpen, setSessionSwitcherOpen, toggleSidebar]); }, [isMobile, isSessionSwitcherOpen, setSessionSwitcherOpen, toggleSidebar]);
const handleOpenSettings = React.useCallback(() => {
setSessionSwitcherOpen(false);
setSettingsDialogOpen(true);
}, [setSessionSwitcherOpen, setSettingsDialogOpen]);
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground'; const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground';
const desktopPaddingClass = React.useMemo(() => { const desktopPaddingClass = React.useMemo(() => {
@@ -415,6 +421,22 @@ export const Header: React.FC = () => {
); );
})} })}
</div> </div>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleOpenSettings}
aria-label="Open settings"
className={headerIconButtonClass}
>
<RiSettings3Line className="h-5 w-5" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Settings</p>
</TooltipContent>
</Tooltip>
</div> </div>
</div> </div>
); );
@@ -137,7 +137,10 @@ export const SettingsDialog: React.FC<SettingsDialogProps> = ({ isOpen, onClose
); );
const mainContent = ( const mainContent = (
<div className="flex flex-1 flex-col overflow-hidden"> <div className={cn(
'flex flex-col min-h-0',
isMobile ? 'h-full overflow-hidden' : 'flex-1 overflow-hidden'
)}>
<div className="flex flex-wrap items-center gap-1 border-b border-border/40 bg-background/95 px-3 py-1.5"> <div className="flex flex-wrap items-center gap-1 border-b border-border/40 bg-background/95 px-3 py-1.5">
{SETTINGS_SECTIONS.map(({ id, label, icon: Icon }) => { {SETTINGS_SECTIONS.map(({ id, label, icon: Icon }) => {
const isActive = activeTab === id; const isActive = activeTab === id;
@@ -204,6 +207,7 @@ export const SettingsDialog: React.FC<SettingsDialogProps> = ({ isOpen, onClose
onClose={onClose} onClose={onClose}
title="Settings" title="Settings"
className="max-w-full" className="max-w-full"
contentMaxHeightClassName="h-[min(80dvh,720px)]"
renderHeader={(closeButton) => ( renderHeader={(closeButton) => (
<div className="flex items-center justify-between px-3 py-2 border-b border-border/40"> <div className="flex items-center justify-between px-3 py-2 border-b border-border/40">
<div className="relative flex flex-1 items-center justify-center"> <div className="relative flex flex-1 items-center justify-center">
@@ -8,6 +8,7 @@ import { cn } from '@/lib/utils';
import { ButtonSmall } from '@/components/ui/button-small'; import { ButtonSmall } from '@/components/ui/button-small';
import { NumberInput } from '@/components/ui/number-input'; import { NumberInput } from '@/components/ui/number-input';
import { isVSCodeRuntime } from '@/lib/desktop'; import { isVSCodeRuntime } from '@/lib/desktop';
import { useDeviceInfo } from '@/lib/device';
interface Option<T extends string> { interface Option<T extends string> {
id: T; id: T;
@@ -55,6 +56,7 @@ const DIFF_LAYOUT_OPTIONS: Option<'dynamic' | 'inline' | 'side-by-side'>[] = [
]; ];
export const AppearanceSettings: React.FC = () => { export const AppearanceSettings: React.FC = () => {
const { isMobile } = useDeviceInfo();
const showReasoningTraces = useUIStore(state => state.showReasoningTraces); const showReasoningTraces = useUIStore(state => state.showReasoningTraces);
const setShowReasoningTraces = useUIStore(state => state.setShowReasoningTraces); const setShowReasoningTraces = useUIStore(state => state.setShowReasoningTraces);
const toolCallExpansion = useUIStore(state => state.toolCallExpansion); const toolCallExpansion = useUIStore(state => state.toolCallExpansion);
@@ -95,44 +97,46 @@ export const AppearanceSettings: React.FC = () => {
</div> </div>
)} )}
<div className="space-y-4"> {!isMobile && (
<div className="space-y-1"> <div className="space-y-4">
<h3 className="typography-ui-header font-semibold text-foreground"> <div className="space-y-1">
Font Size <h3 className="typography-ui-header font-semibold text-foreground">
</h3> Font Size
</h3>
</div>
<div className="flex items-center gap-3 w-full max-w-md">
<input
type="range"
min="50"
max="200"
step="5"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
className="flex-1 min-w-0 h-2 bg-muted rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:border-0"
/>
<NumberInput
value={fontSize}
onValueChange={setFontSize}
min={50}
max={200}
step={5}
aria-label="Font size percentage"
/>
<ButtonSmall
type="button"
variant="ghost"
onClick={() => setFontSize(100)}
disabled={fontSize === 100}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
aria-label="Reset font size"
title="Reset"
>
<RiRestartLine className="h-3.5 w-3.5" />
</ButtonSmall>
</div>
</div> </div>
<div className="flex items-center gap-3 w-full max-w-md"> )}
<input
type="range"
min="50"
max="200"
step="5"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
className="flex-1 min-w-0 h-2 bg-muted rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:border-0"
/>
<NumberInput
value={fontSize}
onValueChange={setFontSize}
min={50}
max={200}
step={5}
aria-label="Font size percentage"
/>
<ButtonSmall
type="button"
variant="ghost"
onClick={() => setFontSize(100)}
disabled={fontSize === 100}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
aria-label="Reset font size"
title="Reset"
>
<RiRestartLine className="h-3.5 w-3.5" />
</ButtonSmall>
</div>
</div>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
@@ -141,36 +145,68 @@ export const AppearanceSettings: React.FC = () => {
</h3> </h3>
</div> </div>
<div className="flex items-center gap-3 w-full max-w-md">
<input {isMobile ? (
type="range" <div className="flex items-center gap-2 w-full">
min="50" <input
max="200" type="range"
step="5" min="50"
value={padding} max="200"
onChange={(e) => setPadding(Number(e.target.value))} step="5"
className="flex-1 min-w-0 h-2 bg-muted rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:border-0" value={padding}
/> onChange={(e) => setPadding(Number(e.target.value))}
<NumberInput className="flex-1 min-w-0 h-3 bg-muted rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:border-0"
value={padding} aria-label="Spacing percentage"
onValueChange={setPadding} />
min={50}
max={200} <span className="typography-ui-label font-medium text-foreground tabular-nums rounded-md border border-border bg-background px-2 py-1.5 min-w-[3.75rem] text-center">
step={5} {padding}
aria-label="Spacing percentage" </span>
/>
<ButtonSmall <ButtonSmall
type="button" type="button"
variant="ghost" variant="ghost"
onClick={() => setPadding(100)} onClick={() => setPadding(100)}
disabled={padding === 100} disabled={padding === 100}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background" className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
aria-label="Reset spacing" aria-label="Reset spacing"
title="Reset" title="Reset"
> >
<RiRestartLine className="h-3.5 w-3.5" /> <RiRestartLine className="h-3.5 w-3.5" />
</ButtonSmall> </ButtonSmall>
</div> </div>
) : (
<div className="flex items-center gap-3 w-full max-w-md">
<input
type="range"
min="50"
max="200"
step="5"
value={padding}
onChange={(e) => setPadding(Number(e.target.value))}
className="flex-1 min-w-0 h-2 bg-muted rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-primary [&::-moz-range-thumb]:border-0"
/>
<NumberInput
value={padding}
onValueChange={setPadding}
min={50}
max={200}
step={5}
aria-label="Spacing percentage"
/>
<ButtonSmall
type="button"
variant="ghost"
onClick={() => setPadding(100)}
disabled={padding === 100}
className="h-8 w-8 px-0 border border-border bg-background hover:bg-accent disabled:opacity-100 disabled:bg-background"
aria-label="Reset spacing"
title="Reset"
>
<RiRestartLine className="h-3.5 w-3.5" />
</ButtonSmall>
</div>
)}
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
@@ -197,34 +233,36 @@ export const AppearanceSettings: React.FC = () => {
</div> </div>
{} {}
<div className="space-y-4"> {!isMobile && (
<div className="space-y-1"> <div className="space-y-4">
<h3 className="typography-ui-header font-semibold text-foreground"> <div className="space-y-1">
Diff layout (Diff tab) <h3 className="typography-ui-header font-semibold text-foreground">
</h3> Diff layout (Diff tab)
<p className="typography-meta text-muted-foreground/80"> </h3>
Choose the default layout for file diffs. You can still override layout per file from the Diff tab. <p className="typography-meta text-muted-foreground/80">
</p> Choose the default layout for file diffs. You can still override layout per file from the Diff tab.
</div> </p>
</div>
<div className="flex flex-col gap-2">
<div className="flex gap-1 w-fit"> <div className="flex flex-col gap-2">
{DIFF_LAYOUT_OPTIONS.map((option) => ( <div className="flex gap-1 w-fit">
<ButtonSmall {DIFF_LAYOUT_OPTIONS.map((option) => (
key={option.id} <ButtonSmall
variant={diffLayoutPreference === option.id ? 'default' : 'outline'} key={option.id}
className={cn(diffLayoutPreference === option.id ? undefined : 'text-foreground')} variant={diffLayoutPreference === option.id ? 'default' : 'outline'}
onClick={() => setDiffLayoutPreference(option.id)} className={cn(diffLayoutPreference === option.id ? undefined : 'text-foreground')}
> onClick={() => setDiffLayoutPreference(option.id)}
{option.label} >
</ButtonSmall> {option.label}
))} </ButtonSmall>
))}
</div>
<p className="typography-meta text-muted-foreground/80 max-w-xl">
{DIFF_LAYOUT_OPTIONS.find((option) => option.id === diffLayoutPreference)?.description}
</p>
</div> </div>
<p className="typography-meta text-muted-foreground/80 max-w-xl">
{DIFF_LAYOUT_OPTIONS.find((option) => option.id === diffLayoutPreference)?.description}
</p>
</div> </div>
</div> )}
{} {}
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
@@ -11,6 +11,7 @@ interface MobileOverlayPanelProps {
children: React.ReactNode; children: React.ReactNode;
footer?: React.ReactNode; footer?: React.ReactNode;
className?: string; className?: string;
contentMaxHeightClassName?: string;
renderHeader?: (closeButton: React.ReactNode) => React.ReactNode; renderHeader?: (closeButton: React.ReactNode) => React.ReactNode;
} }
@@ -34,6 +35,7 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
children, children,
footer, footer,
className, className,
contentMaxHeightClassName,
renderHeader, renderHeader,
}) => { }) => {
const overlayRootRef = React.useRef<HTMLElement | null>(null); const overlayRootRef = React.useRef<HTMLElement | null>(null);
@@ -64,6 +66,8 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
return null; return null;
} }
const contentMaxHeight = contentMaxHeightClassName ?? 'max-h-[min(70vh,520px)]';
const content = ( const content = (
<div <div
className="fixed inset-0 z-50 flex flex-col bg-background/40 backdrop-blur-sm" className="fixed inset-0 z-50 flex flex-col bg-background/40 backdrop-blur-sm"
@@ -101,7 +105,7 @@ export const MobileOverlayPanel: React.FC<MobileOverlayPanelProps> = ({
</div> </div>
); );
})()} })()}
<ScrollableOverlay outerClassName="max-h-[min(70vh,520px)]" className="px-2 py-2 pwa-overlay-scroll"> <ScrollableOverlay outerClassName={contentMaxHeight} className="px-2 py-2 pwa-overlay-scroll">
{children} {children}
</ScrollableOverlay> </ScrollableOverlay>
{footer ? ( {footer ? (