Настройки внешнего вида
Сделай экран настроек внешнего вида: сегментированный переключатель темы на три варианта, ползунок масштаба интерфейса от 80 до 140 процентов с показом текущего значения, переключатель плотной вёрстки и раздел «Ещё» под разделителем с выбором шрифта. Справа от заголовка экрана метка с текущей темой. Состояние храни в компоненте и отдавай наружу через колбэк onChange.
appearance-settings__readme__claude-code__claude-opus-5__1 · 23.09.2026, 20:15:08 мск
Первый вызов модели получил 9 582 токена входа, включая системный промпт Claude Code. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.
Компилятор
- Строка 146 · TS2322
Type '{ id: string; value: string; onValueChange: (value: string) => void; options: { value: string; label: string; }[]; }' is not assignable to type 'IntrinsicAttributes & JxSelectProps'.
Линтер и покрытие
- inline-styleСтроки 51, 61, 62, 64, 76, 77, 83, 105, 108, 110, 123, 124, 138, 140, 141, 142Инлайновые стили вместо классов и токенов библиотеки
JxButton, JxSelect, JxSlider
Ожидались JxToggle, JxSlider, JxSwitch, JxDivider, JxBadge · покрытие 20%
README (README.md, ~644)
Вход 2 · выход 3 860 · из кэша 8 233 · в кэш 1 347 · 39 с
Рендер
Код компонента
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
// Имена/пропсы Jinx UI ниже опираются на публично задокументированную часть API
// (JxButton, JxSlider, JxSelect); остальное — разметка на токенах var(--jx-*).
import { useState } from 'react';
import { JxButton, JxSelect, JxSlider } from '@jinx-ui/react';
export type AppearanceSettings = {
theme: 'light' | 'dark' | 'system';
scale: number;
dense: boolean;
fontFamily: string;
};
type Props = {
defaultValue?: Partial<AppearanceSettings>;
onChange?: (settings: AppearanceSettings) => void;
};
const THEMES: ReadonlyArray<{ value: AppearanceSettings['theme']; label: string }> = [
{ value: 'light', label: 'Светлая' },
{ value: 'dark', label: 'Тёмная' },
{ value: 'system', label: 'Системная' },
];
const FONTS = [
{ value: 'system', label: 'Системный' },
{ value: 'inter', label: 'Inter' },
{ value: 'mono', label: 'Моноширинный' },
];
const DEFAULTS: AppearanceSettings = {
theme: 'system',
scale: 100,
dense: false,
fontFamily: 'system',
};
export default function AppearanceSettingsScreen({ defaultValue, onChange }: Props) {
const [settings, setSettings] = useState<AppearanceSettings>({ ...DEFAULTS, ...defaultValue });
const update = (patch: Partial<AppearanceSettings>) => {
const next = { ...settings, ...patch };
setSettings(next);
onChange?.(next);
};
const themeLabel = THEMES.find((t) => t.value === settings.theme)?.label ?? '';
return (
<section
className="jx-stack"
style={{
display: 'flex',
flexDirection: 'column',
gap: 'var(--jx-space-6)',
padding: 'var(--jx-space-6)',
background: 'var(--jx-color-surface)',
color: 'var(--jx-color-fg)',
borderRadius: 'var(--jx-radius-lg)',
}}
>
<header style={{ display: 'flex', alignItems: 'center', gap: 'var(--jx-space-3)' }}>
<h2 style={{ margin: 0, font: 'var(--jx-font-heading-md)' }}>Внешний вид</h2>
<span
style={{
padding: 'var(--jx-space-1) var(--jx-space-3)',
borderRadius: 'var(--jx-radius-full)',
border: '1px solid var(--jx-color-border)',
color: 'var(--jx-color-fg-muted)',
font: 'var(--jx-font-body-sm)',
}}
>
{themeLabel}
</span>
</header>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--jx-space-2)' }}>
<span id="jx-theme-label" style={{ font: 'var(--jx-font-body-sm)', color: 'var(--jx-color-fg-muted)' }}>
Тема
</span>
<div
role="group"
aria-labelledby="jx-theme-label"
style={{
display: 'inline-flex',
gap: 'var(--jx-space-1)',
padding: 'var(--jx-space-1)',
borderRadius: 'var(--jx-radius-md)',
border: '1px solid var(--jx-color-border)',
width: 'fit-content',
}}
>
{THEMES.map((option) => (
<JxButton
key={option.value}
variant={settings.theme === option.value ? 'primary' : 'ghost'}
aria-pressed={settings.theme === option.value}
onClick={() => update({ theme: option.value })}
>
{option.label}
</JxButton>
))}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--jx-space-2)' }}>
<label
htmlFor="jx-scale"
style={{ display: 'flex', justifyContent: 'space-between', font: 'var(--jx-font-body-sm)' }}
>
<span style={{ color: 'var(--jx-color-fg-muted)' }}>Масштаб интерфейса</span>
<span aria-live="polite">{settings.scale}%</span>
</label>
<JxSlider
id="jx-scale"
min={80}
max={140}
step={5}
value={settings.scale}
onValueChange={(value: number) => update({ scale: value })}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--jx-space-4)' }}>
<span id="jx-dense-label" style={{ font: 'var(--jx-font-body-sm)' }}>
Плотная вёрстка
</span>
<JxButton
variant={settings.dense ? 'primary' : 'secondary'}
role="switch"
aria-checked={settings.dense}
aria-labelledby="jx-dense-label"
onClick={() => update({ dense: !settings.dense })}
>
{settings.dense ? 'Включена' : 'Выключена'}
</JxButton>
</div>
<hr style={{ border: 0, borderTop: '1px solid var(--jx-color-border)', margin: 0 }} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--jx-space-3)' }}>
<h3 style={{ margin: 0, font: 'var(--jx-font-heading-sm)' }}>Ещё</h3>
<label htmlFor="jx-font" style={{ font: 'var(--jx-font-body-sm)', color: 'var(--jx-color-fg-muted)' }}>
Шрифт
</label>
<JxSelect
id="jx-font"
value={settings.fontFamily}
onValueChange={(value: string) => update({ fontFamily: value })}
options={FONTS}
/>
</div>
</section>
);
}