primitives

Dropdown Menu

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

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

src/ui/dropdown-menu/DropdownMenu.vue

<script setup lang="ts">
/**
 * Порт базового радикс-слоя (registry/bases/radix/ui/dropdown-menu.tsx, MIT).
 * Radix DropdownMenuPrimitive -> reka-ui DropdownMenu*, один в один: те же
 * подкомпоненты (Root/Portal/Trigger/Content/Group/Item/CheckboxItem/
 * RadioGroup/RadioItem/Label/Separator/Sub/SubTrigger/SubContent),
 * `data-slot` и Tailwind-строки как в оригинале.
 *
 * Позиционирующие CSS-переменные Radix (`--radix-dropdown-menu-*`)
 * оставлены буквально как в оригинале (правило 1: cva/Tailwind-строки
 * переносятся дословно, без правок), хотя reka-ui выставляет
 * содержательно те же переменные под своим префиксом
 * (`--reka-dropdown-menu-*`). В открытом состоянии эти конкретные
 * `max-h-()`/`w-()`/`origin-()` поэтому не резолвятся — тот же выбор,
 * что уже сделан в `select`/`phone-input`; гейт проверяет закрытое
 * состояние, где Content не рендерится вовсе. См. docs/PORTING.md.
 *
 * Никаких собственных пропсов: все атрибуты (`open`, `defaultOpen`,
 * `modal`, `dir`, `@update:open` и т.д.) идут автоматическим fallthrough
 * на корневой reka-ui компонент.
 */
import { DropdownMenuRoot } from "reka-ui"
</script>

<template>
  <DropdownMenuRoot data-slot="dropdown-menu">
    <slot />
  </DropdownMenuRoot>
</template>

src/ui/dropdown-menu/DropdownMenuCheckboxItem.vue

<script setup lang="ts">
/**
 * `checked` в оригинале — проп Radix CheckboxItem. У reka-ui тот же
 * примитив принимает `modelValue`/`v-model` (см. docs/PORTING.md) — это
 * стандартное расхождение именования между Radix (React) и reka-ui (Vue),
 * а не решение порта; шаблон ниже не переименовывает его обратно, чтобы
 * остаться совместимым с shadcn-vue.
 */
import type { HTMLAttributes } from "vue"
import { DropdownMenuCheckboxItem, DropdownMenuItemIndicator } from "reka-ui"
import { cn } from "@/lib/utils"

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

<template>
  <DropdownMenuCheckboxItem
    data-slot="dropdown-menu-checkbox-item"
    :data-inset="props.inset"
    :class="
      cn(
        'cn-dropdown-menu-checkbox-item relative flex 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-dropdown-menu-item-indicator pointer-events-none"
      data-slot="dropdown-menu-checkbox-item-indicator"
    >
      <DropdownMenuItemIndicator>
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="14"
          height="14"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <polyline points="20 6 9 17 4 12" />
        </svg>
      </DropdownMenuItemIndicator>
    </span>
    <slot />
  </DropdownMenuCheckboxItem>
</template>

src/ui/dropdown-menu/DropdownMenuContent.vue

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

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes["class"]
    align?: "start" | "center" | "end"
    sideOffset?: number
  }>(),
  {
    align: "start",
    sideOffset: 4,
  }
)
</script>

<template>
  <DropdownMenuPortal>
    <DropdownMenuContent
      data-slot="dropdown-menu-content"
      :side-offset="props.sideOffset"
      :align="props.align"
      :class="
        cn(
          'cn-dropdown-menu-content cn-menu-target cn-menu-translucent z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto data-[state=closed]:overflow-hidden',
          props.class
        )
      "
    >
      <slot />
    </DropdownMenuContent>
  </DropdownMenuPortal>
</template>

src/ui/dropdown-menu/DropdownMenuGroup.vue

<script setup lang="ts">
import { DropdownMenuGroup } from "reka-ui"
</script>

<template>
  <DropdownMenuGroup data-slot="dropdown-menu-group">
    <slot />
  </DropdownMenuGroup>
</template>

src/ui/dropdown-menu/DropdownMenuItem.vue

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

const props = withDefaults(
  defineProps<{
    class?: HTMLAttributes["class"]
    inset?: boolean
    variant?: "default" | "destructive"
  }>(),
  {
    variant: "default",
  }
)
</script>

