primitives

Menubar

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

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

src/ui/menubar/Menubar.vue

<script setup lang="ts">
/**
 * Порт базового радикс-слоя (registry/bases/radix/ui/menubar.tsx, MIT).
 * Radix MenubarPrimitive -> reka-ui Menubar*, один в один: те же
 * подкомпоненты (Root/Menu/Trigger/Portal/Content/Group/Item/CheckboxItem/
 * RadioGroup/RadioItem/Label/Separator/Sub/SubTrigger/SubContent),
 * `data-slot` и Tailwind-строки как в оригинале.
 *
 * Позиционирующая CSS-переменная Radix (`--radix-menubar-content-transform-
 * origin`) оставлена буквально как в оригинале (правило 1) — см.
 * docs/PORTING.md, тот же выбор, что уже сделан для dropdown-menu/select.
 */
import type { HTMLAttributes } from "vue"
import { MenubarRoot } from "reka-ui"
import { cn } from "@/lib/utils"

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

<template>
  <MenubarRoot data-slot="menubar" :class="cn('cn-menubar flex items-center', props.class)">
    <slot />
  </MenubarRoot>
</template>

src/ui/menubar/MenubarCheckboxItem.vue

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

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

<template>
  <MenubarCheckboxItem
    data-slot="menubar-checkbox-item"
    :data-inset="props.inset"
    :class="
      cn(
        'cn-menubar-checkbox-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0',
        props.class
      )
    "
  >
    <span
      class="cn-menubar-checkbox-item-indicator pointer-events-none absolute flex items-center justify-center"
      data-slot="menubar-checkbox-item-indicator"
    >
      <MenubarItemIndicator>
        <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>
      </MenubarItemIndicator>
    </span>
    <slot />
  </MenubarCheckboxItem>
</template>

src/ui/menubar/MenubarContent.vue

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

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

<template>
  <MenubarPortal>
    <MenubarContent
      data-slot="menubar-content"
      :align="props.align"
      :align-offset="props.alignOffset"
      :side-offset="props.sideOffset"
      :class="
        cn(
          'cn-menubar-content cn-menu-target cn-menu-translucent z-50 origin-(--radix-menubar-content-transform-origin) overflow-hidden',
          props.class
        )
      "
    >
      <slot />
    </MenubarContent>
  </MenubarPortal>
</template>

src/ui/menubar/MenubarGroup.vue

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

<template>
  <MenubarGroup data-slot="menubar-group">
    <slot />
  </MenubarGroup>
</template>

src/ui/menubar/MenubarItem.vue

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

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

<template>
  <MenubarItem
    data-slot="menubar-item"
    :data-inset="props.inset"
    :data-variant="props.variant"
    :class="
      cn(
        'cn-menubar-item group/menubar-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0',
        props.class
      )
    "
  >
    <slot />
  </MenubarItem>
</template>

src/ui/menubar/MenubarLabel.vue

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

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

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

src/ui/menubar/MenubarMenu.vue

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

<template>
  <MenubarMenu data-slot="menubar-menu">
    <slot />
  </MenubarMenu>
</template>

src/ui/menubar/MenubarPortal.vue

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

<template>
  <MenubarPortal data-slot="menubar-portal">
    <slot />
  </MenubarPortal>
</template>

src/ui/menubar/MenubarRadioGroup.vue

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

<template>
  <MenubarRadioGroup data-slot="menubar-radio-group">
    <slot />
  </MenubarRadioGroup>
</template>

src/ui/menubar/MenubarRadioItem.vue

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

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

<template>
  <MenubarRadioItem
    data-slot="menubar-radio-item"
    :value="props.value"
    :data-inset="props.inset"
    :class="
      cn(
        'cn-menubar-radio-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0',
        props.class
      )
    "
  >
    <span
      class="cn-menubar-radio-item-indicator pointer-events-none absolute flex items-center justify-center"
      data-slot="menubar-radio-item-indicator"
    >
      <MenubarItemIndicator>
        <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>
      </MenubarItemIndicator>
    </span>
    <slot />
  </MenubarRadioItem>
</template>

src/ui/menubar/MenubarSeparator.vue

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

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

<template>
  <MenubarSeparator
    data-slot="menubar-separator"
    :class="cn('cn-menubar-separator -mx-1 my-1 h-px', props.class)"
  />
</template>

src/ui/menubar/MenubarShortcut.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="menubar-shortcut" :class="cn('cn-menubar-shortcut ml-auto', props.class)"
    ><slot
  /></span>
</template>

src/ui/menubar/MenubarSub.vue

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

<template>
  <MenubarSub data-slot="menubar-sub">
    <slot />
  </MenubarSub>
</template>

src/ui/menubar/MenubarSubContent.vue

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

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

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

src/ui/menubar/MenubarSubTrigger.vue

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

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

<template>
  <MenubarSubTrigger
    data-slot="menubar-sub-trigger"
    :data-inset="props.inset"
    :class="
      cn(
        'cn-menubar-sub-trigger flex cursor-default items-center outline-none select-none',
        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 size-4"
    >
      <path d="m9 18 6-6-6-6" />
    </svg>
  </MenubarSubTrigger>
</template>

src/ui/menubar/MenubarTrigger.vue

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

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

<template>
  <MenubarTrigger
    data-slot="menubar-trigger"
    :class="cn('cn-menubar-trigger flex items-center outline-hidden select-none', props.class)"
  >
    <slot />
  </MenubarTrigger>
</template>

src/ui/menubar/index.ts

export { default as Menubar } from "./Menubar.vue"
export { default as MenubarMenu } from "./MenubarMenu.vue"
export { default as MenubarGroup } from "./MenubarGroup.vue"
export { default as MenubarPortal } from "./MenubarPortal.vue"
export { default as MenubarRadioGroup } from "./MenubarRadioGroup.vue"
export { default as MenubarTrigger } from "./MenubarTrigger.vue"
export { default as MenubarContent } from "./MenubarContent.vue"
export { default as MenubarItem } from "./MenubarItem.vue"
export { default as MenubarCheckboxItem } from "./MenubarCheckboxItem.vue"
export { default as MenubarRadioItem } from "./MenubarRadioItem.vue"
export { default as MenubarLabel } from "./MenubarLabel.vue"
export { default as MenubarSeparator } from "./MenubarSeparator.vue"
export { default as MenubarShortcut } from "./MenubarShortcut.vue"
export { default as MenubarSub } from "./MenubarSub.vue"
export { default as MenubarSubTrigger } from "./MenubarSubTrigger.vue"
export { default as MenubarSubContent } from "./MenubarSubContent.vue"

Установка

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

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

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

  • reka-ui

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