All checks were successful
Deploy Trade-In / build-frontend (push) Successful in 1m55s
Deploy Trade-In / deploy (push) Successful in 44s
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
926 lines
29 KiB
TypeScript
926 lines
29 KiB
TypeScript
"use client";
|
||
|
||
import { Fragment } from "react";
|
||
import type { Ref } from "react";
|
||
import { tokens } from "./tokens";
|
||
import {
|
||
ranges,
|
||
resultCards,
|
||
resultMeta,
|
||
scatterMini,
|
||
sources,
|
||
} from "./fixtures";
|
||
import type { ResultPanelData } from "./mappers";
|
||
|
||
const {
|
||
accent,
|
||
accentDeep,
|
||
onAccent,
|
||
ink,
|
||
body,
|
||
body2,
|
||
muted,
|
||
muted2,
|
||
muted3,
|
||
line,
|
||
line2,
|
||
lineSoft,
|
||
bracket,
|
||
barLo,
|
||
barMid,
|
||
track,
|
||
success,
|
||
hint,
|
||
disabledBorder,
|
||
disabledValue,
|
||
disabledLabel,
|
||
infoSoftBg,
|
||
surface,
|
||
font,
|
||
} = tokens;
|
||
|
||
// Default presentation data (unwired usage): the existing design fixtures.
|
||
const RESULT_FIXTURE: ResultPanelData = {
|
||
cards: resultCards,
|
||
meta: resultMeta,
|
||
ranges,
|
||
scatterMini,
|
||
sources,
|
||
};
|
||
|
||
interface ResultPanelProps {
|
||
data?: ResultPanelData;
|
||
onNavigate: (i: number) => void;
|
||
// #2264 C7: after a successful estimate the page moves focus here (a labelled,
|
||
// programmatically-focusable region) so keyboard/SR users land on the result
|
||
// instead of the <body>. tabIndex={-1} makes it focus()-able without adding a
|
||
// Tab stop.
|
||
regionRef?: Ref<HTMLDivElement>;
|
||
}
|
||
|
||
// Mini-histogram column tint: peak column = accent, its neighbours = mid, rest = lo.
|
||
function barColor(i: number, bars: number[]): string {
|
||
const peak = bars.indexOf(Math.max(...bars));
|
||
if (i === peak) return accent;
|
||
if (i === peak - 1 || i === peak + 1) return barMid;
|
||
return barLo;
|
||
}
|
||
|
||
// Render a string[] with <br/> between lines (faithful to the design line breaks).
|
||
function lines(parts: string[]) {
|
||
return parts.map((part, i) => (
|
||
<Fragment key={i}>
|
||
{i > 0 && <br />}
|
||
{part}
|
||
</Fragment>
|
||
));
|
||
}
|
||
|
||
// Split a source name on its parenthetical so the design's forced
|
||
// "РОСРЕЕСТР / (ВНУТР.)" two-line break (line-height 1.3) is reproduced; plain
|
||
// names without " (" stay on a single line.
|
||
function sourceName(name: string) {
|
||
const idx = name.indexOf(" (");
|
||
if (idx === -1) return name;
|
||
return (
|
||
<>
|
||
{name.slice(0, idx)}
|
||
<br />
|
||
{name.slice(idx + 1)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
export default function ResultPanel({
|
||
data = RESULT_FIXTURE,
|
||
onNavigate,
|
||
regionRef,
|
||
}: ResultPanelProps) {
|
||
return (
|
||
<div
|
||
ref={regionRef}
|
||
role="region"
|
||
aria-labelledby="v2-result-heading"
|
||
tabIndex={-1}
|
||
style={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
gap: 14,
|
||
minWidth: 0,
|
||
outline: "none",
|
||
}}
|
||
>
|
||
<style>{`
|
||
.rp-more { transition: opacity 120ms ease; }
|
||
.rp-more:hover { opacity: .6; }
|
||
`}</style>
|
||
|
||
{/* header */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "baseline",
|
||
justifyContent: "space-between",
|
||
}}
|
||
>
|
||
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
|
||
<span style={{ fontFamily: font.mono, fontSize: 15, color: accent }}>
|
||
02
|
||
</span>
|
||
{/* Panel heading — real <h2> + the region's accessible name
|
||
(aria-labelledby). Preflight not loaded → reset UA margin inline. */}
|
||
<h2
|
||
id="v2-result-heading"
|
||
style={{
|
||
margin: 0,
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
letterSpacing: 2,
|
||
}}
|
||
>
|
||
РЕЗУЛЬТАТ ОЦЕНКИ
|
||
</h2>
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 22,
|
||
fontSize: 10,
|
||
letterSpacing: 1,
|
||
color: muted,
|
||
}}
|
||
>
|
||
<span>
|
||
ИСТОЧНИКОВ:{" "}
|
||
<b style={{ color: ink, fontFamily: font.mono }}>
|
||
{data.meta.sources}
|
||
</b>
|
||
</span>
|
||
<span>
|
||
ДОСТОВЕРНОСТЬ: <b style={{ color: ink }}>{data.meta.confidence}</b>
|
||
</span>
|
||
<span>
|
||
РАЗБРОС ЦЕН{" "}
|
||
<b style={{ color: accent, fontFamily: font.mono }}>
|
||
{data.meta.cv}
|
||
</b>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* result cards */}
|
||
<div
|
||
style={{
|
||
display: "grid",
|
||
// #1991 — the registry (ДКП) tile is an external anchor, not a peer
|
||
// of the two prices that actually matter (asking / expected). Equal
|
||
// 1fr columns still read as "three equivalent options" even with
|
||
// the demoted colors/type-scale below, so the two primary cards get
|
||
// ~50% more width and the registry tile narrows into a visibly
|
||
// secondary, reference-only column (closer to a convergence strip
|
||
// than a full-size card, per audit acceptance criteria).
|
||
gridTemplateColumns: "1.3fr 1.3fr 0.85fr",
|
||
gap: 14,
|
||
flex: "0 0 auto",
|
||
}}
|
||
>
|
||
{data.cards.map((card, ci) => {
|
||
// Price hierarchy (audit H2 + M1). The three cards are NOT equal weight:
|
||
// ci 0 — РЕКОМЕНДОВАННАЯ ЦЕНА В ОБЪЯВЛЕНИИ (market asking, baseline)
|
||
// ci 1 — ОЖИДАЕМАЯ ЦЕНА СДЕЛКИ (the real answer → headline)
|
||
// ci 2 — ДКП · РОСРЕЕСТР (registry reference, ~−43% → demoted)
|
||
// The headline gets an accent border + tint + glow + a larger accent
|
||
// value so the user can't confuse it with the registry figure; the
|
||
// registry card is demoted to a softer, smaller, muted "СПРАВОЧНО"
|
||
// tile. Value scale reads expected(40) ≫ asking(30) > ДКП(28).
|
||
// #1991 — column width above is the third, independent axis: asking
|
||
// and expected visually dominate the row, ДКП is narrow/secondary.
|
||
//
|
||
// H1 — when the expected card has no prediction (emptyNote), it drops
|
||
// to an honest empty-state and the accent frame moves onto the asking
|
||
// card (ci 0) so the panel always has ONE clear headline, never a hole.
|
||
const expectedEmpty = Boolean(data.cards[1]?.emptyNote);
|
||
const isEmptyCard = Boolean(card.emptyNote);
|
||
const isRegistry = ci === 2;
|
||
const headlineIndex = expectedEmpty ? 0 : 1;
|
||
const isHeadline = !isEmptyCard && ci === headlineIndex;
|
||
const isAsking = ci === 0;
|
||
return (
|
||
<div
|
||
key={ci}
|
||
style={{
|
||
position: "relative",
|
||
background: isHeadline
|
||
? infoSoftBg
|
||
: isRegistry
|
||
? surface.w40
|
||
: surface.w55,
|
||
backdropFilter: "blur(6px)",
|
||
border: isHeadline
|
||
? `1.5px solid ${accent}`
|
||
: isRegistry
|
||
? `1px solid ${lineSoft}`
|
||
: `1px solid ${line2}`,
|
||
borderRadius: 8,
|
||
padding: "15px 16px 13px",
|
||
boxShadow: isHeadline
|
||
? "0 6px 22px rgba(46,139,255,.16)"
|
||
: undefined,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: -1,
|
||
top: -1,
|
||
width: 12,
|
||
height: 12,
|
||
borderLeft: `1.5px solid ${isHeadline ? accent : bracket}`,
|
||
borderTop: `1.5px solid ${isHeadline ? accent : bracket}`,
|
||
}}
|
||
/>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "flex-start",
|
||
justifyContent: "space-between",
|
||
gap: 8,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: 9.5,
|
||
letterSpacing: 1.5,
|
||
// Contrast follow-up (audit #2266): accent (#2e8bff) on the
|
||
// headline card's pale infoSoftBg tint measured ≈2.87:1 — below
|
||
// even the AA "large text" floor (3:1). accentDeep is the same
|
||
// brand hue, darkened (existing token, not a one-off hex) — it
|
||
// clears ~4.2:1 here, so the label reads reliably too.
|
||
color: isHeadline ? accentDeep : isRegistry ? muted2 : body2,
|
||
fontWeight: isHeadline ? 600 : undefined,
|
||
lineHeight: 1.6,
|
||
}}
|
||
>
|
||
{lines(card.title)}
|
||
</div>
|
||
{/* #1991 — explicit "this is the figure that goes into the offer"
|
||
mark on the headline card, so the two dominant prices are not
|
||
just visually bigger but literally labelled which one to quote. */}
|
||
{isHeadline && !isEmptyCard && (
|
||
<span
|
||
style={{
|
||
flex: "0 0 auto",
|
||
fontSize: 8,
|
||
fontWeight: 700,
|
||
letterSpacing: 1,
|
||
color: onAccent,
|
||
background: accentDeep,
|
||
borderRadius: 3,
|
||
padding: "3px 6px",
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
В ОФФЕР
|
||
</span>
|
||
)}
|
||
</div>
|
||
{isRegistry && (
|
||
<div
|
||
style={{
|
||
fontSize: 8,
|
||
letterSpacing: 1.2,
|
||
color: muted3,
|
||
marginTop: 3,
|
||
}}
|
||
>
|
||
{/* §M8 — расшифровка «ДКП» один раз на секцию. */}
|
||
СПРАВОЧНО · ДОГОВОРЫ КУПЛИ-ПРОДАЖИ
|
||
</div>
|
||
)}
|
||
{/* H1 empty-state: no bare «—» / gauge / «· с учётом торга» — just an
|
||
honest note pointing at the recommended asking price. */}
|
||
{isEmptyCard ? (
|
||
<div
|
||
style={{
|
||
marginTop: 12,
|
||
fontSize: 11,
|
||
lineHeight: 1.5,
|
||
color: body,
|
||
maxWidth: "94%",
|
||
}}
|
||
>
|
||
{card.emptyNote}
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div
|
||
style={{
|
||
marginTop: 14,
|
||
display: "flex",
|
||
alignItems: "baseline",
|
||
gap: 6,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontFamily: font.mono,
|
||
// M1 — asking (30) sits above the ДКП «справочно» tier (28)
|
||
// yet clearly below the accent headline (40).
|
||
fontSize: isHeadline ? 40 : isRegistry ? 28 : 30,
|
||
fontWeight: isHeadline ? 600 : 400,
|
||
letterSpacing: -1,
|
||
// Contrast follow-up (audit #2266) — accentDeep, not
|
||
// accent, for the same reason as the title label above.
|
||
color: isHeadline
|
||
? accentDeep
|
||
: isRegistry
|
||
? muted
|
||
: isAsking
|
||
? ink
|
||
: undefined,
|
||
}}
|
||
>
|
||
{card.value}
|
||
</span>
|
||
<span
|
||
style={{
|
||
fontSize: 13,
|
||
color: muted,
|
||
letterSpacing: 1,
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
{card.unit}
|
||
</span>
|
||
</div>
|
||
{card.range && (
|
||
<div
|
||
style={{
|
||
fontFamily: font.mono,
|
||
fontSize: 11,
|
||
color: muted,
|
||
marginTop: 10,
|
||
}}
|
||
>
|
||
{card.range}
|
||
</div>
|
||
)}
|
||
{card.ppm && (
|
||
<div
|
||
style={{
|
||
fontFamily: font.mono,
|
||
fontSize: 10,
|
||
color: muted2,
|
||
marginTop: 4,
|
||
}}
|
||
>
|
||
{card.ppm}
|
||
</div>
|
||
)}
|
||
{card.note && (
|
||
<div
|
||
style={{
|
||
fontSize: 9,
|
||
color: muted2,
|
||
marginTop: 4,
|
||
lineHeight: 1.4,
|
||
maxWidth: "82%",
|
||
}}
|
||
>
|
||
{card.note}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{isEmptyCard ? (
|
||
<button
|
||
type="button"
|
||
className="rp-more"
|
||
onClick={() => onNavigate(card.nav)}
|
||
aria-label={`Подробнее — ${card.title.join(" ")}`}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "flex-end",
|
||
gap: 4,
|
||
cursor: "pointer",
|
||
fontSize: 9,
|
||
letterSpacing: 0.3,
|
||
color: accent,
|
||
width: "100%",
|
||
background: "none",
|
||
border: "none",
|
||
padding: 0,
|
||
margin: 0,
|
||
marginTop: 16,
|
||
fontFamily: "inherit",
|
||
}}
|
||
>
|
||
Подробнее <span>→</span>
|
||
</button>
|
||
) : card.bars ? (
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "flex-end",
|
||
justifyContent: "space-between",
|
||
gap: 12,
|
||
marginTop: 12,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "flex-end",
|
||
gap: 2,
|
||
height: 20,
|
||
flex: 1,
|
||
}}
|
||
>
|
||
{card.bars.map((h, bi) => (
|
||
<span
|
||
key={bi}
|
||
style={{
|
||
flex: 1,
|
||
height: `${h}%`,
|
||
background: barColor(bi, card.bars as number[]),
|
||
}}
|
||
/>
|
||
))}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
className="rp-more"
|
||
onClick={() => onNavigate(card.nav)}
|
||
aria-label={`Подробнее — ${card.title.join(" ")}`}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 4,
|
||
cursor: "pointer",
|
||
fontSize: 9,
|
||
letterSpacing: 0.3,
|
||
color: accent,
|
||
whiteSpace: "nowrap",
|
||
background: "none",
|
||
border: "none",
|
||
padding: 0,
|
||
margin: 0,
|
||
fontFamily: "inherit",
|
||
}}
|
||
>
|
||
Подробнее <span>→</span>
|
||
</button>
|
||
</div>
|
||
) : (
|
||
// M2 — calm delta pill (was a 51px circular gauge that read like a
|
||
// tech "занижение" indicator). Full text «−18% к цене объявления»,
|
||
// neutral accentDeep on a soft-blue chip, tabular-nums.
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
gap: 10,
|
||
marginTop: 14,
|
||
}}
|
||
>
|
||
{card.delta ? (
|
||
<span
|
||
style={{
|
||
display: "inline-flex",
|
||
alignItems: "baseline",
|
||
gap: 5,
|
||
background: infoSoftBg,
|
||
border: `1px solid ${lineSoft}`,
|
||
borderRadius: 20,
|
||
padding: "3px 10px",
|
||
color: accentDeep,
|
||
fontFamily: font.sans,
|
||
fontVariantNumeric: "tabular-nums",
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontFamily: font.mono,
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
}}
|
||
>
|
||
{card.delta}
|
||
</span>
|
||
<span style={{ fontSize: 9.5, letterSpacing: 0.2 }}>
|
||
{card.deltaLabel}
|
||
</span>
|
||
</span>
|
||
) : (
|
||
<span />
|
||
)}
|
||
<button
|
||
type="button"
|
||
className="rp-more"
|
||
onClick={() => onNavigate(card.nav)}
|
||
aria-label={`Подробнее — ${card.title.join(" ")}`}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 4,
|
||
cursor: "pointer",
|
||
fontSize: 9,
|
||
letterSpacing: 0.3,
|
||
color: accent,
|
||
whiteSpace: "nowrap",
|
||
background: "none",
|
||
border: "none",
|
||
padding: 0,
|
||
margin: 0,
|
||
fontFamily: "inherit",
|
||
}}
|
||
>
|
||
Подробнее <span>→</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* §1.2 честный разброс: диапазоны цен = рыночный спред, НЕ погрешность
|
||
оценки. Ставим сразу под ценовыми карточками, чтобы широкий диапазон
|
||
не читался как «неуверенность расчёта». */}
|
||
<div
|
||
style={{
|
||
// M3 — было 10px muted-микропринт; повышено до 12px body, чтобы
|
||
// ключевая honest-оговорка о разбросе читалась, а не терялась.
|
||
fontSize: 12,
|
||
lineHeight: 1.5,
|
||
color: body,
|
||
marginTop: -4,
|
||
}}
|
||
>
|
||
Диапазоны показывают разброс цен на рынке, а не погрешность оценки.
|
||
</div>
|
||
|
||
{/* ranges + radar */}
|
||
<div
|
||
style={{
|
||
position: "relative",
|
||
background: surface.w50,
|
||
backdropFilter: "blur(6px)",
|
||
border: `1px solid ${line2}`,
|
||
borderRadius: 8,
|
||
padding: "18px 20px",
|
||
display: "grid",
|
||
gridTemplateColumns: "1fr 1fr",
|
||
gap: 24,
|
||
alignItems: "center",
|
||
flex: "0 0 auto",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
right: -1,
|
||
bottom: -1,
|
||
width: 13,
|
||
height: 13,
|
||
borderRight: `1.5px solid ${bracket}`,
|
||
borderBottom: `1.5px solid ${bracket}`,
|
||
}}
|
||
/>
|
||
|
||
{/* left range — ads */}
|
||
<div>
|
||
<div
|
||
style={{
|
||
fontSize: 9,
|
||
letterSpacing: 1.3,
|
||
color: muted2,
|
||
lineHeight: 1.5,
|
||
}}
|
||
>
|
||
{lines(data.ranges.ads.label)}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontFamily: font.mono,
|
||
fontSize: 11,
|
||
color: muted,
|
||
margin: "12px 0 16px",
|
||
}}
|
||
>
|
||
медиана · <b style={{ color: ink }}>{data.ranges.ads.median}</b>
|
||
</div>
|
||
<div
|
||
style={{
|
||
position: "relative",
|
||
height: 3,
|
||
background: track,
|
||
borderRadius: 2,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: data.ranges.ads.marker.fillLeft,
|
||
right: data.ranges.ads.marker.fillRight,
|
||
top: 0,
|
||
bottom: 0,
|
||
background: `linear-gradient(90deg,${barMid},${accent})`,
|
||
borderRadius: 2,
|
||
}}
|
||
/>
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: data.ranges.ads.marker.dotLeft,
|
||
top: "50%",
|
||
transform: "translate(-50%,-50%)",
|
||
width: 3,
|
||
height: 16,
|
||
borderRadius: 1,
|
||
background: accent,
|
||
}}
|
||
/>
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
fontFamily: font.mono,
|
||
fontSize: 10,
|
||
color: muted2,
|
||
marginTop: 10,
|
||
}}
|
||
>
|
||
<span>{data.ranges.ads.lo}</span>
|
||
<span style={{ color: accent }}>{data.ranges.ads.median}</span>
|
||
<span>{data.ranges.ads.hi}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* right range — deals */}
|
||
<div>
|
||
<div
|
||
style={{
|
||
fontSize: 9,
|
||
letterSpacing: 1.3,
|
||
color: muted2,
|
||
lineHeight: 1.5,
|
||
}}
|
||
>
|
||
{lines(data.ranges.deals.label)}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontFamily: font.mono,
|
||
fontSize: 11,
|
||
color: muted,
|
||
margin: "12px 0 16px",
|
||
}}
|
||
>
|
||
медиана · <b style={{ color: ink }}>{data.ranges.deals.median}</b>
|
||
</div>
|
||
<div
|
||
style={{
|
||
position: "relative",
|
||
height: 3,
|
||
background: track,
|
||
borderRadius: 2,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: data.ranges.deals.marker.fillLeft,
|
||
right: data.ranges.deals.marker.fillRight,
|
||
top: 0,
|
||
bottom: 0,
|
||
background: `linear-gradient(90deg,${barMid},${accent})`,
|
||
borderRadius: 2,
|
||
}}
|
||
/>
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
left: data.ranges.deals.marker.dotLeft,
|
||
top: "50%",
|
||
transform: "translate(-50%,-50%)",
|
||
width: 3,
|
||
height: 16,
|
||
borderRadius: 1,
|
||
background: accent,
|
||
}}
|
||
/>
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
fontFamily: font.mono,
|
||
fontSize: 10,
|
||
color: muted2,
|
||
marginTop: 10,
|
||
}}
|
||
>
|
||
<span>{data.ranges.deals.lo}</span>
|
||
<span style={{ color: accent }}>{data.ranges.deals.median}</span>
|
||
<span>{data.ranges.deals.hi}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* sources */}
|
||
<div
|
||
style={{
|
||
background: surface.w50,
|
||
backdropFilter: "blur(6px)",
|
||
border: `1px solid ${line2}`,
|
||
borderRadius: 8,
|
||
padding: "14px 18px 13px",
|
||
flex: 1,
|
||
minHeight: 0,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
marginBottom: 11,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontSize: 11,
|
||
fontWeight: 600,
|
||
letterSpacing: 2,
|
||
color: ink,
|
||
}}
|
||
>
|
||
ИСТОЧНИКИ ДАННЫХ
|
||
</span>
|
||
<button
|
||
type="button"
|
||
className="rp-more"
|
||
onClick={() => onNavigate(2)}
|
||
aria-label="Подробнее — источники данных"
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 4,
|
||
cursor: "pointer",
|
||
fontSize: 9.5,
|
||
letterSpacing: 0.3,
|
||
color: accent,
|
||
background: "none",
|
||
border: "none",
|
||
padding: 0,
|
||
margin: 0,
|
||
fontFamily: "inherit",
|
||
}}
|
||
>
|
||
Подробнее <span>→</span>
|
||
</button>
|
||
</div>
|
||
{/* Fix #1 (audit) — the tile counts below are summed from the top-10
|
||
persisted sample, which can be less than builtOn's full n_analogs
|
||
total (footer, below) for large result sets. */}
|
||
{data.meta.sourcesNote && (
|
||
<div style={{ fontSize: 9.5, color: hint, marginBottom: 8 }}>
|
||
{data.meta.sourcesNote}
|
||
</div>
|
||
)}
|
||
<div
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns: "repeat(7,1fr)",
|
||
gap: 9,
|
||
flex: 1,
|
||
minHeight: 0,
|
||
alignItems: "stretch",
|
||
alignContent: "stretch",
|
||
}}
|
||
>
|
||
{data.sources.map((s, si) =>
|
||
s.active ? (
|
||
<div
|
||
key={si}
|
||
style={{
|
||
border: `1px solid ${line}`,
|
||
borderRadius: 6,
|
||
padding: "10px 9px",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
background: surface.w50,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: 9,
|
||
letterSpacing: 0.5,
|
||
color: ink,
|
||
fontWeight: 600,
|
||
...(s.name.includes(" (") ? { lineHeight: 1.3 } : {}),
|
||
}}
|
||
>
|
||
{sourceName(s.name)}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontFamily: font.mono,
|
||
fontSize: 26,
|
||
fontWeight: 300,
|
||
marginTop: "auto",
|
||
}}
|
||
>
|
||
{s.count}
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 5,
|
||
fontSize: 9,
|
||
color: muted,
|
||
marginTop: 3,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
width: 6,
|
||
height: 6,
|
||
borderRadius: "50%",
|
||
background: success,
|
||
}}
|
||
/>
|
||
{s.label}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div
|
||
key={si}
|
||
style={{
|
||
border: `1px solid ${disabledBorder}`,
|
||
borderRadius: 6,
|
||
padding: "10px 9px",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
background: surface.w25,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: 9,
|
||
letterSpacing: 0.5,
|
||
color: muted3,
|
||
fontWeight: 600,
|
||
}}
|
||
>
|
||
{s.name}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontFamily: font.mono,
|
||
fontSize: 22,
|
||
fontWeight: 300,
|
||
marginTop: "auto",
|
||
color: disabledValue,
|
||
}}
|
||
>
|
||
{s.count}
|
||
</div>
|
||
<div
|
||
style={{ fontSize: 9, color: disabledLabel, marginTop: 3 }}
|
||
>
|
||
{s.label}
|
||
</div>
|
||
</div>
|
||
),
|
||
)}
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
marginTop: 11,
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 10, color: hint }}>
|
||
При недоступности источника частичный результат не блокируется
|
||
</span>
|
||
<span style={{ fontSize: 9.5, letterSpacing: 1.5, color: muted2 }}>
|
||
{data.meta.builtOn ?? "—"}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|