-
-
Notifications
You must be signed in to change notification settings - Fork 485
feat: add Share button to package pages via Web Share API #2997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| <script setup lang="ts"> | ||
| const props = defineProps<{ | ||
| packageName: string | ||
| description?: string | null | ||
| }>() | ||
|
|
||
| const canShare = 'share' in navigator | ||
| const buttonRef = useTemplateRef<{ click: () => void }>('buttonRef') | ||
|
|
||
| if (canShare) { | ||
| onKeyStroke( | ||
| e => isKeyWithoutModifiers(e, 'v') && !isEditableElement(e.target), | ||
| e => { | ||
| e.preventDefault() | ||
| // Click the button element so the browser anchors the share sheet at | ||
| // the button's position rather than the current mouse cursor position. | ||
| buttonRef.value?.click() | ||
| }, | ||
| ) | ||
| } | ||
|
|
||
| async function getOgImageFile(): Promise<File | null> { | ||
| // Files sharing is not universally supported; bail out early if not available. | ||
| if (!navigator.canShare?.({ files: [new File([], 'x.png', { type: 'image/png' })] })) { | ||
| return null | ||
| } | ||
| try { | ||
| const ogMeta = document.querySelector<HTMLMetaElement>('meta[property="og:image"]') | ||
| if (!ogMeta?.content) return null | ||
| const blob = await fetch(ogMeta.content).then(r => r.blob()) | ||
| if (!blob.type.startsWith('image/')) return null | ||
| return new File([blob], `${props.packageName}.png`, { type: blob.type }) | ||
| } catch { | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| async function share() { | ||
| const shareData: ShareData = { | ||
| title: props.packageName, | ||
| text: props.description ?? props.packageName, | ||
| url: window.location.href, | ||
| } | ||
|
|
||
| const imageFile = await getOgImageFile() | ||
| if (imageFile) { | ||
| const shareDataWithFile: ShareData = { ...shareData, files: [imageFile] } | ||
| // Some implementations support sharing files or url/text, but not the | ||
| // combination of both. Only attach the file once the full payload | ||
| // (title + text + url + files) is confirmed shareable, so we keep the | ||
| // url/text fallback otherwise. | ||
| if (navigator.canShare?.(shareDataWithFile)) { | ||
| shareData.files = shareDataWithFile.files | ||
| } | ||
| } | ||
|
|
||
| await navigator.share(shareData).catch(() => {}) | ||
| } | ||
|
Comment on lines
+38
to
+58
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Share payload diverges from the command-palette share action.
Consider extracting the shared logic (title/text/url + optional image attachment) into a composable (e.g. 🤖 Prompt for AI Agents |
||
| </script> | ||
|
|
||
| <template> | ||
| <ButtonBase | ||
| v-if="canShare" | ||
| ref="buttonRef" | ||
| variant="secondary" | ||
| classicon="i-lucide:share-2" | ||
| :aria-label="$t('package.share_aria_label', { package: packageName })" | ||
| :ariaKeyshortcuts="'v'" | ||
| @click="share" | ||
| > | ||
| <span class="max-sm:sr-only">{{ $t('package.links.share') }}</span> | ||
| </ButtonBase> | ||
| </template> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 25678
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 20092
Register
vthrough the shared shortcut registryThe raw
onKeyStrokehere bypasses the global keyboard-shortcut toggle, sovstill works when shortcuts are disabled. Wire this throughuseShortcuts()like the other package actions inHeader.vueso it honours the setting.🤖 Prompt for AI Agents