fixed search stats tooltips

This commit is contained in:
Flaminel
2026-03-23 11:55:37 +02:00
parent e63b44de09
commit 0ed2bd7ffd
4 changed files with 129 additions and 16 deletions

View File

@@ -94,6 +94,13 @@
}
.instance-card {
position: relative;
z-index: 0;
&:has(.tooltip-bubble--visible) {
z-index: 1;
}
&__header {
display: flex;
align-items: center;

View File

@@ -1,4 +1,12 @@
<span class="tooltip-wrapper">
<span class="tooltip-wrapper" #wrapper>
<ng-content />
<span class="tooltip-bubble" [class]="'tooltip-bubble--' + position()">{{ text() }}</span>
<span
#bubble
class="tooltip-bubble"
[class.tooltip-bubble--top]="resolvedPosition() === 'top'"
[class.tooltip-bubble--bottom]="resolvedPosition() === 'bottom'"
[class.tooltip-bubble--left]="resolvedPosition() === 'left'"
[class.tooltip-bubble--right]="resolvedPosition() === 'right'"
[class.tooltip-bubble--visible]="visible()"
>{{ text() }}</span>
</span>

View File

@@ -3,39 +3,55 @@
display: inline-flex;
align-items: center;
cursor: help;
&:hover .tooltip-bubble {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
}
.tooltip-bubble {
position: absolute;
left: 50%;
transform: translateX(-50%) translateY(4px);
padding: var(--space-2) var(--space-3);
background: var(--surface-overlay, rgba(30, 20, 50, 0.95));
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
font-size: var(--font-size-xs);
color: var(--text-primary);
white-space: nowrap;
white-space: normal;
width: max-content;
max-width: 220px;
text-align: center;
z-index: var(--z-tooltip);
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity var(--duration-fast) ease, visibility var(--duration-fast) ease, transform var(--duration-fast) ease;
transition: opacity var(--duration-fast) ease, visibility var(--duration-fast) ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
&--visible {
opacity: 1;
visibility: visible;
}
// Vertical positions (top/bottom): centered horizontally
&--top {
bottom: calc(100% + 6px);
top: auto;
left: 50%;
transform: translateX(-50%);
}
&--bottom {
top: calc(100% + 6px);
bottom: auto;
left: 50%;
transform: translateX(-50%);
}
// Horizontal positions (left/right): centered vertically
&--left {
right: calc(100% + 6px);
top: 50%;
transform: translateY(-50%);
}
&--right {
left: calc(100% + 6px);
top: 50%;
transform: translateY(-50%);
}
}

View File

@@ -1,4 +1,6 @@
import { Component, ChangeDetectionStrategy, input } from '@angular/core';
import { Component, ChangeDetectionStrategy, input, signal, ElementRef, viewChild } from '@angular/core';
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
@Component({
selector: 'app-tooltip',
@@ -6,8 +8,88 @@ import { Component, ChangeDetectionStrategy, input } from '@angular/core';
templateUrl: './tooltip.component.html',
styleUrl: './tooltip.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'(mouseenter)': 'onMouseEnter()',
'(mouseleave)': 'onMouseLeave()',
},
})
export class TooltipComponent {
text = input.required<string>();
position = input<'top' | 'bottom'>('top');
position = input<TooltipPosition>('top');
resolvedPosition = signal<TooltipPosition>('top');
visible = signal(false);
private wrapper = viewChild<ElementRef<HTMLElement>>('wrapper');
onMouseEnter(): void {
this.resolvedPosition.set(this.calculatePosition());
this.visible.set(true);
}
onMouseLeave(): void {
this.visible.set(false);
}
private calculatePosition(): TooltipPosition {
const preferred = this.position();
const wrapperEl = this.wrapper()?.nativeElement;
if (!wrapperEl) return preferred;
const rect = wrapperEl.getBoundingClientRect();
const margin = 12;
const estimatedTooltipHeight = 50;
const estimatedTooltipWidth = 230;
// Find the actual content boundary (accounts for sidebar)
const contentArea = wrapperEl.closest('.shell__content') as HTMLElement | null;
const contentLeft = contentArea ? contentArea.getBoundingClientRect().left : 0;
const spaceTop = rect.top;
const spaceBottom = window.innerHeight - rect.bottom;
const spaceLeft = rect.left - contentLeft;
const spaceRight = window.innerWidth - rect.right;
const hasSpace: Record<TooltipPosition, boolean> = {
top: spaceTop > estimatedTooltipHeight + margin,
bottom: spaceBottom > estimatedTooltipHeight + margin,
left: spaceLeft > estimatedTooltipWidth + margin,
right: spaceRight > estimatedTooltipWidth + margin,
};
if (hasSpace[preferred] && (preferred === 'left' || preferred === 'right')) {
return preferred;
}
if (hasSpace[preferred] && (preferred === 'top' || preferred === 'bottom')) {
const halfTooltip = estimatedTooltipWidth / 2;
const centerX = rect.left + rect.width / 2;
// Check if centered tooltip would overflow left (behind sidebar)
if (centerX - halfTooltip < contentLeft + margin) {
if (hasSpace['right']) return 'right';
if (hasSpace['bottom']) return 'bottom';
}
// Check if centered tooltip would overflow right
if (centerX + halfTooltip > window.innerWidth - margin) {
if (hasSpace['left']) return 'left';
if (hasSpace['bottom']) return 'bottom';
}
return preferred;
}
// Fallback order
const fallbacks: TooltipPosition[] =
preferred === 'top' || preferred === 'bottom'
? ['top', 'bottom', 'right', 'left']
: ['right', 'left', 'top', 'bottom'];
for (const pos of fallbacks) {
if (hasSpace[pos]) return pos;
}
return preferred;
}
}