A dame.is & pds.dad production bsky38.com

Share a ballot or a campaign to any Atmosphere client that takes an intent #63

Merged dame.is
targeting main from deleted fork:share-intent-links
Merged opened by and assigned to dame.is
targeting main from deleted fork:share-intent-links

A finished ballot had nowhere to go. $lib/atproto/share turns the Aturi waypoint catalog's compose intents into a list of clients that will open a composer with our text already in it, and ShareMenu renders that list plus a "Copy share text" row for everyone else — a client we do not name, a different network, a DM.

These are anchors to …/intent/compose?text=…, never a fetch: the composer opens, the words are there, and the person still has to hit send. So the feature needs no scope, no token and no write path, and the one thing this app writes to a repo is still a vote.

Which clients appear is two rules. FEATURED_WAYPOINT_IDS is now exported so "Share to…" offers the same shortlist "Open in…" does rather than a second, rival list; and prefillsText drops the clients that route the intent but ignore the text, where a link would silently share an empty composer. That leaves Bluesky, Northsky, Blacksky, Anisota and Mu — Red Dwarf is featured for reading but publishes no intent route. Picking a client here records it as the reader's client, the same openIn preference the "Open in…" menus write, since it is the same question asked twice.

The ballot's message names no handles. A full ballot is 38 of them, three post limits, and a partial list would put an implied ranking back on picks the ballot card works hard to keep unranked. The messages carry the full origin rather than the bare host, because a scheme is what makes the composer's link detection certain, and they read data.origin, not location.origin — the address is going into somebody else's timeline. share.test.ts pins the shortlist and asserts every message fits 300 grapheme clusters, including one built from a display name at Bluesky's own 64-character ceiling.

The dropdown behaviour moved out of OpenIn into MenuController rather than being copied: Escape, the arrow rotation, the outside click, the tab-out and handing focus back to the trigger are the parts a second copy gets quietly wrong. Panel and row styling are menuPanel/menuRow in $lib/ui.

Alongside, the profile card's action row, which this made four buttons wide:

  • It is a two-by-two grid on mobile and one row from sm up. A grid, not a wrapping flex row, because grid items ignore flex-1 — so every cell is exactly half and every button keeps its h-11.
  • btnRowOutline is w-full sm:flex-1. Bare flex-1 sets flex-basis: 0 on the main axis, which in the old stacked column landed on the height, beat h-11, and collapsed "Copy link" to its text; sm:flex-1 alone left the two buttons that sit inside a dropdown wrapper sized to their own text and narrower than their neighbours. Both rules are needed.
  • "Edit campaign profile" is "Edit" on the card and on the ballot page; the editor panel it opens still carries the full name. The anonymous CTA is "Log in", which at half a phone's width no longer wraps to two lines beside three single-line siblings — the sticky bar below still says it in full.
  • A "copied" toast is no longer desktopOnly. The vote confirmation is hidden on mobile because the ballot bar repeats it; nothing repeats this one, so desktopOnly is now per toast rather than per page.

OpenIn also carries a name variant — a handle inside a sentence, for the footer bylines — written in a parallel session on top of this refactor. Its consumer is not in this commit, so it lands here unused.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

Commits 1

Share a ballot or a campaign to any Atmosphere client that takes an intent