From a954591493d54fbd011ab226626634fe4bca06e8 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sat, 27 Jun 2026 02:11:45 +0700 Subject: [PATCH] =?UTF-8?q?feat(web):=20ReUI=20Autocomplete=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D1=8B/=D0=B3=D0=BE?= =?UTF-8?q?=D1=80=D0=BE=D0=B4=D0=B0=20=D0=B8=20=D0=B2=D0=B5=D1=80=D1=82?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B0=20=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../src/components/auto-complete-input.tsx | 193 ++++++++---------- apps/web/src/routes/_auth/vps.tsx | 54 +++-- 2 files changed, 111 insertions(+), 136 deletions(-) diff --git a/apps/web/src/components/auto-complete-input.tsx b/apps/web/src/components/auto-complete-input.tsx index 24c86d8..3c698fd 100644 --- a/apps/web/src/components/auto-complete-input.tsx +++ b/apps/web/src/components/auto-complete-input.tsx @@ -1,26 +1,22 @@ +'use client' + import * as React from 'react' -import { CheckIcon, ChevronsUpDownIcon, SearchIcon } from 'lucide-react' +import { CheckIcon } from 'lucide-react' -import { Button } from '@cfdm/ui/components/button' -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from '@cfdm/ui/components/command' -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@cfdm/ui/components/popover' import { cn } from '@cfdm/ui/lib/utils' +import { + Autocomplete, + AutocompleteContent, + AutocompleteEmpty, + AutocompleteInput, + AutocompleteItem, + AutocompleteList, +} from '@/components/reui/autocomplete' export interface AutoCompleteOption { value: string label: string - /** Левый префикс (например эмодзи-флаг). */ + /** Левый префикс (например флаг страны). */ leading?: React.ReactNode } @@ -33,7 +29,7 @@ interface AutoCompleteInputProps { searchPlaceholder?: string emptyText?: string className?: string - /** Показывать ли выбранный leading в триггере (например флаг). */ + /** Показывать ли выбранный leading в поле (например флаг). */ showLeadingInInput?: boolean /** Разрешать ли произвольный ввод (не только из списка). По умолчанию true. */ allowFreeText?: boolean @@ -45,107 +41,88 @@ export function AutoCompleteInput({ onChange, options, placeholder = 'Выбрать…', - searchPlaceholder = 'Поиск…', + searchPlaceholder, emptyText = 'Ничего не найдено', className, showLeadingInInput = true, allowFreeText = true, }: AutoCompleteInputProps) { - const [open, setOpen] = React.useState(false) - const [query, setQuery] = React.useState('') + const trimmedValue = value.trim() - const filtered = React.useMemo(() => { - const q = query.trim().toLowerCase() - if (!q) return options - return options.filter( - (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q), - ) - }, [options, query]) - - const selected = options.find( - (o) => o.value.toLowerCase() === value.trim().toLowerCase(), + const selected = React.useMemo( + () => options.find((o) => o.value.toLowerCase() === trimmedValue.toLowerCase()), + [options, trimmedValue], ) - const displayLabel = selected?.label ?? value const leading = showLeadingInInput ? selected?.leading : undefined - const handleSelect = (next: string) => { - onChange(next) - setQuery('') - setOpen(false) - } + const inputPlaceholder = searchPlaceholder ?? placeholder + + const handleValueChange = React.useCallback( + (inputVal: string) => { + const q = inputVal.trim() + const match = options.find( + (o) => + o.label.toLowerCase() === q.toLowerCase() || + o.value.toLowerCase() === q.toLowerCase(), + ) + if (match) { + onChange(match.value) + return + } + if (allowFreeText) { + onChange(inputVal) + } + }, + [options, onChange, allowFreeText], + ) return ( - { - setOpen(o) - if (!o) setQuery('') - }}> - - - {leading ? {leading} : null} - - {value ? displayLabel : placeholder} - - - - - } - /> - - - - - {emptyText} - - {allowFreeText && query.trim() && !filtered.some( - (o) => o.label.toLowerCase() === query.trim().toLowerCase(), - ) ? ( - handleSelect(query.trim())} - className="gap-2.5" - > - - - Использовать: {query.trim()} - - - ) : null} - {filtered.map((opt) => { - const isSelected = opt.value.toLowerCase() === value.trim().toLowerCase() - return ( - handleSelect(opt.value)} - className="gap-2.5" - > - {opt.leading ? ( - {opt.leading} - ) : null} - {opt.label} - {isSelected ? : null} - - ) - })} - - - - - + item.label} + mode="list" + autoHighlight + openOnInputClick + > +
+ {leading ? ( + + {leading} + + ) : null} + +
+ + {emptyText} + + {(item) => { + const isSelected = item.value.toLowerCase() === trimmedValue.toLowerCase() + return ( + + {item.leading ? ( + {item.leading} + ) : null} + {item.label} + {isSelected ? ( + + ) : null} + + ) + }} + + +
) } diff --git a/apps/web/src/routes/_auth/vps.tsx b/apps/web/src/routes/_auth/vps.tsx index 9dc1ebc..4215e29 100644 --- a/apps/web/src/routes/_auth/vps.tsx +++ b/apps/web/src/routes/_auth/vps.tsx @@ -421,34 +421,32 @@ function VpsPage() { -
- - setValue('country', v)} - options={countryOptions} - searchPlaceholder="Поиск страны…" - emptyText="Нет вариантов" - /> - - - setValue('city', v)} - options={cityOptions} - searchPlaceholder="Поиск города…" - emptyText="Нет вариантов" - showLeadingInInput={false} - /> - - - - -
+ + setValue('country', v)} + options={countryOptions} + searchPlaceholder="Поиск страны…" + emptyText="Нет вариантов" + /> + + + setValue('city', v)} + options={cityOptions} + searchPlaceholder="Поиск города…" + emptyText="Нет вариантов" + showLeadingInInput={false} + /> + + + +