Inputs
Phone Field
Телефон с маской от цифр: вставка из буфера в любом формате не ломает поле, а стирание не застревает на скобке.
- input
- phone
- mask
- form
Превью
1440pxHost theme
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/input-005
+7
Пришлём код подтверждения в СМСПоказать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «input-005» (Phone Field) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/input-005.json
Registry item: https://vibeui.ru/r/input-005.json
Ставится в: components/vibeui/input-005.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Телефон с маской от цифр: вставка из буфера в любом формате не ломает поле, а стирание не застревает на скобке.
Поле телефона: код страны рядом с полем, маска поверх цифр и вставка в любом формате. onChange отдаёт только цифры. Ноль зависимостей, один файл.
## 3. Как использовать
import { Input005 } from "@/components/vibeui/input-005"
<Input005 prefix="+7" onChange={(digits) => setPhone(digits)} />
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- локальную палитру --vibeui-input-005-* и не подменять её токенами вашей темы
- хранение только цифр: маска — это отображение, а не значение
- код страны вне поля ввода — его не стирают случайно
- inputMode="tel": на телефоне это цифровая клавиатура
- табличные цифры, чтобы номер не дёргался при вводе
- отдачу чистых цифр в onChange, а не отформатированной строки
## 6. Можно менять
- prefix — код страны
- формат маски под свою страну
- label и hint
- акцент через проп accent
## 7. Правила
- Маска рассчитана на десять цифр: для других стран формат придётся заменить.
- Не подставляйте код страны в значение: сервер обычно ждёт полный номер, соберите его при отправке.
- Валидацию номера делает сервер: длина в маске ничего не гарантирует.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/input-005.jsonhttps://vibeui.ru/r/input-005.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-input-005-*. Клиентский: "use client". В состоянии живут только цифры, скобки и дефисы дорисовываются функцией форматирования — поэтому любой формат вставки даёт один результат. Код страны вынесен из поля отдельным элементом, чтобы его не стирали.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
"use client"
import { useId, useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"
export type Input005Props = Omit<
ComponentPropsWithoutRef<"div">,
"children" | "onChange"
> & {
label?: string
prefix?: string
hint?: string
onChange?: (digits: string) => void
accent?: string
}
// Идея компонента: телефон с маской, которая не мешает. Форматирование идёт
// от цифр: из ввода вынимаются только они, а скобки и дефисы дорисовываются
// сверху. Так вставка из буфера в любом формате не ломает поле, а стирание
// не застревает на скобке.
const STYLES = `
:where([data-vibeui-block="input-005"]){
--vibeui-input-005-bg:oklch(1 0 0);
--vibeui-input-005-fg:oklch(0.22 0.014 265);
--vibeui-input-005-muted:oklch(0.56 0.014 265);
--vibeui-input-005-border:oklch(0.9 0.006 265);
--vibeui-input-005-field:oklch(0.985 0.002 265);
--vibeui-input-005-accent:oklch(0.55 0.17 265);
--vibeui-input-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="input-005"]{
display:flex;flex-direction:column;gap:0.375rem;
width:100%;max-width:20rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-input-005-bg);
border:1px solid var(--vibeui-input-005-border);border-radius:0.875rem;
font-family:var(--vibeui-input-005-font);color:var(--vibeui-input-005-fg);
}
[data-vibeui-block="input-005"] label{font-size:0.8125rem;font-weight:600}
[data-vibeui-block="input-005"] [data-part="row"]{
display:flex;align-items:center;gap:0.5rem;
box-sizing:border-box;height:2.5rem;padding:0 0.75rem;
border:1px solid var(--vibeui-input-005-border);border-radius:0.625rem;
background:var(--vibeui-input-005-field);
}
[data-vibeui-block="input-005"] [data-part="row"]:focus-within{
outline:2px solid var(--vibeui-input-005-accent);outline-offset:1px;border-color:transparent;
}
/* Код страны — не часть поля: его не стирают и не редактируют случайно. */
[data-vibeui-block="input-005"] [data-part="prefix"]{
flex:none;color:var(--vibeui-input-005-muted);font-size:0.875rem;font-variant-numeric:tabular-nums;
}
[data-vibeui-block="input-005"] input{
flex:1;min-width:0;height:100%;border:0;background:transparent;color:inherit;
font:inherit;font-size:0.875rem;font-variant-numeric:tabular-nums;letter-spacing:0.01em;
}
[data-vibeui-block="input-005"] input:focus{outline:none}
[data-vibeui-block="input-005"] [data-part="hint"]{font-size:0.75rem;line-height:1.4;color:var(--vibeui-input-005-muted)}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="input-005"] *{animation:none!important;transition:none!important}}
`
// Форматирование идёт от цифр: вставка «+7 (999) 000-00-00» и «79990000000»
// даёт одинаковый результат, а стирание не застревает на скобке.
function format(digits: string) {
const value = digits.slice(0, 10)
const parts = [
value.slice(0, 3),
value.slice(3, 6),
value.slice(6, 8),
value.slice(8, 10),
].filter(Boolean)
if (parts.length === 0) return ""
if (parts.length === 1) return `(${parts[0]}`
return `(${parts[0]}) ${parts.slice(1).join("-")}`
}
/**
* Телефон с маской от цифр: вставка в любом формате не ломает поле.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Input005({
label = "Телефон",
prefix = "+7",
hint = "Пришлём код подтверждения в СМС",
onChange,
accent,
className,
style,
...props
}: Input005Props) {
const id = useId()
const [digits, setDigits] = useState("9990000000")
const palette = {
...(accent ? { "--vibeui-input-005-accent": accent } : null),
...style,
} as CSSProperties
return (
<>
<style href="vibeui-input-005" precedence="medium">
{STYLES}
</style>
<div
{...props}
data-vibeui-block="input-005"
className={className}
style={palette}
>
<label htmlFor={id}>{label}</label>
<div data-part="row">
<span data-part="prefix">{prefix}</span>
<input
id={id}
type="tel"
inputMode="tel"
autoComplete="tel-national"
placeholder="(999) 000-00-00"
value={format(digits)}
aria-describedby={hint ? `${id}-hint` : undefined}
onChange={(event) => {
const next = event.target.value.replace(/\D/g, "").slice(0, 10)
setDigits(next)
onChange?.(next)
}}
/>
</div>
{hint ? (
<span data-part="hint" id={`${id}-hint`}>
{hint}
</span>
) : null}
</div>
</>
)
}