From 7e1fc0d82e3fdbd1b1193d65955ad134e85778eb Mon Sep 17 00:00:00 2001 From: Mayank Sharma Date: Sat, 26 Sep 2026 18:56:23 +0530 Subject: [PATCH 1/2] fix: trigger card animation automatically on mobile viewport entry (#8093) Add IntersectionObserver to observe Kanvas feature cards on the Operate page, triggering card animations automatically when they enter the viewport on mobile and small screens while preserving desktop hover interactions. Signed-off-by: Mayank Sharma --- .../kanvas-visualize-features.js | 491 ++++++++++++++---- 1 file changed, 379 insertions(+), 112 deletions(-) diff --git a/src/sections/Kanvas/Kanvas-visualize/kanvas-visualize-features.js b/src/sections/Kanvas/Kanvas-visualize/kanvas-visualize-features.js index 54b4f05a079ff1..e891829622a9c9 100644 --- a/src/sections/Kanvas/Kanvas-visualize/kanvas-visualize-features.js +++ b/src/sections/Kanvas/Kanvas-visualize/kanvas-visualize-features.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect, useRef } from "react"; import styled from "styled-components"; import { Row, Col } from "../../../reusecore/Layout"; import PerformanceMetrics from "./images/performance-metrics-bars.svg"; @@ -14,94 +14,93 @@ import InteractiveTerminal from "./images/interactive-terminal.svg"; import InteractiveTerminalCode from "./images/interactive-terminal-code.svg"; import LogStream from "./images/log-stream.svg"; import LogStreamSearch from "./images/log-stream-search.svg"; -import { useState } from "react"; import { useStyledDarkMode } from "../../../theme/app/useStyledDarkMode"; import DocsIcon from "../../Pricing/icons/docs.js"; const VisualizerFeaturesWrapper = styled.div` + display: flex; + width: 80%; + justify-content: center; + align-items: center; + z-index: 10; -display: flex; -width: 80%; -justify-content: center; -align-items: center; -z-index: 10; - -.project__block__wrap{ - position: relative; - width: 100%; - height: fit-content; - display: flex; - flex-wrap: wrap; - margin-bottom: 2rem; - &:before { - position: absolute; - right: -2rem; - bottom: 0; - } + .project__block__wrap { + position: relative; + width: 100%; + height: fit-content; + display: flex; + flex-wrap: wrap; + margin-bottom: 2rem; + &:before { + position: absolute; + right: -2rem; + bottom: 0; } - .project__block__inner { - position: relative; - display: flex; - flex-direction: column; - background: ${props => props.theme.grey212121ToWhite}; - box-shadow: 0px 0px ${props => props.theme.projectShadowsize} ${props => props.theme.whiteToGreyE6E6E6}; - transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); + } + .project__block__inner { + position: relative; + display: flex; + flex-direction: column; + background: ${(props) => props.theme.grey212121ToWhite}; + box-shadow: 0px 0px ${(props) => props.theme.projectShadowsize} + ${(props) => props.theme.whiteToGreyE6E6E6}; + transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); - &:hover{ - box-shadow: 0px 0px 5px ${props => props.theme.green00D3A9ToGreen3C494F}; - } - padding: 10% 8%; - height: 25rem; - border-radius: 4%; - text-align: center; - justify-content: center; - align-items: center; - transition: all 0.5s ease; - margin: 5% 0%; - - h3{ - margin: auto; - font-weight: 700; - font-size: 1.5rem; - color: ${props => props.theme.secondaryColor}; - text-align: center; - justify-content: center; - margin-bottom: 2%; - } - p{ - font-weight: 300; - color: ${props => props.theme.text}; - margin: auto; - transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); - } - - img{ - height: auto; - width: 70%; - } - - .gatsby-image-wrapper{ - margin: 10px auto; - min-height: 40px; - } + &:hover { + box-shadow: 0px 0px 5px ${(props) => props.theme.green00D3A9ToGreen3C494F}; } + padding: 10% 8%; + height: 25rem; + border-radius: 4%; + text-align: center; + justify-content: center; + align-items: center; + transition: all 0.5s ease; + margin: 5% 0%; - .darken { - background: ${props => props.theme.grey121212ToWhite}; - /* scale:1.1; */ - /* transition: all 0.5s ease; */ - opacity: 0.85; + h3 { + margin: auto; + font-weight: 700; + font-size: 1.5rem; + color: ${(props) => props.theme.secondaryColor}; + text-align: center; + justify-content: center; + margin-bottom: 2%; + } + p { + font-weight: 300; + color: ${(props) => props.theme.text}; + margin: auto; transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); } - .feature-image { - position: relative; - padding: 0% 0% 10%; + img { + height: auto; + width: 70%; + } + + .gatsby-image-wrapper { + margin: 10px auto; + min-height: 40px; + } + } + + .darken { + background: ${(props) => props.theme.grey121212ToWhite}; + /* scale:1.1; */ + /* transition: all 0.5s ease; */ + opacity: 0.85; + transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); + } + + .feature-image { + position: relative; + padding: 0% 0% 10%; - svg { - .service-performance-meter-colorMode_svg__colorMode1{ - fill: ${props => props.theme.grey212121ToGreyF3F3F3}; - transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); + svg { + .service-performance-meter-colorMode_svg__colorMode1 { + fill: ${(props) => props.theme.grey212121ToGreyF3F3F3}; + transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1); } } @@ -119,7 +118,9 @@ z-index: 10; opacity: 1; transform-origin: center center; transform: rotateZ(200deg); - transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out 0.3s; + transition: + opacity 0.5s ease-in-out, + transform 0.5s ease-in-out 0.3s; /* transition: transform 1s ease-in-out 0.2s; */ } } @@ -128,16 +129,77 @@ z-index: 10; position: absolute; top: 16px; right: 16px; - color: ${props => props.theme.secondaryColor}; + color: ${(props) => props.theme.secondaryColor}; } `; -const KanvasisualizerFeatures = () => { +const KanvasVisualizerFeatures = () => { const [isHovered, setisHovered] = useState(false); const [hoveredFeature, sethoveredFeature] = useState(""); + const [inViewFeatures, setInViewFeatures] = useState(() => new Set()); + const [isSmallScreen, setIsSmallScreen] = useState(false); + const cardRefs = useRef([]); const { isDark } = useStyledDarkMode(); + useEffect(() => { + if (typeof window === "undefined" || !("matchMedia" in window)) return; + + const mediaQuery = window.matchMedia("(max-width: 992px), (hover: none)"); + const updateMatches = () => setIsSmallScreen(mediaQuery.matches); + + updateMatches(); + + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener("change", updateMatches); + return () => mediaQuery.removeEventListener("change", updateMatches); + } else if (mediaQuery.addListener) { + mediaQuery.addListener(updateMatches); + return () => mediaQuery.removeListener(updateMatches); + } + }, []); + + useEffect(() => { + if (typeof window === "undefined" || !("IntersectionObserver" in window)) + return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + const id = entry.target.getAttribute("data-feature-id"); + if (!id) return; + + if (entry.isIntersecting) { + setInViewFeatures((prev) => { + if (prev.has(id)) return prev; + const next = new Set(prev); + next.add(id); + return next; + }); + } else { + setInViewFeatures((prev) => { + if (!prev.has(id)) return prev; + const next = new Set(prev); + next.delete(id); + return next; + }); + } + }); + }, + { + threshold: 0.25, + }, + ); + + cardRefs.current.forEach((el) => { + if (el) observer.observe(el); + }); + + return () => { + observer.disconnect(); + }; + }, []); + const handleMouseOver = (num) => { setisHovered(true); sethoveredFeature(`${"Feature"}${num}`); @@ -148,49 +210,156 @@ const KanvasisualizerFeatures = () => { sethoveredFeature(""); }; + const isFeatureActive = (featureId) => { + return ( + hoveredFeature === featureId || + (isSmallScreen && inViewFeatures.has(featureId)) + ); + }; + return (
- -
handleMouseOver(1)} onMouseOut={handleMouseOut}> + +
{ + cardRefs.current[0] = el; + }} + data-feature-id="Feature1" + className={ + isHovered && hoveredFeature != "Feature1" + ? "project__block__inner darken" + : "project__block__inner" + } + onMouseOver={() => handleMouseOver(1)} + onMouseOut={handleMouseOut} + >
- +
- Application Import - + Application Import +

