-
-
Notifications
You must be signed in to change notification settings - Fork 144
feat: add copy button functionality to code blocks in README #636
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
base: main
Are you sure you want to change the base?
Changes from all commits
fdabb6a
270f799
683ea2b
481cceb
6880564
71f6b69
e3bc3d0
94d0ad6
4f5177d
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 |
|---|---|---|
|
|
@@ -2,10 +2,43 @@ | |
| defineProps<{ | ||
| html: string | ||
| }>() | ||
|
|
||
| const { copy } = useClipboard() | ||
|
|
||
| const handleCopy = async (e: MouseEvent) => { | ||
| const target = (e.target as HTMLElement).closest('[data-copy]') | ||
| if (!target) return | ||
|
|
||
| const wrapper = target.closest('.readme-code-block') | ||
| if (!wrapper) return | ||
|
|
||
| const pre = wrapper.querySelector('pre') | ||
| if (!pre?.textContent) return | ||
|
|
||
| await copy(pre.textContent) | ||
|
|
||
| const icon = target.querySelector('span') | ||
| if (!icon) return | ||
|
|
||
| const originalIcon = 'i-carbon:copy' | ||
| const successIcon = 'i-carbon:checkmark' | ||
|
|
||
| icon.classList.remove(originalIcon) | ||
| icon.classList.add(successIcon) | ||
|
|
||
| setTimeout(() => { | ||
| icon.classList.remove(successIcon) | ||
| icon.classList.add(originalIcon) | ||
| }, 2000) | ||
|
Comment on lines
+23
to
+32
Collaborator
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. I wonder if there's any way we could accomplish this with native CSS transitions? |
||
| } | ||
| </script> | ||
|
|
||
| <template> | ||
| <article class="readme prose prose-invert max-w-[70ch] lg:max-w-none" v-html="html" /> | ||
| <article | ||
| class="readme prose prose-invert max-w-[70ch] lg:max-w-none" | ||
| v-html="html" | ||
| @click="handleCopy" | ||
| /> | ||
| </template> | ||
|
|
||
| <style scoped> | ||
|
|
@@ -99,6 +132,90 @@ defineProps<{ | |
| box-sizing: border-box; | ||
| } | ||
|
|
||
| .readme :deep(.readme-code-block) { | ||
| display: block; | ||
| width: 100%; | ||
| position: relative; | ||
| } | ||
|
|
||
| .readme :deep(.readme-copy-button) { | ||
| position: absolute; | ||
| top: 0.4rem; | ||
| inset-inline-end: 0.4rem; | ||
| display: inline-flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding: 0.25rem; | ||
| border-radius: 6px; | ||
| background: color-mix(in srgb, var(--bg-subtle) 80%, transparent); | ||
| border: 1px solid var(--border); | ||
| color: var(--fg-subtle); | ||
| opacity: 0; | ||
| transition: | ||
| opacity 0.2s ease, | ||
| color 0.2s ease, | ||
| border-color 0.2s ease; | ||
| } | ||
|
|
||
| .readme :deep(.readme-code-block:hover .readme-copy-button), | ||
| .readme :deep(.readme-copy-button:focus-visible) { | ||
| opacity: 1; | ||
| } | ||
|
|
||
| .readme :deep(.readme-copy-button:hover) { | ||
|
Member
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. this should also be visible when focused 🙏
Collaborator
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. 👍🏼 works now |
||
| color: var(--fg); | ||
| border-color: var(--border-hover); | ||
| } | ||
|
|
||
| .readme :deep(.readme-copy-button > span) { | ||
| width: 1rem; | ||
| height: 1rem; | ||
| display: inline-block; | ||
| pointer-events: none; | ||
| } | ||
|
|
||
| .readme :deep(.readme-code-block) { | ||
| display: block; | ||
| width: 100%; | ||
| position: relative; | ||
| } | ||
|
|
||
| .readme :deep(.readme-copy-button) { | ||
| position: absolute; | ||
| top: 0.4rem; | ||
| inset-inline-end: 0.4rem; | ||
| display: inline-flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding: 0.25rem; | ||
| border-radius: 6px; | ||
| background: color-mix(in srgb, var(--bg-subtle) 80%, transparent); | ||
| border: 1px solid var(--border); | ||
| color: var(--fg-subtle); | ||
| opacity: 0; | ||
| transition: | ||
| opacity 0.2s ease, | ||
| color 0.2s ease, | ||
| border-color 0.2s ease; | ||
| } | ||
|
|
||
| .readme :deep(.readme-code-block:hover .readme-copy-button), | ||
| .readme :deep(.readme-copy-button:focus-visible) { | ||
| opacity: 1; | ||
| } | ||
|
|
||
| .readme :deep(.readme-copy-button:hover) { | ||
| color: var(--fg); | ||
| border-color: var(--border-hover); | ||
| } | ||
|
|
||
| .readme :deep(.readme-copy-button > span) { | ||
| width: 1.05rem; | ||
| height: 1.05rem; | ||
| display: inline-block; | ||
| pointer-events: none; | ||
| } | ||
|
|
||
| .readme :deep(pre code), | ||
| .readme :deep(.shiki code) { | ||
| background: transparent !important; | ||
|
|
@@ -308,4 +425,17 @@ defineProps<{ | |
| margin: 0 0.25rem 0.25rem 0; | ||
| border-radius: 4px; | ||
| } | ||
|
|
||
| /* Screen reader only text */ | ||
| .readme :deep(.sr-only) { | ||
| position: absolute; | ||
| width: 1px; | ||
| height: 1px; | ||
| padding: 0; | ||
| margin: -1px; | ||
| overflow: hidden; | ||
| clip: rect(0, 0, 0, 0); | ||
| white-space: nowrap; | ||
| border-width: 0; | ||
| } | ||
| </style> | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -135,12 +135,14 @@ const ALLOWED_TAGS = [ | |||||||||
| 'sub', | ||||||||||
| 'kbd', | ||||||||||
| 'mark', | ||||||||||
| 'button', | ||||||||||
| ] | ||||||||||
|
|
||||||||||
| const ALLOWED_ATTR: Record<string, string[]> = { | ||||||||||
| a: ['href', 'title', 'target', 'rel'], | ||||||||||
| img: ['src', 'alt', 'title', 'width', 'height'], | ||||||||||
| source: ['src', 'srcset', 'type', 'media'], | ||||||||||
| button: ['class', 'title', 'type', 'aria-label', 'data-copy'], | ||||||||||
| th: ['colspan', 'rowspan', 'align'], | ||||||||||
| td: ['colspan', 'rowspan', 'align'], | ||||||||||
| h3: ['id', 'data-level', 'align'], | ||||||||||
|
|
@@ -306,7 +308,15 @@ export async function renderReadmeHtml( | |||||||||
|
|
||||||||||
| // Syntax highlighting for code blocks (uses shared highlighter) | ||||||||||
| renderer.code = ({ text, lang }: Tokens.Code) => { | ||||||||||
| return highlightCodeSync(shiki, text, lang || 'text') | ||||||||||
| const html = highlightCodeSync(shiki, text, lang || 'text') | ||||||||||
| // Add copy button | ||||||||||
|
Collaborator
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.
Suggested change
|
||||||||||
| return `<div class="readme-code-block" > | ||||||||||
| <button type="button" class="readme-copy-button" aria-label="Copy code" check-icon="i-carbon:checkmark" copy-icon="i-carbon:copy" data-copy> | ||||||||||
| <span class="i-carbon:copy" aria-hidden="true"></span> | ||||||||||
| <span class="sr-only">Copy code</span> | ||||||||||
| </button> | ||||||||||
| ${html} | ||||||||||
| </div>` | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Resolve image URLs (with GitHub blob → raw conversion) | ||||||||||
|
|
||||||||||
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.
Since this is making leaky assumptions about the DOM structure here, this feature seems like a prime candidate for some test coverage.
🙏🏼 Would you be up for opening a follow-up PR with at least a happy path test? Otherwise we can open an issue to track it.