-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat(web): add copy-to-clipboard to CLI install CTA with success check icon #8411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,50 @@ | |
| import { VscVscode } from "react-icons/vsc" | ||
| import Link from "next/link" | ||
| import { motion } from "framer-motion" | ||
| import { Copy, Check } from "lucide-react" | ||
| import { useState, useEffect, useRef } from "react" | ||
|
|
||
| interface InstallSectionProps { | ||
| downloads: string | null | ||
| } | ||
|
|
||
| export function InstallSection({ downloads }: InstallSectionProps) { | ||
| const [copied, setCopied] = useState(false) | ||
| const installCmd = "code --install-extension RooVeterinaryInc.roo-cline" | ||
|
|
||
| const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null) | ||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| if (timeoutRef.current) { | ||
| clearTimeout(timeoutRef.current) | ||
| } | ||
| } | ||
| }, []) | ||
|
|
||
| const handleCopy = async () => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P3] Optional: feature-detect isSecureContext and navigator.clipboard?.writeText before calling Clipboard API to avoid relying on exceptions for control flow. |
||
| try { | ||
| await navigator.clipboard.writeText(installCmd) | ||
| setCopied(true) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In handleCopy, the logic for setting the copied state and timeout is duplicated in both try and fallback blocks. Also, consider clearing any existing timeout before setting a new one to avoid potential overlaps on rapid clicks. |
||
| const id = setTimeout(() => setCopied(false), 1000) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Clear any existing timeout before scheduling a new one to avoid overlapping timers and stale state on rapid clicks. |
||
| timeoutRef.current = id | ||
| } catch (_e) { | ||
| // Fallback for environments without Clipboard API support | ||
| const textarea = document.createElement("textarea") | ||
| textarea.value = installCmd | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P3] In fallback, set textarea to readonly and position it offscreen (fixed, left:-9999px) before appendChild to avoid layout shift and improve reliability. |
||
| document.body.appendChild(textarea) | ||
| textarea.select() | ||
| try { | ||
| document.execCommand("copy") | ||
| setCopied(true) | ||
| const id = setTimeout(() => setCopied(false), 1000) | ||
| timeoutRef.current = id | ||
| } finally { | ||
| document.body.removeChild(textarea) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const backgroundVariants = { | ||
| hidden: { | ||
| opacity: 0, | ||
|
|
@@ -84,12 +122,29 @@ export function InstallSection({ downloads }: InstallSectionProps) { | |
| <div className="absolute -inset-px rounded-xl bg-gradient-to-r from-blue-500/50 via-cyan-500/50 to-purple-500/50 opacity-30 blur-sm transition-all duration-500 group-hover:opacity-60 dark:opacity-40 dark:group-hover:opacity-70" /> | ||
| <div className="relative overflow-hidden rounded-xl border border-border bg-background/80 shadow-lg backdrop-blur-xl transition-all duration-500 ease-out group-hover:border-blue-500/50 group-hover:shadow-xl group-hover:shadow-blue-500/10 dark:border-border/50 dark:bg-background/60 dark:group-hover:border-blue-400/50"> | ||
| <div className="border-b border-border/50 bg-muted/30 px-4 py-3 dark:bg-muted/20"> | ||
| <div className="text-sm font-medium text-foreground">or via CLI</div> | ||
| <div className="flex items-center justify-between"> | ||
| <div className="text-sm font-medium text-foreground">or via CLI</div> | ||
| <button | ||
|
||
| type="button" | ||
| onClick={handleCopy} | ||
| className="inline-flex items-center gap-2 rounded-md px-2 py-1 text-xs font-medium text-foreground/80 hover:text-foreground hover:bg-foreground/5 transition-colors" | ||
| aria-label={copied ? "Copied" : "Copy install command to clipboard"} | ||
| title={copied ? "Copied!" : "Copy"}> | ||
| {copied ? ( | ||
| <Check className="h-4 w-4 text-green-500" /> | ||
| ) : ( | ||
| <Copy className="h-4 w-4" /> | ||
| )} | ||
| <span className="sr-only" aria-live="polite" role="status"> | ||
| {copied ? "Copied" : "Copy"} | ||
|
||
| </span> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| <div className="overflow-x-auto bg-background/50 dark:bg-background/30"> | ||
| <pre className="p-4"> | ||
| <code className="whitespace-pre-wrap break-all text-sm font-mono text-foreground sm:break-normal sm:text-base"> | ||
| code --install-extension RooVeterinaryInc.roo-cline | ||
| {installCmd} | ||
| </code> | ||
| </pre> | ||
| </div> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P3] Optional: extract installCmd to a shared constant (e.g., apps/web-roo-code/src/lib/constants.ts) to keep it DRY across the site.