Skip to content

Commit

Permalink
feat: added detail panel for hovered node
Browse files Browse the repository at this point in the history
  • Loading branch information
Rassl committed Dec 13, 2024
1 parent faa5305 commit 2446a36
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import CameraCenterIcon from '~/components/Icons/CameraCenterIcon'
import { useGraphStore } from '~/stores/useGraphStore'

export const CameraRecenterControl = () => {
const [cameraFocusTrigger, setCameraFocusTrigger] = useGraphStore((s) => [
s.cameraFocusTrigger,
s.setCameraFocusTrigger,
])
const cameraFocusTrigger = useGraphStore((s) => s.cameraFocusTrigger)
const setCameraFocusTrigger = useGraphStore((s) => s.setCameraFocusTrigger)

return (
<CameraCenterButton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
import styled from 'styled-components'
import { Flex } from '~/components/common/Flex'
import { Text } from '~/components/common/Text'
import { TypeBadge } from '~/components/common/TypeBadge'
import { useSchemaStore } from '~/stores/useSchemaStore'
import { Node } from '~/types'
import { colors } from '~/utils/colors'

type Props = {
title?: string
description?: string
node: Node
}

export const HoverCard = ({ title, description }: Props) => (
<Portal>
export const HoverCard = ({ node }: Props) => {
const { getNodeKeysByType } = useSchemaStore((s) => s)

const keyProperty = getNodeKeysByType(node.node_type) || ''

const description = node?.properties ? node?.properties[keyProperty] : ''

return (
<TooltipContainer>
<ContentWrapper>
<Title>{title}</Title>
<Title>
<TypeBadge type={node.node_type} />
</Title>
{description && <Description>{description}</Description>}
</ContentWrapper>
</TooltipContainer>
</Portal>
)

const Portal = styled.div`
position: fixed;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1000;
`
)
}

const TooltipContainer = styled(Flex)`
width: 390px;
Expand All @@ -34,20 +36,17 @@ const TooltipContainer = styled(Flex)`
border-radius: 8px;
padding: 15px;
padding-bottom: 3px !important;
position: fixed;
flex-direction: column;
gap: 4px;
top: calc(-230px);
left: 100%;
z-index: 1000;
margin-left: 450px;
pointer-events: auto;
align-items: flex-start;
`

const ContentWrapper = styled(Flex)`
margin-top: 0;
flex-direction: column;
gap: 4px;
align-items: flex-start;
`

const Title = styled(Text)`
Expand All @@ -67,4 +66,11 @@ const Description = styled(Text)`
color: ${colors.white};
margin: 0;
opacity: 0.8;
white-space: normal;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-clamp: 3;
-webkit-line-clamp: 3;
`
83 changes: 30 additions & 53 deletions src/components/Universe/CursorTooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,43 @@
import { useEffect, useRef } from 'react'
import styled from 'styled-components'
import { Flex } from '~/components/common/Flex'
import { TypeBadge } from '~/components/common/TypeBadge'
import { useHoveredNode } from '~/stores/useGraphStore'
import { useSchemaStore } from '~/stores/useSchemaStore'
import { colors } from '~/utils'
import { HoverCard } from './HoverCard/index'

export const CursorTooltip = () => {
const tooltipRef = useRef<HTMLDivElement | null>(null)

const node = useHoveredNode()

const getIndexByType = useSchemaStore((s) => s.getIndexByType)

const indexKey = node ? getIndexByType(node.node_type) : ''
useEffect(() => {
if (tooltipRef.current) {
tooltipRef.current.style.display = node ? 'block' : 'none'
}
}, [node, tooltipRef])

useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (tooltipRef.current) {
const tooltip = tooltipRef.current
const tooltipWidth = tooltip.offsetWidth
const tooltipHeight = tooltip.offsetHeight

let top = e.clientY - 20 // 20px above the cursor
let left = e.clientX - 20 // 20px to the left of the cursor
const tooltip = tooltipRef.current

// Prevent clipping at the bottom of the screen
if (top + tooltipHeight > window.innerHeight) {
top = window.innerHeight - tooltipHeight - 10 // 10px padding
}
if (!tooltip) {
return
}

// Prevent clipping on the right of the screen
if (left + tooltipWidth > window.innerWidth) {
left = window.innerWidth - tooltipWidth - 10 // 10px padding
}
tooltip.style.top = `${e.clientY + 10}px`
tooltip.style.left = `${e.clientX + 10}px`

// Prevent clipping on the left of the screen
if (left < 0) {
left = 10 // Minimum padding
}
// Prevent clipping at screen edges
const tooltipWidth = tooltip.offsetWidth
const tooltipHeight = tooltip.offsetHeight
const maxX = window.innerWidth - tooltipWidth - 10
const maxY = window.innerHeight - tooltipHeight - 10

// Prevent clipping at the top of the screen
if (top < 0) {
top = 10 // Minimum padding
}
if (e.clientX + 10 + tooltipWidth > window.innerWidth) {
tooltip.style.left = `${maxX}px`
}

tooltip.style.top = `${top}px`
tooltip.style.left = `${left}px`
if (e.clientY + 10 + tooltipHeight > window.innerHeight) {
tooltip.style.top = `${maxY}px`
}
}

Expand All @@ -55,23 +46,9 @@ export const CursorTooltip = () => {
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
}, [])

// Ensure node exists before rendering tooltip
if (!node) {
return null
}

const content = node.properties && indexKey && node.properties[indexKey] ? node.properties[indexKey] : ''
}, []) // Empty array ensures the listener is added only once

