feat(localize): halo Submit once every object is accepted - #380
Merged
Conversation
The localize gate opens silently. Submit sits in the rail footer under every object row, while the last accept usually happens up in the editor — so the moment the alert becomes shippable is exactly the moment the annotator is looking somewhere else, and the button just quietly stops being grey. Pulse the existing `animate-pine-glow` halo on it instead. Same keyframe the add-object flow puts on its solid-pine "Create object" CTA, so "the work is done, move forward" reads as one signal rather than two. Keyed on `!submitBlocked`, not on `allObjectsAccepted`: an alert whose boxes are all drawn but whose sibling is still marked unsure stays blocked, and haloing that would pulse a button that cannot be clicked. Dropped again on `isPending` so the halo and the submit spinner never run at once.
Review caught that the halo was erasing the focus indicator. The focus ring is a box-shadow, `pine-glow` animates box-shadow, and animation declarations outrank normal author ones — so on a haloed button the ring never painted, and `focus:outline-none` had already removed the native fallback. A keyboard user tabbing to the primary action saw nothing. `focus-visible:animate-none` stops the halo on keyboard focus and hands the ring back. Nothing is lost: the halo's job is to get you to the button, and you cannot focus it without having found it. Also add the transition test the other three were missing. They only ever observed a gate that was already open, or already shut, at arrival — an implementation that decided the halo once at mount would have passed all of them. The new one accepts both objects through the CTA bar and watches the halo arrive with the enablement. And correct the comment's claim that the halo drops "the moment the click lands": on the soft-confirm path handleSubmitClick opens the dialog and returns without mutating, so isPending stays false.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Pulses a green halo on the Localize Submit button the moment the alert becomes submittable.
Why
The localize gate opens silently. Submit sits in the rail footer below every object row, while the last accept usually happens up in the editor — so the moment the alert becomes shippable is exactly the moment the annotator is looking somewhere else. The button just quietly stops being grey, and nothing invites the click.
How
One conditional class on the existing button:
No new CSS.
animate-pine-glowalready exists intailwind.config.jsand already rings the solid-pine "Create object" CTA inAddObjectOverlay.tsx, so this reuses the established "work is done, move forward" signal rather than inventing a second one.The condition is the substantive decision. Keyed on
!submitBlocked, not onallObjectsAccepted— those diverge. An alert can have every box drawn while a sibling lane is still marked unsure, which keeps Submit disabled; haloing on the boxes alone would pulse a button that cannot be clicked.focus-visible:animate-noneis load-bearing, not decoration. Tailwind's focus ring is abox-shadow, the halo animatesbox-shadow, and animation declarations outrank normal author ones — so a running halo erases the focus ring, andfocus:outline-nonehas already removed the native fallback. Without this, a keyboard user tabbing to the page's primary action would see no focus indicator at all. Confirmed against the generated CSS: the ring is emitted asbox-shadow, the variant is emitted, and it wins on both specificity (0,2,0 vs 0,1,0) and source order.Testing
Four assertions in the existing
Submit alertblock, TDD'd — the first was watched failing before implementing (button reached enabled, class absent).Every assertion was mutation-tested, since both "does not have class" checks and already-settled-state checks are easy places to write something vacuous:
allObjectsAcceptedFull frontend suite: 1474 passed / 108 files.
type-check,lint --max-warnings 0,format:checkall clean.Verified in the browser against the local stack — the halo paints and pulses on the real button, which a class assertion alone cannot prove.
Known follow-up (out of scope)
AddObjectOverlay.tsxhas the identical focus-ring bug —animate-pine-glowwithfocus:outline-none focus:ring-2and nofocus-visible:animate-none. Pre-existing; left alone deliberately. One-class fix whenever someone wants it.