stats/ (Flet) генерировал графики через matplotlib в PNG и отдавал их через Flutter web с WebSocket — та же архитектура, что подводила основной фронтенд (см. известные ошибки в README). Плавающая версия flet>=0.82.2 недавно подтянула 0.86.0 с несовместимым протоколом фреймирования, и дашборд перестал открываться в браузере. stats-react/ — SPA по тому же паттерну, что и web-react (Vite+React+TS, свой Dockerfile+nginx, same-origin прокси /api вместо WebSocket). Функциональность 1:1: 4 summary-карточки, общие фильтры (тест/ организация/группа/год), два таба (прохождения / результаты тестов), 8 графиков, топ-10 таблица, фильтр по оси + сравнение по организациям/ группам. Круговая диаграмма заменена на 100%-stacked bar (dataviz: "part-to-whole rides on the stacked bar chart; donut stays deprioritized") с фолдингом длинного хвоста категорий в "Другое" — у оригинала было 32 категории на 8 цветов, из-за чего сегменты становились неразличимы. Категориальная палитра графиков провалидирована на CVD-безопасность (scripts/validate_palette.js, light+dark) — 8-цветная палитра из исходного designer.py не проходила проверку (hard fail по normal-vision floor). Бренд-оранжевый сохранён в палитре, остальные 7 слотов — из референсного набора skill'а. stats/ и Dockerfile.stats удалены, docker-compose.yml переключён на stats-react. Проверено сборкой всех трёх образов, полным compose up и визуально (headless Chrome, light+dark, обе вкладки, реальные данные с прод-сервера). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
2.1 KiB
TypeScript
68 lines
2.1 KiB
TypeScript
// Небольшой набор inline-SVG иконок (замена Material Icons из Flet-версии).
|
|
import type { SVGProps } from "react";
|
|
|
|
type P = SVGProps<SVGSVGElement>;
|
|
const base = (p: P) => ({
|
|
width: "1em",
|
|
height: "1em",
|
|
viewBox: "0 0 24 24",
|
|
fill: "currentColor",
|
|
...p,
|
|
});
|
|
|
|
export const IconAssignment = (p: P) => (
|
|
<svg {...base(p)}>
|
|
<path d="M19 3h-4.18a3 3 0 0 0-5.64 0H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Zm-7 0a1 1 0 1 1-1 1 1 1 0 0 1 1-1Zm-1.5 14.5-3.5-3.5 1.4-1.4 2.1 2.1 5.1-5.1L17 11l-6.5 6.5Z" />
|
|
</svg>
|
|
);
|
|
|
|
export const IconQuiz = (p: P) => (
|
|
<svg {...base(p)}>
|
|
<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Zm-6.5 15h-1v-1h1Zm1-4.4c-.6.5-.7.7-.7 1.4h-1c0-1.1.3-1.6 1-2.2.5-.4.8-.7.8-1.3 0-.7-.5-1.1-1.3-1.1s-1.3.5-1.4 1.2l-1.1-.2c.2-1.2 1.1-2 2.5-2 1.5 0 2.4.9 2.4 2.1 0 .8-.4 1.4-1.2 2.1Z" />
|
|
</svg>
|
|
);
|
|
|
|
export const IconPlayCircle = (p: P) => (
|
|
<svg {...base(p)}>
|
|
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2Zm-2 14.5v-9l7 4.5Z" />
|
|
</svg>
|
|
);
|
|
|
|
export const IconBusiness = (p: P) => (
|
|
<svg {...base(p)}>
|
|
<path d="M6 3h8v4h6v14H4V3Zm2 2v14h4v-4h4v4h4V9h-4V5H8Zm2 3h2v2h-2Zm4 0h2v2h-2ZM10 12h2v2h-2Zm4 0h2v2h-2Z" />
|
|
</svg>
|
|
);
|
|
|
|
export const IconFilterOff = (p: P) => (
|
|
<svg {...base(p)}>
|
|
<path d="M2.4 3.8 3.8 2.4l18 18-1.4 1.4-5.9-5.9-1.5 1.9v3.8h-2v-3.8L4.5 8.9l1.5-1.5H4V5h2.4Zm5.9 0H20v2h-9.7Z" />
|
|
</svg>
|
|
);
|
|
|
|
export const IconRefresh = (p: P) => (
|
|
<svg {...base(p)} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
<path d="M20 11A8 8 0 1 0 18.5 16" />
|
|
<path d="M20 4v7h-7" />
|
|
</svg>
|
|
);
|
|
|
|
export const IconBarChart = (p: P) => (
|
|
<svg {...base(p)}>
|
|
<path d="M5 10h3v10H5Zm11-6h3v16h-3ZM10.5 15h3v5h-3Z" />
|
|
</svg>
|
|
);
|
|
|
|
export const IconInsights = (p: P) => (
|
|
<svg {...base(p)} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M3 17l6-6 4 4 8-8" />
|
|
<path d="M15 6h6v6" />
|
|
</svg>
|
|
);
|
|
|
|
export const IconCaretDown = (p: P) => (
|
|
<svg {...base(p)}>
|
|
<path d="M7 10l5 5 5-5Z" />
|
|
</svg>
|
|
);
|