primitives

Combobox

Combobox — базовый примитив RevueUI (Reka UI + shadcn-совместимый API).

Загрузка превью…

src/ui/combobox/ComboboxChip.vue

<script setup lang="ts">
/**
 * Оригинал: `ComboboxPrimitive.Chip` + `ComboboxPrimitive.ChipRemove`
 * (Base UI), рендерит remove-кнопку через `render={<Button variant="ghost"
 * size="icon-xs" />}`. У reka-ui нет отдельного примитива чипсов (см.
 * `ComboboxChips.vue`) — здесь тег и кнопка удаления собраны вручную:
 * `<span>`-тег и `<button>` с классом, вычисленным из `buttonVariants`
 * (см. `variants.ts`), но без встроенной логики "удалить значение из
 * modelValue" (её нет в reka-ui `Combobox`, приложение обязано слушать
 * `@click` и обновлять `v-model` само — задокументированное упрощение).
 */
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { comboboxChipRemoveButtonClass } from "./variants"

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes["class"]
    showRemove?: boolean
  }>(),
  {
    showRemove: true,
  }
)

const emit = defineEmits<{ remove: [] }>()
</script>

<template>
  <span
    data-slot="combobox-chip"
    :class="
      cn(
        'cn-combobox-chip has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50',
        props.class
      )
    "
  >
    <slot />
    <button
      v-if="props.showRemove"
      type="button"
      data-slot="combobox-chip-remove"
      :class="comboboxChipRemoveButtonClass"
      @click="emit('remove')"
    >
      <svg
        class="cn-combobox-chip-indicator-icon pointer-events-none"
        xmlns="http://www.w3.org/2000/svg"
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <path d="M18 6 6 18" />
        <path d="m6 6 12 12" />
      </svg>
    </button>
  </span>
</template>

src/ui/combobox/ComboboxChips.vue

<script setup lang="ts">
/**
 * Оригинал: `ComboboxPrimitive.Chips` (Base UI) — контейнер тегов
 * выбранных значений для множественного выбора. У reka-ui `Combobox`
 * нет отдельного примитива для чипсов (архитектурное отличие: `multiple`
 * в reka-ui управляет только `modelValue`/выделением в списке, разметку
 * тегов приложение собирает само). Здесь — обычный `<div>` с тем же
 * `data-slot`/классом, без скрытой логики примитива: это сознательное
 * упрощение, а не сокрытая эквивалентность. См. также комментарий в
 * `ComboboxContent.vue` про непокрытый `anchor`-пропс для chips-режима.
 */
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div data-slot="combobox-chips" :class="cn('cn-combobox-chips', props.class)">
    <slot />
  </div>
</template>

src/ui/combobox/ComboboxChipsInput.vue

<script setup lang="ts">
/**
 * Оригинал: тот же `ComboboxPrimitive.Input`, что и `ComboboxInput`, но
 * с другим набором классов — для строки ввода внутри `ComboboxChips`
 * (без своей `InputGroup`-обвязки, растягивается на оставшееся место).
 */
import type { HTMLAttributes } from "vue"
import type { ComboboxInputProps } from "reka-ui"
import { ComboboxInput } from "reka-ui"
import { cn } from "@/lib/utils"

defineProps<ComboboxInputProps & { class?: HTMLAttributes["class"] }>()
</script>

<template>
  <ComboboxInput
    data-slot="combobox-chip-input"
    v-bind="$props"
    :class="cn('cn-combobox-chip-input min-w-16 flex-1 outline-none', $props.class)"
  />
</template>

src/ui/combobox/ComboboxClear.vue

<script setup lang="ts">
/**
 * Оригинал рендерит `Combobox.Clear` через `render={<InputGroupButton
 * variant="ghost" size="icon-xs" />}` (Base UI `render`-паттерн: разметка
 * `InputGroupButton` подменяет собственный DOM `Clear`, поведение
 * примитива накладывается поверх). У reka-ui `ComboboxCancel` делает то
 * же самое по смыслу (Root shadcn/vue называет её `Cancel`, а не `Clear`,
 * но семантика идентична: сброс поискового термина/значения). Здесь
 * итоговый класс кнопки — заранее вычисленный `cn(...)` от
 * `InputGroupButton(variant: "ghost", size: "icon-xs")`, см. `variants.ts`.
 * Иконка — инлайновый `<svg>` (`XIcon` из `lucide-react`).
 */
