Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ unguarded hover style sits latched on a phone until the next tap elsewhere.
Any hover rule that changes a control's fill belongs inside
`@media (hover: hover)`.

## Sharing is gated on the pointer, not on the API

`navigator.share` exists on desktop Chromium and desktop Safari, so a
feature-detect alone sends Brave, Chrome and Safari on macOS into the OS
share sheet. That sheet offers Mail, Messages, Notes and Reminders and no
clipboard entry, which leaves no way to get the link out of a button that
otherwise says Copy Link. The test is
`navigator.share && matchMedia("(pointer: coarse)")`, and the markup
comment beside the button says so too, because the label it describes is
written from the script.

The failure is quiet in both directions: a desktop visitor gets a sheet
that cannot do the one thing they pressed for, and a `share()` that
rejects for any reason other than a cancelled sheet leaves the button
doing nothing at all. Only `AbortError` returns early; everything else
falls through to the clipboard.

The browser pane cannot verify the clipboard half of this.
`navigator.clipboard.writeText` throws `NotAllowedError` there because the
pane's document is never focused, so the copy path can be read but not
run. Press the button in a real browser before believing it.

## The inline script has one scope

`index.html` holds a single long inline script. Two consequences:
Expand Down
53 changes: 34 additions & 19 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -911,10 +911,11 @@ <h2 class="section-title export-title">Export &amp; share</h2>
<button id="themeToggleBtn" class="secondary icon-btn" title="Switch to light theme"><i class="ph ph-sun"></i></button>
</div>
<div class="split-btn">
<!-- Label and icon swap to "Share" wherever the OS share sheet
is available (see the feature-detect below); the id, the
menu beside it, and the link it hands off all stay the
same either way. -->
<!-- Label and icon swap to "Share" on a touch device, where
the OS sheet is the better hand-off (see the pointer test
below; desktop browsers expose the API too and must not
take that branch). The id, the menu beside it, and the
link it hands off all stay the same either way. -->
<button id="copyLinkBtn" class="secondary split-btn-main" title="Copy a link to this exact composition, locks included"><i class="ph ph-link"></i> Copy Link</button>
<!-- Whether the copied link opens as a plain still or also
offers the recipient Auto-randomize at the current speed
Expand Down Expand Up @@ -4164,26 +4165,23 @@ <h1 class="brand"><a href="./">Flield<span class="sr-only">: seamless animated b

const copyLinkBtn = document.getElementById("copyLinkBtn");

// Wherever the OS share sheet exists (Messages, AirDrop, most mobile
// and some desktop browsers), the button hands the link straight to
// it instead of the clipboard; the label and icon change to match so
// it's never a hidden feature riding along under an unrelated label.
const canShare = !!navigator.share;
// On a touch device the OS share sheet is the better hand-off
// (Messages, AirDrop, whatever else is installed), so the button
// gives it the link instead of the clipboard, and the label and icon
// change to match rather than hiding the swap under Copy Link.
//
// Gated on the pointer, not on the API: desktop Chromium (Brave and
// Chrome on macOS) exposes navigator.share too, and the sheet it
// opens there offers Mail, Messages, Notes and Reminders but no
// clipboard, which is the one thing someone pressing a button
// labelled Copy Link was after.
const canShare = !!navigator.share && window.matchMedia("(pointer: coarse)").matches;
if (canShare) {
copyLinkBtn.innerHTML = iconTag("share-network") + " Share";
copyLinkBtn.title = "Share this exact composition, locks included";
}

copyLinkBtn.addEventListener("click", async () => {
const { url, autoplay } = buildShareLink();
if (canShare) {
try {
await navigator.share({ title: "Flield", text: "A composition I made with Flield:", url });
} catch (err) {
// AbortError on a cancelled share sheet is normal, not a failure.
}
return;
}
async function copyShareLink(url, autoplay) {
try {
await navigator.clipboard.writeText(url);
showToast(autoplay ? "Link copied, plays on open" : "Link copied");
Expand All @@ -4194,6 +4192,23 @@ <h1 class="brand"><a href="./">Flield<span class="sr-only">: seamless animated b
// Dialogs are unavailable in some environments; nothing more to do.
}
}
}

copyLinkBtn.addEventListener("click", async () => {
const { url, autoplay } = buildShareLink();
if (canShare) {
try {
await navigator.share({ title: "Flield", text: "A composition I made with Flield:", url });
return;
} catch (err) {
// A cancelled sheet is normal and wants nothing after it. Any
// other failure (a browser that refuses the call, a sheet that
// never opens) would leave the button doing nothing at all, so
// the clipboard below still gets the link.
if (err && err.name === "AbortError") return;
}
}
copyShareLink(url, autoplay);
});

if (!loadStateFromURL()) {
Expand Down
2 changes: 1 addition & 1 deletion sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://flield.com/</loc>
<lastmod>2026-09-29</lastmod>
<lastmod>2026-10-02</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
Expand Down
Loading