11import { TextAttributes } from '@opentui/core'
22import open from 'open'
3- import React , { useCallback , useState } from 'react'
3+ import React , { useState } from 'react'
44
55import { Button } from './button'
6+ import { Clickable } from './clickable'
67import { useTerminalDimensions } from '../hooks/use-terminal-dimensions'
78import { useTheme } from '../hooks/use-theme'
89import { IS_FREEBUFF } from '../utils/constants'
@@ -34,14 +35,6 @@ export const AdBanner: React.FC<AdBannerProps> = ({ ad, onDisableAds, isFreeMode
3435 const [ isHideHovered , setIsHideHovered ] = useState ( false )
3536 const [ isCloseHovered , setIsCloseHovered ] = useState ( false )
3637
37- const handleClick = useCallback ( ( ) => {
38- if ( ad . clickUrl ) {
39- open ( ad . clickUrl ) . catch ( ( err ) => {
40- logger . error ( err , 'Failed to open ad link' )
41- } )
42- }
43- } , [ ad . clickUrl ] )
44-
4538 // Use 'url' field for display domain (the actual destination)
4639 const domain = extractDomain ( ad . url )
4740 // Use cta field for button text, with title as fallback
@@ -51,6 +44,17 @@ export const AdBanner: React.FC<AdBannerProps> = ({ ad, onDisableAds, isFreeMode
5144 // Account for: padding (2), "Ad ?" label with space (5)
5245 const maxTextWidth = separatorWidth - 7
5346
47+ // Wrapper for hover detection - makes entire ad content clickable
48+ const handleAdMouseOver = ( ) => setIsLinkHovered ( true )
49+ const handleAdMouseOut = ( ) => setIsLinkHovered ( false )
50+ const handleAdClick = ( ) => {
51+ if ( ad . clickUrl ) {
52+ open ( ad . clickUrl ) . catch ( ( err ) => {
53+ logger . error ( err , 'Failed to open ad link' )
54+ } )
55+ }
56+ }
57+
5458 return (
5559 < box
5660 style = { {
@@ -60,59 +64,75 @@ export const AdBanner: React.FC<AdBannerProps> = ({ ad, onDisableAds, isFreeMode
6064 >
6165 { /* Horizontal divider line */ }
6266 < text style = { { fg : theme . muted } } > { '─' . repeat ( terminalWidth ) } </ text >
63- { /* Top line: ad text + Ad label */ }
64- < box
67+ { /* Clickable ad content area - wrapped in Button for click detection */ }
68+ < Button
69+ onClick = { handleAdClick }
70+ onMouseOver = { handleAdMouseOver }
71+ onMouseOut = { handleAdMouseOut }
6572 style = { {
6673 width : '100%' ,
67- paddingLeft : 1 ,
68- paddingRight : 1 ,
69- flexDirection : 'row' ,
70- justifyContent : 'space-between' ,
71- alignItems : 'flex-start' ,
74+ flexDirection : 'column' ,
7275 } }
7376 >
74- < text
77+ { /* Top line: ad text + Ad label */ }
78+ < box
7579 style = { {
76- fg : theme . foreground ,
77- flexShrink : 1 ,
78- maxWidth : maxTextWidth ,
80+ width : '100%' ,
81+ paddingLeft : 1 ,
82+ paddingRight : 1 ,
83+ flexDirection : 'row' ,
84+ justifyContent : 'space-between' ,
85+ alignItems : 'flex-start' ,
7986 } }
80- >
81- { ad . adText }
82- </ text >
83- < Button
84- onClick = { ( ) => setShowInfoPanel ( true ) }
85- onMouseOver = { ( ) => setIsAdLabelHovered ( true ) }
86- onMouseOut = { ( ) => setIsAdLabelHovered ( false ) }
8787 >
8888 < text
8989 style = { {
90- fg : isAdLabelHovered && ! showInfoPanel ? theme . foreground : theme . muted ,
91- flexShrink : 0 ,
90+ fg : theme . foreground ,
91+ flexShrink : 1 ,
92+ maxWidth : maxTextWidth ,
9293 } }
9394 >
94- { isAdLabelHovered && ! showInfoPanel ? 'Ad ?' : ' Ad' }
95+ { ad . adText }
9596 </ text >
96- </ Button >
97- </ box >
98- { /* Bottom line: button, domain, credits */ }
99- < box
100- style = { {
101- width : '100%' ,
102- paddingLeft : 1 ,
103- paddingRight : 1 ,
104- flexDirection : 'row' ,
105- flexWrap : 'wrap' ,
106- columnGap : 2 ,
107- alignItems : 'center' ,
108- } }
109- >
110- { ctaText && (
111- < Button
112- onClick = { handleClick }
113- onMouseOver = { ( ) => setIsLinkHovered ( true ) }
114- onMouseOut = { ( ) => setIsLinkHovered ( false ) }
115- >
97+ { ! IS_FREEBUFF ? (
98+ < Clickable
99+ onMouseDown = { ( ) => setShowInfoPanel ( true ) }
100+ onMouseOver = { ( ) => setIsAdLabelHovered ( true ) }
101+ onMouseOut = { ( ) => setIsAdLabelHovered ( false ) }
102+ >
103+ < text
104+ style = { {
105+ fg : isAdLabelHovered && ! showInfoPanel ? theme . foreground : theme . muted ,
106+ flexShrink : 0 ,
107+ } }
108+ >
109+ { isAdLabelHovered && ! showInfoPanel ? 'Ad ?' : ' Ad' }
110+ </ text >
111+ </ Clickable >
112+ ) : (
113+ < text
114+ style = { {
115+ fg : theme . muted ,
116+ flexShrink : 0 ,
117+ } }
118+ >
119+ { ' Ad' }
120+ </ text >
121+ ) }
122+ </ box >
123+ { /* Bottom line: button, domain, credits */ }
124+ < box
125+ style = { {
126+ width : '100%' ,
127+ paddingLeft : 1 ,
128+ paddingRight : 1 ,
129+ flexDirection : 'row' ,
130+ flexWrap : 'wrap' ,
131+ columnGap : 2 ,
132+ alignItems : 'center' ,
133+ } }
134+ >
135+ { ctaText && (
116136 < text
117137 style = { {
118138 fg : theme . name === 'light' ? '#ffffff' : theme . background ,
@@ -122,14 +142,8 @@ export const AdBanner: React.FC<AdBannerProps> = ({ ad, onDisableAds, isFreeMode
122142 >
123143 { ` ${ ctaText } ` }
124144 </ text >
125- </ Button >
126- ) }
127- { domain && (
128- < Button
129- onClick = { handleClick }
130- onMouseOver = { ( ) => setIsLinkHovered ( true ) }
131- onMouseOut = { ( ) => setIsLinkHovered ( false ) }
132- >
145+ ) }
146+ { domain && (
133147 < text
134148 style = { {
135149 fg : theme . muted ,
@@ -138,13 +152,13 @@ export const AdBanner: React.FC<AdBannerProps> = ({ ad, onDisableAds, isFreeMode
138152 >
139153 { domain }
140154 </ text >
141- </ Button >
142- ) }
143- < box style = { { flexGrow : 1 } } />
144- { ! IS_FREEBUFF && ad . credits != null && ad . credits > 0 && (
145- < text style = { { fg : theme . muted } } > + { ad . credits } credits </ text >
146- ) }
147- </ box >
155+ ) }
156+ < box style = { { flexGrow : 1 } } />
157+ { ! IS_FREEBUFF && ad . credits != null && ad . credits > 0 && (
158+ < text style = { { fg : theme . muted } } > + { ad . credits } credits </ text >
159+ ) }
160+ </ box >
161+ </ Button >
148162 { /* Info panel: shown when Ad label is clicked, below the ad */ }
149163 { showInfoPanel && (
150164 < box
0 commit comments