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"); + }); +});