[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add copy button functionality to code blocks in README (#636)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Naveed Azhar
autofix-ci[bot]
and committed by
GitHub
d7145915 d3b742ae

+143 -3
+131 -1
app/components/Readme.vue
··· 2 2 defineProps<{ 3 3 html: string 4 4 }>() 5 + 6 + const { copy } = useClipboard() 7 + 8 + const handleCopy = async (e: MouseEvent) => { 9 + const target = (e.target as HTMLElement).closest('[data-copy]') 10 + if (!target) return 11 + 12 + const wrapper = target.closest('.readme-code-block') 13 + if (!wrapper) return 14 + 15 + const pre = wrapper.querySelector('pre') 16 + if (!pre?.textContent) return 17 + 18 + await copy(pre.textContent) 19 + 20 + const icon = target.querySelector('span') 21 + if (!icon) return 22 + 23 + const originalIcon = 'i-carbon:copy' 24 + const successIcon = 'i-carbon:checkmark' 25 + 26 + icon.classList.remove(originalIcon) 27 + icon.classList.add(successIcon) 28 + 29 + setTimeout(() => { 30 + icon.classList.remove(successIcon) 31 + icon.classList.add(originalIcon) 32 + }, 2000) 33 + } 5 34 </script> 6 35 7 36 <template> 8 - <article class="readme prose prose-invert max-w-[70ch] lg:max-w-none" v-html="html" /> 37 + <article 38 + class="readme prose prose-invert max-w-[70ch] lg:max-w-none" 39 + v-html="html" 40 + @click="handleCopy" 41 + /> 9 42 </template> 10 43 11 44 <style scoped> ··· 99 132 box-sizing: border-box; 100 133 } 101 134 135 + .readme :deep(.readme-code-block) { 136 + display: block; 137 + width: 100%; 138 + position: relative; 139 + } 140 + 141 + .readme :deep(.readme-copy-button) { 142 + position: absolute; 143 + top: 0.4rem; 144 + inset-inline-end: 0.4rem; 145 + display: inline-flex; 146 + align-items: center; 147 + justify-content: center; 148 + padding: 0.25rem; 149 + border-radius: 6px; 150 + background: color-mix(in srgb, var(--bg-subtle) 80%, transparent); 151 + border: 1px solid var(--border); 152 + color: var(--fg-subtle); 153 + opacity: 0; 154 + transition: 155 + opacity 0.2s ease, 156 + color 0.2s ease, 157 + border-color 0.2s ease; 158 + } 159 + 160 + .readme :deep(.readme-code-block:hover .readme-copy-button), 161 + .readme :deep(.readme-copy-button:focus-visible) { 162 + opacity: 1; 163 + } 164 + 165 + .readme :deep(.readme-copy-button:hover) { 166 + color: var(--fg); 167 + border-color: var(--border-hover); 168 + } 169 + 170 + .readme :deep(.readme-copy-button > span) { 171 + width: 1rem; 172 + height: 1rem; 173 + display: inline-block; 174 + pointer-events: none; 175 + } 176 + 177 + .readme :deep(.readme-code-block) { 178 + display: block; 179 + width: 100%; 180 + position: relative; 181 + } 182 + 183 + .readme :deep(.readme-copy-button) { 184 + position: absolute; 185 + top: 0.4rem; 186 + inset-inline-end: 0.4rem; 187 + display: inline-flex; 188 + align-items: center; 189 + justify-content: center; 190 + padding: 0.25rem; 191 + border-radius: 6px; 192 + background: color-mix(in srgb, var(--bg-subtle) 80%, transparent); 193 + border: 1px solid var(--border); 194 + color: var(--fg-subtle); 195 + opacity: 0; 196 + transition: 197 + opacity 0.2s ease, 198 + color 0.2s ease, 199 + border-color 0.2s ease; 200 + } 201 + 202 + .readme :deep(.readme-code-block:hover .readme-copy-button), 203 + .readme :deep(.readme-copy-button:focus-visible) { 204 + opacity: 1; 205 + } 206 + 207 + .readme :deep(.readme-copy-button:hover) { 208 + color: var(--fg); 209 + border-color: var(--border-hover); 210 + } 211 + 212 + .readme :deep(.readme-copy-button > span) { 213 + width: 1.05rem; 214 + height: 1.05rem; 215 + display: inline-block; 216 + pointer-events: none; 217 + } 218 + 102 219 .readme :deep(pre code), 103 220 .readme :deep(.shiki code) { 104 221 background: transparent !important; ··· 307 424 display: inline-block; 308 425 margin: 0 0.25rem 0.25rem 0; 309 426 border-radius: 4px; 427 + } 428 + 429 + /* Screen reader only text */ 430 + .readme :deep(.sr-only) { 431 + position: absolute; 432 + width: 1px; 433 + height: 1px; 434 + padding: 0; 435 + margin: -1px; 436 + overflow: hidden; 437 + clip: rect(0, 0, 0, 0); 438 + white-space: nowrap; 439 + border-width: 0; 310 440 } 311 441 </style>
+1 -1
server/api/registry/readme/[...pkg].get.ts
··· 126 126 swr: true, 127 127 getKey: event => { 128 128 const pkg = getRouterParam(event, 'pkg') ?? '' 129 - return `readme:v5:${pkg.replace(/\/+$/, '').trim()}` 129 + return `readme:v6:${pkg.replace(/\/+$/, '').trim()}` 130 130 }, 131 131 }, 132 132 )
+11 -1
server/utils/readme.ts
··· 135 135 'sub', 136 136 'kbd', 137 137 'mark', 138 + 'button', 138 139 ] 139 140 140 141 const ALLOWED_ATTR: Record<string, string[]> = { 141 142 a: ['href', 'title', 'target', 'rel'], 142 143 img: ['src', 'alt', 'title', 'width', 'height', 'align'], 143 144 source: ['src', 'srcset', 'type', 'media'], 145 + button: ['class', 'title', 'type', 'aria-label', 'data-copy'], 144 146 th: ['colspan', 'rowspan', 'align'], 145 147 td: ['colspan', 'rowspan', 'align'], 146 148 h3: ['id', 'data-level', 'align'], ··· 306 308 307 309 // Syntax highlighting for code blocks (uses shared highlighter) 308 310 renderer.code = ({ text, lang }: Tokens.Code) => { 309 - return highlightCodeSync(shiki, text, lang || 'text') 311 + const html = highlightCodeSync(shiki, text, lang || 'text') 312 + // Add copy button 313 + return `<div class="readme-code-block" > 314 + <button type="button" class="readme-copy-button" aria-label="Copy code" check-icon="i-carbon:checkmark" copy-icon="i-carbon:copy" data-copy> 315 + <span class="i-carbon:copy" aria-hidden="true"></span> 316 + <span class="sr-only">Copy code</span> 317 + </button> 318 + ${html} 319 + </div>` 310 320 } 311 321 312 322 // Resolve image URLs (with GitHub blob → raw conversion)