return (
<TooltipContainer ref={tooltipRef}>
<Flex>
<TypeBadge type={node.node_type || ''} />
</Flex>
<Flex>{content}</Flex>
</TooltipContainer>
)
return <TooltipContainer ref={tooltipRef}>{node && <HoverCard node={node} />}</TooltipContainer>
}

const TooltipContainer = styled(Flex)`
Expand All @@ -80,10 +57,10 @@ const TooltipContainer = styled(Flex)`
color: white;
padding: 5px;
border-radius: 3px;
pointer-events: none; /* Prevent interference with mouse events */
z-index: 1000; /* Ensure it's on top */
max-width: 200px; /* Optional: prevent overly large tooltips */
white-space: nowrap; /* Optional: prevent text wrapping */
overflow: hidden; /* Optional: prevent text overflow */
text-overflow: ellipsis; /* Optional: add ellipsis for overflowing text */
pointer-events: none; /* Tooltip won't block mouse events */
z-index: 1000;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: none; /* Start hidden */
`
2 changes: 2 additions & 0 deletions src/components/Universe/Graph/Connections/LineComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const _LineComponent = ({ isSelected, position, label, target, source }: LineCom
const line = lineRef.current
const activeNode = selectedNode || hoveredNode

line.visible = !activeNode

if (activeNode?.ref_id === source || activeNode?.ref_id === target) {
line.visible = true

Expand Down
55 changes: 5 additions & 50 deletions src/components/Universe/Overlay/index.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,11 @@
import { useCallback, useEffect, useState } from 'react'
import styled from 'styled-components'
import { ActionsToolbar } from '~/components/App/ActionsToolbar'
import { useGraphStore } from '~/stores/useGraphStore'
import { Tooltip } from '../Graph/Cubes/Cube/components/Tooltip'

export const Overlay = () => {
const [hoveredNode, isHovering] = useGraphStore((s) => [s.hoveredNode, s.isHovering])
const [isVisible, setIsVisible] = useState(false)
const [isTooltipHovered, setIsTooltipHovered] = useState(false)

useEffect(() => {
let timer: NodeJS.Timeout | null = null

if (isHovering || isTooltipHovered) {
setIsVisible(true)
} else {
timer = setTimeout(() => setIsVisible(false), 300)
}

return () => {
if (timer) {
clearTimeout(timer)
}
}
}, [isHovering, isTooltipHovered])

const handleTooltipMouseEnter = useCallback(() => {
setIsTooltipHovered(true)
}, [])

const handleTooltipMouseLeave = useCallback(() => {
setIsTooltipHovered(false)
}, [])

return (
<OverlayWrap>
{hoveredNode && isVisible && (
<TooltipWrapper onMouseEnter={handleTooltipMouseEnter} onMouseLeave={handleTooltipMouseLeave}>
<Tooltip node={hoveredNode} />
</TooltipWrapper>
)}
<ActionsToolbar />
</OverlayWrap>
)
}
export const Overlay = () => (
<OverlayWrap>
<ActionsToolbar />
</OverlayWrap>
)

const OverlayWrap = styled('div')(({ theme }) => ({
position: 'absolute',
Expand All @@ -65,10 +27,3 @@ const OverlayWrap = styled('div')(({ theme }) => ({
top: 50,
},
}))

const TooltipWrapper = styled.div`
position: absolute;
top: 65px;
right: 55px;
z-index: 100;
`
2 changes: 2 additions & 0 deletions src/components/Universe/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Flex } from '../common/Flex'
import { outlineEffectColor } from './constants'
import { Controls } from './Controls'
import { initialCameraPosition } from './Controls/CameraAnimations/constants'
import { CursorTooltip } from './CursorTooltip'
import { Graph } from './Graph'
import { Lights } from './Lights'
import { Overlay } from './Overlay'
Expand Down Expand Up @@ -147,6 +148,7 @@ const _Universe = () => {
</Suspense>
{universeQuestionIsOpen && <UniverseQuestion />}
{isLoading && <Preloader fullSize={false} />}
<CursorTooltip />
<Overlay />
</Wrapper>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Html } from '@react-three/drei'
import { useThree } from '@react-three/fiber'
import { memo, useState } from 'react'
import { Flex } from '~/components/common/Flex'
import { HoverCard } from '../../../HoverCard'
import { HoverCard } from '../../../../../Universe/CursorTooltip/HoverCard'
import { RoundedRectangle } from '../RoundedRectangle'
import { Content } from './Content'
import { Image } from './Image'
Expand Down

0 comments on commit 2446a36

Please sign in to comment.