From 293184ebc64754e147eebc11a75b0bf54e33191f Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 4 Jul 2026 07:02:56 +0000 Subject: [PATCH] =?UTF-8?q?fix(tradein/v2):=20mobile=20layout=20=E2=80=94?= =?UTF-8?q?=20remove=20overflow=20floor=20+=20honest=20stacked=20mobile=20?= =?UTF-8?q?block=20(#2275)=20(#2389)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tradein-mvp/frontend/src/app/v2/page.tsx | 367 +++++---- .../src/components/trade-in/v2/HeroBar.tsx | 718 ++++++++++-------- 2 files changed, 597 insertions(+), 488 deletions(-) diff --git a/tradein-mvp/frontend/src/app/v2/page.tsx b/tradein-mvp/frontend/src/app/v2/page.tsx index 4d5ca0df..c6c8659b 100644 --- a/tradein-mvp/frontend/src/app/v2/page.tsx +++ b/tradein-mvp/frontend/src/app/v2/page.tsx @@ -411,16 +411,25 @@ export default function TradeInV2Page() { useEffect(() => setMounted(true), []); // Scale-fit: shrink the fixed 1536×1024 artboard to fit smaller viewports - // instead of clipping. Never upscale (capped at 1); recomputed on resize. + // instead of clipping. Never upscale (capped at 1). #2275: this used to floor + // at 0.88, which on a 390px phone still rendered the artboard ~1350px wide — + // page-wide horizontal scroll + a hero clipped at the left edge + the summary + // rail pushed off-screen. Width is now the only constraint (a scaled artboard + // taller than the viewport just scrolls the page vertically, which is normal + // and not the bug being fixed here). const [artboardScale, setArtboardScale] = useState(1); + // #2275: below this viewport width the fixed-width artboard can no longer + // read at a usable size even scaled-to-fit, so HeroBar + ObjectSummary (the + // "at a glance" content — report meta, PDF CTA, per-section totals) get a real + // fluid mobile block above the artboard instead of a tiny scaled-down copy. + // The rest of the dashboard (ParamsPanel / ResultPanel / overlays) stays + // inside the scaled artboard — smaller on phones, but no longer overflowing. + const [isMobile, setIsMobile] = useState(false); useEffect(() => { - const compute = () => - setArtboardScale( - Math.max( - 0.88, - Math.min(1, window.innerWidth / 1536, window.innerHeight / 1024), - ), - ); + const compute = () => { + setArtboardScale(Math.min(1, window.innerWidth / 1536)); + setIsMobile(window.innerWidth < 768); + }; compute(); window.addEventListener("resize", compute); return () => window.removeEventListener("resize", compute); @@ -573,7 +582,8 @@ export default function TradeInV2Page() { const topNavUser = useMemo(() => { const scope = me.data; if (!scope) return undefined; - const nameParts = scope.display_name?.trim().split(/\s+/).filter(Boolean) ?? []; + const nameParts = + scope.display_name?.trim().split(/\s+/).filter(Boolean) ?? []; const initials = nameParts.length >= 2 ? (nameParts[0][0] + nameParts[1][0]).toUpperCase() @@ -693,172 +703,221 @@ export default function TradeInV2Page() { } return ( -
+
+ {/* #2275 mobile quick-view — a real fluid, native-size block for the "at a + glance" content (HeroBar's report meta + PDF CTA, ObjectSummary's + per-section totals). Inside the scaled artboard below, these would read + as tiny/illegible text on a phone rather than the honest mobile layout + the design deserves — so under the mobile breakpoint they render here + instead (and are skipped inside the artboard, see `!isMobile` below). + The rest of the dashboard (ParamsPanel / ResultPanel / overlays) has no + equivalent fluid mobile layout yet (follow-up) and stays inside the + scaled artboard: smaller on phones, but — with the 0.88 floor removed + above — never wider than the viewport. */} + {isMobile && ( +
+ setDrawerOpen(true)} + hasEstimate={hasEstimate} + /> +
{rightContent}
+
+ )}
- +
+ - {/* #2264 C5: the page's single visible "title" is the SVG logo in TopNav, + {/* #2264 C5: the page's single visible "title" is the SVG logo in TopNav, so give the document a real, visually-hidden

(page-has-heading-one + a top of the heading order for the overlay/panel

s). */} -

