fix(tradein/v2): mobile layout — remove overflow floor + honest stacked mobile block (#2275) #2389

Merged
lekss361 merged 1 commit from fix/tradein-v2-mobile-layout into main 2026-07-04 07:02:56 +00:00
2 changed files with 597 additions and 488 deletions

View file

@ -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 (
<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={{
position: "relative",
width: 1536,
height: 1024,
overflow: "hidden",
background: tokens.gradientBg,
fontFamily: tokens.font.sans,
color: tokens.ink,
transform: `scale(${artboardScale})`,
transformOrigin: "top left",
}}
style={{ width: 1536 * artboardScale, height: 1024 * artboardScale }}
>
<style>{HELPER_STYLES}</style>
<div
style={{
position: "relative",
width: 1536,
height: 1024,
overflow: "hidden",
background: tokens.gradientBg,
fontFamily: tokens.font.sans,
color: tokens.ink,
transform: `scale(${artboardScale})`,
transformOrigin: "top left",
}}
>
<style>{HELPER_STYLES}</style>
{/* #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 <h1> (page-has-heading-one
+ a top of the heading order for the overlay/panel <h2>s). */}
<h1
style={{
position: "absolute",
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clip: "rect(0 0 0 0)",
whiteSpace: "nowrap",
border: 0,
}}
>
Мера оценка квартиры
</h1>
{/* 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 */}
<div
style={{
position: "absolute",
inset: FRAME_INSET,
border: `1px solid ${tokens.line}`,
borderRadius: 4,
clipPath: FRAME_CLIP,
pointerEvents: "none",
}}
/>
{/* 4 corner brackets */}
{brackets.map((b) => (
<div
key={b.key}
<h1
style={{
position: "absolute",
width: BRACKET_ARM,
height: BRACKET_ARM,
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clip: "rect(0 0 0 0)",
whiteSpace: "nowrap",
border: 0,
}}
>
Мера оценка квартиры
</h1>
{/* 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 */}
<div
style={{
position: "absolute",
inset: FRAME_INSET,
border: `1px solid ${tokens.line}`,
borderRadius: 4,
clipPath: FRAME_CLIP,
pointerEvents: "none",
...b.style,
}}
/>
))}
{/* 4 corner brackets */}
{brackets.map((b) => (
<div
key={b.key}
style={{
position: "absolute",
width: BRACKET_ARM,
height: BRACKET_ARM,
pointerEvents: "none",
...b.style,
}}
/>
))}
{/* CONTENT WRAP */}
<div
style={{
position: "absolute",
inset: 14,
padding: "16px 28px",
display: "flex",
flexDirection: "column",
}}
>
{/* TopNav stays interactive while a section overlay is open so the
{/* CONTENT WRAP */}
<div
style={{
position: "absolute",
inset: 14,
padding: "16px 28px",
display: "flex",
flexDirection: "column",
}}
>
{/* 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 (<main>/<footer>) is still inerted behind ANY
overlay/drawer preserving the focus + click guard from a11y
audit #2081 for everything except the top tabs. */}
<nav
aria-label="Разделы"
inert={drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<TopNav
active={nav}
onNavigate={setNav}
reports={reportsCount ?? 0}
user={topNavUser}
onLogout={logout}
/>
</nav>
<main
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<HeroBar
data={{ report, object: objectInfo }}
estimateId={mounted ? currentEstimateId : null}
onOpenInfo={() => setDrawerOpen(true)}
hasEstimate={hasEstimate}
/>
<div
style={{
display: "grid",
gridTemplateColumns: "474px 1fr 250px",
gap: 18,
flex: 1,
minHeight: 0,
}}
<nav
aria-label="Разделы"
inert={drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<ParamsPanel
key={estimate?.estimate_id ?? "new"}
onSubmit={handleSubmit}
isPending={mutation.isPending}
hasEstimate={hasEstimate}
error={apiError}
initialValues={initialValues}
markers={markers}
<TopNav
active={nav}
onNavigate={setNav}
reports={reportsCount ?? 0}
user={topNavUser}
onLogout={logout}
/>
{middleContent}
{rightContent}
</div>
</main>
</nav>
<main
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}
/>
)}
<footer
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<Footer data={report} hasEstimate={hasEstimate} />
</footer>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "474px 1fr 250px",
gap: 18,
flex: 1,
minHeight: 0,
}}
>
<ParamsPanel
key={estimate?.estimate_id ?? "new"}
onSubmit={handleSubmit}
isPending={mutation.isPending}
hasEstimate={hasEstimate}
error={apiError}
initialValues={initialValues}
markers={markers}
/>
{middleContent}
{/* #2275: on mobile ObjectSummary is rendered fluid in the
quick-view block above instead of here (tiny scaled copy). */}
{!isMobile && rightContent}
</div>
</main>
{nav !== 0 && (
<SectionOverlay
active={nav}
onClose={() => setNav(0)}
onNavigate={setNav}
history={historyData}
analytics={analyticsViewData}
sources={sourcesData}
cache={cacheData}
<footer
inert={nav !== 0 || drawerOpen ? true : undefined}
style={{ display: "contents" }}
>
<Footer data={report} hasEstimate={hasEstimate} />
</footer>
</div>
{nav !== 0 && (
<SectionOverlay
active={nav}
onClose={() => setNav(0)}
onNavigate={setNav}
history={historyData}
analytics={analyticsViewData}
sources={sourcesData}
cache={cacheData}
/>
)}
<LocationDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
data={locationData}
/>
)}
<LocationDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
data={locationData}
/>
</div>
</div>
</div>
);

