Inputs

Paste Splitter

Теги по Enter, запятой и точке с запятой, а вставка списка из буфера разбирается тем же правилом и превращается в готовые чипы.

  • tags
  • paste
  • keyboard
  • email

Превью

1440pxHost theme

Использовать с AI

  1. 1. Скопируйте ссылку.
  2. 2. Напишите агенту своими словами и вставьте её в предложение.
  3. 3. Агент откроет ссылку и поставит компонент из registry.

размести это в шапке: https://vibeui.ru/c/tags-005

anna@example.com

Enter,;заканчивают тег, вставка списка разбирается целиком

Показать полную инструкцию

То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.

# Установка и размещение «tags-005» (Paste Splitter) из VibeUI

## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/tags-005.json

Registry item: https://vibeui.ru/r/tags-005.json
Ставится в: components/vibeui/tags-005.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.

Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.

## 2. Что это
Теги по Enter, запятой и точке с запятой, а вставка списка из буфера разбирается тем же правилом и превращается в готовые чипы.

Поле тегов с тремя разделителями и разбором вставленного списка: адреса из письма становятся чипами за одну вставку. Ноль зависимостей, один файл.

## 3. Как использовать
import { Tags005 } from "@/components/vibeui/tags-005"

<Tags005 label="Кому отправить" defaultValue={["anna@example.com"]} />

Полный список пропсов — в установленном файле.

## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.

Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)

## 5. Сохранить как установлено
- локальную палитру --vibeui-tags-005-* и не подменять её токенами вашей темы
- собственную светлую подложку: поле показывают поверх любого фона
- три разделителя сразу: привычки к Enter, запятой и точке с запятой встречаются одинаково часто
- разбор вставки тем же правилом: иначе список из письма превращается в один гигантский тег
- перехват вставки только при наличии разделителей: обычная вставка слова обязана работать как обычно
- удаление последнего чипа по Backspace на пустом поле: иначе приходится целиться в крестик
- обрезку длинного текста в чипе: адрес на сорок символов иначе растягивает поле в строку

## 6. Можно менять
- label — подпись поля
- defaultValue — стартовый список
- набор разделителей в функции split
- ширину, на которой обрезается текст чипа
- акцент через проп accent

## 7. Правила
- Проверки адреса тут нет: добавьте свою, если поле принимает почту.
- Пробел разделителем не считается намеренно: теги из двух слов иначе развалились бы.
- Повторы отсекаются молча — для видимой реакции на дубликат нужен отдельный вариант компонента.
- Не выносите CSS-переменные в globals.css: компонент обязан остаться одним файлом.

## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.

Для разработчика

npx shadcn@latest add https://vibeui.ru/r/tags-005.json
https://vibeui.ru/r/tags-005.json

Компонент самодостаточен: один файл, ноль зависимостей, палитра в локальных переменных --vibeui-tags-005-*. Клиентский: список, черновик и число добавленных из последней вставки в состоянии. Разбор один на все случаи — функция split по запятой, точке с запятой, переводу строки и табу, — поэтому набор с клавиатуры и вставка из письма ведут себя одинаково. Обработчик onPaste перехватывает только текст с разделителями: обычная вставка одного слова должна работать как обычно. Backspace на пустом поле удаляет последний чип.

Исходник компонента

Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.

"use client"

import { useId, useState } from "react"
import type {
  ClipboardEvent,
  ComponentPropsWithoutRef,
  CSSProperties,
  KeyboardEvent,
} from "react"

export type Tags005Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "defaultValue"
> & {
  label?: string
  defaultValue?: string[]
  accent?: string
}

