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)