Two geometry problems around hover chrome in the Iris chat. Both predate #367 and were explicitly left out of it, but they share a root: chrome that occupies layout space (or overhangs it) instead of being reserved once.
#367 fixed this class of problem for the ordinary message bubble by giving every message one always-mounted footer row that recedes by colour. These two surfaces were not converted.
1. The episode timeline shifts the list on hover
EpisodeTimeline.module.css:80-94 animates .foot from max-height: 0; margin-top: 0 to max-height: 28px; margin-top: 6px. That is real flow, not opacity, so hovering any row grows it and pushes every following row and every following list item down. Measured at roughly 34px per hovered row.
The current behaviour is deliberate, and any fix has to preserve the intent documented at EpisodeTimeline.module.css:77-79: the transition delay sits on the resting state so the collapse lingers about 0.4s, giving the pointer a grace window to reach Dismiss.
Two candidate approaches, with a real trade-off:
Worth deciding deliberately rather than picking the first one that compiles. Note the last row already pins its foot open via .footPersistent when it carries offer or Dismiss buttons, so only the non-last rows actually animate.
2. A failed proactive offer overlaps its error row
MessageBubble.tsx:97:
const showOfferButtons = isProactive && !grouped && !!offer && !offer.answered && !!onOfferAnswer;
Unlike showDismiss one line above, this does not exclude isFailed. So a proactive offer whose send failed renders both its floating action bar and its error footer. The bar is absolutely positioned at bottom: -14px (MessageBubble.module.css, .actionRow) while the error footer starts only .bubbleColumn's 4px gap below the bubble, so the two share roughly 10px. It is invisible at rest because the bar sits at opacity: 0, and appears on hover as the bar covering the error text.
Root fix: gate on !isFailed, matching showDismiss. Answering an offer that never reached the server is not a meaningful action anyway, so suppressing the bar is correct behaviour, not just a layout workaround.
Acceptance
- Hovering any row of an episode timeline does not move any other row or any following message.
- Whatever approach is chosen for item 1, the pointer can still travel from a row to its Dismiss button without the target vanishing.
- A failed proactive offer renders no action bar, and its error footer is fully readable in every state.
- Tests cover both: a timeline row's height is stable across hover, and a failed offer renders no offer buttons.
Two geometry problems around hover chrome in the Iris chat. Both predate #367 and were explicitly left out of it, but they share a root: chrome that occupies layout space (or overhangs it) instead of being reserved once.
#367 fixed this class of problem for the ordinary message bubble by giving every message one always-mounted footer row that recedes by colour. These two surfaces were not converted.
1. The episode timeline shifts the list on hover
EpisodeTimeline.module.css:80-94animates.footfrommax-height: 0; margin-top: 0tomax-height: 28px; margin-top: 6px. That is real flow, not opacity, so hovering any row grows it and pushes every following row and every following list item down. Measured at roughly 34px per hovered row.The current behaviour is deliberate, and any fix has to preserve the intent documented at
EpisodeTimeline.module.css:77-79: the transition delay sits on the resting state so the collapse lingers about 0.4s, giving the pointer a grace window to reach Dismiss.Two candidate approaches, with a real trade-off:
MessageBubble): the row never changes height, so nothing reflows and the grace window becomes unnecessary, since nothing collapses. Cost: every timeline row gets permanently taller by the height of its timestamp line, which is exactly the "reserve dead space" pattern fix(iris-chat): stop hover chrome reserving dead space in the message list (#366) #367 removed. In a card with several rows that adds up..actionRow): no height change and no extra resting height, but it then overhangs the next row and needs its own clearance, which is the same problem as item 2 below.Worth deciding deliberately rather than picking the first one that compiles. Note the last row already pins its foot open via
.footPersistentwhen it carries offer or Dismiss buttons, so only the non-last rows actually animate.2. A failed proactive offer overlaps its error row
MessageBubble.tsx:97:Unlike
showDismissone line above, this does not excludeisFailed. So a proactive offer whose send failed renders both its floating action bar and its error footer. The bar is absolutely positioned atbottom: -14px(MessageBubble.module.css,.actionRow) while the error footer starts only.bubbleColumn's 4px gap below the bubble, so the two share roughly 10px. It is invisible at rest because the bar sits atopacity: 0, and appears on hover as the bar covering the error text.Root fix: gate on
!isFailed, matchingshowDismiss. Answering an offer that never reached the server is not a meaningful action anyway, so suppressing the bar is correct behaviour, not just a layout workaround.Acceptance