feat: Make input controls and settings responsive for mobile devices

This commit is contained in:
anonymous
2026-01-11 22:48:39 -08:00
committed by Shirone
parent c7def000df
commit df7a0f8687
4 changed files with 230 additions and 122 deletions

View File

@@ -79,7 +79,7 @@ export function InputControls({
{/* Text Input and Controls */} {/* Text Input and Controls */}
<div <div
className={cn( className={cn(
'flex gap-2 transition-all duration-200 rounded-xl p-1', 'flex flex-col gap-2 transition-all duration-200 rounded-xl p-1',
isDragOver && 'bg-primary/5 ring-2 ring-primary/30' isDragOver && 'bg-primary/5 ring-2 ring-primary/30'
)} )}
onDragEnter={onDragEnter} onDragEnter={onDragEnter}
@@ -87,7 +87,8 @@ export function InputControls({
onDragOver={onDragOver} onDragOver={onDragOver}
onDrop={onDrop} onDrop={onDrop}
> >
<div className="flex-1 relative"> {/* Textarea - full width on mobile */}
<div className="relative w-full">
<Textarea <Textarea
ref={inputRef} ref={inputRef}
placeholder={ placeholder={
@@ -105,14 +106,14 @@ export function InputControls({
data-testid="agent-input" data-testid="agent-input"
rows={1} rows={1}
className={cn( className={cn(
'min-h-11 bg-background border-border rounded-xl pl-4 pr-20 text-sm transition-all resize-none max-h-36 overflow-y-auto py-2.5', 'min-h-11 w-full bg-background border-border rounded-xl pl-4 pr-4 sm:pr-20 text-sm transition-all resize-none max-h-36 overflow-y-auto py-2.5',
'focus:ring-2 focus:ring-primary/20 focus:border-primary/50', 'focus:ring-2 focus:ring-primary/20 focus:border-primary/50',
hasFiles && 'border-primary/30', hasFiles && 'border-primary/30',
isDragOver && 'border-primary bg-primary/5' isDragOver && 'border-primary bg-primary/5'
)} )}
/> />
{hasFiles && !isDragOver && ( {hasFiles && !isDragOver && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-xs bg-primary text-primary-foreground px-2 py-0.5 rounded-full font-medium"> <div className="hidden sm:block absolute right-3 top-1/2 -translate-y-1/2 text-xs bg-primary text-primary-foreground px-2 py-0.5 rounded-full font-medium">
files attached files attached
</div> </div>
)} )}
@@ -124,58 +125,64 @@ export function InputControls({
)} )}
</div> </div>
{/* Model Selector */} {/* Controls row - responsive layout */}
<AgentModelSelector <div className="flex items-center gap-2 flex-wrap">
value={modelSelection} {/* Model Selector */}
onChange={onModelSelect} <AgentModelSelector
disabled={!isConnected} value={modelSelection}
/> onChange={onModelSelect}
{/* File Attachment Button */}
<Button
variant="outline"
size="icon"
onClick={onToggleImageDropZone}
disabled={!isConnected}
className={cn(
'h-11 w-11 rounded-xl border-border',
showImageDropZone && 'bg-primary/10 text-primary border-primary/30',
hasFiles && 'border-primary/30 text-primary'
)}
title="Attach files (images, .txt, .md)"
>
<Paperclip className="w-4 h-4" />
</Button>
{/* Stop Button (only when processing) */}
{isProcessing && (
<Button
onClick={onStop}
disabled={!isConnected} disabled={!isConnected}
className="h-11 px-4 rounded-xl" />
variant="destructive"
data-testid="stop-agent"
title="Stop generation"
>
<Square className="w-4 h-4 fill-current" />
</Button>
)}
{/* Send / Queue Button */} {/* File Attachment Button */}
<Button <Button
onClick={onSend} variant="outline"
disabled={!canSend} size="icon"
className="h-11 px-4 rounded-xl" onClick={onToggleImageDropZone}
variant={isProcessing ? 'outline' : 'default'} disabled={!isConnected}
data-testid="send-message" className={cn(
title={isProcessing ? 'Add to queue' : 'Send message'} 'h-11 w-11 rounded-xl border-border shrink-0',
> showImageDropZone && 'bg-primary/10 text-primary border-primary/30',
{isProcessing ? <ListOrdered className="w-4 h-4" /> : <Send className="w-4 h-4" />} hasFiles && 'border-primary/30 text-primary'
</Button> )}
title="Attach files (images, .txt, .md)"
>
<Paperclip className="w-4 h-4" />
</Button>
{/* Spacer to push action buttons to the right */}
<div className="flex-1" />
{/* Stop Button (only when processing) */}
{isProcessing && (
<Button
onClick={onStop}
disabled={!isConnected}
className="h-11 px-4 rounded-xl shrink-0"
variant="destructive"
data-testid="stop-agent"
title="Stop generation"
>
<Square className="w-4 h-4 fill-current" />
</Button>
)}
{/* Send / Queue Button */}
<Button
onClick={onSend}
disabled={!canSend}
className="h-11 px-4 rounded-xl shrink-0"
variant={isProcessing ? 'outline' : 'default'}
data-testid="send-message"
title={isProcessing ? 'Add to queue' : 'Send message'}
>
{isProcessing ? <ListOrdered className="w-4 h-4" /> : <Send className="w-4 h-4" />}
</Button>
</div>
</div> </div>
{/* Keyboard hint */} {/* Keyboard hint */}
<p className="text-[11px] text-muted-foreground mt-2 text-center"> <p className="text-[11px] text-muted-foreground mt-2 text-center hidden sm:block">
Press <kbd className="px-1.5 py-0.5 bg-muted rounded text-[10px] font-medium">Enter</kbd> to Press <kbd className="px-1.5 py-0.5 bg-muted rounded text-[10px] font-medium">Enter</kbd> to
send,{' '} send,{' '}
<kbd className="px-1.5 py-0.5 bg-muted rounded text-[10px] font-medium">Shift+Enter</kbd>{' '} <kbd className="px-1.5 py-0.5 bg-muted rounded text-[10px] font-medium">Shift+Enter</kbd>{' '}

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store'; import { useSetupStore } from '@/store/setup-store';
@@ -30,6 +30,9 @@ import { PromptCustomizationSection } from './settings-view/prompts';
import type { Project as SettingsProject, Theme } from './settings-view/shared/types'; import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
import type { Project as ElectronProject } from '@/lib/electron'; import type { Project as ElectronProject } from '@/lib/electron';
// Breakpoint constant for mobile (matches Tailwind lg breakpoint)
const LG_BREAKPOINT = 1024;
export function SettingsView() { export function SettingsView() {
const { const {
theme, theme,
@@ -101,6 +104,33 @@ export function SettingsView() {
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false); const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);
// Mobile navigation state - default to showing on desktop, hidden on mobile
const [showNavigation, setShowNavigation] = useState(() => {
if (typeof window !== 'undefined') {
return window.innerWidth >= LG_BREAKPOINT;
}
return true; // Default to showing on SSR
});
// Auto-close navigation on mobile when a section is selected
useEffect(() => {
if (typeof window !== 'undefined' && window.innerWidth < LG_BREAKPOINT) {
setShowNavigation(false);
}
}, [activeView]);
// Handle window resize to show/hide navigation appropriately
useEffect(() => {
const handleResize = () => {
if (window.innerWidth >= LG_BREAKPOINT) {
setShowNavigation(true);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Render the active section based on current view // Render the active section based on current view
const renderActiveSection = () => { const renderActiveSection = () => {
switch (activeView) { switch (activeView) {
@@ -187,20 +217,25 @@ export function SettingsView() {
return ( return (
<div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="settings-view"> <div className="flex-1 flex flex-col overflow-hidden content-bg" data-testid="settings-view">
{/* Header Section */} {/* Header Section */}
<SettingsHeader /> <SettingsHeader
showNavigation={showNavigation}
onToggleNavigation={() => setShowNavigation(!showNavigation)}
/>
{/* Content Area with Sidebar */} {/* Content Area with Sidebar */}
<div className="flex-1 flex overflow-hidden"> <div className="flex-1 flex overflow-hidden">
{/* Side Navigation - No longer scrolls, just switches views */} {/* Side Navigation - Overlay on mobile, sidebar on desktop */}
<SettingsNavigation <SettingsNavigation
navItems={NAV_ITEMS} navItems={NAV_ITEMS}
activeSection={activeView} activeSection={activeView}
currentProject={currentProject} currentProject={currentProject}
onNavigate={handleNavigate} onNavigate={handleNavigate}
isOpen={showNavigation}
onClose={() => setShowNavigation(false)}
/> />
{/* Content Panel - Shows only the active section */} {/* Content Panel - Shows only the active section */}
<div className="flex-1 overflow-y-auto p-8"> <div className="flex-1 overflow-y-auto p-4 lg:p-8">
<div className="max-w-4xl mx-auto">{renderActiveSection()}</div> <div className="max-w-4xl mx-auto">{renderActiveSection()}</div>
</div> </div>
</div> </div>

View File

@@ -1,14 +1,19 @@
import { Settings } from 'lucide-react'; import { Settings, PanelLeft, PanelLeftClose } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
interface SettingsHeaderProps { interface SettingsHeaderProps {
title?: string; title?: string;
description?: string; description?: string;
showNavigation?: boolean;
onToggleNavigation?: () => void;
} }
export function SettingsHeader({ export function SettingsHeader({
title = 'Settings', title = 'Settings',
description = 'Configure your API keys and preferences', description = 'Configure your API keys and preferences',
showNavigation,
onToggleNavigation,
}: SettingsHeaderProps) { }: SettingsHeaderProps) {
return ( return (
<div <div
@@ -18,21 +23,39 @@ export function SettingsHeader({
'bg-gradient-to-r from-card/90 via-card/70 to-card/80 backdrop-blur-xl' 'bg-gradient-to-r from-card/90 via-card/70 to-card/80 backdrop-blur-xl'
)} )}
> >
<div className="px-8 py-6"> <div className="px-4 py-4 lg:px-8 lg:py-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-3 lg:gap-4">
{/* Mobile menu toggle button - only visible on mobile */}
{onToggleNavigation && (
<Button
variant="ghost"
size="sm"
onClick={onToggleNavigation}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground lg:hidden"
aria-label={showNavigation ? 'Close navigation menu' : 'Open navigation menu'}
>
{showNavigation ? (
<PanelLeftClose className="w-5 h-5" />
) : (
<PanelLeft className="w-5 h-5" />
)}
</Button>
)}
<div <div
className={cn( className={cn(
'w-12 h-12 rounded-2xl flex items-center justify-center', 'w-10 h-10 lg:w-12 lg:h-12 rounded-xl lg:rounded-2xl flex items-center justify-center',
'bg-gradient-to-br from-brand-500 to-brand-600', 'bg-gradient-to-br from-brand-500 to-brand-600',
'shadow-lg shadow-brand-500/25', 'shadow-lg shadow-brand-500/25',
'ring-1 ring-white/10' 'ring-1 ring-white/10'
)} )}
> >
<Settings className="w-6 h-6 text-white" /> <Settings className="w-5 h-5 lg:w-6 lg:h-6 text-white" />
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold text-foreground tracking-tight">{title}</h1> <h1 className="text-xl lg:text-2xl font-bold text-foreground tracking-tight">
<p className="text-sm text-muted-foreground/80 mt-0.5">{description}</p> {title}
</h1>
<p className="text-xs lg:text-sm text-muted-foreground/80 mt-0.5">{description}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react'; import { ChevronDown, ChevronRight, X } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import type { Project } from '@/lib/electron'; import type { Project } from '@/lib/electron';
import type { NavigationItem, NavigationGroup } from '../config/navigation'; import type { NavigationItem, NavigationGroup } from '../config/navigation';
import { GLOBAL_NAV_GROUPS, PROJECT_NAV_ITEMS } from '../config/navigation'; import { GLOBAL_NAV_GROUPS, PROJECT_NAV_ITEMS } from '../config/navigation';
@@ -13,6 +14,8 @@ interface SettingsNavigationProps {
activeSection: SettingsViewId; activeSection: SettingsViewId;
currentProject: Project | null; currentProject: Project | null;
onNavigate: (sectionId: SettingsViewId) => void; onNavigate: (sectionId: SettingsViewId) => void;
isOpen?: boolean;
onClose?: () => void;
} }
function NavButton({ function NavButton({
@@ -167,75 +170,115 @@ export function SettingsNavigation({
activeSection, activeSection,
currentProject, currentProject,
onNavigate, onNavigate,
isOpen = true,
onClose,
}: SettingsNavigationProps) { }: SettingsNavigationProps) {
// On mobile, only show when isOpen is true
// On desktop (lg+), always show regardless of isOpen
const shouldShow = isOpen;
if (!shouldShow) {
return null;
}
return ( return (
<nav <>
className={cn( {/* Mobile backdrop overlay */}
'hidden lg:block w-64 shrink-0 overflow-y-auto', <div
'border-r border-border/50', className="fixed inset-0 bg-black/50 z-20 lg:hidden"
'bg-gradient-to-b from-card/80 via-card/60 to-card/40 backdrop-blur-xl' onClick={onClose}
)} data-testid="settings-nav-backdrop"
> />
<div className="sticky top-0 p-4 space-y-1">
{/* Global Settings Groups */}
{GLOBAL_NAV_GROUPS.map((group, groupIndex) => (
<div key={group.label}>
{/* Group divider (except for first group) */}
{groupIndex > 0 && <div className="my-3 border-t border-border/50" />}
{/* Group Label */} {/* Navigation sidebar */}
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider"> <nav
{group.label} className={cn(
// Mobile: fixed position overlay
'fixed inset-y-0 left-0 w-72 z-30',
// Desktop: relative position in layout
'lg:relative lg:w-64 lg:z-auto',
'shrink-0 overflow-y-auto',
'border-r border-border/50',
'bg-gradient-to-b from-card/95 via-card/90 to-card/85 backdrop-blur-xl',
// Desktop background
'lg:from-card/80 lg:via-card/60 lg:to-card/40'
)}
>
{/* Mobile close button */}
<div className="lg:hidden flex items-center justify-between px-4 py-3 border-b border-border/50">
<span className="text-sm font-semibold text-foreground">Navigation</span>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
aria-label="Close navigation menu"
>
<X className="w-4 h-4" />
</Button>
</div>
<div className="sticky top-0 p-4 space-y-1">
{/* Global Settings Groups */}
{GLOBAL_NAV_GROUPS.map((group, groupIndex) => (
<div key={group.label}>
{/* Group divider (except for first group) */}
{groupIndex > 0 && <div className="my-3 border-t border-border/50" />}
{/* Group Label */}
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">
{group.label}
</div>
{/* Group Items */}
<div className="space-y-1">
{group.items.map((item) =>
item.subItems ? (
<NavItemWithSubItems
key={item.id}
item={item}
activeSection={activeSection}
onNavigate={onNavigate}
/>
) : (
<NavButton
key={item.id}
item={item}
isActive={activeSection === item.id}
onNavigate={onNavigate}
/>
)
)}
</div>
</div> </div>
))}
{/* Group Items */} {/* Project Settings - only show when a project is selected */}
<div className="space-y-1"> {currentProject && (
{group.items.map((item) => <>
item.subItems ? ( {/* Divider */}
<NavItemWithSubItems <div className="my-3 border-t border-border/50" />
key={item.id}
item={item} {/* Project Settings Label */}
activeSection={activeSection} <div className="px-3 py-2 text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">
onNavigate={onNavigate} Project Settings
/> </div>
) : (
{/* Project Settings Items */}
<div className="space-y-1">
{PROJECT_NAV_ITEMS.map((item) => (
<NavButton <NavButton
key={item.id} key={item.id}
item={item} item={item}
isActive={activeSection === item.id} isActive={activeSection === item.id}
onNavigate={onNavigate} onNavigate={onNavigate}
/> />
) ))}
)} </div>
</div> </>
</div> )}
))} </div>
</nav>
{/* Project Settings - only show when a project is selected */} </>
{currentProject && (
<>
{/* Divider */}
<div className="my-3 border-t border-border/50" />
{/* Project Settings Label */}
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">
Project Settings
</div>
{/* Project Settings Items */}
<div className="space-y-1">
{PROJECT_NAV_ITEMS.map((item) => (
<NavButton
key={item.id}
item={item}
isActive={activeSection === item.id}
onNavigate={onNavigate}
/>
))}
</div>
</>
)}
</div>
</nav>
); );
} }