From d89b11d34dce834ed600c48e2f2856500c806af3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 10 Aug 2026 22:57:33 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=9B=EF=B8=8F=20feat:=20Adopt=20Compose?= =?UTF-8?q?r=20Density=20Tokens=20(#14730)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/Chat/Input/AudioRecorder.tsx | 20 ++++++++----- .../Chat/Input/DuringRunSendButton.tsx | 2 +- .../Chat/Input/Files/AttachFile.tsx | 2 +- .../Chat/Input/Files/AttachFileMenu.tsx | 2 +- .../Chat/Input/InterruptSteerButton.tsx | 4 +-- .../src/components/Chat/Input/MCPSelect.tsx | 8 ++--- .../src/components/Chat/Input/SendButton.tsx | 4 +-- .../src/components/Chat/Input/StopButton.tsx | 2 +- .../Chat/Input/TokenUsage/index.tsx | 2 +- .../components/Chat/Input/ToolsDropdown.tsx | 2 +- .../Chat/Input/__tests__/MCPSelect.spec.tsx | 7 ++++- .../Input/__tests__/appearanceTokens.spec.ts | 29 +++++++++++++++++++ .../src/components/CheckboxButton.spec.tsx | 17 +++++++++++ .../client/src/components/CheckboxButton.tsx | 6 ++-- 14 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 client/src/components/Chat/Input/__tests__/appearanceTokens.spec.ts create mode 100644 packages/client/src/components/CheckboxButton.spec.tsx diff --git a/client/src/components/Chat/Input/AudioRecorder.tsx b/client/src/components/Chat/Input/AudioRecorder.tsx index 09c5b23b70..fa3c2ad6f2 100644 --- a/client/src/components/Chat/Input/AudioRecorder.tsx +++ b/client/src/components/Chat/Input/AudioRecorder.tsx @@ -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({ {renderIcon()} - + } /> ); diff --git a/client/src/components/Chat/Input/DuringRunSendButton.tsx b/client/src/components/Chat/Input/DuringRunSendButton.tsx index d4ad5a718e..00c42b4902 100644 --- a/client/src/components/Chat/Input/DuringRunSendButton.tsx +++ b/client/src/components/Chat/Input/DuringRunSendButton.tsx @@ -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} diff --git a/client/src/components/Chat/Input/Files/AttachFile.tsx b/client/src/components/Chat/Input/Files/AttachFile.tsx index dd0cb0eb75..12122e63db 100644 --- a/client/src/components/Chat/Input/Files/AttachFile.tsx +++ b/client/src/components/Chat/Input/Files/AttachFile.tsx @@ -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) { diff --git a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx index 7b2aff0ed4..c18fd3ce72 100644 --- a/client/src/components/Chat/Input/Files/AttachFileMenu.tsx +++ b/client/src/components/Chat/Input/Files/AttachFileMenu.tsx @@ -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', )} > diff --git a/client/src/components/Chat/Input/InterruptSteerButton.tsx b/client/src/components/Chat/Input/InterruptSteerButton.tsx index 5b3e05bc02..1d52212a7c 100644 --- a/client/src/components/Chat/Input/InterruptSteerButton.tsx +++ b/client/src/components/Chat/Input/InterruptSteerButton.tsx @@ -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', diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index ef6fd1911d..ff9f6007d1 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -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={ diff --git a/client/src/components/Chat/Input/SendButton.tsx b/client/src/components/Chat/Input/SendButton.tsx index a07e574928..c68b363dbf 100644 --- a/client/src/components/Chat/Input/SendButton.tsx +++ b/client/src/components/Chat/Input/SendButton.tsx @@ -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" diff --git a/client/src/components/Chat/Input/StopButton.tsx b/client/src/components/Chat/Input/StopButton.tsx index cc45348072..2984544448 100644 --- a/client/src/components/Chat/Input/StopButton.tsx +++ b/client/src/components/Chat/Input/StopButton.tsx @@ -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) => { diff --git a/client/src/components/Chat/Input/TokenUsage/index.tsx b/client/src/components/Chat/Input/TokenUsage/index.tsx index 3edb02c27c..b8118e0c4b 100644 --- a/client/src/components/Chat/Input/TokenUsage/index.tsx +++ b/client/src/components/Chat/Input/TokenUsage/index.tsx @@ -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', )} diff --git a/client/src/components/Chat/Input/ToolsDropdown.tsx b/client/src/components/Chat/Input/ToolsDropdown.tsx index c1c665e63b..2365a4f128 100644 --- a/client/src/components/Chat/Input/ToolsDropdown.tsx +++ b/client/src/components/Chat/Input/ToolsDropdown.tsx @@ -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', )} > diff --git a/client/src/components/Chat/Input/__tests__/MCPSelect.spec.tsx b/client/src/components/Chat/Input/__tests__/MCPSelect.spec.tsx index 7662ee5e6e..421b8cf473 100644 --- a/client/src/components/Chat/Input/__tests__/MCPSelect.spec.tsx +++ b/client/src/components/Chat/Input/__tests__/MCPSelect.spec.tsx @@ -67,7 +67,12 @@ describe('MCPSelect', () => { it('renders the menu button', () => { render(); - 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 () => { diff --git a/client/src/components/Chat/Input/__tests__/appearanceTokens.spec.ts b/client/src/components/Chat/Input/__tests__/appearanceTokens.spec.ts new file mode 100644 index 0000000000..f7b5de5d85 --- /dev/null +++ b/client/src/components/Chat/Input/__tests__/appearanceTokens.spec.ts @@ -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)); + }); +}); diff --git a/packages/client/src/components/CheckboxButton.spec.tsx b/packages/client/src/components/CheckboxButton.spec.tsx new file mode 100644 index 0000000000..78add9bc6f --- /dev/null +++ b/packages/client/src/components/CheckboxButton.spec.tsx @@ -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(icon} />); + + expect(screen.getByRole('checkbox', { name: 'Tools' })).toHaveClass( + 'size-theme-control', + 'rounded-theme-control-round', + 'gap-theme-compact', + 'p-theme-compact', + 'md:px-theme-normal', + ); + }); +}); diff --git a/packages/client/src/components/CheckboxButton.tsx b/packages/client/src/components/CheckboxButton.tsx index 03a9a926f5..20116a21ac 100644 --- a/packages/client/src/components/CheckboxButton.tsx +++ b/packages/client/src/components/CheckboxButton.tsx @@ -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