🎨 style: Outline the In-Flight Steer Bubble and Move the Bolt Inline

The filled bubble read as a settled message. An outline reads as
provisional, which is what an in-flight steer is, and separates it from
the composer surface behind it.

- Border + bubble keeps the composer's rounded-3xl radius so it reads as
  anchored to the input rather than floating over it. Border stays
  NEUTRAL: the failed-steer row already owns a colored (red) border, so
  a colored outline on the happy path would read as a warning.
- The Zap moves inside the bubble, left of the text, where it prefixes
  the words as a status label instead of competing with cancel for the
  right edge. items-start pins it to the first line when text wraps.
- Cancel drops plain `opacity-0` for `[@media(hover:hover)]:opacity-0`,
  matching SteerPart's info affordance: a hover-revealed control is
  unreachable on touch until a first tap (the #14272 pattern).
This commit is contained in:
Danny Avila 2026-07-16 10:19:47 -04:00
parent 8135c808df
commit 9594ee7146
2 changed files with 30 additions and 10 deletions

View file

@ -87,26 +87,34 @@ const InFlightSteer = memo(function InFlightSteer({
<div className="flex max-w-full items-center gap-1.5">
<div
className={cn(
'markdown prose message-content dark:prose-invert light min-w-0 break-words',
'rounded-3xl bg-surface-secondary px-4 py-2 text-sm text-text-primary dark:text-gray-20',
!enableUserMsgMarkdown && 'whitespace-pre-wrap',
/* Outlined, not just filled: an in-flight steer is provisional
* the fill alone reads as a settled message. */
'flex min-w-0 items-start gap-2 rounded-3xl border border-border-medium',
'bg-surface-secondary py-2 pl-3 pr-4 text-sm text-text-primary',
sending && 'opacity-70',
)}
>
{enableUserMsgMarkdown ? <MarkdownLite content={steer.text} /> : steer.text}
<Zap className="mt-1 h-3.5 w-3.5 shrink-0 text-amber-500" aria-hidden="true" />
<span className="sr-only">{localize('com_ui_steer_in_flight')}</span>
<div
className={cn(
'markdown prose message-content dark:prose-invert light min-w-0 break-words',
'dark:text-gray-20',
!enableUserMsgMarkdown && 'whitespace-pre-wrap',
)}
>
{enableUserMsgMarkdown ? <MarkdownLite content={steer.text} /> : steer.text}
</div>
</div>
<Zap
className={cn('h-3.5 w-3.5 shrink-0 text-amber-500', sending && 'opacity-50')}
aria-hidden="true"
/>
<span className="sr-only">{localize('com_ui_steer_in_flight')}</span>
{!sending && (
/* Hidden-at-rest only on hover-capable pointers: a hover-revealed
* control is unreachable on touch until a first tap. */
<button
type="button"
aria-label={localize('com_ui_steer_cancel')}
onClick={() => cancelSteer(steer)}
data-testid="steer-cancel"
className="shrink-0 rounded-full p-1 text-text-secondary opacity-0 transition-opacity duration-200 hover:bg-surface-tertiary hover:text-text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-xheavy group-hover:opacity-100"
className="shrink-0 rounded-full p-1 text-text-secondary transition-opacity duration-200 hover:bg-surface-tertiary hover:text-text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-xheavy group-hover:opacity-100 [@media(hover:hover)]:opacity-0"
>
<X className="h-3.5 w-3.5" aria-hidden="true" />
</button>

View file

@ -87,6 +87,18 @@ describe('InFlightSteers', () => {
expect(screen.queryByTestId('in-flight-steers')).toBeNull();
});
it('keeps cancel reachable on touch, hover-revealed on hover-capable pointers', () => {
renderSteers([
{ steerId: 's-ack', text: 'waiting on boundary', status: 'pending', createdAt: 1 },
]);
// A plain `opacity-0` reveal would make the bubble hover-dependent, so on
// touch the X would need a first tap to appear (see the #14272 pattern).
const cancel = screen.getByTestId('steer-cancel');
expect(cancel.className).toContain('[@media(hover:hover)]:opacity-0');
expect(cancel.className).toContain('group-hover:opacity-100');
expect(cancel.className).toContain('focus-visible:opacity-100');
});
it('only offers cancel once the steer is acknowledged', () => {
renderSteers([
{ steerId: 'local-1', text: 'still posting', status: 'sending', createdAt: 1 },