@@ -31,7 +31,7 @@ const createHeadingWithCopyLink = (
3131 ...props
3232 } : HTMLAttributes < HTMLHeadingElement > ) => {
3333 const [ copied , setCopied ] = useState ( false )
34- const [ showCopyButton , setShowCopyButton ] = useState ( false )
34+ const [ showMobileBadge , setShowMobileBadge ] = useState ( false )
3535 const isMobile = useIsMobile ( )
3636
3737 useEffect ( ( ) => {
@@ -42,14 +42,14 @@ const createHeadingWithCopyLink = (
4242 return undefined
4343 } , [ copied ] )
4444
45- // Auto-hide copy button on mobile after 3 seconds
45+ // Auto-hide mobile badge after 3 seconds
4646 useEffect ( ( ) => {
47- if ( isMobile && showCopyButton ) {
48- const timer = setTimeout ( ( ) => setShowCopyButton ( false ) , 1_500 )
47+ if ( isMobile && showMobileBadge ) {
48+ const timer = setTimeout ( ( ) => setShowMobileBadge ( false ) , 3000 )
4949 return ( ) => clearTimeout ( timer )
5050 }
5151 return undefined
52- } , [ isMobile , showCopyButton ] )
52+ } , [ isMobile , showMobileBadge ] )
5353
5454 const title = children ?. toString ( )
5555
@@ -77,7 +77,7 @@ const createHeadingWithCopyLink = (
7777 < HeadingComponent
7878 { ...props }
7979 className = { cn (
80- 'group relative hover:cursor-pointer hover:underline scroll-m-20' ,
80+ 'hover:cursor-pointer hover:underline scroll-m-20' ,
8181 defaultClasses ,
8282 className
8383 ) }
@@ -102,48 +102,78 @@ const createHeadingWithCopyLink = (
102102 document . getElementById ( id ) ?. scrollIntoView ( { behavior : 'smooth' } )
103103 }
104104
105- // On mobile, toggle copy button visibility when title is tapped
105+ // On mobile, show floating badge when title is tapped
106106 if ( isMobile ) {
107- setShowCopyButton ( ! showCopyButton )
107+ setShowMobileBadge ( true )
108108 }
109109 }
110110
111- // Determine button visibility based on device type
112- const buttonVisibilityClass = isMobile
113- ? showCopyButton
114- ? 'opacity-100'
115- : 'opacity-0'
116- : 'xs:opacity-100 xl:opacity-0 group-hover:opacity-100'
111+ // Extract margin classes from defaultClasses for container
112+ const marginClasses =
113+ defaultClasses
114+ . match (
115+ / (?: ^ | \s ) ( m t - \S + | m b - \S + | m y - \S + | m - \S + | f i r s t : m t - \S + | f i r s t : m b - \S + ) (? = \s | $ ) / g
116+ )
117+ ?. join ( ' ' ) || ''
118+ const textClasses = defaultClasses
119+ . replace (
120+ / (?: ^ | \s ) (?: m t - \S + | m b - \S + | m y - \S + | m - \S + | f i r s t : m t - \S + | f i r s t : m b - \S + ) (? = \s | $ ) / g,
121+ ''
122+ )
123+ . trim ( )
117124
118125 return (
119- < div className = "group" >
120- < HeadingComponent
121- { ...props }
122- id = { id }
123- className = { cn (
124- 'hover:cursor-pointer hover:underline scroll-m-20 inline-flex items-center gap-2' ,
125- defaultClasses ,
126- className
127- ) }
128- onClick = { handleClick }
129- >
130- { children }
131- < button
132- onClick = { handleCopy }
126+ < div className = { cn ( marginClasses ) } >
127+ < div className = "group inline-flex items-baseline" >
128+ < HeadingComponent
129+ { ...props }
130+ id = { id }
133131 className = { cn (
134- buttonVisibilityClass ,
135- 'p-1.5 rounded-md bg-muted/50 hover:bg-muted border border-border/50 hover:border-border transition-all duration-200 ease-in-out inline-flex items-center justify-center shadow-sm hover:shadow-md' ,
136- isMobile ? 'min-h-[44px] min-w-[44px]' : 'h-auto w-auto'
132+ 'relative hover:cursor-pointer hover:underline scroll-m-20' ,
133+ textClasses ,
134+ className
137135 ) }
138- aria-label = "Copy link to section"
136+ onClick = { handleClick }
139137 >
140- { copied ? (
141- < Check className = "text-green-500 h-4 w-4" />
142- ) : (
143- < Link className = "h-4 w-4 text-muted-foreground hover:text-foreground" />
138+ { children }
139+ { /* Mobile floating badge */ }
140+ { isMobile && showMobileBadge && (
141+ < div className = "absolute -top-12 left-0 z-10" >
142+ < button
143+ onClick = { handleCopy }
144+ className = "bg-background border border-border rounded-lg px-3 py-2 shadow-lg flex items-center gap-2 animate-in fade-in-0 slide-in-from-bottom-2 duration-200 hover:bg-muted transition-colors cursor-pointer"
145+ aria-label = "Copy link to section"
146+ >
147+ < div className = "flex items-center justify-center p-1.5 rounded-md bg-muted/50 border border-border/50" >
148+ { copied ? (
149+ < Check className = "h-3 w-3 text-green-500" />
150+ ) : (
151+ < Link className = "h-3 w-3 text-muted-foreground" />
152+ ) }
153+ </ div >
154+ < span className = "text-sm font-medium" >
155+ { copied ? 'Copied!' : 'Copy link' }
156+ </ span >
157+ </ button >
158+ </ div >
144159 ) }
145- </ button >
146- </ HeadingComponent >
160+ </ HeadingComponent >
161+
162+ { /* Desktop copy button right next to heading */ }
163+ { ! isMobile && (
164+ < button
165+ onClick = { handleCopy }
166+ className = "flex-shrink-0 ml-2 opacity-0 group-hover:opacity-100 transition-all duration-200 p-1.5 rounded-md bg-muted/50 hover:bg-muted border border-border/50 hover:border-border flex items-center justify-center shadow-sm hover:shadow-md"
167+ aria-label = "Copy link to section"
168+ >
169+ { copied ? (
170+ < Check className = "h-3 w-3 text-green-500" />
171+ ) : (
172+ < Link className = "h-3 w-3 text-muted-foreground hover:text-foreground" />
173+ ) }
174+ </ button >
175+ ) }
176+ </ div >
147177 </ div >
148178 )
149179 }
@@ -247,7 +277,7 @@ const components = {
247277 code : ( { className, ...props } : HTMLAttributes < HTMLElement > ) => (
248278 < code
249279 className = { cn (
250- 'relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm bg-muted' ,
280+ 'relative rounded px-[0.3rem] py-[0.2rem] mx-2 font-mono text-sm bg-muted' ,
251281 className
252282 ) }
253283 { ...props }
0 commit comments