reui

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)