diff --git a/app/components/Readme.vue b/app/components/Readme.vue index c05e7aa14..ee673e1e9 100644 --- a/app/components/Readme.vue +++ b/app/components/Readme.vue @@ -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) +} diff --git a/server/api/registry/readme/[...pkg].get.ts b/server/api/registry/readme/[...pkg].get.ts index b892b93ad..c523bbd01 100644 --- a/server/api/registry/readme/[...pkg].get.ts +++ b/server/api/registry/readme/[...pkg].get.ts @@ -126,7 +126,7 @@ export default defineCachedEventHandler( swr: true, getKey: event => { const pkg = getRouterParam(event, 'pkg') ?? '' - return `readme:v5:${pkg.replace(/\/+$/, '').trim()}` + return `readme:v6:${pkg.replace(/\/+$/, '').trim()}` }, }, ) diff --git a/server/utils/readme.ts b/server/utils/readme.ts index 88110c2fe..a581c497b 100644 --- a/server/utils/readme.ts +++ b/server/utils/readme.ts @@ -135,12 +135,14 @@ const ALLOWED_TAGS = [ 'sub', 'kbd', 'mark', + 'button', ] const ALLOWED_ATTR: Record = { 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 + return `
+ +${html} +
` } // Resolve image URLs (with GitHub blob → raw conversion)