mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): align browse button and select chevrons (#1279)
* fix(web): align new-session browse button * fix(web): inset select chevrons
This commit is contained in:
@@ -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 ? (
|
||||
<label className="block min-w-0 text-xs text-[var(--app-hint)]">
|
||||
<span className="mb-1 block">{t('codexSync.confirm.cwdFilter')}</span>
|
||||
<select
|
||||
className="h-8 w-full rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] px-2 text-xs text-[var(--app-fg)] outline-none focus:ring-2 focus:ring-[var(--app-link)]"
|
||||
<SelectControl
|
||||
className="h-8 rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] pl-2 text-xs text-[var(--app-fg)] outline-none focus:ring-2 focus:ring-[var(--app-link)]"
|
||||
value={workdirFilter}
|
||||
disabled={isPending || isLoading || workdirOptions.length === 0}
|
||||
onChange={(event) => setWorkdirFilter(event.target.value)}
|
||||
@@ -304,7 +305,7 @@ export function CodexSessionSyncDialog(props: {
|
||||
{directory}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SelectControl>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -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')}{' '}
|
||||
<span className="font-normal">({t('newSession.model.optional')})</span>
|
||||
</label>
|
||||
<select
|
||||
<SelectControl
|
||||
value={props.value}
|
||||
onChange={(e) => props.onChange(e.target.value as CodexCollaborationMode)}
|
||||
disabled={props.isDisabled}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
className="py-2 pl-3 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
>
|
||||
{getCodexCollaborationModeOptions().map((option) => (
|
||||
<option key={option.mode} value={option.mode}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SelectControl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
<DirectorySection
|
||||
directory=""
|
||||
suggestions={[]}
|
||||
selectedIndex={0}
|
||||
isDisabled={false}
|
||||
recentPaths={[]}
|
||||
onDirectoryChange={vi.fn()}
|
||||
onDirectoryFocus={vi.fn()}
|
||||
onDirectoryBlur={vi.fn()}
|
||||
onDirectoryKeyDown={vi.fn()}
|
||||
onSuggestionSelect={vi.fn()}
|
||||
onPathClick={vi.fn()}
|
||||
onChooseFolder={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'newSession.browse' })).toHaveClass('self-stretch')
|
||||
})
|
||||
})
|
||||
@@ -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')}
|
||||
>
|
||||
<FolderIcon className="h-3.5 w-3.5" />
|
||||
|
||||
@@ -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')}{' '}
|
||||
<span className="font-normal">({t('newSession.model.optional')})</span>
|
||||
</label>
|
||||
<select
|
||||
<SelectControl
|
||||
value={props.value}
|
||||
onChange={(e) => props.onChange(e.target.value as NewSessionServiceTier)}
|
||||
disabled={props.isDisabled}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
className="py-2 pl-3 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
>
|
||||
<option value="standard">{t('misc.fastModeStandard')}</option>
|
||||
<option value="fast">{t('misc.fastModeFast')}</option>
|
||||
</select>
|
||||
</SelectControl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
<label className="text-xs font-medium text-[var(--app-hint)]">
|
||||
{t('misc.permissionMode')}
|
||||
</label>
|
||||
<select
|
||||
<SelectControl
|
||||
value={props.value}
|
||||
onChange={(event) => props.onChange(event.target.value as GrokPermissionMode)}
|
||||
disabled={props.isDisabled}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
className="py-2 pl-3 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
>
|
||||
{getPermissionModeOptionsForFlavor('grok').map((option) => {
|
||||
const unavailable = option.mode === 'auto'
|
||||
@@ -36,7 +37,7 @@ export function GrokPermissionModeSelector(props: {
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
</SelectControl>
|
||||
{props.autoPermissionModeSupported === false ? (
|
||||
<span className="text-xs text-[var(--app-hint)]">
|
||||
{t('newSession.grokAutoUnavailableDesc')}
|
||||
|
||||
@@ -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')}{' '}
|
||||
<span className="font-normal">({t('newSession.model.optional')})</span>
|
||||
</label>
|
||||
<select
|
||||
<SelectControl
|
||||
value={props.effort}
|
||||
onChange={(e) => props.onEffortChange(e.target.value)}
|
||||
disabled={props.isDisabled}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
className="py-2 pl-3 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SelectControl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
<label className="text-xs font-medium text-[var(--app-hint)]">
|
||||
{t('newSession.machine')}
|
||||
</label>
|
||||
<select
|
||||
<SelectControl
|
||||
value={props.machineId ?? ''}
|
||||
onChange={(e) => props.onChange(e.target.value)}
|
||||
disabled={props.isDisabled}
|
||||
className="w-full rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] p-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
className="rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] py-2 pl-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
>
|
||||
{props.isLoading && (
|
||||
<option value="">{t('loading.machines')}</option>
|
||||
@@ -39,7 +40,7 @@ export function MachineSelector(props: {
|
||||
{m.metadata?.platform ? ` (${m.metadata.platform})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SelectControl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
<span className="font-normal">({t('newSession.model.optional')})</span>
|
||||
) : null}
|
||||
</label>
|
||||
<select
|
||||
<SelectControl
|
||||
value={props.model}
|
||||
onChange={(e) => props.onModelChange(e.target.value)}
|
||||
disabled={props.isDisabled || props.isLoading}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
className="py-2 pl-3 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SelectControl>
|
||||
{props.error ? (
|
||||
<div className="text-xs text-red-600">
|
||||
{props.error}
|
||||
|
||||
@@ -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')}{' '}
|
||||
<span className="font-normal">({t('newSession.model.optional')})</span>
|
||||
</label>
|
||||
<select
|
||||
<SelectControl
|
||||
value={props.value}
|
||||
onChange={(e) => props.onChange(e.target.value as CodexReasoningEffort)}
|
||||
disabled={props.isDisabled}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
className="py-2 pl-3 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SelectControl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = (
|
||||
<div className="flex items-center gap-2">
|
||||
<MachineIcon className="h-4 w-4 text-[var(--app-hint)] shrink-0" />
|
||||
<select
|
||||
<SelectControl
|
||||
value={machineId ?? ''}
|
||||
onChange={e => setMachineId(e.target.value || null)}
|
||||
disabled={machinesLoading}
|
||||
className="flex-1 bg-transparent text-sm text-[var(--app-fg)] outline-none"
|
||||
containerClassName="flex-1"
|
||||
className="bg-transparent text-sm text-[var(--app-fg)] outline-none"
|
||||
>
|
||||
{machines.map(m => (
|
||||
<option key={m.id} value={m.id}>
|
||||
@@ -286,7 +288,7 @@ export function WorkspaceBrowser(props: {
|
||||
{machines.length === 0 && (
|
||||
<option value="">{machinesLoading ? t('loading') : t('misc.noMachines')}</option>
|
||||
)}
|
||||
</select>
|
||||
</SelectControl>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -329,17 +331,17 @@ export function WorkspaceBrowser(props: {
|
||||
|
||||
{workspaceRoots.length > 1 && (
|
||||
<div className="mt-2">
|
||||
<select
|
||||
<SelectControl
|
||||
value={selectedRoot ?? ''}
|
||||
onChange={(e) => setSelectedRoot(e.target.value || null)}
|
||||
className="w-full bg-transparent text-xs text-[var(--app-hint)] outline-none"
|
||||
className="bg-transparent text-xs text-[var(--app-hint)] outline-none"
|
||||
>
|
||||
{workspaceRoots.map((root) => (
|
||||
<option key={root} value={root}>
|
||||
{root}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SelectControl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
<span className="mb-1 block text-sm text-[var(--app-fg)]">
|
||||
{props.t('settings.voice.character.preset.label')}
|
||||
</span>
|
||||
<select value={prefs.preset}
|
||||
<SelectControl value={prefs.preset}
|
||||
onChange={(e) => setPreset(e.target.value as VoicePersonalityPresetId)}
|
||||
className="w-full rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] px-2 py-2 text-sm text-[var(--app-fg)]">
|
||||
className="rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] py-2 pl-2 text-sm text-[var(--app-fg)]">
|
||||
{VOICE_PERSONALITY_PRESETS.map((preset) => (
|
||||
<option key={preset.id} value={preset.id}>{props.t(preset.labelKey)}</option>
|
||||
))}
|
||||
</select>
|
||||
</SelectControl>
|
||||
<p className="mt-1 text-xs text-[var(--app-hint)]">
|
||||
{props.t(getVoicePersonalityPreset(prefs.preset).descriptionKey)}
|
||||
</p>
|
||||
|
||||
@@ -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(
|
||||
<SelectControl aria-label="Machine" defaultValue="local">
|
||||
<option value="local">Local</option>
|
||||
</SelectControl>
|
||||
)
|
||||
|
||||
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(
|
||||
<SelectControl
|
||||
aria-label="Language"
|
||||
containerClassName="max-w-[55%]"
|
||||
defaultValue="en"
|
||||
onChange={onChange}
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="zh">Chinese</option>
|
||||
</SelectControl>
|
||||
)
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { forwardRef, type SelectHTMLAttributes } from 'react'
|
||||
|
||||
export interface SelectControlProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
||||
containerClassName?: string
|
||||
}
|
||||
|
||||
export const SelectControl = forwardRef<HTMLSelectElement, SelectControlProps>(
|
||||
function SelectControl({ children, className = '', containerClassName = '', ...props }, ref) {
|
||||
return (
|
||||
<span className={`relative block min-w-0 ${containerClassName}`}>
|
||||
<select
|
||||
ref={ref}
|
||||
className={`peer w-full appearance-none pr-10 ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--app-fg)] peer-disabled:opacity-50"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -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}
|
||||
<label className="flex min-h-12 items-center justify-between gap-3 px-3 py-3">
|
||||
<span className="text-sm font-medium text-[var(--app-fg)]">{t('settings.voice.language')}</span>
|
||||
<select
|
||||
<SelectControl
|
||||
value={voice.voiceLanguage ?? ''}
|
||||
onChange={(event) => {
|
||||
const language = voice.voiceLanguages.find((option) => (option.code ?? '') === event.target.value)
|
||||
if (language) voice.setVoiceLanguage(language)
|
||||
}}
|
||||
className="max-w-[55%] rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] px-2 py-1.5 text-sm text-[var(--app-fg)]"
|
||||
containerClassName="w-full max-w-[55%]"
|
||||
className="rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] py-1.5 pl-2 text-sm text-[var(--app-fg)]"
|
||||
>
|
||||
{voice.voiceLanguages.map((language) => <option key={language.code ?? 'auto'} value={language.code ?? ''}>{language.code === null ? t('settings.voice.autoDetect') : getLanguageDisplayName(language)}</option>)}
|
||||
</select>
|
||||
</SelectControl>
|
||||
</label>
|
||||
<SettingsLinkRow
|
||||
label={t('settings.voice.voice')}
|
||||
|
||||
Reference in New Issue
Block a user