Compare commits

..

No commits in common. "f60f2b24e6c0aa12865d23ba061b78838498de81" and "0641183ecd37b4728f98ebcd7fcdb5948e154ccf" have entirely different histories.

5 changed files with 26 additions and 205 deletions

View file

@ -617,19 +617,6 @@ export default function TradeInV2Page() {
rightContent = <SummaryPlaceholder />; rightContent = <SummaryPlaceholder />;
} }
// Polite SR announcement reflecting the estimate lifecycle. Same precedence as
// middleContent: loading → error → insufficient → ready → silent.
let statusMessage = "";
if (loading) {
statusMessage = "Идёт расчёт оценки…";
} else if (restoreError) {
statusMessage = "Ошибка загрузки отчёта";
} else if (estimate && insufficient) {
statusMessage = "Недостаточно данных для оценки";
} else if (estimate && !insufficient) {
statusMessage = "Оценка готова";
}
return ( return (
<div style={{ width: 1536 * artboardScale, height: 1024 * artboardScale }}> <div style={{ width: 1536 * artboardScale, height: 1024 * artboardScale }}>
<div <div
@ -647,25 +634,6 @@ export default function TradeInV2Page() {
> >
<style>{HELPER_STYLES}</style> <style>{HELPER_STYLES}</style>
{/* Polite, visually-hidden status announcer for SR users. */}
<div
aria-live="polite"
role="status"
style={{
position: "absolute",
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clip: "rect(0 0 0 0)",
whiteSpace: "nowrap",
border: 0,
}}
>
{statusMessage}
</div>
{/* OUTER HUD FRAME */} {/* OUTER HUD FRAME */}
<div <div
style={{ style={{
@ -693,7 +661,6 @@ export default function TradeInV2Page() {
{/* CONTENT WRAP */} {/* CONTENT WRAP */}
<div <div
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{ style={{
position: "absolute", position: "absolute",
inset: 14, inset: 14,

View file

@ -39,7 +39,7 @@ export function ObjectSummary({
}} }}
> >
<style>{` <style>{`
.os-row { background: none; border: none; transition: background .12s; } .os-row { transition: background .12s; }
.os-row:hover { background: rgba(46,139,255,.07); } .os-row:hover { background: rgba(46,139,255,.07); }
`}</style> `}</style>
@ -159,26 +159,21 @@ export function ObjectSummary({
{/* Per-section totals (clickable) */} {/* Per-section totals (clickable) */}
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}> <div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
{data.summary.rows.map((row, i) => ( {data.summary.rows.map((row, i) => (
<button <div
key={row.label} key={row.label}
type="button"
className="os-row" className="os-row"
onClick={() => onNavigate(row.nav)} onClick={() => onNavigate(row.nav)}
aria-label={`${row.label} — подробнее`}
style={{ style={{
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
padding: "9px 6px", padding: "9px 6px",
cursor: "pointer",
borderRadius: 5,
width: "100%",
fontFamily: "inherit",
textAlign: "left",
borderBottom: borderBottom:
i < data.summary.rows.length - 1 i < data.summary.rows.length - 1
? `1px dotted ${tokens.lineGradient}` ? `1px dotted ${tokens.lineGradient}`
: undefined, : undefined,
cursor: "pointer",
borderRadius: 5,
}} }}
> >
<span <span
@ -216,7 +211,7 @@ export function ObjectSummary({
</span> </span>
<span style={{ color: tokens.accent, fontSize: 11 }}></span> <span style={{ color: tokens.accent, fontSize: 11 }}></span>
</span> </span>
</button> </div>
))} ))}
</div> </div>

View file

@ -273,11 +273,9 @@ export default function ResultPanel({
/> />
))} ))}
</div> </div>
<button <div
type="button"
className="rp-more" className="rp-more"
onClick={() => onNavigate(card.nav)} onClick={() => onNavigate(card.nav)}
aria-label={`Подробнее — ${card.title.join(" ")}`}
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@ -287,24 +285,18 @@ export default function ResultPanel({
letterSpacing: 0.3, letterSpacing: 0.3,
color: accent, color: accent,
whiteSpace: "nowrap", whiteSpace: "nowrap",
background: "none",
border: "none",
padding: 0,
margin: 0,
fontFamily: "inherit",
}} }}
> >
Подробнее <span></span> Подробнее <span></span>
</button> </div>
</div> </div>
) : ( ) : (
<> <>
<button <div
type="button"
className="rp-more" className="rp-more"
onClick={() => onNavigate(card.nav)} onClick={() => onNavigate(card.nav)}
aria-label={`Подробнее — ${card.title.join(" ")}`}
style={{ style={{
marginTop: 16,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "flex-end", justifyContent: "flex-end",
@ -313,17 +305,10 @@ export default function ResultPanel({
fontSize: 9, fontSize: 9,
letterSpacing: 0.3, letterSpacing: 0.3,
color: accent, color: accent,
width: "100%",
background: "none",
border: "none",
padding: 0,
margin: 0,
marginTop: 16,
fontFamily: "inherit",
}} }}
> >
Подробнее <span></span> Подробнее <span></span>
</button> </div>
<div <div
style={{ style={{
position: "absolute", position: "absolute",
@ -650,11 +635,9 @@ export default function ResultPanel({
объект объект
</span> </span>
</div> </div>
<button <div
type="button"
className="rp-more" className="rp-more"
onClick={() => onNavigate(3)} onClick={() => onNavigate(3)}
aria-label="Подробнее — цена и срок продажи"
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@ -663,16 +646,11 @@ export default function ResultPanel({
fontSize: 9, fontSize: 9,
letterSpacing: 0.3, letterSpacing: 0.3,
color: accent, color: accent,
background: "none",
border: "none",
padding: 0,
margin: 0,
marginTop: 2, marginTop: 2,
fontFamily: "inherit",
}} }}
> >
Подробнее <span></span> Подробнее <span></span>
</button> </div>
</div> </div>
{/* right range — deals */} {/* right range — deals */}
@ -778,11 +756,9 @@ export default function ResultPanel({
> >
ИСТОЧНИКИ ДАННЫХ ИСТОЧНИКИ ДАННЫХ
</span> </span>
<button <span
type="button"
className="rp-more" className="rp-more"
onClick={() => onNavigate(2)} onClick={() => onNavigate(2)}
aria-label="Подробнее — источники данных"
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@ -791,15 +767,10 @@ export default function ResultPanel({
fontSize: 9.5, fontSize: 9.5,
letterSpacing: 0.3, letterSpacing: 0.3,
color: accent, color: accent,
background: "none",
border: "none",
padding: 0,
margin: 0,
fontFamily: "inherit",
}} }}
> >
Подробнее <span></span> Подробнее <span></span>
</button> </span>
</div> </div>
<div <div
style={{ style={{

View file

@ -6,8 +6,6 @@
// with HUD corner brackets, a header (section number + title + "← К ОЦЕНКЕ" // with HUD corner brackets, a header (section number + title + "← К ОЦЕНКЕ"
// button) and a scrollable body that swaps the active view. // button) and a scrollable body that swaps the active view.
import { useEffect, useRef } from "react";
import { tokens } from "./tokens"; import { tokens } from "./tokens";
import { overlayNums, overlayTitles } from "./fixtures"; import { overlayNums, overlayTitles } from "./fixtures";
import HistoryView from "./HistoryView"; import HistoryView from "./HistoryView";
@ -39,78 +37,8 @@ export default function SectionOverlay({
analytics, analytics,
cache, cache,
}: SectionOverlayProps) { }: SectionOverlayProps) {
// Modal dialog plumbing. `dialogRef` is the dialog root, `lastFocused` keeps
// the element that had focus before the overlay opened so we can restore it on
// close, and `onCloseRef` holds the latest onClose so the mount-only effect
// (deps []) never re-runs / re-traps focus on parent re-renders even though
// page.tsx passes a fresh arrow each render.
const dialogRef = useRef<HTMLDivElement>(null);
const lastFocused = useRef<Element | null>(null);
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
useEffect(() => {
const dialog = dialogRef.current;
// Remember the trigger, then move focus into the dialog.
lastFocused.current = document.activeElement;
dialog?.focus();
function getFocusable(): HTMLElement[] {
if (!dialog) return [];
const nodes = dialog.querySelectorAll<HTMLElement>(
'button, [href], input, select, [tabindex]:not([tabindex="-1"])',
);
return Array.from(nodes).filter((el) => !el.hasAttribute("disabled"));
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
e.preventDefault();
onCloseRef.current();
return;
}
if (e.key !== "Tab" || !dialog) return;
const focusable = getFocusable();
if (focusable.length === 0) {
// Nothing tabbable inside — keep focus pinned on the dialog itself.
e.preventDefault();
dialog.focus();
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
const activeEl = document.activeElement;
const inside = dialog.contains(activeEl);
if (e.shiftKey) {
if (activeEl === first || activeEl === dialog || !inside) {
e.preventDefault();
last.focus();
}
} else if (activeEl === last || !inside) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
// Return focus to whatever opened the overlay.
(lastFocused.current as HTMLElement | null)?.focus?.();
};
// Mount-only: refs (dialogRef/onCloseRef) are stable, so no extra deps.
}, []);
return ( return (
<div <div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="v2-overlay-title"
tabIndex={-1}
style={{ style={{
position: "absolute", position: "absolute",
left: 28, left: 28,
@ -131,7 +59,7 @@ export default function SectionOverlay({
}} }}
> >
<style>{` <style>{`
.tiv2-so-back { color: ${tokens.ink}; transition: all .15s; } .tiv2-so-back { transition: all .15s; }
.tiv2-so-back:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; } .tiv2-so-back:hover { border-color: ${tokens.accent}; color: ${tokens.accent}; }
`}</style> `}</style>
@ -183,7 +111,6 @@ export default function SectionOverlay({
{overlayNums[active]} {overlayNums[active]}
</span> </span>
<span <span
id="v2-overlay-title"
style={{ style={{
fontSize: 14, fontSize: 14,
fontWeight: 600, fontWeight: 600,
@ -194,8 +121,7 @@ export default function SectionOverlay({
{overlayTitles[active]} {overlayTitles[active]}
</span> </span>
</div> </div>
<button <div
type="button"
onClick={onClose} onClick={onClose}
className="tiv2-so-back" className="tiv2-so-back"
style={{ style={{
@ -207,9 +133,6 @@ export default function SectionOverlay({
borderRadius: 6, borderRadius: 6,
padding: "6px 12px", padding: "6px 12px",
background: tokens.surface.w60, background: tokens.surface.w60,
// reset UA button defaults while keeping the original box look;
// base colour lives in .tiv2-so-back so :hover can win
fontFamily: "inherit",
}} }}
> >
<span style={{ fontSize: 13 }}></span> <span style={{ fontSize: 13 }}></span>
@ -218,7 +141,7 @@ export default function SectionOverlay({
> >
К ОЦЕНКЕ К ОЦЕНКЕ
</span> </span>
</button> </div>
</div> </div>
{/* overlay body */} {/* overlay body */}

View file

@ -80,10 +80,8 @@ export default function TopNav({
.tnav-tab { color: ${tokens.muted}; font-weight: 400; cursor: pointer; transition: color .2s; } .tnav-tab { color: ${tokens.muted}; font-weight: 400; cursor: pointer; transition: color .2s; }
.tnav-tab:hover { color: ${tokens.ink}; } .tnav-tab:hover { color: ${tokens.ink}; }
.tnav-tab--active { color: ${tokens.ink}; font-weight: 600; } .tnav-tab--active { color: ${tokens.ink}; font-weight: 600; }
.tnav-userbtn { background: none; border: none; }
.tnav-userbtn:hover { background: rgba(46,139,255,.07); } .tnav-userbtn:hover { background: rgba(46,139,255,.07); }
.tnav-menuitem:hover { background: rgba(46,139,255,.09); } .tnav-menuitem:hover { background: rgba(46,139,255,.09); }
.tnav-logout { background: none; border: none; }
.tnav-logout:hover { background: rgba(205,104,104,.1); } .tnav-logout:hover { background: rgba(205,104,104,.1); }
`}</style> `}</style>
@ -186,27 +184,11 @@ export default function TopNav({
}} }}
> >
{navLabels.map((label, i) => ( {navLabels.map((label, i) => (
<button <div
type="button"
key={label} key={label}
onClick={() => onNavigate(i)} onClick={() => onNavigate(i)}
aria-current={active === i ? "page" : undefined}
className={`tnav-tab${active === i ? " tnav-tab--active" : ""}`} className={`tnav-tab${active === i ? " tnav-tab--active" : ""}`}
style={{ style={{ position: "relative", whiteSpace: "nowrap" }}
position: "relative",
whiteSpace: "nowrap",
background: "none",
border: "none",
padding: 0,
margin: 0,
// NB: family/size/letterSpacing inherit for chrome reset, but do
// NOT set `font`/`color` shorthand — the .tnav-tab(--active)/:hover
// CSS must keep driving tab colour + weight.
fontFamily: "inherit",
fontSize: "inherit",
letterSpacing: "inherit",
cursor: "pointer",
}}
> >
{label} {label}
<div <div
@ -238,23 +220,15 @@ export default function TopNav({
}} }}
/> />
)} )}
</button> </div>
))} ))}
</div> </div>
{/* User menu */} {/* User menu */}
<div <div style={{ position: "relative" }}>
style={{ position: "relative" }} <div
onKeyDown={(e) => {
if (e.key === "Escape") setUserOpen(false);
}}
>
<button
type="button"
onClick={() => setUserOpen((o) => !o)} onClick={() => setUserOpen((o) => !o)}
className="tnav-userbtn" className="tnav-userbtn"
aria-haspopup="true"
aria-expanded={userOpen}
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@ -263,8 +237,6 @@ export default function TopNav({
padding: "5px 8px 5px 6px", padding: "5px 8px 5px 6px",
borderRadius: "9px", borderRadius: "9px",
transition: "background .15s", transition: "background .15s",
fontFamily: "inherit",
textAlign: "left",
}} }}
> >
<div <div
@ -309,7 +281,7 @@ export default function TopNav({
</div> </div>
</div> </div>
<span style={{ color: tokens.muted2, fontSize: "9px" }}></span> <span style={{ color: tokens.muted2, fontSize: "9px" }}></span>
</button> </div>
{userOpen && ( {userOpen && (
<div <div
@ -453,20 +425,13 @@ export default function TopNav({
}} }}
/> />
<button <div
type="button"
onClick={() => { onClick={() => {
setUserOpen(false); setUserOpen(false);
onLogout?.(); onLogout?.();
}} }}
className="tnav-logout" className="tnav-logout"
style={{ style={{ ...menuItemStyle, color: tokens.danger }}
...menuItemStyle,
color: tokens.danger,
width: "100%",
fontFamily: "inherit",
textAlign: "left",
}}
> >
<svg width="15" height="15" viewBox="0 0 15 15" fill="none"> <svg width="15" height="15" viewBox="0 0 15 15" fill="none">
<path <path
@ -476,7 +441,7 @@ export default function TopNav({
/> />
</svg> </svg>
Выйти Выйти
</button> </div>
</div> </div>
)} )}
</div> </div>