From b3fd053e3d3451c5c7f8518205725fcae84cd786 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Sun, 21 Jun 2026 03:41:13 +0500 Subject: [PATCH] feat(ptica): map glyph pins + compass, equal-height grids, full-height rail; dark Site Finder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ПТИЦА cockpit: - map: POI/competitors/connections now render as glyph divIcon pins (per-category glyph + color) with dashed connection lines + a compass — matches the prototype (was plain CircleMarker dots). Legend/zoom/scale/base-toggle/tools kept. - lower+bottom grids: align-items stretch → equal-height rows; cards are flex columns with footer «Подробнее» pinned to bottom and sparse empty-states centred (fixes «выровняй тут все»). - rail: dropped sticky+height:100vh so it stretches to full page height like the prototype (fixes «обрезан»). Main Site Finder (entry landing) — restyled to the ПТИЦА dark cockpit theme, scoped to .sfRoot[data-theme=dark] (no leak to light pages): dark CARTO tiles, Leaflet attribution flag removed (prefix=false, © credit kept), dark markers / filter chips / legend / recent list / drawer / cad input. --- .../analysis/[cad]/ptica/ptica.module.css | 83 ++++- frontend/src/app/site-finder/page.tsx | 23 +- .../app/site-finder/site-finder.module.css | 132 ++++++++ .../src/components/site-finder/CadInput.tsx | 34 +- .../components/site-finder/entry/EntryMap.tsx | 61 ++-- .../site-finder/entry/MapFilterBar.tsx | 40 +-- .../site-finder/entry/ParcelDrawer.tsx | 71 ++--- .../site-finder/entry/ParcelLegend.tsx | 18 +- .../site-finder/entry/RecentParcels.tsx | 38 +-- .../site-finder/ptica/PticaMapInner.tsx | 297 ++++++++++++++---- 10 files changed, 591 insertions(+), 206 deletions(-) create mode 100644 frontend/src/app/site-finder/site-finder.module.css diff --git a/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css b/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css index b87003c1..e392f924 100644 --- a/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css +++ b/frontend/src/app/site-finder/analysis/[cad]/ptica/ptica.module.css @@ -125,9 +125,11 @@ flex-direction: column; padding: 16px 10px 12px; gap: 14px; - position: sticky; - top: 0; - height: 100vh; + /* Prototype .sidebar is a plain flex grid-child that stretches to the FULL + page height (footer pinned via .navFoot margin-top:auto). The old + `position:sticky; top:0; height:100vh` capped the rail strip at one viewport + so on tall pages it looked «обрезан» partway down. Drop it — the rail now + stretches to the full .shell grid height like the prototype. */ } .brand { display: flex; @@ -2451,6 +2453,19 @@ backdrop-filter: blur(6px); } +/* Static compass (prototype .map-compass) — bottom-right "N" arrow above the + Leaflet panes; non-interactive so it never blocks panning. */ +.mapCompass { + position: absolute; + right: 18px; + bottom: 16px; + width: 46px; + height: 46px; + color: var(--text-muted); + z-index: 4; + pointer-events: none; +} + /* ===================== LOWER GRID (cockpit) ===================== Ported from prototype .lower-grid: ОКС · Development Potential · Recommended Product · Инсоляционная матрица. */ @@ -2458,11 +2473,45 @@ display: grid; grid-template-columns: 1.05fr 1.05fr 1.15fr 0.95fr; gap: 10px; - /* HEADLINE BUG FIX: default align-items:stretch made the sparse/empty ОКС - card (isoBox 84px + emptyState flex:1 + button) stretch to the tallest - sibling, and .emptyState's flex:1 then filled the void → huge empty box. - align-items:start lets each card size to its own content. */ - align-items: start; + /* Equal-height row (прот.: «выровняй тут все»). Each card is a flex column + (rule below) so a stretched card lays out intentionally: the footer action + pins to the bottom (margin-top:auto) and sparse empty-states (.emptyState / + .placeholder flex:1) centre their content instead of leaving an awkward gap. */ + align-items: stretch; +} + +/* Make every lower/bottom-grid card a flex column so stretching is intentional: + the footer «Подробнее» button drops to the bottom and the .emptyState / + .placeholder body grows to fill (centred) rather than the card being a tall + empty box. */ +.lowerGrid > .card, +.bottomGrid > .card { + display: flex; + flex-direction: column; +} +/* Footer actions (DIRECT children of the card — the «Подробнее» button or the + Инсоляция .placeholder block) pinned to the bottom of a stretched flex-column + card. Header buttons (inside .cardHead) are NOT direct children, so they stay + put. */ +.lowerGrid > .card > .detailBtn, +.bottomGrid > .card > .detailBtn, +.lowerGrid > .card > .placeholder { + margin-top: auto; + align-self: flex-start; +} +/* The Инсоляция .placeholder is itself a centred column — its OWN button keeps + normal spacing inside it (the placeholder block is what gets pinned). */ +.lowerGrid > .card > .placeholder { + align-self: stretch; +} +.lowerGrid > .card > .placeholder .detailBtn { + margin-top: 8px; + align-self: center; +} +/* Sparse bodies grow + centre when their card is stretched (ОКС «Нет данных»). */ +.lowerGrid > .card > .emptyState, +.bottomGrid > .card > .emptyState { + flex: 1; } /* Lower-grid квартирография bars match the prototype .bar-row (narrow label / @@ -2592,9 +2641,11 @@ display: grid; grid-template-columns: 1.5fr 0.62fr 0.86fr 1.2fr; gap: 10px; - /* Same grid-stretch defect as .lowerGrid — sparse Legal Status / cards must - not stretch to the tallest sibling. Pin each card to the top. */ - align-items: start; + /* Equal-height row (как .lowerGrid · «выровняй тут все»). Cards are flex + columns (rule above): footer actions pin to the bottom, sparse bodies + (.emptyState flex:1) centre, and Buy Signal re-centres its gauge — so a + stretched card lays out intentionally instead of leaving an awkward gap. */ + align-items: stretch; } /* Investment Clearance — bignum row */ @@ -2639,10 +2690,12 @@ color: var(--text-soft); } -/* Buy Signal — centered gauge */ -.buySignalCard { - display: grid; - place-items: center; +/* Buy Signal — centered gauge. Flex-column centering (not grid) so it composes + with the `.bottomGrid > .card` flex-column stretch rule; the gauge body sits + centred in the equal-height row. */ +.bottomGrid > .card.buySignalCard { + align-items: center; + justify-content: center; } .buySignalTitle { align-self: flex-start; diff --git a/frontend/src/app/site-finder/page.tsx b/frontend/src/app/site-finder/page.tsx index 82c2080c..c55fc79d 100644 --- a/frontend/src/app/site-finder/page.tsx +++ b/frontend/src/app/site-finder/page.tsx @@ -19,6 +19,7 @@ import type { ParcelBboxItem, BboxCoords, } from "@/lib/site-finder-api"; +import styles from "./site-finder.module.css"; // EntryMap uses Leaflet — must load without SSR const EntryMap = dynamic( @@ -31,13 +32,13 @@ const EntryMap = dynamic( style={{ flex: 1, minHeight: 480, - background: "var(--bg-card-alt)", - border: "1px dashed var(--border-strong)", + background: "rgba(9, 22, 31, 0.62)", + border: "1px dashed rgba(150, 192, 214, 0.4)", borderRadius: 12, display: "flex", alignItems: "center", justifyContent: "center", - color: "var(--fg-tertiary)", + color: "#8ba6b3", fontSize: 14, }} > @@ -129,9 +130,10 @@ export default function SiteFinderPage() { return (
← Главная - · + ·

SiteFinder · карта участков @@ -174,7 +177,7 @@ export default function SiteFinderPage() { style={{ marginLeft: "auto", fontSize: 13, - color: "var(--accent)", + color: "#7fd0ee", textDecoration: "none", fontWeight: 500, }} diff --git a/frontend/src/app/site-finder/site-finder.module.css b/frontend/src/app/site-finder/site-finder.module.css new file mode 100644 index 00000000..781619a7 --- /dev/null +++ b/frontend/src/app/site-finder/site-finder.module.css @@ -0,0 +1,132 @@ +/* + * SiteFinder entry page — ПТИЦА dark "operator terminal" skin (Track B). + * + * Every design token is scoped under `.sfRoot[data-theme="dark"]` (the page + * wrapper carries BOTH the class and the attribute) so these dark vars NEVER + * leak onto the global light theme of other pages (analytics etc.). This + * mirrors how the ПТИЦА cockpit scopes its dark tokens under + * `.pticaRoot[data-theme="dark"]`. The dark palette is the SAME cockpit palette + * (#060f16 / #08131c surfaces, #dcebf2 text, #7fd0ee accent-cyan, token borders). + */ + +/* ===================== SCOPED TOKENS (dark) ===================== */ +.sfRoot[data-theme="dark"] { + --sf-font-mono: "IBM Plex Mono", "Roboto Mono", ui-monospace, monospace; + + /* accents */ + --sf-accent-cyan: #7fd0ee; + --sf-accent-cyan-strong: #8ed3ff; + --sf-accent-green: #38c172; + --sf-accent-yellow: #e0a93b; + --sf-accent-red: #d2655b; + + /* dark surface palette */ + --sf-bg: #060f16; + --sf-bg-2: #08131c; + --sf-grid-line: rgba(120, 165, 190, 0.06); + --sf-surface: rgba(11, 26, 36, 0.72); + --sf-surface-2: rgba(9, 22, 31, 0.62); + --sf-surface-strong: rgba(12, 30, 42, 0.94); + --sf-surface-muted: rgba(26, 52, 67, 0.55); + --sf-surface-inset: rgba(4, 12, 18, 0.6); + + --sf-text: #dcebf2; + --sf-text-strong: #f2fafe; + --sf-text-muted: #8ba6b3; + --sf-text-soft: #5f7886; + + --sf-border: rgba(150, 192, 214, 0.18); + --sf-border-strong: rgba(150, 192, 214, 0.4); + --sf-border-faint: rgba(150, 192, 214, 0.1); + + --sf-map-filter: saturate(0.7) contrast(1.12) brightness(0.7); + --sf-glow: rgba(110, 200, 240, 0.28); + --sf-glow-strong: rgba(110, 200, 240, 0.55); + --sf-shadow: 0 18px 70px rgba(0, 0, 0, 0.5); + + /* root paint — subtle grid + corner glow, same recipe as the cockpit */ + color: var(--sf-text); + font-variant-numeric: tabular-nums; + -webkit-font-smoothing: antialiased; + background: + linear-gradient(90deg, transparent 31px, var(--sf-grid-line) 32px), + linear-gradient(0deg, transparent 31px, var(--sf-grid-line) 32px), + radial-gradient(circle at 82% -8%, var(--sf-glow), transparent 42%), + var(--sf-bg); + background-size: + 32px 32px, + 32px 32px, + 100% 100%, + 100% 100%; +} + +/* ===================== LEAFLET (dark map chrome) ===================== */ +.sfRoot :global(.leaflet-container) { + background: var(--sf-bg-2); + font-family: inherit; +} +/* dark base tiles → muted / contrast-bumped so markers stay readable */ +.sfRoot :global(.leaflet-tile-pane) { + filter: var(--sf-map-filter); +} +/* glassy dark popups (match the cockpit chrome) */ +.sfRoot :global(.leaflet-popup-content-wrapper) { + background: var(--sf-surface-strong); + color: var(--sf-text); + border: 1px solid var(--sf-border); + border-radius: 8px; + box-shadow: var(--sf-shadow); +} +.sfRoot :global(.leaflet-popup-tip) { + background: var(--sf-surface-strong); +} +.sfRoot :global(.leaflet-popup-content) { + margin: 10px 12px; +} +.sfRoot :global(.leaflet-container a.leaflet-popup-close-button) { + color: var(--sf-text-muted); +} +/* dark tooltips */ +.sfRoot :global(.leaflet-tooltip) { + background: var(--sf-surface-strong); + color: var(--sf-text); + border: 1px solid var(--sf-border); + box-shadow: var(--sf-shadow); +} +.sfRoot :global(.leaflet-tooltip-top::before) { + border-top-color: var(--sf-border); +} +.sfRoot :global(.leaflet-control-attribution) { + background: rgba(8, 19, 28, 0.55); + color: var(--sf-text-soft); + font-size: 9px; + padding: 1px 6px; + opacity: 0.65; + backdrop-filter: blur(4px); +} +.sfRoot :global(.leaflet-control-attribution a) { + color: var(--sf-text-muted); +} +/* dark-styled zoom +/- control (prototype map controls) */ +.sfRoot :global(.leaflet-control-zoom) { + border: 1px solid var(--sf-border-strong); + border-radius: 4px; + overflow: hidden; + box-shadow: 0 0 12px var(--sf-glow); +} +.sfRoot :global(.leaflet-control-zoom a) { + background: var(--sf-surface-strong); + color: var(--sf-text); + border-bottom: 1px solid var(--sf-border); + width: 28px; + height: 28px; + line-height: 28px; +} +.sfRoot :global(.leaflet-control-zoom a:hover) { + background: var(--sf-surface-muted); + color: var(--sf-accent-cyan); +} +.sfRoot :global(.leaflet-control-zoom a.leaflet-disabled) { + background: var(--sf-surface-strong); + color: var(--sf-text-soft); +} diff --git a/frontend/src/components/site-finder/CadInput.tsx b/frontend/src/components/site-finder/CadInput.tsx index 414b517e..15a62783 100644 --- a/frontend/src/components/site-finder/CadInput.tsx +++ b/frontend/src/components/site-finder/CadInput.tsx @@ -30,7 +30,15 @@ export function CadInput({ onSubmit, loading }: Props) { } return ( -
+