[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: copy package name button (#473)

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

authored by

WilcoSp
autofix-ci[bot]
Daniel Roe
and committed by
GitHub
d206a7a2 e2b44322

+102 -35
+16
app/components/AnnounceTooltip.vue
··· 1 + <script setup lang="ts"> 2 + const props = defineProps<{ 3 + /** Tooltip text */ 4 + text: string 5 + /** Position: 'top' | 'bottom' | 'left' | 'right' */ 6 + position?: 'top' | 'bottom' | 'left' | 'right' 7 + /** is tooltip visible */ 8 + isVisible: boolean 9 + }>() 10 + </script> 11 + 12 + <template> 13 + <BaseTooltip :text :isVisible :position :tooltip-attr="{ 'aria-live': 'polite' }" 14 + ><slot 15 + /></BaseTooltip> 16 + </template>
+8 -32
app/components/AppTooltip.vue
··· 9 9 const isVisible = shallowRef(false) 10 10 const tooltipId = useId() 11 11 12 - const positionClasses: Record<string, string> = { 13 - top: 'bottom-full inset-is-1/2 -translate-x-1/2 mb-1', 14 - bottom: 'top-full inset-is-0 mt-1', 15 - left: 'inset-ie-full top-1/2 -translate-y-1/2 me-2', 16 - right: 'inset-is-full top-1/2 -translate-y-1/2 ms-2', 17 - } 18 - 19 - const tooltipPosition = computed(() => positionClasses[props.position || 'bottom']) 20 - 21 12 function show() { 22 13 isVisible.value = true 23 14 } ··· 28 19 </script> 29 20 30 21 <template> 31 - <div 32 - class="relative inline-flex" 33 - :aria-describedby="isVisible ? tooltipId : undefined" 22 + <BaseTooltip 23 + :text 24 + :isVisible 25 + :position 26 + :tooltip-attr="{ role: 'tooltip', id: tooltipId }" 34 27 @mouseenter="show" 35 28 @mouseleave="hide" 36 29 @focusin="show" 37 30 @focusout="hide" 38 - > 39 - <slot /> 40 - 41 - <Transition 42 - enter-active-class="transition-opacity duration-150 motion-reduce:transition-none" 43 - leave-active-class="transition-opacity duration-100 motion-reduce:transition-none" 44 - enter-from-class="opacity-0" 45 - leave-to-class="opacity-0" 46 - > 47 - <div 48 - v-if="isVisible" 49 - :id="tooltipId" 50 - role="tooltip" 51 - class="absolute px-2 py-1 font-mono text-xs text-fg bg-bg-elevated border border-border rounded shadow-lg whitespace-nowrap z-[100] pointer-events-none" 52 - :class="tooltipPosition" 53 - > 54 - {{ text }} 55 - </div> 56 - </Transition> 57 - </div> 31 + :aria-describedby="isVisible ? tooltipId : undefined" 32 + ><slot 33 + /></BaseTooltip> 58 34 </template>
+45
app/components/BaseTooltip.vue
··· 1 + <script setup lang="ts"> 2 + import type { HTMLAttributes } from 'vue' 3 + 4 + const props = defineProps<{ 5 + /** Tooltip text */ 6 + text: string 7 + /** Position: 'top' | 'bottom' | 'left' | 'right' */ 8 + position?: 'top' | 'bottom' | 'left' | 'right' 9 + /** is tooltip visible */ 10 + isVisible: boolean 11 + /** attributes for tooltip element */ 12 + tooltipAttr?: HTMLAttributes 13 + }>() 14 + 15 + const positionClasses: Record<string, string> = { 16 + top: 'bottom-full inset-is-1/2 -translate-x-1/2 mb-1', 17 + bottom: 'top-full inset-is-0 mt-1', 18 + left: 'inset-ie-full top-1/2 -translate-y-1/2 me-2', 19 + right: 'inset-is-full top-1/2 -translate-y-1/2 ms-2', 20 + } 21 + 22 + const tooltipPosition = computed(() => positionClasses[props.position || 'bottom']) 23 + </script> 24 + 25 + <template> 26 + <div class="relative inline-flex"> 27 + <slot /> 28 + 29 + <Transition 30 + enter-active-class="transition-opacity duration-150 motion-reduce:transition-none" 31 + leave-active-class="transition-opacity duration-100 motion-reduce:transition-none" 32 + enter-from-class="opacity-0" 33 + leave-to-class="opacity-0" 34 + > 35 + <div 36 + v-if="props.isVisible" 37 + class="absolute px-2 py-1 font-mono text-xs text-fg bg-bg-elevated border border-border rounded shadow-lg whitespace-nowrap z-[100] pointer-events-none" 38 + :class="tooltipPosition" 39 + v-bind="tooltipAttr" 40 + > 41 + {{ text }} 42 + </div> 43 + </Transition> 44 + </div> 45 + </template>
+18 -2
app/pages/[...package].vue
··· 86 86 return pkg.value.versions[latestTag] ?? null 87 87 }) 88 88 89 + //copy package name 90 + const { copied: copiedPkgName, copy: copyPkgName } = useClipboard({ 91 + source: packageName, 92 + copiedDuring: 2000, 93 + }) 94 + 89 95 // Fetch dependency analysis (lazy, client-side) 90 96 // This is the same composable used by PackageVulnerabilityTree and PackageDeprecatedTree 91 97 const { ··· 388 394 :to="{ name: 'org', params: { org: orgName } }" 389 395 class="text-fg-muted hover:text-fg transition-colors duration-200" 390 396 >@{{ orgName }}</NuxtLink 391 - ><span v-if="orgName">/</span 392 - >{{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }} 397 + ><span v-if="orgName">/</span> 398 + <AnnounceTooltip :text="$t('common.copied')" :isVisible="copiedPkgName"> 399 + <button 400 + @click="copyPkgName()" 401 + aria-describedby="copy-pkg-name" 402 + class="cursor-copy ms-1 mt-1 active:scale-95 transition-transform" 403 + > 404 + {{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }} 405 + </button> 406 + </AnnounceTooltip> 393 407 </h1> 408 + 409 + <span id="copy-pkg-name" class="sr-only">{{ $t('package.copy_name') }}</span> 394 410 <span 395 411 v-if="displayVersion" 396 412 class="inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
+1 -1
app/pages/code/[...path].vue
··· 418 418 <button 419 419 v-if="selectedLines" 420 420 type="button" 421 - class="px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded hover:text-fg hover:border-border-hover transition-colors" 421 + class="px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded hover:text-fg hover:border-border-hover transition-colors active:scale-95" 422 422 @click="copyPermalinkUrl" 423 423 > 424 424 {{ permalinkCopied ? $t('common.copied') : $t('code.copy_link') }}
+1
i18n/locales/en.json
··· 111 111 "verified_provenance": "Verified provenance", 112 112 "view_permalink": "View permalink for this version", 113 113 "navigation": "Package", 114 + "copy_name": "Copy package name", 114 115 "deprecation": { 115 116 "package": "This package has been deprecated.", 116 117 "version": "This version has been deprecated.",
+1
lunaria/files/en-US.json
··· 111 111 "verified_provenance": "Verified provenance", 112 112 "view_permalink": "View permalink for this version", 113 113 "navigation": "Package", 114 + "copy_name": "Copy package name", 114 115 "deprecation": { 115 116 "package": "This package has been deprecated.", 116 117 "version": "This version has been deprecated.",
+12
test/nuxt/components.spec.ts
··· 56 56 import AppHeader from '~/components/AppHeader.vue' 57 57 import AppFooter from '~/components/AppFooter.vue' 58 58 import AppTooltip from '~/components/AppTooltip.vue' 59 + import AnnounceTooltip from '~/components/AnnounceTooltip.vue' 59 60 import LoadingSpinner from '~/components/LoadingSpinner.vue' 60 61 import JsrBadge from '~/components/JsrBadge.vue' 61 62 import ProvenanceBadge from '~/components/ProvenanceBadge.vue' ··· 187 188 it('should have no accessibility violations', async () => { 188 189 const component = await mountSuspended(AppTooltip, { 189 190 props: { text: 'Tooltip content' }, 191 + slots: { default: '<button>Trigger</button>' }, 192 + }) 193 + const results = await runAxe(component) 194 + expect(results.violations).toEqual([]) 195 + }) 196 + }) 197 + 198 + describe('AnnounceTooltip', () => { 199 + it('should have no accessibility violations', async () => { 200 + const component = await mountSuspended(AnnounceTooltip, { 201 + props: { text: 'Tooltip content', isVisible: true }, 190 202 slots: { default: '<button>Trigger</button>' }, 191 203 }) 192 204 const results = await runAxe(component)