<template>
  <DropdownMenuItem
    data-slot="dropdown-menu-item"
    :data-inset="props.inset"
    :data-variant="props.variant"
    :class="
      cn(
        'cn-dropdown-menu-item group/dropdown-menu-item relative flex 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 />
  </DropdownMenuItem>
</template>

src/ui/dropdown-menu/DropdownMenuLabel.vue

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

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

<template>
  <DropdownMenuLabel
    data-slot="dropdown-menu-label"
    :data-inset="props.inset"
    :class="cn('cn-dropdown-menu-label', props.class)"
  >
    <slot />
  </DropdownMenuLabel>
</template>

src/ui/dropdown-menu/DropdownMenuPortal.vue

<script setup lang="ts">
import { DropdownMenuPortal } from "reka-ui"
</script>

<template>
  <DropdownMenuPortal data-slot="dropdown-menu-portal">
    <slot />
  </DropdownMenuPortal>
</template>

src/ui/dropdown-menu/DropdownMenuRadioGroup.vue

<script setup lang="ts">
import { DropdownMenuRadioGroup } from "reka-ui"
</script>

<template>
  <DropdownMenuRadioGroup data-slot="dropdown-menu-radio-group">
    <slot />
  </DropdownMenuRadioGroup>
</template>

src/ui/dropdown-menu/DropdownMenuRadioItem.vue

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

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

<template>
  <DropdownMenuRadioItem
    data-slot="dropdown-menu-radio-item"
    :value="props.value"
    :data-inset="props.inset"
    :class="
      cn(
        'cn-dropdown-menu-radio-item relative flex 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-dropdown-menu-item-indicator pointer-events-none"
      data-slot="dropdown-menu-radio-item-indicator"
    >
      <DropdownMenuItemIndicator>
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="14"
          height="14"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <polyline points="20 6 9 17 4 12" />
        </svg>
      </DropdownMenuItemIndicator>
    </span>
    <slot />
  </DropdownMenuRadioItem>
</template>

src/ui/dropdown-menu/DropdownMenuSeparator.vue

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

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

<template>
  <DropdownMenuSeparator
    data-slot="dropdown-menu-separator"
    :class="cn('cn-dropdown-menu-separator', props.class)"
  />
</template>

src/ui/dropdown-menu/DropdownMenuShortcut.vue

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

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

<template>
  <span
    data-slot="dropdown-menu-shortcut"
    :class="cn('cn-dropdown-menu-shortcut', props.class)"
  ><slot /></span>
</template>

src/ui/dropdown-menu/DropdownMenuSub.vue

<script setup lang="ts">
import { DropdownMenuSub } from "reka-ui"
</script>

<template>
  <DropdownMenuSub data-slot="dropdown-menu-sub">
    <slot />
  </DropdownMenuSub>
</template>

src/ui/dropdown-menu/DropdownMenuSubContent.vue

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

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

<template>
  <DropdownMenuSubContent
    data-slot="dropdown-menu-sub-content"
    :class="
      cn(
        'cn-dropdown-menu-sub-content cn-menu-target cn-menu-translucent z-50 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden',
        props.class
      )
    "
  >
    <slot />
  </DropdownMenuSubContent>
</template>

src/ui/dropdown-menu/DropdownMenuSubTrigger.vue

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

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

<template>
  <DropdownMenuSubTrigger
    data-slot="dropdown-menu-sub-trigger"
    :data-inset="props.inset"
    :class="
      cn(
        'cn-dropdown-menu-sub-trigger flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0',
        props.class
      )
    "
  >
    <slot />
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="14"
      height="14"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
      class="cn-rtl-flip ml-auto"
    >
      <path d="m9 18 6-6-6-6" />
    </svg>
  </DropdownMenuSubTrigger>
</template>

src/ui/dropdown-menu/DropdownMenuTrigger.vue

<script setup lang="ts">
import { DropdownMenuTrigger } from "reka-ui"
</script>

<template>
  <DropdownMenuTrigger data-slot="dropdown-menu-trigger">
    <slot />
  </DropdownMenuTrigger>
</template>

src/ui/dropdown-menu/index.ts

export { default as DropdownMenu } from "./DropdownMenu.vue"
export { default as DropdownMenuPortal } from "./DropdownMenuPortal.vue"
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue"
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue"
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue"
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue"
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue"
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue"
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue"
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue"
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue"
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue"
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue"
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue"
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue"

Установка

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

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

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

  • reka-ui

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