Панель расхода с фильтрами
Сделай панель расхода по проектам. Сверху чипы-фильтры по статусу, которые можно снимать. Под ними на каждый проект строка с названием, круговым индикатором заполненности квоты в процентах и меткой статуса. Список постраничный, по пять строк. Если после фильтрации ничего не осталось, вместо списка показывается пустое состояние с кнопкой сброса фильтров.
usage-dashboard__docs-rules__claude-code__claude-opus-5__1 · 23.09.2026, 21:56:44 мск
Первый вызов модели получил 49 092 токена входа, включая системный промпт Claude Code. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.
Компилятор
Ошибок нет: все компоненты и пропсы существуют.
Линтер и покрытие
- inline-styleСтроки 67, 69, 72, 80, 119, 126, 137, 139, 149Инлайновые стили вместо классов и токенов библиотеки
JxBadge, JxButton, JxChip, JxEmptyState, JxPagination, JxProgressCircle
Ожидались JxChip, JxProgressCircle, JxBadge, JxPagination, JxEmptyState · покрытие 100%
Доки (llms-full.txt, ~22548) · Правила (jinx-ui, ~1083)
Вход 2 · выход 2 471 · из кэша 8 233 · в кэш 40 857 · 26 с
Рендер
Код компонента
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
import { useMemo, useState } from 'react';
import {
JxBadge,
JxButton,
JxChip,
JxEmptyState,
JxPagination,
JxProgressCircle,
} from '@jinx-ui/react';
type ProjectStatus = 'active' | 'paused' | 'over';
interface Project {
id: string;
name: string;
used: number;
quota: number;
status: ProjectStatus;
}
const STATUSES: Array<{ value: ProjectStatus; label: string; tone: 'success' | 'warning' | 'danger' }> = [
{ value: 'active', label: 'Активные', tone: 'success' },
{ value: 'paused', label: 'На паузе', tone: 'warning' },
{ value: 'over', label: 'Превышен расход', tone: 'danger' },
];
const PROJECTS: Project[] = [
{ id: 'p1', name: 'Контекстная библиотека', used: 420, quota: 1000, status: 'active' },
{ id: 'p2', name: 'Индексация документации', used: 980, quota: 1000, status: 'over' },
{ id: 'p3', name: 'Ночные прогоны оценок', used: 150, quota: 800, status: 'paused' },
{ id: 'p4', name: 'Панель наблюдаемости', used: 610, quota: 900, status: 'active' },
{ id: 'p5', name: 'Миграция хранилища заметок', used: 75, quota: 500, status: 'paused' },
{ id: 'p6', name: 'Экспорт отчётов в Obsidian', used: 1240, quota: 1000, status: 'over' },
{ id: 'p7', name: 'Сборка токенов оформления', used: 320, quota: 600, status: 'active' },
{ id: 'p8', name: 'Проверка доступности интерфейса', used: 210, quota: 700, status: 'active' },
{ id: 'p9', name: 'Архив прошлых экспериментов', used: 40, quota: 400, status: 'paused' },
{ id: 'p10', name: 'Пересчёт квот по командам', used: 1010, quota: 1000, status: 'over' },
{ id: 'p11', name: 'Правила набора и линтер', used: 480, quota: 900, status: 'active' },
];
const PAGE_SIZE = 5;
function ProjectSpendPanel() {
const [statuses, setStatuses] = useState<ProjectStatus[]>(['active', 'paused', 'over']);
const [page, setPage] = useState(1);
const visible = useMemo(
() => PROJECTS.filter((project) => statuses.includes(project.status)),
[statuses],
);
const pageCount = Math.max(1, Math.ceil(visible.length / PAGE_SIZE));
const currentPage = Math.min(page, pageCount);
const rows = visible.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
const toggleStatus = (value: ProjectStatus) => {
setStatuses((prev) => (prev.includes(value) ? prev.filter((item) => item !== value) : [...prev, value]));
setPage(1);
};
const resetFilters = () => {
setStatuses(STATUSES.map((status) => status.value));
setPage(1);
};
return (
<section style={{ display: 'grid', gap: '20px', fontFamily: 'var(--jx-font-sans)', color: 'var(--jx-text)' }}>
<div>
<h2 style={{ margin: '0 0 4px', fontFamily: 'var(--jx-font-display)', fontSize: '20px' }}>
Расход по проектам
</h2>
<p style={{ margin: 0, color: 'var(--jx-text-2)', fontSize: '14px' }}>
Заполненность квоты за текущий месяц.
</p>
</div>
<div
role="group"
aria-label="Фильтр по статусу"
style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' }}
>
{STATUSES.map((status) => {
const active = statuses.includes(status.value);
return (
<JxChip
key={status.value}
active={active}
removable={active}
onRemove={() => toggleStatus(status.value)}
role="button"
tabIndex={0}
aria-pressed={active}
onClick={() => toggleStatus(status.value)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
toggleStatus(status.value);
}
}}
>
{status.label}
</JxChip>
);
})}
</div>
{rows.length === 0 ? (
<JxEmptyState
title="Под фильтры ничего не подошло"
message="Сняты все статусы или под выбранные условия нет проектов. Верните фильтры, чтобы увидеть список."
action={
<JxButton variant="primary" onClick={resetFilters}>
Сбросить фильтры
</JxButton>
}
/>
) : (
<>
<ul style={{ display: 'grid', gap: '8px', listStyle: 'none', margin: 0, padding: 0 }}>
{rows.map((project) => {
const status = STATUSES.find((item) => item.value === project.status);
const percent = Math.round((project.used / project.quota) * 100);
return (
<li
key={project.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
padding: '12px 16px',
background: 'var(--jx-surface)',
border: '1px solid var(--jx-border)',
borderRadius: 'var(--jx-r)',
}}
>
<JxProgressCircle value={percent} max={100} showValue aria-label={`Квота: ${percent} процентов`} />
<div style={{ display: 'grid', gap: '2px', minWidth: 0, flex: 1 }}>
<span
style={{
fontSize: '15px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={project.name}
>
{project.name}
</span>
<span style={{ fontSize: '13px', color: 'var(--jx-text-2)' }}>
{project.used} из {project.quota} единиц
</span>
</div>
{status ? <JxBadge tone={status.tone} dot>{status.label}</JxBadge> : null}
</li>
);
})}
</ul>
{pageCount > 1 ? (
<JxPagination total={pageCount} page={currentPage} onPageChange={setPage} aria-label="Страницы проектов" />
) : null}
</>
)}
</section>
);
}
export default ProjectSpendPanel;