Compare commits

..

No commits in common. "86a22dfc8f6a3d409b799cdff15e5a9309f3bef4" and "3be80620623052e85ae3744e1e08067acd59440f" have entirely different histories.

6 changed files with 47 additions and 114 deletions

View file

@ -390,18 +390,10 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
</div> </div>
)} )}
{/* §1.1 «Одна цифра + честный разброс»: expected_sold цифра-герой {/* Две равноправные hero-цены: «Выставить» (asking) + «Получить» (sold).
(«оценка» / ожидаемая цена продажи), median вторичный ориентир При hasSold=false (старые оценки / пустая ratio) одиночный layout
«рекомендованная цена в объявлении», НЕ конкурирует за внимание. только с «Выставить». Empty-state при insufficientData. */}
При hasSold=false (старые оценки / пустая ratio) median остаётся <div className={`hero-duo${hasSold && !insufficientData ? "" : " hero-duo--single"}`}>
единственной честной цифрой. Empty-state при insufficientData. */}
<div
className={
!insufficientData && hasSold
? "hero-duo hero-duo--sold-primary"
: "hero-duo hero-duo--single"
}
>
{insufficientData ? ( {insufficientData ? (
<div className="hero-duo__cell"> <div className="hero-duo__cell">
<div className="hero-duo__label">Оценка недоступна</div> <div className="hero-duo__label">Оценка недоступна</div>
@ -412,50 +404,11 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
снова. снова.
</p> </p>
</div> </div>
) : hasSold ? ( ) : (
<> <>
{/* Цифра-герой: «оценка» = ожидаемая цена продажи (expected_sold) */} {/* Левая ячейка: «Выставить в объявлении» */}
<div className="hero-duo__cell" data-role="sold">
<div className="hero-duo__label">
Оценка · ожидаемая цена продажи
{showDiscount && (
<span
className="hero-duo__delta"
title="Насколько ожидаемая цена сделки ниже цены в объявлении. Это разрыв «объявление → реальная сделка», а не скидка сервиса."
aria-label={`ожидаемая цена продажи на ${discountPct}% ниже цены в объявлении`}
>
{discountPct}% к объявлению
</span>
)}
</div>
<div className="hero-duo__value mono">{formatMln(sold as number)} </div>
{typeof soldLo === "number" && typeof soldHi === "number" && (
<div className="hero-duo__range mono">
рыночный разброс: {formatMln(soldLo)} {formatMln(soldHi)}
</div>
)}
{typeof soldPerM2 === "number" && (
<div className="hero-duo__perm2 mono">
{soldPerM2.toLocaleString("ru-RU")} /м²
</div>
)}
<p className="hero-duo__hint">
Диапазон отражает разброс цен по рынку, а не погрешность оценки.
</p>
{estimate.ratio_basis === "global_fallback" ? (
<div className="hero-duo__note">
Ср.-городской коэффициент (мало сделок в сегменте)
</div>
) : estimate.ratio_basis === "per_rooms" ? (
<div className="hero-duo__note">
Коэффициент по сделкам той же планировки
</div>
) : null}
</div>
{/* Вторичный ориентир: рекомендованная цена в объявлении (median) */}
<div className="hero-duo__cell" data-role="asking"> <div className="hero-duo__cell" data-role="asking">
<div className="hero-duo__label">Рекомендованная цена в объявлении</div> <div className="hero-duo__label">Выставить в объявлении</div>
<div className="hero-duo__value mono">{formatMln(m)} </div> <div className="hero-duo__value mono">{formatMln(m)} </div>
<div className="hero-duo__range mono"> <div className="hero-duo__range mono">
{formatMln(lo)} {formatMln(hi)} {formatMln(lo)} {formatMln(hi)}
@ -464,18 +417,44 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
{estimate.median_price_per_m2.toLocaleString("ru-RU")} /м² {estimate.median_price_per_m2.toLocaleString("ru-RU")} /м²
</div> </div>
</div> </div>
{/* Правая ячейка: «Получить (ожидаемая сделка)» — только при hasSold */}
{hasSold && (
<div className="hero-duo__cell" data-role="sold">
<div className="hero-duo__label">
Получить (ожидаемая сделка)
{showDiscount && (
<span
className="hero-duo__delta"
aria-label={`на ${discountPct}% ниже цены в объявлении`}
>
{discountPct}%
</span>
)}
</div>
<div className="hero-duo__value mono">{formatMln(sold as number)} </div>
{typeof soldLo === "number" && typeof soldHi === "number" && (
<div className="hero-duo__range mono">
{formatMln(soldLo)} {formatMln(soldHi)}
</div>
)}
{typeof soldPerM2 === "number" && (
<div className="hero-duo__perm2 mono">
{soldPerM2.toLocaleString("ru-RU")} /м²
</div>
)}
{estimate.ratio_basis === "global_fallback" ? (
<div className="hero-duo__note">
Ср.-городской коэффициент (мало сделок в сегменте)
</div>
) : estimate.ratio_basis === "per_rooms" ? (
<div className="hero-duo__note">
Коэффициент по сделкам той же планировки
</div>
) : null}
</div>
)}
</> </>
) : (
<div className="hero-duo__cell" data-role="asking">
<div className="hero-duo__label">Рекомендованная цена в объявлении</div>
<div className="hero-duo__value mono">{formatMln(m)} </div>
<div className="hero-duo__range mono">
{formatMln(lo)} {formatMln(hi)}
</div>
<div className="hero-duo__perm2 mono">
{estimate.median_price_per_m2.toLocaleString("ru-RU")} /м²
</div>
</div>
)} )}
</div> </div>

