mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-29 05:20:49 +00:00
♿ fix: Restore WCAG AA Contrast for Text Tokens & Hide Edit Action While Streaming (#14677)
* ♿ fix: Restore WCAG AA Contrast for Text Tokens & Hide Edit Action While Streaming Fixes the unreadable composer placeholder and the edit pencil that appears on hover mid-generation, plus the sibling token failures found while tracing the root cause. Placeholder: #13879 moved the composer from `dark:placeholder-white/60` to the semantic `placeholder:text-text-tertiary`, but `--text-tertiary` was `var(--gray-500)` in *both* themes, and #595959 is a dark gray. Dark mode fell from 5.90:1 to 1.91:1. Fixed at the token (dark -> gray-400, 4.56:1) rather than the call site: the token has 99 usages and was failing at 1.91-2.77:1 on every dark surface. The .gizmo dark theme already uses a light gray (#999999) for the same token, so only the default dark theme carried the inverted value. Two more instances of the same "token never tuned per theme" bug: - `--text-warning` was amber-500 in both themes: 2.15:1 in light across 13 real warning strings. Now amber-700 (5.02:1). - Light `status-{success,warning,error}` on their own `-subtle` fill measured 3.58 / 3.07 / 4.41 -- the exact pairing Alert, Badge, Tag and Chip use for every status variant. Bumped to the 700 ramp (5.21 / 4.84 / 5.91). Solid `bg-status-*` is only used for dots, so nothing renders text on it. Edit action: `hideEditButton` already covers `isSubmitting` and the button got `isVisible={false}` -> `opacity-0`, but `group-hover:opacity-100` (0,2,0) outranks bare `opacity-0` (0,1,0), so hovering the row revealed a disabled pencil. The reveal classes are now gated on `isVisible`, with `pointer-events-none` so the hidden button is inert. Both token sources of truth (style.css and themes/*.ts) were updated and verified in sync across all 67 tokens. Tests: new HoverButtons spec covers both hover states; semanticTokens.spec.ts gains a contrast guardrail over text tokens x surfaces and each status hue against its subtle fill, verified to fail on the original values. applyTheme.spec.ts now derives its expectation from the theme object instead of pinning a hex, so retuning a hue no longer breaks an unrelated plumbing test. * 🔤 style: Sort imports in HoverButtons spec CI's changed-files import-order check flagged the new spec; the previous commit bypassed the lint-staged hook that would have caught it.
This commit is contained in:
parent
5ff46d8c67
commit
0db511fee8
7 changed files with 182 additions and 12 deletions
|
|
@ -96,8 +96,9 @@ const HoverButton = memo(
|
|||
'hover:text-text-primary hover:bg-surface-hover',
|
||||
'group-hover:visible group-focus-within:visible group-[.final-completion]:visible',
|
||||
!isLast &&
|
||||
isVisible &&
|
||||
'group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:hover)]:opacity-0',
|
||||
!isVisible && 'opacity-0',
|
||||
!isVisible && 'pointer-events-none opacity-0',
|
||||
'focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:outline-none',
|
||||
isActive && isVisible && 'active text-text-primary bg-surface-hover',
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { RecoilRoot, type MutableSnapshot } from 'recoil';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { EModelEndpoint, type TConversation, type TMessage } from 'librechat-data-provider';
|
||||
import HoverButtons from '~/components/Chat/Messages/HoverButtons';
|
||||
import store from '~/store';
|
||||
|
||||
const conversation = {
|
||||
conversationId: 'convo-1',
|
||||
endpoint: EModelEndpoint.agents,
|
||||
title: 'Test',
|
||||
} as TConversation;
|
||||
|
||||
const userMessage = {
|
||||
messageId: 'user-1',
|
||||
conversationId: 'convo-1',
|
||||
parentMessageId: null,
|
||||
isCreatedByUser: true,
|
||||
text: 'tell me a long story',
|
||||
} as TMessage;
|
||||
|
||||
function renderHoverButtons(isSubmitting: boolean) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
const initializeState = ({ set }: MutableSnapshot) => set(store.textToSpeech, false);
|
||||
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RecoilRoot initializeState={initializeState}>
|
||||
<MemoryRouter>
|
||||
<HoverButtons
|
||||
index={0}
|
||||
isLast={false}
|
||||
isEditing={false}
|
||||
message={userMessage}
|
||||
conversation={conversation}
|
||||
isSubmitting={isSubmitting}
|
||||
enterEdit={jest.fn()}
|
||||
regenerate={jest.fn()}
|
||||
handleContinue={jest.fn()}
|
||||
copyToClipboard={jest.fn()}
|
||||
latestMessageId="assistant-1"
|
||||
/>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const editButton = container.querySelector<HTMLButtonElement>(`#edit-${userMessage.messageId}`);
|
||||
if (!editButton) {
|
||||
throw new Error('edit button not rendered');
|
||||
}
|
||||
return editButton;
|
||||
}
|
||||
|
||||
describe('HoverButtons edit affordance', () => {
|
||||
it('stays hidden on row hover while a generation is in flight', () => {
|
||||
const editButton = renderHoverButtons(true);
|
||||
|
||||
expect(editButton).toBeDisabled();
|
||||
expect(editButton).toHaveClass('opacity-0', 'pointer-events-none');
|
||||
expect(editButton.className).not.toMatch(/group-hover:opacity-100/);
|
||||
expect(editButton.className).not.toMatch(/group-focus-within:opacity-100/);
|
||||
});
|
||||
|
||||
it('reveals on row hover once the generation settles', () => {
|
||||
const editButton = renderHoverButtons(false);
|
||||
|
||||
expect(editButton).toBeEnabled();
|
||||
expect(editButton).toHaveClass('group-hover:opacity-100');
|
||||
expect(editButton).not.toHaveClass('pointer-events-none', 'opacity-0');
|
||||
});
|
||||
});
|
||||
|
|
@ -151,7 +151,7 @@ html {
|
|||
--text-secondary: var(--gray-600);
|
||||
--text-secondary-alt: var(--gray-500);
|
||||
--text-tertiary: var(--gray-500);
|
||||
--text-warning: var(--amber-500);
|
||||
--text-warning: var(--amber-700);
|
||||
--text-destructive: var(--red-600);
|
||||
--link: var(--blue-600);
|
||||
--link-hover: var(--blue-700);
|
||||
|
|
@ -190,7 +190,7 @@ html {
|
|||
--border-xheavy: var(--gray-500);
|
||||
--border-xheavy-alpha: 1;
|
||||
--border-destructive: var(--red-600);
|
||||
--status-success: var(--green-600);
|
||||
--status-success: var(--green-700);
|
||||
--status-success-subtle: var(--green-50);
|
||||
--status-success-border: var(--green-300);
|
||||
--status-success-strong: 2 133 94;
|
||||
|
|
@ -198,11 +198,11 @@ html {
|
|||
--status-info-subtle: var(--blue-50);
|
||||
--status-info-border: var(--blue-300);
|
||||
--status-info-strong: var(--gray-500);
|
||||
--status-warning: var(--amber-600);
|
||||
--status-warning: var(--amber-700);
|
||||
--status-warning-subtle: var(--amber-50);
|
||||
--status-warning-border: var(--amber-300);
|
||||
--status-warning-strong: 199 82 9;
|
||||
--status-error: var(--red-600);
|
||||
--status-error: var(--red-700);
|
||||
--status-error-subtle: var(--red-50);
|
||||
--status-error-border: var(--red-300);
|
||||
--status-error-strong: 224 47 31;
|
||||
|
|
@ -226,7 +226,7 @@ html {
|
|||
--text-primary: var(--gray-100);
|
||||
--text-secondary: var(--gray-300);
|
||||
--text-secondary-alt: var(--gray-400);
|
||||
--text-tertiary: var(--gray-500);
|
||||
--text-tertiary: var(--gray-400);
|
||||
--text-warning: var(--amber-500);
|
||||
--text-destructive: var(--status-error);
|
||||
--link: var(--blue-400);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue