primitives
Select
Select — базовый примитив RevueUI (Reka UI + shadcn-совместимый API).
Загрузка превью…
src/ui/select/Select.vue
<script setup lang="ts">
/**
* Порт `Select` (registry/bases/radix/ui/select.tsx, MIT) — базовый слой,
* не ReUI-обёртка. Оригинал — тонкая обёртка над `radix-ui`'s
* `Select.Root` (`<SelectPrimitive.Root data-slot="select" {...props} />`);
* здесь — тонкая обёртка над `SelectRoot` из reka-ui, тот же примитив 1:1
* (Radix Select ⇄ Reka UI Select — соответствие прямое, оба построены по
* одному API). Пропсы/события намеренно не декларируются явно: как и в
* оригинале, всё, что передано снаружи (включая `v-model`), просто
* протекает на `SelectRoot` через автоматический fallthrough атрибутов.
*/
import { SelectRoot } from "reka-ui"
</script>
<template>
<SelectRoot data-slot="select">
<slot />
</SelectRoot>
</template>
src/ui/select/SelectContent.vue
<script setup lang="ts">
/**
* Строки Tailwind-классов ниже перенесены буквально из оригинала, включая
* CSS-переменные в Radix-нотации (`--radix-select-content-available-height`
* и т.п.). reka-ui эмитит функционально аналогичные переменные под своим
* неймспейсом (`--reka-select-content-available-height` и т.д., см.
* `SelectPopperPosition.js` в пакете), поэтому в открытом состоянии эти
* конкретные `max-h-()`/`origin-()` не резолвятся. Тот же выбор уже сделан
* в `phone-input` (`PhoneInputCountrySelect.vue` держит Base-UI-шные
* `--available-height`/`--anchor-width` нетронутыми): диф гейтит только
* закрытое состояние (см. правило проверки в задаче на `select`/
* `combobox`), а строки cva/Tailwind — по правилу 1 — не переформатируются
* и не правятся даже ради пущей смысловой точности.
*/
import type { HTMLAttributes } from "vue"
import type { SelectContentProps } from "reka-ui"
import { SelectContent, SelectPortal, SelectViewport } from "reka-ui"
import { cn } from "@/lib/utils"
import SelectScrollDownButton from "./SelectScrollDownButton.vue"
import SelectScrollUpButton from "./SelectScrollUpButton.vue"
const props = withDefaults(
defineProps<
SelectContentProps & {
class?: HTMLAttributes["class"]
align?: "start" | "center" | "end"
}
>(),
{
position: "item-aligned",
align: "center",
}
)
</script>
<template>
<SelectPortal>
<SelectContent
data-slot="select-content"
:data-align-trigger="props.position === 'item-aligned'"
:class="
cn(
'cn-select-content cn-menu-target cn-menu-translucent relative z-50 max-h-(--radix-select-content-available-height) origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none',
props.position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class
)
"
:position="props.position"
:align="props.align"
>
<SelectScrollUpButton />
<SelectViewport
:data-position="props.position"
:class="
cn(
'cn-select-viewport data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)',
props.position === 'popper' && ''
)
"
>
<slot />
</SelectViewport>
<SelectScrollDownButton />
</SelectContent>
</SelectPortal>
</template>
src/ui/select/SelectGroup.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { SelectGroup } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<SelectGroup data-slot="select-group" :class="cn('cn-select-group', props.class)">
<slot />
</SelectGroup>
</template>
src/ui/select/SelectItem.vue
<script setup lang="ts">
/**
* Иконка индикатора — инлайновый `<svg>` (`CheckIcon` из `lucide-react`)
* вместо `IconPlaceholder`, см. комментарий в `SelectTrigger.vue`.
*/
import type { HTMLAttributes } from "vue"
import type { AcceptableValue, SelectItemProps } from "reka-ui"
import { SelectItem, SelectItemIndicator, SelectItemText } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<
SelectItemProps<AcceptableValue> & { class?: HTMLAttributes["class"] }
>()
</script>
<template>
<SelectItem
data-slot="select-item"
:value="props.value"
:disabled="props.disabled"
:text-value="props.textValue"
:class="
cn(
'cn-select-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
)
"
>
<span class="cn-select-item-indicator">
<SelectItemIndicator>
<svg
class="cn-select-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>
</SelectItemIndicator>
</span>
<SelectItemText><slot /></SelectItemText>
</SelectItem>
</template>
src/ui/select/SelectLabel.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { SelectLabel } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<SelectLabel data-slot="select-label" :class="cn('cn-select-label', props.class)">
<slot />
</SelectLabel>
</template>
src/ui/select/SelectScrollDownButton.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { SelectScrollDownButton } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<SelectScrollDownButton
data-slot="select-scroll-down-button"
:class="cn('cn-select-scroll-down-button', props.class)"
>
<svg
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>
</SelectScrollDownButton>
</template>
src/ui/select/SelectScrollUpButton.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { SelectScrollUpButton } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<SelectScrollUpButton
data-slot="select-scroll-up-button"
:class="cn('cn-select-scroll-up-button', props.class)"
>
<svg
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 15-6-6-6 6" />
</svg>
</SelectScrollUpButton>
</template>
src/ui/select/SelectSeparator.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { SelectSeparator } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<SelectSeparator
data-slot="select-separator"
:class="cn('cn-select-separator pointer-events-none', props.class)"
/>
</template>
src/ui/select/SelectTrigger.vue
<script setup lang="ts">
/**
* Иконка — инлайновый `<svg>` вместо апстримного `IconPlaceholder`
* (dev-инструмент сайта ReUI, читает выбранную библиотеку иконок из
* `localStorage`, недоступен вне апстрима). По умолчанию апстрим сам
* выбирает `lucide` (`DEFAULT_CONFIG.iconLibrary`), поэтому здесь —
* путь `ChevronDownIcon` из `lucide-react` 1:1 (см. также `rating`,
* `tree`, `phone-input` — тот же приём).
*/
import type { HTMLAttributes } from "vue"
import type { SelectTriggerProps } from "reka-ui"
import { SelectIcon, SelectTrigger } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<
SelectTriggerProps & {
class?: HTMLAttributes["class"]
size?: "sm" | "default"
}
>(),
{
size: "default",
}
)
</script>
<template>
<SelectTrigger
data-slot="select-trigger"
:data-size="props.size"
:class="
cn(
'cn-select-trigger flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
props.class
)
"
>
<slot />
<SelectIcon as-child>
<svg
class="cn-select-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>
</SelectIcon>
</SelectTrigger>
</template>
src/ui/select/SelectValue.vue
<script setup lang="ts">
import type { SelectValueProps } from "reka-ui"
import { SelectValue } from "reka-ui"
defineProps<SelectValueProps>()
</script>
<template>
<SelectValue data-slot="select-value" v-bind="$props">
<template v-if="$slots.default" #default="slotProps"><slot v-bind="slotProps" /></template>
</SelectValue>
</template>
src/ui/select/index.ts
export { default as Select } from "./Select.vue"
export { default as SelectContent } from "./SelectContent.vue"
export { default as SelectGroup } from "./SelectGroup.vue"
export { default as SelectItem } from "./SelectItem.vue"
export { default as SelectLabel } from "./SelectLabel.vue"
export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue"
export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue"
export { default as SelectSeparator } from "./SelectSeparator.vue"
export { default as SelectTrigger } from "./SelectTrigger.vue"
export { default as SelectValue } from "./SelectValue.vue"
Установка
npx shadcn-vue@latest add https://revueui.rootapi.dev/r/select.jsonЗависимости реестра
npm-зависимости
- reka-ui
Источник: порт из ReUI (Keenthemes, MIT)