View file

@ -58,7 +58,7 @@ export function OfferCard({ estimate, brandSlug }: Props) {
</div> </div>
<div className="card-meta"> <div className="card-meta">
<div> <div>
Цена в объявлении:{" "} Оценка по рынку:{" "}
<b className="mono"> <b className="mono">
{estimate.insufficient_data ? "—" : `${formatMln(estimate.median_price_rub)}`} {estimate.insufficient_data ? "—" : `${formatMln(estimate.median_price_rub)}`}
</b> </b>

View file

@ -756,35 +756,6 @@
font-style: italic; font-style: italic;
} }
/* §1.1 «Одна цифра-герой»: expected_sold доминирует, median вторичный
ориентир (не конкурирует за внимание). Применяется только когда есть
expected_sold (класс .hero-duo--sold-primary). */
.hero-duo--sold-primary {
grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
align-items: stretch;
}
/* Цифра-герой (expected_sold) — крупнее, чем вторичный median. */
.hero-duo--sold-primary .hero-duo__cell[data-role=sold] .hero-duo__value {
font-size: 34px;
}
/* Вторичный median-ориентир — приглушённый фон + меньший кегль числа. */
.hero-duo--sold-primary .hero-duo__cell[data-role=asking] {
background: var(--surface-1, var(--bg));
}
.hero-duo--sold-primary .hero-duo__cell[data-role=asking] .hero-duo__value {
font-size: 20px;
font-weight: 600;
color: var(--fg-2);
}
/* Честное пояснение к разбросу — «не погрешность оценки». */
.hero-duo__hint {
margin: 6px 0 0;
font-size: 11px;
line-height: 1.45;
color: var(--muted);
max-width: 44ch;
}
/* ── Компактный ряд бенчмарков под hero-duo ── */ /* ── Компактный ряд бенчмарков под hero-duo ── */
.hero-benchmarks { .hero-benchmarks {
display: flex; display: flex;
@ -1434,7 +1405,6 @@
.form-card { position: static; max-height: none; } .form-card { position: static; max-height: none; }
.hero-top { grid-template-columns: 1fr; } .hero-top { grid-template-columns: 1fr; }
.hero-duo { grid-template-columns: 1fr; } .hero-duo { grid-template-columns: 1fr; }
.hero-duo--sold-primary { grid-template-columns: 1fr; }
.hero-prices { grid-template-columns: 1fr; } .hero-prices { grid-template-columns: 1fr; }
.hero-photo { aspect-ratio: 16/9; } .hero-photo { aspect-ratio: 16/9; }
.progress-list { grid-template-columns: 1fr; } .progress-list { grid-template-columns: 1fr; }

View file

@ -432,20 +432,6 @@ export default function ResultPanel({
})} })}
</div> </div>
{/* §1.2 честный разброс: диапазоны цен = рыночный спред, НЕ погрешность
оценки. Ставим сразу под ценовыми карточками, чтобы широкий диапазон
не читался как «неуверенность расчёта». */}
<div
style={{
fontSize: 10,
lineHeight: 1.5,
color: hint,
marginTop: -4,
}}
>
Диапазоны показывают разброс цен на рынке, а не погрешность оценки.
</div>
{/* ranges + radar */} {/* ranges + radar */}
<div <div
style={{ style={{

View file

@ -68,7 +68,7 @@ export const resultCards: ResultCard[] = [
range: "6,83 — 12,81 млн ₽", range: "6,83 — 12,81 млн ₽",
ppm: "199 973 ₽/м²", ppm: "199 973 ₽/м²",
delta: "11%", delta: "11%",
deltaLabel: "К ОБЪЯВЛ.", deltaLabel: "К РЫНКУ",
nav: 2, nav: 2,
}, },
{ {

View file

@ -855,9 +855,7 @@ export function mapResultPanel(
e.asking_to_sold_ratio != null e.asking_to_sold_ratio != null
? fmtPct((e.asking_to_sold_ratio - 1) * 100) ? fmtPct((e.asking_to_sold_ratio - 1) * 100)
: "—", : "—",
// §1.3: разрыв «цена в объявлении → ожидаемая сделка», НЕ скидка сервиса. deltaLabel: "К РЫНКУ",
// «К ОБЪЯВЛ.» однозначнее прежнего «К РЫНКУ» (не читается как «скидка от нас»).
deltaLabel: "К ОБЪЯВЛ.",
note: note:
dealTier != null && dealTier != null &&
e.expected_sold_price_rub != null && e.expected_sold_price_rub != null &&