Application Import

-

Import your existing Kubernetes, Helm, or Docker Compose applications.

+

+ Import your existing Kubernetes, Helm, or Docker Compose + applications. +

- -
handleMouseOver(2)} onMouseOut={handleMouseOut}> + +
{ + cardRefs.current[1] = el; + }} + data-feature-id="Feature2" + className={ + isHovered && hoveredFeature != "Feature2" + ? "project__block__inner darken" + : "project__block__inner" + } + onMouseOver={() => handleMouseOver(2)} + onMouseOut={handleMouseOut} + >
- +
- Performance Metrics - + Performance Metrics +

Real-time performance metrics

-

Monitor your clusters performing in action, set alerts and work with object-specific metrics.

+

+ Monitor your clusters performing in action, set alerts and + work with object-specific metrics. +

- -
handleMouseOver(3)} onMouseOut={handleMouseOut}> + +
{ + cardRefs.current[2] = el; + }} + data-feature-id="Feature3" + className={ + isHovered && hoveredFeature != "Feature3" + ? "project__block__inner darken" + : "project__block__inner" + } + onMouseOver={() => handleMouseOver(3)} + onMouseOut={handleMouseOut} + >
- +
- Interactive Terminal - + Interactive Terminal +

Interactive Terminal

Establish sessions with one or more pods at a time.