import type { HTMLAttributes } from "vue"
import { ComboboxCancel } from "reka-ui"
import { cn } from "@/lib/utils"
import { comboboxClearButtonClass } from "./variants"

const props = defineProps<{ class?: HTMLAttributes["class"]; disabled?: boolean }>()
</script>

<template>
  <ComboboxCancel
    data-slot="combobox-clear"
    data-variant="ghost"
    data-size="icon-xs"
    :disabled="props.disabled"
    :class="cn('cn-combobox-clear', comboboxClearButtonClass, props.class)"
  >
    <svg
      class="cn-combobox-clear-icon pointer-events-none"
      xmlns="http://www.w3.org/2000/svg"
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
    >
      <path d="M18 6 6 18" />
      <path d="m6 6 12 12" />
    </svg>
  </ComboboxCancel>
</template>

src/ui/combobox/ComboboxCollection.vue

<script setup lang="ts">
/**
 * Оригинал: `ComboboxPrimitive.Collection` (Base UI) — служебный
 * компонент, регистрирующий DOM-порядок динамического списка опций для
 * клавиатурной навигации/виртуализации; сам по себе не привносит
 * видимой разметки сверх группировки children.
 *
 * У reka-ui `ComboboxItem` регистрирует себя в списке автоматически
 * (через `ListboxRoot`/`ListboxItem` context), отдельного компонента
 * уровня "Collection" нет и он не нужен. Здесь — прозрачный
 * passthrough, сохранённый как отдельный файл ради совместимости
 * сигнатуры (`<ComboboxCollection>{items}</ComboboxCollection>`
 * компилируется и рендерит `children` без изменений), но
 * функционально это no-op.
 */
</script>

<template>
  <slot />
</template>

src/ui/combobox/ComboboxContent.vue

<script setup lang="ts">
/**
 * Оригинал: `Portal + Positioner + Popup` (Base UI). У reka-ui нет
 * отдельного `Positioner` — `ComboboxContent` сам Popper-контент, как
 * `PopoverContent`/`DropdownMenuContent`; `position="popper"` включает
 * тот же попап-режим позиционирования, что и у `PhoneInputCountrySelect`
 * (`registry/../phone-input/PhoneInputCountrySelect.vue`, тот же приём
 * для того же Base UI Combobox → reka-ui Combobox перехода).
 *
 * `anchor` — в оригинале переопределяет DOM-узел, относительно которого
 * позиционируется попап (нужно в chips-режиме множественного выбора,
 * чтобы попап привязывался ко всей группе чипсов, а не к узкому `Input`).
 * У reka-ui `ComboboxContent` нет отдельного пропа под явный anchor-узел
 * (позиционирование завязано на `rootContext.triggerElement`/
 * `inputElement`, которые сам примитив регистрирует по `onMounted`
 * своих `ComboboxTrigger`/`ComboboxInput`). Проп принят ради сохранения
 * сигнатуры, но не влияет на позиционирование — задокументированный
 * пробел, аналогичный незакрытому попапу `phone-input`: раскрытое
 * состояние combobox с чипсами не гарантированно совпадает пиксель в
 * пиксель, закрытое — не завязано на эту логику вовсе.
 *
 * Строки Tailwind-классов, включая CSS-переменные Base UI-нотации
 * (`--available-height`, `--anchor-width`, `--transform-origin`),
 * перенесены буквально из `combobox.tsx`, без переименования под
 * `--reka-combobox-*` — тот же выбор, что и в `select`/`phone-input`
 * (см. комментарий в `SelectContent.vue`).
 */
import type { HTMLAttributes } from "vue"
import type { ComboboxContentProps } from "reka-ui"
import { ComboboxContent, ComboboxPortal } from "reka-ui"
import { cn } from "@/lib/utils"

const props = withDefaults(
  defineProps<
    ComboboxContentProps & {
      class?: HTMLAttributes["class"]
      anchor?: unknown
    }
  >(),
  {
    side: "bottom",
    sideOffset: 6,
    align: "start",
    alignOffset: 0,
  }
)
</script>

