Measure the corner the kitten sits in, not the kitten

The overlap test read the badge's own box, which bobs, shrinks 10% when it
yields, and grows 4% on hover — so the mascot's answer to "is a card in my way"
depended on what it was currently doing. Against the live build, with three
cards up and no bubble, it flipped between drawn widths of 293 and 264 on its
own: the 0.9 yield scale, cycling.

Measure a probe span instead. It sits exactly where the badge sits, never
animates, and moves the size variable to .petal-corner so both are sized from
the same number. Nothing the mascot does can now change what it yields to.

Cards also settle a pixel from the corner routinely — a rail re-pack, a resize,
a browser bar appearing — so hold a yield until the card has retreated 24px
rather than deciding on the exact edge.

Verified in a real browser over CDP: seven overlap depths against the mascot's
top edge, each settling once and holding, drawn width steady where it used to
swing.

Claude-Session: https://claude.ai/code/session_01GJHNvirh7Hzhc9RL3HAvz7
This commit is contained in:
prosolis
2026-07-28 22:21:33 -07:00
parent 5cd3aeabde
commit f82f2b589d
3 changed files with 55 additions and 23 deletions
@@ -88,13 +88,18 @@ export function PetalCompanion({
const [pickerOpen, setPickerOpen] = useState(false)
const rootRef = useRef<HTMLDivElement>(null)
const badgeRef = useRef<HTMLButtonElement>(null)
// Stand-in for the badge's corner, used only for measuring — see
// useCardOverlap. It sits exactly where the badge sits but never bobs, shrinks
// or hovers, so what the mascot yields to can't depend on whether it is
// currently yielding.
const probeRef = useRef<HTMLSpanElement>(null)
// When suggestion cards stack down into the corner, the kitten fades to
// translucent and shrinks a step so the card stays readable and clickable.
// It wakes back up whenever it has something to say (bubble) or is being
// interacted with (picker open) — except under an open History or Garden
// panel, where even a cheer would cover the controls she just reached for.
const crowded = useCardOverlap(badgeRef)
const crowded = useCardOverlap(probeRef)
const faded = crowded.modal || (crowded.cards && !pickerOpen && !bubble)
// Awake companions (no sleeping clip) don't visibly nap — when the engine
@@ -135,7 +140,7 @@ export function PetalCompanion({
return (
<div
ref={rootRef}
className="pointer-events-none fixed bottom-4 right-4 z-40 flex flex-col items-end gap-2"
className="petal-corner pointer-events-none fixed bottom-4 right-4 z-40 flex flex-col items-end gap-2"
>
{pickerOpen && (
<div
@@ -251,6 +256,16 @@ export function PetalCompanion({
</div>
)}
<span
ref={probeRef}
aria-hidden
className="pointer-events-none absolute bottom-0 right-0"
style={{
width: 'var(--petal-companion-size, 9rem)',
height: 'var(--petal-companion-size, 9rem)',
}}
/>
<button
ref={badgeRef}
type="button"