mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🎨 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:
parent
8135c808df
commit
9594ee7146
2 changed files with 30 additions and 10 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue