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';