fix(trade-in/ui): расшифровка ДКП/IMV/CV + графики на --viz токены + убрать emoji (#1871) #1922

Merged
lekss361 merged 1 commit from fix/tradein-redesign-pack into main 2026-06-26 16:19:23 +00:00
8 changed files with 51 additions and 27 deletions

View file

@ -191,7 +191,19 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
lineHeight: 1.4, lineHeight: 1.4,
}} }}
> >
Адрес распознан неточно оценка может быть приближённой. <span
aria-hidden="true"
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
background: "var(--warn)",
marginRight: 8,
verticalAlign: "middle",
}}
/>
Адрес распознан неточно оценка может быть приближённой.
Уточните адрес и пересчитайте для более точного результата. Уточните адрес и пересчитайте для более точного результата.
</div> </div>
)} )}
@ -208,7 +220,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
</b> </b>
</div> </div>
<div style={{ marginTop: 4 }}> <div style={{ marginTop: 4 }}>
Достоверность · <b style={{ color: conf.color }}>{conf.txt}</b> · CV {cv.toFixed(1)}% Достоверность · <b style={{ color: conf.color }}>{conf.txt}</b> · <abbr title="Коэффициент вариации (разброс цен аналогов)">CV</abbr> {cv.toFixed(1)}%
{precisionBadge && ( {precisionBadge && (
<span className={precisionBadge.className} title="Точность определения адреса"> <span className={precisionBadge.className} title="Точность определения адреса">
{precisionBadge.text} {precisionBadge.text}
@ -376,7 +388,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
<div className="hero-benchmarks"> <div className="hero-benchmarks">
{imvPrice !== null && ( {imvPrice !== null && (
<div className="bench-chip"> <div className="bench-chip">
<span className="bench-chip__src">Avito IMV</span> <span className="bench-chip__src">Avito <abbr title="Information Market Value — оценка рыночной стоимости от Avito">IMV</abbr></span>
<span className="bench-chip__val mono">{formatMln(imvPrice)} </span> <span className="bench-chip__val mono">{formatMln(imvPrice)} </span>
</div> </div>
)} )}
@ -395,7 +407,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
)} )}
{showDkp && dkp && ( {showDkp && dkp && (
<div className="bench-chip"> <div className="bench-chip">
<span className="bench-chip__src">ДКП · Росреестр</span> <span className="bench-chip__src"><abbr title="Договор купли-продажи (реальные сделки Росреестра)">ДКП</abbr> · Росреестр</span>
<span className="bench-chip__val mono"> <span className="bench-chip__val mono">
{formatMln(dkp.median_ppm2 * dkpArea)} {formatMln(dkp.median_ppm2 * dkpArea)}
</span> </span>
@ -438,7 +450,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
title="Индекс Market Value Avito по этому дому — реальный рыночный якорь" title="Индекс Market Value Avito по этому дому — реальный рыночный якорь"
style={{ marginTop: 6, fontSize: 12, color: "var(--muted)" }} style={{ marginTop: 6, fontSize: 12, color: "var(--muted)" }}
> >
Avito IMV: {formatMln(imvPrice)} Avito <abbr title="Information Market Value — оценка рыночной стоимости от Avito">IMV</abbr>: {formatMln(imvPrice)}
</div> </div>
)} )}
</div> </div>
@ -446,7 +458,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
{showDkp && dkp && ( {showDkp && dkp && (
<div className="price-figure" data-kind="dkp"> <div className="price-figure" data-kind="dkp">
<div className="price-figure-label"> <div className="price-figure-label">
Цены по фактическим сделкам (ДКП · Росреестр) Цены по фактическим сделкам (<abbr title="Договор купли-продажи (реальные сделки Росреестра)">ДКП</abbr> · Росреестр)
</div> </div>
<div className="price-figure-value mono"> <div className="price-figure-value mono">
{formatMln(dkp.median_ppm2 * dkpArea)} {formatMln(dkp.median_ppm2 * dkpArea)}
@ -455,7 +467,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
{formatMln(dkp.low_ppm2 * dkpArea)} {formatMln(dkp.high_ppm2 * dkpArea)} {formatMln(dkp.low_ppm2 * dkpArea)} {formatMln(dkp.high_ppm2 * dkpArea)}
</div> </div>
<div className="price-figure-perm2 mono"> <div className="price-figure-perm2 mono">
{dkp.median_ppm2.toLocaleString("ru-RU")} /м² · {dkp.count} ДКП за{" "} {dkp.median_ppm2.toLocaleString("ru-RU")} /м² · {dkp.count} <abbr title="Договор купли-продажи (реальные сделки Росреестра)">ДКП</abbr> за{" "}
{dkp.period_months} мес {dkp.period_months} мес
</div> </div>
</div> </div>
@ -641,7 +653,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
</span> </span>
<div> <div>
<b>Цены в объявлениях это ожидания собственников.</b> Реальные сделки <b>Цены в объявлениях это ожидания собственников.</b> Реальные сделки
проходят на <b>512% ниже</b> по данным ДКП Росреестра. проходят на <b>512% ниже</b> по данным <abbr title="Договор купли-продажи (реальные сделки Росреестра)">ДКП</abbr> Росреестра.
</div> </div>
</div> </div>
<div className="warn-row"> <div className="warn-row">

View file

@ -253,9 +253,20 @@ export function HeroTransparency({ estimate, brandSlug, brandName }: Props) {
{showPrecisionWarning && ( {showPrecisionWarning && (
<span <span
aria-label="Предупреждение: адрес определён приблизительно при низкой достоверности" aria-label="Предупреждение: адрес определён приблизительно при низкой достоверности"
style={{ marginLeft: 6 }} style={{ marginLeft: 6, display: "inline-flex", alignItems: "center", gap: 4 }}
> >
<span
aria-hidden="true"
style={{
display: "inline-block",
width: 7,
height: 7,
borderRadius: "50%",
background: "var(--warn)",
verticalAlign: "middle",
}}
/>
<span style={{ fontSize: 11, color: "var(--warn)", fontWeight: 600 }}>Приблизительно</span>
</span> </span>
)} )}
</span> </span>

View file

@ -48,7 +48,7 @@ export function IMVBenchmark({ benchmark, isLoading }: Props) {
return ( return (
<section className="imv-benchmark" aria-label="Avito IMV benchmark"> <section className="imv-benchmark" aria-label="Avito IMV benchmark">
<header className="imv-benchmark__head"> <header className="imv-benchmark__head">
<span className="imv-benchmark__title">Avito IMV</span> <span className="imv-benchmark__title">Avito <abbr title="Information Market Value — оценка рыночной стоимости от Avito">IMV</abbr></span>
<a <a
href="https://www.avito.ru/evaluation/realty" href="https://www.avito.ru/evaluation/realty"
target="_blank" target="_blank"

View file

@ -16,10 +16,11 @@ const LEAFLET_JS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.js`;
const LEAFLET_CSS_SRI = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="; const LEAFLET_CSS_SRI = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
const LEAFLET_JS_SRI = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="; const LEAFLET_JS_SRI = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
// Цвета пинов: asking (синий) / ДКП (зелёный) / target (оранжевый акцент). // Цвета пинов: asking (синий --viz-1) / ДКП (бирюза --viz-3) / target (янтарь --viz-4).
const COLOR_ANALOG = "#2563eb"; // Leaflet принимает строку-цвет напрямую (не CSS vars) — используем hex-значения токенов.
const COLOR_DEAL = "#16a34a"; const COLOR_ANALOG = "#1d4ed8"; // --viz-1
const COLOR_TARGET = "#ea580c"; const COLOR_DEAL = "#14b8a6"; // --viz-3
const COLOR_TARGET = "#f59e0b"; // --viz-4
/** Подгружает Leaflet с CDN один раз, резолвит window.L. (mirror MapPicker) */ /** Подгружает Leaflet с CDN один раз, резолвит window.L. (mirror MapPicker) */
function loadLeaflet(): Promise<any> { function loadLeaflet(): Promise<any> {

View file

@ -130,9 +130,9 @@ export function MapPicker({ onPick, onClose }: Props) {
else else
marker = L.circleMarker([lat, lon], { marker = L.circleMarker([lat, lon], {
radius: 8, radius: 8,
color: "#1d4ed8", color: "#1d4ed8", // --viz-1 hex (Leaflet не принимает CSS vars)
weight: 2, weight: 2,
fillColor: "#1d4ed8", fillColor: "#1d4ed8", // --viz-1 hex
fillOpacity: 0.45, fillOpacity: 0.45,
}).addTo(map); }).addTo(map);
setAddress(null); setAddress(null);

View file

@ -79,7 +79,7 @@ export function PriceHistoryChart({ points }: Props) {
<Line <Line
type="monotone" type="monotone"
dataKey="avito_imv" dataKey="avito_imv"
stroke="#2563eb" stroke="var(--viz-1)"
strokeWidth={2} strokeWidth={2}
dot={{ r: 3 }} dot={{ r: 3 }}
name="Avito" name="Avito"
@ -88,7 +88,7 @@ export function PriceHistoryChart({ points }: Props) {
<Line <Line
type="monotone" type="monotone"
dataKey="yandex_valuation" dataKey="yandex_valuation"
stroke="#ea580c" stroke="var(--viz-4)"
strokeWidth={2} strokeWidth={2}
dot={{ r: 3 }} dot={{ r: 3 }}
name="Яндекс" name="Яндекс"

View file

@ -30,7 +30,7 @@ export function PriceRangeBar({ rangeLow, rangeHigh, median }: Props) {
position: "relative", position: "relative",
height: 10, height: 10,
borderRadius: 999, borderRadius: 999,
background: "linear-gradient(to right, #bfdbfe, #1d4ed8)", background: "linear-gradient(to right, color-mix(in srgb, var(--viz-1) 30%, white), var(--viz-1))",
}} }}
> >
{/* Median marker */} {/* Median marker */}
@ -44,7 +44,7 @@ export function PriceRangeBar({ rangeLow, rangeHigh, median }: Props) {
width: 14, width: 14,
height: 14, height: 14,
borderRadius: "50%", borderRadius: "50%",
background: "#1d4ed8", background: "var(--viz-1)",
border: "2px solid #fff", border: "2px solid #fff",
boxShadow: "0 1px 4px rgba(0,0,0,0.25)", boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
}} }}
@ -65,7 +65,7 @@ export function PriceRangeBar({ rangeLow, rangeHigh, median }: Props) {
<span <span
style={{ style={{
fontWeight: 700, fontWeight: 700,
color: "#1d4ed8", color: "var(--viz-1)",
fontSize: 13, fontSize: 13,
}} }}
> >

View file

@ -12,10 +12,10 @@ const BUCKET_LABELS: Record<string, string> = {
}; };
const BUCKET_COLORS: Record<string, string> = { const BUCKET_COLORS: Record<string, string> = {
cheap: "#dcfce7", // light green cheap: "var(--success-soft)", // light green
median: "#dbeafe", // light blue median: "var(--accent-soft)", // light blue
plus5: "#fef3c7", // light yellow plus5: "var(--warn-soft)", // light yellow
plus10: "#fee2e2", // light red plus10: "var(--danger-soft)", // light red
}; };
function BucketCard({ bucket }: { bucket: SellTimeBucket }) { function BucketCard({ bucket }: { bucket: SellTimeBucket }) {
@ -30,7 +30,7 @@ function BucketCard({ bucket }: { bucket: SellTimeBucket }) {
background: BUCKET_COLORS[bucket.price_premium_label] ?? "#f9fafb", background: BUCKET_COLORS[bucket.price_premium_label] ?? "#f9fafb",
border: border:
bucket.price_premium_label === "median" bucket.price_premium_label === "median"
? "2px solid #2563eb" ? "2px solid var(--viz-1)"
: "1px solid var(--border, #e5e7eb)", : "1px solid var(--border, #e5e7eb)",
}} }}
> >