mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-19 10:46:48 +02:00
Merge 65e70ecd19
into 443d5cf554
This commit is contained in:
commit
10de112f06
2 changed files with 426 additions and 10 deletions
|
@ -7,11 +7,17 @@
|
||||||
class="beat-hover-area"
|
class="beat-hover-area"
|
||||||
:class="{ 'empty': (beat === 0) }"
|
:class="{ 'empty': (beat === 0) }"
|
||||||
:style="beatHoverAreaStyle"
|
:style="beatHoverAreaStyle"
|
||||||
:title="getBeatTitle(beat)"
|
:aria-label="getBeatAriaLabel(beat)"
|
||||||
|
role="status"
|
||||||
|
tabindex="0"
|
||||||
|
@mouseenter="showTooltip(beat, $event)"
|
||||||
|
@mouseleave="hideTooltip"
|
||||||
|
@focus="showTooltip(beat, $event)"
|
||||||
|
@blur="hideTooltip"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="beat"
|
class="beat"
|
||||||
:class="{ 'empty': (beat === 0), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
|
:class="{ 'empty': (beat === 0 || beat === null || beat.status === null), 'down': (beat.status === 0), 'pending': (beat.status === 2), 'maintenance': (beat.status === 3) }"
|
||||||
:style="beatStyle"
|
:style="beatStyle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,13 +30,26 @@
|
||||||
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
|
<div v-if="$root.styleElapsedTime === 'with-line'" class="connecting-line"></div>
|
||||||
<div>{{ timeSinceLastBeat }}</div>
|
<div>{{ timeSinceLastBeat }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom Tooltip -->
|
||||||
|
<Tooltip
|
||||||
|
:visible="tooltipVisible"
|
||||||
|
:content="tooltipContent"
|
||||||
|
:x="tooltipX"
|
||||||
|
:y="tooltipY"
|
||||||
|
:position="tooltipPosition"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import Tooltip from "./Tooltip.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
Tooltip,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
/** Size of the heartbeat bar */
|
/** Size of the heartbeat bar */
|
||||||
size: {
|
size: {
|
||||||
|
@ -46,6 +65,11 @@ export default {
|
||||||
heartbeatList: {
|
heartbeatList: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: null,
|
default: null,
|
||||||
|
},
|
||||||
|
/** Heartbeat bar days */
|
||||||
|
heartbeatBarDays: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -56,10 +80,25 @@ export default {
|
||||||
beatHoverAreaPadding: 4,
|
beatHoverAreaPadding: 4,
|
||||||
move: false,
|
move: false,
|
||||||
maxBeat: -1,
|
maxBeat: -1,
|
||||||
|
// Tooltip data
|
||||||
|
tooltipVisible: false,
|
||||||
|
tooltipContent: null,
|
||||||
|
tooltipX: 0,
|
||||||
|
tooltipY: 0,
|
||||||
|
tooltipPosition: "below",
|
||||||
|
tooltipTimeoutId: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalized heartbeatBarDays as a number
|
||||||
|
* @returns {number} Number of days for heartbeat bar
|
||||||
|
*/
|
||||||
|
normalizedHeartbeatBarDays() {
|
||||||
|
return Math.max(0, Math.min(365, Math.floor(this.heartbeatBarDays || 0)));
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If heartbeatList is null, get it from $root.heartbeatList
|
* If heartbeatList is null, get it from $root.heartbeatList
|
||||||
* @returns {object} Heartbeat list
|
* @returns {object} Heartbeat list
|
||||||
|
@ -80,6 +119,12 @@ export default {
|
||||||
if (!this.beatList) {
|
if (!this.beatList) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For configured ranges, no padding needed since we show all beats
|
||||||
|
if (this.normalizedHeartbeatBarDays > 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
let num = this.beatList.length - this.maxBeat;
|
let num = this.beatList.length - this.maxBeat;
|
||||||
|
|
||||||
if (this.move) {
|
if (this.move) {
|
||||||
|
@ -98,8 +143,20 @@ export default {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If heartbeat days is configured (not auto), data is already aggregated from server
|
||||||
|
if (this.normalizedHeartbeatBarDays > 0 && this.beatList.length > 0) {
|
||||||
|
// Show all beats from server - they are already properly aggregated
|
||||||
|
return this.beatList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original logic for auto mode (heartbeatBarDays = 0)
|
||||||
let placeholders = [];
|
let placeholders = [];
|
||||||
|
|
||||||
|
// Handle case where maxBeat is -1 (no limit)
|
||||||
|
if (this.maxBeat <= 0) {
|
||||||
|
return this.beatList;
|
||||||
|
}
|
||||||
|
|
||||||
let start = this.beatList.length - this.maxBeat;
|
let start = this.beatList.length - this.maxBeat;
|
||||||
|
|
||||||
if (this.move) {
|
if (this.move) {
|
||||||
|
@ -172,13 +229,17 @@ export default {
|
||||||
* @returns {string} The time elapsed in minutes or hours.
|
* @returns {string} The time elapsed in minutes or hours.
|
||||||
*/
|
*/
|
||||||
timeSinceFirstBeat() {
|
timeSinceFirstBeat() {
|
||||||
|
if (this.normalizedHeartbeatBarDays === 1) {
|
||||||
|
return (this.normalizedHeartbeatBarDays * 24) + "h";
|
||||||
|
}
|
||||||
|
if (this.normalizedHeartbeatBarDays >= 2) {
|
||||||
|
return this.normalizedHeartbeatBarDays + "d";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to calculate from actual data
|
||||||
const firstValidBeat = this.shortBeatList.at(this.numPadding);
|
const firstValidBeat = this.shortBeatList.at(this.numPadding);
|
||||||
const minutes = dayjs().diff(dayjs.utc(firstValidBeat?.time), "minutes");
|
const minutes = dayjs().diff(dayjs.utc(firstValidBeat?.time), "minutes");
|
||||||
if (minutes > 60) {
|
return minutes > 60 ? Math.floor(minutes / 60) + "h" : minutes + "m";
|
||||||
return (minutes / 60).toFixed(0) + "h";
|
|
||||||
} else {
|
|
||||||
return minutes + "m";
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -205,7 +266,7 @@ export default {
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
beatList: {
|
beatList: {
|
||||||
handler(val, oldVal) {
|
handler() {
|
||||||
this.move = true;
|
this.move = true;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -217,6 +278,10 @@ export default {
|
||||||
},
|
},
|
||||||
unmounted() {
|
unmounted() {
|
||||||
window.removeEventListener("resize", this.resize);
|
window.removeEventListener("resize", this.resize);
|
||||||
|
// Clean up tooltip timeout
|
||||||
|
if (this.tooltipTimeoutId) {
|
||||||
|
clearTimeout(this.tooltipTimeoutId);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
if (this.heartbeatList === null) {
|
if (this.heartbeatList === null) {
|
||||||
|
@ -256,7 +321,23 @@ export default {
|
||||||
*/
|
*/
|
||||||
resize() {
|
resize() {
|
||||||
if (this.$refs.wrap) {
|
if (this.$refs.wrap) {
|
||||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2));
|
const newMaxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatHoverAreaPadding * 2));
|
||||||
|
|
||||||
|
// If maxBeat changed and we're in configured days mode, notify parent to reload data
|
||||||
|
if (newMaxBeat !== this.maxBeat && this.normalizedHeartbeatBarDays > 0) {
|
||||||
|
this.maxBeat = newMaxBeat;
|
||||||
|
|
||||||
|
// Find the closest parent with reloadHeartbeatData method (StatusPage)
|
||||||
|
let parent = this.$parent;
|
||||||
|
while (parent && !parent.reloadHeartbeatData) {
|
||||||
|
parent = parent.$parent;
|
||||||
|
}
|
||||||
|
if (parent && parent.reloadHeartbeatData) {
|
||||||
|
parent.reloadHeartbeatData(newMaxBeat);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.maxBeat = newMaxBeat;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -267,7 +348,105 @@ export default {
|
||||||
* @returns {string} Beat title
|
* @returns {string} Beat title
|
||||||
*/
|
*/
|
||||||
getBeatTitle(beat) {
|
getBeatTitle(beat) {
|
||||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
if (!beat) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show timestamp for all beats (both individual and aggregated)
|
||||||
|
return `${this.$root.datetime(beat.time)}${beat.msg ? ` - ${beat.msg}` : ""}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the aria-label for accessibility
|
||||||
|
* @param {object} beat Beat to get aria-label from
|
||||||
|
* @returns {string} Aria label
|
||||||
|
*/
|
||||||
|
getBeatAriaLabel(beat) {
|
||||||
|
if (beat === 0 || !beat) {
|
||||||
|
return "No data";
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = {
|
||||||
|
0: "Down",
|
||||||
|
1: "Up",
|
||||||
|
2: "Pending",
|
||||||
|
3: "Maintenance"
|
||||||
|
}[beat.status] || "Unknown";
|
||||||
|
|
||||||
|
return `${statusText} at ${this.$root.datetime(beat.time)}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show custom tooltip
|
||||||
|
* @param {object} beat Beat data
|
||||||
|
* @param {Event} event Mouse event
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
showTooltip(beat, event) {
|
||||||
|
if (beat === 0 || !beat) {
|
||||||
|
this.hideTooltip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (this.tooltipTimeoutId) {
|
||||||
|
clearTimeout(this.tooltipTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay for better UX
|
||||||
|
this.tooltipTimeoutId = setTimeout(() => {
|
||||||
|
this.tooltipContent = beat;
|
||||||
|
|
||||||
|
// Calculate position relative to viewport
|
||||||
|
const rect = event.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Position relative to viewport
|
||||||
|
const x = rect.left + (rect.width / 2);
|
||||||
|
const y = rect.top;
|
||||||
|
|
||||||
|
// Check if tooltip would go off-screen and adjust position
|
||||||
|
const tooltipHeight = 80; // Approximate tooltip height
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const spaceAbove = y;
|
||||||
|
const spaceBelow = viewportHeight - y - rect.height;
|
||||||
|
|
||||||
|
if (spaceAbove > tooltipHeight && spaceBelow < tooltipHeight) {
|
||||||
|
// Show above - arrow points down
|
||||||
|
this.tooltipPosition = "above";
|
||||||
|
this.tooltipY = y - 10;
|
||||||
|
} else {
|
||||||
|
// Show below - arrow points up
|
||||||
|
this.tooltipPosition = "below";
|
||||||
|
this.tooltipY = y + rect.height + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure tooltip doesn't go off the left or right edge
|
||||||
|
const tooltipWidth = 120; // Approximate tooltip width
|
||||||
|
let adjustedX = x;
|
||||||
|
|
||||||
|
if ((x - tooltipWidth / 2) < 10) {
|
||||||
|
adjustedX = tooltipWidth / 2 + 10;
|
||||||
|
} else if ((x + tooltipWidth / 2) > (window.innerWidth - 10)) {
|
||||||
|
adjustedX = window.innerWidth - tooltipWidth / 2 - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltipX = adjustedX;
|
||||||
|
this.tooltipVisible = true;
|
||||||
|
}, 150);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide custom tooltip
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
hideTooltip() {
|
||||||
|
if (this.tooltipTimeoutId) {
|
||||||
|
clearTimeout(this.tooltipTimeoutId);
|
||||||
|
this.tooltipTimeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tooltipVisible = false;
|
||||||
|
this.tooltipContent = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -345,4 +524,5 @@ export default {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
236
src/components/Tooltip.vue
Normal file
236
src/components/Tooltip.vue
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
<template>
|
||||||
|
<teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="visible && content"
|
||||||
|
ref="tooltip"
|
||||||
|
class="tooltip-wrapper"
|
||||||
|
:style="tooltipStyle"
|
||||||
|
:class="{ 'tooltip-above': position === 'above' }"
|
||||||
|
>
|
||||||
|
<div class="tooltip-content">
|
||||||
|
<slot :content="content">
|
||||||
|
<!-- Default content if no slot provided -->
|
||||||
|
<div class="tooltip-status" :class="statusClass">
|
||||||
|
{{ statusText }}
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-time">{{ timeText }}</div>
|
||||||
|
<div v-if="content?.msg" class="tooltip-message">{{ content.msg }}</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip-arrow" :class="{ 'arrow-above': position === 'above' }"></div>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "Tooltip",
|
||||||
|
props: {
|
||||||
|
/** Whether tooltip is visible */
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
/** Content object to display */
|
||||||
|
content: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
/** X position (viewport coordinates) */
|
||||||
|
x: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
/** Y position (viewport coordinates) */
|
||||||
|
y: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
/** Position relative to target element */
|
||||||
|
position: {
|
||||||
|
type: String,
|
||||||
|
default: "below",
|
||||||
|
validator: (value) => [ "above", "below" ].includes(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tooltipStyle() {
|
||||||
|
return {
|
||||||
|
left: this.x + "px",
|
||||||
|
top: this.y + "px",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
statusText() {
|
||||||
|
if (!this.content || this.content === 0) {
|
||||||
|
return this.$t("Unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusMap = {
|
||||||
|
0: this.$t("Down"),
|
||||||
|
1: this.$t("Up"),
|
||||||
|
2: this.$t("Pending"),
|
||||||
|
3: this.$t("Maintenance")
|
||||||
|
};
|
||||||
|
return statusMap[this.content.status] || this.$t("Unknown");
|
||||||
|
},
|
||||||
|
|
||||||
|
statusClass() {
|
||||||
|
if (!this.content || this.content === 0) {
|
||||||
|
return "status-empty";
|
||||||
|
}
|
||||||
|
|
||||||
|
const classMap = {
|
||||||
|
0: "status-down",
|
||||||
|
1: "status-up",
|
||||||
|
2: "status-pending",
|
||||||
|
3: "status-maintenance"
|
||||||
|
};
|
||||||
|
return classMap[this.content.status] || "status-unknown";
|
||||||
|
},
|
||||||
|
|
||||||
|
timeText() {
|
||||||
|
if (!this.content || this.content === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return this.$root.datetime(this.content.time);
|
||||||
|
},
|
||||||
|
|
||||||
|
message() {
|
||||||
|
if (!this.content || this.content === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return this.content.msg || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.tooltip-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
.tooltip-content {
|
||||||
|
background: rgba(17, 24, 39, 0.95);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(75, 85, 99, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.25);
|
||||||
|
min-width: 120px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.tooltip-status {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
&.status-up {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-down {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-pending {
|
||||||
|
color: $warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-maintenance {
|
||||||
|
color: $maintenance;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-empty {
|
||||||
|
color: $secondary-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-time {
|
||||||
|
color: #d1d5db;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-message {
|
||||||
|
color: #f3f4f6;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-top: 1px solid rgba(75, 85, 99, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
|
||||||
|
// Default: tooltip below element, arrow points up
|
||||||
|
border-bottom: 6px solid rgba(17, 24, 39, 0.95);
|
||||||
|
top: -6px;
|
||||||
|
|
||||||
|
&.arrow-above {
|
||||||
|
// Tooltip above element, arrow points down
|
||||||
|
top: auto;
|
||||||
|
bottom: -6px;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top: 6px solid rgba(17, 24, 39, 0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth entrance animation
|
||||||
|
animation: tooltip-fade-in 0.2s $easing-out;
|
||||||
|
|
||||||
|
&.tooltip-above {
|
||||||
|
transform: translateX(-50%) translateY(-8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark theme adjustments
|
||||||
|
.dark .tooltip-wrapper {
|
||||||
|
.tooltip-content {
|
||||||
|
background: rgba(31, 41, 55, 0.95);
|
||||||
|
border-color: rgba(107, 114, 128, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-arrow {
|
||||||
|
border-bottom-color: rgba(31, 41, 55, 0.95);
|
||||||
|
|
||||||
|
&.arrow-above {
|
||||||
|
border-top-color: rgba(31, 41, 55, 0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tooltip-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%) translateY(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessibility improvements
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.tooltip-wrapper {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Add table
Reference in a new issue