Выбор тегов

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

tag-picker__mcp__claude-code__claude-opus-5__1 · 24.09.2026, 13:28:37 мск

Проверки пройдены
MCP · claude-opus-5 · Claude Code
Контекст до задачи~2 405
Токенов на прогон48 563
Цена$0.126
Ошибок компилятора0

Первый вызов модели получил 12 466 токенов входа, включая системный промпт Claude Code; вызовов модели 3, каждый отправлял разговор заново, и самый большой получил 17 211 токенов. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.

Компилятор

Ошибок нет: все компоненты и пропсы существуют.

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

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

JxChip, JxInputField

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

Вызовы инструментов · 4
  1. get_rules({"task":"ui"}) → ~1247 токенов
  2. get_component_api({"name":"JxTagInput","detail":"full"}) → ~323 токенов
  3. get_component_api({"name":"JxChip","detail":"full"}) → ~360 токенов
  4. get_component_api({"name":"JxInputField","detail":"full"}) → ~484 токенов
Контекст

Инструменты (search_components,get_component_api,get_component_examples,list_design_tokens,get_docs,get_rules, ~1213)

Вход 6 · выход 2 915 · из кэша 40 448 · в кэш 5 194 · 40 с

Рендер

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

tsx102 строки
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { useState, type KeyboardEvent } from 'react';
import { JxChip, JxInputField } from '@jinx-ui/react';

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

export default function ArticleTagsField({
  label = 'Теги статьи',
  placeholder = 'Введите тег и нажмите Enter',
  value,
  defaultValue,
  onChange,
}: ArticleTagsFieldProps) {
  const [draft, setDraft] = useState('');
  const [error, setError] = useState<string | undefined>(undefined);
  const [internalTags, setInternalTags] = useState<string[]>(defaultValue ?? []);

  const tags = value ?? internalTags;

  const commit = (next: string[]) => {
    if (value === undefined) setInternalTags(next);
    onChange?.(next);
  };

  const addTag = () => {
    const tag = draft.trim();
    if (tag.length === 0) return;
    if (tags.some((existing) => existing.toLowerCase() === tag.toLowerCase())) {
      setError(`Тег «${tag}» уже добавлен`);
      return;
    }
    commit([...tags, tag]);
    setDraft('');
    setError(undefined);
  };

  const removeTag = (tag: string) => {
    commit(tags.filter((existing) => existing !== tag));
    setError(undefined);
  };

  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === 'Enter' || event.key === ',') {
      event.preventDefault();
      addTag();
      return;
    }
    if (event.key === 'Backspace' && draft.length === 0 && tags.length > 0) {
      event.preventDefault();
      removeTag(tags[tags.length - 1]);
    }
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', fontFamily: 'var(--jx-font-sans)' }}>
      <JxInputField
        label={label}
        placeholder={placeholder}
        value={draft}
        onChange={(event) => {
          setDraft(event.target.value);
          setError(undefined);
        }}
        onKeyDown={handleKeyDown}
        onBlur={addTag}
        errorText={error}
        helperText="Enter или запятая добавляет тег, Backspace удаляет последний"
        aria-describedby="article-tags-list"
      />

      <div
        id="article-tags-list"
        role="group"
        aria-label="Выбранные теги"
        style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '8px', minHeight: '28px' }}
      >
        {tags.length === 0 ? (
          <span style={{ color: 'var(--jx-text-2)', fontSize: '13px' }}>
            Пока ни одного тега — введите первый и нажмите Enter
          </span>
        ) : (
          tags.map((tag) => (
            <JxChip
              key={tag}
              removable
              onRemove={() => removeTag(tag)}
              title={tag}
              style={{ maxWidth: '220px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
            >
              {tag}
            </JxChip>
          ))
        )}
      </div>
    </div>
  );
}