Настройки внешнего вида

Сделай экран настроек внешнего вида: сегментированный переключатель темы на три варианта, ползунок масштаба интерфейса от 80 до 140 процентов с показом текущего значения, переключатель плотной вёрстки и раздел «Ещё» под разделителем с выбором шрифта. Справа от заголовка экрана метка с текущей темой. Состояние храни в компоненте и отдавай наружу через колбэк onChange.

appearance-settings__readme__claude-code__claude-opus-5__1 · 23.09.2026, 20:15:08 мск

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

Первый вызов модели получил 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'.
Выдуманные пропсы
JxSelect.id

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

  • 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 с

Рендер

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

tsx154 строки
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>
  );
}