fix(tradein/v2): mobile layout — remove overflow floor + honest stacked mobile block (#2275) (#2389)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m20s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 47s
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m20s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 47s
This commit is contained in:
parent
4f0cae5c18
commit
293184ebc6
2 changed files with 597 additions and 488 deletions
|
|
@ -411,16 +411,25 @@ export default function TradeInV2Page() {
|
||||||
useEffect(() => setMounted(true), []);
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
// Scale-fit: shrink the fixed 1536×1024 artboard to fit smaller viewports
|
// 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);
|
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(() => {
|
useEffect(() => {
|
||||||
const compute = () =>
|
const compute = () => {
|
||||||
setArtboardScale(
|
setArtboardScale(Math.min(1, window.innerWidth / 1536));
|
||||||
Math.max(
|
setIsMobile(window.innerWidth < 768);
|
||||||
0.88,
|
};
|
||||||
Math.min(1, window.innerWidth / 1536, window.innerHeight / 1024),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
compute();
|
compute();
|
||||||
window.addEventListener("resize", compute);
|
window.addEventListener("resize", compute);
|
||||||
return () => window.removeEventListener("resize", compute);
|
return () => window.removeEventListener("resize", compute);
|
||||||
|
|
@ -573,7 +582,8 @@ export default function TradeInV2Page() {
|
||||||
const topNavUser = useMemo(() => {
|
const topNavUser = useMemo(() => {
|
||||||
const scope = me.data;
|
const scope = me.data;
|
||||||
if (!scope) return undefined;
|
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 =
|
const initials =
|
||||||
nameParts.length >= 2
|
nameParts.length >= 2
|
||||||
? (nameParts[0][0] + nameParts[1][0]).toUpperCase()
|
? (nameParts[0][0] + nameParts[1][0]).toUpperCase()
|
||||||
|
|
@ -693,7 +703,49 @@ export default function TradeInV2Page() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
|
@ -805,12 +857,16 @@ export default function TradeInV2Page() {
|
||||||
inert={nav !== 0 || drawerOpen ? true : undefined}
|
inert={nav !== 0 || drawerOpen ? true : undefined}
|
||||||
style={{ display: "contents" }}
|
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
|
<HeroBar
|
||||||
data={{ report, object: objectInfo }}
|
data={{ report, object: objectInfo }}
|
||||||
estimateId={mounted ? currentEstimateId : null}
|
estimateId={mounted ? currentEstimateId : null}
|
||||||
onOpenInfo={() => setDrawerOpen(true)}
|
onOpenInfo={() => setDrawerOpen(true)}
|
||||||
hasEstimate={hasEstimate}
|
hasEstimate={hasEstimate}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -831,7 +887,9 @@ export default function TradeInV2Page() {
|
||||||
markers={markers}
|
markers={markers}
|
||||||
/>
|
/>
|
||||||
{middleContent}
|
{middleContent}
|
||||||
{rightContent}
|
{/* #2275: on mobile ObjectSummary is rendered fluid in the
|
||||||
|
quick-view block above instead of here (tiny scaled copy). */}
|
||||||
|
{!isMobile && rightContent}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
@ -861,5 +919,6 @@ export default function TradeInV2Page() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,12 @@ interface HeroBarProps {
|
||||||
// are hidden, mirroring how the PDF control already gates on estimate presence.
|
// are hidden, mirroring how the PDF control already gates on estimate presence.
|
||||||
hasEstimate: boolean;
|
hasEstimate: boolean;
|
||||||
onOpenInfo: () => void;
|
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 = {
|
const pdfBtnStyle: CSSProperties = {
|
||||||
|
|
@ -80,6 +86,7 @@ export default function HeroBar({
|
||||||
estimateId,
|
estimateId,
|
||||||
hasEstimate,
|
hasEstimate,
|
||||||
onOpenInfo,
|
onOpenInfo,
|
||||||
|
compact = false,
|
||||||
}: HeroBarProps) {
|
}: HeroBarProps) {
|
||||||
const pdfHref = hasEstimate ? pdfDownloadHref(estimateId) : null;
|
const pdfHref = hasEstimate ? pdfDownloadHref(estimateId) : null;
|
||||||
// A downloadable report exists ⇔ the estimate is ready ⇒ the PDF button is the
|
// 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";
|
const rule = filled ? "rgba(255,255,255,.75)" : "#6f8195";
|
||||||
return (
|
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="M2 1h9l5 5v13H2z" stroke={frame} strokeWidth="1.2" />
|
||||||
<path d="M11 1v5h5" 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} />
|
<line x1="5" y1="11" x2="13" y2="11" stroke={rule} />
|
||||||
|
|
@ -110,8 +123,9 @@ export default function HeroBar({
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: 30,
|
flexDirection: compact ? "column" : "row",
|
||||||
padding: "14px 2px 12px",
|
gap: compact ? 16 : 30,
|
||||||
|
padding: compact ? "0" : "14px 2px 12px",
|
||||||
flex: "0 0 auto",
|
flex: "0 0 auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -134,10 +148,18 @@ export default function HeroBar({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "center",
|
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>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -215,7 +237,15 @@ export default function HeroBar({
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 ? (
|
{pdfHref ? (
|
||||||
<a
|
<a
|
||||||
className={pdfFilled ? "hero-pdf-btn-filled" : "hero-pdf-btn"}
|
className={pdfFilled ? "hero-pdf-btn-filled" : "hero-pdf-btn"}
|
||||||
|
|
@ -265,7 +295,12 @@ export default function HeroBar({
|
||||||
</div>
|
</div>
|
||||||
</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
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
|
@ -372,7 +407,11 @@ export default function HeroBar({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{ height: 1, background: tokens.lineSoft, margin: "8px 0" }}
|
style={{
|
||||||
|
height: 1,
|
||||||
|
background: tokens.lineSoft,
|
||||||
|
margin: "8px 0",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -385,7 +424,11 @@ export default function HeroBar({
|
||||||
{data.object.streetView}
|
{data.object.streetView}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{ height: 1, background: tokens.lineSoft, margin: "8px 0" }}
|
style={{
|
||||||
|
height: 1,
|
||||||
|
background: tokens.lineSoft,
|
||||||
|
margin: "8px 0",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
// role=button + tabIndex + Enter/Space (#2264 C6): keyboard-operable
|
// role=button + tabIndex + Enter/Space (#2264 C6): keyboard-operable
|
||||||
|
|
@ -490,7 +533,13 @@ export default function HeroBar({
|
||||||
color: tokens.accent,
|
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" />
|
<circle cx="11" cy="11" r="10" stroke="#2e8bff" strokeWidth="1" />
|
||||||
<path d="M11 3 L13 11 L11 9 L9 11 Z" fill="#2e8bff" />
|
<path d="M11 3 L13 11 L11 9 L9 11 Z" fill="#2e8bff" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -626,6 +675,7 @@ export default function HeroBar({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue