@@ -6,11 +6,11 @@ import { ToolIcon } from "@app/components/shared/ToolIcon";
66import { ToolRegistryEntry } from "@app/data/toolsTaxonomy" ;
77import { useToolNavigation } from "@app/hooks/useToolNavigation" ;
88import { handleUnlessSpecialClick } from "@app/utils/clickHandlers" ;
9- import { openUrl } from "@app/utils/urlUtils " ;
9+ import { openUrl } from "@app/utils/urlExtensions " ;
1010import FitText from "@app/components/shared/FitText" ;
1111import { useHotkeys } from "@app/contexts/HotkeyContext" ;
1212import HotkeyDisplay from "@app/components/hotkeys/HotkeyDisplay" ;
13- import FavoriteStar from "@app/components/tools/toolpicker /FavoriteStar" ;
13+ import FavoriteStar from "@app/components/tools/toolPicker /FavoriteStar" ;
1414import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext" ;
1515import type { ToolId } from "@app/types/toolId" ;
1616import {
@@ -28,45 +28,27 @@ interface ToolButtonProps {
2828 onSelect : ( id : ToolId ) => void ;
2929 rounded ?: boolean ;
3030 disableNavigation ?: boolean ;
31+ onUnavailableClick ?: ( ) => void ;
3132 matchedSynonym ?: string ;
3233 hasStars ?: boolean ;
33- /** Called when an unavailable tool is clicked; if provided, overrides the default no-op */
34- onUnavailableClick ?: ( ) => void ;
3534}
3635
3736const ToolButton : React . FC < ToolButtonProps > = ( {
3837 id,
3938 tool,
4039 isSelected,
4140 onSelect,
41+ rounded = false ,
4242 disableNavigation = false ,
43+ onUnavailableClick,
4344 matchedSynonym,
4445 hasStars = false ,
45- onUnavailableClick,
4646} ) => {
4747 const { t } = useTranslation ( ) ;
48+ const { getToolNavigation } = useToolNavigation ( ) ;
49+ const { toolAvailability } = useToolWorkflow ( ) ;
4850 const { config } = useAppConfig ( ) ;
4951 const premiumEnabled = config ?. premiumEnabled ;
50- const { isFavorite, toggleFavorite, toolAvailability } = useToolWorkflow ( ) ;
51- const disabledReason = getToolDisabledReason (
52- id ,
53- tool ,
54- toolAvailability ,
55- premiumEnabled ,
56- ) ;
57- const isUnavailable = disabledReason !== null ;
58- // If onUnavailableClick is provided for a non-comingSoon tool, render as "cloud-available":
59- // full opacity, cloud badge, normal tooltip — clicking still fires onUnavailableClick (e.g. sign-in).
60- const showAsCloudAvailable =
61- isUnavailable &&
62- ! ! onUnavailableClick &&
63- disabledReason !== "comingSoon" &&
64- disabledReason !== "selfHostedOffline" ;
65- const visuallyUnavailable = isUnavailable && ! showAsCloudAvailable ;
66- const { hotkeys } = useHotkeys ( ) ;
67- const binding = hotkeys [ id ] ;
68- const { getToolNavigation } = useToolNavigation ( ) ;
69- const fav = isFavorite ( id as ToolId ) ;
7052
7153 // Check if this tool will route to SaaS backend (desktop only)
7254 const rawEndpoint = tool . operationConfig ?. endpoint ;
@@ -94,109 +76,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({
9476 ? getToolNavigation ( id , tool )
9577 : null ;
9678
97- const { key : disabledKey , fallback : disabledFallback } =
98- getDisabledLabel ( disabledReason ) ;
99- const disabledMessage = t ( disabledKey , disabledFallback ) ;
100-
101- const tooltipContent = visuallyUnavailable ? (
102- < span >
103- < strong > { disabledMessage } </ strong > { tool . description }
104- </ span >
105- ) : (
106- < div style = { { display : "flex" , flexDirection : "column" , gap : "0.35rem" } } >
107- < span > { tool . description } </ span >
108- < div
109- style = { {
110- display : "flex" ,
111- alignItems : "center" ,
112- gap : "0.5rem" ,
113- fontSize : "0.75rem" ,
114- } }
115- >
116- { binding ? (
117- < >
118- < span
119- style = { { color : "var(--mantine-color-dimmed)" , fontWeight : 500 } }
120- >
121- { t ( "settings.hotkeys.shortcut" , "Shortcut" ) }
122- </ span >
123- < HotkeyDisplay binding = { binding } />
124- </ >
125- ) : (
126- < span
127- style = { {
128- color : "var(--mantine-color-dimmed)" ,
129- fontWeight : 500 ,
130- fontStyle : "italic" ,
131- } }
132- >
133- { t ( "settings.hotkeys.noShortcut" , "No shortcut set" ) }
134- </ span >
135- ) }
136- </ div >
137- </ div >
138- ) ;
139-
140- const buttonContent = (
141- < >
142- < ToolIcon icon = { tool . icon } opacity = { visuallyUnavailable ? 0.25 : 1 } />
143- < div
144- style = { {
145- display : "flex" ,
146- flexDirection : "column" ,
147- alignItems : "flex-start" ,
148- flex : 1 ,
149- overflow : "visible" ,
150- } }
151- >
152- < div
153- style = { {
154- display : "flex" ,
155- alignItems : "center" ,
156- gap : "0.5rem" ,
157- width : "100%" ,
158- } }
159- >
160- < FitText
161- text = { tool . name }
162- lines = { 1 }
163- minimumFontScale = { 0.8 }
164- as = "span"
165- style = { {
166- display : "inline-block" ,
167- maxWidth : "100%" ,
168- opacity : visuallyUnavailable ? 0.25 : 1 ,
169- } }
170- />
171- { tool . versionStatus === "alpha" && (
172- < Badge
173- size = "xs"
174- variant = "light"
175- color = "orange"
176- style = { { flexShrink : 0 , opacity : visuallyUnavailable ? 0.25 : 1 } }
177- >
178- { t ( "toolPanel.alpha" , "Alpha" ) }
179- </ Badge >
180- ) }
181- { usesCloud && ! visuallyUnavailable && < CloudBadge /> }
182- </ div >
183- { matchedSynonym && (
184- < span
185- style = { {
186- fontSize : "0.75rem" ,
187- color : "var(--mantine-color-dimmed)" ,
188- opacity : visuallyUnavailable ? 0.25 : 1 ,
189- marginTop : "1px" ,
190- overflow : "visible" ,
191- whiteSpace : "nowrap" ,
192- } }
193- >
194- { matchedSynonym }
195- </ span >
196- ) }
197- </ div >
198- </ >
199- ) ;
79+ const isUnavailable = toolAvailability ?. [ id ] === "unavailable" ;
20080
20181 const handleExternalClick = ( e : React . MouseEvent ) => {
20282 handleUnlessSpecialClick ( e , ( ) => handleClick ( id ) ) ;
@@ -211,96 +91,119 @@ const ToolButton: React.FC<ToolButtonProps> = ({
21191 variant = { isSelected ? "filled" : "subtle" }
21292 size = "sm"
21393 radius = "md"
214- fullWidth
215- justify = "flex-start"
216- className = "tool-button"
217- data-tour = { `tool-button-${ id } ` }
218- styles = { {
219- root : {
220- borderRadius : 0 ,
221- color : "var(--tools-text-and-icon-color)" ,
222- overflow : "visible" ,
223- } ,
224- label : { overflow : "visible" } ,
225- } }
94+ className = { `tool-button ${ rounded ? "tool-button--rounded" : "" } ${
95+ isSelected ? "tool-button--selected" : ""
96+ } ${ isUnavailable ? "tool-button--unavailable" : "" } `}
97+ title = { tool . name }
98+ data-tool-id = { id }
99+ disabled = { isUnavailable }
100+ aria-label = { tool . name }
226101 >
227- { buttonContent }
102+ < div className = "tool-button-content" >
103+ < ToolIcon icon = { tool . icon } size = { 20 } />
104+ < FitText
105+ text = { matchedSynonym || tool . name }
106+ className = "tool-button-label"
107+ maxLines = { 1 }
108+ />
109+ { hasStars && < FavoriteStar isFavorite = { false } onToggle = { ( ) => { } } /> }
110+ { usesCloud && < CloudBadge /> }
111+ { isUnavailable && (
112+ < Badge
113+ size = "xs"
114+ variant = "light"
115+ color = "gray"
116+ className = "tool-button-unavailable-badge"
117+ >
118+ { t ( "common.unavailable" , "Unavailable" ) }
119+ </ Badge >
120+ ) }
121+ </ div >
228122 </ Button >
229123 ) : tool . link && ! isUnavailable ? (
230124 // For external links, render Button as an anchor with proper href
231125 < Button
232126 component = "a"
233127 href = { tool . link }
234- target = "_blank"
235- rel = "noopener noreferrer"
236128 onClick = { handleExternalClick }
237129 variant = { isSelected ? "filled" : "subtle" }
238130 size = "sm"
239131 radius = "md"
240- fullWidth
241- justify = "flex-start"
242- className = "tool-button"
243- data-tour = { `tool-button-${ id } ` }
244- styles = { {
245- root : {
246- borderRadius : 0 ,
247- color : "var(--tools-text-and-icon-color)" ,
248- overflow : "visible" ,
249- } ,
250- label : { overflow : "visible" } ,
251- } }
132+ className = { `tool-button ${ rounded ? "tool-button--rounded" : "" } ${
133+ isSelected ? "tool-button--selected" : ""
134+ } `}
135+ title = { tool . name }
136+ data-tool-id = { id }
137+ target = "_blank"
138+ rel = "noopener noreferrer"
139+ aria-label = { tool . name }
252140 >
253- { buttonContent }
141+ < div className = "tool-button-content" >
142+ < ToolIcon icon = { tool . icon } size = { 20 } />
143+ < FitText
144+ text = { matchedSynonym || tool . name }
145+ className = "tool-button-label"
146+ maxLines = { 1 }
147+ />
148+ { hasStars && < FavoriteStar isFavorite = { false } onToggle = { ( ) => { } } /> }
149+ </ div >
254150 </ Button >
255151 ) : (
256- // For unavailable tools, use regular button
152+ // For normal tools without URLs
257153 < Button
258154 variant = { isSelected ? "filled" : "subtle" }
259155 onClick = { ( ) => handleClick ( id ) }
260156 size = "sm"
261157 radius = "md"
262- fullWidth
263- justify = "flex-start"
264- className = "tool-button"
265- aria-disabled = { isUnavailable }
266- data-tour = { `tool-button-${ id } ` }
267- styles = { {
268- root : {
269- borderRadius : 0 ,
270- color : "var(--tools-text-and-icon-color)" ,
271- cursor : visuallyUnavailable ? "not-allowed" : undefined ,
272- overflow : "visible" ,
273- } ,
274- label : { overflow : "visible" } ,
275- } }
158+ className = { `tool-button ${ rounded ? "tool-button--rounded" : "" } ${
159+ isSelected ? "tool-button--selected" : ""
160+ } ${ isUnavailable ? "tool-button--unavailable" : "" } `}
161+ title = { tool . name }
162+ data-tool-id = { id }
163+ disabled = { isUnavailable }
164+ aria-label = { tool . name }
276165 >
277- { buttonContent }
166+ < div className = "tool-button-content" >
167+ < ToolIcon icon = { tool . icon } size = { 20 } />
168+ < FitText
169+ text = { matchedSynonym || tool . name }
170+ className = "tool-button-label"
171+ maxLines = { 1 }
172+ />
173+ { hasStars && < FavoriteStar isFavorite = { false } onToggle = { ( ) => { } } /> }
174+ { usesCloud && < CloudBadge /> }
175+ { isUnavailable && (
176+ < Badge
177+ size = "xs"
178+ variant = "light"
179+ color = "gray"
180+ className = "tool-button-unavailable-badge"
181+ >
182+ { t ( "common.unavailable" , "Unavailable" ) }
183+ </ Badge >
184+ ) }
185+ </ div >
278186 </ Button >
279187 ) ;
280188
281- const star =
282- hasStars && ! visuallyUnavailable ? (
283- < FavoriteStar
284- isFavorite = { fav }
285- onToggle = { ( ) => toggleFavorite ( id as ToolId ) }
286- className = "tool-button-star"
287- size = "xs"
288- />
289- ) : null ;
189+ const unavailableReason = isUnavailable
190+ ? getToolDisabledReason ( tool , premiumEnabled )
191+ : null ;
192+ const disabledLabel = unavailableReason
193+ ? getDisabledLabel ( unavailableReason )
194+ : null ;
290195
291196 return (
292- < div className = "tool-button-container" >
293- { star }
294- < Tooltip
295- content = { tooltipContent }
296- position = "right"
297- arrow = { true }
298- delay = { 500 }
299- >
300- { buttonElement }
301- </ Tooltip >
302- </ div >
197+ < Tooltip
198+ label = { disabledLabel || tool . description }
199+ disabled = { ! disabledLabel && ! tool . description }
200+ position = "right"
201+ withArrow
202+ openDelay = { 500 }
203+ >
204+ { buttonElement }
205+ </ Tooltip >
303206 ) ;
304207} ;
305208
306- export default ToolButton ;
209+ export default ToolButton ;
0 commit comments