11import type { ToolItem } from '~/lib/hooks/useToolMentionAutocomplete' ;
2- import { createPortal } from 'react-dom' ;
3- import { useEffect , useRef , useState } from 'react' ;
2+ import { useEffect , useRef } from 'react' ;
43import { classNames } from '~/utils/classNames' ;
4+ import * as Dialog from '@radix-ui/react-dialog' ;
55
66interface ToolMentionAutocompleteProps {
7+ isOpen : boolean ;
78 tools : ToolItem [ ] ;
89 selectedIndex : number ;
910 position : { x : number ; y : number } | null ;
1011 onSelect : ( toolName : string ) => void ;
1112 onHover : ( index : number ) => void ;
13+ onClose : ( ) => void ;
1214 searchQuery : string ;
1315}
1416
1517export function ToolMentionAutocomplete ( {
18+ isOpen,
1619 tools,
1720 selectedIndex,
1821 position,
1922 onSelect,
2023 onHover,
24+ onClose,
2125 searchQuery,
2226} : ToolMentionAutocompleteProps ) {
2327 const dropdownRef = useRef < HTMLDivElement > ( null ) ;
24- const [ adjustedPosition , setAdjustedPosition ] = useState ( position ) ;
25-
26- useEffect ( ( ) => {
27- if ( ! position || ! dropdownRef . current ) {
28- setAdjustedPosition ( position ) ;
29-
30- return ;
31- }
32-
33- const dropdown = dropdownRef . current ;
34- const rect = dropdown . getBoundingClientRect ( ) ;
35- const viewportHeight = window . innerHeight ;
36- const viewportWidth = window . innerWidth ;
37-
38- let { x, y } = position ;
39-
40- if ( y + rect . height > viewportHeight ) {
41- y = position . y - rect . height - 10 ;
42- }
43-
44- if ( x + rect . width > viewportWidth ) {
45- x = viewportWidth - rect . width - 10 ;
46- }
47-
48- setAdjustedPosition ( { x, y } ) ;
49- } , [ position , tools ] ) ;
5028
5129 useEffect ( ( ) => {
5230 if ( dropdownRef . current && selectedIndex >= 0 ) {
@@ -58,7 +36,7 @@ export function ToolMentionAutocomplete({
5836 }
5937 } , [ selectedIndex ] ) ;
6038
61- if ( ! adjustedPosition || tools . length === 0 ) {
39+ if ( ! isOpen || ! position ) {
6240 return null ;
6341 }
6442
@@ -80,74 +58,87 @@ export function ToolMentionAutocomplete({
8058
8159 let globalIndex = 0 ;
8260
83- const content = (
84- < div
85- ref = { dropdownRef }
86- className = "fixed z-[9999] min-w-[400px] max-w-[500px] bg-codinit-elements-bg-depth-1 border border-codinit-elements-borderColor rounded-lg shadow-lg transition-theme"
87- style = { {
88- left : `${ adjustedPosition . x } px` ,
89- top : `${ adjustedPosition . y } px` ,
90- } }
91- >
92- < div className = "max-h-[300px] overflow-y-auto p-2" >
93- { tools . length === 0 ? (
94- < div className = "py-4 text-center text-sm text-codinit-elements-textTertiary" >
95- No tools found for "{ searchQuery } "
96- </ div >
97- ) : (
98- serverNames . map ( ( serverName ) => {
99- const serverTools = groupedTools [ serverName ] ;
100-
101- return (
102- < div key = { serverName } className = "mb-2" >
103- { showServerGroups && (
104- < div className = "px-3 py-2 text-xs font-medium text-codinit-elements-textSecondary" >
105- 📦 { serverName }
106- </ div >
107- ) }
108- { serverTools . map ( ( tool ) => {
109- const currentIndex = globalIndex ++ ;
110- const isSelected = currentIndex === selectedIndex ;
61+ return (
62+ < Dialog . Root open = { isOpen } onOpenChange = { ( open ) => ! open && onClose ( ) } modal = { false } >
63+ < Dialog . Portal >
64+ < Dialog . Content
65+ onOpenAutoFocus = { ( e ) => e . preventDefault ( ) }
66+ onCloseAutoFocus = { ( e ) => e . preventDefault ( ) }
67+ onEscapeKeyDown = { onClose }
68+ onPointerDownOutside = { onClose }
69+ onInteractOutside = { ( e ) => e . preventDefault ( ) }
70+ className = "fixed z-[9999] outline-none"
71+ style = { {
72+ left : `${ position . x } px` ,
73+ top : `${ position . y } px` ,
74+ } }
75+ >
76+ < div
77+ ref = { dropdownRef }
78+ className = "min-w-[400px] max-w-[500px] bg-codinit-elements-bg-depth-1 border border-codinit-elements-borderColor rounded-lg shadow-lg transition-theme"
79+ >
80+ < div className = "max-h-[300px] overflow-y-auto p-2" >
81+ { tools . length === 0 ? (
82+ < div className = "py-4 text-center text-sm text-codinit-elements-textTertiary" >
83+ No tools found for "{ searchQuery } "
84+ </ div >
85+ ) : (
86+ serverNames . map ( ( serverName ) => {
87+ const serverTools = groupedTools [ serverName ] ;
11188
11289 return (
113- < div
114- key = { `${ serverName } -${ tool . name } ` }
115- onClick = { ( ) => onSelect ( tool . name ) }
116- onMouseEnter = { ( ) => onHover ( currentIndex ) }
117- data-selected = { isSelected }
118- className = { classNames (
119- 'cursor-pointer rounded-md px-3 py-2 mb-1 transition-colors' ,
120- isSelected
121- ? 'bg-accent-500 text-white'
122- : 'hover:bg-codinit-elements-item-backgroundDefault text-codinit-elements-textPrimary' ,
123- ) }
124- >
125- < div className = "flex flex-col gap-1 w-full" >
126- < div className = "flex items-center gap-2" >
127- < span className = "text-base" > 🔧</ span >
128- < span className = "font-medium text-sm" > { tool . name } </ span >
90+ < div key = { serverName } className = "mb-2" >
91+ { showServerGroups && (
92+ < div className = "px-3 py-2 text-xs font-medium text-codinit-elements-textSecondary" >
93+ 📦 { serverName }
12994 </ div >
130- { tool . description && (
95+ ) }
96+ { serverTools . map ( ( tool ) => {
97+ const currentIndex = globalIndex ++ ;
98+ const isSelected = currentIndex === selectedIndex ;
99+
100+ return (
131101 < div
102+ key = { `${ serverName } -${ tool . name } ` }
103+ onClick = { ( ) => onSelect ( tool . name ) }
104+ onMouseEnter = { ( ) => onHover ( currentIndex ) }
105+ data-selected = { isSelected }
132106 className = { classNames (
133- 'text-xs ml-6' ,
134- isSelected ? 'text-white opacity-90' : 'text-codinit-elements-textSecondary' ,
107+ 'cursor-pointer rounded-md px-3 py-2 mb-1 transition-colors' ,
108+ isSelected
109+ ? 'bg-accent-500 text-white'
110+ : 'hover:bg-codinit-elements-item-backgroundDefault text-codinit-elements-textPrimary' ,
135111 ) }
136112 >
137- { tool . description . length > 100 ? `${ tool . description . slice ( 0 , 100 ) } ...` : tool . description }
113+ < div className = "flex flex-col gap-1 w-full" >
114+ < div className = "flex items-center gap-2" >
115+ < span className = "text-base" > 🔧</ span >
116+ < span className = "font-medium text-sm" > { tool . name } </ span >
117+ </ div >
118+ { tool . description && (
119+ < div
120+ className = { classNames (
121+ 'text-xs ml-6' ,
122+ isSelected ? 'text-white opacity-90' : 'text-codinit-elements-textSecondary' ,
123+ ) }
124+ >
125+ { tool . description . length > 100
126+ ? `${ tool . description . slice ( 0 , 100 ) } ...`
127+ : tool . description }
128+ </ div >
129+ ) }
130+ </ div >
138131 </ div >
139- ) }
140- </ div >
132+ ) ;
133+ } ) }
141134 </ div >
142135 ) ;
143- } ) }
144- </ div >
145- ) ;
146- } )
147- ) }
148- </ div >
149- </ div >
136+ } )
137+ ) }
138+ </ div >
139+ </ div >
140+ </ Dialog . Content >
141+ </ Dialog . Portal >
142+ </ Dialog . Root >
150143 ) ;
151-
152- return createPortal ( content , document . body ) ;
153144}
0 commit comments