From 9b6ca6f848dc422eaf53d39b9448bea84323cb2a Mon Sep 17 00:00:00 2001 From: aaverbitskiy Date: Fri, 14 Aug 2026 19:09:47 +0000 Subject: [PATCH] polish: appearance menu offsets, anon map badge, bar label overflow (v0.2.4) - appearance menu opens 20px below the header/chips and 58px from the right edge - map "no coordinates" badge is manager-only (hidden for anonymous visitors) - timeline bar label that is wider than its bar now renders just past the bar's right edge as dark text on the track, instead of being clipped; labels that fit still sit inside the bar in white. Applied post-render (measure overflow), re-run on changed/rangechanged with a double rAF so bar widths are settled. Co-Authored-By: Claude Opus 4.8 --- frontend/package.json | 2 +- frontend/src/main.ts | 4 ++-- frontend/src/map.ts | 15 ++++++++++----- frontend/src/styles.css | 15 ++++++++++++--- frontend/src/timeline.ts | 29 ++++++++++++++++++++++++++++- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index e17358e..3d26b8c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mapdash-frontend", "private": true, - "version": "0.2.3", + "version": "0.2.4", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 1a71496..10482a2 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -190,7 +190,7 @@ function positionDecorMenu(): void { if (chips && chips.style.display !== 'none' && chips.getClientRects().length) { refBottom = Math.max(refBottom, chips.getBoundingClientRect().bottom); } - const top = refBottom + 25; + const top = refBottom + 20; el.decorMenu.style.top = top + 'px'; el.decorMenu.style.maxHeight = Math.max(120, window.innerHeight - top - 30) + 'px'; } @@ -370,7 +370,7 @@ async function refreshMap(): Promise { if (key === mapLoadedKey) return; mapLoadedKey = key; const surfaces = await api.map(currentFilters()); - mapView.render(surfaces); + mapView.render(surfaces, isManager()); } catch (e) { console.error('map load failed', e); } finally { diff --git a/frontend/src/map.ts b/frontend/src/map.ts index 3e37ecf..fb68d7b 100644 --- a/frontend/src/map.ts +++ b/frontend/src/map.ts @@ -59,7 +59,7 @@ export interface MapElements { } export interface MapView { - render(surfaces: MapSurface[]): void; + render(surfaces: MapSurface[], managerView: boolean): void; ensureInit(): Promise; invalidateSize(): void; } @@ -203,6 +203,10 @@ export function createMapView(els: MapElements, apiKey: string): MapView { tip.style.display = 'none'; } + // Whether the current viewer is a manager. The "no coordinates" badge is a + // manager-only tool, so it stays hidden for anonymous visitors. + let managerView = false; + function draw(surfaces: MapSurface[]): void { if (!objectManager) return; const withCoords = surfaces.filter((s) => s.lat != null && s.lon != null); @@ -233,14 +237,14 @@ export function createMapView(els: MapElements, apiKey: string): MapView { // (which can't be drawn but are still surfaces — listed under "без координат"). els.counter.textContent = `всего: ${surfaces.length}`; - // Actionable "no coordinates" badge + list. + // Actionable "no coordinates" badge + list — manager-only. if (noCoordsBtn) { - noCoordsBtn.style.display = noCoordsList.length ? '' : 'none'; + noCoordsBtn.style.display = managerView && noCoordsList.length ? '' : 'none'; noCoordsBtn.textContent = `без координат: ${noCoordsList.length}`; } if (noCoordsPanel) { noCoordsPanel.innerHTML = renderNoCoords(noCoordsList); - if (!noCoordsList.length) noCoordsPanel.style.display = 'none'; + if (!managerView || !noCoordsList.length) noCoordsPanel.style.display = 'none'; } els.empty.style.display = withCoords.length ? 'none' : 'flex'; @@ -258,7 +262,8 @@ export function createMapView(els: MapElements, apiKey: string): MapView { } return { - render(surfaces: MapSurface[]): void { + render(surfaces: MapSurface[], mv: boolean): void { + managerView = mv; if (!objectManager) { pending = surfaces; return; diff --git a/frontend/src/styles.css b/frontend/src/styles.css index fabc299..f989adc 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -256,9 +256,9 @@ input::placeholder { color: var(--text-muted); } } .decor-btn:hover::after { opacity: 1; } .decor-menu { - /* Fixed to the viewport: JS sets `top` (25px below the header) and `max-height` - (ending ≥30px above the window bottom) on open, so it never overflows. */ - display: none; position: fixed; right: 28px; + /* Fixed to the viewport: JS sets `top` (20px below the header/chips) and + `max-height` (ending ≥30px above the window bottom) on open. */ + display: none; position: fixed; right: 58px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-md); padding: var(--size-3); z-index: 50; width: max-content; max-width: min(420px, 92vw); overflow-y: auto; @@ -469,6 +469,7 @@ html.role-pending .manager-only { display: none !important; } .vis-item.vis-range .vis-item-overflow { display: flex; align-items: center; height: 100%; width: 100%; overflow: hidden; } .vis-item.vis-range .vis-item-content { padding: 0; width: 100%; } .bar-inner { + position: relative; height: var(--bar-h); width: 100%; box-sizing: border-box; @@ -483,6 +484,14 @@ html.role-pending .manager-only { display: none !important; } overflow: hidden; } .vis-item.vis-range:hover .bar-inner { filter: brightness(0.92); } +/* Label wider than the bar: JS adds .bar-of, which lifts the clipping and + places the label just past the bar's right edge as dark text on the track. */ +.vis-item.vis-range.bar-of .vis-item-overflow { overflow: visible; } +.vis-item.vis-range.bar-of .bar-inner { overflow: visible; } +.vis-item.vis-range.bar-of .bar-text { + position: absolute; left: 100%; top: 50%; transform: translateY(-50%); + margin-left: 5px; color: var(--text); white-space: nowrap; pointer-events: none; +} .vis-foreground .vis-group { min-height: var(--row-h); } .vis-labelset .vis-label { font-size: var(--label-font); display: flex; align-items: center; min-height: var(--row-h); } diff --git a/frontend/src/timeline.ts b/frontend/src/timeline.ts index 69b5b1b..ca7fabb 100644 --- a/frontend/src/timeline.ts +++ b/frontend/src/timeline.ts @@ -138,6 +138,32 @@ export function createTimelineView(el: TimelineElements, appearance: Appearance) window.addEventListener('resize', scheduleHeightFit); scheduleHeightFit(); + // When a bar's label is wider than the bar itself, move it just past the bar's + // right edge (dark text on the track) instead of clipping it inside. Re-run on + // every redraw (zoom/pan changes bar widths). Batched: reset all, measure all, + // then apply — so at most one reflow per pass. + let barTextRaf = 0; + function layoutBarText(): void { + const items = Array.from(el.timelineEl.querySelectorAll('.vis-item.vis-range')); + for (const it of items) it.classList.remove('bar-of'); + const overflowing: HTMLElement[] = []; + for (const it of items) { + const inner = it.querySelector('.bar-inner'); + if (inner && inner.scrollWidth > inner.clientWidth + 1) overflowing.push(it); + } + for (const it of overflowing) it.classList.add('bar-of'); + } + function scheduleBarText(): void { + if (barTextRaf) return; + // Double rAF: let vis finish positioning bars before we measure widths. + barTextRaf = requestAnimationFrame(() => + requestAnimationFrame(() => { barTextRaf = 0; layoutBarText(); }), + ); + } + // 'changed' = data/redraw; 'rangechanged' = zoom/pan settled (bars resized). + timeline.on('changed', scheduleBarText); + timeline.on('rangechanged', scheduleBarText); + // ---- clicks: bar -> Planfix task; row label -> board detail panel ---- timeline.on('click', (props: any) => { if (props.item) { @@ -367,7 +393,7 @@ export function createTimelineView(el: TimelineElements, appearance: Appearance) group: bk.board_id, start: bk.start_date, end: end.toISOString().slice(0, 10), - content: `
${barContent}
`, + content: `
${barContent}
`, type: 'range', tooltipHtml, taskId: bk.task_id, @@ -382,6 +408,7 @@ export function createTimelineView(el: TimelineElements, appearance: Appearance) if (fit) timeline.fit({ animation: false }); scheduleHeightFit(); + scheduleBarText(); } function refresh(): void {