feat: add settings dialog and header button for mobile view
This commit is contained in:
@@ -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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user