/* MBEX — shared helpers and UI primitives for the new Mini App shell */ function AreaChart({ data, w = 326, h = 120, pad = 6, color = "acc", showDot = true }) { const input = Array.isArray(data) && data.length ? data : [0, 0]; const points = input.length === 1 ? [input[0], input[0]] : input; const min = Math.min(...points); const max = Math.max(...points); const range = max - min || 1; const innerH = h - pad * 2; const coords = points.map((value, index) => ({ x: pad + (index / Math.max(1, points.length - 1)) * (w - pad * 2), y: pad + (1 - (value - min) / range) * innerH, })); function smoothPath(list) { if (list.length < 2) { return ""; } let path = `M ${list[0].x} ${list[0].y}`; for (let index = 0; index < list.length - 1; index += 1) { const current = list[index]; const next = list[index + 1]; const cx = (current.x + next.x) / 2; path += ` C ${cx} ${current.y}, ${cx} ${next.y}, ${next.x} ${next.y}`; } return path; } const line = smoothPath(coords); const area = `${line} L ${coords[coords.length - 1].x} ${h} L ${coords[0].x} ${h} Z`; const accent = color === "pos" ? "var(--pos)" : "var(--accent)"; const gradientId = `chart_${color}_${points.length}_${Math.round(points[0] || 0)}`; const last = coords[coords.length - 1]; return ( ); } function monotoneCubicPath(points) { if (points.length < 2) { return points.length ? `M ${points[0].x} ${points[0].y}` : ""; } const slopes = []; for (let index = 0; index < points.length - 1; index += 1) { const width = points[index + 1].x - points[index].x; slopes.push(width ? (points[index + 1].y - points[index].y) / width : 0); } const tangents = points.map((_, index) => { if (index === 0) { return slopes[0]; } if (index === points.length - 1) { return slopes[slopes.length - 1]; } const previous = slopes[index - 1]; const next = slopes[index]; if (previous === 0 || next === 0 || previous * next < 0) { return 0; } return (2 * previous * next) / (previous + next); }); let path = `M ${points[0].x} ${points[0].y}`; for (let index = 0; index < points.length - 1; index += 1) { const current = points[index]; const next = points[index + 1]; const width = next.x - current.x; path += ` C ${(current.x + width / 3).toFixed(1)} ${(current.y + tangents[index] * width / 3).toFixed(1)}, ${(next.x - width / 3).toFixed(1)} ${(next.y - tangents[index + 1] * width / 3).toFixed(1)}, ${next.x} ${next.y}`; } return path; } function formatRateAxis(value, fallback) { if (!value || !Number.isFinite(Date.parse(value))) { return fallback; } return new Date(Date.parse(value)).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", timeZone: "Europe/Moscow", }); } let heroRateChartHasRevealed = false; function HeroRateChart({ data, updatedAt = "", w = 326, h = 116, pad = 6 }) { const source = Array.isArray(data) && data.length ? data : [0, 0]; const rawPoints = source.length === 1 ? [source[0], source[0]] : source; const points = rawPoints.map((item) => ({ value: Number(typeof item === "object" ? item.value : item) || 0, timestamp: typeof item === "object" ? item.timestamp || "" : "", })); const min = Math.min(...points.map((point) => point.value)); const max = Math.max(...points.map((point) => point.value)); const range = max - min || 1; const innerH = h - pad * 2; const [activeIndex, setActiveIndex] = React.useState(null); const [isRevealing, setIsRevealing] = React.useState(false); const activeIndexRef = React.useRef(null); const hasLoadedHistory = points.some((point) => ( point.timestamp && Number.isFinite(Date.parse(point.timestamp)) )); React.useLayoutEffect(() => { if (heroRateChartHasRevealed || !hasLoadedHistory) { return; } heroRateChartHasRevealed = true; setIsRevealing(true); }, [hasLoadedHistory]); React.useEffect(() => { activeIndexRef.current = null; setActiveIndex(null); }, [data]); const coords = points.map((point, index) => ({ x: Number((pad + (index / Math.max(1, points.length - 1)) * (w - pad * 2)).toFixed(1)), y: Number((pad + (1 - (point.value - min) / range) * innerH).toFixed(1)), })); const line = monotoneCubicPath(coords); const area = `${line} L ${coords[coords.length - 1].x} ${h} L ${coords[0].x} ${h} Z`; const activePoint = activeIndex === null ? null : points[activeIndex]; const activeCoord = activeIndex === null ? null : coords[activeIndex]; const activeTime = activePoint?.timestamp || updatedAt; const activeTimeLabel = activeTime && Number.isFinite(Date.parse(activeTime)) ? new Date(Date.parse(activeTime)).toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit", timeZone: "Europe/Moscow", }) : "Время неизвестно"; const axisIndexes = [0, 0.25, 0.5, 0.75, 1].map((position) => ( Math.round(position * (points.length - 1)) )); const fallbackAxis = ["00:00", "06:00", "12:00", "18:00", "24:00"]; function selectPoint(event) { const rect = event.currentTarget.getBoundingClientRect(); if (!rect.width) { return; } const progress = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)); const nextIndex = Math.round(progress * (points.length - 1)); if (activeIndexRef.current !== nextIndex) { activeIndexRef.current = nextIndex; const haptic = window.Telegram?.WebApp?.HapticFeedback; if (haptic?.selectionChanged) { haptic.selectionChanged(); } } setActiveIndex(nextIndex); } function clearSelection() { activeIndexRef.current = null; setActiveIndex(null); } return (