1- import { useMemo } from "react" ;
1+ import { Link } from "@remix-run/react" ;
2+ import { ClipboardCheckIcon , ClipboardIcon } from "lucide-react" ;
3+ import { useCallback , useMemo , useState } from "react" ;
4+ import { SimpleTooltip } from "~/components/primitives/Tooltip" ;
5+ import { cn } from "~/utils/cn" ;
26import tagLeftPath from "./tag-left.svg" ;
37
48type Tag = string | { key : string ; value : string } ;
59
6- export function RunTag ( { tag } : { tag : string } ) {
7- const tagResult = useMemo ( ( ) => splitTag ( tag ) , [ tag ] ) ;
8-
9- if ( typeof tagResult === "string" ) {
10- return (
11- < span className = "flex h-6 items-stretch" >
12- < img src = { tagLeftPath } alt = "" className = "block h-full w-[0.5625rem]" />
13- < span className = "flex items-center rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-800 pr-1.5 text-text-dimmed" >
14- { tag }
15- </ span >
16- </ span >
17- ) ;
18- } else {
19- return (
20- < span className = "flex h-6 items-stretch" >
21- < img src = { tagLeftPath } alt = "" className = "block h-full w-[0.5625rem]" />
22- < span className = "flex items-center border-y border-r border-charcoal-700 bg-charcoal-800 pr-1.5 text-text-dimmed" >
23- { tagResult . key }
24- </ span >
25- < span className = "flex items-center whitespace-nowrap rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-750 px-1.5 text-text-dimmed" >
26- { tagResult . value }
27- </ span >
28- </ span >
29- ) ;
30- }
31- }
32-
3310/** Takes a string and turns it into a tag
3411 *
3512 * If the string has 12 or fewer alpha characters followed by an underscore or colon then we return an object with a key and value
@@ -46,3 +23,103 @@ function splitTag(tag: string): Tag {
4623
4724 return tag ;
4825}
26+
27+ export function RunTag ( { tag, to, tooltip } : { tag : string ; to ?: string ; tooltip ?: string } ) {
28+ const tagResult = useMemo ( ( ) => splitTag ( tag ) , [ tag ] ) ;
29+ const [ isHovered , setIsHovered ] = useState ( false ) ;
30+
31+ // Render the basic tag content
32+ const renderBasicTagContent = ( ) => {
33+ if ( typeof tagResult === "string" ) {
34+ return (
35+ < >
36+ < img src = { tagLeftPath } alt = "" className = "block h-full w-[0.5625rem]" />
37+ < span className = "flex items-center rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-800 pr-1.5 text-text-dimmed group-hover:rounded-r-none group-has-[[href]]:group-hover:border-charcoal-650 group-has-[[href]]:group-hover:text-charcoal-300" >
38+ { tag }
39+ </ span >
40+ </ >
41+ ) ;
42+ } else {
43+ return (
44+ < >
45+ < img src = { tagLeftPath } alt = "" className = "block h-full w-[0.5625rem]" />
46+ < span className = "flex items-center border-y border-r border-charcoal-700 bg-charcoal-800 pr-1.5 text-text-dimmed group-has-[[href]]:group-hover:border-charcoal-650 group-has-[[href]]:group-hover:text-charcoal-300" >
47+ { tagResult . key }
48+ </ span >
49+ < span className = "flex items-center whitespace-nowrap rounded-r-sm border-y border-r border-charcoal-700 bg-charcoal-750 px-1.5 text-text-dimmed group-hover:rounded-r-none group-has-[[href]]:group-hover:border-charcoal-650 group-has-[[href]]:group-hover:bg-charcoal-700 group-has-[[href]]:group-hover:text-charcoal-300" >
50+ { tagResult . value }
51+ </ span >
52+ </ >
53+ ) ;
54+ }
55+ } ;
56+
57+ // The main tag content, optionally wrapped in a Link and SimpleTooltip
58+ const tagContent = to ? (
59+ < SimpleTooltip
60+ button = {
61+ < Link to = { to } className = "group" >
62+ < span className = "flex h-6 items-stretch" > { renderBasicTagContent ( ) } </ span >
63+ </ Link >
64+ }
65+ content = { tooltip || `Filter runs by ${ tag } ` }
66+ disableHoverableContent
67+ />
68+ ) : (
69+ < span className = "flex h-6 items-stretch" > { renderBasicTagContent ( ) } </ span >
70+ ) ;
71+
72+ return (
73+ < div
74+ className = "group relative inline-flex"
75+ onMouseEnter = { ( ) => setIsHovered ( true ) }
76+ onMouseLeave = { ( ) => setIsHovered ( false ) }
77+ >
78+ { tagContent }
79+ < CopyButton textToCopy = { tag } isHovered = { isHovered } />
80+ </ div >
81+ ) ;
82+ }
83+
84+ function CopyButton ( { textToCopy, isHovered } : { textToCopy : string ; isHovered : boolean } ) {
85+ const [ copied , setCopied ] = useState ( false ) ;
86+
87+ const copy = useCallback (
88+ ( e : React . MouseEvent ) => {
89+ e . preventDefault ( ) ;
90+ e . stopPropagation ( ) ;
91+ navigator . clipboard . writeText ( textToCopy ) ;
92+ setCopied ( true ) ;
93+ setTimeout ( ( ) => {
94+ setCopied ( false ) ;
95+ } , 1500 ) ;
96+ } ,
97+ [ textToCopy ]
98+ ) ;
99+
100+ return (
101+ < SimpleTooltip
102+ button = {
103+ < button
104+ onClick = { copy }
105+ onMouseDown = { ( e ) => e . stopPropagation ( ) }
106+ className = { cn (
107+ "absolute -right-6 top-0 z-10 flex size-6 items-center justify-center rounded-r-sm border-y border-r border-charcoal-650 bg-charcoal-750" ,
108+ isHovered ? "opacity-100" : "opacity-0" ,
109+ copied
110+ ? "text-green-500"
111+ : "text-text-dimmed hover:border-charcoal-600 hover:bg-charcoal-700 hover:text-text-bright"
112+ ) }
113+ >
114+ { copied ? (
115+ < ClipboardCheckIcon className = "size-3.5" />
116+ ) : (
117+ < ClipboardIcon className = "size-3.5" />
118+ ) }
119+ </ button >
120+ }
121+ content = { copied ? "Copied!" : "Copy tag" }
122+ disableHoverableContent
123+ />
124+ ) ;
125+ }
0 commit comments