From 6aae44498d49c1d6abdbaccb7b5299497b085b5b Mon Sep 17 00:00:00 2001 From: Ananovo Date: Sat, 1 Aug 2026 22:55:21 +0800 Subject: [PATCH] fix(web): align browse button and select chevrons (#1279) * fix(web): align new-session browse button * fix(web): inset select chevrons --- web/src/components/CodexSessionSyncDialog.tsx | 7 +-- .../NewSession/CollaborationModeSelector.tsx | 7 +-- .../NewSession/DirectorySection.test.tsx | 30 +++++++++++++ .../NewSession/DirectorySection.tsx | 2 +- .../NewSession/FastModeSelector.tsx | 7 +-- .../NewSession/GrokPermissionModeSelector.tsx | 7 +-- .../NewSession/LaunchEffortSelector.tsx | 7 +-- .../components/NewSession/MachineSelector.tsx | 7 +-- .../components/NewSession/ModelSelector.tsx | 7 +-- .../NewSession/ReasoningEffortSelector.tsx | 7 +-- web/src/components/WorkspaceBrowser.tsx | 14 +++--- .../settings/VoiceAdvancedControls.tsx | 7 +-- web/src/components/ui/select-control.test.tsx | 43 +++++++++++++++++++ web/src/components/ui/select-control.tsx | 33 ++++++++++++++ web/src/routes/settings/voice.tsx | 8 ++-- 15 files changed, 156 insertions(+), 37 deletions(-) create mode 100644 web/src/components/NewSession/DirectorySection.test.tsx create mode 100644 web/src/components/ui/select-control.test.tsx create mode 100644 web/src/components/ui/select-control.tsx diff --git a/web/src/components/CodexSessionSyncDialog.tsx b/web/src/components/CodexSessionSyncDialog.tsx index b51a7e68..a9e54ebc 100644 --- a/web/src/components/CodexSessionSyncDialog.tsx +++ b/web/src/components/CodexSessionSyncDialog.tsx @@ -8,6 +8,7 @@ import { DialogDescription } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' +import { SelectControl } from '@/components/ui/select-control' import { useTranslation } from '@/lib/use-translation' import { readCodexImportedSessions, subscribeCodexImportedSessions } from '@/lib/codexImportedSessions' @@ -290,8 +291,8 @@ export function CodexSessionSyncDialog(props: { {sessions.length > 0 ? ( ) : null} diff --git a/web/src/components/NewSession/CollaborationModeSelector.tsx b/web/src/components/NewSession/CollaborationModeSelector.tsx index b2dabb56..7648f908 100644 --- a/web/src/components/NewSession/CollaborationModeSelector.tsx +++ b/web/src/components/NewSession/CollaborationModeSelector.tsx @@ -1,6 +1,7 @@ import { getCodexCollaborationModeOptions, type CodexCollaborationMode } from '@hapi/protocol' import { useTranslation } from '@/lib/use-translation' import type { AgentType } from './types' +import { SelectControl } from '@/components/ui/select-control' export function CollaborationModeSelector(props: { agent: AgentType @@ -20,18 +21,18 @@ export function CollaborationModeSelector(props: { {t('newSession.collaborationMode')}{' '} ({t('newSession.model.optional')}) - + ) } diff --git a/web/src/components/NewSession/DirectorySection.test.tsx b/web/src/components/NewSession/DirectorySection.test.tsx new file mode 100644 index 00000000..5b189cf6 --- /dev/null +++ b/web/src/components/NewSession/DirectorySection.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { DirectorySection } from './DirectorySection' + +vi.mock('@/lib/use-translation', () => ({ + useTranslation: () => ({ t: (key: string) => key }) +})) + +describe('DirectorySection', () => { + it('stretches the Browse button to the directory input height', () => { + render( + + ) + + expect(screen.getByRole('button', { name: 'newSession.browse' })).toHaveClass('self-stretch') + }) +}) diff --git a/web/src/components/NewSession/DirectorySection.tsx b/web/src/components/NewSession/DirectorySection.tsx index 20f060fa..bcd91c5a 100644 --- a/web/src/components/NewSession/DirectorySection.tsx +++ b/web/src/components/NewSession/DirectorySection.tsx @@ -90,7 +90,7 @@ export function DirectorySection(props: { type="button" onClick={props.onChooseFolder} disabled={props.isDisabled} - className="shrink-0 flex items-center gap-1 rounded-md border border-[var(--app-border)] bg-[var(--app-subtle-bg)] px-2 py-2 text-xs text-[var(--app-fg)] hover:bg-[var(--app-secondary-bg)] transition-colors disabled:opacity-50" + className="self-stretch shrink-0 flex items-center gap-1 rounded-md border border-[var(--app-border)] bg-[var(--app-subtle-bg)] px-2 py-2 text-xs text-[var(--app-fg)] hover:bg-[var(--app-secondary-bg)] transition-colors disabled:opacity-50" title={t('newSession.browse')} > diff --git a/web/src/components/NewSession/FastModeSelector.tsx b/web/src/components/NewSession/FastModeSelector.tsx index 5a15b357..8345606e 100644 --- a/web/src/components/NewSession/FastModeSelector.tsx +++ b/web/src/components/NewSession/FastModeSelector.tsx @@ -1,5 +1,6 @@ import { useTranslation } from '@/lib/use-translation' import type { NewSessionServiceTier } from './types' +import { SelectControl } from '@/components/ui/select-control' export type { NewSessionServiceTier } @@ -21,15 +22,15 @@ export function FastModeSelector(props: { {t('newSession.fastMode')}{' '} ({t('newSession.model.optional')}) - + ) } diff --git a/web/src/components/NewSession/GrokPermissionModeSelector.tsx b/web/src/components/NewSession/GrokPermissionModeSelector.tsx index 0d72f71e..480434db 100644 --- a/web/src/components/NewSession/GrokPermissionModeSelector.tsx +++ b/web/src/components/NewSession/GrokPermissionModeSelector.tsx @@ -4,6 +4,7 @@ import { } from '@hapi/protocol' import { useTranslation } from '@/lib/use-translation' import type { AgentType } from './types' +import { SelectControl } from '@/components/ui/select-control' export function GrokPermissionModeSelector(props: { agent: AgentType @@ -21,11 +22,11 @@ export function GrokPermissionModeSelector(props: { - + {props.autoPermissionModeSupported === false ? ( {t('newSession.grokAutoUnavailableDesc')} diff --git a/web/src/components/NewSession/LaunchEffortSelector.tsx b/web/src/components/NewSession/LaunchEffortSelector.tsx index 97e1c569..d5bc736d 100644 --- a/web/src/components/NewSession/LaunchEffortSelector.tsx +++ b/web/src/components/NewSession/LaunchEffortSelector.tsx @@ -1,6 +1,7 @@ import type { AgentType, LaunchEffort } from './types' import { CLAUDE_EFFORT_OPTIONS, GROK_EFFORT_OPTIONS } from './types' import { useTranslation } from '@/lib/use-translation' +import { SelectControl } from '@/components/ui/select-control' export function LaunchEffortSelector(props: { agent: AgentType @@ -25,18 +26,18 @@ export function LaunchEffortSelector(props: { {t('newSession.effort')}{' '} ({t('newSession.model.optional')}) - + ) } diff --git a/web/src/components/NewSession/MachineSelector.tsx b/web/src/components/NewSession/MachineSelector.tsx index 070b72ce..1ee076e4 100644 --- a/web/src/components/NewSession/MachineSelector.tsx +++ b/web/src/components/NewSession/MachineSelector.tsx @@ -1,5 +1,6 @@ import type { Machine } from '@/types/api' import { useTranslation } from '@/lib/use-translation' +import { SelectControl } from '@/components/ui/select-control' function getMachineTitle(machine: Machine): string { if (machine.metadata?.displayName) return machine.metadata.displayName @@ -21,11 +22,11 @@ export function MachineSelector(props: { - + ) } diff --git a/web/src/components/NewSession/ModelSelector.tsx b/web/src/components/NewSession/ModelSelector.tsx index 248b1193..587b9270 100644 --- a/web/src/components/NewSession/ModelSelector.tsx +++ b/web/src/components/NewSession/ModelSelector.tsx @@ -1,6 +1,7 @@ import type { AgentType } from './types' import { MODEL_OPTIONS } from './types' import { useTranslation } from '@/lib/use-translation' +import { SelectControl } from '@/components/ui/select-control' export function ModelSelector(props: { agent: AgentType @@ -26,18 +27,18 @@ export function ModelSelector(props: { ({t('newSession.model.optional')}) ) : null} - + {props.error ? (
{props.error} diff --git a/web/src/components/NewSession/ReasoningEffortSelector.tsx b/web/src/components/NewSession/ReasoningEffortSelector.tsx index 36f042c8..59928794 100644 --- a/web/src/components/NewSession/ReasoningEffortSelector.tsx +++ b/web/src/components/NewSession/ReasoningEffortSelector.tsx @@ -2,6 +2,7 @@ import type { AgentType, CodexReasoningEffort } from './types' import { CODEX_REASONING_EFFORT_OPTIONS } from './types' import { useTranslation } from '@/lib/use-translation' import { getCodexComposerReasoningEffortOptions } from '@/components/AssistantChat/codexReasoningEffortOptions' +import { SelectControl } from '@/components/ui/select-control' export function ReasoningEffortSelector(props: { agent: AgentType @@ -31,18 +32,18 @@ export function ReasoningEffortSelector(props: { {t('newSession.reasoningEffort')}{' '} ({t('newSession.model.optional')}) - +
) } diff --git a/web/src/components/WorkspaceBrowser.tsx b/web/src/components/WorkspaceBrowser.tsx index 12f72463..b82e71d1 100644 --- a/web/src/components/WorkspaceBrowser.tsx +++ b/web/src/components/WorkspaceBrowser.tsx @@ -4,6 +4,7 @@ import type { ApiClient } from '@/api/client' import type { Machine, MachineDirectoryEntry } from '@/types/api' import { queryKeys } from '@/lib/query-keys' import { useTranslation } from '@/lib/use-translation' +import { SelectControl } from '@/components/ui/select-control' function FolderIcon(props: { className?: string }) { return ( @@ -271,11 +272,12 @@ export function WorkspaceBrowser(props: { const machineSelector = (
- +
) @@ -329,17 +331,17 @@ export function WorkspaceBrowser(props: { {workspaceRoots.length > 1 && (
- +
)} diff --git a/web/src/components/settings/VoiceAdvancedControls.tsx b/web/src/components/settings/VoiceAdvancedControls.tsx index 0d66b0bd..66d227f0 100644 --- a/web/src/components/settings/VoiceAdvancedControls.tsx +++ b/web/src/components/settings/VoiceAdvancedControls.tsx @@ -1,4 +1,5 @@ import { useState } from 'react' +import { SelectControl } from '@/components/ui/select-control' import { DEFAULT_VOICE_CHARACTER, DEFAULT_VOICE_IDENTITY, @@ -197,13 +198,13 @@ export function VoicePersonaControls(props: { {props.t('settings.voice.character.preset.label')} - +

{props.t(getVoicePersonalityPreset(prefs.preset).descriptionKey)}

diff --git a/web/src/components/ui/select-control.test.tsx b/web/src/components/ui/select-control.test.tsx new file mode 100644 index 00000000..ff7a3064 --- /dev/null +++ b/web/src/components/ui/select-control.test.tsx @@ -0,0 +1,43 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { SelectControl } from './select-control' + +describe('SelectControl', () => { + it('replaces the native arrow with an inset decorative chevron', () => { + render( + + + + ) + + const select = screen.getByRole('combobox', { name: 'Machine' }) + const chevron = select.nextElementSibling + + expect(select).toHaveClass('appearance-none', 'pr-10') + expect(chevron).toHaveAttribute('aria-hidden', 'true') + expect(chevron).toHaveClass('pointer-events-none', 'right-3') + }) + + it('preserves native select behavior and wrapper sizing classes', () => { + const onChange = vi.fn() + + render( + + + + + ) + + const select = screen.getByRole('combobox', { name: 'Language' }) + expect(select.parentElement).toHaveClass('max-w-[55%]') + + fireEvent.change(select, { target: { value: 'zh' } }) + expect(onChange).toHaveBeenCalledOnce() + expect(select).toHaveValue('zh') + }) +}) diff --git a/web/src/components/ui/select-control.tsx b/web/src/components/ui/select-control.tsx new file mode 100644 index 00000000..90277907 --- /dev/null +++ b/web/src/components/ui/select-control.tsx @@ -0,0 +1,33 @@ +import { forwardRef, type SelectHTMLAttributes } from 'react' + +export interface SelectControlProps extends SelectHTMLAttributes { + containerClassName?: string +} + +export const SelectControl = forwardRef( + function SelectControl({ children, className = '', containerClassName = '', ...props }, ref) { + return ( + + + + + ) + } +) diff --git a/web/src/routes/settings/voice.tsx b/web/src/routes/settings/voice.tsx index e7a724eb..336f0c7d 100644 --- a/web/src/routes/settings/voice.tsx +++ b/web/src/routes/settings/voice.tsx @@ -5,6 +5,7 @@ import { getLanguageDisplayName } from '@/lib/languages' import { useTranslation } from '@/lib/use-translation' import { VoiceRespondsControls } from '@/components/settings/VoiceAdvancedControls' import { SettingsChoiceGroup, SettingsLinkRow, SettingsPageContent, SettingsSection } from '@/components/settings/SettingsPrimitives' +import { SelectControl } from '@/components/ui/select-control' import { useVoiceSettings } from './useVoiceSettings' export default function SettingsVoicePage() { @@ -34,16 +35,17 @@ export default function SettingsVoicePage() { ) : null}