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)