Skip to content

Commit 6374608

Browse files
authored
Merge pull request #71 from Multiplier-Labs/fix/mobile-ui-cleanup
Clean up mobile UI: 34px buttons, context menu, larger font
2 parents 38d91f1 + 06c8acb commit 6374608

4 files changed

Lines changed: 169 additions & 71 deletions

File tree

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ export default function App() {
546546
<div className="relative flex-1 min-h-0 flex flex-col">
547547
<ChatView
548548
messages={[...messages, ...tentativeMessages]}
549-
fontSize={settings.fontSize}
549+
fontSize={settings.fontSize + (isMobile ? 1 : 0)}
550550
theme={settings.theme}
551551
disabled={!settings.token}
552552
planningMode={planningMode}

src/components/InputBar.tsx

Lines changed: 159 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { useState, useRef, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react'
10-
import { IconSend, IconPaperclip, IconX, IconTerminal2, IconChevronDown } from '@tabler/icons-react'
10+
import { IconSend, IconPaperclip, IconX, IconTerminal2, IconChevronDown, IconDots } from '@tabler/icons-react'
1111
import { SkillMenu, type SkillGroup } from './SkillMenu'
1212
import { DropZone } from './DropZone'
1313

@@ -54,6 +54,8 @@ export const InputBar = forwardRef<InputBarHandle, InputBarProps>(function Input
5454
const [value, setValue] = useState(initialValue)
5555
const [skillMenuOpen, setSkillMenuOpen] = useState(false)
5656
const [modelMenuOpen, setModelMenuOpen] = useState(false)
57+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
58+
const mobileMenuRef = useRef<HTMLDivElement>(null)
5759
const MOBILE_HEIGHT = 100
5860
const [height, setHeight] = useState(() => {
5961
if (isMobile) return MOBILE_HEIGHT
@@ -80,6 +82,18 @@ export const InputBar = forwardRef<InputBarHandle, InputBarProps>(function Input
8082
prevWaiting.current = isWaiting
8183
}, [isWaiting, isMobile])
8284

85+
// Close mobile context menu on outside click
86+
useEffect(() => {
87+
if (!mobileMenuOpen) return
88+
const handler = (e: MouseEvent) => {
89+
if (mobileMenuRef.current && !mobileMenuRef.current.contains(e.target as Node)) {
90+
setMobileMenuOpen(false)
91+
}
92+
}
93+
document.addEventListener('mousedown', handler)
94+
return () => document.removeEventListener('mousedown', handler)
95+
}, [mobileMenuOpen])
96+
8397
const onDragStart = useCallback((e: React.MouseEvent) => {
8498
e.preventDefault()
8599
const startY = e.clientY
@@ -133,6 +147,8 @@ export const InputBar = forwardRef<InputBarHandle, InputBarProps>(function Input
133147
e.target.value = ''
134148
}, [onAddFiles])
135149

150+
const hasSkills = skillGroups && skillGroups.some(g => g.skills.length > 0)
151+
136152
return (
137153
<div className="app-input-bar relative flex flex-col border-t border-l border-neutral-9 bg-neutral-10" style={isMobile ? { minHeight: MOBILE_HEIGHT } : { height }}>
138154
<DropZone onUpload={onAddFiles} disabled={disabled} />
@@ -176,7 +192,7 @@ export const InputBar = forwardRef<InputBarHandle, InputBarProps>(function Input
176192
disabled={disabled}
177193
autoFocus
178194
placeholder={placeholder ?? (isWaiting ? 'Type response...' : 'What do you want to build?')}
179-
className="flex-1 min-h-0 resize-none bg-transparent text-[15px] leading-snug text-neutral-1 placeholder:text-neutral-5 outline-none disabled:opacity-50 overflow-y-auto"
195+
className={`flex-1 min-h-0 resize-none bg-transparent ${isMobile ? 'text-[16px]' : 'text-[15px]'} leading-snug text-neutral-1 placeholder:text-neutral-5 outline-none disabled:opacity-50 overflow-y-auto`}
180196
/>
181197
<input
182198
ref={fileInputRef}
@@ -185,75 +201,157 @@ export const InputBar = forwardRef<InputBarHandle, InputBarProps>(function Input
185201
onChange={handleFileChange}
186202
className="hidden"
187203
/>
188-
<div className={`flex flex-shrink-0 flex-row items-end ${isMobile ? 'gap-2' : 'gap-1'} pb-0.5`}>
189-
{currentModel && onModelChange && (
190-
<div className="relative">
204+
<div className={`flex flex-shrink-0 flex-row items-end ${isMobile ? 'gap-1.5' : 'gap-1'} pb-0.5`}>
205+
{/* Desktop: show all buttons inline */}
206+
{!isMobile && (
207+
<>
208+
{currentModel && onModelChange && (
209+
<div className="relative">
210+
<button
211+
onClick={() => setModelMenuOpen(!modelMenuOpen)}
212+
className="flex items-center gap-0.5 rounded px-1.5 pb-1 pt-0.5 text-[13px] font-medium text-neutral-4 hover:text-neutral-2 hover:bg-neutral-7 transition-colors"
213+
title="Change model"
214+
>
215+
{shortModelLabel(currentModel)}
216+
<IconChevronDown size={12} stroke={2} />
217+
</button>
218+
{modelMenuOpen && (
219+
<div className="absolute bottom-full mb-1 right-0 z-50 min-w-[160px] rounded border border-neutral-6 bg-neutral-8 shadow-lg py-1">
220+
{MODELS.map(m => (
221+
<button
222+
key={m.id}
223+
onClick={() => { onModelChange(m.id); setModelMenuOpen(false) }}
224+
className={`w-full text-left px-3 py-1.5 text-[13px] hover:bg-neutral-7 transition-colors ${m.id === currentModel ? 'text-primary-4' : 'text-neutral-2'}`}
225+
>
226+
{m.label}
227+
</button>
228+
))}
229+
</div>
230+
)}
231+
</div>
232+
)}
233+
{hasSkills && (
234+
<div className="relative">
235+
<button
236+
onClick={() => setSkillMenuOpen(!skillMenuOpen)}
237+
disabled={disabled}
238+
className="flex items-center justify-center rounded p-1 text-neutral-3 hover:text-neutral-1 hover:bg-neutral-7 transition-colors disabled:opacity-30"
239+
title="Claude Skills"
240+
>
241+
<IconTerminal2 size={20} stroke={2} />
242+
</button>
243+
{skillMenuOpen && (
244+
<SkillMenu
245+
groups={skillGroups!}
246+
onSelectSkill={(command) => {
247+
setValue(command + ' ')
248+
setSkillMenuOpen(false)
249+
setTimeout(() => textareaRef.current?.focus(), 0)
250+
}}
251+
onClose={() => setSkillMenuOpen(false)}
252+
/>
253+
)}
254+
</div>
255+
)}
191256
<button
192-
onClick={() => setModelMenuOpen(!modelMenuOpen)}
193-
className="flex items-center gap-0.5 rounded px-1.5 pb-1 pt-0.5 text-[13px] font-medium text-neutral-4 hover:text-neutral-2 hover:bg-neutral-7 transition-colors"
194-
title="Change model"
257+
onClick={handleFileSelect}
258+
disabled={disabled}
259+
className="flex items-center justify-center rounded p-1 text-neutral-3 hover:text-neutral-1 hover:bg-neutral-7 transition-colors disabled:opacity-30"
260+
title="Attach files"
195261
>
196-
{shortModelLabel(currentModel)}
197-
<IconChevronDown size={12} stroke={2} />
262+
<IconPaperclip size={20} stroke={2} />
198263
</button>
199-
{modelMenuOpen && (
200-
<div className="absolute bottom-full mb-1 right-0 z-50 min-w-[160px] rounded border border-neutral-6 bg-neutral-8 shadow-lg py-1">
201-
{MODELS.map(m => (
264+
<button
265+
onClick={handleSend}
266+
disabled={disabled || (!value.trim() && pendingFiles.length === 0)}
267+
className={`flex items-center justify-center rounded p-1 transition-colors disabled:opacity-30 ${
268+
value.trim() || pendingFiles.length > 0
269+
? 'bg-primary-8 text-neutral-1 hover:bg-primary-7'
270+
: 'text-neutral-5'
271+
}`}
272+
title="Send (Enter)"
273+
>
274+
<IconSend size={20} stroke={2} />
275+
</button>
276+
</>
277+
)}
278+
279+
{/* Mobile: context menu (...) + send button only */}
280+
{isMobile && (
281+
<>
282+
<div className="relative" ref={mobileMenuRef}>
283+
<button
284+
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
285+
disabled={disabled}
286+
className="flex items-center justify-center rounded min-w-[34px] min-h-[34px] p-1.5 text-neutral-3 hover:text-neutral-1 hover:bg-neutral-7 transition-colors disabled:opacity-30"
287+
title="More options"
288+
>
289+
<IconDots size={20} stroke={2} />
290+
</button>
291+
{mobileMenuOpen && (
292+
<div className="absolute bottom-full mb-1 right-0 z-50 min-w-[180px] rounded-lg border border-neutral-6 bg-neutral-8 shadow-lg py-1">
293+
{/* Model selector */}
294+
{currentModel && onModelChange && (
295+
<>
296+
<div className="px-3 py-1.5 text-[12px] text-neutral-5 uppercase tracking-wider">Model</div>
297+
{MODELS.map(m => (
298+
<button
299+
key={m.id}
300+
onClick={() => { onModelChange(m.id); setMobileMenuOpen(false) }}
301+
className={`w-full text-left px-3 py-2 text-[14px] hover:bg-neutral-7 transition-colors ${m.id === currentModel ? 'text-primary-4' : 'text-neutral-2'}`}
302+
>
303+
{m.label}
304+
</button>
305+
))}
306+
<div className="my-1 border-t border-neutral-7" />
307+
</>
308+
)}
309+
{/* Skills */}
310+
{hasSkills && (
311+
<button
312+
onClick={() => { setMobileMenuOpen(false); setSkillMenuOpen(!skillMenuOpen) }}
313+
className="flex items-center gap-2 w-full text-left px-3 py-2 text-[14px] text-neutral-2 hover:bg-neutral-7 transition-colors"
314+
>
315+
<IconTerminal2 size={18} stroke={2} className="text-neutral-4" />
316+
Skills
317+
</button>
318+
)}
319+
{/* Attach files */}
202320
<button
203-
key={m.id}
204-
onClick={() => { onModelChange(m.id); setModelMenuOpen(false) }}
205-
className={`w-full text-left px-3 py-1.5 text-[13px] hover:bg-neutral-7 transition-colors ${m.id === currentModel ? 'text-primary-4' : 'text-neutral-2'}`}
321+
onClick={() => { setMobileMenuOpen(false); handleFileSelect() }}
322+
className="flex items-center gap-2 w-full text-left px-3 py-2 text-[14px] text-neutral-2 hover:bg-neutral-7 transition-colors"
206323
>
207-
{m.label}
324+
<IconPaperclip size={18} stroke={2} className="text-neutral-4" />
325+
Attach files
208326
</button>
209-
))}
210-
</div>
211-
)}
212-
</div>
213-
)}
214-
{skillGroups && skillGroups.some(g => g.skills.length > 0) && (
215-
<div className="relative">
327+
</div>
328+
)}
329+
{skillMenuOpen && (
330+
<SkillMenu
331+
groups={skillGroups!}
332+
onSelectSkill={(command) => {
333+
setValue(command + ' ')
334+
setSkillMenuOpen(false)
335+
setTimeout(() => textareaRef.current?.focus(), 0)
336+
}}
337+
onClose={() => setSkillMenuOpen(false)}
338+
/>
339+
)}
340+
</div>
216341
<button
217-
onClick={() => setSkillMenuOpen(!skillMenuOpen)}
218-
disabled={disabled}
219-
className={`flex items-center justify-center rounded ${isMobile ? 'p-2.5 min-w-[44px] min-h-[44px]' : 'p-1'} text-neutral-3 hover:text-neutral-1 hover:bg-neutral-7 transition-colors disabled:opacity-30`}
220-
title="Claude Skills"
342+
onClick={handleSend}
343+
disabled={disabled || (!value.trim() && pendingFiles.length === 0)}
344+
className={`flex items-center justify-center rounded min-w-[34px] min-h-[34px] p-1.5 transition-colors disabled:opacity-30 ${
345+
value.trim() || pendingFiles.length > 0
346+
? 'bg-primary-8 text-neutral-1 hover:bg-primary-7'
347+
: 'text-neutral-5'
348+
}`}
349+
title="Send (Enter)"
221350
>
222-
<IconTerminal2 size={isMobile ? 24 : 20} stroke={2} />
351+
<IconSend size={20} stroke={2} />
223352
</button>
224-
{skillMenuOpen && (
225-
<SkillMenu
226-
groups={skillGroups}
227-
onSelectSkill={(command) => {
228-
setValue(command + ' ')
229-
setSkillMenuOpen(false)
230-
setTimeout(() => textareaRef.current?.focus(), 0)
231-
}}
232-
onClose={() => setSkillMenuOpen(false)}
233-
/>
234-
)}
235-
</div>
353+
</>
236354
)}
237-
<button
238-
onClick={handleFileSelect}
239-
disabled={disabled}
240-
className={`flex items-center justify-center rounded ${isMobile ? 'p-2.5 min-w-[44px] min-h-[44px]' : 'p-1'} text-neutral-3 hover:text-neutral-1 hover:bg-neutral-7 transition-colors disabled:opacity-30`}
241-
title="Attach files"
242-
>
243-
<IconPaperclip size={isMobile ? 24 : 20} stroke={2} />
244-
</button>
245-
<button
246-
onClick={handleSend}
247-
disabled={disabled || (!value.trim() && pendingFiles.length === 0)}
248-
className={`flex items-center justify-center rounded ${isMobile ? 'p-2.5 min-w-[44px] min-h-[44px]' : 'p-1'} transition-colors disabled:opacity-30 ${
249-
value.trim() || pendingFiles.length > 0
250-
? 'bg-primary-8 text-neutral-1 hover:bg-primary-7'
251-
: 'text-neutral-5'
252-
}`}
253-
title="Send (Enter)"
254-
>
255-
<IconSend size={isMobile ? 24 : 20} stroke={2} />
256-
</button>
257355
</div>
258356
</div>
259357
</div>

src/components/MobileTopBar.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ export function MobileTopBar({ repoName, sessionName, onMenuOpen, onNewSession,
2323
<div className="app-left-sidebar mobile-top-bar-safe flex items-center h-12 px-1.5 border-b border-neutral-8/30 bg-neutral-12 flex-shrink-0">
2424
<button
2525
onClick={onMenuOpen}
26-
className="flex items-center justify-center rounded-lg size-10 text-neutral-3 hover:text-neutral-1 hover:bg-neutral-6 transition-colors"
26+
className="flex items-center justify-center rounded-lg size-[34px] text-neutral-3 hover:text-neutral-1 hover:bg-neutral-6 transition-colors"
2727
aria-label="Open menu"
2828
>
29-
<IconMenu2 size={22} stroke={2} />
29+
<IconMenu2 size={20} stroke={2} />
3030
</button>
3131

3232
<div className="flex items-center gap-2 flex-1 min-w-0 px-1.5">
3333
<AppIcon size={20} className="text-primary-7 flex-shrink-0" />
34-
<div className="flex-1 min-w-0 truncate text-[15px] text-neutral-2">
34+
<div className="flex-1 min-w-0 truncate text-[16px] text-neutral-2">
3535
{repoName ? (
3636
<>
3737
<span className="font-semibold">{repoName}</span>
@@ -45,21 +45,21 @@ export function MobileTopBar({ repoName, sessionName, onMenuOpen, onNewSession,
4545
</div>
4646
</div>
4747

48-
<div className="flex items-center">
48+
<div className="flex items-center gap-0.5">
4949
<button
5050
onClick={onSettingsOpen}
51-
className="flex items-center justify-center rounded-lg size-10 text-neutral-4 hover:text-neutral-1 hover:bg-neutral-6 transition-colors"
51+
className="flex items-center justify-center rounded-lg size-[34px] text-neutral-4 hover:text-neutral-1 hover:bg-neutral-6 transition-colors"
5252
aria-label="Settings"
5353
>
54-
<IconSettings size={20} stroke={2} />
54+
<IconSettings size={18} stroke={2} />
5555
</button>
5656

5757
<button
5858
onClick={onNewSession}
59-
className="flex items-center justify-center rounded-lg size-10 text-neutral-4 hover:text-neutral-1 hover:bg-neutral-6 transition-colors"
59+
className="flex items-center justify-center rounded-lg size-[34px] text-neutral-4 hover:text-neutral-1 hover:bg-neutral-6 transition-colors"
6060
aria-label="New session"
6161
>
62-
<IconPlus size={20} stroke={2} />
62+
<IconPlus size={18} stroke={2} />
6363
</button>
6464
</div>
6565
</div>

src/components/PromptButtons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ interface PromptButtonsProps {
3333
/** Sticky prompt bar for permission approvals, single/multi-select questions, and multi-question AskUserQuestion flows. */
3434
export function PromptButtons({ options, question, multiSelect, promptType, questions, approvePattern, onSelect, isMobile = false }: PromptButtonsProps) {
3535
const isPermission = promptType === 'permission'
36-
const btnPad = isMobile ? 'px-5 py-3 text-[15px] min-h-[44px]' : 'px-3 py-0.5 text-[13px]'
36+
const btnPad = isMobile ? 'px-4 py-2.5 text-[16px] min-h-[34px]' : 'px-3 py-0.5 text-[13px]'
3737

3838
// Auto-allow countdown for permission prompts
3939
const [timeLeft, setTimeLeft] = useState(15)

0 commit comments

Comments
 (0)