From ab45cacfbfdf7da99d07f0f9ae7eabd671de607e Mon Sep 17 00:00:00 2001 From: Light1YT Date: Sat, 13 Jun 2026 10:56:43 +0500 Subject: [PATCH] =?UTF-8?q?fix(site-finder):=20MiniMap=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=BF=D0=B0=D0=B5=D1=82=20layer-controls=20?= =?UTF-8?q?=E2=80=94=20=D1=80=D1=8B=D0=BD=D0=BE=D1=87=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D1=81=D0=BB=D0=BE=D0=B8=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF?= =?UTF-8?q?=D0=BD=D1=8B=20(#1218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 1 mini-map (/site-finder/analysis/[cad] — единственная карта страницы) оборачивал SiteMap в height:320 + overflow:hidden. SiteMap рендерит карту фиксированной высотой 420px + ПОД ней (flow) кнопку "Добавить POI", легенду и CpLayerControlPanel с тумблерами competitors/pipeline/risk/opportunity/redlines. Нижние 100px карты и ВСЕ контролы обрезались clip-обёрткой → недостижимы кликом. visibleMarketLayers init = {"competitors"} — единственный способ включить остальные слои (#999 zone-of-risk, #958 §12.1-13 opportunity parcels / red lines) — те самые скрытые чекбоксы → слои физически невозможно было включить на странице анализа, хотя данные пробрасывались props'ами ради них. Patch (Вариант A из issue): - SiteMap.tsx: новый prop mapHeight?: number (default 420 — backward- compat для /site-finder/[cad]). - MiniMap.tsx: снято height:320 + overflow:hidden; передаём mapHeight=280 (компактнее но не клипает); только width-constraint (maxWidth 400) на обёртке. 2 новых vitest: mapHeight=280 пробрасывается в SiteMap (mocked stub), clipping-стили отсутствуют. 82/82 frontend тестов зелёные. tsc + lint clean. Closes #1218 --- .../src/components/site-finder/SiteMap.tsx | 8 ++- .../site-finder/analysis/MiniMap.tsx | 43 +++++++----- .../analysis/__tests__/MiniMap.test.tsx | 67 +++++++++++++++++++ 3 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/site-finder/analysis/__tests__/MiniMap.test.tsx diff --git a/frontend/src/components/site-finder/SiteMap.tsx b/frontend/src/components/site-finder/SiteMap.tsx index 25b63f2d..a69cd427 100644 --- a/frontend/src/components/site-finder/SiteMap.tsx +++ b/frontend/src/components/site-finder/SiteMap.tsx @@ -126,6 +126,11 @@ interface Props { // §12.1-13 (#958) — opportunity-ЗУ + красные линии застройки (geom_wkt). opportunityParcels?: OpportunityParcel[]; redLines?: RedLine[]; + // #1218 — высота тайла карты (px). Default 420 — режим обычной странички + // /site-finder/[cad]. MiniMap на /analysis передаёт меньше (≈280), чтобы + // карта была компактнее. На высоту НЕ влияет на flow-контролы ниже + // (POI add button, легенда, CpLayerControlPanel) — они идут потоком. + mapHeight?: number; } // Ключи переключаемых рыночных слоёв (#999 + §12.1-13 #958). @@ -167,6 +172,7 @@ export function SiteMap({ riskZones, opportunityParcels, redLines, + mapHeight = 420, }: Props) { // Fix Leaflet default icon paths broken by webpack bundler useEffect(() => { @@ -310,7 +316,7 @@ export function SiteMap({ border: "1px solid #e5e7eb", borderRadius: 12, overflow: "hidden", - height: 420, + height: mapHeight, cursor: addMode ? "crosshair" : "grab", }} > diff --git a/frontend/src/components/site-finder/analysis/MiniMap.tsx b/frontend/src/components/site-finder/analysis/MiniMap.tsx index 4cf4b4c3..5c4ff079 100644 --- a/frontend/src/components/site-finder/analysis/MiniMap.tsx +++ b/frontend/src/components/site-finder/analysis/MiniMap.tsx @@ -1,9 +1,15 @@ "use client"; /** - * MiniMap — compact Leaflet map (400×320) showing parcel polygon + top POI. + * MiniMap — compact Leaflet map (≈400px wide) showing parcel polygon + top POI. * Dynamic import of react-leaflet (ssr:false) to avoid SSR breakage. - * Wraps SiteMap with fixed dimensions for Section 1 layout. + * Wraps SiteMap with constrained width for Section 1 layout. + * + * #1218: ранее обёртка имела `height: 320 + overflow: hidden` — это клипало + * legend + CpLayerControlPanel, рендерящиеся flow-div ПОД картой в SiteMap, + * и делало недостижимыми переключатели рыночных слоёв (Будущие проекты, + * Зоны риска, Перспективные ЗУ, Красные линии). Фикс: задаём только ширину, + * высота тайла карты — через prop `mapHeight` SiteMap; контент течёт вниз. */ import dynamic from "next/dynamic"; @@ -11,6 +17,10 @@ import type { ParcelAnalyzeResponse } from "@/lib/site-finder-api"; import type { ParcelAnalysis } from "@/types/site-finder"; import type { Geometry } from "geojson"; +// MiniMap высота тайла карты — компромисс между плотностью Section 1 и +// читаемостью. Layer-controls/легенда вне этого размера (flow ниже). +const MINIMAP_HEIGHT = 280; + // Lazy-mount to avoid SSR breakage (Leaflet uses window) const SiteMap = dynamic( () => @@ -22,7 +32,7 @@ const SiteMap = dynamic( loading: () => (
-
- -
+
); } diff --git a/frontend/src/components/site-finder/analysis/__tests__/MiniMap.test.tsx b/frontend/src/components/site-finder/analysis/__tests__/MiniMap.test.tsx new file mode 100644 index 00000000..636d7508 --- /dev/null +++ b/frontend/src/components/site-finder/analysis/__tests__/MiniMap.test.tsx @@ -0,0 +1,67 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { MiniMap } from "../MiniMap"; +import type { ParcelAnalyzeResponse } from "@/lib/site-finder-api"; + +// Stub SiteMap so we can assert the props MiniMap passes through (mapHeight +// in particular — #1218: ранее MiniMap клипал layer-панель/легенду через +// `overflow: hidden + height: 320`; теперь высота карты задаётся через prop). +// Stub does NOT use `unknown` — we capture a typed snapshot of the relevant +// props (mapHeight) for the assertion. + +interface CapturedProps { + mapHeight?: number; +} + +const captured: { last: CapturedProps | null } = { last: null }; + +vi.mock("@/components/site-finder/SiteMap", () => ({ + SiteMap: (props: CapturedProps) => { + captured.last = { mapHeight: props.mapHeight }; + return
map(h={props.mapHeight ?? "?"})
; + }, +})); + +// Minimal ParcelAnalyzeResponse — only fields MiniMap touches (toSiteMapData). +const FIXTURE: ParcelAnalyzeResponse = { + cad_num: "66:41:0000000:1", + source: "cad_building", + geom_geojson: null, + district: null, + score: 0, + score_breakdown: {}, + poi_count: 0, + competitors: [], + pipeline_24mo: null, + nspd_risk_zones: [], + nspd_opportunity_parcels: [], + nspd_red_lines: [], +}; + +describe("MiniMap (#1218)", () => { + it("passes mapHeight=280 to SiteMap (compact tile, controls flow below)", async () => { + render(); + // SiteMap is dynamically imported (ssr:false) — wait until our stub renders. + await waitFor(() => + expect(screen.getByTestId("sitemap-stub")).toBeInTheDocument(), + ); + expect(captured.last?.mapHeight).toBe(280); + }); + + it("wrapper does NOT clip overflow — layer controls must remain visible", async () => { + const { container } = render(); + await waitFor(() => + expect(screen.getByTestId("sitemap-stub")).toBeInTheDocument(), + ); + // The outer wrapper is the first element child — SiteMap stub lives inside. + const wrapper = container.firstElementChild as HTMLElement | null; + expect(wrapper).not.toBeNull(); + // No fixed height (must be empty so flow children — legend + control panel — + // are not clipped to 320px), and no overflow: hidden. + expect(wrapper?.style.height).toBe(""); + expect(wrapper?.style.overflow).toBe(""); + // Width constraint preserved (column slot is 400px in Section 1 grid). + expect(wrapper?.style.maxWidth).toBe("400px"); + }); +});