// Идея компонента: тег заканчивается там, где человек поставил разделитель.
// Enter, запятая и точка с запятой делают одно и то же, потому что все три
// привычки встречаются одинаково часто, и заставлять переучиваться незачем.
// Вставка из буфера разбирается тем же правилом: список адресов, скопированный
// из письма, превращается в готовые чипы за одну вставку, а не в один
// гигантский тег со всем текстом внутри.
const STYLES = `
:where([data-vibeui-block="tags-005"]){
--vibeui-tags-005-surface:oklch(1 0 0);
--vibeui-tags-005-field:oklch(1 0 0);
--vibeui-tags-005-shell:oklch(0.9 0.006 265);
--vibeui-tags-005-fg:oklch(0.23 0.014 265);
--vibeui-tags-005-muted:oklch(0.55 0.014 265);
--vibeui-tags-005-border:oklch(0.88 0.008 265);
--vibeui-tags-005-chip:oklch(0.55 0.15 145 / 14%);
--vibeui-tags-005-accent:oklch(0.48 0.15 145);
--vibeui-tags-005-key:oklch(0.96 0.004 265);
--vibeui-tags-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--vibeui-tags-005-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
/* Своя светлая подложка: поле показывают поверх любого фона. */
[data-vibeui-block="tags-005"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:23rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-tags-005-surface);
border:1px solid var(--vibeui-tags-005-shell);border-radius:0.875rem;
font-family:var(--vibeui-tags-005-font);color:var(--vibeui-tags-005-fg);
}
[data-vibeui-block="tags-005"] label{font-size:0.8125rem;font-weight:650}
[data-vibeui-block="tags-005"] [data-part="field"]{
display:flex;flex-wrap:wrap;align-items:center;gap:0.375rem;
min-height:3rem;padding:0.375rem 0.5rem;box-sizing:border-box;
background:var(--vibeui-tags-005-field);
border:1px solid var(--vibeui-tags-005-border);border-radius:0.625rem;
}
[data-vibeui-block="tags-005"] [data-part="field"]:focus-within{
border-color:var(--vibeui-tags-005-accent);
box-shadow:0 0 0 2px oklch(0.48 0.15 145 / 18%);
}
[data-vibeui-block="tags-005"] [data-part="chip"]{
display:inline-flex;align-items:center;gap:0.25rem;
max-width:100%;height:1.75rem;padding:0 0.25rem 0 0.5rem;border-radius:0.4375rem;
background:var(--vibeui-tags-005-chip);color:var(--vibeui-tags-005-accent);
font-size:0.8125rem;font-weight:600;
}
/* Длинный адрес обрезается, а не растягивает поле в одну строку. */
[data-vibeui-block="tags-005"] [data-part="text"]{
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:11rem;
}
[data-vibeui-block="tags-005"] [data-part="chip"] button{
appearance:none;border:0;background:none;cursor:pointer;flex:none;
width:1.125rem;height:1.125rem;border-radius:0.3125rem;
color:inherit;font:inherit;font-size:0.875rem;line-height:1;
}
[data-vibeui-block="tags-005"] [data-part="chip"] button:focus-visible{outline:2px solid var(--vibeui-tags-005-accent);outline-offset:1px}
[data-vibeui-block="tags-005"] input{
flex:1 1 7rem;min-width:7rem;
appearance:none;border:0;background:none;outline:none;
height:1.75rem;color:inherit;font:inherit;font-size:0.875rem;
}
[data-vibeui-block="tags-005"] [data-part="legend"]{
display:flex;flex-wrap:wrap;align-items:center;gap:0.375rem;margin:0;
font-size:0.75rem;color:var(--vibeui-tags-005-muted);
}
[data-vibeui-block="tags-005"] kbd{
display:inline-block;padding:0.0625rem 0.375rem;
border:1px solid var(--vibeui-tags-005-border);border-bottom-width:2px;border-radius:0.3125rem;
background:var(--vibeui-tags-005-key);color:var(--vibeui-tags-005-fg);
font-family:var(--vibeui-tags-005-mono);font-size:0.6875rem;line-height:1.4;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="tags-005"] *{animation:none!important;transition:none!important}}
`

// Одно правило разбора на все случаи: набор, Enter и вставка из буфера.
function split(value: string) {
  return value
    .split(/[,;\n\t]/)
    .map((part) => part.trim())
    .filter(Boolean)
}

/**
 * Теги по Enter, запятой и точке с запятой, со вставкой целого списка из буфера.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Tags005({
  label = "Кому отправить",
  defaultValue = ["anna@example.com"],
  accent,
  className,
  style,
  ...props
}: Tags005Props) {
  const id = useId()
  const [tags, setTags] = useState(defaultValue)
  const [draft, setDraft] = useState("")
  const [added, setAdded] = useState(0)

  const push = (parts: string[]) => {
    const fresh = parts.filter((part) => !tags.includes(part))
    if (fresh.length === 0) return
    setTags([...tags, ...fresh])
    setAdded(fresh.length)
  }

  const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === "Enter" || event.key === "," || event.key === ";") {
      event.preventDefault()
      push(split(draft))
      setDraft("")
      return
    }
    if (event.key === "Backspace" && draft === "" && tags.length > 0) {
      setTags(tags.slice(0, -1))
    }
  }

  // Вставка разбирается тем же правилом: список из письма становится чипами.
  const onPaste = (event: ClipboardEvent<HTMLInputElement>) => {
    const text = event.clipboardData.getData("text")
    if (!/[,;\n\t]/.test(text)) return
    event.preventDefault()
    push(split(text))
    setDraft("")
  }

  const palette = {
    ...(accent ? { "--vibeui-tags-005-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-tags-005" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="tags-005"
        className={className}
        style={palette}
      >
        <label htmlFor={id}>{label}</label>
        <div data-part="field">
          {tags.map((tag) => (
            <span key={tag} data-part="chip">
              <span data-part="text">{tag}</span>
              <button
                type="button"
                aria-label={`Убрать ${tag}`}
                onClick={() => setTags(tags.filter((item) => item !== tag))}
              >
                ×
              </button>
            </span>
          ))}
          <input
            id={id}
            type="text"
            value={draft}
            placeholder="Введите или вставьте список…"
            aria-describedby={`${id}-legend`}
            onChange={(event) => setDraft(event.target.value)}
            onKeyDown={onKeyDown}
            onPaste={onPaste}
            onBlur={() => {
              push(split(draft))
              setDraft("")
            }}
          />
        </div>
        <p id={`${id}-legend`} data-part="legend" aria-live="polite">
          {added > 1 ? (
            <span>Из вставки добавлено сразу {added} адресов</span>
          ) : (
            <>
              <kbd>Enter</kbd>
              <kbd>,</kbd>
              <kbd>;</kbd>
              <span>заканчивают тег, вставка списка разбирается целиком</span>
            </>
          )}
        </p>
      </div>
    </>
  )
}