map/header UI: unified marker colours, vertical legend, smart tooltip, click fixes
Map (map.ts, styles.css): - Single markers now use the same donut colours (islands#circleDotIcon + iconColor #37b24d / #e24b4a) instead of Yandex's brighter presets, so single and cluster markers match. - Legend is a vertical stack with relabelled live counts: "свободно / занято / всего", and the "без координат" badge hides when the count is zero. - Cluster tooltip shows the brand for occupied surfaces (coral); legend raised above the Yandex controls. - Hover tooltip: smart positioning — flips above/left of the cursor when it would overflow the viewport (bottom clusters no longer clip), max-height safety. - Bug fix: hide the hover tooltip on marker/cluster click — it no longer sticks and follows the cursor after a balloon opens. - Bug fix: clusterDisableClickZoom=true so a cluster click opens its balloon at any zoom instead of only at max zoom. Header (index.html, styles.css): - Scale + appearance + checkboxes recomposed into one compact block; the "Оформление" control is a round "+" button pinned to the header's bottom-right corner with a hover tooltip; counters relabelled "Поверхности / Брони"; the counters plate and view-mode switch share one borderless grey fill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a68cac80d1
commit
de0c369bb5
@ -6,10 +6,7 @@ import type { MapSurface } from './types';
|
||||
|
||||
const PLANFIX_TASK_URL = 'https://green-media.planfix.ru/task/';
|
||||
// Marker colour is intentionally "occupied today", independent of the date
|
||||
// filters. Kept as a single function so the colour scheme is easy to change later.
|
||||
function presetFor(s: MapSurface): string {
|
||||
return s.occupied_now ? 'islands#redCircleDotIcon' : 'islands#greenCircleDotIcon';
|
||||
}
|
||||
// filters. Single markers reuse the exact donut colours (below) via iconColor.
|
||||
function dot(occupied: boolean): string {
|
||||
return `<span class="tip-dot ${occupied ? 'tip-busy' : 'tip-free'}"></span>`;
|
||||
}
|
||||
@ -174,8 +171,23 @@ export function createMapView(els: MapElements, apiKey: string): MapView {
|
||||
let mx = 0;
|
||||
let my = 0;
|
||||
function positionTip(): void {
|
||||
tip.style.left = mx + 14 + 'px';
|
||||
tip.style.top = my + 14 + 'px';
|
||||
const pad = 14;
|
||||
const margin = 6;
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const r = tip.getBoundingClientRect();
|
||||
const tw = r.width || 260;
|
||||
const th = r.height || 120;
|
||||
// Horizontal: to the right of the cursor, flip left if it would overflow.
|
||||
let left = mx + pad;
|
||||
if (left + tw > vw - margin) left = mx - pad - tw;
|
||||
left = Math.max(margin, Math.min(left, vw - tw - margin));
|
||||
// Vertical: below the cursor, flip above if it would overflow the bottom.
|
||||
let top = my + pad;
|
||||
if (top + th > vh - margin) top = my - pad - th;
|
||||
top = Math.max(margin, Math.min(top, vh - th - margin));
|
||||
tip.style.left = left + 'px';
|
||||
tip.style.top = top + 'px';
|
||||
}
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
mx = e.clientX;
|
||||
@ -207,16 +219,17 @@ export function createMapView(els: MapElements, apiKey: string): MapView {
|
||||
balloonContentBody: balloonBody(s),
|
||||
occupied: s.occupied_now,
|
||||
},
|
||||
options: { preset: presetFor(s) },
|
||||
// Same colours as the donut cluster (islands#circleDotIcon is colourable).
|
||||
options: { preset: 'islands#circleDotIcon', iconColor: s.occupied_now ? CLUSTER_BUSY : CLUSTER_FREE },
|
||||
}));
|
||||
hideTip();
|
||||
objectManager.removeAll();
|
||||
objectManager.add({ type: 'FeatureCollection', features });
|
||||
|
||||
// Live legend counts (react to the current filter).
|
||||
if (legendFree) legendFree.textContent = `свободна: ${free}`;
|
||||
if (legendBusy) legendBusy.textContent = `занята: ${busy}`;
|
||||
els.counter.textContent = `поверхностей: ${withCoords.length}`;
|
||||
if (legendFree) legendFree.textContent = `свободно: ${free}`;
|
||||
if (legendBusy) legendBusy.textContent = `занято: ${busy}`;
|
||||
els.counter.textContent = `всего: ${withCoords.length}`;
|
||||
|
||||
// Actionable "no coordinates" badge + list.
|
||||
if (noCoordsBtn) {
|
||||
@ -289,7 +302,8 @@ export function createMapView(els: MapElements, apiKey: string): MapView {
|
||||
objectManager = new ymaps.ObjectManager({
|
||||
clusterize: true,
|
||||
gridSize: 112,
|
||||
clusterDisableClickZoom: false,
|
||||
// Click opens the balloon at any zoom (instead of zooming into the cluster).
|
||||
clusterDisableClickZoom: true,
|
||||
clusterIconLayout: DonutClusterLayout,
|
||||
// Hit area for hover/click — a circle centred on the geo point matching
|
||||
// the 60px donut (radius 30). Without a shape the icon is inert.
|
||||
@ -310,6 +324,11 @@ export function createMapView(els: MapElements, apiKey: string): MapView {
|
||||
if (list.length) showTip(clusterTip(list));
|
||||
});
|
||||
objectManager.clusters.events.add(['mouseleave'], hideTip);
|
||||
// Hide the hover tooltip when a balloon opens — otherwise mouseleave may
|
||||
// not fire (the balloon covers the marker) and the tip stays stuck,
|
||||
// following the cursor across the whole map.
|
||||
objectManager.objects.events.add(['click'], hideTip);
|
||||
objectManager.clusters.events.add(['click'], hideTip);
|
||||
|
||||
if (pending) {
|
||||
draw(pending);
|
||||
|
||||
@ -343,10 +343,13 @@ input::placeholder { color: var(--text-muted); }
|
||||
#map-legend {
|
||||
position: absolute; left: 10px; bottom: 44px; z-index: 5;
|
||||
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 7px 11px; font-size: 12px; color: var(--text); display: flex; gap: 14px; align-items: center;
|
||||
padding: 8px 12px; font-size: 12px; color: var(--text);
|
||||
display: flex; flex-direction: column; align-items: flex-start; gap: 5px;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
#map-legend .lg-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||
#map-legend .lg-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; font-size: 12px; }
|
||||
#map-legend #map-counter { padding-left: 18px; } /* align "всего" with the dotted rows */
|
||||
#map-legend .lg-nocoords { margin-top: 2px; font-size: 12px; }
|
||||
#map-legend .lg-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
|
||||
#map-legend .lg-free { background: #37b24d; }
|
||||
#map-legend .lg-busy { background: #e24b4a; }
|
||||
@ -359,7 +362,7 @@ input::placeholder { color: var(--text-muted); }
|
||||
.lg-nocoords:hover { border-color: var(--border-strong); color: var(--text); }
|
||||
/* Panel listing surfaces missing coordinates. */
|
||||
#map-nocoords-panel {
|
||||
position: absolute; left: 10px; bottom: 86px; z-index: 6;
|
||||
position: absolute; left: 10px; bottom: 150px; z-index: 6;
|
||||
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-md); padding: 8px 10px; font-size: 12px; color: var(--text);
|
||||
max-width: 300px; max-height: 300px; overflow-y: auto;
|
||||
@ -397,6 +400,7 @@ input::placeholder { color: var(--text-muted); }
|
||||
position: fixed; z-index: 1000; pointer-events: none;
|
||||
background: var(--tip-bg); color: var(--tip-text); font-size: 12px; line-height: 1.35;
|
||||
padding: 8px 10px; border-radius: var(--radius-sm); max-width: 340px;
|
||||
max-height: calc(100vh - 24px); overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.map-tip .tip-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 6px; vertical-align: baseline; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user