<template>
  <ComboboxPortal>
    <ComboboxContent
      position="popper"
      :side="props.side"
      :side-offset="props.sideOffset"
      :align="props.align"
      :align-offset="props.alignOffset"
      data-slot="combobox-content"
      :data-chips="!!props.anchor"
      :class="
        cn(
          // 'isolate z-50' в оригинале — класс обёртки `Positioner`,
          // у reka-ui Positioner и Popup слиты в один компонент.
          'isolate z-50',
          'cn-combobox-content cn-combobox-content-logical cn-menu-target cn-menu-translucent group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)',
          props.class
        )
      "
    >
      <slot />
    </ComboboxContent>
  </ComboboxPortal>
</template>

src/ui/combobox/ComboboxEmpty.vue

<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { ComboboxEmpty } from "reka-ui"
import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <ComboboxEmpty data-slot="combobox-empty" :class="cn('cn-combobox-empty', props.class)">
    <slot />
  </ComboboxEmpty>
</template>

src/ui/combobox/ComboboxGroup.vue

<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { ComboboxGroup } from "reka-ui"
import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <ComboboxGroup data-slot="combobox-group" :class="cn('cn-combobox-group', props.class)">
    <slot />
  </ComboboxGroup>
</template>

src/ui/combobox/ComboboxInput.vue

<script setup lang="ts">
/**
 * Оригинал собирает `ComboboxInput` из `InputGroup` + `InputGroupAddon` +
 * `InputGroupButton` (`registry/bases/radix/ui/input-group.tsx`) —
 * примитивы вне области этого порта (задача — только `select` и
 * `combobox`). Как и `PhoneInputCountrySelect.vue` инлайнит разметку
 * `ComboboxList`/`ScrollArea`, здесь инлайнится разметка `InputGroup`:
 * тот же DOM, те же классы (см. `variants.ts` для точных вычисленных
 * строк), но без отдельных компонентов-файлов.
 *
 * `showTrigger`/`showClear` — как в оригинале: показывает шеврон-триггер
 * и/или крестик-очистку внутри поля ввода.
 */
import type { HTMLAttributes } from "vue"
import type { ComboboxInputProps } from "reka-ui"
import { ComboboxInput } from "reka-ui"
import { cn } from "@/lib/utils"
import ComboboxClear from "./ComboboxClear.vue"
import ComboboxTrigger from "./ComboboxTrigger.vue"
import {
  comboboxInputAddonClass,
  comboboxInputControlClass,
  comboboxInputGroupClass,
  comboboxInputTriggerButtonClass,
} from "./variants"

const props = withDefaults(
  defineProps<
    ComboboxInputProps & {
      class?: HTMLAttributes["class"]
      disabled?: boolean
      showTrigger?: boolean
      showClear?: boolean
    }
  >(),
  {
    disabled: false,
    showTrigger: true,
    showClear: false,
  }
)

// Компонент — обёртка из нескольких корневых узлов (input + addon + slot),
// поэтому Vue по умолчанию кладёт fallthrough-атрибуты (например
// `placeholder`, обычный HTML-атрибут `<input>`, не объявленный пропом ни
// у reka-ui `ComboboxInput`, ни здесь) на внешний `<div data-slot=
// "input-group">`, а не на настоящий `<input>` — молча теряя их.
// `inheritAttrs: false` + явный `v-bind="$attrs"` на внутреннем
// `ComboboxInput` чинит проброс.
defineOptions({ inheritAttrs: false })

function focusInputUnlessButton(event: MouseEvent) {
  const target = event.target as HTMLElement
  if (target.closest("button")) return
  ;(event.currentTarget as HTMLElement).parentElement
    ?.querySelector("input")
    ?.focus()
}
</script>

