Root cause: soma/scroll-area-scrollbar.svelte gated the entire <div> on
`state.shouldShow`. For type='hover' / 'scroll', `shouldShow` starts
false, so the bar never reached the DOM. With no DOM node:
- bind:this never fired → scrollbar's `ref` stayed null
- `requestFrame(cb, ref.current)` ran with a null element, so the
`mounted` flag never flipped
- the hover / drag listeners had no element to attach to
- the recipe's `data-state` opacity transition had no element to
animate
- `shouldShow` could therefore never become true → deadlock
Fix — split the concern:
- `shouldMount` (new derived) — true whenever the axis overflows OR
type='always'. Drives the `{#if}` in the part component.
- `shouldShow` — drives `data-state="visible|hidden"`. The recipe
transitions opacity.
The bar now lives in the DOM whenever it could ever be needed, fades
in/out via CSS, and stays a stable hit target for hover / click /
drag. Dropped the `mounted` flag and the requestFrame mount-frame
scheduling — they only existed to suppress a flash that the opacity
transition handles cleanly.
Also dropped the inline `border-radius: inherit` on the thumb so the
recipe's `--scroll-area-thumb-radius` (the new `radius` prop) wins.
Without this the prop was silently overridden to `0px`.
Test updates:
- removed `cancelFrame` assertion (no mount-frame to cancel)
- added `shouldMount` assertion
Verified in browser at /uix/components/scroll-area:
- type='hover' + vertical: bar mounts hidden, fades in on root hover,
fades out after delay
- type='scroll' + horizontal: bar mounts hidden, fades in on scroll,
thumb tracks scrollLeft, fades out after 600ms
- type='always' + both: both bars + corner visible at all times
- size=lg, radius=full: 12px-wide bar, fully rounded thumb
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>