From 26cc1dac8adb3468eafe6b14b547f7053b06e112 Mon Sep 17 00:00:00 2001 From: dcarrillo Date: Sun, 12 Apr 2026 20:51:56 +0200 Subject: [PATCH] fix: edge tooltips were hidden inside the box (#5) * fix: edge tooltips were hidden inside the box * Update README.md --- README.md | 2 +- status-page/src/components/UptimeBars.astro | 83 +++++++++++++++++---- status-page/src/scripts/charts.ts | 68 +++++++++++++---- 3 files changed, 121 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 00f5220..6832e17 100644 --- a/README.md +++ b/README.md @@ -368,4 +368,4 @@ npm run check:pages # pages (astro check + tsc) - [ ] Refine the status page to look... well... less IA generated. - [ ] Initial support for incident management (manual status overrides, incident timeline). - [x] Branded status page (simple custom banner). -- [] Add support for notifications other than webhooks. +- [ ] Add support for notifications other than webhooks. diff --git a/status-page/src/components/UptimeBars.astro b/status-page/src/components/UptimeBars.astro index ffcdc70..404449e 100644 --- a/status-page/src/components/UptimeBars.astro +++ b/status-page/src/components/UptimeBars.astro @@ -61,6 +61,8 @@ function getBarColor(uptimePercent: number | undefined): string { gap: 1px; width: 100%; height: 14px; + position: relative; + overflow: visible; } .bar { @@ -93,13 +95,17 @@ function getBarColor(uptimePercent: number | undefined): string { background: var(--no-data); } - /* Tooltip */ + /* Tooltip - positioned below to avoid conflict with bar labels */ + .bar { + position: relative; + } + .bar::before { content: attr(data-tooltip); position: absolute; - bottom: 100%; + top: calc(100% + 8px); left: 50%; - transform: translateX(-50%) translateY(-8px); + transform: translateX(-50%); background: var(--text); color: var(--bg); padding: 4px 8px; @@ -112,23 +118,78 @@ function getBarColor(uptimePercent: number | undefined): string { transition: opacity 0.2s ease; pointer-events: none; z-index: 1000; + /* Prevent tooltip overflow */ + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + /* Ensure tooltip stays within screen bounds */ + left: 50%; + transform: translateX(-50%); + /* Constrain to viewport */ + max-width: min(200px, calc(100vw - 32px)); + } + + /* For left-edge bars, prevent left overflow */ + .bar:nth-child(1)::before, + .bar:nth-child(2)::before, + .bar:nth-child(3)::before, + .bar:nth-child(4)::before, + .bar:nth-child(5)::before, + .bar:nth-child(6)::before, + .bar:nth-child(7)::before, + .bar:nth-child(8)::before, + .bar:nth-child(9)::before, + .bar:nth-child(10)::before { + left: 0; + transform: translateX(0); + } + + /* For right-edge bars, prevent right overflow */ + .bar:nth-last-child(1)::before, + .bar:nth-last-child(2)::before, + .bar:nth-last-child(3)::before, + .bar:nth-last-child(4)::before, + .bar:nth-last-child(5)::before, + .bar:nth-last-child(6)::before, + .bar:nth-last-child(7)::before, + .bar:nth-last-child(8)::before, + .bar:nth-last-child(9)::before, + .bar:nth-last-child(10)::before { + left: auto; + right: 0; + transform: translateX(0); } .bar::after { content: ''; position: absolute; - bottom: 100%; + top: calc(100% + 2px); left: 50%; - transform: translateX(-50%) translateY(-2px); + transform: translateX(-50%); border: 4px solid transparent; - border-top-color: var(--text); + border-bottom-color: var(--text); opacity: 0; visibility: hidden; transition: opacity 0.2s ease; pointer-events: none; z-index: 1000; } - + + /* Adjust arrow position for edge bars */ + .bar:nth-child(1)::after, + .bar:nth-child(2)::after, + .bar:nth-child(3)::after, + .bar:nth-child(4)::after, + .bar:nth-child(5)::after, + .bar:nth-child(6)::after, + .bar:nth-child(7)::after, + .bar:nth-child(8)::after, + .bar:nth-child(9)::after, + .bar:nth-child(10)::after { + left: 8px; + transform: translateX(0); + } + .bar:hover::before, .bar:focus::before, .bar:hover::after, @@ -136,12 +197,4 @@ function getBarColor(uptimePercent: number | undefined): string { opacity: 1; visibility: visible; } - - .bar-labels { - display: flex; - justify-content: space-between; - font-size: 9px; - color: var(--text-dim); - margin-top: 3px; - } diff --git a/status-page/src/scripts/charts.ts b/status-page/src/scripts/charts.ts index 8ef60b1..91b3695 100644 --- a/status-page/src/scripts/charts.ts +++ b/status-page/src/scripts/charts.ts @@ -79,18 +79,18 @@ function tooltipPlugin(strokeColor: string): uPlot.Plugin { tooltipElement = document.createElement('div'); tooltipElement.className = 'chart-tooltip'; tooltipElement.style.cssText = ` - position: absolute; - pointer-events: none; - background: rgba(15, 23, 42, 0.95); - border: 1px solid ${strokeColor}; - color: #e2e8f0; - padding: 4px 8px; - border-radius: 4px; - font: 500 10px 'Geist Mono', monospace; - display: none; - white-space: nowrap; - z-index: 10; - `; + position: absolute; + pointer-events: none; + background: rgba(15, 23, 42, 0.95); + border: 1px solid ${strokeColor}; + color: #e2e8f0; + padding: 4px 8px; + border-radius: 4px; + font: 500 10px 'Geist Mono', monospace; + display: none; + white-space: nowrap; + z-index: 10; + `; // Cast needed: @cloudflare/workers-types overrides DOM append() signature (over as ParentNode).append(tooltipElement); @@ -131,18 +131,54 @@ function tooltipPlugin(strokeColor: string): uPlot.Plugin { const msString = Math.round(yValue) + ' ms'; tooltipElement.textContent = `${timeString} ${msString}`; - // Position tooltip, flipping side if near right edge + // Position tooltip, ensuring it stays within chart bounds const tipWidth = tooltipElement.offsetWidth; + const tipHeight = tooltipElement.offsetHeight; const plotWidth = over.clientWidth; + const plotHeight = over.clientHeight; const shiftX = 12; const shiftY = -10; - let posLeft = left + shiftX; - if (posLeft + tipWidth > plotWidth) { + + // Calculate horizontal position - choose side with more space + let posLeft: number; + const spaceRight = plotWidth - (left + shiftX); + const spaceLeft = left - shiftX; + + // Default to right side if there's enough space + if (spaceRight >= tipWidth) { + posLeft = left + shiftX; + } + // Try left side if there's more space there + else if (spaceLeft >= tipWidth) { posLeft = left - tipWidth - shiftX; } + // Not enough space on either side, choose the better option + else { + // If right side has more space than left, use right side clamped + if (spaceRight > spaceLeft) { + posLeft = plotWidth - tipWidth; + } else { + // Use left side clamped + posLeft = 0; + } + } + + // Ensure we don't go outside bounds + posLeft = Math.max(0, Math.min(posLeft, plotWidth - tipWidth)); + + // Calculate vertical position + let posTop = (top ?? 0) + shiftY; + // Check if tooltip would overflow top edge + if (posTop < 0) { + posTop = 0; + } + // Check if tooltip would overflow bottom edge + else if (posTop + tipHeight > plotHeight) { + posTop = plotHeight - tipHeight; + } tooltipElement.style.left = posLeft + 'px'; - tooltipElement.style.top = (top ?? 0) + shiftY + 'px'; + tooltipElement.style.top = posTop + 'px'; }, ], },