- Мера — оценка квартиры -

- - {/* Polite, visually-hidden status announcer for SR users. */} -
- {statusMessage} -
- - {/* OUTER HUD FRAME */} -
- {/* 4 corner brackets */} - {brackets.map((b) => ( -
+ Мера — оценка квартиры + + + {/* Polite, visually-hidden status announcer for SR users. */} +
+ {statusMessage} +
+ + {/* OUTER HUD FRAME */} +
- ))} + {/* 4 corner brackets */} + {brackets.map((b) => ( +
+ ))} - {/* CONTENT WRAP */} -
- {/* TopNav stays interactive while a section overlay is open so the + {/* CONTENT WRAP */} +
+ {/* TopNav stays interactive while a section overlay is open so the user can switch sections directly from the overlay. Only the LocationDrawer (a full modal over the whole HUD) inerts it. The dashboard body (
/
) is still inerted behind ANY overlay/drawer — preserving the focus + click guard from a11y audit #2081 for everything except the top tabs. */} - -
- setDrawerOpen(true)} - hasEstimate={hasEstimate} - /> - -
- - {middleContent} - {rightContent} -
-
+ +
+ {/* #2275: on mobile the readable HeroBar lives in the fluid + quick-view block above instead of a tiny scaled-down copy. */} + {!isMobile && ( + setDrawerOpen(true)} + hasEstimate={hasEstimate} + /> + )} -
-
-
-
+
+ + {middleContent} + {/* #2275: on mobile ObjectSummary is rendered fluid in the + quick-view block above instead of here (tiny scaled copy). */} + {!isMobile && rightContent} +
+ - {nav !== 0 && ( - setNav(0)} - onNavigate={setNav} - history={historyData} - analytics={analyticsViewData} - sources={sourcesData} - cache={cacheData} +
+
+
+
+ + {nav !== 0 && ( + setNav(0)} + onNavigate={setNav} + history={historyData} + analytics={analyticsViewData} + sources={sourcesData} + cache={cacheData} + /> + )} + setDrawerOpen(false)} + data={locationData} /> - )} - setDrawerOpen(false)} - data={locationData} - /> +
); diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/HeroBar.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/HeroBar.tsx index 51c13c44..afea7a59 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/HeroBar.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/v2/HeroBar.tsx @@ -45,6 +45,12 @@ interface HeroBarProps { // are hidden, mirroring how the PDF control already gates on estimate presence. hasEstimate: boolean; onOpenInfo: () => void; + // #2275 mobile quick-view: a real fluid layout instead of the fixed-width + // desktop one — meta/buttons stack, the decorative building photo (and the + // address/coef card baked into it) is dropped since it assumes a 560×152 box + // that cannot reflow. «КАК РАССЧИТАНО» still opens the same location-coef + // drawer, so no functionality is lost, only the redundant photo-card copy. + compact?: boolean; } const pdfBtnStyle: CSSProperties = { @@ -80,6 +86,7 @@ export default function HeroBar({ estimateId, hasEstimate, onOpenInfo, + compact = false, }: HeroBarProps) { const pdfHref = hasEstimate ? pdfDownloadHref(estimateId) : null; // A downloadable report exists ⇔ the estimate is ready ⇒ the PDF button is the @@ -93,7 +100,13 @@ export default function HeroBar({ const rule = filled ? "rgba(255,255,255,.75)" : "#6f8195"; return ( <> -
@@ -134,10 +148,18 @@ export default function HeroBar({ display: "flex", flexDirection: "column", justifyContent: "center", - gap: 22, + gap: compact ? 14 : 22, }} > -
+
-
+
{pdfHref ? (
- {/* RIGHT: photo + overlays */} -
- {!imgFailed && ( - setImgFailed(true)} - style={{ - position: "absolute", - inset: 0, - width: "100%", - height: "100%", - objectFit: "contain", - objectPosition: "right center", - filter: "saturate(.25) brightness(1.06) contrast(.95)", - }} - /> - )} - {/* blue duotone tint toward HUD accent — recedes the photo (only over - the real image; skip when it 404s so the placeholder stays clean) */} - {!imgFailed && ( -
- )} + {/* RIGHT: photo + overlays — dropped in compact mode (#2275): the box is + a fixed 560×152 with several absolutely-positioned children (address + card, compass, distance scale) pinned to that size, so it cannot + reflow to a phone width. «КАК РАССЧИТАНО» above still opens the same + location-coef drawer, so no functionality is lost. */} + {!compact && (
-
-
- - {/* address card */} -
-
- АДРЕС -
-
- {data.object.address} -
- - {data.object.city} - -
-
-
- {data.object.streetView} -
-
-
(which, with preflight - // off, would need a UA-chrome reset and risk the row's exact box - // geometry / negative-margin hover bleed). Opens the methodology drawer. - role="button" - tabIndex={0} - className="hero-coef-row" - onClick={onOpenInfo} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onOpenInfo(); - } - }} - aria-label="Пояснение к расчёту коэффициента локации" - style={{ - display: "flex", - alignItems: "center", - justifyContent: "space-between", - cursor: "pointer", - borderRadius: 4, - margin: "-3px -5px", - padding: "3px 5px", - transition: "background .15s", - }} - > - setImgFailed(true)} style={{ - fontSize: "8.5px", - letterSpacing: "1.5px", + position: "absolute", + inset: 0, + width: "100%", + height: "100%", + objectFit: "contain", + objectPosition: "right center", + filter: "saturate(.25) brightness(1.06) contrast(.95)", + }} + /> + )} + {/* blue duotone tint toward HUD accent — recedes the photo (only over + the real image; skip when it 404s so the placeholder stays clean) */} + {!imgFailed && ( +
+ )} +
+
+
+ + {/* address card */} +
+
- КОЭФ. ЛОКАЦИИ - - - {data.object.locationCoef === "—" ? ( - - {/* #2317: coef is a live feature now (GET /location-coef) — a + АДРЕС +
+
+ {data.object.address} +
+ + {data.object.city} + +
+
+
+ {data.object.streetView} +
+
+
(which, with preflight + // off, would need a UA-chrome reset and risk the row's exact box + // geometry / negative-margin hover bleed). Opens the methodology drawer. + role="button" + tabIndex={0} + className="hero-coef-row" + onClick={onOpenInfo} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onOpenInfo(); + } + }} + aria-label="Пояснение к расчёту коэффициента локации" + style={{ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + cursor: "pointer", + borderRadius: 4, + margin: "-3px -5px", + padding: "3px 5px", + transition: "background .15s", + }} + > + + КОЭФ. ЛОКАЦИИ + + + {data.object.locationCoef === "—" ? ( + + {/* #2317: coef is a live feature now (GET /location-coef) — a dash here means unavailable/loading for THIS estimate (unavailable geo_source, no lat/lon, or query pending), never "not built yet", so «скоро» would be stale/false. */} - нет данных - - ) : ( + нет данных + + ) : ( + + {data.object.locationCoef} + + )} - )} - +
+
+ + {/* compass */} +
+ + + {data.object.compass}
-
- {/* compass */} -
- - - {data.object.compass} - -
+
+ + + + + +
+
+ 0 + 25 + 50 + 75 + 100м +
+
- {/* distance scale */} -
+ {/* corner brackets */}
- - - - - -
+ />
- 0 - 25 - 50 - 75 - 100м -
+ /> +
+
- - {/* corner brackets */} -
-
-
-
-
+ )}
); }