fix(tradein/v2): mobile layout — remove overflow floor + honest stacked mobile block (#2275) #2389
2 changed files with 597 additions and 488 deletions
|
|
@ -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,7 +703,49 @@ export default function TradeInV2Page() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: 1536 * artboardScale, height: 1024 * artboardScale }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{/* #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 && (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 480,
|
||||
margin: "0 auto",
|
||||
padding: "14px 14px 20px",
|
||||
boxSizing: "border-box",
|
||||
fontFamily: tokens.font.sans,
|
||||
color: tokens.ink,
|
||||
}}
|
||||
>
|
||||
<HeroBar
|
||||
compact
|
||||
data={{ report, object: objectInfo }}
|
||||
estimateId={mounted ? currentEstimateId : null}
|
||||
onOpenInfo={() => setDrawerOpen(true)}
|
||||
hasEstimate={hasEstimate}
|
||||
/>
|
||||
<div style={{ marginTop: 14 }}>{rightContent}</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{ width: 1536 * artboardScale, height: 1024 * artboardScale }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
|
|
@ -805,12 +857,16 @@ export default function TradeInV2Page() {
|
|||
inert={nav !== 0 || drawerOpen ? true : undefined}
|
||||
style={{ display: "contents" }}
|
||||
>
|
||||
{/* #2275: on mobile the readable HeroBar lives in the fluid
|
||||
quick-view block above instead of a tiny scaled-down copy. */}
|
||||
{!isMobile && (
|
||||
<HeroBar
|
||||
data={{ report, object: objectInfo }}
|
||||
estimateId={mounted ? currentEstimateId : null}
|
||||
onOpenInfo={() => setDrawerOpen(true)}
|
||||
hasEstimate={hasEstimate}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -831,7 +887,9 @@ export default function TradeInV2Page() {
|
|||
markers={markers}
|
||||
/>
|
||||
{middleContent}
|
||||
{rightContent}
|
||||
{/* #2275: on mobile ObjectSummary is rendered fluid in the
|
||||
quick-view block above instead of here (tiny scaled copy). */}
|
||||
{!isMobile && rightContent}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
|
@ -861,5 +919,6 @@ export default function TradeInV2Page() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" aria-hidden="true">
|
||||
<svg
|
||||
width="18"
|
||||
height="20"
|
||||
viewBox="0 0 18 20"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2 1h9l5 5v13H2z" stroke={frame} strokeWidth="1.2" />
|
||||
<path d="M11 1v5h5" stroke={frame} strokeWidth="1.2" />
|
||||
<line x1="5" y1="11" x2="13" y2="11" stroke={rule} />
|
||||
|
|
@ -110,8 +123,9 @@ export default function HeroBar({
|
|||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 30,
|
||||
padding: "14px 2px 12px",
|
||||
flexDirection: compact ? "column" : "row",
|
||||
gap: compact ? 16 : 30,
|
||||
padding: compact ? "0" : "14px 2px 12px",
|
||||
flex: "0 0 auto",
|
||||
}}
|
||||
>
|
||||
|
|
@ -134,10 +148,18 @@ export default function HeroBar({
|
|||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
gap: 22,
|
||||
gap: compact ? 14 : 22,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: compact ? "wrap" : "nowrap",
|
||||
gap: compact ? 18 : 26,
|
||||
rowGap: compact ? 8 : undefined,
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 26, alignItems: "flex-start" }}>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -215,7 +237,15 @@ export default function HeroBar({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 12, width: "100%", maxWidth: 440 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: compact ? "column" : "row",
|
||||
gap: 12,
|
||||
width: "100%",
|
||||
maxWidth: compact ? "none" : 440,
|
||||
}}
|
||||
>
|
||||
{pdfHref ? (
|
||||
<a
|
||||
className={pdfFilled ? "hero-pdf-btn-filled" : "hero-pdf-btn"}
|
||||
|
|
@ -265,7 +295,12 @@ export default function HeroBar({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT: photo + overlays */}
|
||||
{/* 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 && (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
|
|
@ -372,7 +407,11 @@ export default function HeroBar({
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{ height: 1, background: tokens.lineSoft, margin: "8px 0" }}
|
||||
style={{
|
||||
height: 1,
|
||||
background: tokens.lineSoft,
|
||||
margin: "8px 0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -385,7 +424,11 @@ export default function HeroBar({
|
|||
{data.object.streetView}
|
||||
</div>
|
||||
<div
|
||||
style={{ height: 1, background: tokens.lineSoft, margin: "8px 0" }}
|
||||
style={{
|
||||
height: 1,
|
||||
background: tokens.lineSoft,
|
||||
margin: "8px 0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
// role=button + tabIndex + Enter/Space (#2264 C6): keyboard-operable
|
||||
|
|
@ -490,7 +533,13 @@ export default function HeroBar({
|
|||
color: tokens.accent,
|
||||
}}
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" aria-hidden="true">
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 22 22"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="11" cy="11" r="10" stroke="#2e8bff" strokeWidth="1" />
|
||||
<path d="M11 3 L13 11 L11 9 L9 11 Z" fill="#2e8bff" />
|
||||
</svg>
|
||||
|
|
@ -626,6 +675,7 @@ export default function HeroBar({
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue