Skip to content

fix: resolve readme copy functionality in Safari#2554

Merged
graphieros merged 8 commits intonpmx-dev:mainfrom
MatteoGabriele:fix/async-clipboard-not-working-in-safari
Apr 17, 2026
Merged

fix: resolve readme copy functionality in Safari#2554
graphieros merged 8 commits intonpmx-dev:mainfrom
MatteoGabriele:fix/async-clipboard-not-working-in-safari

Conversation

@MatteoGabriele
Copy link
Copy Markdown
Member

@MatteoGabriele MatteoGabriele commented Apr 16, 2026

🔗 Linked issue

resolves #2151

🧭 Context

The copy Readme button doesn't work in Safari

📚 Description

Unfortunately, for security reasons, Safari doesn't allow the Clipboard API to be used with async operations, so it silently fails without copying anything.
I've created a custom useClipboardAsync that addresses the security issues and handles the async call.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 16, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 16, 2026 10:07pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 16, 2026 10:07pm
npmx-lunaria Ignored Ignored Apr 16, 2026 10:07pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 16, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaced the README "copy as Markdown" clipboard wiring on the package page with an async clipboard composable. The page now uses useClipboardAsync which calls an async function that fetches README markdown and writes an async ClipboardItem to navigator.clipboard. (50 words)

Changes

