mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-23 14:01:32 +00:00
fix(ui): remove mobile bottom nav, unify processing indicator, and improve tooltip behavior on mobile (#632)
* fix: update tooltip component * fix: remove the mobile navigation component In addition, - the sidebar is also updated to take full space - the terminal shortcuts in shell are updated to not interfere with the shell content. * fix: remove mobile nav component * fix: remove "Thinking..." indicator In addition, the claude status component has been restyled to be more compact and less obtrusive. - The type and prop arguments for ChatMessagesPane have been updated to remove the isLoading prop, which was only used to control the display of the AssistantThinkingIndicator. * fix: show elapsed time only when loading --------- Co-authored-by: Haileyesus <something@gmail.com> Co-authored-by: Simos Mikelatos <simosmik@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { cn } from '../../../lib/utils';
|
||||
|
||||
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
|
||||
@@ -11,21 +12,6 @@ type TooltipProps = {
|
||||
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':
|
||||
@@ -46,11 +32,54 @@ function Tooltip({
|
||||
content,
|
||||
position = 'top',
|
||||
className = '',
|
||||
delay = 500,
|
||||
delay = 350,
|
||||
}: TooltipProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
// Store the timer id without forcing re-renders while hovering.
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
const longPressTriggeredRef = useRef(false);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
const [tooltipStyle, setTooltipStyle] = useState<React.CSSProperties | null>(null);
|
||||
|
||||
const updateTooltipPosition = useCallback(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
const spacing = 8;
|
||||
const style: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
zIndex: 9999,
|
||||
};
|
||||
|
||||
// Calculate tooltip position based on the specified position prop.
|
||||
switch (position) {
|
||||
case 'bottom':
|
||||
style.left = rect.left + rect.width / 2;
|
||||
style.top = rect.bottom + spacing;
|
||||
style.transform = 'translateX(-50%)';
|
||||
break;
|
||||
case 'left':
|
||||
style.left = rect.left - spacing;
|
||||
style.top = rect.top + rect.height / 2;
|
||||
style.transform = 'translate(-100%, -50%)';
|
||||
break;
|
||||
case 'right':
|
||||
style.left = rect.right + spacing;
|
||||
style.top = rect.top + rect.height / 2;
|
||||
style.transform = 'translateY(-50%)';
|
||||
break;
|
||||
case 'top':
|
||||
default:
|
||||
style.left = rect.left + rect.width / 2;
|
||||
style.top = rect.top - spacing;
|
||||
style.transform = 'translate(-50%, -100%)';
|
||||
break;
|
||||
}
|
||||
|
||||
setTooltipStyle(style);
|
||||
}, [position]);
|
||||
|
||||
const clearTooltipTimer = () => {
|
||||
if (timeoutRef.current !== null) {
|
||||
@@ -71,6 +100,23 @@ function Tooltip({
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const handleTouchStart = () => {
|
||||
clearTooltipTimer();
|
||||
longPressTriggeredRef.current = false;
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
longPressTriggeredRef.current = true;
|
||||
setIsVisible(true);
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
clearTooltipTimer();
|
||||
if (longPressTriggeredRef.current) {
|
||||
return;
|
||||
}
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Avoid delayed updates after unmount.
|
||||
return () => {
|
||||
@@ -78,26 +124,73 @@ function Tooltip({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
const target = event.target;
|
||||
if (target instanceof Node && containerRef.current?.contains(target)) {
|
||||
return;
|
||||
}
|
||||
setIsVisible(false);
|
||||
longPressTriggeredRef.current = false;
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', handlePointerDown, true);
|
||||
return () => document.removeEventListener('pointerdown', handlePointerDown, true);
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
setTooltipStyle(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const rafId = window.requestAnimationFrame(updateTooltipPosition);
|
||||
const handleViewportChange = () => updateTooltipPosition();
|
||||
|
||||
window.addEventListener('resize', handleViewportChange);
|
||||
window.addEventListener('scroll', handleViewportChange, true);
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(rafId);
|
||||
window.removeEventListener('resize', handleViewportChange);
|
||||
window.removeEventListener('scroll', handleViewportChange, true);
|
||||
};
|
||||
}, [isVisible, updateTooltipPosition]);
|
||||
|
||||
if (!content) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative inline-block" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative inline-block"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onTouchCancel={handleTouchEnd}
|
||||
>
|
||||
{children}
|
||||
{isVisible && (
|
||||
{isVisible && typeof document !== 'undefined' && createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
style={tooltipStyle || { position: 'fixed', top: '-9999px', left: '-9999px', opacity: 0 }}
|
||||
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',
|
||||
'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>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user