77 */
88
99import { 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'
1111import { SkillMenu , type SkillGroup } from './SkillMenu'
1212import { 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 >
0 commit comments