@@ -198,39 +367,136 @@ const KanvasisualizerFeatures = () => {
-
handleMouseOver(4)} onMouseOut={handleMouseOut}> +
{ + cardRefs.current[3] = el; + }} + data-feature-id="Feature4" + className={ + isHovered && hoveredFeature != "Feature4" + ? "project__block__inner darken" + : "project__block__inner" + } + onMouseOver={() => handleMouseOver(4)} + onMouseOut={handleMouseOut} + >
- Timeline - + Timeline +

Timeline (DVR)

-

Playback service transactions. Scrub over the history of changes to your deployments.

+

+ Playback service transactions. Scrub over the history of changes + to your deployments. +

-
handleMouseOver(5)} onMouseOut={handleMouseOut}> +
{ + cardRefs.current[4] = el; + }} + data-feature-id="Feature5" + className={ + isHovered && hoveredFeature != "Feature5" + ? "project__block__inner darken" + : "project__block__inner" + } + onMouseOver={() => handleMouseOver(5)} + onMouseOut={handleMouseOut} + >
- Service Performance - +

Service Performance

-

Continuous visibility across all of your clusters and workloads metrics.

+

+ Continuous visibility across all of your clusters and workloads + metrics. +

- -
handleMouseOver(6)} onMouseOut={handleMouseOut}> + +
{ + cardRefs.current[5] = el; + }} + data-feature-id="Feature6" + className={ + isHovered && hoveredFeature != "Feature6" + ? "project__block__inner darken" + : "project__block__inner" + } + onMouseOver={() => handleMouseOver(6)} + onMouseOut={handleMouseOut} + >
- +
- Log Stream - + Log Stream +

Log Stream

-

Stream and filter through the logs using keywords for multiple Kubernetes Pods simultaneously.

+

+ Stream and filter through the logs using keywords for multiple + Kubernetes Pods simultaneously. +

@@ -239,4 +505,5 @@ const KanvasisualizerFeatures = () => { ); }; -export default KanvasisualizerFeatures; \ No newline at end of file + +export default KanvasVisualizerFeatures; From 319750d98272b9c781abb0b3caad85c3fdf54bb4 Mon Sep 17 00:00:00 2001 From: Mayank Sharma Date: Mon, 28 Sep 2026 13:56:22 +0530 Subject: [PATCH 2/2] fix: include any-pointer: coarse in touch activation query Update mediaQuery to include (any-pointer: coarse) so touch-capable devices activate cards even when the viewport is wider than 992px and the primary pointer supports hover. Signed-off-by: Mayank Sharma --- .../Kanvas/Kanvas-visualize/kanvas-visualize-features.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sections/Kanvas/Kanvas-visualize/kanvas-visualize-features.js b/src/sections/Kanvas/Kanvas-visualize/kanvas-visualize-features.js index e891829622a9c9..97d0673bb403eb 100644 --- a/src/sections/Kanvas/Kanvas-visualize/kanvas-visualize-features.js +++ b/src/sections/Kanvas/Kanvas-visualize/kanvas-visualize-features.js @@ -145,7 +145,9 @@ const KanvasVisualizerFeatures = () => { useEffect(() => { if (typeof window === "undefined" || !("matchMedia" in window)) return; - const mediaQuery = window.matchMedia("(max-width: 992px), (hover: none)"); + const mediaQuery = window.matchMedia( + "(max-width: 992px), (hover: none), (any-pointer: coarse)", + ); const updateMatches = () => setIsSmallScreen(mediaQuery.matches); updateMatches();