🎛️ feat: Adopt Composer Density Tokens (#14730)

This commit is contained in:
Danny Avila 2026-08-10 22:57:33 -04:00 committed by GitHub
parent c93609cb82
commit d89b11d34d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 82 additions and 25 deletions

View file

@ -1,10 +1,15 @@
import { memo, useCallback, useRef } from 'react';
import { MicOff } from 'lucide-react';
import { Button, useToastContext, TooltipAnchor, ListeningIcon, Spinner } from '@librechat/client';
import {
IconButton,
useToastContext,
TooltipAnchor,
ListeningIcon,
Spinner,
} from '@librechat/client';
import { useLocalize, useSpeechToText, useGetAudioSettings } from '~/hooks';
import { globalAudioId, type TAskFunction } from '~/common';
import { useChatFormContext } from '~/Providers';
import { cn } from '~/utils';
const isExternalSTT = (speechToTextEndpoint: string) => speechToTextEndpoint === 'external';
export default memo(function AudioRecorder({
@ -107,19 +112,20 @@ export default memo(function AudioRecorder({
<TooltipAnchor
description={localize('com_ui_use_micrphone')}
render={
<Button
<IconButton
id="audio-recorder"
type="button"
variant="ghost"
size="icon"
aria-label={localize('com_ui_use_micrphone')}
size="theme"
shape="theme"
label={localize('com_ui_use_micrphone')}
onClick={isListening === true ? handleStopRecording : handleStartRecording}
disabled={disabled}
className={cn('size-9 rounded-full p-1 hover:bg-surface-composer-hover')}
className="p-1 hover:bg-surface-composer-hover"
aria-pressed={isListening}
>
{renderIcon()}
</Button>
</IconButton>
}
/>
);

View file

@ -190,7 +190,7 @@ const DuringRunSendButton = React.memo(
id="during-run-send-button"
disabled={!content || props.disabled === true}
className={cn(
'rounded-full bg-text-primary p-1.5 text-text-primary outline-offset-4 transition-all duration-200 disabled:cursor-not-allowed disabled:text-text-secondary disabled:opacity-10',
'size-theme-control rounded-theme-control-round bg-text-primary p-theme-compact text-text-primary outline-offset-4 transition-all duration-theme-normal disabled:cursor-not-allowed disabled:text-text-secondary disabled:opacity-10',
)}
data-testid="during-run-send-button"
data-during-run-action={primary}

View file

@ -45,7 +45,7 @@ const AttachFile = ({
aria-keyshortcuts={ariaKey}
disabled={isUploadDisabled}
className={cn(
'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-composer-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:ring-opacity-50',
'flex size-theme-control items-center justify-center rounded-theme-control-round p-1 transition-colors duration-theme-fast hover:bg-surface-composer-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:ring-opacity-50',
)}
onKeyDownCapture={(e) => {
if (!inputRef.current) {

View file

@ -307,7 +307,7 @@ const AttachFileMenu = ({
aria-label="Attach File Options"
aria-keyshortcuts={uploadFileAriaKey}
className={cn(
'flex size-9 items-center justify-center rounded-full p-1 hover:bg-surface-composer-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:ring-opacity-50',
'flex size-theme-control items-center justify-center rounded-theme-control-round p-1 transition-colors duration-theme-fast hover:bg-surface-composer-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:ring-opacity-50',
isPopoverActive && 'bg-surface-composer-hover',
)}
>

View file

@ -51,8 +51,8 @@ const InterruptSteerButton = React.memo((props: InterruptSteerButtonProps) => {
disabled={disabled}
onClick={onClick}
className={cn(
'flex size-9 items-center justify-center rounded-full border border-border-light',
'text-text-secondary transition-colors duration-200',
'flex size-theme-control items-center justify-center rounded-theme-control-round border border-border-light',
'text-text-secondary transition-colors duration-theme-normal',
'hover:bg-surface-composer-hover hover:text-text-primary',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-xheavy',
'disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent',

View file

@ -1,8 +1,8 @@
import React, { memo, useMemo } from 'react';
import * as Ariakit from '@ariakit/react';
import { ChevronDown } from 'lucide-react';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { TooltipAnchor } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import MCPServerMenuItem from '~/components/MCP/MCPServerMenuItem';
import MCPConfigDialog from '~/components/MCP/MCPConfigDialog';
import StackedMCPIcons from '~/components/MCP/StackedMCPIcons';
@ -71,11 +71,11 @@ function MCPSelectContent() {
render={
<Ariakit.MenuButton
className={cn(
'group relative inline-flex items-center justify-center gap-1.5',
'group relative inline-flex items-center justify-center gap-theme-compact',
'border border-border-medium text-sm font-medium transition-all',
'h-9 min-w-9 rounded-full bg-transparent px-2.5 shadow-sm',
'h-theme-control min-w-theme-control rounded-theme-control-round bg-transparent px-2.5 shadow-sm',
'hover:bg-surface-hover hover:shadow-md active:shadow-inner',
'md:w-fit md:justify-start md:px-3',
'md:w-fit md:justify-start md:px-theme-normal',
isOpen && 'bg-surface-hover',
)}
/>

View file

@ -1,7 +1,7 @@
import React, { forwardRef } from 'react';
import { useWatch } from 'react-hook-form';
import type { Control } from 'react-hook-form';
import { SendIcon, TooltipAnchor } from '@librechat/client';
import type { Control } from 'react-hook-form';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -23,7 +23,7 @@ const SubmitButton = React.memo(
id="send-button"
disabled={props.disabled}
className={cn(
'rounded-full bg-text-primary p-1.5 text-text-primary outline-offset-4 transition-all duration-200 disabled:cursor-not-allowed disabled:text-text-secondary disabled:opacity-10',
'size-theme-control rounded-theme-control-round bg-text-primary p-theme-compact text-text-primary outline-offset-4 transition-all duration-theme-normal disabled:cursor-not-allowed disabled:text-text-secondary disabled:opacity-10',
)}
data-testid="send-button"
type="submit"

View file

@ -20,7 +20,7 @@ export default memo(function StopButton({
type="button"
data-testid="stop-generation-button"
className={cn(
'rounded-full bg-text-primary p-1.5 text-text-primary outline-offset-4 transition-all duration-200 disabled:cursor-not-allowed disabled:text-text-secondary disabled:opacity-10',
'size-theme-control rounded-theme-control-round bg-text-primary p-theme-compact text-text-primary outline-offset-4 transition-all duration-theme-normal disabled:cursor-not-allowed disabled:text-text-secondary disabled:opacity-10',
)}
aria-label={localize('com_nav_stop_generating')}
onClick={(e) => {

View file

@ -72,7 +72,7 @@ function TokenUsageIndicator({
aria-label={ariaLabel}
aria-haspopup="dialog"
className={cn(
'flex size-9 items-center justify-center rounded-full p-1 transition-colors',
'flex size-theme-control items-center justify-center rounded-theme-control-round transition-colors',
'hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary',
'duration-300 animate-in fade-in zoom-in-95',
)}

View file

@ -379,7 +379,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
id="tools-dropdown-button"
aria-label="Tools Options"
className={cn(
'flex size-9 items-center justify-center rounded-full p-1 hover:bg-surface-composer-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:ring-opacity-50',
'flex size-theme-control items-center justify-center rounded-theme-control-round p-1 transition-colors duration-theme-fast hover:bg-surface-composer-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:ring-opacity-50',
isPopoverActive && 'bg-surface-composer-hover',
)}
>

View file

@ -67,7 +67,12 @@ describe('MCPSelect', () => {
it('renders the menu button', () => {
render(<MCPSelect />);
expect(screen.getByRole('button', { name: /MCP Servers/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /MCP Servers/i })).toHaveClass(
'h-theme-control',
'min-w-theme-control',
'rounded-theme-control-round',
'gap-theme-compact',
);
});
it('opens menu on button click and shows server items', async () => {

View file

@ -0,0 +1,29 @@
import { join } from 'node:path';
import { readFileSync } from 'node:fs';
const inputRoot = join(__dirname, '..');
const source = (file: string): string => readFileSync(join(inputRoot, file), 'utf8');
const themedControls = [
['SendButton.tsx', ['size-theme-control', 'rounded-theme-control-round', 'p-theme-compact']],
['StopButton.tsx', ['size-theme-control', 'rounded-theme-control-round', 'p-theme-compact']],
[
'DuringRunSendButton.tsx',
['size-theme-control', 'rounded-theme-control-round', 'p-theme-compact'],
],
['InterruptSteerButton.tsx', ['size-theme-control', 'rounded-theme-control-round']],
['AudioRecorder.tsx', ['size="theme"', 'shape="theme"']],
['MCPSelect.tsx', ['h-theme-control', 'rounded-theme-control-round']],
['TokenUsage/index.tsx', ['size-theme-control', 'rounded-theme-control-round']],
['Files/AttachFile.tsx', ['size-theme-control', 'rounded-theme-control-round']],
['Files/AttachFileMenu.tsx', ['size-theme-control', 'rounded-theme-control-round']],
['ToolsDropdown.tsx', ['size-theme-control', 'rounded-theme-control-round']],
] as const;
describe('Composer appearance tokens', () => {
it.each(themedControls)('%s uses shared control geometry', (file, expectedTokens) => {
const contents = source(file);
expectedTokens.forEach((token) => expect(contents).toContain(token));
});
});

View file

@ -0,0 +1,17 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import CheckboxButton from './CheckboxButton';
describe('CheckboxButton', () => {
it('uses theme-owned control geometry', () => {
render(<CheckboxButton label="Tools" icon={<span>icon</span>} />);
expect(screen.getByRole('checkbox', { name: 'Tools' })).toHaveClass(
'size-theme-control',
'rounded-theme-control-round',
'gap-theme-compact',
'p-theme-compact',
'md:px-theme-normal',
);
});
});

View file

@ -62,9 +62,9 @@ const CheckboxButton: React.ForwardRefExoticComponent<
onChange={onChange}
className={cn(
// Base styling from MultiSelect's selectClassName
'group relative inline-flex items-center justify-center gap-1.5',
'rounded-full border border-border-medium text-sm font-medium',
'size-9 max-w-fit p-2 transition-all md:w-full md:p-3',
'group relative inline-flex items-center justify-center gap-theme-compact',
'rounded-theme-control-round border border-border-medium text-sm font-medium',
'size-theme-control max-w-fit p-theme-compact transition-all md:w-full md:px-theme-normal',
'bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner',
// Checked state styling