Cohort / File(s) Summary
Package page (README copy)
app/pages/package/[[org]]/[name].vue
Replaced synchronous useClipboard + copyReadmeHandler() with useClipboardAsync; template click binding updated from copyReadmeHandler() to copyReadme.
New async clipboard composable
app/composables/useClipboardAsync.ts
Added useClipboardAsync(fn, options) exporting { copy, copied }. copy() awaits provided async fn, creates a ClipboardItem with a 'text/plain' Blob, calls navigator.clipboard.write, and manages a copied timeout.
CI config
.lighthouserc.cjs
Adjusted startServerReadyPattern from 'Listening' to `'(Listening

Sequence Diagram(s)

sequenceDiagram
  participant User as "User (click)"
  participant Page as "Package Page\n(`copyReadme`)" rect rgba(52,152,219,0.5)
  participant Composable as "useClipboardAsync\n(copy)" rect rgba(46,204,113,0.5)
  participant Fetch as "fetchReadmeMarkdown()" rect rgba(155,89,182,0.5)
  User->>Page: Click "Copy README"
  Page->>Composable: call copy() (async provider)
  Composable->>Fetch: invoke async fn
  Fetch-->>Composable: returns markdown string
  Composable->>Composable: create ClipboardItem(Blob(markdown))
  Composable->>Composable: navigator.clipboard.write([item])
  Composable-->>Page: set copied = true
  Page-->>User: UI feedback (copied)
Loading

Possibly related PRs

Suggested reviewers

  • danielroe
  • ghostdevv
🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR partially addresses the linked issue #2151. It implements an async clipboard solution, but the suggested fallback to document.execCommand('copy') for Safari compatibility was not implemented. Implement the suggested fallback mechanism using document.execCommand('copy') via a hidden textarea for Safari environments where the Clipboard API fails.
Out of Scope Changes check ⚠️ Warning The Lighthouse configuration change to startServerReadyPattern in .lighthouserc.cjs appears unrelated to the stated objective of fixing Safari clipboard functionality. Remove the unrelated .lighthouserc.cjs change or provide justification for why it is included in this Safari fix pull request.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the main change: fixing README copy functionality in Safari, which aligns with the primary objective of resolving issue #2151.
Description check ✅ Passed The pull request description directly addresses the changeset by explaining the Safari Clipboard API limitation and the custom useClipboardAsync solution implemented.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 16, 2026

Codecov Report

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

Files with missing lines Patch % Lines
app/composables/useClipboardAsync.ts 0.00% 12 Missing and 1 partial ⚠️
app/pages/package/[[org]]/[name].vue 0.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/composables/useClipboardAsync.ts (1)

8-10: copiedDuring default of 0 makes the copied state unobservable.

copiedDuring is typed as required on UseClipboardAsyncOptions but read with options?.copiedDuring ?? 0. Two small issues:

  • The type says required but the runtime treats it as optional — mark it optional (copiedDuring?: number) to match usage.
  • A default of 0 ms resets copied on the next tick, so any UI binding to copied will effectively never see true. Consider a sensible default (e.g. 1500) to match typical clipboard-feedback UX.
♻️ Proposed change
 type UseClipboardAsyncOptions = {
-  copiedDuring: number
+  copiedDuring?: number
 }
@@
-  const timeout = useTimeoutFn(() => (copied.value = false), options?.copiedDuring ?? 0, {
+  const timeout = useTimeoutFn(() => (copied.value = false), options?.copiedDuring ?? 1500, {
     immediate: false,
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useClipboardAsync.ts` around lines 8 - 10, The
UseClipboardAsyncOptions type currently declares copiedDuring as required but
the runtime reads options?.copiedDuring ?? 0, and the 0ms default makes the
copied state unobservable; update the type to make copiedDuring optional
(copiedDuring?: number) and change the runtime default from 0 to a sensible
value (e.g. 1500) where used in the useClipboardAsync composable (the
options?.copiedDuring ?? 0 expression) so UI bindings can observe the copied
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/composables/useClipboardAsync.ts`:
- Around line 21-31: The copy function currently sets copied.value = true before
calling navigator.clipboard.write and does not handle its Promise; change copy
to return Promise<void>, create the ClipboardItem as you do but await
navigator.clipboard.write([...]) (or attach .then/.catch) and only set
copied.value = true and call timeout.start() after the write promise resolves;
in the catch handler reset copied.value = false, surface or rethrow the error so
callers can handle it, and ensure any rejection from fn() propagated by the
ClipboardItem is also handled via the awaited write call.

---

Nitpick comments:
In `@app/composables/useClipboardAsync.ts`:
- Around line 8-10: The UseClipboardAsyncOptions type currently declares
copiedDuring as required but the runtime reads options?.copiedDuring ?? 0, and
the 0ms default makes the copied state unobservable; update the type to make
copiedDuring optional (copiedDuring?: number) and change the runtime default
from 0 to a sensible value (e.g. 1500) where used in the useClipboardAsync
composable (the options?.copiedDuring ?? 0 expression) so UI bindings can
observe the copied state.
🪄 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: ce1e0991-99c1-47bb-af65-61ac19e0656f

📥 Commits

Reviewing files that changed from the base of the PR and between db85c7b and f3f11b1.

📒 Files selected for processing (2)
  • app/composables/useClipboardAsync.ts
  • app/pages/package/[[org]]/[name].vue
✅ Files skipped from review due to trivial changes (1)
  • app/pages/package/[[org]]/[name].vue

Comment thread app/composables/useClipboardAsync.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.lighthouserc.cjs (1)

28-28: Unrelated config change; scope check.

This Lighthouse CI readiness pattern tweak appears unrelated to the Safari clipboard fix described in the PR objectives. The broader regex is reasonable (matches Nuxt/Vite dev output), but consider splitting unrelated infra changes into their own PR for cleaner history, or clarify in the description why it's bundled here.

One minor note: :3000 will match any line containing that substring (including unrelated log output that happens to reference the port). If that's intentional as a catch-all, no action needed; otherwise anchoring to something like localhost:3000 would be slightly more precise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.lighthouserc.cjs at line 28, The change to startServerReadyPattern in
.lighthouserc.cjs is an unrelated infra tweak; either move this regex update
into its own PR for cleaner history or document in the current PR why it’s
bundled, and if you want the pattern to be more precise update
startServerReadyPattern to anchor the port (for example use 'localhost:3000' or
add word/line boundaries) instead of the generic ':3000' so it doesn’t
accidentally match unrelated log lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.lighthouserc.cjs:
- Line 28: The change to startServerReadyPattern in .lighthouserc.cjs is an
unrelated infra tweak; either move this regex update into its own PR for cleaner
history or document in the current PR why it’s bundled, and if you want the
pattern to be more precise update startServerReadyPattern to anchor the port
(for example use 'localhost:3000' or add word/line boundaries) instead of the
generic ':3000' so it doesn’t accidentally match unrelated log lines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d01b413f-4013-4e25-a883-4a700228ebc8

📥 Commits

Reviewing files that changed from the base of the PR and between 0f3c79a and a89338c.

📒 Files selected for processing (1)
  • .lighthouserc.cjs

Copy link
Copy Markdown
Member

@knowler knowler left a comment

Choose a reason for hiding this comment

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

Works! I remember running into this one time and using a similar solution.

@MatteoGabriele MatteoGabriele added this pull request to the merge queue Apr 16, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Apr 16, 2026
Copy link
Copy Markdown
Contributor

@graphieros graphieros left a comment

Choose a reason for hiding this comment

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

Beautiful 🌿

@graphieros graphieros added this pull request to the merge queue Apr 17, 2026
Merged via the queue into npmx-dev:main with commit 791ce70 Apr 17, 2026
22 checks passed
@ghostdevv
Copy link
Copy Markdown
Contributor

Ty for solving this! Did you play around with useClipboardItems from vueuse? I tried to get it to work but I couldn't 🫠

@MatteoGabriele
Copy link
Copy Markdown
Member Author

Ty for solving this! Did you play around with useClipboardItems from vueuse? I tried to get it to work but I couldn't 🫠

I did, but I would crash on me, so I went the custom way. At least for the asynchronous operations. Perhaps opening an issue on vueuse could be an option as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Copy as Markdown" button on README doesn't copy to clipboard in Safari

4 participants