mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-13 18:07:29 +00:00
refactor: Move Tooltip and DarkModeToggle to shared/ui
This commit is contained in:
53
src/shared/ui/dark-mode-toggle/DarkModeToggle.tsx
Normal file
53
src/shared/ui/dark-mode-toggle/DarkModeToggle.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
|
||||
type DarkModeToggleProps = {
|
||||
checked?: boolean;
|
||||
onToggle?: (nextValue: boolean) => void;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
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 && onToggle) {
|
||||
onToggle(!isEnabled);
|
||||
return;
|
||||
}
|
||||
|
||||
toggleDarkMode();
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<span className="sr-only">{ariaLabel}</span>
|
||||
<span
|
||||
className={`${
|
||||
isEnabled ? 'translate-x-7' : 'translate-x-1'
|
||||
} h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200 flex items-center justify-center`}
|
||||
>
|
||||
{isEnabled ? (
|
||||
<Moon className="h-3.5 w-3.5 text-gray-700" />
|
||||
) : (
|
||||
<Sun className="h-3.5 w-3.5 text-yellow-500" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default DarkModeToggle;
|
||||
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