SIGN IN SIGN UP

button: Fix loading buttons reacting to the pointer and dimming unevenly (#2668)

Two problems with the loading state, both of which made it behave
differently from what `loading` implies.

## 1. A loading button still reacted to the pointer

It highlighted on hover, showed the active style when pressed, and kept
the pointer cursor on the link and text variants — even though clicking
it does nothing.

`clickable()` and `hoverable()` already excluded `loading`, but they
only gate the `on_click` and `on_hover` listeners. Three other places
were keyed off `disabled` alone:

- the `hover` and `active` styling
- the `cursor_pointer` of link and text variants
- the `mouse_down` handler, whose comment says it exists to keep a
dropdown menu from opening — which applies just as much to loading

Add `Button::interactive()` (`!(disabled || loading)`) and use it in all
of those places, so loading is as inert as disabled. `clickable()` and
`hoverable()` are now that plus their listener check.

The normal background and border stay outside the check: only the
`hover` and `active` blocks are gated, so a loading button keeps its own
colors instead of falling back to an unstyled background.

Loading deliberately keeps its own look rather than taking the disabled
styling: it is a temporary state caused by the click the user just made,
and graying the button out would weaken that feedback.

## 2. Only the Primary variant looked like it was loading

The loading styling faded `bg`, `border` and `fg` one by one, which only
lands on variants that have a background to begin with. `Ghost`, `Link`
and `Text` are transparent, so an alpha on their background changes
nothing, and the light background of `Default` barely shifts once
blended with the page. Only a saturated fill with white text showed both
layers, which is why Primary dimmed and the rest looked untouched.

Fade the element instead — `Styled::opacity` applies to the whole
subtree — so every variant dims by the same amount. Net 5 lines shorter.

It sits after the `disabled` branch and before `refine_style`, so
disabled wins when both are set, and a caller-supplied style can still
override it.

## Notes

`DropdownButton` is unaffected — it already passes
`disabled(self.disabled || self.loading)` to its inner button.
`ToggleButton` has no loading state. Spinner behavior is unchanged.

## Testing

`test_button_loading_is_not_interactive` covers the four `interactive`
combinations, plus a loading button with an `on_hover` listener still
not being hoverable. Verified every variant in the story gallery with
the loading checkbox.

AI generated: the patch was written with Claude Code and reviewed by me.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
J
Jason Lee committed
424f24577bc7ac939f9cfc1331f470186bf19c4c
Parent: d0c469e
Committed by GitHub <noreply@github.com> on 8/10/2026, 8:32:47 AM