Skip to content

Commit e8ee375

Browse files
committed
feat: extracted copy to clipboard button
1 parent 6b72a71 commit e8ee375

File tree

3 files changed

+112
-131
lines changed

3 files changed

+112
-131
lines changed
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<script setup lang="ts">
2+
defineOptions({
3+
inheritAttrs: false,
4+
})
5+
6+
const props = defineProps<{
7+
copied: boolean
8+
copyText?: string
9+
copiedText?: string
10+
ariaLabelCopy?: string
11+
ariaLabelCopied?: string
12+
}>()
13+
14+
const buttonCopyText = computed(() => props.copyText || $t('common.copy'))
15+
const buttonCopiedText = computed(() => props.copiedText || $t('common.copied'))
16+
const buttonAriaLabelCopy = computed(() => props.ariaLabelCopy || $t('common.copy'))
17+
const buttonAriaLabelCopied = computed(() => props.ariaLabelCopied || $t('common.copied'))
18+
19+
const emit = defineEmits<{
20+
click: []
21+
}>()
22+
23+
function handleClick() {
24+
emit('click')
25+
}
26+
</script>
27+
28+
<template>
29+
<div class="group relative" v-bind="$attrs">
30+
<slot />
31+
<button
32+
type="button"
33+
@click="handleClick"
34+
class="absolute z-20 inset-is-0 top-full inline-flex items-center gap-1 px-2 py-1 rounded border text-xs font-mono whitespace-nowrap transition-all duration-150 opacity-0 -translate-y-1 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto focus-visible:opacity-100 focus-visible:translate-y-0 focus-visible:pointer-events-auto"
35+
:class="[
36+
$style.copyButton,
37+
copied ? 'text-accent bg-accent/10' : 'text-fg-muted bg-bg border-border',
38+
]"
39+
:aria-label="copied ? buttonAriaLabelCopied : buttonAriaLabelCopy"
40+
>
41+
<span
42+
:class="copied ? 'i-lucide:check' : 'i-lucide:copy'"
43+
class="w-3.5 h-3.5"
44+
aria-hidden="true"
45+
/>
46+
{{ copied ? buttonCopiedText : buttonCopyText }}
47+
</button>
48+
</div>
49+
</template>
50+
51+
<style module>
52+
.copyButton {
53+
clip: rect(0 0 0 0);
54+
clip-path: inset(50%);
55+
height: 1px;
56+
overflow: hidden;
57+
width: 1px;
58+
transition:
59+
opacity 0.25s 0.1s,
60+
translate 0.15s 0.1s,
61+
clip 0.01s 0.34s allow-discrete,
62+
clip-path 0.01s 0.34s allow-discrete,
63+
height 0.01s 0.34s allow-discrete,
64+
width 0.01s 0.34s allow-discrete;
65+
}
66+
67+
:global(.group):hover .copyButton,
68+
.copyButton:focus-visible {
69+
clip: auto;
70+
clip-path: none;
71+
height: auto;
72+
overflow: visible;
73+
width: auto;
74+
transition:
75+
opacity 0.15s,
76+
translate 0.15s;
77+
}
78+
79+
@media (hover: none) {
80+
.copyButton {
81+
display: none;
82+
}
83+
}
84+
</style>

app/pages/compare.vue