View file

@ -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", gap: 26, alignItems: "flex-start" }}>
<div
style={{
display: "flex",
flexWrap: compact ? "wrap" : "nowrap",
gap: compact ? 18 : 26,
rowGap: compact ? 8 : undefined,
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,367 +295,387 @@ export default function HeroBar({
</div>
</div>
{/* RIGHT: photo + overlays */}
<div
style={{
position: "relative",
width: 560,
height: 152,
flex: "0 0 auto",
border: `1px solid ${tokens.line3}`,
borderRadius: 6,
overflow: "hidden",
background: tokens.photoBg,
boxShadow: "0 6px 26px rgba(40,80,130,.10)",
}}
>
{!imgFailed && (
<img
src={`${BP}/trade-in-v2/building.png`}
alt=""
onError={() => 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 && (
<div
style={{
position: "absolute",
inset: 0,
background: "rgba(46,139,255,.14)",
mixBlendMode: "multiply",
pointerEvents: "none",
}}
/>
)}
{/* 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: "absolute",
inset: 0,
background:
"linear-gradient(90deg,rgba(238,244,250,.96) 0%,rgba(238,244,250,.5) 22%,transparent 40%)",
}}
/>
<div
style={{
position: "absolute",
inset: 0,
background:
"linear-gradient(0deg,rgba(230,240,250,.45),transparent 40%)",
}}
/>
<div
style={{
position: "absolute",
left: 0,
right: 0,
top: "34%",
height: 1,
background:
"linear-gradient(90deg,transparent,rgba(46,139,255,.5),transparent)",
animation: "hero-scanv 6s linear infinite",
}}
/>
{/* address card */}
<div
style={{
position: "absolute",
left: 16,
top: "50%",
transform: "translateY(-50%)",
width: 182,
background: tokens.surface.w72,
backdropFilter: "blur(5px)",
border: `1px solid ${tokens.line}`,
borderRadius: 5,
padding: "11px 13px",
position: "relative",
width: 560,
height: 152,
flex: "0 0 auto",
border: `1px solid ${tokens.line3}`,
borderRadius: 6,
overflow: "hidden",
background: tokens.photoBg,
boxShadow: "0 6px 26px rgba(40,80,130,.10)",
}}
>
<div
style={{
fontSize: 9,
letterSpacing: "2.5px",
color: tokens.muted2,
marginBottom: 3,
}}
>
АДРЕС
</div>
<div style={{ fontSize: 14, fontWeight: 600, lineHeight: 1.3 }}>
{data.object.address}
<br />
<span
style={{ fontSize: 11, fontWeight: 400, color: tokens.muted }}
>
{data.object.city}
</span>
</div>
<div
style={{ height: 1, background: tokens.lineSoft, margin: "8px 0" }}
/>
<div
style={{
fontFamily: tokens.font.mono,
fontSize: 9,
color: tokens.muted,
lineHeight: 1.55,
}}
>
{data.object.streetView}
</div>
<div
style={{ height: 1, background: tokens.lineSoft, margin: "8px 0" }}
/>
<div
// role=button + tabIndex + Enter/Space (#2264 C6): keyboard-operable
// without switching the element to a <button> (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",
}}
>
<span
{!imgFailed && (
<img
src={`${BP}/trade-in-v2/building.png`}
alt=""
onError={() => 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 && (
<div
style={{
position: "absolute",
inset: 0,
background: "rgba(46,139,255,.14)",
mixBlendMode: "multiply",
pointerEvents: "none",
}}
/>
)}
<div
style={{
position: "absolute",
inset: 0,
background:
"linear-gradient(90deg,rgba(238,244,250,.96) 0%,rgba(238,244,250,.5) 22%,transparent 40%)",
}}
/>
<div
style={{
position: "absolute",
inset: 0,
background:
"linear-gradient(0deg,rgba(230,240,250,.45),transparent 40%)",
}}
/>
<div
style={{
position: "absolute",
left: 0,
right: 0,
top: "34%",
height: 1,
background:
"linear-gradient(90deg,transparent,rgba(46,139,255,.5),transparent)",
animation: "hero-scanv 6s linear infinite",
}}
/>
{/* address card */}
<div
style={{
position: "absolute",
left: 16,
top: "50%",
transform: "translateY(-50%)",
width: 182,
background: tokens.surface.w72,
backdropFilter: "blur(5px)",
border: `1px solid ${tokens.line}`,
borderRadius: 5,
padding: "11px 13px",
}}
>
<div
style={{
fontSize: 9,
letterSpacing: "2.5px",
color: tokens.muted2,
marginBottom: 3,
}}
>
КОЭФ. ЛОКАЦИИ
</span>
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
{data.object.locationCoef === "—" ? (
<span
style={{
fontSize: "9px",
letterSpacing: ".5px",
// body2 (not muted2): муть на badgeTint давала 2.3:1 < AA;
// body2 на badgeTint ≈ 6.3:1 — читаемо, остаётся «приглушённым».
color: tokens.body2,
background: tokens.badgeTint,
border: `1px solid ${tokens.line2}`,
borderRadius: 10,
padding: "1px 8px",
}}
>
{/* #2317: coef is a live feature now (GET /location-coef) a
АДРЕС
</div>
<div style={{ fontSize: 14, fontWeight: 600, lineHeight: 1.3 }}>
{data.object.address}
<br />
<span
style={{ fontSize: 11, fontWeight: 400, color: tokens.muted }}
>
{data.object.city}
</span>
</div>
<div
style={{
height: 1,
background: tokens.lineSoft,
margin: "8px 0",
}}
/>
<div
style={{
fontFamily: tokens.font.mono,
fontSize: 9,
color: tokens.muted,
lineHeight: 1.55,
}}
>
{data.object.streetView}
</div>
<div
style={{
height: 1,
background: tokens.lineSoft,
margin: "8px 0",
}}
/>
<div
// role=button + tabIndex + Enter/Space (#2264 C6): keyboard-operable
// without switching the element to a <button> (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",
}}
>
<span
style={{
fontSize: "8.5px",
letterSpacing: "1.5px",
color: tokens.muted2,
}}
>
КОЭФ. ЛОКАЦИИ
</span>
<span style={{ display: "flex", alignItems: "center", gap: 6 }}>
{data.object.locationCoef === "—" ? (
<span
style={{
fontSize: "9px",
letterSpacing: ".5px",
// body2 (not muted2): муть на badgeTint давала 2.3:1 < AA;
// body2 на badgeTint ≈ 6.3:1 — читаемо, остаётся «приглушённым».
color: tokens.body2,
background: tokens.badgeTint,
border: `1px solid ${tokens.line2}`,
borderRadius: 10,
padding: "1px 8px",
}}
>
{/* #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. */}
нет данных
</span>
) : (
нет данных
</span>
) : (
<span
style={{
fontFamily: tokens.font.mono,
fontSize: 15,
fontWeight: 500,
color: tokens.accent,
}}
>
{data.object.locationCoef}
</span>
)}
<span
style={{
fontFamily: tokens.font.mono,
fontSize: 15,
fontWeight: 500,
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 14,
height: 14,
border: `1px solid ${tokens.accent}`,
borderRadius: "50%",
fontSize: "8.5px",
color: tokens.accent,
fontWeight: 600,
}}
aria-hidden="true"
>
{data.object.locationCoef}
?
</span>
)}
<span
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 14,
height: 14,
border: `1px solid ${tokens.accent}`,
borderRadius: "50%",
fontSize: "8.5px",
color: tokens.accent,
fontWeight: 600,
}}
aria-hidden="true"
>
?
</span>
</div>
</div>
{/* compass */}
<div
style={{
position: "absolute",
right: 16,
top: 14,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
color: tokens.accent,
}}
>
<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>
<span
style={{
fontFamily: tokens.font.mono,
fontSize: 8,
letterSpacing: ".5px",
color: tokens.muted,
whiteSpace: "nowrap",
}}
>
{data.object.compass}
</span>
</div>
</div>
{/* compass */}
<div
style={{
position: "absolute",
right: 16,
top: 14,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 2,
color: tokens.accent,
}}
>
<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>
<span
{/* distance scale */}
<div
style={{
position: "absolute",
left: "50%",
transform: "translateX(-50%)",
bottom: 16,
width: 170,
fontFamily: tokens.font.mono,
fontSize: 8,
letterSpacing: ".5px",
color: tokens.muted,
whiteSpace: "nowrap",
}}
>
{data.object.compass}
</span>
</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "flex-end",
height: 8,
borderBottom: "1px solid rgba(46,139,255,.55)",
}}
>
<span
style={{
width: 1,
height: 8,
background: "rgba(46,139,255,.55)",
}}
/>
<span
style={{
width: 1,
height: 5,
background: "rgba(46,139,255,.45)",
}}
/>
<span
style={{
width: 1,
height: 5,
background: "rgba(46,139,255,.45)",
}}
/>
<span
style={{
width: 1,
height: 5,
background: "rgba(46,139,255,.45)",
}}
/>
<span
style={{
width: 1,
height: 8,
background: "rgba(46,139,255,.55)",
}}
/>
</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: "7.5px",
color: tokens.muted,
marginTop: 3,
}}
>
<span>0</span>
<span>25</span>
<span>50</span>
<span>75</span>
<span>100м</span>
</div>
</div>
{/* distance scale */}
<div
style={{
position: "absolute",
left: "50%",
transform: "translateX(-50%)",
bottom: 16,
width: 170,
fontFamily: tokens.font.mono,
}}
>
{/* corner brackets */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "flex-end",
height: 8,
borderBottom: "1px solid rgba(46,139,255,.55)",
position: "absolute",
left: 8,
top: 8,
width: 14,
height: 14,
borderLeft: `1.5px solid ${tokens.accent}`,
borderTop: `1.5px solid ${tokens.accent}`,
}}
>
<span
style={{
width: 1,
height: 8,
background: "rgba(46,139,255,.55)",
}}
/>
<span
style={{
width: 1,
height: 5,
background: "rgba(46,139,255,.45)",
}}
/>
<span
style={{
width: 1,
height: 5,
background: "rgba(46,139,255,.45)",
}}
/>
<span
style={{
width: 1,
height: 5,
background: "rgba(46,139,255,.45)",
}}
/>
<span
style={{
width: 1,
height: 8,
background: "rgba(46,139,255,.55)",
}}
/>
</div>
/>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: "7.5px",
color: tokens.muted,
marginTop: 3,
position: "absolute",
right: 8,
top: 8,
width: 14,
height: 14,
borderRight: `1.5px solid ${tokens.accent}`,
borderTop: `1.5px solid ${tokens.accent}`,
}}
>
<span>0</span>
<span>25</span>
<span>50</span>
<span>75</span>
<span>100м</span>
</div>
/>
<div
style={{
position: "absolute",
left: 8,
bottom: 8,
width: 14,
height: 14,
borderLeft: `1.5px solid ${tokens.accent}`,
borderBottom: `1.5px solid ${tokens.accent}`,
}}
/>
<div
style={{
position: "absolute",
right: 8,
bottom: 8,
width: 14,
height: 14,
borderRight: `1.5px solid ${tokens.accent}`,
borderBottom: `1.5px solid ${tokens.accent}`,
}}
/>
</div>
{/* corner brackets */}
<div
style={{
position: "absolute",
left: 8,
top: 8,
width: 14,
height: 14,
borderLeft: `1.5px solid ${tokens.accent}`,
borderTop: `1.5px solid ${tokens.accent}`,
}}
/>
<div
style={{
position: "absolute",
right: 8,
top: 8,
width: 14,
height: 14,
borderRight: `1.5px solid ${tokens.accent}`,
borderTop: `1.5px solid ${tokens.accent}`,
}}
/>
<div
style={{
position: "absolute",
left: 8,
bottom: 8,
width: 14,
height: 14,
borderLeft: `1.5px solid ${tokens.accent}`,
borderBottom: `1.5px solid ${tokens.accent}`,
}}
/>
<div
style={{
position: "absolute",
right: 8,
bottom: 8,
width: 14,
height: 14,
borderRight: `1.5px solid ${tokens.accent}`,
borderBottom: `1.5px solid ${tokens.accent}`,
}}
/>
</div>
)}
</div>
);
}