mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
web: add terminal font size setting (#324)
This commit is contained in:
@@ -5,6 +5,10 @@ import { en } from '@/lib/locales'
|
||||
import { PROTOCOL_VERSION } from '@hapi/protocol'
|
||||
import SettingsPage from './index'
|
||||
|
||||
vi.mock('@hapi/protocol', () => ({
|
||||
PROTOCOL_VERSION: 1,
|
||||
}))
|
||||
|
||||
// Mock the router hooks
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
useNavigate: () => vi.fn(),
|
||||
@@ -22,6 +26,15 @@ vi.mock('@/hooks/useFontScale', () => ({
|
||||
],
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/useTerminalFontSize', () => ({
|
||||
useTerminalFontSize: () => ({ terminalFontSize: 13, setTerminalFontSize: vi.fn() }),
|
||||
getTerminalFontSizeOptions: () => [
|
||||
{ value: 9, label: '9px' },
|
||||
{ value: 13, label: '13px' },
|
||||
{ value: 17, label: '17px' },
|
||||
],
|
||||
}))
|
||||
|
||||
// Mock useTheme hook
|
||||
vi.mock('@/hooks/useTheme', () => ({
|
||||
useAppearance: () => ({ appearance: 'system', setAppearance: vi.fn() }),
|
||||
@@ -121,4 +134,10 @@ describe('SettingsPage', () => {
|
||||
expect(calledKeys).toContain('settings.display.appearance')
|
||||
expect(calledKeys).toContain('settings.display.appearance.system')
|
||||
})
|
||||
|
||||
it('renders the Terminal Font Size setting', () => {
|
||||
renderWithProviders(<SettingsPage />)
|
||||
expect(screen.getAllByText('Terminal Font Size').length).toBeGreaterThanOrEqual(1)
|
||||
expect(screen.getAllByText('13px').length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation, type Locale } from '@/lib/use-translation'
|
||||
import { useAppGoBack } from '@/hooks/useAppGoBack'
|
||||
import { getElevenLabsSupportedLanguages, getLanguageDisplayName, type Language } from '@/lib/languages'
|
||||
import { getFontScaleOptions, useFontScale, type FontScale } from '@/hooks/useFontScale'
|
||||
import { getTerminalFontSizeOptions, useTerminalFontSize, type TerminalFontSize } from '@/hooks/useTerminalFontSize'
|
||||
import { useAppearance, getAppearanceOptions, type AppearancePreference } from '@/hooks/useTheme'
|
||||
import { PROTOCOL_VERSION } from '@hapi/protocol'
|
||||
|
||||
@@ -76,12 +77,15 @@ export default function SettingsPage() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isAppearanceOpen, setIsAppearanceOpen] = useState(false)
|
||||
const [isFontOpen, setIsFontOpen] = useState(false)
|
||||
const [isTerminalFontOpen, setIsTerminalFontOpen] = useState(false)
|
||||
const [isVoiceOpen, setIsVoiceOpen] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const appearanceContainerRef = useRef<HTMLDivElement>(null)
|
||||
const fontContainerRef = useRef<HTMLDivElement>(null)
|
||||
const terminalFontContainerRef = useRef<HTMLDivElement>(null)
|
||||
const voiceContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { fontScale, setFontScale } = useFontScale()
|
||||
const { terminalFontSize, setTerminalFontSize } = useTerminalFontSize()
|
||||
const { appearance, setAppearance } = useAppearance()
|
||||
|
||||
// Voice language state - read from localStorage
|
||||
@@ -90,10 +94,12 @@ export default function SettingsPage() {
|
||||
})
|
||||
|
||||
const fontScaleOptions = getFontScaleOptions()
|
||||
const terminalFontSizeOptions = getTerminalFontSizeOptions()
|
||||
const appearanceOptions = getAppearanceOptions()
|
||||
const currentLocale = locales.find((loc) => loc.value === locale)
|
||||
const currentAppearanceLabel = appearanceOptions.find((opt) => opt.value === appearance)?.labelKey ?? 'settings.display.appearance.system'
|
||||
const currentFontScaleLabel = fontScaleOptions.find((opt) => opt.value === fontScale)?.label ?? '100%'
|
||||
const currentTerminalFontSizeLabel = terminalFontSizeOptions.find((opt) => opt.value === terminalFontSize)?.label ?? '13px'
|
||||
const currentVoiceLanguage = voiceLanguages.find((lang) => lang.code === voiceLanguage)
|
||||
|
||||
const handleLocaleChange = (newLocale: Locale) => {
|
||||
@@ -111,6 +117,11 @@ export default function SettingsPage() {
|
||||
setIsFontOpen(false)
|
||||
}
|
||||
|
||||
const handleTerminalFontSizeChange = (newSize: TerminalFontSize) => {
|
||||
setTerminalFontSize(newSize)
|
||||
setIsTerminalFontOpen(false)
|
||||
}
|
||||
|
||||
const handleVoiceLanguageChange = (language: Language) => {
|
||||
setVoiceLanguage(language.code)
|
||||
if (language.code === null) {
|
||||
@@ -123,7 +134,7 @@ export default function SettingsPage() {
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
if (!isOpen && !isAppearanceOpen && !isFontOpen && !isVoiceOpen) return
|
||||
if (!isOpen && !isAppearanceOpen && !isFontOpen && !isTerminalFontOpen && !isVoiceOpen) return
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (isOpen && containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
@@ -135,6 +146,9 @@ export default function SettingsPage() {
|
||||
if (isFontOpen && fontContainerRef.current && !fontContainerRef.current.contains(event.target as Node)) {
|
||||
setIsFontOpen(false)
|
||||
}
|
||||
if (isTerminalFontOpen && terminalFontContainerRef.current && !terminalFontContainerRef.current.contains(event.target as Node)) {
|
||||
setIsTerminalFontOpen(false)
|
||||
}
|
||||
if (isVoiceOpen && voiceContainerRef.current && !voiceContainerRef.current.contains(event.target as Node)) {
|
||||
setIsVoiceOpen(false)
|
||||
}
|
||||
@@ -142,24 +156,25 @@ export default function SettingsPage() {
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [isOpen, isAppearanceOpen, isFontOpen, isVoiceOpen])
|
||||
}, [isOpen, isAppearanceOpen, isFontOpen, isTerminalFontOpen, isVoiceOpen])
|
||||
|
||||
// Close on escape key
|
||||
useEffect(() => {
|
||||
if (!isOpen && !isAppearanceOpen && !isFontOpen && !isVoiceOpen) return
|
||||
if (!isOpen && !isAppearanceOpen && !isFontOpen && !isTerminalFontOpen && !isVoiceOpen) return
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
setIsAppearanceOpen(false)
|
||||
setIsFontOpen(false)
|
||||
setIsTerminalFontOpen(false)
|
||||
setIsVoiceOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
return () => document.removeEventListener('keydown', handleEscape)
|
||||
}, [isOpen, isAppearanceOpen, isFontOpen, isVoiceOpen])
|
||||
}, [isOpen, isAppearanceOpen, isFontOpen, isTerminalFontOpen, isVoiceOpen])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
@@ -334,6 +349,54 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div ref={terminalFontContainerRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsTerminalFontOpen(!isTerminalFontOpen)}
|
||||
className="flex w-full items-center justify-between px-3 py-3 text-left transition-colors hover:bg-[var(--app-subtle-bg)]"
|
||||
aria-expanded={isTerminalFontOpen}
|
||||
aria-haspopup="listbox"
|
||||
>
|
||||
<span className="text-[var(--app-fg)]">{t('settings.display.terminalFontSize')}</span>
|
||||
<span className="flex items-center gap-1 text-[var(--app-hint)]">
|
||||
<span>{currentTerminalFontSizeLabel}</span>
|
||||
<ChevronDownIcon className={`transition-transform ${isTerminalFontOpen ? 'rotate-180' : ''}`} />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isTerminalFontOpen && (
|
||||
<div
|
||||
className="absolute right-3 top-full mt-1 min-w-[140px] rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] shadow-lg overflow-hidden z-50"
|
||||
role="listbox"
|
||||
aria-label={t('settings.display.terminalFontSize')}
|
||||
>
|
||||
{terminalFontSizeOptions.map((opt) => {
|
||||
const isSelected = terminalFontSize === opt.value
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => handleTerminalFontSizeChange(opt.value)}
|
||||
className={`flex items-center justify-between w-full px-3 py-2 text-base text-left transition-colors ${
|
||||
isSelected
|
||||
? 'text-[var(--app-link)] bg-[var(--app-subtle-bg)]'
|
||||
: 'text-[var(--app-fg)] hover:bg-[var(--app-subtle-bg)]'
|
||||
}`}
|
||||
>
|
||||
<span>{opt.label}</span>
|
||||
{isSelected && (
|
||||
<span className="ml-2 text-[var(--app-link)]">
|
||||
<CheckIcon />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Voice Assistant section */}
|
||||
|
||||
Reference in New Issue
Block a user