+const props = defineProps<{
+ packageName: string
+ resolvedVersion: string
+ isLatest: boolean
+ license?: string
+}>()
+
+const { origin } = useRequestURL()
+const colorMode = useColorMode()
+const theme = computed(() => (colorMode.value === 'dark' ? 'dark' : 'light'))
+const { selectedAccentColor } = useAccentColor()
+
+const colorParam = computed(() =>
+ selectedAccentColor.value ? `&color=${encodeURIComponent(selectedAccentColor.value)}` : '',
+)
+
+const cardUrl = computed(
+ () => `/api/card/${props.packageName}.png?theme=${theme.value}${colorParam.value}`,
+)
+const absoluteCardUrl = computed(() => `${origin}${cardUrl.value}`)
+
+// Downloads for alt text
+const compactFormatter = useCompactNumberFormatter()
+const { data: downloadsData } = usePackageDownloads(
+ computed(() => props.packageName),
+ 'last-week',
+)
+
+// e.g. nuxt 4.4.2 (latest) — 1.4M weekly downloads — MIT license — via npmx.dev
+const altText = computed(() => {
+ const tag = props.isLatest ? 'latest' : props.resolvedVersion
+ const parts: string[] = [`${props.packageName} ${props.resolvedVersion} (${tag})`]
+ const dl = downloadsData.value?.downloads
+ if (dl && dl > 0) {
+ parts.push(`${compactFormatter.value.format(dl)} weekly downloads`)
+ }
+ if (props.license) parts.push(`${props.license} license`)
+ parts.push('via npmx.dev')
+ return parts.join(' — ')
+})
+
+// Copy link button
+const { copied: linkCopied, copy: copyLink } = useClipboard({
+ source: absoluteCardUrl,
+ copiedDuring: 1500,
+})
+
+// Copy alt text button
+const { copied: altCopied, copy: copyAlt } = useClipboard({
+ source: altText,
+ copiedDuring: 1500,
+})
+
+// Reveal Copy ALT after the user has downloaded or copied the link
+const showAlt = ref(false)
+
+// Image load state
+const imgLoaded = ref(false)
+const imgError = ref(false)
+
+watch(cardUrl, () => {
+ imgLoaded.value = false
+ imgError.value = false
+ showAlt.value = false
+})
+
+function downloadCard() {
+ downloadFileLink(cardUrl.value, `${props.packageName.replace('/', '-')}-card.png`)
+ showAlt.value = true
+}
+
+function handleCopyLink() {
+ copyLink()
+ showAlt.value = true
+}
+
+
+
+
+
+
+
+
+ Generating card…
+
+
+
+
+ Failed to load card.
+
+
+
+
![]()
+
+
+
+
+
+
+
+ {{ altCopied ? 'Copied!' : 'Copy ALT' }}
+
+
+
+
+
+
+ {{ linkCopied ? 'Copied!' : 'Copy link' }}
+
+
+
+ Download PNG
+
+
+
+
+
diff --git a/app/components/Package/Skeleton.vue b/app/components/Package/Skeleton.vue
index 0c2409fd64..818a18a78f 100644
--- a/app/components/Package/Skeleton.vue
+++ b/app/components/Package/Skeleton.vue
@@ -15,6 +15,8 @@
+
+
diff --git a/app/pages/package/[[org]]/[name].vue b/app/pages/package/[[org]]/[name].vue
index bc10db6210..3ea9551616 100644
--- a/app/pages/package/[[org]]/[name].vue
+++ b/app/pages/package/[[org]]/[name].vue
@@ -1,11 +1,20 @@
+
+