From fdaafc93dde2494ecd47170e9a2d14fe83cd3808 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:37:40 -0700 Subject: [PATCH] refactor(renderer): split SettingsFormControls below max-lines limit (#13747) --- config/max-lines-baseline.txt | 1 - .../components/settings/FontAutocomplete.tsx | 333 +++++++++++ .../settings/SettingsFormControls.tsx | 537 +----------------- .../settings/TerminalThemePicker.tsx | 207 +++++++ 4 files changed, 545 insertions(+), 533 deletions(-) create mode 100644 src/renderer/src/components/settings/FontAutocomplete.tsx create mode 100644 src/renderer/src/components/settings/TerminalThemePicker.tsx diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index 8710b4e82e3..0b88c5f537c 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -232,7 +232,6 @@ inline src/renderer/src/components/settings/AgentsPane.tsx inline src/renderer/src/components/settings/RepositoryHooksSection.tsx inline src/renderer/src/components/settings/RuntimeEnvironmentsPane.tsx inline src/renderer/src/components/settings/Settings.tsx -inline src/renderer/src/components/settings/SettingsFormControls.tsx inline src/renderer/src/components/sidebar/RemoteFileBrowser.tsx inline src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx inline src/renderer/src/components/sidebar/WorktreeContextMenu.tsx diff --git a/src/renderer/src/components/settings/FontAutocomplete.tsx b/src/renderer/src/components/settings/FontAutocomplete.tsx new file mode 100644 index 00000000000..aae2c65eaaf --- /dev/null +++ b/src/renderer/src/components/settings/FontAutocomplete.tsx @@ -0,0 +1,333 @@ +import type React from 'react' +import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' +import { Check, ChevronsUpDown, CircleX } from 'lucide-react' +import { Input } from '../ui/input' +import { Popover, PopoverAnchor, PopoverContent } from '../ui/popover' +import { ScrollArea } from '../ui/scroll-area' +import { filterFontSuggestions, getRenderedFontSuggestions } from './settings-form-option-filter' +import { translate } from '@/i18n/i18n' + +type FontAutocompleteProps = { + value: string + suggestions: string[] + onChange: (value: string) => void + placeholder?: string + onRequestSuggestions?: () => void + /** Fires with whichever option the user is currently highlighting in the + * dropdown (via mouse hover or keyboard arrow), or null when nothing is + * highlighted / the dropdown is closed. Lets a consumer show a live + * preview of the font without committing the selection. */ + onPreviewFontFamily?: (font: string | null) => void +} + +export function FontAutocomplete({ + value, + suggestions, + onChange, + placeholder = 'SF Mono', + onRequestSuggestions, + onPreviewFontFamily +}: FontAutocompleteProps): React.JSX.Element { + const [query, setQuery] = useState(value) + const [prevValue, setPrevValue] = useState(value) + const [open, setOpen] = useState(false) + const [highlightedIndex, setHighlightedIndex] = useState(-1) + const [isFilteringQuery, setIsFilteringQuery] = useState(false) + const inputRef = useRef(null) + const rootRef = useRef(null) + const previewFontFamilyRef = useRef(onPreviewFontFamily) + const listboxId = useId() + + useEffect(() => { + previewFontFamilyRef.current = onPreviewFontFamily + }, [onPreviewFontFamily]) + + const setRootNode = useCallback((element: HTMLDivElement | null): void => { + rootRef.current = element + if (!element) { + // Why: settings search can unmount this control while a hover preview is + // active; the consumer must not keep rendering that transient font. + previewFontFamilyRef.current?.(null) + } + }, []) + + if (value !== prevValue) { + setPrevValue(value) + setQuery(value) + if (value !== query) { + setIsFilteringQuery(false) + } + } + + const requestSuggestions = useCallback((): void => { + onRequestSuggestions?.() + }, [onRequestSuggestions]) + + const handleOpenChange = (nextOpen: boolean): void => { + setOpen(nextOpen) + if (nextOpen) { + requestSuggestions() + } + if (!nextOpen) { + setIsFilteringQuery(false) + } + } + + const normalizedQuery = query.trim().toLowerCase() + const normalizedValue = value.trim().toLowerCase() + const filteredSuggestions = useMemo( + () => filterFontSuggestions(suggestions, query), + [suggestions, query] + ) + // Why: the committed font fills the input, but opening the chooser should + // still reveal every installed font instead of only fonts sharing that name. + const visibleSuggestions = + !isFilteringQuery && normalizedQuery === normalizedValue ? suggestions : filteredSuggestions + const renderedSuggestions = useMemo( + () => getRenderedFontSuggestions(visibleSuggestions, highlightedIndex), + [visibleSuggestions, highlightedIndex] + ) + + // Why: sync the highlighted index during render rather than via useEffect so + // the correct item is highlighted on the very first paint after open/filter + // changes — useEffect would leave one render with the stale index visible. + const [prevVisibleSuggestions, setPrevVisibleSuggestions] = useState(visibleSuggestions) + const [prevOpen, setPrevOpen] = useState(open) + const [prevHighlightedValue, setPrevHighlightedValue] = useState(value) + if ( + visibleSuggestions !== prevVisibleSuggestions || + open !== prevOpen || + value !== prevHighlightedValue + ) { + setPrevVisibleSuggestions(visibleSuggestions) + setPrevOpen(open) + setPrevHighlightedValue(value) + if (!open || visibleSuggestions.length === 0) { + setHighlightedIndex(-1) + } else { + const selectedIndex = visibleSuggestions.indexOf(value) + setHighlightedIndex(Math.max(selectedIndex, 0)) + } + } + + // Why: notify the consumer of the currently-highlighted font so it can + // render a live preview. Closing the dropdown or moving past all options + // clears the preview back to the committed value. + useEffect(() => { + if (!onPreviewFontFamily) { + return + } + if (!open || highlightedIndex < 0) { + onPreviewFontFamily(null) + return + } + onPreviewFontFamily(visibleSuggestions[highlightedIndex] ?? null) + }, [visibleSuggestions, highlightedIndex, onPreviewFontFamily, open]) + + const commitValue = (nextValue: string): void => { + setQuery(nextValue) + setIsFilteringQuery(false) + onChange(nextValue) + setOpen(false) + } + + const focusInput = (): void => { + inputRef.current?.focus() + } + const popoverAvailableHeightStyle = { + // Why: tailwind-merge rewrites this arbitrary max-height class on the + // ScrollArea root, so keep the Radix available-height clamp as inline style. + maxHeight: 'var(--radix-popover-content-available-height)' + } as React.CSSProperties + + return ( +
+ + +
+ { + const next = e.target.value + requestSuggestions() + setQuery(next) + setIsFilteringQuery(true) + onChange(next) + setOpen(true) + }} + onFocus={() => { + requestSuggestions() + setIsFilteringQuery(false) + setOpen(true) + }} + onKeyDown={(e) => { + if (e.key === 'Escape') { + if (open) { + e.preventDefault() + setOpen(false) + setIsFilteringQuery(false) + } + return + } + + if (e.key === 'ArrowDown') { + e.preventDefault() + setOpen(true) + if (visibleSuggestions.length > 0) { + setHighlightedIndex((current) => + current < 0 ? 0 : Math.min(current + 1, visibleSuggestions.length - 1) + ) + } + return + } + + if (e.key === 'ArrowUp') { + e.preventDefault() + setOpen(true) + if (visibleSuggestions.length > 0) { + setHighlightedIndex((current) => + current < 0 ? visibleSuggestions.length - 1 : Math.max(current - 1, 0) + ) + } + return + } + + if (e.key === 'Enter' && open && highlightedIndex >= 0) { + const highlightedFont = visibleSuggestions[highlightedIndex] + if (highlightedFont) { + e.preventDefault() + commitValue(highlightedFont) + } + } + }} + placeholder={placeholder} + className="pr-18" + role="combobox" + aria-autocomplete="list" + aria-expanded={open} + aria-controls={listboxId} + aria-activedescendant={ + open && highlightedIndex >= 0 + ? `${listboxId}-option-${highlightedIndex}` + : undefined + } + /> +
+ {query ? ( + + ) : null} + +
+
+
+ + {/* Why: portal the dropdown outside the settings section — an in-flow + absolute panel makes the highlighted option's scrollIntoView scroll + the whole settings pane, pushing the section content out of view. */} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + onInteractOutside={(e) => { + // Why: the input and its clear/toggle buttons are the anchor, not + // the content, so Radix would otherwise dismiss on every click there. + if (rootRef.current?.contains(e.target as Node)) { + e.preventDefault() + } + }} + > + 8 ? 'h-64' : undefined} + style={popoverAvailableHeightStyle} + viewportProps={{ style: popoverAvailableHeightStyle }} + > +
+ {visibleSuggestions.length > 0 ? ( + renderedSuggestions.map(({ font, sourceIndex }) => ( + + )) + ) : ( +
+ {translate( + 'auto.components.settings.SettingsFormControls.42a4d15a30', + 'No matching fonts.' + )} +
+ )} +
+
+
+
+
+ ) +} diff --git a/src/renderer/src/components/settings/SettingsFormControls.tsx b/src/renderer/src/components/settings/SettingsFormControls.tsx index 703dbbfbb21..b82e90a0984 100644 --- a/src/renderer/src/components/settings/SettingsFormControls.tsx +++ b/src/renderer/src/components/settings/SettingsFormControls.tsx @@ -1,26 +1,16 @@ -/* eslint-disable max-lines -- Why: these small settings form primitives and controls -co-locate shared layout and keyboard interaction logic, which keeps the settings -panel wiring simple even though the file exceeds the default line limit. */ import type React from 'react' -import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' -import { ScrollArea } from '../ui/scroll-area' +import { useState } from 'react' import { Input } from '../ui/input' import { Label } from '../ui/label' -import { Popover, PopoverAnchor, PopoverContent } from '../ui/popover' import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' import { Switch } from '../ui/switch' -import { Check, ChevronsUpDown, CircleX } from 'lucide-react' -import { normalizeColor, type TerminalThemeOption } from '@/lib/terminal-theme' -import { MAX_THEME_RESULTS } from './SettingsConstants' -import { - filterFontSuggestions, - filterTerminalThemeOptions, - getRenderedFontSuggestions, - isSettingsFormOptionQueryTooLarge -} from './settings-form-option-filter' +import { normalizeColor } from '@/lib/terminal-theme' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' +export { FontAutocomplete } from './FontAutocomplete' +export { ThemePicker } from './TerminalThemePicker' + type SettingsSwitchProps = { checked: boolean onChange: () => void @@ -261,19 +251,6 @@ export function SettingsSubsectionHeader({ ) } -type ThemePickerProps = { - label: string - description: string - selectedTheme: string - themeOptions: TerminalThemeOption[] - query: string - onQueryChange: (value: string) => void - onSelectTheme: (theme: string) => void - /** Bumps when themes are imported; scrolls the Imported group into view and - * briefly highlights it so freshly-imported themes are easy to find. */ - importedHighlightSignal?: number -} - type ColorFieldProps = { label: string description: string @@ -294,200 +271,6 @@ type NumberFieldProps = { suffix?: string } -type FontAutocompleteProps = { - value: string - suggestions: string[] - onChange: (value: string) => void - placeholder?: string - onRequestSuggestions?: () => void - /** Fires with whichever option the user is currently highlighting in the - * dropdown (via mouse hover or keyboard arrow), or null when nothing is - * highlighted / the dropdown is closed. Lets a consumer show a live - * preview of the font without committing the selection. */ - onPreviewFontFamily?: (font: string | null) => void -} - -export function ThemePicker({ - label, - description, - selectedTheme, - themeOptions, - query, - onQueryChange, - onSelectTheme, - importedHighlightSignal -}: ThemePickerProps): React.JSX.Element { - const importedGroupRef = useRef(null) - const [highlightImported, setHighlightImported] = useState(false) - - // Why: imported themes render below the built-in list inside a fixed-height - // scroll area, so after an import they sit off-screen. On each import signal, - // scroll the Imported group into view and flash a highlight so it's easy to spot. - useEffect(() => { - if (!importedHighlightSignal) { - return - } - importedGroupRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) - setHighlightImported(true) - const timer = setTimeout(() => setHighlightImported(false), 2000) - return () => clearTimeout(timer) - }, [importedHighlightSignal]) - - const themeQuery = query.trim() - const shouldShowThemeQueryLabel = - themeQuery.length > 0 && !isSettingsFormOptionQueryTooLarge(themeQuery) - const matchingThemes = filterTerminalThemeOptions(themeOptions, query) - const selectedThemeLabel = - themeOptions.find((option) => option.value === selectedTheme)?.label ?? selectedTheme - const groupedThemes = [ - { - label: translate('auto.components.settings.SettingsFormControls.builtin_themes', 'Built-in'), - themes: matchingThemes - .filter((theme) => theme.group === 'built-in') - .slice(0, MAX_THEME_RESULTS) - }, - { - label: translate('auto.components.settings.SettingsFormControls.imported_themes', 'Imported'), - themes: matchingThemes - .filter((theme) => theme.group === 'imported') - .slice(0, MAX_THEME_RESULTS) - } - ].filter((group) => group.themes.length > 0) - const visibleThemeCount = groupedThemes.reduce((sum, group) => sum + group.themes.length, 0) - - return ( -
-
- -

{description}

-
- onQueryChange(e.target.value)} - placeholder={translate( - 'auto.components.settings.SettingsFormControls.search_terminal_themes', - 'Search terminal themes' - )} - /> -
-
- - {translate('auto.components.settings.SettingsFormControls.fbb428db98', 'Selected:')}{' '} - {selectedThemeLabel} - - - {translate('auto.components.settings.SettingsFormControls.4e11f87ca6', 'Showing')}{' '} - {visibleThemeCount} - {shouldShowThemeQueryLabel - ? translate( - 'auto.components.settings.SettingsFormControls.c822571b2e', - ' matching "{{value0}}"', - { value0: themeQuery } - ) - : translate( - 'auto.components.settings.SettingsFormControls.cb330ef7f8', - ' of {{value0}}', - { value0: themeOptions.length } - )} - -
- -
- {groupedThemes.map((group) => { - const isImported = - group.label === - translate( - 'auto.components.settings.SettingsFormControls.imported_themes', - 'Imported' - ) - return ( -
-

- {group.label} -

- {group.themes.map((theme) => ( - - ))} -
- ) - })} - {visibleThemeCount === 0 ? ( -
- {translate( - 'auto.components.settings.SettingsFormControls.ceefb9d7f1', - 'No themes found.' - )} -
- ) : null} -
-
-
-
- ) -} - export function ColorField({ label, description, @@ -596,313 +379,3 @@ export function NumberField({ /> ) } - -export function FontAutocomplete({ - value, - suggestions, - onChange, - placeholder = 'SF Mono', - onRequestSuggestions, - onPreviewFontFamily -}: FontAutocompleteProps): React.JSX.Element { - const [query, setQuery] = useState(value) - const [prevValue, setPrevValue] = useState(value) - const [open, setOpen] = useState(false) - const [highlightedIndex, setHighlightedIndex] = useState(-1) - const [isFilteringQuery, setIsFilteringQuery] = useState(false) - const inputRef = useRef(null) - const rootRef = useRef(null) - const previewFontFamilyRef = useRef(onPreviewFontFamily) - const listboxId = useId() - - previewFontFamilyRef.current = onPreviewFontFamily - - const setRootNode = useCallback((element: HTMLDivElement | null): void => { - rootRef.current = element - if (!element) { - // Why: settings search can unmount this control while a hover preview is - // active; the consumer must not keep rendering that transient font. - previewFontFamilyRef.current?.(null) - } - }, []) - - if (value !== prevValue) { - setPrevValue(value) - setQuery(value) - if (value !== query) { - setIsFilteringQuery(false) - } - } - - const requestSuggestions = useCallback((): void => { - onRequestSuggestions?.() - }, [onRequestSuggestions]) - - const handleOpenChange = (nextOpen: boolean): void => { - setOpen(nextOpen) - if (nextOpen) { - requestSuggestions() - } - if (!nextOpen) { - setIsFilteringQuery(false) - } - } - - const normalizedQuery = query.trim().toLowerCase() - const normalizedValue = value.trim().toLowerCase() - const filteredSuggestions = useMemo( - () => filterFontSuggestions(suggestions, query), - [suggestions, query] - ) - // Why: the committed font fills the input, but opening the chooser should - // still reveal every installed font instead of only fonts sharing that name. - const visibleSuggestions = - !isFilteringQuery && normalizedQuery === normalizedValue ? suggestions : filteredSuggestions - const renderedSuggestions = useMemo( - () => getRenderedFontSuggestions(visibleSuggestions, highlightedIndex), - [visibleSuggestions, highlightedIndex] - ) - - // Why: sync the highlighted index during render rather than via useEffect so - // the correct item is highlighted on the very first paint after open/filter - // changes — useEffect would leave one render with the stale index visible. - const [prevVisibleSuggestions, setPrevVisibleSuggestions] = useState(visibleSuggestions) - const [prevOpen, setPrevOpen] = useState(open) - const [prevHighlightedValue, setPrevHighlightedValue] = useState(value) - if ( - visibleSuggestions !== prevVisibleSuggestions || - open !== prevOpen || - value !== prevHighlightedValue - ) { - setPrevVisibleSuggestions(visibleSuggestions) - setPrevOpen(open) - setPrevHighlightedValue(value) - if (!open || visibleSuggestions.length === 0) { - setHighlightedIndex(-1) - } else { - const selectedIndex = visibleSuggestions.indexOf(value) - setHighlightedIndex(Math.max(selectedIndex, 0)) - } - } - - // Why: notify the consumer of the currently-highlighted font so it can - // render a live preview. Closing the dropdown or moving past all options - // clears the preview back to the committed value. - useEffect(() => { - if (!onPreviewFontFamily) { - return - } - if (!open || highlightedIndex < 0) { - onPreviewFontFamily(null) - return - } - onPreviewFontFamily(visibleSuggestions[highlightedIndex] ?? null) - }, [visibleSuggestions, highlightedIndex, onPreviewFontFamily, open]) - - const commitValue = (nextValue: string): void => { - setQuery(nextValue) - setIsFilteringQuery(false) - onChange(nextValue) - setOpen(false) - } - - const focusInput = (): void => { - inputRef.current?.focus() - } - const popoverAvailableHeightStyle = { - // Why: tailwind-merge rewrites this arbitrary max-height class on the - // ScrollArea root, so keep the Radix available-height clamp as inline style. - maxHeight: 'var(--radix-popover-content-available-height)' - } as React.CSSProperties - - return ( -
- - -
- { - const next = e.target.value - requestSuggestions() - setQuery(next) - setIsFilteringQuery(true) - onChange(next) - setOpen(true) - }} - onFocus={() => { - requestSuggestions() - setIsFilteringQuery(false) - setOpen(true) - }} - onKeyDown={(e) => { - if (e.key === 'Escape') { - if (open) { - e.preventDefault() - setOpen(false) - setIsFilteringQuery(false) - } - return - } - - if (e.key === 'ArrowDown') { - e.preventDefault() - setOpen(true) - if (visibleSuggestions.length > 0) { - setHighlightedIndex((current) => - current < 0 ? 0 : Math.min(current + 1, visibleSuggestions.length - 1) - ) - } - return - } - - if (e.key === 'ArrowUp') { - e.preventDefault() - setOpen(true) - if (visibleSuggestions.length > 0) { - setHighlightedIndex((current) => - current < 0 ? visibleSuggestions.length - 1 : Math.max(current - 1, 0) - ) - } - return - } - - if (e.key === 'Enter' && open && highlightedIndex >= 0) { - const highlightedFont = visibleSuggestions[highlightedIndex] - if (highlightedFont) { - e.preventDefault() - commitValue(highlightedFont) - } - } - }} - placeholder={placeholder} - className="pr-18" - role="combobox" - aria-autocomplete="list" - aria-expanded={open} - aria-controls={listboxId} - aria-activedescendant={ - open && highlightedIndex >= 0 - ? `${listboxId}-option-${highlightedIndex}` - : undefined - } - /> -
- {query ? ( - - ) : null} - -
-
-
- - {/* Why: portal the dropdown outside the settings section — an in-flow - absolute panel makes the highlighted option's scrollIntoView scroll - the whole settings pane, pushing the section content out of view. */} - e.preventDefault()} - onCloseAutoFocus={(e) => e.preventDefault()} - onInteractOutside={(e) => { - // Why: the input and its clear/toggle buttons are the anchor, not - // the content, so Radix would otherwise dismiss on every click there. - if (rootRef.current?.contains(e.target as Node)) { - e.preventDefault() - } - }} - > - 8 ? 'h-64' : undefined} - style={popoverAvailableHeightStyle} - viewportProps={{ style: popoverAvailableHeightStyle }} - > -
- {visibleSuggestions.length > 0 ? ( - renderedSuggestions.map(({ font, sourceIndex }) => ( - - )) - ) : ( -
- {translate( - 'auto.components.settings.SettingsFormControls.42a4d15a30', - 'No matching fonts.' - )} -
- )} -
-
-
-
-
- ) -} diff --git a/src/renderer/src/components/settings/TerminalThemePicker.tsx b/src/renderer/src/components/settings/TerminalThemePicker.tsx new file mode 100644 index 00000000000..f37d3a5492e --- /dev/null +++ b/src/renderer/src/components/settings/TerminalThemePicker.tsx @@ -0,0 +1,207 @@ +import type React from 'react' +import { useEffect, useRef, useState } from 'react' +import { ScrollArea } from '../ui/scroll-area' +import { Input } from '../ui/input' +import { Label } from '../ui/label' +import { MAX_THEME_RESULTS } from './SettingsConstants' +import { + filterTerminalThemeOptions, + isSettingsFormOptionQueryTooLarge +} from './settings-form-option-filter' +import { cn } from '@/lib/utils' +import { translate } from '@/i18n/i18n' +import type { TerminalThemeOption } from '@/lib/terminal-theme' + +type ThemePickerProps = { + label: string + description: string + selectedTheme: string + themeOptions: TerminalThemeOption[] + query: string + onQueryChange: (value: string) => void + onSelectTheme: (theme: string) => void + /** Bumps when themes are imported; scrolls the Imported group into view and + * briefly highlights it so freshly-imported themes are easy to find. */ + importedHighlightSignal?: number +} + +export function ThemePicker({ + label, + description, + selectedTheme, + themeOptions, + query, + onQueryChange, + onSelectTheme, + importedHighlightSignal +}: ThemePickerProps): React.JSX.Element { + const importedGroupRef = useRef(null) + const [highlightImported, setHighlightImported] = useState(false) + + // Why: imported themes render below the built-in list inside a fixed-height + // scroll area, so after an import they sit off-screen. On each import signal, + // scroll the Imported group into view and flash a highlight so it's easy to spot. + useEffect(() => { + if (!importedHighlightSignal) { + return + } + importedGroupRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) + setHighlightImported(true) + const timer = setTimeout(() => setHighlightImported(false), 2000) + return () => clearTimeout(timer) + }, [importedHighlightSignal]) + + const themeQuery = query.trim() + const shouldShowThemeQueryLabel = + themeQuery.length > 0 && !isSettingsFormOptionQueryTooLarge(themeQuery) + const matchingThemes = filterTerminalThemeOptions(themeOptions, query) + const selectedThemeLabel = + themeOptions.find((option) => option.value === selectedTheme)?.label ?? selectedTheme + const groupedThemes = [ + { + label: translate('auto.components.settings.SettingsFormControls.builtin_themes', 'Built-in'), + themes: matchingThemes + .filter((theme) => theme.group === 'built-in') + .slice(0, MAX_THEME_RESULTS) + }, + { + label: translate('auto.components.settings.SettingsFormControls.imported_themes', 'Imported'), + themes: matchingThemes + .filter((theme) => theme.group === 'imported') + .slice(0, MAX_THEME_RESULTS) + } + ].filter((group) => group.themes.length > 0) + const visibleThemeCount = groupedThemes.reduce((sum, group) => sum + group.themes.length, 0) + + return ( +
+
+ +

{description}

+
+ onQueryChange(e.target.value)} + placeholder={translate( + 'auto.components.settings.SettingsFormControls.search_terminal_themes', + 'Search terminal themes' + )} + /> +
+
+ + {translate('auto.components.settings.SettingsFormControls.fbb428db98', 'Selected:')}{' '} + {selectedThemeLabel} + + + {translate('auto.components.settings.SettingsFormControls.4e11f87ca6', 'Showing')}{' '} + {visibleThemeCount} + {shouldShowThemeQueryLabel + ? translate( + 'auto.components.settings.SettingsFormControls.c822571b2e', + ' matching "{{value0}}"', + { value0: themeQuery } + ) + : translate( + 'auto.components.settings.SettingsFormControls.cb330ef7f8', + ' of {{value0}}', + { value0: themeOptions.length } + )} + +
+ +
+ {groupedThemes.map((group) => { + const isImported = + group.label === + translate( + 'auto.components.settings.SettingsFormControls.imported_themes', + 'Imported' + ) + return ( +
+

+ {group.label} +

+ {group.themes.map((theme) => ( + + ))} +
+ ) + })} + {visibleThemeCount === 0 ? ( +
+ {translate( + 'auto.components.settings.SettingsFormControls.ceefb9d7f1', + 'No themes found.' + )} +
+ ) : null} +
+
+
+
+ ) +}