refactor: Move Tooltip and DarkModeToggle to shared/ui

This commit is contained in:
Haileyesus
2026-02-27 22:26:42 +03:00
parent 5b7995751e
commit 6e03964b8b
10 changed files with 126 additions and 100 deletions

View File

@@ -16,7 +16,7 @@ import {
GripVertical
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import DarkModeToggle from './DarkModeToggle';
import { DarkModeToggle } from '../shared/ui';
import { useUiPreferences } from '../hooks/useUiPreferences';
import { useTheme } from '../contexts/ThemeContext';
@@ -445,4 +445,4 @@ const QuickSettingsPanel = () => {
);
};
export default QuickSettingsPanel;
export default QuickSettingsPanel;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Clock, CheckCircle, Circle, AlertCircle, Pause, X, ArrowRight, ChevronUp, Minus, Flag } from 'lucide-react';
import { cn } from '../lib/utils';
import Tooltip from './Tooltip';
import { Tooltip } from '../shared/ui';
const TaskCard = ({
task,
@@ -207,4 +207,4 @@ const TaskCard = ({
);
};
export default TaskCard;
export default TaskCard;

View File

@@ -1,91 +0,0 @@
import React, { useState } from 'react';
import { cn } from '../lib/utils';
const Tooltip = ({
children,
content,
position = 'top',
className = '',
delay = 500
}) => {
const [isVisible, setIsVisible] = useState(false);
const [timeoutId, setTimeoutId] = useState(null);
const handleMouseEnter = () => {
const id = setTimeout(() => {
setIsVisible(true);
}, delay);
setTimeoutId(id);
};
const handleMouseLeave = () => {
if (timeoutId) {
clearTimeout(timeoutId);
setTimeoutId(null);
}
setIsVisible(false);
};
const getPositionClasses = () => {
switch (position) {
case 'top':
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
case 'bottom':
return 'top-full left-1/2 transform -translate-x-1/2 mt-2';
case 'left':
return 'right-full top-1/2 transform -translate-y-1/2 mr-2';
case 'right':
return 'left-full top-1/2 transform -translate-y-1/2 ml-2';
default:
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
}
};
const getArrowClasses = () => {
switch (position) {
case 'top':
return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
case 'bottom':
return 'bottom-full left-1/2 transform -translate-x-1/2 border-b-gray-900 dark:border-b-gray-100';
case 'left':
return 'left-full top-1/2 transform -translate-y-1/2 border-l-gray-900 dark:border-l-gray-100';
case 'right':
return 'right-full top-1/2 transform -translate-y-1/2 border-r-gray-900 dark:border-r-gray-100';
default:
return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
}
};
if (!content) {
return children;
}
return (
<div
className="relative inline-block"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
{isVisible && (
<div className={cn(
'absolute z-50 px-2 py-1 text-xs font-medium text-white bg-gray-900 dark:bg-gray-100 dark:text-gray-900 rounded shadow-lg whitespace-nowrap pointer-events-none',
'animate-in fade-in-0 zoom-in-95 duration-200',
getPositionClasses(),
className
)}>
{content}
{/* Arrow */}
<div className={cn(
'absolute w-0 h-0 border-4 border-transparent',
getArrowClasses()
)} />
</div>
)}
</div>
);
};
export default Tooltip;

View File

@@ -1,5 +1,5 @@
import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react';
import Tooltip from '../../../Tooltip';
import { Tooltip } from '../../../../shared/ui';
import type { AppTab } from '../../../../types/app';
import type { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,6 +1,6 @@
import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import DarkModeToggle from '../../../DarkModeToggle';
import { DarkModeToggle } from '../../../../shared/ui';
import LanguageSelector from '../../../LanguageSelector';
import type { CodeEditorSettingsState, ProjectSortOrder } from '../../types/types';

View File

@@ -1,5 +1,5 @@
import { Moon, Sun } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { useTheme } from '../../../contexts/ThemeContext';
type DarkModeToggleProps = {
checked?: boolean;
@@ -7,13 +7,18 @@ type DarkModeToggleProps = {
ariaLabel?: string;
};
function DarkModeToggle({ checked, onToggle, ariaLabel = 'Toggle dark mode' }: DarkModeToggleProps) {
function DarkModeToggle({
checked,
onToggle,
ariaLabel = 'Toggle dark mode',
}: DarkModeToggleProps) {
const { isDarkMode, toggleDarkMode } = useTheme();
// Support controlled usage while keeping ThemeContext as the default source of truth.
const isControlled = typeof checked === 'boolean' && typeof onToggle === 'function';
const isEnabled = isControlled ? checked : isDarkMode;
const handleToggle = () => {
if (isControlled) {
if (isControlled && onToggle) {
onToggle(!isEnabled);
return;
}

View File

@@ -0,0 +1,2 @@
export { default } from './DarkModeToggle';
export { default as DarkModeToggle } from './DarkModeToggle';

2
src/shared/ui/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { DarkModeToggle } from './dark-mode-toggle';
export { Tooltip } from './tooltip';

View File

@@ -0,0 +1,106 @@
import { type ReactNode, useEffect, useRef, useState } from 'react';
import { cn } from '../../../lib/utils';
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
type TooltipProps = {
children: ReactNode;
content?: ReactNode;
position?: TooltipPosition;
className?: string;
delay?: number;
};
function getPositionClasses(position: TooltipPosition): string {
switch (position) {
case 'top':
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
case 'bottom':
return 'top-full left-1/2 transform -translate-x-1/2 mt-2';
case 'left':
return 'right-full top-1/2 transform -translate-y-1/2 mr-2';
case 'right':
return 'left-full top-1/2 transform -translate-y-1/2 ml-2';
default:
return 'bottom-full left-1/2 transform -translate-x-1/2 mb-2';
}
}
function getArrowClasses(position: TooltipPosition): string {
switch (position) {
case 'top':
return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
case 'bottom':
return 'bottom-full left-1/2 transform -translate-x-1/2 border-b-gray-900 dark:border-b-gray-100';
case 'left':
return 'left-full top-1/2 transform -translate-y-1/2 border-l-gray-900 dark:border-l-gray-100';
case 'right':
return 'right-full top-1/2 transform -translate-y-1/2 border-r-gray-900 dark:border-r-gray-100';
default:
return 'top-full left-1/2 transform -translate-x-1/2 border-t-gray-900 dark:border-t-gray-100';
}
}
function Tooltip({
children,
content,
position = 'top',
className = '',
delay = 500,
}: TooltipProps) {
const [isVisible, setIsVisible] = useState(false);
// Store the timer id without forcing re-renders while hovering.
const timeoutRef = useRef<number | null>(null);
const clearTooltipTimer = () => {
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
const handleMouseEnter = () => {
clearTooltipTimer();
timeoutRef.current = window.setTimeout(() => {
setIsVisible(true);
}, delay);
};
const handleMouseLeave = () => {
clearTooltipTimer();
setIsVisible(false);
};
useEffect(() => {
// Avoid delayed updates after unmount.
return () => {
clearTooltipTimer();
};
}, []);
if (!content) {
return <>{children}</>;
}
return (
<div className="relative inline-block" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
{children}
{isVisible && (
<div
className={cn(
'absolute z-50 px-2 py-1 text-xs font-medium text-white bg-gray-900 dark:bg-gray-100 dark:text-gray-900 rounded shadow-lg whitespace-nowrap pointer-events-none',
'animate-in fade-in-0 zoom-in-95 duration-200',
getPositionClasses(position),
className
)}
>
{content}
{/* Arrow */}
<div className={cn('absolute w-0 h-0 border-4 border-transparent', getArrowClasses(position))} />
</div>
)}
</div>
);
}
export default Tooltip;

View File

@@ -0,0 +1,2 @@
export { default } from './Tooltip';
export { default as Tooltip } from './Tooltip';