Skip to content

feat: add Share button to package pages via Web Share API#2997

Open
tomayac wants to merge 1 commit into
npmx-dev:mainfrom
tomayac:feat/web-share-button
Open

feat: add Share button to package pages via Web Share API#2997
tomayac wants to merge 1 commit into
npmx-dev:mainfrom
tomayac:feat/web-share-button

Conversation

@tomayac

@tomayac tomayac commented Jul 4, 2026

Copy link
Copy Markdown

Summary

  • Adds a share button to the package header that uses 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.
  • Adds a v keyboard shortcut and a command palette entry for sharing the current package.
image

Split out of #2982 per review feedback to keep that PR focused on PWA/WCO/badging.

Test plan

  • pnpm test:unit — 1673/1673 passing
  • pnpm run test:types — passing
  • vp lint — 0 errors
  • node scripts/find-invalid-translations.ts — no missing/unused i18n keys
  • knip — clean

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.
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Jul 4, 2026 2:11pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Jul 4, 2026 2:11pm
npmx-lunaria Ignored Ignored Jul 4, 2026 2:11pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds native Web Share API support for packages: a new PackageShareButton client component, sharePackage() logic and a command palette entry in the package header, an exposed click() method on ButtonBase, plus i18n strings and an a11y test skip entry.

Changes

Package Web Share feature

Layer / File(s) Summary
Expose click() on ButtonBase
app/components/Button/Base.vue
Adds a click() method to the exposed instance API, delegating to the underlying element's .click().
ShareButton client component
app/components/Package/ShareButton.client.vue
New client-only component detects share capability, builds ShareData with an optional og:image file, wires a "v" keyboard shortcut, and calls navigator.share() via a ButtonBase instance.
Package header integration
app/components/Package/Header.vue
Adds canShare detection and sharePackage(), a conditional "package-share" command palette entry, and renders PackageShareButton with packageName and description props.
i18n strings and a11y test coverage
i18n/locales/en.json, i18n/schema.json, test/unit/a11y-component-coverage.spec.ts
Adds "share" and "share_aria_label" translation strings and schema field, and lists ShareButton.client.vue as a skipped a11y test with justification.

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)
Loading

Suggested reviewers: ghostdevv

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: adding a Share button using the Web Share API on package pages.
Description check ✅ Passed The description is directly related to the changeset and accurately summarises the share button, keyboard shortcut, and command palette entry.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 19.35484% with 25 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/Package/ShareButton.client.vue 19.23% 14 Missing and 7 partials ⚠️
app/components/Package/Header.vue 25.00% 2 Missing and 1 partial ⚠️
app/components/Button/Base.vue 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/components/Package/ShareButton.client.vue (2)

22-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test coverage for share fallback logic.

getOgImageFile() and share() 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 using vitest"; 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 value

Silently swallowing all share errors.

.catch(() => {}) suppresses both expected cancellations (AbortError) and genuine failures. Consider only swallowing AbortError and 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

📥 Commits

Reviewing files that changed from the base of the PR and between e731a54 and 8d40798.

📒 Files selected for processing (6)
  • app/components/Button/Base.vue
  • app/components/Package/Header.vue
  • app/components/Package/ShareButton.client.vue
  • i18n/locales/en.json
  • i18n/schema.json
  • test/unit/a11y-component-coverage.spec.ts

Comment on lines +10 to +20
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()
},
)
}

Copy link
Copy Markdown
Contributor

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:

#!/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
fi

Repository: 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 -n

Repository: 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.

Comment on lines +38 to +58
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(() => {})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

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.

tomayac added a commit to tomayac/npmx.dev that referenced this pull request Jul 4, 2026
…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.
@ghostdevv ghostdevv added the needs discussion An idea that needs more discussion to understand the scope and impact. label Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs discussion An idea that needs more discussion to understand the scope and impact.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants