Autocomplete
Autocomplete — кастомный компонент, портированный из ReUI (keenthemes/reui, MIT).
Загрузка превью…
src/reui/autocomplete/Autocomplete.vue
<script setup lang="ts">
/**
* Порт ReUI Autocomplete (registry-reui/bases/radix/reui/autocomplete.tsx, MIT).
*
* Замена примитива: у `@base-ui/react/autocomplete` нет Vue-порта. Задание
* называет заменой `Combobox` из reka-ui (как уже сделано в phone-input),
* но у reka-ui на самом деле есть ОТДЕЛЬНОЕ семейство `Autocomplete*`
* (`AutocompleteRoot`/`AutocompleteInput` — собственные компоненты, не
* просто алиасы; остальные части — `AutocompleteTrigger/Content/Item/
* Group/Label/Empty/Cancel/Portal/Separator/Arrow/Viewport` — это
* переэкспорт соответствующих Combobox-примитивов reka под именем
* Autocomplete). Семантически `AutocompleteRoot` ближе к Base UI
* Autocomplete, чем `ComboboxRoot`: значение — это набираемый текст
* (`modelValue: string`), а не выбранный объект-значение, и фильтрация
* работает по тому, что реально введено в поле. Поэтому здесь используется
* именно `Autocomplete*` из reka-ui, а не `Combobox*`.
*
* Расхождение API (осознанное): оригинальный `AutocompletePrimitive.Root`
* принимает проп `items` и рендерит элементы списка через render-проп
* (`children: (item) => ReactNode` на `AutocompleteList`). У reka-ui нет
* аналога — элементы объявляются явно через `v-for` + `AutocompleteItem`,
* как в идиоматичном использовании reka (см. также `autoHighlight`,
* которого нет в `AutocompleteRootProps` reka-ui — ближайший аналог
* `highlightOnHover`, но это другая семантика, поэтому не подставляется
* незаметно, а просто не поддерживается).
*
* `Autocomplete` в оригинале — это чистый алиас `AutocompletePrimitive.Root`
* без своей обёртки и без `data-slot`. Здесь то же самое: обёртка нужна
* только чтобы прокинуть типизированные пропы/события reka-ui.
*/
import {
AutocompleteRoot as AutocompleteRootPrimitive,
type AutocompleteRootEmits,
type AutocompleteRootProps,
} from "reka-ui"
defineOptions({ inheritAttrs: false })
defineProps<AutocompleteRootProps>()
const emit = defineEmits<AutocompleteRootEmits>()
</script>
<template>
<AutocompleteRootPrimitive
v-bind="{ ...$props, ...$attrs }"
@update:model-value="(v) => emit('update:modelValue', v)"
@update:open="(v) => emit('update:open', v)"
@highlight="(v) => emit('highlight', v)"
>
<slot />
</AutocompleteRootPrimitive>
</template>
src/reui/autocomplete/AutocompleteArrow.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Arrow`. Примитив — `AutocompleteArrow`
* из reka-ui (переэкспорт `ComboboxArrow`); рендерится только когда попап
* открыт и позиционирование — `popper` (см. AutocompletePositioner.vue).
*/
import { AutocompleteArrow as AutocompleteArrowPrimitive } from "reka-ui"
</script>
<template>
<AutocompleteArrowPrimitive data-slot="autocomplete-arrow">
<slot />
</AutocompleteArrowPrimitive>
</template>
src/reui/autocomplete/AutocompleteBackdrop.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Backdrop` — затемняющий слой позади
* попапа, монтируемый только пока попап открыт (внутри Base UI это
* `Presence`). У reka-ui нет отдельного примитива backdrop для
* Combobox/Autocomplete (это не модальный компонент), поэтому здесь —
* обычный `<div>`, условно рендерящийся по общему для Combobox/Autocomplete
* контексту reka-ui (`injectComboboxRootContext`, который `AutocompleteRoot`
* тоже провайдит — см. Autocomplete.vue). Не используется ни в одном
* демо ReUI по умолчанию (`showBackdrop` в `AutocompleteContent` по
* умолчанию `false`), поэтому не покрыт кейсом.
*/
import { injectComboboxRootContext } from "reka-ui"
const rootContext = injectComboboxRootContext()
</script>
<template>
<div v-if="rootContext.open.value" data-slot="autocomplete-backdrop">
<slot />
</div>
</template>
src/reui/autocomplete/AutocompleteClear.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Clear`. Ближайший аналог в reka-ui —
* `AutocompleteCancel` (переэкспорт `ComboboxCancel`): сбрасывает значение
* и фокусирует поле ввода. Иконка `XIcon` — инлайновый `<svg>` вместо
* `IconPlaceholder` (см. AutocompleteTrigger.vue).
*
* Расхождение примитивов: React `ComboboxClear` рендерит `null`, пока
* поле ввода пустое (`visible = inputValue !== ''`), поэтому в React
* DOM-узла клира просто нет и CSS-селектор `has-[+[data-slot=
* autocomplete-clear]]:hidden` на триггере не матчится. `ComboboxCancel`
* из reka-ui рендерится безусловно — без этого `v-if` клир всегда
* присутствует в DOM, и триггер (когда заданы оба showTrigger+showClear)
* навсегда скрыт тем же CSS-селектором, даже при пустом поле.
*/
import type { HTMLAttributes } from "vue"
import { AutocompleteCancel as AutocompleteClearPrimitive, injectAutocompleteRootContext } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const rootContext = injectAutocompleteRootContext()
</script>
<template>
<AutocompleteClearPrimitive
v-if="!!rootContext.modelValue.value"
data-slot="autocomplete-clear"
:class="
cn(
'ring-offset-background focus:ring-ring absolute top-1/2 -translate-y-1/2 cursor-pointer opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none data-disabled:pointer-events-none',
props.class
)
"
>
<slot>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</slot>
</AutocompleteClearPrimitive>
</template>
src/reui/autocomplete/AutocompleteCollection.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Collection` — служебный компонент Base
* UI для индексации детей (используется вместе с render-проп `items`,
* который в этом порте не поддержан, см. Autocomplete.vue); сам по себе
* не рендерит собственный DOM-узел, только регистрирует children в
* контексте коллекции. У reka-ui нет аналога (элементы находятся через
* обычный `v-for`), поэтому здесь — прозрачный проброс `<slot />` без
* обёртки, ничего не добавляющий в дерево.
*/
</script>
<template>
<slot />
</template>
src/reui/autocomplete/AutocompleteContent.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompleteContent` — составной компонент, собирающий
* Portal + (опционально) Backdrop + Positioner + стилизованный Popup.
* Здесь та же композиция из соседних портов (AutocompletePortal.vue,
* AutocompleteBackdrop.vue, AutocompletePositioner.vue); "Popup" — простой
* `<div data-slot="autocomplete-popup">` внутри Positioner'а, т.к. в самом
* Base UI `Popup` тоже не несёт поведения помимо стилизации (позиционирует
* именно Positioner). См. AutocompletePositioner.vue про CSS-переменные
* позиционирования, которые не совпадают с апстримом в открытом состоянии.
*/
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import AutocompletePortal from "./AutocompletePortal.vue"
import AutocompleteBackdrop from "./AutocompleteBackdrop.vue"
import AutocompletePositioner from "./AutocompletePositioner.vue"
import type { AutocompleteContentProps as AutocompletePositionerProps } from "reka-ui"
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
showBackdrop?: boolean
align?: AutocompletePositionerProps["align"]
sideOffset?: AutocompletePositionerProps["sideOffset"]
alignOffset?: AutocompletePositionerProps["alignOffset"]
side?: AutocompletePositionerProps["side"]
anchor?: unknown
}>(),
{
showBackdrop: false,
align: "start",
sideOffset: 4,
alignOffset: 0,
side: "bottom",
}
)
</script>
<template>
<AutocompletePortal>
<AutocompleteBackdrop v-if="props.showBackdrop" />
<AutocompletePositioner
:align="props.align"
:side-offset="props.sideOffset"
:align-offset="props.alignOffset"
:side="props.side"
:anchor="props.anchor"
>
<div class="relative flex max-h-full">
<div
v-bind="$attrs"
data-slot="autocomplete-popup"
:class="
cn(
'bg-popover text-popover-foreground style-vega:rounded-md style-nova:rounded-lg style-maia:rounded-2xl style-lyra:rounded-none style-mira:rounded-lg style-luma:rounded-3xl style-rhea:rounded-2xl style-sera:rounded-none shadow-md ring-foreground/10 flex max-h-[min(var(--available-height),24rem)] w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) scroll-pt-2 scroll-pb-2 flex-col overscroll-contain py-0.5 ring-1 transition-[scale,opacity] has-data-starting-style:scale-98 has-data-starting-style:opacity-0 has-data-[side=none]:scale-100 has-data-[side=none]:transition-none',
props.class
)
"
>
<slot />
</div>
</div>
</AutocompletePositioner>
</AutocompletePortal>
</template>
src/reui/autocomplete/AutocompleteEmpty.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Empty`. Примитив — `AutocompleteEmpty`
* из reka-ui (переэкспорт `ComboboxEmpty`), сам решает, показываться ли,
* по количеству отфильтрованных элементов — так же, как в Base UI.
*/
import type { HTMLAttributes } from "vue"
import { AutocompleteEmpty as AutocompleteEmptyPrimitive } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<AutocompleteEmptyPrimitive
data-slot="autocomplete-empty"
:class="cn('text-muted-foreground px-2 py-1.5 text-sm text-center empty:m-0 empty:p-0', props.class)"
>
<slot />
</AutocompleteEmptyPrimitive>
</template>
src/reui/autocomplete/AutocompleteGroup.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Group`. Примитив — `AutocompleteGroup`
* из reka-ui (переэкспорт `ComboboxGroup`).
*/
import { AutocompleteGroup as AutocompleteGroupPrimitive } from "reka-ui"
</script>
<template>
<AutocompleteGroupPrimitive data-slot="autocomplete-group">
<slot />
</AutocompleteGroupPrimitive>
</template>
src/reui/autocomplete/AutocompleteGroupLabel.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.GroupLabel`. Примитив — `AutocompleteLabel`
* из reka-ui (переэкспорт `ComboboxLabel`; называется иначе, но это тот же
* заголовок группы, привязанный `aria-labelledby` к `AutocompleteGroup`).
*/
import type { HTMLAttributes } from "vue"
import { AutocompleteLabel as AutocompleteGroupLabelPrimitive } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<AutocompleteGroupLabelPrimitive
data-slot="autocomplete-group-label"
:class="cn('text-muted-foreground px-1.5 py-1 text-xs font-medium', props.class)"
>
<slot />
</AutocompleteGroupLabelPrimitive>
</template>
src/reui/autocomplete/AutocompleteInput.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Input` + обёртка `showClear`/`showTrigger`.
* Примитив ввода — `AutocompleteInput` из reka-ui (собственный, не алиас
* `ComboboxInput`): значение — набираемый текст, синхронизированный с
* `AutocompleteRoot` через `injectAutocompleteRootContext` (см. комментарий
* в Autocomplete.vue).
*/
import type { HTMLAttributes } from "vue"
import { AutocompleteInput as AutocompleteInputPrimitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { inputVariants, type InputVariants } from "./variants"
import AutocompleteTrigger from "./AutocompleteTrigger.vue"
import AutocompleteClear from "./AutocompleteClear.vue"
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
size?: InputVariants["size"]
showClear?: boolean
showTrigger?: boolean
}>(),
{
size: "default",
showClear: false,
showTrigger: false,
}
)
</script>
<template>
<div class="relative w-full">
<AutocompleteInputPrimitive
v-bind="$attrs"
data-slot="autocomplete-input"
:data-size="props.size"
:class="cn(inputVariants({ size: props.size }), props.class)"
/>
<AutocompleteTrigger v-if="props.showTrigger" />
<AutocompleteClear v-if="props.showClear" />
</div>
</template>
src/reui/autocomplete/AutocompleteItem.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Item`. Примитив — `AutocompleteItem`
* из reka-ui (переэкспорт `ComboboxItem`): `value`/`textValue`/`disabled`
* работают так же, как в Base UI.
*/
import type { HTMLAttributes } from "vue"
import { AutocompleteItem as AutocompleteItemPrimitive } from "reka-ui"
import { cn } from "@/lib/utils"
// Пропы объявлены явно (а не через `AutocompleteItemProps` reka-ui) —
// `defineProps<T>()` не умеет извлечь рантайм-пропы из внешнего
// generic-типа, импортированного из node_modules (ошибка компилятора
// Vue SFC "Unresolvable type reference or unsupported built-in utility
// type"). Поведение то же самое: `value`/`textValue`/`disabled`
// прокидываются в примитив как есть.
const props = defineProps<{
class?: HTMLAttributes["class"]
value: string | number | bigint | Record<string, unknown> | null
textValue?: string
disabled?: boolean
}>()
</script>
<template>
<AutocompleteItemPrimitive
:value="props.value"
:disabled="props.disabled"
:text-value="props.textValue"
data-slot="autocomplete-item"
:class="
cn(
'text-foreground data-highlighted:text-foreground data-highlighted:before:bg-accent gap-1.5',
'style-vega:rounded-sm style-nova:rounded-md style-maia:rounded-xl style-lyra:rounded-none style-mira:rounded-md style-luma:rounded-2xl style-rhea:rounded-2xl style-sera:rounded-none',
'style-vega:data-highlighted:before:rounded-sm style-nova:data-highlighted:before:rounded-md style-maia:data-highlighted:before:rounded-xl style-lyra:data-highlighted:before:rounded-none style-mira:data-highlighted:before:rounded-md style-luma:data-highlighted:before:rounded-2xl style-rhea:data-highlighted:before:rounded-2xl style-sera:data-highlighted:before:rounded-none',
`px-1.5 py-1 text-sm ([class*='size-'])]:size-4 ([class*='size-'])]:size-4 [&_svg:not([class*='size-'])]:size-4 ([class*='size-'])]:size-4 ([class*='size-'])]:size-3.5 ([class*='size-'])]:size-4 ([class*='size-'])]:size-3.5 relative flex cursor-default items-center outline-hidden transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:relative data-highlighted:z-0 data-highlighted:before:absolute data-highlighted:before:inset-x-0 data-highlighted:before:inset-y-0 data-highlighted:before:z-[-1] [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([role=img]):not([class*=text-])]:opacity-60`,
props.class
)
"
>
<slot />
</AutocompleteItemPrimitive>
</template>
src/reui/autocomplete/AutocompleteList.vue
<script setup lang="ts">
/**
* Оригинал оборачивает `AutocompletePrimitive.List` в shadcn `ScrollArea`
* (`bases/base/ui/scroll-area`) — общий ui-примитив вне области порта
* ReUI-компонента. Как и в `PhoneInputCountrySelect.vue`, разметка и
* классы `ScrollArea`/`ScrollAreaViewport` (`cn-scroll-area`,
* `cn-scroll-area-viewport` — уже вендорены в per-style CSS) воспроизведены
* инлайново, без `ScrollBar`/`Corner` (они условно рендерятся только при
* реальном переполнении и не участвуют в закрытом состоянии, которое
* покрывает кейс).
*
* `scrollFade`/`scrollbarGutter` объявлены в оригинальном типе
* `AutocompleteList`, но ни на что не влияют и в самой функции ReUI —
* поведение не "чинится", пропы просто принимаются для API-совместимости
* (см. docs/PORTING.md, 4a).
*/
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
scrollAreaClassName?: HTMLAttributes["class"]
scrollFade?: boolean
scrollbarGutter?: boolean
}>()
</script>
<template>
<div
data-slot="scroll-area"
:class="
cn(
'cn-scroll-area relative',
'size-full min-h-0 **:data-[slot=scroll-area-viewport]:h-full **:data-[slot=scroll-area-viewport]:overscroll-contain',
scrollAreaClassName
)
"
>
<div
data-slot="scroll-area-viewport"
class="cn-scroll-area-viewport size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
<div
data-slot="autocomplete-list"
:class="cn('not-empty:px-1 not-empty:py-1 not-empty:scroll-py-1 in-data-has-overflow-y:me-3', props.class)"
>
<slot />
</div>
</div>
</div>
</template>
src/reui/autocomplete/AutocompletePortal.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Portal`. Прямой аналог в reka-ui —
* `AutocompletePortal` (переэкспорт `ComboboxPortal`, телепортирует
* содержимое в `document.body` по умолчанию).
*/
import { AutocompletePortal as AutocompletePortalPrimitive } from "reka-ui"
</script>
<template>
<AutocompletePortalPrimitive data-slot="autocomplete-portal">
<slot />
</AutocompletePortalPrimitive>
</template>
src/reui/autocomplete/AutocompletePositioner.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Positioner` — отдельный от `Popup` слой
* позиционирования (Base UI разносит "куда поставить" и "как покрасить"
* на два разных примитива). У reka-ui `AutocompleteContent` (переэкспорт
* `ComboboxContent`) объединяет обе роли в одном элементе floating-ui.
* Здесь `AutocompletePositioner` оборачивает именно этот объединённый
* примитив с `position="popper"`, беря на себя только слой позиционирования
* (класс `z-50 outline-none`, как в оригинале) — стилизация самого попапа
* (`bg-popover`, тени, радиусы...) добавляется отдельным `<div>` уже внутри,
* в AutocompleteContent.vue, повторяя структуру Positioner > div.relative >
* Popup из оригинала.
*
* `anchor` (кастомный DOM-референс вместо триггера) не поддержан: у
* `PopperContent` reka-ui нет прямого аналога этого пропа (свой якорь
* задаётся через `ComboboxAnchor`/`AutocompleteAnchor`-обёртку вокруг
* триггера, а не пропом на контенте) — осознанное сокращение API.
*
* CSS-переменные `--available-height`/`--available-width`/`--anchor-width`/
* `--transform-origin`, на которые опираются классы попапа (см.
* AutocompleteContent.vue), — именование Base UI. reka-ui публикует свои
* под другими именами (`--reka-popper-*`), поэтому при реально открытом
* попапе размеры/transform-origin не совпадут с апстримом побайтово. Кейс
* (как и `phone-input`) не открывает попап и проверяет только закрытое
* состояние — см. docs/PORTING.md.
*/
import type { HTMLAttributes } from "vue"
import {
AutocompleteContent as AutocompleteContentPrimitive,
type AutocompleteContentProps,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
align?: AutocompleteContentProps["align"]
sideOffset?: AutocompleteContentProps["sideOffset"]
alignOffset?: AutocompleteContentProps["alignOffset"]
side?: AutocompleteContentProps["side"]
anchor?: unknown
}>()
</script>
<template>
<AutocompleteContentPrimitive
position="popper"
data-slot="autocomplete-positioner"
:align="props.align"
:side-offset="props.sideOffset"
:align-offset="props.alignOffset"
:side="props.side"
:class="cn('z-50 outline-none', props.class)"
>
<slot />
</AutocompleteContentPrimitive>
</template>
src/reui/autocomplete/AutocompleteRow.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Row` — группировка элементов в строку
* для двумерной (grid) клавиатурной навигации Base UI. У reka-ui нет
* аналога такой навигации для Combobox/Autocomplete, поэтому здесь —
* обычный `<div>` с теми же классами, без специальной ролевой логики.
*/
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div data-slot="autocomplete-row" :class="cn('flex items-center gap-2', props.class)">
<slot />
</div>
</template>
src/reui/autocomplete/AutocompleteSeparator.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Separator`. Примитив — `AutocompleteSeparator`
* из reka-ui (переэкспорт `ComboboxSeparator`).
*/
import type { HTMLAttributes } from "vue"
import { AutocompleteSeparator as AutocompleteSeparatorPrimitive } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<AutocompleteSeparatorPrimitive
data-slot="autocomplete-separator"
:class="cn('bg-border my-1.5 h-px', props.class)"
>
<slot />
</AutocompleteSeparatorPrimitive>
</template>
src/reui/autocomplete/AutocompleteStatus.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Status` — живая область (`aria-live`)
* с числом найденных элементов, целиком обёрнутая вокруг переданного
* содержимого без собственной логики подсчёта в этом файле (подсчёт живёт
* внутри Base UI). У reka-ui нет отдельного примитива статуса для
* Combobox/Autocomplete, а публичного доступа к счётчику отфильтрованных
* элементов (`filterState.count`) reka-ui не экспортирует. Здесь —
* обычный `role="status"` контейнер, прозрачно передающий содержимое
* через `<slot />`; текст статуса собирает сам потребитель компонента,
* как и в оригинале, где содержимое `Status` тоже задаётся снаружи.
*/
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="autocomplete-status"
role="status"
aria-live="polite"
:class="cn('text-muted-foreground px-2 py-1.5 text-sm empty:m-0 empty:p-0', props.class)"
>
<slot />
</div>
</template>
src/reui/autocomplete/AutocompleteTrigger.vue
<script setup lang="ts">
/**
* Оригинал использует `IconPlaceholder` (dev-обвязка сайта ReUI, не
* компилируется в этом воркспейсе) для иконки `ChevronsUpDownIcon`.
* Здесь — инлайновый `<svg>` с тем же путём (lucide `chevrons-up-down`),
* как запасной вариант через `<slot>`, что и в остальных портах (см.
* NumberFieldIncrement.vue/NumberFieldDecrement.vue).
*/
import type { HTMLAttributes } from "vue"
import { AutocompleteTrigger as AutocompleteTriggerPrimitive } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
disabled?: boolean
}>()
</script>
<template>
<AutocompleteTriggerPrimitive
:disabled="props.disabled"
data-slot="autocomplete-trigger"
:class="
cn(
'focus:ring-ring ring-offset-background absolute top-1/2 -translate-y-1/2 cursor-pointer focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none has-[+[data-slot=autocomplete-clear]]:hidden data-disabled:pointer-events-none',
props.class
)
"
>
<slot>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="size-4 opacity-70"
>
<path d="m7 15 5 5 5-5" />
<path d="m7 9 5-5 5 5" />
</svg>
</slot>
</AutocompleteTriggerPrimitive>
</template>
src/reui/autocomplete/AutocompleteValue.vue
<script setup lang="ts">
/**
* Оригинал: `AutocompletePrimitive.Value` — компонент-рендер-проп,
* отображающий текущее значение поля (текст) без собственной стилизации
* кроме `data-slot`. У reka-ui нет отдельного компонента "Value" для
* Autocomplete (в отличие от `SelectValue`), но есть публично
* экспортированный `injectAutocompleteRootContext`, дающий доступ к тому
* же `modelValue`, которым управляет `AutocompleteInput`. Скоуп-слот
* передаёт значение наружу — аналог render-проп `children(value)` в React.
*/
import { injectAutocompleteRootContext } from "reka-ui"
const context = injectAutocompleteRootContext()
</script>
<template>
<span data-slot="autocomplete-value">
<slot :value="context.modelValue.value">{{ context.modelValue.value }}</slot>
</span>
</template>
src/reui/autocomplete/index.ts
export { default as Autocomplete } from "./Autocomplete.vue"
export { default as AutocompleteValue } from "./AutocompleteValue.vue"
export { default as AutocompleteTrigger } from "./AutocompleteTrigger.vue"
export { default as AutocompleteInput } from "./AutocompleteInput.vue"
export { default as AutocompleteStatus } from "./AutocompleteStatus.vue"
export { default as AutocompletePortal } from "./AutocompletePortal.vue"
export { default as AutocompleteBackdrop } from "./AutocompleteBackdrop.vue"
export { default as AutocompletePositioner } from "./AutocompletePositioner.vue"
export { default as AutocompleteContent } from "./AutocompleteContent.vue"
export { default as AutocompleteList } from "./AutocompleteList.vue"
export { default as AutocompleteCollection } from "./AutocompleteCollection.vue"
export { default as AutocompleteRow } from "./AutocompleteRow.vue"
export { default as AutocompleteItem } from "./AutocompleteItem.vue"
export { default as AutocompleteGroup } from "./AutocompleteGroup.vue"
export { default as AutocompleteGroupLabel } from "./AutocompleteGroupLabel.vue"
export { default as AutocompleteEmpty } from "./AutocompleteEmpty.vue"
export { default as AutocompleteClear } from "./AutocompleteClear.vue"
export { default as AutocompleteArrow } from "./AutocompleteArrow.vue"
export { default as AutocompleteSeparator } from "./AutocompleteSeparator.vue"
export { inputVariants, type InputVariants } from "./variants"
src/reui/autocomplete/variants.ts
import { cva, type VariantProps } from "class-variance-authority"
/**
* Перенесено дословно из ReUI (registry-reui/bases/radix/reui/autocomplete.tsx, MIT).
*
* cva-блоки — чистые JS-объекты и переносятся между React и Vue без единой
* правки. Это и есть главный рычаг всего порта: вся визуальная семантика
* живёт здесь, а фреймворк-специфичной остаётся только разметка.
*/
export const inputVariants = cva(
"outline-none flex w-full text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [[readonly]]:bg-muted/80 [[readonly]]:cursor-not-allowed border border-input focus-visible:border-ring aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 style-vega:rounded-md style-nova:rounded-lg style-maia:rounded-4xl style-lyra:rounded-none style-mira:rounded-md style-luma:rounded-3xl style-rhea:rounded-2xl style-sera:rounded-none bg-transparent dark:bg-input/30 text-sm transition-colors focus-visible:ring-ring/50 focus-visible:ring-3 aria-invalid:ring-3",
{
variants: {
size: {
sm: "h-7 px-2 [&~[data-slot=autocomplete-clear]]:end-1.5 [&~[data-slot=autocomplete-trigger]]:end-1.5",
default:
"h-8 px-2.5 [&~[data-slot=autocomplete-clear]]:end-1.75 [&~[data-slot=autocomplete-trigger]]:end-1.75",
lg: "h-9 px-2.5 [&~[data-slot=autocomplete-clear]]:end-2 [&~[data-slot=autocomplete-trigger]]:end-2",
},
},
defaultVariants: {
size: "default",
},
}
)
export type InputVariants = VariantProps<typeof inputVariants>
Установка
npx shadcn-vue@latest add https://revueui.rootapi.dev/r/autocomplete.jsonЗависимости реестра
npm-зависимости
- class-variance-authority
- reka-ui
Источник: порт из ReUI (Keenthemes, MIT)