Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/components/CodeBlock/CodeBlock.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React, { useState, useRef, useEffect } from 'react';

Check failure on line 1 in src/components/CodeBlock/CodeBlock.jsx

View workflow job for this annotation

GitHub Actions / Lint (ubuntu-latest, lts/*)

'React' is defined but never used
import PropTypes from 'prop-types';
import './CodeBlock.scss';

const CodeBlock = ({ children, ...props }) => {
const [copied, setCopied] = useState(false);
const preRef = useRef(null);
const timeoutRef = useRef(null);

// Extract the code content from the children
const getCodeContent = () => {
if (!preRef.current) return '';

const codeElement = preRef.current.querySelector('code');
if (codeElement) {
return codeElement.textContent || codeElement.innerText || '';
}

// Fallback: get text from pre element
return preRef.current.textContent || preRef.current.innerText || '';
};

const handleCopy = async () => {
const codeContent = getCodeContent();

if (!codeContent) return;

try {
await navigator.clipboard.writeText(codeContent);
setCopied(true);

// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}

// Reset the copied state after 2 seconds
timeoutRef.current = setTimeout(() => {
setCopied(false);
}, 2000);
} catch (err) {
// Fallback for browsers that don't support clipboard API
console.error('Failed to copy code:', err);

// Try the fallback method
const textArea = document.createElement('textarea');
textArea.value = codeContent;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();

try {
document.execCommand('copy');
setCopied(true);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setCopied(false);
}, 2000);
} catch (fallbackErr) {
console.error('Fallback copy failed:', fallbackErr);
}

document.body.removeChild(textArea);
}
};

// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);

return (
<div className="code-block-wrapper">
<pre ref={preRef} {...props}>
{children}
</pre>
<button
type="button"
className="code-block-copy-button"
onClick={handleCopy}
aria-label={copied ? 'Copied!' : 'Copy code'}
title={copied ? 'Copied!' : 'Copy code'}
>
<svg
className="code-block-copy-icon"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{copied ? (
// Checkmark icon
<path
d="M13.5 4.5L6 12L2.5 8.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
) : (
// Copy icon - two overlapping squares
<>
<rect
x="4"
y="4"
width="8"
height="8"
rx="1"
stroke="currentColor"
strokeWidth="1.5"
fill="none"
/>
<rect
x="6"
y="6"
width="8"
height="8"
rx="1"
stroke="currentColor"
strokeWidth="1.5"
fill="none"
/>
</>
)}
</svg>
<span className="code-block-copy-text">
{copied ? 'Copied!' : 'Copy'}
</span>
</button>
</div>
);
};

CodeBlock.propTypes = {
children: PropTypes.node.isRequired,
};

export default CodeBlock;

98 changes: 98 additions & 0 deletions src/components/CodeBlock/CodeBlock.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
@import 'vars';
@import 'functions';

.code-block-wrapper {
position: relative;
margin: 1em 0; // Match the margin from .markdown pre

// Ensure pre has relative positioning for absolute button
pre {
position: relative;
margin: 0; // Remove margin from pre since wrapper has it
}
}

.code-block-copy-button {
position: absolute;
top: 8px;
right: 8px;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background-color: transparentize(getColor(elephant), 0.2);
border: 1px solid transparentize(getColor(white), 0.9);
border-radius: 4px;
color: getColor(malibu);
font-size: 12px;
font-family: $font-stack-body;
cursor: pointer;
transition: all 200ms ease;
z-index: 10;

// Subtle by default, more visible on hover
opacity: 0.7;

.code-block-wrapper:hover & {
opacity: 1;
background-color: transparentize(getColor(elephant), 0.1);
border-color: transparentize(getColor(white), 0.8);
}

&:hover {
background-color: transparentize(getColor(elephant), 0.05);
border-color: transparentize(getColor(white), 0.75);
color: lighten(getColor(malibu), 10%);
opacity: 1;
}

&:active {
transform: scale(0.98);
}

&:focus {
outline: 2px solid getColor(malibu);
outline-offset: 2px;
opacity: 1;
}
}

.code-block-copy-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}

.code-block-copy-text {
white-space: nowrap;
font-weight: 500;
}

// Dark theme support
[data-theme='dark'] {
.code-block-copy-button {
background-color: transparentize(#131b1f, 0.3);
border-color: transparentize(getColor(white), 0.95);
color: #69a8ee;
opacity: 0.7;

.code-block-wrapper:hover & {
opacity: 1;
background-color: transparentize(#131b1f, 0.15);
border-color: transparentize(getColor(white), 0.9);
}

&:hover {
background-color: transparentize(#131b1f, 0.05);
border-color: transparentize(getColor(white), 0.8);
color: #82b7f6;
opacity: 1;
}

&:focus {
outline-color: #69a8ee;
opacity: 1;
}
}
}

2 changes: 2 additions & 0 deletions src/mdx-components.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import Badge from './components/Badge/Badge';
import LinkComponent from './components/mdxComponents/Link';
import StackBlitzPreview from './components/StackBlitzPreview/StackBlitzPreview';
import CodeBlock from './components/CodeBlock/CodeBlock';

/** @returns {import('mdx/types.js').MDXComponents} */
export function useMDXComponents() {
return {
a: LinkComponent,
Badge: Badge,
StackBlitzPreview: StackBlitzPreview,
pre: CodeBlock,
};
}
Loading