mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 12:13:30 +00:00
🎛️ feat: Adopt Composer Density Tokens (#14730)
This commit is contained in:
parent
c93609cb82
commit
d89b11d34d
14 changed files with 82 additions and 25 deletions
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
17
packages/client/src/components/CheckboxButton.spec.tsx
Normal file
17
packages/client/src/components/CheckboxButton.spec.tsx
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue