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 <noreply@anthropic.com>
This commit is contained in:
parent
159590d62e
commit
9b6ca6f848
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mapdash-frontend",
|
||||
"private": true,
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -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<void> {
|
||||
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 {
|
||||
|
||||
@ -59,7 +59,7 @@ export interface MapElements {
|
||||
}
|
||||
|
||||
export interface MapView {
|
||||
render(surfaces: MapSurface[]): void;
|
||||
render(surfaces: MapSurface[], managerView: boolean): void;
|
||||
ensureInit(): Promise<void>;
|
||||
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;
|
||||
|
||||
@ -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); }
|
||||
|
||||
|
||||
@ -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<HTMLElement>('.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<HTMLElement>('.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: `<div class="bar-inner" style="background-color:${color};">${barContent}</div>`,
|
||||
content: `<div class="bar-inner" style="background-color:${color};"><span class="bar-text">${barContent}</span></div>`,
|
||||
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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user