ui: toggles, stat tiles, dropdown search, bubble scale slider (v0.2.12)
Modernised several controls (reference: easystudy ui_elements catalogue): - Display checkboxes (Бренд/Компания/Коллизии/Все поверхности) are now toggle switches; the option checkboxes inside filter dropdowns stay normal. - Counters became stat tiles: header "Поверхности N" (accent) and the map legend (свободно/занято/всего + dashed "без координат"). - Filter dropdowns gained an in-panel search that filters the option list (shown for lists >7, auto-focused on open). - Timeline scale control restyled as a filled slider with a value bubble, moved out of the header to float over the top-right of the chart just below the time axis (5px gap), bubble pointing up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d7c1ce7b77
commit
f5166b888a
@ -40,7 +40,7 @@
|
|||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="brand-name">Green Media</div>
|
<div class="brand-name">Green Media</div>
|
||||||
<div class="brand-sub">Адресная программа</div>
|
<div class="brand-sub">Адресная программа</div>
|
||||||
<div id="status"></div>
|
<div id="status" class="stat-tile stat-accent"><span class="st-l">Поверхности</span><span class="st-v">…</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -120,14 +120,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Group 3: scale + appearance + checkboxes (compact stack) -->
|
<!-- Group 3: display toggles (the scale control now floats over the timeline) -->
|
||||||
<div class="hgroup hgroup-controls">
|
<div class="hgroup hgroup-controls">
|
||||||
<div class="field zoom-field">
|
|
||||||
<label for="zoom-slider">Масштаб графика: <span id="zoom-label">12М</span></label>
|
|
||||||
<div class="zoom-row">
|
|
||||||
<input type="range" id="zoom-slider" min="0" max="4" step="1" value="1" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="checks manager-only">
|
<div class="checks manager-only">
|
||||||
<div class="checks-row">
|
<div class="checks-row">
|
||||||
<div class="field checkbox-field">
|
<div class="field checkbox-field">
|
||||||
@ -164,6 +158,13 @@
|
|||||||
<div id="pane-timeline">
|
<div id="pane-timeline">
|
||||||
<div id="chart-wrap">
|
<div id="chart-wrap">
|
||||||
<div id="timeline"></div>
|
<div id="timeline"></div>
|
||||||
|
<div class="field zoom-field" id="zoom-field">
|
||||||
|
<label for="zoom-slider">Масштаб графика</label>
|
||||||
|
<div class="zoom-row">
|
||||||
|
<span id="zoom-label" class="zoom-bubble">12М</span>
|
||||||
|
<input type="range" id="zoom-slider" min="0" max="4" step="1" value="1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="col-resizer" title="Потяните, чтобы изменить ширину колонки"></div>
|
<div id="col-resizer" title="Потяните, чтобы изменить ширину колонки"></div>
|
||||||
<div id="empty" class="empty-state" style="display:none">
|
<div id="empty" class="empty-state" style="display:none">
|
||||||
<svg class="empty-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
<svg class="empty-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
|
||||||
@ -177,10 +178,10 @@
|
|||||||
<div id="pane-map">
|
<div id="pane-map">
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<div id="map-legend">
|
<div id="map-legend">
|
||||||
<span class="lg-item"><span class="lg-dot lg-free"></span><span id="lg-free">свободна</span></span>
|
<div class="stat-tile"><span class="st-l"><span class="lg-dot lg-free"></span>свободно</span><span class="st-v st-free" id="lg-free">0</span></div>
|
||||||
<span class="lg-item"><span class="lg-dot lg-busy"></span><span id="lg-busy">занята</span></span>
|
<div class="stat-tile"><span class="st-l"><span class="lg-dot lg-busy"></span>занято</span><span class="st-v st-busy" id="lg-busy">0</span></div>
|
||||||
<span class="lg-item" id="map-counter"></span>
|
<div class="stat-tile"><span class="st-l"><span class="lg-dot lg-total"></span>всего</span><span class="st-v" id="map-counter">0</span></div>
|
||||||
<button type="button" class="lg-nocoords" id="map-nocoords-btn" style="display:none"></button>
|
<button type="button" class="stat-tile st-nc" id="map-nocoords-btn" style="display:none"><span class="st-l"><span class="lg-dot"></span>без координат</span><span class="st-v">0</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="map-nocoords-panel" style="display:none"></div>
|
<div id="map-nocoords-panel" style="display:none"></div>
|
||||||
<div id="map-empty" class="empty-state" style="display:none">
|
<div id="map-empty" class="empty-state" style="display:none">
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mapdash-frontend",
|
"name": "mapdash-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.11",
|
"version": "0.2.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -42,6 +42,30 @@ export function createDropdown(id: string, formatLabel: (v: string) => string =
|
|||||||
|
|
||||||
function renderPanel(): void {
|
function renderPanel(): void {
|
||||||
panel.innerHTML = '';
|
panel.innerHTML = '';
|
||||||
|
|
||||||
|
// Show all available options, plus any currently-selected value that is no
|
||||||
|
// longer among them (so a selection made incompatible by other filters
|
||||||
|
// stays visible and can still be unchecked).
|
||||||
|
const extra = Array.from(selected).filter((v) => !allValues.includes(v));
|
||||||
|
const display = allValues.concat(extra);
|
||||||
|
|
||||||
|
const optionEls: { v: string; el: HTMLElement }[] = [];
|
||||||
|
// Search box — filters the option list in place (only for longer lists).
|
||||||
|
if (display.length > 7) {
|
||||||
|
const search = document.createElement('input');
|
||||||
|
search.type = 'text';
|
||||||
|
search.className = 'dp-search';
|
||||||
|
search.placeholder = 'Поиск…';
|
||||||
|
search.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
search.addEventListener('input', () => {
|
||||||
|
const q = search.value.trim().toLowerCase();
|
||||||
|
for (const o of optionEls) {
|
||||||
|
o.el.style.display = !q || formatLabel(o.v).toLowerCase().includes(q) ? '' : 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
panel.appendChild(search);
|
||||||
|
}
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'dp-actions';
|
actions.className = 'dp-actions';
|
||||||
const selAll = document.createElement('a');
|
const selAll = document.createElement('a');
|
||||||
@ -64,11 +88,6 @@ export function createDropdown(id: string, formatLabel: (v: string) => string =
|
|||||||
actions.appendChild(selNone);
|
actions.appendChild(selNone);
|
||||||
panel.appendChild(actions);
|
panel.appendChild(actions);
|
||||||
|
|
||||||
// Show all available options, plus any currently-selected value that is no
|
|
||||||
// longer among them (so a selection made incompatible by other filters
|
|
||||||
// stays visible and can still be unchecked).
|
|
||||||
const extra = Array.from(selected).filter((v) => !allValues.includes(v));
|
|
||||||
const display = allValues.concat(extra);
|
|
||||||
for (const v of display) {
|
for (const v of display) {
|
||||||
const label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
const cb = document.createElement('input');
|
const cb = document.createElement('input');
|
||||||
@ -83,6 +102,7 @@ export function createDropdown(id: string, formatLabel: (v: string) => string =
|
|||||||
});
|
});
|
||||||
label.appendChild(cb);
|
label.appendChild(cb);
|
||||||
label.appendChild(document.createTextNode(formatLabel(v)));
|
label.appendChild(document.createTextNode(formatLabel(v)));
|
||||||
|
optionEls.push({ v, el: label });
|
||||||
panel.appendChild(label);
|
panel.appendChild(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,6 +113,14 @@ export function createDropdown(id: string, formatLabel: (v: string) => string =
|
|||||||
if (d !== root) d.classList.remove('open');
|
if (d !== root) d.classList.remove('open');
|
||||||
});
|
});
|
||||||
root.classList.toggle('open');
|
root.classList.toggle('open');
|
||||||
|
if (root.classList.contains('open')) {
|
||||||
|
const s = panel.querySelector<HTMLInputElement>('.dp-search');
|
||||||
|
if (s) {
|
||||||
|
s.value = '';
|
||||||
|
s.dispatchEvent(new Event('input')); // reset any prior filtering
|
||||||
|
s.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
if (!root.contains(e.target as Node)) root.classList.remove('open');
|
if (!root.contains(e.target as Node)) root.classList.remove('open');
|
||||||
|
|||||||
@ -324,7 +324,7 @@ el.emptyResetTimeline.addEventListener('click', resetFilters);
|
|||||||
el.emptyResetMap.addEventListener('click', resetFilters);
|
el.emptyResetMap.addEventListener('click', resetFilters);
|
||||||
|
|
||||||
async function loadData(fit: boolean): Promise<void> {
|
async function loadData(fit: boolean): Promise<void> {
|
||||||
el.status.textContent = 'Загрузка…';
|
el.status.innerHTML = '<span class="st-l">Поверхности</span><span class="st-v">…</span>';
|
||||||
loadStart();
|
loadStart();
|
||||||
try {
|
try {
|
||||||
const f = currentFilters();
|
const f = currentFilters();
|
||||||
@ -338,7 +338,7 @@ async function loadData(fit: boolean): Promise<void> {
|
|||||||
lastBoards = boards;
|
lastBoards = boards;
|
||||||
lastBookings = bookings;
|
lastBookings = bookings;
|
||||||
view.render(boards, bookings, fit, flags());
|
view.render(boards, bookings, fit, flags());
|
||||||
el.status.textContent = `Поверхности: ${boards.length}`;
|
el.status.innerHTML = `<span class="st-l">Поверхности</span><span class="st-v">${boards.length}</span>`;
|
||||||
rebuildDecorMenu(); // status list may have changed
|
rebuildDecorMenu(); // status list may have changed
|
||||||
renderChips();
|
renderChips();
|
||||||
filtersToUrl();
|
filtersToUrl();
|
||||||
@ -422,11 +422,22 @@ el.search.addEventListener('input', scheduleReload);
|
|||||||
el.dateStart.addEventListener('change', scheduleReload);
|
el.dateStart.addEventListener('change', scheduleReload);
|
||||||
el.dateEnd.addEventListener('change', scheduleReload);
|
el.dateEnd.addEventListener('change', scheduleReload);
|
||||||
|
|
||||||
|
// Fill the track up to the thumb and ride the value bubble above the thumb.
|
||||||
|
function updateZoomUi(): void {
|
||||||
|
const max = parseInt(el.zoomSlider.max, 10) || 1;
|
||||||
|
const frac = max ? parseInt(el.zoomSlider.value, 10) / max : 0;
|
||||||
|
el.zoomSlider.style.setProperty('--zoom-fill', frac * 100 + '%');
|
||||||
|
const thumb = 16;
|
||||||
|
const w = el.zoomSlider.offsetWidth || 120;
|
||||||
|
el.zoomLabel.style.left = thumb / 2 + frac * (w - thumb) + 'px';
|
||||||
|
}
|
||||||
el.zoomSlider.addEventListener('input', () => {
|
el.zoomSlider.addEventListener('input', () => {
|
||||||
const step = zoomSteps[parseInt(el.zoomSlider.value, 10)]!;
|
const step = zoomSteps[parseInt(el.zoomSlider.value, 10)]!;
|
||||||
el.zoomLabel.textContent = step.label;
|
el.zoomLabel.textContent = step.label;
|
||||||
view.setScale(step.days);
|
view.setScale(step.days);
|
||||||
|
updateZoomUi();
|
||||||
});
|
});
|
||||||
|
window.addEventListener('resize', updateZoomUi);
|
||||||
|
|
||||||
// Clicking a surface in the timeline's left column flies the map to it: switch to
|
// Clicking a surface in the timeline's left column flies the map to it: switch to
|
||||||
// the combined "Карта и график" view if only the timeline is open, then focus the
|
// the combined "Карта и график" view if only the timeline is open, then focus the
|
||||||
@ -458,6 +469,7 @@ async function init(): Promise<void> {
|
|||||||
el.zoomSlider.value = String(defaultZoomIdx);
|
el.zoomSlider.value = String(defaultZoomIdx);
|
||||||
el.zoomLabel.textContent = zoomSteps[defaultZoomIdx]!.label;
|
el.zoomLabel.textContent = zoomSteps[defaultZoomIdx]!.label;
|
||||||
view.setScale(zoomSteps[defaultZoomIdx]!.days);
|
view.setScale(zoomSteps[defaultZoomIdx]!.days);
|
||||||
|
updateZoomUi();
|
||||||
// Apply the persisted view mode (inits the map if it starts visible).
|
// Apply the persisted view mode (inits the map if it starts visible).
|
||||||
void onModeChange(viewModes.getMode());
|
void onModeChange(viewModes.getMode());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -476,17 +476,19 @@ export function createMapView(els: MapElements, apiKey: string): MapView {
|
|||||||
objectManager.removeAll();
|
objectManager.removeAll();
|
||||||
objectManager.add({ type: 'FeatureCollection', features });
|
objectManager.add({ type: 'FeatureCollection', features });
|
||||||
|
|
||||||
// Live legend counts (react to the current filter).
|
// Live legend counts (react to the current filter) — values only; the labels
|
||||||
if (legendFree) legendFree.textContent = `свободно: ${free}`;
|
// are static in the tile markup.
|
||||||
if (legendBusy) legendBusy.textContent = `занято: ${busy}`;
|
if (legendFree) legendFree.textContent = String(free);
|
||||||
|
if (legendBusy) legendBusy.textContent = String(busy);
|
||||||
// Total counts the whole inventory, including surfaces without coordinates
|
// Total counts the whole inventory, including surfaces without coordinates
|
||||||
// (which can't be drawn but are still surfaces — listed under "без координат").
|
// (which can't be drawn but are still surfaces — listed under "без координат").
|
||||||
els.counter.textContent = `всего: ${surfaces.length}`;
|
els.counter.textContent = String(surfaces.length);
|
||||||
|
|
||||||
// Actionable "no coordinates" badge + list — manager-only.
|
// Actionable "no coordinates" tile + list — manager-only.
|
||||||
if (noCoordsBtn) {
|
if (noCoordsBtn) {
|
||||||
noCoordsBtn.style.display = managerView && noCoordsList.length ? '' : 'none';
|
noCoordsBtn.style.display = managerView && noCoordsList.length ? '' : 'none';
|
||||||
noCoordsBtn.textContent = `без координат: ${noCoordsList.length}`;
|
const ncVal = noCoordsBtn.querySelector<HTMLElement>('.st-v');
|
||||||
|
if (ncVal) ncVal.textContent = String(noCoordsList.length);
|
||||||
}
|
}
|
||||||
if (noCoordsPanel) {
|
if (noCoordsPanel) {
|
||||||
noCoordsPanel.innerHTML = renderNoCoords(noCoordsList);
|
noCoordsPanel.innerHTML = renderNoCoords(noCoordsList);
|
||||||
|
|||||||
@ -179,12 +179,7 @@ input::placeholder { color: var(--text-muted); }
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Counters — compact inset inside the brand block. */
|
/* Counters — compact inset inside the brand block. */
|
||||||
.brand #status {
|
.brand #status { margin-top: auto; }
|
||||||
margin-top: auto; padding: 8px 12px;
|
|
||||||
background: var(--surface-2); border: none;
|
|
||||||
border-radius: var(--radius-sm); font-size: 12px; color: var(--text-muted);
|
|
||||||
line-height: 1.5; white-space: nowrap; font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scale group: stack the "Оформление" button under the zoom slider. */
|
/* Scale group: stack the "Оформление" button under the zoom slider. */
|
||||||
.checkbox-field { flex-direction: row; align-items: center; gap: 6px; align-self: center; }
|
.checkbox-field { flex-direction: row; align-items: center; gap: 6px; align-self: center; }
|
||||||
@ -193,11 +188,57 @@ input::placeholder { color: var(--text-muted); }
|
|||||||
font-size: 13px; color: var(--text); text-transform: none; letter-spacing: normal;
|
font-size: 13px; color: var(--text); text-transform: none; letter-spacing: normal;
|
||||||
font-weight: 400; cursor: pointer; white-space: nowrap;
|
font-weight: 400; cursor: pointer; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.checkbox-field input[type=checkbox] { cursor: pointer; accent-color: var(--accent); }
|
/* Toggle switch: the checkbox keeps its semantics/JS, only the look changes.
|
||||||
|
Scoped to .checkbox-field so the dropdown option checkboxes stay normal. */
|
||||||
|
.checkbox-field input[type=checkbox] {
|
||||||
|
appearance: none; -webkit-appearance: none; margin: 0; flex: 0 0 auto;
|
||||||
|
width: 34px; height: 20px; border-radius: 20px; background: #cbd0d8;
|
||||||
|
position: relative; cursor: pointer; transition: background var(--transition);
|
||||||
|
}
|
||||||
|
.checkbox-field input[type=checkbox]::after {
|
||||||
|
content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
|
||||||
|
border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, .25);
|
||||||
|
transition: left var(--transition);
|
||||||
|
}
|
||||||
|
.checkbox-field input[type=checkbox]:checked { background: var(--accent); }
|
||||||
|
.checkbox-field input[type=checkbox]:checked::after { left: 16px; }
|
||||||
|
.checkbox-field input[type=checkbox]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
|
||||||
.zoom-field { gap: 4px; }
|
.zoom-field { gap: 4px; }
|
||||||
.zoom-field input[type=range] { width: 100px; cursor: pointer; accent-color: var(--accent); }
|
|
||||||
.zoom-field label { white-space: nowrap; }
|
.zoom-field label { white-space: nowrap; }
|
||||||
|
.zoom-field .zoom-row { position: relative; width: 120px; padding-bottom: 24px; display: block; }
|
||||||
|
/* The scale control floats over the top-right of the timeline, just below the
|
||||||
|
time axis (JS sets `top` = axis bottom + 5px; this is only a fallback). */
|
||||||
|
#chart-wrap #zoom-field {
|
||||||
|
position: absolute; top: 52px; right: 16px; z-index: 20;
|
||||||
|
background: rgba(255, 255, 255, .92); border-radius: 8px;
|
||||||
|
padding: 3px 12px 4px; box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
.zoom-field input[type=range] {
|
||||||
|
-webkit-appearance: none; appearance: none; width: 100%; height: 6px; margin: 0;
|
||||||
|
border-radius: 4px; cursor: pointer; outline: none;
|
||||||
|
background: linear-gradient(to right, var(--accent) var(--zoom-fill, 0%), var(--border-strong) var(--zoom-fill, 0%));
|
||||||
|
}
|
||||||
|
.zoom-field input[type=range]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%;
|
||||||
|
background: #fff; border: 2px solid var(--accent); box-shadow: 0 1px 3px rgba(0, 0, 0, .3); cursor: pointer;
|
||||||
|
}
|
||||||
|
.zoom-field input[type=range]::-moz-range-thumb {
|
||||||
|
width: 16px; height: 16px; border-radius: 50%; background: #fff;
|
||||||
|
border: 2px solid var(--accent); box-shadow: 0 1px 3px rgba(0, 0, 0, .3); cursor: pointer;
|
||||||
|
}
|
||||||
|
.zoom-field input[type=range]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
||||||
|
/* Value bubble that rides below the slider thumb, pointing up (JS sets `left`). */
|
||||||
|
.zoom-bubble {
|
||||||
|
position: absolute; bottom: 0; top: auto; left: 0; transform: translateX(-50%);
|
||||||
|
background: var(--accent); color: #fff; font-size: 11px; font-weight: 600;
|
||||||
|
padding: 2px 8px; border-radius: 6px; white-space: nowrap; pointer-events: none;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.zoom-bubble::after {
|
||||||
|
content: ""; position: absolute; top: -4px; bottom: auto; left: 50%; transform: translateX(-50%);
|
||||||
|
border: 4px solid transparent; border-bottom-color: var(--accent); border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* checkbox dropdown filter */
|
/* checkbox dropdown filter */
|
||||||
.dropdown { position: relative; }
|
.dropdown { position: relative; }
|
||||||
@ -233,6 +274,14 @@ input::placeholder { color: var(--text-muted); }
|
|||||||
}
|
}
|
||||||
.dropdown-panel .dp-actions a { font-size: 11px; color: var(--accent); cursor: pointer; text-decoration: none; font-weight: 600; }
|
.dropdown-panel .dp-actions a { font-size: 11px; color: var(--accent); cursor: pointer; text-decoration: none; font-weight: 600; }
|
||||||
.dropdown-panel .dp-actions a:hover { text-decoration: underline; }
|
.dropdown-panel .dp-actions a:hover { text-decoration: underline; }
|
||||||
|
/* In-dropdown search that filters the option list. */
|
||||||
|
.dropdown-panel .dp-search {
|
||||||
|
display: block; width: 100%; box-sizing: border-box; padding: 8px 12px;
|
||||||
|
font-size: 13px; color: var(--text); background: var(--bg);
|
||||||
|
border: none; border-bottom: 1px solid var(--border); outline: none;
|
||||||
|
}
|
||||||
|
.dropdown-panel .dp-search::placeholder { color: var(--text-muted); }
|
||||||
|
.dropdown-panel .dp-search:focus { border-bottom-color: var(--accent); }
|
||||||
|
|
||||||
/* Оформление button + context menu */
|
/* Оформление button + context menu */
|
||||||
/* Pinned to the header's bottom-right corner. */
|
/* Pinned to the header's bottom-right corner. */
|
||||||
@ -378,26 +427,34 @@ html.role-pending .manager-only { display: none !important; }
|
|||||||
/* Donut cluster icon (built in map.ts) — centre the 60px svg on the geo point. */
|
/* Donut cluster icon (built in map.ts) — centre the 60px svg on the geo point. */
|
||||||
.mapdash-cluster { width: 60px; height: 60px; margin: -30px 0 0 -30px; line-height: 0; cursor: pointer; }
|
.mapdash-cluster { width: 60px; height: 60px; margin: -30px 0 0 -30px; line-height: 0; cursor: pointer; }
|
||||||
|
|
||||||
|
/* ---- stat tiles (header counter + map legend) ---- */
|
||||||
|
.stat-tile {
|
||||||
|
display: inline-flex; flex-direction: column; align-items: flex-start; gap: 1px;
|
||||||
|
background: var(--surface); border-radius: 9px; padding: 5px 11px;
|
||||||
|
font-variant-numeric: tabular-nums; border: none;
|
||||||
|
}
|
||||||
|
.st-l { font-size: 11px; color: var(--text-muted); display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
||||||
|
.st-v { font-size: 18px; font-weight: 600; line-height: 1.15; color: var(--text); }
|
||||||
|
.stat-accent { background: #eef0fe; }
|
||||||
|
.stat-accent .st-v { color: var(--accent); }
|
||||||
|
.st-v.st-free { color: #2f9e44; }
|
||||||
|
.st-v.st-busy { color: #d64545; }
|
||||||
|
|
||||||
#map-legend {
|
#map-legend {
|
||||||
position: absolute; left: 10px; bottom: 44px; z-index: 5;
|
position: absolute; left: 10px; bottom: 44px; z-index: 5;
|
||||||
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px;
|
||||||
padding: var(--size-2) var(--size-3); font-size: 12px; color: var(--text);
|
|
||||||
display: flex; flex-direction: column; align-items: flex-start; gap: var(--size-1);
|
|
||||||
box-shadow: var(--shadow-md); font-variant-numeric: tabular-nums;
|
|
||||||
}
|
}
|
||||||
#map-legend .lg-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; font-size: 12px; }
|
#map-legend .stat-tile { box-shadow: var(--shadow-md); }
|
||||||
#map-legend #map-counter { padding-left: 18px; } /* align "всего" with the dotted rows */
|
#map-legend .lg-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
||||||
#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-free { background: #37b24d; }
|
||||||
#map-legend .lg-busy { background: #e24b4a; }
|
#map-legend .lg-busy { background: #e24b4a; }
|
||||||
/* Actionable "no coordinates" badge inside the legend. */
|
#map-legend .lg-total { background: var(--border-strong); }
|
||||||
.lg-nocoords {
|
/* Actionable "no coordinates" tile — dashed accent, clickable. */
|
||||||
border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface);
|
.st-nc { cursor: pointer; background: #fbf6e9; border: 1px dashed #d7c08a; padding: 4px 10px; text-align: left;
|
||||||
color: var(--text-muted); font-size: 12px; padding: 2px 8px; cursor: pointer; white-space: nowrap;
|
transition: border-color var(--transition); }
|
||||||
transition: border-color var(--transition), color var(--transition);
|
.st-nc .lg-dot { background: #e8a317; }
|
||||||
}
|
.st-nc .st-v { color: #b07d16; }
|
||||||
.lg-nocoords:hover { border-color: var(--border-strong); color: var(--text); }
|
.st-nc:hover { border-color: #b07d16; }
|
||||||
/* Panel listing surfaces missing coordinates. */
|
/* Panel listing surfaces missing coordinates. */
|
||||||
#map-nocoords-panel {
|
#map-nocoords-panel {
|
||||||
position: absolute; left: 10px; bottom: 150px; z-index: 6;
|
position: absolute; left: 10px; bottom: 150px; z-index: 6;
|
||||||
|
|||||||
@ -231,6 +231,18 @@ export function createTimelineView(el: TimelineElements, appearance: Appearance)
|
|||||||
timeline.on('changed', positionResizer);
|
timeline.on('changed', positionResizer);
|
||||||
window.addEventListener('resize', positionResizer);
|
window.addEventListener('resize', positionResizer);
|
||||||
|
|
||||||
|
// Keep the floating scale control just below the time axis (5px gap) so it
|
||||||
|
// never overlaps the axis; the axis height varies, so measure it each redraw.
|
||||||
|
function positionZoomField(): void {
|
||||||
|
const zf = document.getElementById('zoom-field');
|
||||||
|
const axis = el.timelineEl.querySelector<HTMLElement>('.vis-panel.vis-top');
|
||||||
|
if (!zf || !axis) return;
|
||||||
|
zf.style.top = axis.getBoundingClientRect().bottom - el.chartWrap.getBoundingClientRect().top + 5 + 'px';
|
||||||
|
}
|
||||||
|
timeline.on('changed', positionZoomField);
|
||||||
|
window.addEventListener('resize', positionZoomField);
|
||||||
|
positionZoomField();
|
||||||
|
|
||||||
let colDragging = false;
|
let colDragging = false;
|
||||||
let colDragStartX = 0;
|
let colDragStartX = 0;
|
||||||
let colDragStartW = 0;
|
let colDragStartW = 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user