Skip to content

Commit 28d05bd

Browse files
committed
[Docs Lab] Portal-based tooltips and UI refinements for Agents toolkit
- Replace native title tooltips with portal-based styled tooltip popups - Convert View as Markdown, Open in Claude, Open in ChatGPT to icon buttons - Remove Copy page link option - Reorder sections: Agents toolkit now sits above Was this helpful - Add horizontal divider between Agents toolkit and feedback prompt - Update tooltip text for icon buttons
1 parent c0bb361 commit 28d05bd

1 file changed

Lines changed: 161 additions & 38 deletions

File tree

src/components/AgentsToolkit.tsx

Lines changed: 161 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,146 @@
11
import {
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";
99
import ClaudeIcon from "./icons/ClaudeIcon";
1010
import ChatGPTIcon from "./icons/ChatGPTIcon";
1111
import { track } from "~/util/zaraz";
1212

1313
type 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+
15144
export 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

Comments
 (0)