<template>
  <div data-slot="input-group" role="group" :class="cn(comboboxInputGroupClass, props.class)">
    <ComboboxInput
      data-slot="input-group-control"
      v-bind="$attrs"
      :disabled="props.disabled"
      :display-value="props.displayValue"
      :class="comboboxInputControlClass"
    />
    <div
      data-slot="input-group-addon"
      role="group"
      data-align="inline-end"
      :class="comboboxInputAddonClass"
      @click="focusInputUnlessButton"
    >
      <ComboboxTrigger
        v-if="props.showTrigger"
        :disabled="props.disabled"
        :class="comboboxInputTriggerButtonClass"
      />
      <ComboboxClear v-if="props.showClear" :disabled="props.disabled" />
    </div>
    <slot />
  </div>
</template>

src/ui/combobox/ComboboxItem.vue

<script setup lang="ts">
/**
 * Иконка индикатора — инлайновый `<svg>` (`CheckIcon` из `lucide-react`)
 * вместо `IconPlaceholder`, тот же приём, что и в `select`.
 */
import type { HTMLAttributes } from "vue"
import type { AcceptableValue, ComboboxItemProps } from "reka-ui"
import { ComboboxItem, ComboboxItemIndicator } from "reka-ui"
import { cn } from "@/lib/utils"

const props = defineProps<
  ComboboxItemProps<AcceptableValue> & { class?: HTMLAttributes["class"] }
>()
</script>

<template>
  <ComboboxItem
    data-slot="combobox-item"
    :value="props.value"
    :disabled="props.disabled"
    :text-value="props.textValue"
    :class="
      cn(
        'cn-combobox-item relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
        props.class
      )
    "
  >
    <slot />
    <ComboboxItemIndicator class="cn-combobox-item-indicator">
      <svg
        class="cn-combobox-item-indicator-icon pointer-events-none"
        xmlns="http://www.w3.org/2000/svg"
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <path d="M20 6 9 17l-5-5" />
      </svg>
    </ComboboxItemIndicator>
  </ComboboxItem>
</template>

src/ui/combobox/ComboboxLabel.vue

<script setup lang="ts">
/**
 * Оригинал использует `ComboboxPrimitive.GroupLabel` (Base UI называет
 * заголовок группы `GroupLabel`, а не `Label`). У reka-ui — `ComboboxLabel`,
 * та же роль (заголовок `ComboboxGroup`).
 */
import type { HTMLAttributes } from "vue"
import { ComboboxLabel } from "reka-ui"
import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <ComboboxLabel data-slot="combobox-label" :class="cn('cn-combobox-label', props.class)">
    <slot />
  </ComboboxLabel>
</template>

src/ui/combobox/ComboboxList.vue

<script setup lang="ts">
/**
 * Оригинал: `ComboboxPrimitive.List` (Base UI) — контейнер списка опций
 * внутри `Popup`. У reka-ui эквивалент по роли — `ComboboxViewport`
 * (как `SelectViewport` у `Select`), но с иным набором классов
 * (`overflow-y-auto overscroll-contain` вместо scroll-контейнера
 * viewport). Здесь — `ComboboxViewport` с классами оригинального
 * `ComboboxList`, чтобы сохранить визуальный контракт `data-slot=
 * "combobox-list"`.
 */
import type { HTMLAttributes } from "vue"
import { ComboboxViewport } from "reka-ui"
import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <ComboboxViewport
    data-slot="combobox-list"
    :class="cn('cn-combobox-list overflow-y-auto overscroll-contain', props.class)"
  >
    <slot />
  </ComboboxViewport>
</template>

src/ui/combobox/ComboboxSeparator.vue

<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { ComboboxSeparator } from "reka-ui"
import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <ComboboxSeparator data-slot="combobox-separator" :class="cn('cn-combobox-separator', props.class)" />
</template>

src/ui/combobox/ComboboxTrigger.vue

<script setup lang="ts">
/**
 * Иконка — инлайновый `<svg>` (`ChevronDownIcon` из `lucide-react`)
 * вместо `IconPlaceholder`, тот же приём, что и в `select`/`phone-input`.
 * reka-ui's `ComboboxTrigger` — прямой аналог Base UI's
 * `Combobox.Trigger`: и там, и там это самостоятельная кнопка-тумблер
 * открытия/закрытия попапа (в отличие от `ComboboxInput`, который сам
 * открывает попап по фокусу/клику согласно `openOnFocus`/`openOnClick`).
 */
