mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat: Make input controls and settings responsive for mobile devices
This commit is contained in:
@@ -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,6 +125,8 @@ export function InputControls({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Controls row - responsive layout */}
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{/* Model Selector */}
|
{/* Model Selector */}
|
||||||
<AgentModelSelector
|
<AgentModelSelector
|
||||||
value={modelSelection}
|
value={modelSelection}
|
||||||
@@ -138,7 +141,7 @@ export function InputControls({
|
|||||||
onClick={onToggleImageDropZone}
|
onClick={onToggleImageDropZone}
|
||||||
disabled={!isConnected}
|
disabled={!isConnected}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-11 w-11 rounded-xl border-border',
|
'h-11 w-11 rounded-xl border-border shrink-0',
|
||||||
showImageDropZone && 'bg-primary/10 text-primary border-primary/30',
|
showImageDropZone && 'bg-primary/10 text-primary border-primary/30',
|
||||||
hasFiles && 'border-primary/30 text-primary'
|
hasFiles && 'border-primary/30 text-primary'
|
||||||
)}
|
)}
|
||||||
@@ -147,12 +150,15 @@ export function InputControls({
|
|||||||
<Paperclip className="w-4 h-4" />
|
<Paperclip className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Spacer to push action buttons to the right */}
|
||||||
|
<div className="flex-1" />
|
||||||
|
|
||||||
{/* Stop Button (only when processing) */}
|
{/* Stop Button (only when processing) */}
|
||||||
{isProcessing && (
|
{isProcessing && (
|
||||||
<Button
|
<Button
|
||||||
onClick={onStop}
|
onClick={onStop}
|
||||||
disabled={!isConnected}
|
disabled={!isConnected}
|
||||||
className="h-11 px-4 rounded-xl"
|
className="h-11 px-4 rounded-xl shrink-0"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
data-testid="stop-agent"
|
data-testid="stop-agent"
|
||||||
title="Stop generation"
|
title="Stop generation"
|
||||||
@@ -165,7 +171,7 @@ export function InputControls({
|
|||||||
<Button
|
<Button
|
||||||
onClick={onSend}
|
onClick={onSend}
|
||||||
disabled={!canSend}
|
disabled={!canSend}
|
||||||
className="h-11 px-4 rounded-xl"
|
className="h-11 px-4 rounded-xl shrink-0"
|
||||||
variant={isProcessing ? 'outline' : 'default'}
|
variant={isProcessing ? 'outline' : 'default'}
|
||||||
data-testid="send-message"
|
data-testid="send-message"
|
||||||
title={isProcessing ? 'Add to queue' : 'Send message'}
|
title={isProcessing ? 'Add to queue' : 'Send message'}
|
||||||
@@ -173,9 +179,10 @@ export function InputControls({
|
|||||||
{isProcessing ? <ListOrdered className="w-4 h-4" /> : <Send className="w-4 h-4" />}
|
{isProcessing ? <ListOrdered className="w-4 h-4" /> : <Send className="w-4 h-4" />}
|
||||||
</Button>
|
</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>{' '}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,15 +170,54 @@ 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 (
|
||||||
|
<>
|
||||||
|
{/* Mobile backdrop overlay */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 z-20 lg:hidden"
|
||||||
|
onClick={onClose}
|
||||||
|
data-testid="settings-nav-backdrop"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Navigation sidebar */}
|
||||||
<nav
|
<nav
|
||||||
className={cn(
|
className={cn(
|
||||||
'hidden lg:block w-64 shrink-0 overflow-y-auto',
|
// 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',
|
'border-r border-border/50',
|
||||||
'bg-gradient-to-b from-card/80 via-card/60 to-card/40 backdrop-blur-xl'
|
'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">
|
<div className="sticky top-0 p-4 space-y-1">
|
||||||
{/* Global Settings Groups */}
|
{/* Global Settings Groups */}
|
||||||
{GLOBAL_NAV_GROUPS.map((group, groupIndex) => (
|
{GLOBAL_NAV_GROUPS.map((group, groupIndex) => (
|
||||||
@@ -237,5 +279,6 @@ export function SettingsNavigation({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user