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
smup. A grid, not a wrapping flex row, because grid items ignoreflex-1— so every cell is exactly half and every button keeps itsh-11. btnRowOutlineisw-full sm:flex-1. Bareflex-1setsflex-basis: 0on the main axis, which in the old stacked column landed on the height, beath-11, and collapsed "Copy link" to its text;sm:flex-1alone 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, sodesktopOnlyis 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