Lines changed: 15 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -246,30 +246,25 @@ useSeoMeta({
246246

247247
<!-- Comparison grid -->
248248
<section v-if="canCompare" class="mt-10" aria-labelledby="comparison-heading">
249-
<div class="relative group mb-4 inline-block">
249+
<CopyToClipboardButton
250+
v-if="packagesData && packagesData.some(p => p !== null)"
251+
:copied="copied"
252+
:copy-text="$t('compare.packages.copy_as_markdown')"
253+
class="mb-4 inline-block hidden md:inline-flex"
254+
@click="copyComparisonGridAsMd"
255+
>
250256
<h2 id="comparison-heading" class="text-xs text-fg-subtle uppercase tracking-wider">
251257
{{ $t('compare.packages.section_comparison') }}
252258
</h2>
259+
</CopyToClipboardButton>
253260

254-
<button
255-
v-if="packagesData && packagesData.some(p => p !== null)"
256-
type="button"
257-
class="absolute z-20 inset-is-0 top-full hidden md:inline-flex items-center gap-1 px-2 py-1 rounded border text-xs font-mono whitespace-nowrap transition-all duration-150 opacity-0 -translate-y-1 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto focus-visible:opacity-100 focus-visible:translate-y-0 focus-visible:pointer-events-auto"
258-
:class="[
259-
$style.copyButton,
260-
copied ? 'text-accent bg-accent/10' : 'text-fg-muted bg-bg border-border',
261-
]"
262-
:aria-label="copied ? $t('common.copied') : $t('compare.packages.copy_as_markdown')"
263-
@click="copyComparisonGridAsMd"
264-
>
265-
<span
266-
:class="copied ? 'i-lucide:check' : 'i-lucide:copy'"
267-
class="w-3.5 h-3.5"
268-
aria-hidden="true"
269-
/>
270-
{{ copied ? $t('common.copied') : $t('compare.packages.copy_as_markdown') }}
271-
</button>
272-
</div>
261+
<h2
262+
v-else
263+
id="comparison-heading"
264+
class="text-xs text-fg-subtle uppercase tracking-wider mb-4"
265+
>
266+
{{ $t('compare.packages.section_comparison') }}
267+
</h2>
273268

274269
<div
275270
v-if="
@@ -351,38 +346,3 @@ useSeoMeta({
351346
</div>
352347
</main>
353348
</template>
354-
355-
<style module>
356-
.copyButton {
357-
clip: rect(0 0 0 0);
358-
clip-path: inset(50%);
359-
height: 1px;
360-
overflow: hidden;
361-
width: 1px;
362-
transition:
363-
opacity 0.25s 0.1s,
364-
translate 0.15s 0.1s,
365-
clip 0.01s 0.34s allow-discrete,
366-
clip-path 0.01s 0.34s allow-discrete,
367-
height 0.01s 0.34s allow-discrete,
368-
width 0.01s 0.34s allow-discrete;
369-
}
370-
371-
:global(.group):hover .copyButton,
372-
.copyButton:focus-visible {
373-
clip: auto;
374-
clip-path: none;
375-
height: auto;
376-
overflow: visible;
377-
width: auto;
378-
transition:
379-
opacity 0.15s,
380-
translate 0.15s;
381-
}
382-
383-
@media (hover: none) {
384-
.copyButton {
385-
display: none;
386-
}
387-
}
388-
</style>

app/pages/package/[[org]]/[name].vue

Lines changed: 13 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,12 @@ const showSkeleton = shallowRef(false)
669669
>
670670
<!-- Package name and version -->
671671
<div class="flex items-baseline gap-x-2 gap-y-1 sm:gap-x-3 flex-wrap min-w-0">
672-
<div class="group relative flex flex-col items-start min-w-0">
672+
<CopyToClipboardButton
673+
:copied="copiedPkgName"
674+
:copy-text="$t('package.copy_name')"
675+
class="flex flex-col items-start min-w-0"
676+
@click="copyPkgName()"
677+
>
673678
<h1
674679
class="font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words"
675680
:title="pkg.name"
@@ -683,30 +688,14 @@ const showSkeleton = shallowRef(false)
683688
{{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
684689
</span>
685690
</h1>
691+
</CopyToClipboardButton>
686692

687-
<!-- Floating copy name button -->
688-
<button
689-
type="button"
690-
@click="copyPkgName()"
691-
class="absolute z-20 inset-is-0 top-full inline-flex items-center gap-1 px-2 py-1 rounded border text-xs font-mono whitespace-nowrap transition-all duration-150 opacity-0 -translate-y-1 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto focus-visible:opacity-100 focus-visible:translate-y-0 focus-visible:pointer-events-auto"
692-
:class="[
693-
$style.copyButton,
694-
copiedPkgName ? 'text-accent bg-accent/10' : 'text-fg-muted bg-bg border-border',
695-
]"
696-
:aria-label="copiedPkgName ? $t('common.copied') : $t('package.copy_name')"
697-
>
698-
<span
699-
:class="copiedPkgName ? 'i-lucide:check' : 'i-lucide:copy'"
700-
class="w-3.5 h-3.5"
701-
aria-hidden="true"
702-
/>
703-
{{ copiedPkgName ? $t('common.copied') : $t('package.copy_name') }}
704-
</button>
705-
</div>
706-
707-
<span
693+
<CopyToClipboardButton
708694
v-if="resolvedVersion"
709-
class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0 relative group"
695+
:copied="copiedVersion"
696+
:copy-text="$t('package.copy_version')"
697+
class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
698+
@click="copyVersion()"
710699
>
711700
<!-- Version resolution indicator (e.g., "latest → 4.2.0") -->
712701
<template v-if="requestedVersion && resolvedVersion !== requestedVersion">
@@ -749,26 +738,7 @@ const showSkeleton = shallowRef(false)
749738
class="text-fg-subtle text-sm shrink-0"
750739
>{{ $t('package.not_latest') }}</span
751740
>
752-
753-
<!-- Floating copy version button -->
754-
<button
755-
type="button"
756-
@click="copyVersion()"
757-
class="absolute z-20 inset-is-0 top-full inline-flex items-center gap-1 px-2 py-1 rounded border text-xs font-mono whitespace-nowrap transition-all duration-150 opacity-0 -translate-y-1 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto focus-visible:opacity-100 focus-visible:translate-y-0 focus-visible:pointer-events-auto"
758-
:class="[
759-
$style.copyButton,
760-
copiedVersion ? 'text-accent bg-accent/10' : 'text-fg-muted bg-bg border-border',
761-
]"
762-
:aria-label="copiedVersion ? $t('common.copied') : $t('package.copy_version')"
763-
>
764-
<span
765-
:class="copiedVersion ? 'i-lucide:check' : 'i-lucide:copy'"
766-
class="w-3.5 h-3.5"
767-
aria-hidden="true"
768-
/>
769-
{{ copiedVersion ? $t('common.copied') : $t('package.copy_version') }}
770-
</button>
771-
</span>
741+
</CopyToClipboardButton>
772742

773743
<!-- Docs + Code + Compare — inline on desktop, floating bottom bar on mobile -->
774744
<ButtonGroup
@@ -1541,39 +1511,6 @@ const showSkeleton = shallowRef(false)
15411511
grid-area: sidebar;
15421512
}
15431513
1544-
.copyButton {
1545-
clip: rect(0 0 0 0);
1546-
clip-path: inset(50%);
1547-
height: 1px;
1548-
overflow: hidden;
1549-
width: 1px;
1550-
transition:
1551-
opacity 0.25s 0.1s,
1552-
translate 0.15s 0.1s,
1553-
clip 0.01s 0.34s allow-discrete,
1554-
clip-path 0.01s 0.34s allow-discrete,
1555-
height 0.01s 0.34s allow-discrete,
1556-
width 0.01s 0.34s allow-discrete;
1557-
}
1558-
1559-
:global(.group):hover .copyButton,
1560-
.copyButton:focus-visible {
1561-
clip: auto;
1562-
clip-path: none;
1563-
height: auto;
1564-
overflow: visible;
1565-
width: auto;
1566-
transition:
1567-
opacity 0.15s,
1568-
translate 0.15s;
1569-
}
1570-
1571-
@media (hover: none) {
1572-
.copyButton {
1573-
display: none;
1574-
}
1575-
}
1576-
15771514
/* Mobile floating nav: safe-area positioning + kbd hiding */
15781515
@media (max-width: 639.9px) {
15791516
.packageNav {

0 commit comments

Comments
 (0)