Выбор тегов

Сделай компонент выбора тегов для статьи: пользователь набирает теги в одном поле, каждый введённый тег превращается в удаляемую метку. Выбранные теги показывай под полем и отдавай наружу через колбэк onChange.

tag-picker__readme__claude-code__claude-opus-5__1 · 23.09.2026, 20:07:05 мск

Есть проблемы
README · claude-opus-5 · Claude Code
Контекст до задачи~798
Токенов на прогон11 327
Цена$0.057
Ошибок компилятора1

Первый вызов модели получил 9 515 токенов входа, включая системный промпт Claude Code. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.

Компилятор

  • Строка 46 · TS2322
    Type '{ label: string; placeholder: string; value: string[]; onValueChange: (next: string[]) => void; }' is not assignable to type 'IntrinsicAttributes & JxTagInputProps'.
Выдуманные пропсы
JxTagInput.label

Линтер и покрытие

  • inline-styleСтроки 39, 55
    Инлайновые стили вместо классов и токенов библиотеки
Компоненты библиотеки

JxButton, JxTagInput

Ожидались JxTagInput · покрытие 100%

Контекст

README (README.md, ~644)

Вход 2 · выход 1 812 · из кэша 8 233 · в кэш 1 280 · 23 с

Рендер

Код компонента

tsx80 строк
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import { useState } from 'react';
import { JxButton, JxTagInput } from '@jinx-ui/react';

export interface ArticleTagsFieldProps {
  value?: string[];
  defaultValue?: string[];
  onChange?: (tags: string[]) => void;
  label?: string;
  placeholder?: string;
  maxTags?: number;
}

export default function ArticleTagsField({
  value,
  defaultValue = [],
  onChange,
  label = 'Теги статьи',
  placeholder = 'Введите тег и нажмите Enter',
  maxTags,
}: ArticleTagsFieldProps) {
  const [internal, setInternal] = useState<string[]>(defaultValue);
  const tags = value ?? internal;

  const commit = (next: string[]) => {
    const normalized = next
      .map((tag) => tag.trim())
      .filter((tag, index, all) => tag.length > 0 && all.indexOf(tag) === index);
    const limited =
      typeof maxTags === 'number' ? normalized.slice(0, maxTags) : normalized;

    if (value === undefined) setInternal(limited);
    onChange?.(limited);
  };

  const remove = (tag: string) => commit(tags.filter((item) => item !== tag));

  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        gap: 'var(--jx-space-2)',
      }}
    >
      <JxTagInput
        label={label}
        placeholder={placeholder}
        value={tags}
        onValueChange={commit}
      />

      {tags.length > 0 && (
        <ul
          aria-label="Выбранные теги"
          style={{
            display: 'flex',
            flexWrap: 'wrap',
            gap: 'var(--jx-space-2)',
            margin: 0,
            padding: 0,
            listStyle: 'none',
          }}
        >
          {tags.map((tag) => (
            <li key={tag}>
              <JxButton
                variant="ghost"
                size="sm"
                onClick={() => remove(tag)}
                aria-label={`Удалить тег ${tag}`}
              >
                {tag} ✕
              </JxButton>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}