feat(frontend): mobile responsiveness (#66) + data-sources страница (#80)
Some checks failed
CI / changes (push) Successful in 8s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Successful in 1m3s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 50s
Deploy / changes (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Some checks failed
CI / changes (push) Successful in 8s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Successful in 1m3s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 50s
Deploy / changes (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
#66: responsive CSS-layer в globals.css (3-контекста <768/768-1280/>1280, не Tailwind sm/md/lg) + ResizeObserver в ChartShell (единственный echarts импортёр → все charts рефлоу) + class-применение (grid-collapse, nav horizontal-scroll, map-stack) на site-finder/analytics. overflow-x guard. #80: /data-sources attribution-страница (OSM ODbL/ДОМ.РФ/Объектив/Open-Meteo/Росреестр/ НСПД, server component, tokens) + footer-link. Closes #66 Closes #80
This commit is contained in:
parent
0b20d98b45
commit
8107c23f18
9 changed files with 497 additions and 6 deletions
|
|
@ -118,6 +118,7 @@ export default function SverdlMarketPage() {
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
className="gd-grid-sidebar"
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "2fr 1fr",
|
gridTemplateColumns: "2fr 1fr",
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export default function RecommendPage() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
className="gd-grid-sidebar"
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "minmax(300px, 360px) 1fr",
|
gridTemplateColumns: "minmax(300px, 360px) 1fr",
|
||||||
|
|
|
||||||
336
frontend/src/app/data-sources/page.tsx
Normal file
336
frontend/src/app/data-sources/page.tsx
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Database, ExternalLink, MapPin } from "lucide-react";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Источники данных и атрибуция · GenDesign",
|
||||||
|
description:
|
||||||
|
"Перечень внешних источников данных GenDesign, их лицензии и требования к атрибуции: OpenStreetMap, ДОМ.РФ, Объектив, Open-Meteo, Росреестр, НСПД.",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Source registry ───────────────────────────────────────────────────────────
|
||||||
|
// Перечень источников + лицензия + строка атрибуции. Содержит фактическую
|
||||||
|
// атрибуцию; юридическая интерпретация ToS ведётся отдельно (vault
|
||||||
|
// decisions/Data_Licenses_Audit.md). Эта страница — публичная attribution-
|
||||||
|
// поверхность, требуемая лицензиями (в первую очередь OSM ODbL).
|
||||||
|
|
||||||
|
interface DataSource {
|
||||||
|
name: string;
|
||||||
|
/** Что именно используется из источника. */
|
||||||
|
usage: string;
|
||||||
|
/** Короткое имя лицензии / режима использования. */
|
||||||
|
license: string;
|
||||||
|
/** Строка атрибуции в том виде, как её требует источник. */
|
||||||
|
attribution: string;
|
||||||
|
/** Канонический URL источника / условий. */
|
||||||
|
href: string;
|
||||||
|
hrefLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SOURCES: DataSource[] = [
|
||||||
|
{
|
||||||
|
name: "OpenStreetMap",
|
||||||
|
usage:
|
||||||
|
"Базовая картографическая подложка (тайлы) на всех картах Leaflet, геокодирование точек интереса.",
|
||||||
|
license: "ODbL 1.0 (данные) · CC BY-SA (картография)",
|
||||||
|
attribution: "© участники OpenStreetMap",
|
||||||
|
href: "https://www.openstreetmap.org/copyright",
|
||||||
|
hrefLabel: "openstreetmap.org/copyright",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ДОМ.РФ",
|
||||||
|
usage:
|
||||||
|
"Реестр строящегося жилья (портфель новостроек, площади, sold %, сроки ввода), сведения ПЗЗ.",
|
||||||
|
license: "Публичные данные · по условиям наш.дом.рф",
|
||||||
|
attribution:
|
||||||
|
"Источник: Единая информационная система жилищного строительства (наш.дом.рф)",
|
||||||
|
href: "https://наш.дом.рф",
|
||||||
|
hrefLabel: "наш.дом.рф",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Объектив",
|
||||||
|
usage:
|
||||||
|
"Цены предложения и скорость продаж по жилым комплексам (sale-graph, velocity).",
|
||||||
|
license: "Коммерческий доступ по договору (платный тариф)",
|
||||||
|
attribution: "Данные о ценах предложения предоставлены сервисом «Объектив»",
|
||||||
|
href: "https://objced.ru",
|
||||||
|
hrefLabel: "objced.ru",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Open-Meteo",
|
||||||
|
usage:
|
||||||
|
"Сезонная погода и климатические нормали для оценки строительного сезона участка.",
|
||||||
|
license: "CC BY 4.0 · коммерческое использование по платному тарифу",
|
||||||
|
attribution: "Погодные данные © Open-Meteo.com",
|
||||||
|
href: "https://open-meteo.com/en/license",
|
||||||
|
hrefLabel: "open-meteo.com/license",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Росреестр / ЕГРН",
|
||||||
|
usage:
|
||||||
|
"Сведения ЕГРН по участку (категория, ВРИ, площадь), ДДУ-сделки (Rosreestr deals) для рыночной аналитики.",
|
||||||
|
license: "Сведения ЕГРН · по условиям предоставления Росреестра",
|
||||||
|
attribution: "Источник: Росреестр (ЕГРН), Публичная кадастровая карта",
|
||||||
|
href: "https://rosreestr.gov.ru",
|
||||||
|
hrefLabel: "rosreestr.gov.ru",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "НСПД",
|
||||||
|
usage:
|
||||||
|
"Национальная система пространственных данных: зонирование, красные линии, ЗОУИТ, инженерные сети рядом с участком.",
|
||||||
|
license: "Публичные пространственные данные · по условиям nspd.gov.ru",
|
||||||
|
attribution:
|
||||||
|
"Источник: Национальная система пространственных данных (НСПД)",
|
||||||
|
href: "https://nspd.gov.ru",
|
||||||
|
hrefLabel: "nspd.gov.ru",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Cell styles ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const thStyle: React.CSSProperties = {
|
||||||
|
textAlign: "left",
|
||||||
|
padding: "10px 12px",
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.04em",
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
borderBottom: "1px solid var(--border-card)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
};
|
||||||
|
|
||||||
|
const tdStyle: React.CSSProperties = {
|
||||||
|
padding: "12px 12px",
|
||||||
|
fontSize: 13,
|
||||||
|
color: "var(--fg-primary)",
|
||||||
|
borderBottom: "1px solid var(--border-soft)",
|
||||||
|
verticalAlign: "top",
|
||||||
|
lineHeight: 1.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DataSourcesPage() {
|
||||||
|
return (
|
||||||
|
<main style={{ background: "var(--bg-app)", minHeight: "100vh" }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: 1024,
|
||||||
|
margin: "0 auto",
|
||||||
|
padding: "24px 24px 48px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<header style={{ marginBottom: 24 }}>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
textDecoration: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← GenDesign
|
||||||
|
</Link>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
margin: "8px 0 0",
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--fg-primary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Источники данных и атрибуция
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "8px 0 0",
|
||||||
|
maxWidth: 720,
|
||||||
|
fontSize: 14,
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
lineHeight: 1.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
GenDesign объединяет открытые и лицензированные источники
|
||||||
|
пространственных и рыночных данных. Ниже — перечень источников,
|
||||||
|
режим их использования и требуемая атрибуция. Картографическая
|
||||||
|
подложка предоставлена участниками OpenStreetMap по лицензии ODbL.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Sources table */}
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-card)",
|
||||||
|
border: "1px solid var(--border-card)",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Database
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
color="var(--fg-secondary)"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--fg-primary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Перечень источников
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* На узких экранах таблица скроллится горизонтально (issue #66). */}
|
||||||
|
<div className="gd-table-scroll">
|
||||||
|
<table
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
minWidth: 720,
|
||||||
|
borderCollapse: "collapse",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={thStyle}>Источник</th>
|
||||||
|
<th style={thStyle}>Что используется</th>
|
||||||
|
<th style={thStyle}>Лицензия / режим</th>
|
||||||
|
<th style={thStyle}>Атрибуция</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{SOURCES.map((s) => (
|
||||||
|
<tr key={s.name}>
|
||||||
|
<td style={{ ...tdStyle, whiteSpace: "nowrap" }}>
|
||||||
|
<span
|
||||||
|
style={{ fontWeight: 600, color: "var(--fg-primary)" }}
|
||||||
|
>
|
||||||
|
{s.name}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href={s.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4,
|
||||||
|
marginTop: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "var(--accent)",
|
||||||
|
textDecoration: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExternalLink size={12} strokeWidth={1.5} />
|
||||||
|
{s.hrefLabel}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td style={{ ...tdStyle, minWidth: 240 }}>{s.usage}</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
...tdStyle,
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
minWidth: 180,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.license}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
...tdStyle,
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
minWidth: 200,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{s.attribution}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* OSM attribution note */}
|
||||||
|
<section
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
background: "var(--bg-card)",
|
||||||
|
border: "1px solid var(--border-card)",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 20,
|
||||||
|
display: "flex",
|
||||||
|
gap: 12,
|
||||||
|
alignItems: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MapPin
|
||||||
|
size={20}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
color="var(--fg-secondary)"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ flexShrink: 0, marginTop: 2 }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--fg-primary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Картография OpenStreetMap
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "6px 0 0",
|
||||||
|
fontSize: 13,
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
lineHeight: 1.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Все интерактивные карты используют тайлы OpenStreetMap. Данные
|
||||||
|
распространяются по лицензии{" "}
|
||||||
|
<a
|
||||||
|
href="https://opendatacommons.org/licenses/odbl/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ color: "var(--accent)", textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
Open Data Commons Open Database License (ODbL)
|
||||||
|
</a>{" "}
|
||||||
|
и сопровождаются строкой «© участники OpenStreetMap» в правом
|
||||||
|
нижнем углу каждой карты.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Legal caveat */}
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
marginTop: 16,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "var(--fg-tertiary)",
|
||||||
|
lineHeight: 1.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Перечень носит информационный характер и отражает текущий состав
|
||||||
|
используемых источников. По вопросам условий использования и
|
||||||
|
лицензирования данных — свяжитесь с командой GenDesign.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -88,3 +88,114 @@ body {
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ── Responsive (issue #66) ───────────────────────────────────────────────────
|
||||||
|
* 3 контекста (НЕ Tailwind sm/md/lg): phone <768, tablet 768–1280, desktop >1280.
|
||||||
|
* Утилиты ниже — единственный источник правды для брейкпоинтов. Компоненты
|
||||||
|
* подключаются через className; инлайн-стили остаются desktop-видом по умолчанию.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Никакого горизонтального скролла на узких экранах (acceptance #66). */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar-grid (напр. "240px 1fr" / "minmax(300px,360px) 1fr"): на планшете
|
||||||
|
* и ниже схлопывается в одну колонку, сайдбар уезжает над контентом. */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.gd-grid-sidebar {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
}
|
||||||
|
/* sticky-сайдбар внутри схлопнутого грида не должен «прилипать». */
|
||||||
|
.gd-grid-sidebar > .gd-sidebar {
|
||||||
|
position: static !important;
|
||||||
|
top: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Карта/контент рядом с фиксированным сайдбаром: на планшете в колонку. */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.gd-split {
|
||||||
|
flex-direction: column !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
.gd-split > .gd-split-aside {
|
||||||
|
width: 100% !important;
|
||||||
|
flex-shrink: 1 !important;
|
||||||
|
/* в desktop у сайдбара не было левого паддинга (он стоял справа от карты);
|
||||||
|
* в колонке возвращаем симметрию. */
|
||||||
|
padding-left: 16px !important;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav-tabs: на телефоне — горизонтальный скролл со снапом, НЕ hamburger. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.gd-nav-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
scroll-snap-type: x proximity;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.gd-nav-scroll::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.gd-nav-scroll > * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Таблицы с фиксированной шириной колонок — оборачивать в .gd-table-scroll. */
|
||||||
|
.gd-table-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KPI-grid: на телефоне одна колонка во всю ширину (ui-conventions). */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.gd-kpi-grid {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Карта Leaflet на узких экранах — гарантированная высота, чтобы не
|
||||||
|
* схлопнулась в 0 когда flex-родитель встаёт в колонку. */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.gd-map-shell {
|
||||||
|
min-height: 420px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section-nav сайдбара анализа: на планшете/телефоне (когда грид схлопнут)
|
||||||
|
* вертикальный список превращается в горизонтальную скролл-полоску со снапом —
|
||||||
|
* чтобы не съедать пол-экрана высоты над контентом (issue #66). */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.gd-section-nav {
|
||||||
|
flex-direction: row !important;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 8px 4px !important;
|
||||||
|
scroll-snap-type: x proximity;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
.gd-section-nav::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.gd-section-nav > * {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
.gd-section-nav button {
|
||||||
|
width: auto !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-left: 12px !important;
|
||||||
|
padding-right: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -699,6 +699,12 @@ export default function HomePage() {
|
||||||
>
|
>
|
||||||
ПКК Росреестра
|
ПКК Росреестра
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href="/data-sources"
|
||||||
|
style={{ color: "var(--accent)", textDecoration: "none" }}
|
||||||
|
>
|
||||||
|
Источники данных
|
||||||
|
</a>
|
||||||
<span>GenDesign {new Date().getFullYear()}</span>
|
<span>GenDesign {new Date().getFullYear()}</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,10 @@ export function AnalysisPageContent({ cad }: Props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Page layout: sidebar (240px) + main scroll */}
|
{/* Page layout: sidebar (240px) + main scroll. На планшете/телефоне
|
||||||
|
.gd-grid-sidebar схлопывает грид в одну колонку (issue #66). */}
|
||||||
<div
|
<div
|
||||||
|
className="gd-grid-sidebar"
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "240px 1fr",
|
gridTemplateColumns: "240px 1fr",
|
||||||
|
|
@ -146,6 +148,7 @@ export function AnalysisPageContent({ cad }: Props) {
|
||||||
>
|
>
|
||||||
{/* ── Sidebar nav ───────────────────────────────────────────────── */}
|
{/* ── Sidebar nav ───────────────────────────────────────────────── */}
|
||||||
<aside
|
<aside
|
||||||
|
className="gd-sidebar"
|
||||||
style={{
|
style={{
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: 72,
|
top: 72,
|
||||||
|
|
@ -280,6 +283,7 @@ function AnalysisSidebarNav() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
|
className="gd-section-nav"
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-card, #FFFFFF)",
|
background: "var(--bg-card, #FFFFFF)",
|
||||||
border: "1px solid var(--border-card, #E6E8EC)",
|
border: "1px solid var(--border-card, #E6E8EC)",
|
||||||
|
|
|
||||||
|
|
@ -165,8 +165,10 @@ export default function SiteFinderPage() {
|
||||||
<FilterBarBridge filters={filters} onChange={setFilters} bbox={bbox} />
|
<FilterBarBridge filters={filters} onChange={setFilters} bbox={bbox} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main layout */}
|
{/* Main layout. На планшете/телефоне .gd-split встаёт в колонку
|
||||||
|
(карта над сайдбаром), .gd-map-shell держит высоту карты (issue #66). */}
|
||||||
<div
|
<div
|
||||||
|
className="gd-split"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -177,6 +179,7 @@ export default function SiteFinderPage() {
|
||||||
>
|
>
|
||||||
{/* Map area — position: relative so ParcelDrawer can anchor absolutely here */}
|
{/* Map area — position: relative so ParcelDrawer can anchor absolutely here */}
|
||||||
<div
|
<div
|
||||||
|
className="gd-map-shell"
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
|
|
@ -204,6 +207,7 @@ export default function SiteFinderPage() {
|
||||||
|
|
||||||
{/* Right sidebar — always visible; CadInput accessible parallel with drawer */}
|
{/* Right sidebar — always visible; CadInput accessible parallel with drawer */}
|
||||||
<div
|
<div
|
||||||
|
className="gd-split-aside"
|
||||||
style={{
|
style={{
|
||||||
width: 300,
|
width: 300,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@ export function AnalyticsNav() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
|
className="gd-nav-scroll"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
borderBottom: "1px solid #e6e8ec",
|
borderBottom: "1px solid var(--border-card)",
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -30,12 +31,13 @@ export function AnalyticsNav() {
|
||||||
style={{
|
style={{
|
||||||
padding: "10px 14px",
|
padding: "10px 14px",
|
||||||
borderBottom: active
|
borderBottom: active
|
||||||
? "2px solid #1d4ed8"
|
? "2px solid var(--accent)"
|
||||||
: "2px solid transparent",
|
: "2px solid transparent",
|
||||||
color: active ? "#1d4ed8" : "#374151",
|
color: active ? "var(--accent)" : "var(--fg-secondary)",
|
||||||
fontWeight: active ? 600 : 500,
|
fontWeight: active ? 600 : 500,
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
|
import type { EChartsInstance } from "echarts-for-react";
|
||||||
|
|
||||||
const ReactECharts = dynamic(() => import("echarts-for-react"), { ssr: false });
|
const ReactECharts = dynamic(() => import("echarts-for-react"), { ssr: false });
|
||||||
|
|
||||||
|
|
@ -20,13 +22,37 @@ export function ChartShell({
|
||||||
loading,
|
loading,
|
||||||
notMerge,
|
notMerge,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const wrapRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const instanceRef = useRef<EChartsInstance | null>(null);
|
||||||
|
|
||||||
|
// ECharts canvas only reflows on window.resize by default. On mobile/tablet
|
||||||
|
// the chart container width changes via CSS (sidebar grid collapse, flex
|
||||||
|
// column) WITHOUT a window resize, so the canvas keeps its desktop width and
|
||||||
|
// overflows. A ResizeObserver on the wrapper resizes the instance whenever its
|
||||||
|
// box changes — covers every chart that goes through ChartShell (issue #66).
|
||||||
|
// The instance is captured via onChartReady (a plain prop) rather than a ref,
|
||||||
|
// because next/dynamic does not forward refs to the wrapped component.
|
||||||
|
useEffect(() => {
|
||||||
|
const el = wrapRef.current;
|
||||||
|
if (el == null || typeof ResizeObserver === "undefined") return;
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(() => {
|
||||||
|
instanceRef.current?.resize();
|
||||||
|
});
|
||||||
|
ro.observe(el);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", height, ...style }}>
|
<div ref={wrapRef} style={{ width: "100%", height, ...style }}>
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
option={option}
|
option={option}
|
||||||
style={{ height: "100%", width: "100%" }}
|
style={{ height: "100%", width: "100%" }}
|
||||||
showLoading={loading}
|
showLoading={loading}
|
||||||
notMerge={notMerge}
|
notMerge={notMerge}
|
||||||
|
onChartReady={(instance: EChartsInstance) => {
|
||||||
|
instanceRef.current = instance;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue