feat: add Share button to package pages via Web Share API#2997
Conversation
Adds a share button to the package header using navigator.share(), with a fallback to url/text-only sharing when the OG image can't be attached, plus a "v" keyboard shortcut and a command palette entry.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
📝 WalkthroughWalkthroughThis PR adds native Web Share API support for packages: a new ChangesPackage Web Share feature
Sequence Diagram(s)sequenceDiagram
participant User
participant Header as Package/Header.vue
participant ShareButton as PackageShareButton
participant NavigatorShare as navigator.share
User->>Header: open command palette / click share
Header->>Header: sharePackage() or delegate to ShareButton
Header->>ShareButton: render with packageName, description
User->>ShareButton: click or "v" shortcut
ShareButton->>ShareButton: build ShareData (title, text, url, optional image)
ShareButton->>NavigatorShare: share(shareData)
NavigatorShare-->>ShareButton: resolve or reject (swallowed)
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Lunaria Status Overview🌕 This pull request will trigger status changes. Learn moreBy default, every PR changing files present in the Lunaria configuration's You can change this by adding one of the keywords present in the Tracked Files
Warnings reference
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
app/components/Package/ShareButton.client.vue (2)
22-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo test coverage for share fallback logic.
getOgImageFile()andshare()contain the core fallback behaviour (capability probing, fetch failure handling, combined-payload gating) but no unit tests were added for this new logic.As per path instructions,
**/*.{test,spec}.{ts,tsx}should "Write unit tests for core functionality usingvitest"; this new share logic is a good candidate.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/components/Package/ShareButton.client.vue` around lines 22 - 36, Add unit tests for the share fallback behavior around getOgImageFile() and share() in ShareButton.client.vue, since the new capability probing, fetch/error handling, and combined-payload gating are not covered. Create vitest tests that exercise the main branches: navigator.canShare unavailable/false returns null, og:image missing or non-image content returns null, fetch failure is swallowed, and share() only includes the OG file when getOgImageFile() succeeds. Use the existing ShareButton.client.vue logic and its getOgImageFile/share symbols to locate the code.Source: Path instructions
57-57: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueSilently swallowing all share errors.
.catch(() => {})suppresses both expected cancellations (AbortError) and genuine failures. Consider only swallowingAbortErrorand logging other errors for observability.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/components/Package/ShareButton.client.vue` at line 57, The share flow in ShareButton.client.vue is swallowing every error from navigator.share, which hides real failures. Update the share handling around navigator.share(shareData) to only ignore expected AbortError cancellations, and route any other errors to the existing logging/observability path so genuine share failures can be diagnosed. Use the ShareButton component and its shareData/navigator.share call site to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/components/Package/ShareButton.client.vue`:
- Around line 38-58: The package share payload is implemented inconsistently
between ShareButton.client.vue and Header.vue, so the command-palette share
action uses a less rich ShareData than the button and shortcut. Extract the
shared share flow into a common helper/composable such as usePackageShare() that
builds the title/text/url payload, optionally attaches the og:image file when
navigator.canShare allows it, and is reused by both share() and Header.vue's
sharePackage() so all entry points behave the same.
- Around line 10-20: The ShareButton.client.vue keyboard handler currently uses
a raw onKeyStroke, so the v shortcut ignores the global shortcuts toggle. Update
the shortcut registration in the ShareButton component to go through
useShortcuts(), matching the pattern used by Header.vue and other package
actions, and keep the existing isKeyWithoutModifiers / isEditableElement checks
and buttonRef click behavior intact.
---
Nitpick comments:
In `@app/components/Package/ShareButton.client.vue`:
- Around line 22-36: Add unit tests for the share fallback behavior around
getOgImageFile() and share() in ShareButton.client.vue, since the new capability
probing, fetch/error handling, and combined-payload gating are not covered.
Create vitest tests that exercise the main branches: navigator.canShare
unavailable/false returns null, og:image missing or non-image content returns
null, fetch failure is swallowed, and share() only includes the OG file when
getOgImageFile() succeeds. Use the existing ShareButton.client.vue logic and its
getOgImageFile/share symbols to locate the code.
- Line 57: The share flow in ShareButton.client.vue is swallowing every error
from navigator.share, which hides real failures. Update the share handling
around navigator.share(shareData) to only ignore expected AbortError
cancellations, and route any other errors to the existing logging/observability
path so genuine share failures can be diagnosed. Use the ShareButton component
and its shareData/navigator.share call site to locate the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 26b214ff-9bba-4cf5-875b-f3a1d72c6e10
📒 Files selected for processing (6)
app/components/Button/Base.vueapp/components/Package/Header.vueapp/components/Package/ShareButton.client.vuei18n/locales/en.jsoni18n/schema.jsontest/unit/a11y-component-coverage.spec.ts
| 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() | ||
| }, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the relevant files first.
git ls-files | rg '^(app/components/Package/ShareButton\.client\.vue|app/components/Package/Header\.vue|app/components/.*/ButtonBase\.vue|.*useShortcuts.*|.*useKeyboardShortcuts.*|.*keyboard_shortcuts_enabled.*|.*shortcuts.*)$' || true
# Find the shortcut composables and the settings flag.
rg -n "useShortcuts|useKeyboardShortcuts|keyboard_shortcuts_enabled|onKeyStroke|useMagicKeys|useEventListener" app . --glob '!node_modules' --glob '!dist' --glob '!build'
# Show concise outlines for the most relevant files if present.
for f in \
app/components/Package/ShareButton.client.vue \
app/components/Package/Header.vue \
app/components/Package/ButtonBase.vue \
; do
if [ -f "$f" ]; then
echo "### OUTLINE: $f"
ast-grep outline "$f" --view expanded || true
fi
done
# Read the relevant sections with line numbers.
for f in \
app/components/Package/ShareButton.client.vue \
app/components/Package/Header.vue \
app/components/Package/ButtonBase.vue \
; do
if [ -f "$f" ]; then
echo "### FILE: $f"
wc -l "$f"
fi
done
# Print the most relevant line ranges if the files exist.
if [ -f app/components/Package/ShareButton.client.vue ]; then
sed -n '1,120p' app/components/Package/ShareButton.client.vue | cat -n
fi
if [ -f app/components/Package/Header.vue ]; then
sed -n '1,180p' app/components/Package/Header.vue | cat -n
fi
if [ -f app/components/Package/ButtonBase.vue ]; then
sed -n '1,220p' app/components/Package/ButtonBase.vue | cat -n
fiRepository: npmx-dev/npmx.dev
Length of output: 25678
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the shortcut composable and settings hook.
sed -n '1,220p' app/composables/useShortcuts.ts | cat -n
printf '\n### useSettings excerpt\n'
sed -n '1,240p' app/composables/useSettings.ts | cat -n
# Inspect a couple of other components that use keyboard shortcuts directly or via the composable.
printf '\n### ActionBar\n'
sed -n '1,120p' app/components/Package/ActionBar.vue | cat -n
printf '\n### ButtonBase\n'
sed -n '1,140p' app/components/Button/Base.vue | cat -n
printf '\n### Header shortcut usage\n'
sed -n '200,260p' app/components/Package/Header.vue | cat -nRepository: npmx-dev/npmx.dev
Length of output: 20092
Register v through the shared shortcut registry
The raw onKeyStroke here bypasses the global keyboard-shortcut toggle, so v still works when shortcuts are disabled. Wire this through useShortcuts() like the other package actions in Header.vue so it honours the setting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/components/Package/ShareButton.client.vue` around lines 10 - 20, The
ShareButton.client.vue keyboard handler currently uses a raw onKeyStroke, so the
v shortcut ignores the global shortcuts toggle. Update the shortcut registration
in the ShareButton component to go through useShortcuts(), matching the pattern
used by Header.vue and other package actions, and keep the existing
isKeyWithoutModifiers / isEditableElement checks and buttonRef click behavior
intact.
| 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(() => {}) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Share payload diverges from the command-palette share action.
share() here conditionally attaches an og:image file when fully shareable, but Header.vue's sharePackage() (used by the "package-share" command) only ever shares title/text/url. Users triggering share via the command palette get a different, less-rich payload than users clicking the button or using the v shortcut.
Consider extracting the shared logic (title/text/url + optional image attachment) into a composable (e.g. usePackageShare()) consumed by both Header.vue and this component, so all three entry points behave identically.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/components/Package/ShareButton.client.vue` around lines 38 - 58, The
package share payload is implemented inconsistently between
ShareButton.client.vue and Header.vue, so the command-palette share action uses
a less rich ShareData than the button and shortcut. Extract the shared share
flow into a common helper/composable such as usePackageShare() that builds the
title/text/url payload, optionally attaches the og:image file when
navigator.canShare allows it, and is reused by both share() and Header.vue's
sharePackage() so all entry points behave the same.
…ate PRs Per review feedback, keeps this PR focused on PWA installability, WCO, and app badging. The Share button and native switch polyfill now live in npmx-dev#2997 and npmx-dev#2998 respectively.
Summary
navigator.share(), attaching the package's OG image when the full payload (title/text/url/files) is confirmed shareable, falling back to url/text-only otherwise.vkeyboard shortcut and a command palette entry for sharing the current package.Split out of #2982 per review feedback to keep that PR focused on PWA/WCO/badging.
Test plan
pnpm test:unit— 1673/1673 passingpnpm run test:types— passingvp lint— 0 errorsnode scripts/find-invalid-translations.ts— no missing/unused i18n keysknip— clean