From 6e03964b8b7d059e47e6b7e4dd1061e01ca4ed9c Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Fri, 27 Feb 2026 22:26:42 +0300 Subject: [PATCH] refactor: Move Tooltip and DarkModeToggle to shared/ui --- src/components/QuickSettingsPanel.jsx | 4 +- src/components/TaskCard.jsx | 4 +- src/components/Tooltip.jsx | 91 --------------- .../subcomponents/MainContentTabSwitcher.tsx | 2 +- .../view/tabs/AppearanceSettingsTab.tsx | 2 +- .../ui/dark-mode-toggle}/DarkModeToggle.tsx | 11 +- src/shared/ui/dark-mode-toggle/index.ts | 2 + src/shared/ui/index.ts | 2 + src/shared/ui/tooltip/Tooltip.tsx | 106 ++++++++++++++++++ src/shared/ui/tooltip/index.ts | 2 + 10 files changed, 126 insertions(+), 100 deletions(-) delete mode 100644 src/components/Tooltip.jsx rename src/{components => shared/ui/dark-mode-toggle}/DarkModeToggle.tsx (82%) create mode 100644 src/shared/ui/dark-mode-toggle/index.ts create mode 100644 src/shared/ui/index.ts create mode 100644 src/shared/ui/tooltip/Tooltip.tsx create mode 100644 src/shared/ui/tooltip/index.ts diff --git a/src/components/QuickSettingsPanel.jsx b/src/components/QuickSettingsPanel.jsx index fc69d492..18e2ee1d 100644 --- a/src/components/QuickSettingsPanel.jsx +++ b/src/components/QuickSettingsPanel.jsx @@ -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; \ No newline at end of file +export default QuickSettingsPanel; diff --git a/src/components/TaskCard.jsx b/src/components/TaskCard.jsx index 3b363a4c..3dac33f1 100644 --- a/src/components/TaskCard.jsx +++ b/src/components/TaskCard.jsx @@ -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; \ No newline at end of file +export default TaskCard; diff --git a/src/components/Tooltip.jsx b/src/components/Tooltip.jsx deleted file mode 100644 index 10421a84..00000000 --- a/src/components/Tooltip.jsx +++ /dev/null @@ -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 ( -
- {children} - - {isVisible && ( -
- {content} - - {/* Arrow */} -
-
- )} -
- ); -}; - -export default Tooltip; \ No newline at end of file diff --git a/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx b/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx index 9a266cfd..6b4df962 100644 --- a/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx +++ b/src/components/main-content/view/subcomponents/MainContentTabSwitcher.tsx @@ -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'; diff --git a/src/components/settings/view/tabs/AppearanceSettingsTab.tsx b/src/components/settings/view/tabs/AppearanceSettingsTab.tsx index b0146118..7022a1b0 100644 --- a/src/components/settings/view/tabs/AppearanceSettingsTab.tsx +++ b/src/components/settings/view/tabs/AppearanceSettingsTab.tsx @@ -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'; diff --git a/src/components/DarkModeToggle.tsx b/src/shared/ui/dark-mode-toggle/DarkModeToggle.tsx similarity index 82% rename from src/components/DarkModeToggle.tsx rename to src/shared/ui/dark-mode-toggle/DarkModeToggle.tsx index 7f12b9bd..bcc53ded 100644 --- a/src/components/DarkModeToggle.tsx +++ b/src/shared/ui/dark-mode-toggle/DarkModeToggle.tsx @@ -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; } diff --git a/src/shared/ui/dark-mode-toggle/index.ts b/src/shared/ui/dark-mode-toggle/index.ts new file mode 100644 index 00000000..f71d7f3c --- /dev/null +++ b/src/shared/ui/dark-mode-toggle/index.ts @@ -0,0 +1,2 @@ +export { default } from './DarkModeToggle'; +export { default as DarkModeToggle } from './DarkModeToggle'; diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts new file mode 100644 index 00000000..5f05475b --- /dev/null +++ b/src/shared/ui/index.ts @@ -0,0 +1,2 @@ +export { DarkModeToggle } from './dark-mode-toggle'; +export { Tooltip } from './tooltip'; diff --git a/src/shared/ui/tooltip/Tooltip.tsx b/src/shared/ui/tooltip/Tooltip.tsx new file mode 100644 index 00000000..a5db5894 --- /dev/null +++ b/src/shared/ui/tooltip/Tooltip.tsx @@ -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(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 ( +
+ {children} + {isVisible && ( +
+ {content} + {/* Arrow */} +
+
+ )} +
+ ); +} + +export default Tooltip; diff --git a/src/shared/ui/tooltip/index.ts b/src/shared/ui/tooltip/index.ts new file mode 100644 index 00000000..4b5f1d20 --- /dev/null +++ b/src/shared/ui/tooltip/index.ts @@ -0,0 +1,2 @@ +export { default } from './Tooltip'; +export { default as Tooltip } from './Tooltip';