Skip to content

Commit

Permalink
feat: Add support for copy button (#314)
Browse files Browse the repository at this point in the history
* Add support for copy button

* Minor fixes

* Clean up global styles

* Update CopyButton components

* Update rehypeCopyButton helper

* Update CopyButton entrypoint

* Minor changes

* Code

---------

Co-authored-by: Antoine BERNIER <antoine.bernier@gmail.com>
  • Loading branch information
dbritto-dev and abernier committed Aug 30, 2024
1 parent ba69c0b commit 87f08b7
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 8 deletions.
8 changes: 0 additions & 8 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ body {

code[class*='language-'],
pre[class*='language-'] {
background: none;
text-align: left;
white-space: pre;
word-spacing: normal;
Expand All @@ -70,13 +69,6 @@ pre[class*='language-'] {
--falsy: #f087bd;
--linenumber-border-width: theme('space.1');
--pad: theme('space.6');

@apply my-5 overflow-auto rounded-lg p-[--pad] font-mono text-sm;
}

:not(pre) > code[class*='language-'],
pre[class*='language-'] {
@apply bg-inverse-surface-light;
}

/* Inline code */
Expand Down
59 changes: 59 additions & 0 deletions src/components/mdx/Code/Code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use client'

import cn from '@/lib/cn'
import { ComponentProps, ReactNode, useEffect, useState } from 'react'
import { TbClipboard, TbClipboardCheck } from 'react-icons/tb'

export const Code = ({ children, className, ...props }: ComponentProps<'pre'>) => {
const [copied, setCopied] = useState(false)

const handleClick = async () => {
const textToCopy = extractTextFromChildren(children)

await navigator.clipboard.writeText(textToCopy)
setCopied(true)
}

useEffect(() => {
if (!copied) return
const int = setTimeout(() => setCopied(false), 2000)
return () => clearTimeout(int)
}, [copied])

return (
<div className={cn('relative')}>
<pre
{...props}
className={cn(
className,
'bg-inverse-surface-light my-5 overflow-auto rounded-lg p-[--pad] font-mono text-sm',
)}
>
{children}
</pre>
<button
className="absolute right-0 top-0 m-4 flex size-8 items-center justify-center rounded-md text-outline-variant transition-colors hover:text-outline"
onClick={handleClick}
>
{copied ? <TbClipboardCheck className="size-6" /> : <TbClipboard className="size-6" />}
</button>
</div>
)
}

// Recursive function to extract text content from React nodes
const extractTextFromChildren = (children: ReactNode): string => {
if (typeof children === 'string') {
return children
}

if (Array.isArray(children)) {
return children.map(extractTextFromChildren).join('')
}

if (typeof children === 'object' && children !== null && 'props' in children) {
return extractTextFromChildren(children.props.children)
}

return ''
}
1 change: 1 addition & 0 deletions src/components/mdx/Code/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Code'
19 changes: 19 additions & 0 deletions src/components/mdx/Code/rehypeCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Root } from 'hast'
import { visit } from 'unist-util-visit'

export function rehypeCode() {
return () => (tree: Root) => {
visit(tree, null, function (node) {
// console.log('node', node)

const isMDPre =
'tagName' in node &&
node.tagName === 'pre' &&
node.properties?.className?.toString()?.includes('language-')

if (isMDPre) {
node.tagName = 'Code' // map to <Code> React component
}
})
}
}
1 change: 1 addition & 0 deletions src/components/mdx/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './Code'
export * from './Codesandbox'
export * from './Details'
export * from './Gha'
Expand Down
2 changes: 2 additions & 0 deletions src/utils/docs.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Doc, DocToC } from '@/app/[...slug]/DocsContext'
import * as components from '@/components/mdx'
import { rehypeCode } from '@/components/mdx/Code/rehypeCode'
import { Codesandbox } from '@/components/mdx/Codesandbox'
import { fetchCSB } from '@/components/mdx/Codesandbox/fetchCSB'
import { rehypeCodesandbox } from '@/components/mdx/Codesandbox/rehypeCodesandbox'
Expand Down Expand Up @@ -156,6 +157,7 @@ async function _getDocs(
rehypeSummary,
rehypeGha,
rehypePrismPlus,
rehypeCode(),
rehypeCodesandbox(boxes), // 1. put all Codesandbox[id] into `doc.boxes`
rehypeToc(tableOfContents, url, title), // 2. will populate `doc.tableOfContents`
],
Expand Down

0 comments on commit 87f08b7

Please sign in to comment.