mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-09 16:07:49 +00:00
refactor: Move Tooltip and DarkModeToggle to shared/ui
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
GripVertical
|
GripVertical
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DarkModeToggle from './DarkModeToggle';
|
import { DarkModeToggle } from '../shared/ui';
|
||||||
|
|
||||||
import { useUiPreferences } from '../hooks/useUiPreferences';
|
import { useUiPreferences } from '../hooks/useUiPreferences';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Clock, CheckCircle, Circle, AlertCircle, Pause, X, ArrowRight, ChevronUp, Minus, Flag } from 'lucide-react';
|
import { Clock, CheckCircle, Circle, AlertCircle, Pause, X, ArrowRight, ChevronUp, Minus, Flag } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import Tooltip from './Tooltip';
|
import { Tooltip } from '../shared/ui';
|
||||||
|
|
||||||
const TaskCard = ({
|
const TaskCard = ({
|
||||||
task,
|
task,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MessageSquare, Terminal, Folder, GitBranch, ClipboardCheck, type LucideIcon } from 'lucide-react';
|
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 { AppTab } from '../../../../types/app';
|
||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DarkModeToggle from '../../../DarkModeToggle';
|
import { DarkModeToggle } from '../../../../shared/ui';
|
||||||
import LanguageSelector from '../../../LanguageSelector';
|
import LanguageSelector from '../../../LanguageSelector';
|
||||||
import type { CodeEditorSettingsState, ProjectSortOrder } from '../../types/types';
|
import type { CodeEditorSettingsState, ProjectSortOrder } from '../../types/types';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Moon, Sun } from 'lucide-react';
|
import { Moon, Sun } from 'lucide-react';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../../../contexts/ThemeContext';
|
||||||
|
|
||||||
type DarkModeToggleProps = {
|
type DarkModeToggleProps = {
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
@@ -7,13 +7,18 @@ type DarkModeToggleProps = {
|
|||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function DarkModeToggle({ checked, onToggle, ariaLabel = 'Toggle dark mode' }: DarkModeToggleProps) {
|
function DarkModeToggle({
|
||||||
|
checked,
|
||||||
|
onToggle,
|
||||||
|
ariaLabel = 'Toggle dark mode',
|
||||||
|
}: DarkModeToggleProps) {
|
||||||
const { isDarkMode, toggleDarkMode } = useTheme();
|
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 isControlled = typeof checked === 'boolean' && typeof onToggle === 'function';
|
||||||
const isEnabled = isControlled ? checked : isDarkMode;
|
const isEnabled = isControlled ? checked : isDarkMode;
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
if (isControlled) {
|
if (isControlled && onToggle) {
|
||||||
onToggle(!isEnabled);
|
onToggle(!isEnabled);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
2
src/shared/ui/dark-mode-toggle/index.ts
Normal file
2
src/shared/ui/dark-mode-toggle/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './DarkModeToggle';
|
||||||
|
export { default as DarkModeToggle } from './DarkModeToggle';
|
||||||
2
src/shared/ui/index.ts
Normal file
2
src/shared/ui/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { DarkModeToggle } from './dark-mode-toggle';
|
||||||
|
export { Tooltip } from './tooltip';
|
||||||
106
src/shared/ui/tooltip/Tooltip.tsx
Normal file
106
src/shared/ui/tooltip/Tooltip.tsx
Normal 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;
|
||||||
2
src/shared/ui/tooltip/index.ts
Normal file
2
src/shared/ui/tooltip/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './Tooltip';
|
||||||
|
export { default as Tooltip } from './Tooltip';
|
||||||
Reference in New Issue
Block a user