11import {
22 PiCopyDuotone ,
33 PiArrowSquareOutLight ,
4- PiLinkLight ,
54 PiPlugsConnectedLight ,
65 PiCheckCircleLight ,
76} from "react-icons/pi" ;
8- import { useState } from "react" ;
7+ import { useState , useRef , useCallback } from "react" ;
8+ import { createPortal } from "react-dom" ;
99import ClaudeIcon from "./icons/ClaudeIcon" ;
1010import ChatGPTIcon from "./icons/ChatGPTIcon" ;
1111import { track } from "~/util/zaraz" ;
1212
1313type CopyFeedback = { key : string ; state : "success" } | null ;
1414
15+ interface ListOption {
16+ key : string ;
17+ label : string ;
18+ tooltip : string ;
19+ icon : React . ComponentType < { className ?: string } > ;
20+ onClick : ( ) => void ;
21+ }
22+
23+ interface IconOption {
24+ key : string ;
25+ tooltip : string ;
26+ icon : React . ComponentType < { className ?: string } > ;
27+ onClick : ( ) => void ;
28+ }
29+
30+ function useTooltip ( ) {
31+ const [ visible , setVisible ] = useState ( false ) ;
32+ const [ position , setPosition ] = useState ( { top : 0 , left : 0 } ) ;
33+ const triggerRef = useRef < HTMLElement | null > ( null ) ;
34+
35+ const show = useCallback ( ( ) => {
36+ if ( triggerRef . current ) {
37+ const rect = triggerRef . current . getBoundingClientRect ( ) ;
38+ setPosition ( {
39+ top : rect . top - 8 ,
40+ left : rect . left + rect . width / 2 ,
41+ } ) ;
42+ }
43+ setVisible ( true ) ;
44+ } , [ ] ) ;
45+
46+ const hide = useCallback ( ( ) => {
47+ setVisible ( false ) ;
48+ } , [ ] ) ;
49+
50+ return { visible, position, triggerRef, show, hide } ;
51+ }
52+
53+ function TooltipPortal ( {
54+ text,
55+ visible,
56+ position,
57+ } : {
58+ text : string ;
59+ visible : boolean ;
60+ position : { top : number ; left : number } ;
61+ } ) {
62+ if ( ! visible ) return null ;
63+
64+ return createPortal (
65+ < div
66+ role = "tooltip"
67+ className = "pointer-events-none fixed z-[99999] -translate-x-1/2 -translate-y-full rounded-md bg-[var(--sl-color-bg-nav)] px-3 py-2 text-center text-[12px] leading-snug text-[var(--sl-color-text)] shadow-lg ring-1 ring-[var(--sl-color-hairline)]"
68+ style = { { top : position . top , left : position . left } }
69+ >
70+ { text }
71+ </ div > ,
72+ document . body ,
73+ ) ;
74+ }
75+
76+ function ListItem ( {
77+ tooltip,
78+ onClick,
79+ justCopied,
80+ icon : Icon ,
81+ label,
82+ } : {
83+ tooltip : string ;
84+ onClick : ( ) => void ;
85+ justCopied : boolean ;
86+ icon : React . ComponentType < { className ?: string } > ;
87+ label : string ;
88+ } ) {
89+ const { visible, position, triggerRef, show, hide } = useTooltip ( ) ;
90+
91+ return (
92+ < li className = "m-0 p-0" >
93+ < TooltipPortal text = { tooltip } visible = { visible } position = { position } />
94+ < button
95+ ref = { triggerRef as React . RefObject < HTMLButtonElement > }
96+ onMouseEnter = { show }
97+ onMouseLeave = { hide }
98+ onFocus = { show }
99+ onBlur = { hide }
100+ onClick = { onClick }
101+ className = "flex w-full cursor-pointer items-center gap-2.5 rounded-sm border-0 bg-transparent px-0 py-1 text-[13px] text-[var(--sl-color-gray-2)] shadow-none transition-colors duration-150 ease-out hover:text-[var(--sl-color-white)] focus-visible:ring-2 focus-visible:ring-[var(--sl-color-text-accent)] focus-visible:outline-none"
102+ >
103+ { justCopied ? (
104+ < PiCheckCircleLight className = "h-3.5 w-3.5 shrink-0 text-green-500" />
105+ ) : (
106+ < Icon className = "h-3.5 w-3.5 shrink-0" />
107+ ) }
108+ < span > { justCopied ? "Copied!" : label } </ span >
109+ </ button >
110+ </ li >
111+ ) ;
112+ }
113+
114+ function IconButton ( {
115+ tooltip,
116+ onClick,
117+ icon : Icon ,
118+ } : {
119+ tooltip : string ;
120+ onClick : ( ) => void ;
121+ icon : React . ComponentType < { className ?: string } > ;
122+ } ) {
123+ const { visible, position, triggerRef, show, hide } = useTooltip ( ) ;
124+
125+ return (
126+ < div >
127+ < TooltipPortal text = { tooltip } visible = { visible } position = { position } />
128+ < button
129+ ref = { triggerRef as React . RefObject < HTMLButtonElement > }
130+ onMouseEnter = { show }
131+ onMouseLeave = { hide }
132+ onFocus = { show }
133+ onBlur = { hide }
134+ onClick = { onClick }
135+ className = "inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-[var(--sl-color-gray-2)] shadow-none transition-colors duration-150 ease-out hover:bg-[var(--color-cl1-gray-9)] hover:text-[var(--sl-color-white)] focus-visible:ring-2 focus-visible:ring-[var(--sl-color-text-accent)] focus-visible:outline-none dark:hover:bg-[var(--color-cl1-gray-2)]"
136+ >
137+ < Icon className = "h-4 w-4" />
138+ < span className = "sr-only" > { tooltip } </ span >
139+ </ button >
140+ </ div >
141+ ) ;
142+ }
143+
15144export default function AgentsToolkit ( ) {
16145 const [ copyFeedback , setCopyFeedback ] = useState < CopyFeedback > ( null ) ;
17146
@@ -20,16 +149,6 @@ export default function AgentsToolkit() {
20149 setTimeout ( ( ) => setCopyFeedback ( null ) , 1500 ) ;
21150 } ;
22151
23- const handleCopyPageLink = async ( ) => {
24- try {
25- await navigator . clipboard . writeText ( window . location . href ) ;
26- track ( "agents toolkit clicked" , { value : "copy page link" } ) ;
27- showFeedback ( "copy-link" ) ;
28- } catch ( error ) {
29- console . error ( "Failed to copy page link:" , error ) ;
30- }
31- } ;
32-
33152 const handleCopyMarkdown = async ( ) => {
34153 const markdownUrl = new URL ( "index.md" , window . location . href ) . toString ( ) ;
35154 try {
@@ -67,40 +186,39 @@ export default function AgentsToolkit() {
67186 window . open ( "/style-guide/ai-tooling/" , "_blank" ) ;
68187 } ;
69188
70- const options = [
189+ const listOptions : ListOption [ ] = [
71190 {
72191 key : "ai-options" ,
73192 label : "Setup your agent" ,
193+ tooltip : "Explore AI tooling options for Cloudflare docs" ,
74194 icon : PiPlugsConnectedLight ,
75195 onClick : handleViewAIOptions ,
76196 } ,
77- {
78- key : "copy-link" ,
79- label : "Copy page link" ,
80- icon : PiLinkLight ,
81- onClick : handleCopyPageLink ,
82- } ,
83197 {
84198 key : "copy-md" ,
85199 label : "Copy as Markdown" ,
200+ tooltip : "Copy this page's Markdown source to clipboard" ,
86201 icon : PiCopyDuotone ,
87202 onClick : handleCopyMarkdown ,
88203 } ,
204+ ] ;
205+
206+ const iconOptions : IconOption [ ] = [
89207 {
90208 key : "view-md" ,
91- label : "View as Markdown" ,
209+ tooltip : "Open the Markdown file in a new tab " ,
92210 icon : PiArrowSquareOutLight ,
93211 onClick : handleViewMarkdown ,
94212 } ,
95213 {
96214 key : "claude" ,
97- label : "Open in Claude" ,
215+ tooltip : "Ask Claude about this page " ,
98216 icon : ClaudeIcon ,
99217 onClick : ( ) => handleExternalAI ( "https://claude.ai/new?q=" , "claude" ) ,
100218 } ,
101219 {
102220 key : "chatgpt" ,
103- label : "Open in ChatGPT" ,
221+ tooltip : "Ask ChatGPT about this page " ,
104222 icon : ChatGPTIcon ,
105223 onClick : ( ) =>
106224 handleExternalAI ( "https://chat.openai.com/?prompt=" , "chatgpt" ) ,
@@ -113,28 +231,33 @@ export default function AgentsToolkit() {
113231 Agents toolkit
114232 </ h3 >
115233 < ul className = "m-0 flex list-none flex-col gap-0 p-0" >
116- { options . map ( ( { key, label, icon : Icon , onClick } ) => {
234+ { listOptions . map ( ( { key, label, tooltip , icon : Icon , onClick } ) => {
117235 const justCopied =
118- copyFeedback ?. key === key &&
119- copyFeedback . state === "success" ;
236+ copyFeedback ?. key === key && copyFeedback . state === "success" ;
120237
121238 return (
122- < li key = { key } className = "m-0 p-0" >
123- < button
124- onClick = { onClick }
125- className = "group flex w-full cursor-pointer items-center gap-2.5 rounded-sm border-0 bg-transparent px-0 py-1 text-[13px] text-[var(--sl-color-gray-2)] shadow-none transition-colors duration-150 ease-out hover:text-[var(--sl-color-white)] focus-visible:ring-2 focus-visible:ring-[var(--sl-color-text-accent)] focus-visible:outline-none"
126- >
127- { justCopied ? (
128- < PiCheckCircleLight className = "h-3.5 w-3.5 shrink-0 text-green-500" />
129- ) : (
130- < Icon className = "h-3.5 w-3.5 shrink-0" />
131- ) }
132- < span > { justCopied ? "Copied!" : label } </ span >
133- </ button >
134- </ li >
239+ < ListItem
240+ key = { key }
241+ tooltip = { tooltip }
242+ onClick = { onClick }
243+ justCopied = { justCopied }
244+ icon = { Icon }
245+ label = { label }
246+ />
135247 ) ;
136248 } ) }
137249 </ ul >
250+
251+ < div className = "mt-2 flex items-center gap-1" >
252+ { iconOptions . map ( ( { key, tooltip, icon : Icon , onClick } ) => (
253+ < IconButton
254+ key = { key }
255+ tooltip = { tooltip }
256+ onClick = { onClick }
257+ icon = { Icon }
258+ />
259+ ) ) }
260+ </ div >
138261 </ div >
139262 ) ;
140263}
0 commit comments