diff --git a/README.md b/README.md
index 7dbf7eb..933045f 100644
--- a/README.md
+++ b/README.md
@@ -71,3 +71,5 @@ export default defineConfig([
},
])
```
+
+
diff --git a/src/layouts/MainLayout/BaseLayout.tsx b/src/layouts/MainLayout/BaseLayout.tsx
index 78cb47a..cafdb9e 100644
--- a/src/layouts/MainLayout/BaseLayout.tsx
+++ b/src/layouts/MainLayout/BaseLayout.tsx
@@ -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 (
-
-
+
{/* Main Content */}
-
+
-
-
+
+ } content="Change theme" position="left" />
-
- );
-};
+ )
+}
-export default BaseLayout;
\ No newline at end of file
+export default BaseLayout
diff --git a/src/shared/Tooltip/Tooltip.stories.tsx b/src/shared/Tooltip/Tooltip.stories.tsx
new file mode 100644
index 0000000..f84a861
--- /dev/null
+++ b/src/shared/Tooltip/Tooltip.stories.tsx
@@ -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
+
+export default meta
+
+type Story = StoryObj
+
+const IconButton = () => (
+
+)
+
+export const Top: Story = {
+ args: {
+ content: 'Top tooltip',
+ position: 'top',
+
+ children: ,
+ },
+}
+
+export const Bottom: Story = {
+ args: {
+ content: 'Bottom tooltip',
+ position: 'bottom',
+
+ children: ,
+ },
+}
+
+export const Left: Story = {
+ args: {
+ content: 'Left tooltip',
+ position: 'left',
+
+ children: ,
+ },
+}
+
+export const Right: Story = {
+ args: {
+ content: 'Right tooltip',
+ position: 'right',
+
+ children: ,
+ },
+}
+
+export const LongContent: Story = {
+ args: {
+ content: 'AI confidence score is below the escalation threshold.',
+
+ position: 'top',
+
+ children: ,
+ },
+}
diff --git a/src/shared/Tooltip/Tooltip.tsx b/src/shared/Tooltip/Tooltip.tsx
new file mode 100644
index 0000000..9d697d4
--- /dev/null
+++ b/src/shared/Tooltip/Tooltip.tsx
@@ -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(null)
+
+ const timeoutRef = useRef(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 */}
+
+ {children}
+
+
+ {/* Tooltip */}
+ {visible &&
+ createPortal(
+
+ {content}
+
+ {/* Arrow */}
+
+
,
+ document.body
+ )}
+ >
+ )
+}
+
+export default Tooltip
diff --git a/src/shared/Tooltip/index.ts b/src/shared/Tooltip/index.ts
new file mode 100644
index 0000000..eaca424
--- /dev/null
+++ b/src/shared/Tooltip/index.ts
@@ -0,0 +1,2 @@
+export { Tooltip } from './Tooltip'
+export type { TooltipProps } from './Tooltip'