Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ export default defineConfig([
},
])
```

<!-- 38 35 34 26 -->
23 changes: 11 additions & 12 deletions src/layouts/MainLayout/BaseLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import ThemeToggleSwitch from '@/ThemeToggleSwitch';
import React, { Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import { Tooltip } from '@/shared/Tooltip'
import ThemeToggleSwitch from '@/ThemeToggleSwitch'
import React, { Suspense } from 'react'
import { Outlet } from 'react-router-dom'

const BaseLayout: React.FC = () => {
return (
<div className="min-h-screen bg-bg-primary flex flex-col">

<div className="bg-bg-primary flex min-h-screen flex-col">
{/* Main Content */}
<main className="flex-1 overflow-y-auto px-4 sm:px-6 lg:px-8">
<div className="py-8 sm:py-12 text-text-primary">
<div className="text-text-primary py-8 sm:py-12">
<Suspense fallback={null}>
<Outlet />
<div className="flex gap-2 absolute top-4 right-10">
<ThemeToggleSwitch />
<div className="absolute top-4 right-10 flex gap-2">
<Tooltip children={<ThemeToggleSwitch />} content="Change theme" position="left" />
</div>
</Suspense>
</div>
</main>

</div>
);
};
)
}

export default BaseLayout;
export default BaseLayout
69 changes: 69 additions & 0 deletions src/shared/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { Meta, StoryObj } from '@storybook/react-vite'

import { InformationCircleIcon } from '@heroicons/react/24/outline'

import { Tooltip } from './Tooltip'

const meta = {
title: 'Shared/Composites/Tooltip',

component: Tooltip,

tags: ['autodocs'],
} satisfies Meta<typeof Tooltip>

export default meta

type Story = StoryObj<typeof meta>

const IconButton = () => (
<button className="bg-bg-secondary border-border-secondary text-text-primary rounded-md border p-2">
<InformationCircleIcon className="h-5 w-5" />
</button>
)

export const Top: Story = {
args: {
content: 'Top tooltip',
position: 'top',

children: <IconButton />,
},
}

export const Bottom: Story = {
args: {
content: 'Bottom tooltip',
position: 'bottom',

children: <IconButton />,
},
}

export const Left: Story = {
args: {
content: 'Left tooltip',
position: 'left',

children: <IconButton />,
},
}

export const Right: Story = {
args: {
content: 'Right tooltip',
position: 'right',

children: <IconButton />,
},
}

export const LongContent: Story = {
args: {
content: 'AI confidence score is below the escalation threshold.',

position: 'top',

children: <IconButton />,
},
}
200 changes: 200 additions & 0 deletions src/shared/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { type ReactNode, useEffect, useRef, useState } from 'react'

import { createPortal } from 'react-dom'

export interface TooltipProps {
content: string

children: ReactNode

position?: 'top' | 'bottom' | 'left' | 'right'

delay?: number

className?: string
}

const arrowClasses = {
top: `
border-l-[6px] border-r-[6px] border-t-[6px]
border-l-transparent
border-r-transparent
border-t-bg-secondary
`,

bottom: `
border-l-[6px] border-r-[6px] border-b-[6px]
border-l-transparent
border-r-transparent
border-b-bg-secondary
`,

left: `
border-t-[6px] border-b-[6px] border-l-[6px]
border-t-transparent
border-b-transparent
border-l-bg-secondary
`,

right: `
border-t-[6px] border-b-[6px] border-r-[6px]
border-t-transparent
border-b-transparent
border-r-bg-secondary
`,
}

const arrowPositionClasses = {
top: `
top-full
left-1/2
-translate-x-1/2
`,

bottom: `
bottom-full
left-1/2
-translate-x-1/2
`,

left: `
left-full
top-1/2
-translate-y-1/2
`,

right: `
right-full
top-1/2
-translate-y-1/2
`,
}

export function Tooltip({
content,
children,

position = 'top',

delay = 400,

className = '',
}: TooltipProps) {
const [visible, setVisible] = useState(false)

const [coords, setCoords] = useState({
top: 0,
left: 0,
})

const triggerRef = useRef<HTMLDivElement>(null)

const timeoutRef = useRef<number | null>(null)

const showTooltip = () => {
timeoutRef.current = window.setTimeout(() => {
if (!triggerRef.current) return

const rect = triggerRef.current.getBoundingClientRect()

const spacing = 10

let top = 0
let left = 0

switch (position) {
case 'top':
top = rect.top - spacing
left = rect.left + rect.width / 2
break

case 'bottom':
top = rect.bottom + spacing
left = rect.left + rect.width / 2
break

case 'left':
top = rect.top + rect.height / 2
left = rect.left - spacing
break

case 'right':
top = rect.top + rect.height / 2
left = rect.right + spacing
break
}

setCoords({
top,
left,
})

setVisible(true)
}, delay)
}

const hideTooltip = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}

setVisible(false)
}

useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])

return (
<>
{/* Trigger */}
<div
ref={triggerRef}
className="inline-flex items-center"
onMouseEnter={showTooltip}
onMouseLeave={hideTooltip}
onFocus={showTooltip}
onBlur={hideTooltip}
>
{children}
</div>

{/* Tooltip */}
{visible &&
createPortal(
<div
role="tooltip"
aria-describedby="tooltip"
className={`bg-bg-secondary text-text-primary pointer-events-none fixed z-50 rounded-md px-3 py-2 text-sm whitespace-nowrap shadow-lg ${className} `}
style={{
top: coords.top,
left: coords.left,

transform:
position === 'top'
? 'translate(-50%, -100%)'
: position === 'bottom'
? 'translate(-50%, 0)'
: position === 'left'
? 'translate(-100%, -5%)'
: 'translate(0, -45%)',
}}
>
{content}

{/* Arrow */}
<div
className={`absolute h-0 w-0 ${arrowClasses[position]} ${arrowPositionClasses[position]} `}
/>
</div>,
document.body
)}
</>
)
}

export default Tooltip
2 changes: 2 additions & 0 deletions src/shared/Tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Tooltip } from './Tooltip'
export type { TooltipProps } from './Tooltip'
Loading