import type { HTMLAttributes } from "vue"
import { ComboboxTrigger } from "reka-ui"
import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"]; disabled?: boolean }>()
</script>

<template>
  <ComboboxTrigger
    data-slot="combobox-trigger"
    :disabled="props.disabled"
    :class="cn('cn-combobox-trigger', props.class)"
  >
    <slot />
    <svg
      class="cn-combobox-trigger-icon pointer-events-none"
      xmlns="http://www.w3.org/2000/svg"
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
    >
      <path d="m6 9 6 6 6-6" />
    </svg>
  </ComboboxTrigger>
</template>

src/ui/combobox/ComboboxValue.vue

<script setup lang="ts">
/**
 * Оригинал: `function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props)
 * { return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} /> }`
 * — но сам Base UI `Combobox.Value` **не рендерит собственный DOM-узел**
 * (см. официальную документацию компонента: "Doesn't render its own HTML
 * element"), поэтому переданный ему `data-slot="combobox-value"` никуда
 * не попадает — проверено эмпирически: реальный DOM апстрима содержит
 * голый текстовый узел без обёртки и без `data-slot` на нём.
 *
 * У reka-ui нет отдельного компонента "Value" для `Combobox` (архитектурное
 * отличие: reka-ui `Combobox` — это всегда редактируемый `Input` + listbox,
 * а не пара «Trigger показывает текст + Input для поиска», как у Base UI).
 * Здесь то же самое собрано вручную: `injectComboboxRootContext` даёт
 * текущий `modelValue`, слот-скоуп прокидывает его наружу для кастомного
 * рендера (как `children`-функция в оригинале) — без обёртки, как и в
 * апстриме.
 */
import { injectComboboxRootContext } from "reka-ui"

const rootContext = injectComboboxRootContext()
</script>

<template>
  <slot :value="rootContext.modelValue.value">{{ rootContext.modelValue.value }}</slot>
</template>

src/ui/combobox/index.ts

/**
 * `Combobox` в оригинале — прямой алиас Base UI-примитива без обёртки
 * (`const Combobox = ComboboxPrimitive.Root`, без `data-slot`). Здесь —
 * то же самое: реэкспорт `ComboboxRoot` из reka-ui без дополнительной
 * обёртки/`data-slot`, верность оригиналу важнее единообразия с
 * остальными частями (см. правило 4a).
 */
export { ComboboxRoot as Combobox } from "reka-ui"

export { default as ComboboxInput } from "./ComboboxInput.vue"
export { default as ComboboxContent } from "./ComboboxContent.vue"
export { default as ComboboxList } from "./ComboboxList.vue"
export { default as ComboboxItem } from "./ComboboxItem.vue"
export { default as ComboboxGroup } from "./ComboboxGroup.vue"
export { default as ComboboxLabel } from "./ComboboxLabel.vue"
export { default as ComboboxCollection } from "./ComboboxCollection.vue"
export { default as ComboboxEmpty } from "./ComboboxEmpty.vue"
export { default as ComboboxSeparator } from "./ComboboxSeparator.vue"
export { default as ComboboxChips } from "./ComboboxChips.vue"
export { default as ComboboxChip } from "./ComboboxChip.vue"
export { default as ComboboxChipsInput } from "./ComboboxChipsInput.vue"
export { default as ComboboxTrigger } from "./ComboboxTrigger.vue"
export { default as ComboboxValue } from "./ComboboxValue.vue"
export { default as ComboboxClear } from "./ComboboxClear.vue"
export { useComboboxAnchor } from "./useComboboxAnchor"

src/ui/combobox/useComboboxAnchor.ts

import { ref, type Ref } from "vue"

/**
 * Оригинал: `function useComboboxAnchor() { return React.useRef<HTMLDivElement
 * | null>(null) }` — реф для DOM-узла, который передаётся в `anchor`
 * `ComboboxContent`, чтобы привязать попап к произвольному элементу
 * (используется в chips-режиме). Vue-эквивалент простого `useRef` — `ref()`.
 *
 * См. комментарий в `ComboboxContent.vue`: `anchor` там принимается, но
 * не влияет на позиционирование — reka-ui не даёт переопределить якорь
 * `ComboboxContent` напрямую. Композабл сохранён ради совместимости
 * сигнатуры вызова, а не потому что он что-то меняет в текущем порте.
 */
