primitives

Input Otp

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

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

src/ui/input-otp/InputOTP.vue

<!--
  Оригинал построен на npm-пакете `input-otp` (единственный визуально скрытый
  `<input>` + React-контекст `OTPInputContext`, читаемый `InputOTPSlot`).
  У Reka UI нет аналога этого пакета: примитив-замена — `PinInput`
  (`PinInputRoot`/`PinInputInput`), где каждый слот — свой собственный
  настоящий `<input>`, а не общий скрытый ввод с виртуальными слотами.
  Здесь `InputOTP` — обёртка над `PinInputRoot`.
-->
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { computed } from "vue"
import { PinInputRoot, type PinInputRootEmits, type PinInputRootProps, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"

const props = defineProps<
  PinInputRootProps & {
    /** Доп. классы. Имя `class` — соглашение shadcn-vue (не `className`). */
    class?: HTMLAttributes["class"]
    containerClassName?: HTMLAttributes["class"]
  }
>()
const emits = defineEmits<PinInputRootEmits>()

const delegatedProps = computed(() => {
  const { class: _class, containerClassName: _containerClassName, ...delegated } = props
  return delegated
})

const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

<template>
  <PinInputRoot
    data-slot="input-otp"
    v-bind="forwarded"
    :class="cn('cn-input-otp flex items-center has-disabled:opacity-50', props.containerClassName)"
  >
    <slot />
  </PinInputRoot>
</template>

src/ui/input-otp/InputOTPGroup.vue

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

const props = defineProps<{
  /** Доп. классы. Имя `class` — соглашение shadcn-vue (не `className`). */
  class?: HTMLAttributes["class"]
}>()
</script>

<template>
  <div data-slot="input-otp-group" :class="cn('cn-input-otp-group flex items-center', props.class)">
    <slot />
  </div>
</template>

src/ui/input-otp/InputOTPSeparator.vue

<!--
  Иконка — инлайновый `<svg>` вместо апстримного `IconPlaceholder`
  (путь lucide "minus"), тем же приёмом, что и в остальных примитивах.
-->
<script setup lang="ts"></script>

<template>
  <div data-slot="input-otp-separator" class="cn-input-otp-separator flex items-center" role="separator">
    <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="M5 12h14" />
    </svg>
  </div>
</template>

src/ui/input-otp/InputOTPSlot.vue

<!--
  Оригинал читает `char`/`hasFakeCaret`/`isActive` из `OTPInputContext`
  (пакет `input-otp`) для одного общего скрытого `<input>`. У `PinInput`
  (Reka UI) каждый слот — собственный настоящий `<input>`
  (`PinInputInput`), поэтому `char`/фейковый каret из оригинала не имеют
  прямого аналога: значение слота отображается самим `<input>`, а не
  текстовым узлом поверх него. `data-active` (фокус слота) здесь не
  воспроизведён — у Reka UI `PinInput` это состояние не публикуется
  наружу как отдельный флаг на слоте.

  Ещё одно следствие замены `<div>` на `<input>`: в оригинале `flex
  items-center justify-center` центрирует текстовый узел `{char}` внутри
  `<div>`. У `<input>` это тот же класс, приложенный к самому элементу, а
  не к его контейнеру — flex-выравнивание не действует на содержимое
  инпута (его текст позиционируется через `text-align`, а не flex), и
  цифра садится по левому краю (браузерный дефолт `text-align: left`)
  вместо центра. `text-center` добавлен как необходимая поправка на смену
  типа элемента, а не как правка исходной строки Tailwind.

  Известное непокрытое расхождение (см. docs/PORTING.md, §5): тема
  `mira` в светлом режиме даёт стабильный вертикальный сдвиг цифры на
  1px внутри `<input>` относительно `<div>` оригинала — расследовано, не
  устранимо через CSS (см. PORTING.md за подробностями и доказательством).
-->
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { PinInputInput, type PinInputInputProps } from "reka-ui"
import { cn } from "@/lib/utils"

const props = defineProps<
  PinInputInputProps & {
    /** Доп. классы. Имя `class` — соглашение shadcn-vue (не `className`). */
    class?: HTMLAttributes["class"]
  }
>()
</script>

<template>
  <PinInputInput
    data-slot="input-otp-slot"
    :index="props.index"
    :disabled="props.disabled"
    :class="cn('cn-input-otp-slot relative flex items-center justify-center text-center data-[active=true]:z-10', props.class)"
  />
</template>

src/ui/input-otp/index.ts

export { default as InputOTP } from "./InputOTP.vue"
export { default as InputOTPGroup } from "./InputOTPGroup.vue"
export { default as InputOTPSeparator } from "./InputOTPSeparator.vue"
export { default as InputOTPSlot } from "./InputOTPSlot.vue"

Установка

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

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

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

  • reka-ui

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