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
132 changes: 131 additions & 1 deletion app/components/Readme.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +8 to +21
Copy link
Collaborator

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.


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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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>
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also be visible when focused 🙏

Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
Expand Down Expand Up @@ -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>
2 changes: 1 addition & 1 deletion server/api/registry/readme/[...pkg].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`
},
},
)
Expand Down
12 changes: 11 additions & 1 deletion server/utils/readme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Add copy button
// Add copy button
// NOTE: `app/components/Readme.vue` makes some assumptions about the exact DOM structured injected here.
// If you make changes here, you may need to update there as well.

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)
Expand Down
Loading