export function useComboboxAnchor(): Ref<HTMLDivElement | null> {
  return ref<HTMLDivElement | null>(null)
}

src/ui/combobox/variants.ts

/**
 * Строки Tailwind-классов ниже — не cva-блок самого `combobox.tsx`
 * (в оригинале там `cva` не используется вовсе), а **вычисленный**
 * результат применения чужих cva-блоков, от которых `combobox.tsx`
 * зависит: `Button` (`registry/bases/radix/ui/button.tsx`) и
 * `InputGroup`/`InputGroupAddon`/`InputGroupButton`/`InputGroupInput`
 * (`registry/bases/radix/ui/input-group.tsx`). Порт этих двух примитивов
 * не входит в задачу (только `select` и `combobox`), а заводить их
 * отдельными полноценными компонентами ради одной кнопки — лишняя
 * площадь. Тот же приём уже применён в `phone-input`
 * (`PhoneInputCountrySelect.vue` инлайнит разметку `ComboboxList`/
 * `ScrollArea`).
 *
 * Каждая строка — буквальный вывод `cn(...)` (twMerge + clsx) для
 * конкретной комбинации пропсов, как её вызывает `combobox.tsx`.
 * Пересчитано вручную через class-variance-authority + tailwind-merge
 * с текущими версиями из package.json; при обновлении upstream-версий
 * этих пакетов пересчитать заново.
 */

// InputGroupButton({variant:"ghost", size:"icon-xs"}) в роли триггера
// внутри `ComboboxInput` (адресуется классом
// `group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent`,
// который передаёт `combobox.tsx` явно). Внутри `InputGroupButton` проп
// `size` в `Button` не прокидывается (только `data-size`), поэтому Button
// применяет свой размер по умолчанию (`cn-button-size-default`) — так же,
// как в оригинале.
export const comboboxInputTriggerButtonClass =
  "cn-button group/button shrink-0 justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 cn-button-variant-ghost cn-button-size-default cn-input-group-button flex items-center shadow-none cn-input-group-button-size-icon-xs group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"

// ComboboxClear: `render={<InputGroupButton variant="ghost" size="icon-xs" />}`,
// без дополнительного className на самой InputGroupButton.
export const comboboxClearButtonClass =
  "cn-button group/button shrink-0 justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 cn-button-variant-ghost cn-button-size-default cn-input-group-button flex items-center shadow-none cn-input-group-button-size-icon-xs"

// ComboboxChip's remove-кнопка: `render={<Button variant="ghost" size="icon-xs" />}`
// напрямую (не через InputGroupButton), поэтому `size` доходит до Button
// без искажений.
export const comboboxChipRemoveButtonClass =
  "cn-button group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 cn-button-variant-ghost cn-button-size-icon-xs cn-combobox-chip-remove"

// InputGroup-обёртка `ComboboxInput`: `cn("group/input-group cn-input-group
// relative flex w-full min-w-0 items-center outline-none
// has-[>textarea]:h-auto", "cn-combobox-input w-auto")` — twMerge убирает
// конфликтующий `w-full` в пользу `w-auto`.
export const comboboxInputGroupClass =
  "group/input-group cn-input-group relative flex min-w-0 items-center outline-none has-[>textarea]:h-auto cn-combobox-input w-auto"

// InputGroupAddon({align: "inline-end"}).
export const comboboxInputAddonClass =
  "cn-input-group-addon flex cursor-text items-center justify-center select-none cn-input-group-addon-align-inline-end order-last"

// InputGroupInput -> Input: `cn("cn-input w-full min-w-0 outline-none
// file:inline-flex file:border-0 file:bg-transparent
// file:text-foreground placeholder:text-muted-foreground
// disabled:pointer-events-none disabled:cursor-not-allowed
// disabled:opacity-50", "cn-input-group-input flex-1")`.
export const comboboxInputControlClass =
  "cn-input w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 cn-input-group-input flex-1"

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/combobox.json

Зависимости реестра

npm-зависимости

  • reka-ui

Источник: порт из ReUI (Keenthemes, MIT)