Repository navigation
One Publish and share button, in a bar on phones; drafts show no public link - #578
Conversation
Closes #457. An open signup's header put a one-click Close signup where Publish had just been, and closing can't be undone. - Desktop: Preview and a ⋯ (More actions) button, outlined like Preview. Its panel holds Close signup, which swaps the panel to a question: where the signup stands ("Nobody has signed up yet.", "18 of 21 spots are still open.", "3 spots are taken."), then "People can't sign up after this, and you can't reopen it." Not yet has focus and hands it back to ⋯; the red Close signup submits through StatusActionButtonForm, so closeAction and statusRefusal are unchanged. A refusal shows in the panel, which stays open while the close is in flight. - Phone: the More actions sheet asks the same question, with 44px+ buttons, and closes once the signup has closed instead of staying open and empty. - After closing, one sr-only role="status" region says "Closed. People can't sign up now." and focus moves to the signup's title, since the ⋯ that had it is gone. - New src/components/ui/Popover.tsx: a small non-modal anchored dialog (Escape, outside mousedown and focus leaving close it; focus returns to the trigger on Escape or a close from inside). useOutsideClose moves to src/hooks and gains an opt-in focus-leaving close; the builder's pickers keep their behaviour. - The layout loads loadCommittedBySlotForOrganizer (cached; the Build tab already calls it) and passes the header spotCounts(), a pure helper in src/lib/spot-counts.ts beside closeSignupQuestion(). - The Settings e2e test scopes its "Saved" status to the form, since the header now has a live region of its own. Budgets: every signup tab's first-load JS grows by 1.3-1.4 kB gzip (the ⋯ menu, the popover, the question and the live region, all in the shared layout chunk), so the five [id] budgets go up by 1 kB each: build 151.1 -> 152.5 (152 -> 153), responses 124.5 -> 125.9 (125 -> 126), settings 124.1 -> 125.4 (125 -> 126), edit and [id] 123.1 -> 124.4 (124 -> 125). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
…is page's close Review fixes for the Close signup change. - A closed signup shows the existing Banner on every tab: "Signup closed" / "People can’t sign up now. Everyone already signed up keeps their spot." It is not a live region (Banner gains `live`, default true), so a screen reader hears the header's "Closed. People can’t sign up now." once rather than both. - Opening ⋯ (or the phone sheet) on an open signup calls router.refresh(), so the question counts sign-ups made since the page loaded. Build's client state keeps its edits through the refresh. - spotCounts gains `open`: each open slot's free spots, never below none, null when an open slot has no limit. A closed slot's free spots no longer count as open, and an over-full slot no longer cancels free spots elsewhere. The question goes by it: "4 of 16 spots are still open.", "No spots are still open." when none are open but not every spot is taken. `taken` and `total` are unchanged. - The announcement and the focus move happen only when this page's Close signup closed the signup: useAnnouncedClose wraps closeAction and marks it on success, and HeaderAnnouncer, now holding both headers, reads the mark on the next status change. A signup closed by another tab or an AI assistant shows the banner without either. - The desktop menu runs Close signup in its own scope, and the question shows a refusal only for a Close signup pressed in that asking, so an earlier refusal doesn't come back. - Not yet and the red Close signup are the same height (both bordered). - The phone ⋯ has aria-haspopup="dialog" and aria-expanded. - The layout skips the spots-taken read for drafts. - Tests: the held-open guard (Escape and an outside click wait while the close is in flight), a clean second asking, the announcer's gating, open spots with a closed or over-full slot, and e2e checks for the banner, fresh counts and a signup closed elsewhere. Budgets: /app/signups/[id]/responses goes from 125.9 to 126.1 kB, over its 126 kB by 0.1 kB, so it rises to 127. The banner is rendered on the server and adds no JavaScript; the 0.2 kB is router.refresh() in both headers, the close-from-here context and the per-asking refusal state. Build is 152.9 of 153 kB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
router.refresh() on every ⋯ open rendered the whole route again, which re-ran the page's reads and its after() telemetry, so each opening wrote another signup.viewed (Build) or signup.editor_opened row. - closeQuestionAction(signupId) in the signup actions reads the slots and spots taken through getSignupForOrganizer (policy guard included) and returns the question, or a refusal through refusalAsResult. It revalidates nothing. - useCloseQuestion asks it when ⋯ opens, on a computer and a phone; the page's question shows until the answer comes, and stays if none does. - Wording: "No spots are open." when no spot is open, and "Every spot is taken." only when every slot is open and full (SpotCounts gains allSlotsOpen). An over-full slot beside a closed slot with free spots no longer reads as every spot taken. - Tests: db tests for the action (fresh counts, a refusal comes back as one, nothing revalidated), the menu asking again as it opens, and the new wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
A pure builder for the message the share panel offers to send with the link: the title, then the dates from the reminder anchor (one day, or the earliest to the latest), the number of slots and, when every slot has a limit, the spots, then "Sign up here: <link>". Each part is left out when it doesn't apply, and the middle line too when nothing is left. `withLink` puts the link back on its own line if an edit took it out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
lean-qr 2.7.4 (MIT, no dependencies), imported from `lean-qr/nano` only when the code mounts, so it stays out of every page's first load. The preview is an inline SVG; the PNG for a flyer is drawn on a canvas only when Download is chosen, and nothing is sent anywhere. If the encoder can't load, or the browser can't draw the PNG, the QR code is left out rather than breaking what's around it. Ported from the share step in #531, without its telemetry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
`useClipboardCopy` copies, says so for a moment, and resolves to whether the clipboard took it. The public link chip uses it, with the same 1.4 s it had, so there is one copy implementation for the chip and the share panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
What the header will open after Publish and from Share: the link with Copy link, an editable message with Copy message and Email it (a mailto: link), and a QR code. On a phone with a share menu, Share… comes first and stands in for Copy message and Email it. The first time it says Published and "Now send people the link"; later "Share the link". The owner keeps the message, so an edit survives the panel closing. `useMediaQuery` will pick the popover or the sheet once the header uses it; it is null until the window can be asked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
PR 1's spotCounts already adds up a signup's capacities, null when a slot has no limit, so the message uses it rather than a second loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Part of #505: recording copies and shares waits for #525. - publishAction returns { published: true } after revalidating the layout instead of redirecting to /build?published=1, the way closeAction stays put. Publish from Responses or Settings stays on that tab, and a reload or a shared address never reopens the panel. - One StatusActionScope runs Publish for both headers, so it outlives the phone's More actions sheet, which closes as the answer lands; Close signup keeps PR 1's scopes in the ⋯ panel and the sheet. ShareScope, inside it, opens the panel in its first-time form when Publish answers that it published, and keeps the message: the server's (built in the layout by shareMessage) until the organizer edits it, then the edit while the page is open. - An open signup's header has Share (Share2): on a computer in the blue spot right of Preview, before ⋯, opening PR 1's Popover (420px, aria-labelledby its heading, aria-haspopup and aria-expanded on the button); on a phone a full-width button under the public link, opening a BottomSheet titled by the heading, with Published as its description the first time. useMediaQuery picks one after mount, so the portalled sheet never opens on a computer. - The panel's body is next/dynamic with ssr: false and renders only while open; the QR encoder loads inside it. - On a phone the More actions sheet closes once the status changes (it remembers the status it was opened on), so after Publish the share sheet takes its place rather than Close signup showing. - Escape or the ✕ puts focus back on Share. The Popover no longer takes focus from a child that put it inside as it mounted, and BottomSheet passes onCloseAutoFocus through. - The header says "Published. Anyone with the link can sign up now." only when this page's Publish answered that it published (useAnnouncePublished, which ShareScope calls), as PR 1 does for a close made here. - PublishedBanner and the Banner's published kind are gone; useDismissibleQueryFlag stays for AiDraftBanner. - The organizer e2e and the run skill's sample script wait for the panel instead of "Signup published". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
"Create and publish your first signup" now says that Publish opens "Now send people the link", and what it offers: Copy link, a message built from the signup that can be changed, Copy message and Email it, Share… on most phones in place of those two, and a QR code to download. Share at the top opens it again. The UI list gains those names in place of "Signup published"; Share…, which the walkthrough's browser can't show, is checked by a unit test. The walkthrough publishes, checks the panel (focus on Copy link, the message's dates, slots and spots, the copies, the mailto: link, the QR code's download), closes it with Escape before the header picture, and reopens it from Share. The phone test checks that the More actions sheet gives way to the share sheet. With HELP_SCREENSHOTS=1 it also takes a picture of the panel, for the article once it's taken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Shared again after people have signed up, the message said every spot the signup ever had. It now goes by spotCounts' open spots: "21 spots" before anyone signs up, "18 spots left" after, and nothing about spots once none are left or an open slot has no limit. A closed slot's spots don't count. The layout passes the spots taken it already reads (none for a draft), and the help page says "how many spots are left". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
…hen a copy is refused Review fixes. - In the App Router, next/dynamic is React.lazy: a panel chunk that fails to load (a weak signal, a tab open across a deploy) threw to an error boundary there isn't, so the page broke right after Publish. loadSharePanel now resolves to ShareUnavailable instead: the link, Copy link, and "Couldn’t load the other ways to share. Reload to try again." - The heading, "Published" and the ✕ are ShareButton's on a computer too, as the phone's sheet already had them, so the popover is named and can be closed while the body loads. SharePanel loses onClose and headingId. - Pressing Publish starts loading the panel (ShareScope now runs Publish's scope and wraps the action), so it's usually in by the time the answer lands. - One live region says the last copy that worked, so a second copy is announced too. - A copy the clipboard refuses (plain HTTP, a denied permission) says "Couldn’t copy. Select the link and copy it." (or the message) and selects it; on a phone the link's box appears for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review fix. A browser that couldn't draw the PNG kept a Download that did nothing, and the row showed an inert Download while the encoder loaded. Now the QR code shows once the encoder is in and the browser has a canvas to draw on; if the canvas then won't give the picture back, the click is cancelled and the QR code goes away. The panel goes on without it in each case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review nits. useMediaQuery made a MediaQueryList on every snapshot; it now makes one per query. The public link chip copies straight from its button, without a wrapper that only awaited. Prettier on the files this change touched, where they were short (SignupHeader keeps PR 1's flat nesting, and the help article its line breaks, to keep the diff to what changed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review fixes. The walkthrough read the clipboard straight after a click, before the copy had landed; it now polls, also for the public link chip's copy that follows the panel's. The CHANGELOG entry says where focus goes after an outside click (a computer leaves it where it landed), that only this page's Publish is announced, and what the review round added: spots left, refused copies, the QR code only where it can be drawn, and the fallback when the panel can't load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
The Share button's code counts against every signup tab's first load, and next/dynamic and the fallback had put about 1.2 kB gzip into it. - React's lazy and Suspense in place of next/dynamic, which is React's lazy plus a runtime of its own (LoadableComponent, BailoutToCSR, PreloadChunks): the panel renders only once opened, never on the server, so ssr: false bought nothing. loadSharePanel still never rejects, so a failed load still shows the fallback. - ShareUnavailable is the public link chip, with its copy button, and "Couldn’t load the other ways to share. Reload to try again.": the chip is in the first load already, where a link box, a Copy link button and its refusal handling of the fallback's own were not. The panel's link box moves back into the panel, its only user. - The phone header test waits, in its act, for the panel that loads as the sheet mounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Every /app/signups/[id] tab shares the header's JavaScript, and Share adds 1.4-1.5 kB gzip to it (measured against PR 1 at 5da0166): build 153.0 -> 154.4 kB (153 -> 155) responses 126.1 -> 127.6 kB (127 -> 128) settings 125.7 -> 127.1 kB (126 -> 128) edit 124.7 -> 126.1 kB (125 -> 127) [id] 124.7 -> 126.1 kB (125 -> 127) What it is: ShareButton with the panel's heading, Published mark and ✕ and its Popover and BottomSheet wiring, plus the Share2 icon (about 1.3 kB alone, before shared compression); ShareScope, which runs Publish's scope and keeps the panel's state, with useMediaQuery (0.7); and the loader with its fallback, the public link chip and one line (0.3). The panel itself (3.8 kB gzip) and the QR encoder (2.0 kB) load only when the panel first opens or Publish is pressed. React's lazy in place of next/dynamic, and the chip as the fallback, already took the first-load cost down from 2.6 kB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
The walkthrough's picture of the panel, taken with the stand-in address, goes in the article under "Now send people the link". The walkthrough allows the suffix a slug gets when an earlier run made a signup with the same title, in the message, the clipboard and the QR code's file name. published.png came out the same, so it stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Tophat fixes. - On a phone, the focused Copy link shrank from the full width to half 50-200 ms after the sheet opened, when the QR code arrived beside it. The QR code now keeps its place while the encoder loads, as an inert stand-in hidden from screen readers, and goes away only if the encoder can't load or the browser can't draw the PNG. On a computer its row is there from the start in the same way. - loadSharePanel starts loading the encoder beside the panel's own code (and so as Publish is pressed), rather than once the panel asks. - Download is as tall as Copy message and Email it, with padding, so its focus ring no longer hugs the word. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Tophat fix. The refusal showed below Email it, about 280px under Copy link on a phone. Now a refused link copy shows under the link's row (on a phone, beside the link box that appears, above Copy link), and a refused message copy under Copy message and Email it. Each place is a live region from the start, so what it says is announced; the link's also says, unseen, each copy that worked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Tophat fix: two pieces the review round added had no test. Publish signup's refusal shows for a press in this opening of the sheet and is gone when it opens again; a signup that turns open without this page's Publish opens no panel and announces nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
…walkthrough CI's e2e failed the publish walkthrough twice in a row: the share panel's message read "Sat, Apr 6 · 2 slots · 4 spots" where the steps had moved the copy to Apr 13. The walkthrough changes the copy's date and goes straight on (Done, delete the empty slot, the Fields dialog, Preview, Publish). A cell saves 800 ms after the last keystroke, and "Saved" can be the delete's; on a slow runner the date's PATCH hadn't reached the database when Publish's re-render built the message, and nothing re-renders the layout after a Build save, so the panel kept the duplicate's old date. Delaying slot PATCHes by 2.5 s locally reproduces the failure exactly; with this change the walkthrough passes under that delay. The walkthrough now waits for that PATCH to land, as an organizer sees Saved, before going on. The race itself, Publish while a Build save is pending or in flight, is fixed in the app by the publish check's flushPendingSaves(), in a later PR of this stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
spotsTakenLine, beside closeSignupQuestion: "0 of 21 spots taken" when every slot has a limit, "3 spots taken" (and "1 spot taken") when one has none or there are no slots to count against, and "0 of 1 spot taken" for a total of one. A closed signup's says so first, "Closed · 3 of 21 spots taken", and an archived one's "Archived · …": its count won't change now. It goes by spotCounts' taken and total, so it counts confirmed and tentative sign-ups as the public page does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
…ic link Closes #499. Publish was a small text-xs button on a computer and, on a phone, an item inside the More actions sheet, and a draft showed a public link whose page only said it wasn't ready yet. - Computer: Preview and Publish and share (Send icon, "Publishing…" while it runs) at the header's normal size, 36px tall, 13px; Share and ⋯ match on an open signup. A draft has no ⋯. Closed and archived show Preview only. - Phone: a bar fixed to the bottom of the screen while the signup is a draft or open, over the page and under Radix's z-50 sheets, with a top border and the home indicator's inset. A draft's bar is Publish and share, 48px tall, with a refusal just above it; an open signup's is the count and Share, which opens PR 2's share sheet and gets focus back when it closes. PR 2's full-width Share under the link moves into it. Closed and archived have no bar. - The page leaves room for the bar on phones only, and only while it shows (PHONE_BAR_ROOM in the layout: the bar's 73px and the home indicator's inset), not in the Build tab as well. In the browser at 390x844 the Build tab's grey runs to the bar, the last slot, Add a slot and an open slot editor stay clear of it, and so do the ends of Responses and Settings. - The More actions sheet loses Publish signup. It holds Preview (a new tab, as on a computer; choosing it closes the sheet) for a draft, and Preview then Close signup for an open signup; a closed or archived signup still has no ⋯. - Under the title, a draft says "Only you can see this draft until you publish it." in place of the public link. Open and closed signups show the link and, beside it on a computer, spotsTakenLine from the layout's counts; on a phone an open signup's count is in the bar and a closed one's under the link. - Publish behaves as in PR 2: no navigation, the share panel opens once and only for this page's Publish, and the same spot then says Share. - The header's button classes live in header-buttons.ts, so Preview, Publish and share, Share and ⋯ stay the same size. - What never changes on the page (the draft line, the phone's link and closed count, the sheet's Preview with its icon) is rendered on the server in HeaderParts.tsx and handed to the phone's header as nodes, which keeps about 0.2 kB out of every signup tab's first load. - Tests: both headers per status, the bar per status, the ⋯ sheet's items per status, a refusal above the bar's button, and Publish from the bar handing focus to its Share. PR 2's test of Publish signup's refusal in the sheet goes with the item. The organizer e2e and the run skill's sample script press Publish and share. - CHANGELOG: PR 2's line says Share is in the bar on a phone, since both ship together. Budgets: every /app/signups/[id] tab grows by 0.2 kB gzip (PhoneBar, the bar's Share and the phone header's props), all within budget: build 154.4 -> 154.6 (155), responses 127.6 -> 127.8 (128), settings 127.1 -> 127.3 (128), edit and [id] 126.2 -> 126.4 (127). Nothing stored or sent changes, so the legal pages stay as they are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
"Create and publish your first signup" follows the new header.
- Create: the signup is a draft, and under its title it says "Only you
can see this draft until you publish it."; there's no link to send
yet. The paragraph saying a draft's link only says it isn't ready
goes, and so do the walkthrough steps that opened it.
- Preview: at the top on a computer, and on a phone under the three
dots, which phones didn't have before.
- Publish: Publish and share, at the top of the page or at the bottom
of the screen on a phone. publishOnPhone ("Publish signup") goes from
the UI list.
- Share again later: Share is at the top, or at the bottom on a phone,
and how many spots are taken is next to the link (beside Share on a
phone). The header picture's crop takes in the count, and
published.png is re-taken with it (605x110 in the article). The
other pictures came out the same apart from text rendering, so they
stay.
- The phone walkthrough opens Preview from the three dots, checks that
Publish and share fills the bar at the bottom, publishes from it, and
finds focus back on the bar's Share beside "0 of 1 spot taken".
- docs/writing-help.md's example of a phone showing a button somewhere
else is the new one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review fixes. ShareScope and StatusActionScope said Publish's scope had to outlive the phone's More actions sheet, "which closes as Publish lands"; Publish is in the bar now, and the scope serves the computer's header and the phone's bar. Nothing else existed only for that: the phone header still tracks the status its sheet was opened on, because Close signup in the sheet changes it. The ⋯ shows when hasPhoneBar says the bar does (a draft or an open signup), instead of repeating the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review fix. With nobody signed up, the tab said "Share the public link to start collecting." whatever the status, but a draft shows no public link now, and a closed or archived signup takes no more sign-ups. noResponsesLine, beside summarizeResponses and tested with it: - draft: "No signups yet. People can sign up once you publish and share it." - open: "No signups yet. Share the public link to start collecting.", as before. - closed or archived: "No signups. People can’t sign up now." No help page names it, and the responses e2e reads only "No signups yet." on an open signup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review fixes. - A refusal makes the bar taller (141px with a two-line one, against the 73px the page left), and it stays on every tab until the next press, so the bottom of Settings' Delete signup could end up under it. PhoneBar now measures itself with a ResizeObserver and puts its height on the page as --phone-bar-h, from a ref callback whose cleanup removes it with the bar. PHONE_BAR_ROOM is that height, or 73px and the home indicator's inset until it's measured. - Tab could scroll a focused control to the bottom edge, behind the fixed bar (WCAG 2.2, 2.4.11 Focus Not Obscured). html now has scroll-padding-bottom: var(--phone-bar-h, 0px), so a control brought into view stops above the bar. On a computer the bar is hidden, so its height, and so the padding, is 0; no :has() needed. - Test: the bar reports its height to the page and clears it once a closed signup has no bar. Checked at 390x844 in the browser: with "This signup changed since the page loaded. Reload to see the latest." the bar is 141px and the room and scroll padding follow it; the ends of Build and Settings, Delete signup included, stay clear. Tabbing through Responses (8 sign-ups), Build, and Settings with the refusal showing leaves no focused control behind the bar; with the padding switched off, 6 of Responses' Remove buttons and Build's Add a slot were behind it. Budgets: the signup tabs grow by another 0.13 kB gzip, within budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Review fix. Two Unreleased entries still described a step on the way: #457's ended "A draft still shows Preview and **Publish**.", now **Publish and share**; #390's put Publish's refusal "on a phone under the button in the More actions sheet", now just above Publish and share in the bar, with Close signup's under its buttons in the ⋯ panel or the More actions sheet. #499's line adds the bar's measured room, the scroll padding that keeps focus above the bar, and the Responses tab's new wording with nobody signed up. The other mentions of Publish, More actions and the public link in Unreleased are still true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Tophat: passedThe full run was on
What changes for organizers: a draft now has one blue Publish and share button: top right on a computer, in a bar along the bottom of a phone screen. On a phone, Publish was hidden in the ⋯ sheet before. A draft no longer shows a public link that only says it isn't ready. It says "Only you can see this draft until you publish it." instead. Once published, the same spot says Share, and the link shows how many spots are taken (in the bar on a phone). On a phone, Preview is now in the ⋯ sheet. The screenshots were sent to the session linked below. They include the draft and open headers at both sizes, the phone bar, the refusal above the bar, the ends of Build and Settings with the bar showing, and the closed and archived states. https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H Generated by Claude Code |
The signup layout's imports conflicted: it keeps shareMessage and takes KEEPS_THEIR_SPOT beside spotCounts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings in the QR picture's right-to-left ellipsis and whole emoji, a second tap on Save image ignored, the share message's days and time taken from the open slots, the message box sized from what the browser measures, the help's "Most phones then open their share menu…", and PR 1's KEEPS_THEIR_SPOT, which the closed banner in the signup layout now uses. Conflicts: - CHANGELOG #505: PR 2's line with its new sentence on the open slots' days and time, keeping this branch's final state: "After **Publish and share**", a sheet on a phone with no More actions sheet closing first, and Share "on a phone in the bar at the bottom of the screen". layout.tsx merged on its own, with KEEPS_THEIR_SPOT in the banner and this branch's room for the phone bar; so did the help article's new sentence and spot-counts.ts, which keeps spotsTakenLine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Each title line was a first-strong isolate, so it took its direction from its own first letter: a Hebrew title whose second line starts with "Zoom" drew that line left to right, with its "…" on the right. The direction now comes once from the whole title's first letter (textDirection in qr.ts: digits, spaces and punctuation don't decide it, as in the bidi algorithm), and every line is isolated in it, with U+2067 for right to left or U+2066 for left to right. Unit tests cover Hebrew, Arabic, English, Hebrew after a number, and a Hebrew title whose second line starts with "Zoom". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
With Nov 7 at 9:00 full and Nov 14 at 11:00 with room, the message read "Sat, Nov 7 and Sat, Nov 14 · 3 spots left": the full slot added a day nobody can sign up for and hid the time the other has, though the spots already skip it. dateSpan now takes committedBySlot and reads the slots people can still take, open with a spot left or no limit; when none has room, the open slots; when none is open, every slot. Tests cover a full slot, no limit as room, and every open slot full. The CHANGELOG says so, and a doubled blank line in the doc comment is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings ff75578 and 7a019bb. CHANGELOG.md conflicted in the #505 entry, which this branch had rewritten: kept this branch's entry and replayed PR 2's one changed sentence, on which slots the days and time come from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings 840c0c8 (drop the unused insufficientScopeResponse), 35288fa (count swept rows from the delete itself) and fe290df (next 15.5.27 to 15.5.28). Merged without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Opening the code pushes the message and its buttons below a phone's screen (Copy message ends 167px below a 375x667 one), and the button looked the same open or not, so testers lost Copy message and Email it. While the code shows, the button now has the app's pressed look, a brand border on the brand tint (ink text, as brand on the tint is under 4.5:1), and says Hide QR code, so one tap brings the message back. Its name follows the label and aria-expanded stays. It is half the row either way, so the row doesn't move, and the code stays under the Copy link row. The tests, the walkthrough's phone steps (which now hide the code and find Email it back on screen), the help article, its UI list (hideQrCode) and the CHANGELOG follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
On a phone without a share menu, Copy message and Email it were 148px each in a wrapping row, so the pair stopped at x=320 while Copy link and QR code above filled the row to x=374. They are now a two-column grid, half the row each, lined up with the row above, which also keeps Email it still when Copy message says Copied. A computer keeps its row and its minimum widths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
rate-limit.ts merged cleanly: it keeps this branch's comment on the link beacon's limit and takes #613's sweep counting from the delete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
The Copy link row was a flex row of two flex-basis-0 buttons, and QR code's border made it 2px wider than Copy link, so the row above sat 1px off the new Copy message and Email it grid (174 and 176px against 175 and 175 at 390 wide). It is now the same two-column grid, the large code spanning both columns, so both rows line up exactly and QR code keeps its width as it says Hide QR code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings d33a05a, 4e0e3fd and 1c178c4 (on a phone, QR code looks pressed and says Hide QR code while the code shows; the message's buttons and the Copy link row are two-column grids), and through PR 1, main's #614, #613 and #617 (next 15.5.28). CHANGELOG.md conflicted in the #505 entry, which this branch had rewritten: kept this branch's entry and replayed PR 2's one changed sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings ee0f0d5 (vite 7.3.5 to 7.3.7, a dev dependency). Merged without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings 0923e7e, which merges main's ee0f0d5 (vite 7.3.5 to 7.3.7) and 740e98d (nodemailer 10.0.9 to 10.1.4). package.json and pnpm-lock.yaml merged without conflict beside this branch's lean-qr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Brings 394f323, which carries main's ee0f0d5 (vite 7.3.5 to 7.3.7) and 740e98d (nodemailer 10.0.9 to 10.1.4) up from PR 1. Merged without conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
main's bcb82b3 is #574 squashed onto #571's 24f8689, and its tree is exactly #574's last head, whose tree this branch already has through 394f323. So every change main has is already here, and this merge keeps this branch's tree as it is (the conflicts were #571's and #574's lines that this branch later changed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H
Replaced by a newer auto-merge request
Auto-merge was disabled for another pull request in the stack
…he 5-prompt branch Through the server, telemetry and screen branches. rate-limit.ts keeps this branch's 12 drafts an hour and main's reworded link-beacon comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UW9ZpEu4MWkeqTv2Qe14ks
…ains an empty closed signup's Responses tab The header now records every status change it shows, with whether a button on this page made it. Before, a change from elsewhere left the last one made here in place, so after a close here, a reopen and a close from somewhere else showed the box again. The box now also needs the last change to be this page's. A closed signup's empty Responses tab says only "Nobody signed up.", which counted on the box being there on every visit. It still says that: "closed" in the header already means nobody can sign up. The comment, the test's comment and #578's CHANGELOG line now say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LrYa57fvbqWjZj89iH975K
Closes #499. This is PR 3 of 5 for the "Publish and share" redesign in #498 (row I), stacked on #574 (the share panel), which is stacked on #571. Review it against
claude/relaxed-dijkstra-cpx7zo-2-share-panel.The problem
text-xsbutton, the same size as Preview. On a phone it sat inside the ⋯ sheet.The fix
One primary button, Publish and share
z-50overlays.--phone-bar-h), so the page always leaves room for it, including when a refusal makes it taller. Until it has measured, it falls back to4.5rem + 1pxplus the safe-area inset.scroll-padding-bottomkeeps a focused control above it (WCAG 2.2, 2.4.11).A draft shows no public link. It says "Only you can see this draft until you publish it." instead, on both sizes.
The count, by the link
spotsTakenLineinsrc/lib/spot-counts.ts, beside Ask before closing a signup, and move Close signup into the ⋯ menu #571's question.The phone's ⋯ sheet
The Responses tab of a signup nobody has signed up to now says something true, and no longer uses "signups" for what people sign up for (
docs/writing-help.md):For the budget, the draft line, the phone's link and closed count, and the sheet's Preview are rendered on the server (
HeaderParts.tsx) and passed to the phone header as nodes.From #499's outline:
?error=.Choices this makes
viewport-fit=cover.env(safe-area-inset-bottom)stays 0 on iPhones without it, and the bar still sits above Safari's toolbar.docs/connect-ai-assistant.md. The line ("Only you can see this draft…") is the mock-up's wording. The doc says other members of the same workspace can see drafts. There's no way to invite anyone today.scroll-padding-bottomwhen it scrolls a focused control into view.Tests
spot-counts.test.ts:spotsTakenLine, covering no slots, unlimited slots, one spot, over-full, closed and archived.responses-summary:noResponsesLineby status, and a test that keeps "signup" out of all four lines.SignupHeader.test.tsx: the buttons, draft line and chip for each status.MobileSignupHeader.test.tsx: the bar for each status; the ⋯ sheet's contents for each status; publishing from the bar opens the share sheet; Escape returns focus to the bar's Share; a refusal shows above the button; the bar reports its height and clears it when there's no bar.organizer.spec.tsand the run skill's sample now press Publish and share.mainand After Publish, a share panel to send the link #574:6cbd391, a merge that holds only the conflict resolutions. Main'ssignupIdnow reaches the phone's chip throughPhoneStanding.19365f9. Its only conflict was the CHANGELOG, where every entry is kept. The help article and walkthrough merged by themselves and were checked by hand: they carry Put a duplicated slot right after the original and open it #568's Duplicate step and this PR's draft line, count and bar checks.31f97ec.31f97ec:pnpm lint && pnpm typecheck && pnpm knip && pnpm test(1506),pnpm build && pnpm budgets, and the full chromium e2e suite withCI=1(65).pnpm test:dbwasn't run: no actions, services or schema changed.HELP_SCREENSHOTS=1ran onb45de83.Budgets
Every
/app/signups/[id]tab's first-load JavaScript grows by 0.33 kB gzip against #574. That covers the phone's bar at the bottom (Publish and share, or the count and Share), the bar measuring its own height, and the phone header's new props. Rendering the static parts on the server keeps another 0.2 kB out of the first load.Two budgets are raised, each for code that reached this branch through its base, not for this PR's own growth:
6ce99a6): Record link copies, failed AI drafts and refused publishes #525's link-copy beacon had already taken it to 127.8 kB, and this PR's 0.33 kB puts it 0.12 kB over 128.31f97ec): Put a duplicated slot right after the original and open it #568 adds about 0.33 kB to the Build tab, which takes After Publish, a share panel to send the link #574 to 154.97 kB. This PR's 0.33 kB then puts it 0.29 kB over 155.cbfc37e)31f97ec)/app/signups/[id]/build/app/signups/[id]/responses/app/signups/[id]/settings/app/signups/[id]/edit,/app/signups/[id]/app/signups/[id]/previewDocs
Help.
create-and-publish-a-signupnow covers:The "isn't ready yet" paragraph and
publishOnPhoneare gone, andpublished.pngwas re-taken with the count (605×110).docs/writing-help.md's phone example named Publish signup and now names the bar.CHANGELOG: under Changed, with the earlier stack lines corrected as above.
Legal: no change. Nothing stored, sent or set as a cookie changes.
The stack
🤖 Generated with Claude Code
https://claude.ai/code/session_01HoJtwsyrp6YC4DU9JAnc7H