mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-22 01:53:33 +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="flex max-w-full items-center gap-1.5">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'markdown prose message-content dark:prose-invert light min-w-0 break-words',
|
/* Outlined, not just filled: an in-flight steer is provisional —
|
||||||
'rounded-3xl bg-surface-secondary px-4 py-2 text-sm text-text-primary dark:text-gray-20',
|
* the fill alone reads as a settled message. */
|
||||||
!enableUserMsgMarkdown && 'whitespace-pre-wrap',
|
'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',
|
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>
|
</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 && (
|
{!sending && (
|
||||||
|
/* Hidden-at-rest only on hover-capable pointers: a hover-revealed
|
||||||
|
* control is unreachable on touch until a first tap. */
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={localize('com_ui_steer_cancel')}
|
aria-label={localize('com_ui_steer_cancel')}
|
||||||
onClick={() => cancelSteer(steer)}
|
onClick={() => cancelSteer(steer)}
|
||||||
data-testid="steer-cancel"
|
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" />
|
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,18 @@ describe('InFlightSteers', () => {
|
||||||
expect(screen.queryByTestId('in-flight-steers')).toBeNull();
|
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', () => {
|
it('only offers cancel once the steer is acknowledged', () => {
|
||||||
renderSteers([
|
renderSteers([
|
||||||
{ steerId: 'local-1', text: 'still posting', status: 'sending', createdAt: 1 },
|
{ steerId: 'local-1', text: 'still posting', status: 'sending', createdAt: 1 },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue