From f0c53e2ce9abab3fcf93feaf5ce23ab213447be3 Mon Sep 17 00:00:00 2001 From: aaverbitskiy Date: Tue, 18 Aug 2026 05:33:30 +0000 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=8B=D0=B3=D1=80=D1=83=D0=B7=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=B0=D1=80=D1=82=D1=8B=20=D0=B8=D0=B7=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BC=D1=8F=D1=82=D0=B8=20+=20=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D1=8B=20=D1=81=D1=82=D0=BE=D0=BB=D0=B1=D1=86=D0=B0=20=D0=B0?= =?UTF-8?q?=D0=B4=D1=80=D0=B5=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) Карта выгружается (map.destroy()) через 2с после ухода в режим «График» — освобождает тайлы/canvas/GPU (~350–450 МБ); отменяется при возврате. Метод MapView.destroy() (сброс map/objectManager/initPromise/pending); в onModeChange сброс mapLoadedKey, иначе refreshMap короткозамыкал render и свежий ObjectManager оставался пустым. 2) MIN_LABEL_W 140→200 — три пилюли Город/Адрес/Код не обрезаются на узком столбце; container-query прячет подпись «Показывать» при ширине < 255px. 3) Правый/левый отступ 7px у содержимого строки (#timeline .vis-label .vis-inner, специфичность выше собственного padding vis-timeline) — адрес не липнет к границе столбца. Co-Authored-By: Claude Opus 4.8 --- frontend/package.json | 2 +- frontend/src/main.ts | 19 +++++++++++++++++-- frontend/src/map.ts | 17 +++++++++++++++++ frontend/src/styles.css | 9 ++++++++- frontend/src/timeline.ts | 2 +- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index d6e7e44..6a09775 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mapdash-frontend", "private": true, - "version": "1.0.1", + "version": "1.0.2", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 924b4da..3457542 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -420,10 +420,25 @@ async function refreshMap(): Promise { } } +let mapDestroyTimer = 0; async function onModeChange(mode: ViewMode): Promise { if (mode !== 'map') view.applyLayout(); // timeline visible (timeline or split) - if (mode === 'timeline') mapView.closePanel(); // map hidden — drop its floating card - if (mode !== 'timeline') await refreshMap(); // map visible (map or split) + if (mode === 'timeline') { + mapView.closePanel(); // map hidden — drop its floating card + // Free the Yandex map (tiles/canvas/GPU — the bulk of the tab's memory) a + // couple of seconds after it's closed; cancelled if a map view returns, so + // quick График⇄Карта toggles don't thrash a re-init. + window.clearTimeout(mapDestroyTimer); + mapDestroyTimer = window.setTimeout(() => { + mapView.destroy(); + // The fresh map/ObjectManager will be empty, so drop the "already loaded" + // key — the next open re-fetches + re-renders instead of short-circuiting. + mapLoadedKey = ''; + }, 2000); + } else { + window.clearTimeout(mapDestroyTimer); // map visible again — keep it + await refreshMap(); // map visible (map or split); ensureInit rebuilds if destroyed + } } const viewModes = createViewModes( diff --git a/frontend/src/map.ts b/frontend/src/map.ts index 56873de..b839e2d 100644 --- a/frontend/src/map.ts +++ b/frontend/src/map.ts @@ -63,6 +63,9 @@ export interface MapView { ensureInit(): Promise; invalidateSize(): void; closePanel(): void; + /** Destroy the Yandex map and free its tiles/canvas/GPU memory. The next + * ensureInit() re-creates it from scratch. */ + destroy(): void; /** Pan + zoom the map to the surface's location (its cluster). No-op if it has no coords. */ focusBoard(boardId: string): void; } @@ -633,6 +636,20 @@ export function createMapView(els: MapElements, apiKey: string): MapView { closePanel(): void { closeMapPanel(); }, + destroy(): void { + closeMapPanel(); + // map.destroy() tears down Yandex's DOM inside #map and releases its tile + // cache / canvas / GPU textures. Our legend/counter overlays are siblings + // of #map, so they survive. Reset state so ensureInit() rebuilds cleanly. + if (map) { + try { map.destroy(); } catch { /* already torn down */ } + } + map = null; + objectManager = null; + initPromise = null; + pendingFocus = null; + pending = null; + }, focusBoard(boardId: string): void { pendingFocus = boardId; applyFocus(); // applies now if already rendered, else waits for the next draw diff --git a/frontend/src/styles.css b/frontend/src/styles.css index ab07240..8434610 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -327,8 +327,11 @@ input::placeholder { color: var(--text-muted); } display: flex; align-items: center; gap: 6px; padding: 0 12px; box-sizing: border-box; overflow: hidden; background: var(--bg); border-bottom: 1px solid var(--border); + container-type: inline-size; } .ci-label { flex: 0 0 auto; font-size: 11px; color: var(--text-muted); white-space: nowrap; } +/* Narrow column: drop the "Показывать" label so the three pills still fit. */ +@container (max-width: 255px) { #chart-wrap #col-info .ci-label { display: none; } } .ci-toggle { flex: 0 0 auto; border: none; cursor: pointer; white-space: nowrap; font-size: 12px; font-weight: 500; padding: 5px 11px; border-radius: 999px; @@ -770,10 +773,14 @@ html.role-pending .manager-only { display: none !important; } /* Row hover */ .vis-label.row-hover, .vis-group.row-hover { background-color: var(--row-hover) !important; } /* Fixed/resizable label column */ -.vis-label .vis-inner { +/* #timeline lifts specificity above vis-timeline's own `.vis-labelset .vis-label + .vis-inner { padding: 5px }`, so the right padding actually applies. */ +#timeline .vis-label .vis-inner { display: inline-block; + box-sizing: border-box; min-width: var(--label-w); max-width: var(--label-w); + padding-left: 7px; padding-right: 7px; /* keep the address off the column borders */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; } diff --git a/frontend/src/timeline.ts b/frontend/src/timeline.ts index 8de4435..c4e6e81 100644 --- a/frontend/src/timeline.ts +++ b/frontend/src/timeline.ts @@ -54,7 +54,7 @@ export const zoomSteps: ReadonlyArray<{ days: number; label: string }> = [ const MONTH_BG_ID = '__month_hover_bg'; const CUR_MONTH_BG_ID = '__cur_month_bg'; -const MIN_LABEL_W = 140; +const MIN_LABEL_W = 200; // fits the three column-info pills (~188px) without clipping // Uniform 3-letter month labels (moment's ru `MMM` mixes full/abbreviated with // dots — "март", "май", "нояб.", "дек."). scale is 'month' at every zoom step.