Пошаговая форма
Сделай трёхшаговую форму регистрации: шаги «Контакты», «Адрес», «Проверка». Сверху показывай индикатор текущего шага, снизу кнопки «Назад» и «Дальше», на последнем шаге кнопка «Отправить» вызывает переданный колбэк onSubmit.
wizard__mcp__claude-code__claude-opus-5__1 · 24.09.2026, 13:26:51 мск
Первый вызов модели получил 12 492 токена входа, включая системный промпт Claude Code; вызовов модели 4, каждый отправлял разговор заново, и самый большой получил 19 839 токенов. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.
Компилятор
Ошибок нет: все компоненты и пропсы существуют.
Линтер и покрытие
- inline-styleСтроки 110, 145, 181, 223, 231, 234, 244Инлайновые стили вместо классов и токенов библиотеки
JxAlert, JxButton, JxDivider, JxInputField, JxStepper, JxTable
Ожидались JxStepper, JxButton, JxInputField · покрытие 100%
- get_rules({"task":"ui","target":"claude"}) → ~1247 токенов
- get_component_api({"name":"JxStepper","detail":"full"}) → ~339 токенов
- get_component_api({"name":"JxButton","detail":"full"}) → ~539 токенов
- get_component_api({"name":"JxInputField","detail":"full"}) → ~484 токенов
- get_component_api({"name":"JxAlert","detail":"signature"}) → ~45 токенов
- get_component_api({"name":"JxTable","detail":"full"}) → ~361 токенов
- get_component_api({"name":"JxDivider","detail":"signature"}) → ~23 токенов
- get_component_api({"name":"JxSelect","detail":"signature"}) → ~68 токенов
- get_component_api({"name":"JxAlert","detail":"full","maxTokens":400}) → ~303 токенов
Инструменты (search_components,get_component_api,get_component_examples,list_design_tokens,get_docs,get_rules, ~1213)
Вход 8 · выход 4 533 · из кэша 60 356 · в кэш 7 822 · 52 с
Рендер
Код компонента
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 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
import { useState } from 'react';
import { JxAlert, JxButton, JxDivider, JxInputField, JxStepper, JxTable } from '@jinx-ui/react';
type SignupValues = {
name: string;
email: string;
phone: string;
country: string;
city: string;
street: string;
zip: string;
};
type SignupErrors = Partial<Record<keyof SignupValues, string>>;
interface SignupWizardProps {
onSubmit: (values: SignupValues) => void | Promise<void>;
}
const STEPS = [{ label: 'Контакты' }, { label: 'Адрес' }, { label: 'Проверка' }];
const EMPTY: SignupValues = {
name: '',
email: '',
phone: '',
country: '',
city: '',
street: '',
zip: '',
};
function validateStep(step: number, values: SignupValues): SignupErrors {
const errors: SignupErrors = {};
if (step === 0) {
if (values.name.trim().length < 2) errors.name = 'Укажите имя целиком, минимум 2 символа';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email.trim())) errors.email = 'Укажите адрес вида name@example.com';
if (values.phone.replace(/\D/g, '').length < 10) errors.phone = 'Укажите номер из 10 цифр';
}
if (step === 1) {
if (values.country.trim().length === 0) errors.country = 'Укажите страну';
if (values.city.trim().length === 0) errors.city = 'Укажите город';
if (values.street.trim().length === 0) errors.street = 'Укажите улицу и дом';
if (!/^\d{5,6}$/.test(values.zip.trim())) errors.zip = 'Индекс — 5 или 6 цифр';
}
return errors;
}
export default function SignupWizard({ onSubmit }: SignupWizardProps) {
const [step, setStep] = useState(0);
const [values, setValues] = useState<SignupValues>(EMPTY);
const [showErrors, setShowErrors] = useState(false);
const [sending, setSending] = useState(false);
const [sendError, setSendError] = useState<string | undefined>(undefined);
const errors = validateStep(step, values);
const visible = (field: keyof SignupValues) => (showErrors ? errors[field] : undefined);
const isLast = step === STEPS.length - 1;
const setField = (field: keyof SignupValues) => (event: React.ChangeEvent<HTMLInputElement>) => {
const next = event.target.value;
setValues((current) => ({ ...current, [field]: next }));
};
const goBack = () => {
setShowErrors(false);
setSendError(undefined);
setStep((current) => Math.max(current - 1, 0));
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (sending) return;
if (Object.keys(errors).length > 0) {
setShowErrors(true);
return;
}
if (!isLast) {
setShowErrors(false);
setStep((current) => current + 1);
return;
}
setSending(true);
setSendError(undefined);
try {
await onSubmit(values);
} catch {
setSendError('Не удалось отправить данные. Проверьте соединение и попробуйте ещё раз.');
} finally {
setSending(false);
}
};
const review: Array<{ label: string; value: string }> = [
{ label: 'Имя', value: values.name },
{ label: 'Почта', value: values.email },
{ label: 'Телефон', value: values.phone },
{ label: 'Страна', value: values.country },
{ label: 'Город', value: values.city },
{ label: 'Улица и дом', value: values.street },
{ label: 'Индекс', value: values.zip },
];
return (
<form
onSubmit={handleSubmit}
noValidate
style={{
display: 'flex',
flexDirection: 'column',
gap: 20,
maxWidth: 520,
padding: 24,
background: 'var(--jx-surface)',
border: '1px solid var(--jx-border)',
borderRadius: 'var(--jx-r-lg)',
boxShadow: 'var(--jx-shadow-sm)',
fontFamily: 'var(--jx-font-sans)',
color: 'var(--jx-text)',
}}
>
<JxStepper steps={STEPS} current={step} />
<JxDivider />
{showErrors && Object.keys(errors).length > 0 ? (
<div role="alert">
<JxAlert intent="danger" title="Проверьте поля шага">
Часть полей заполнена не до конца — исправьте подсвеченные поля и повторите.
</JxAlert>
</div>
) : null}
{sendError ? (
<div role="alert">
<JxAlert intent="danger" title="Отправка не удалась">
{sendError}
</JxAlert>
</div>
) : null}
{step === 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<JxInputField
label="Имя и фамилия"
name="name"
autoComplete="name"
required
value={values.name}
onChange={setField('name')}
errorText={visible('name')}
/>
<JxInputField
label="Электронная почта"
name="email"
type="email"
autoComplete="email"
required
value={values.email}
onChange={setField('email')}
helperText="Нужна для подтверждения регистрации"
errorText={visible('email')}
/>
<JxInputField
label="Телефон"
name="phone"
type="tel"
autoComplete="tel"
prefix="+7"
required
value={values.phone}
onChange={setField('phone')}
errorText={visible('phone')}
/>
</div>
) : null}
{step === 1 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<JxInputField
label="Страна"
name="country"
autoComplete="country-name"
required
value={values.country}
onChange={setField('country')}
errorText={visible('country')}
/>
<JxInputField
label="Город"
name="city"
autoComplete="address-level2"
required
value={values.city}
onChange={setField('city')}
errorText={visible('city')}
/>
<JxInputField
label="Улица и дом"
name="street"
autoComplete="street-address"
required
value={values.street}
onChange={setField('street')}
errorText={visible('street')}
/>
<JxInputField
label="Почтовый индекс"
name="zip"
inputMode="numeric"
autoComplete="postal-code"
required
value={values.zip}
onChange={setField('zip')}
errorText={visible('zip')}
/>
</div>
) : null}
{isLast ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<JxAlert intent="info" title="Проверьте данные перед отправкой">
Вернуться и поправить можно кнопкой «Назад».
</JxAlert>
<JxTable>
<tbody>
{review.map((row) => (
<tr key={row.label}>
<th scope="row" style={{ whiteSpace: 'nowrap', color: 'var(--jx-text-2)' }}>
{row.label}
</th>
<td style={{ wordBreak: 'break-word' }}>{row.value}</td>
</tr>
))}
</tbody>
</JxTable>
</div>
) : null}
<JxDivider />
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<JxButton type="button" variant="ghost" onClick={goBack} disabled={step === 0 || sending}>
Назад
</JxButton>
<JxButton type="submit" variant="primary" disabled={sending}>
{isLast ? (sending ? 'Отправляем…' : 'Отправить') : 'Дальше'}
</JxButton>
</div>
</form>
);
}