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:
Haile
2026-04-10 13:36:06 +03:00
committed by GitHub
parent e61f8a543d
commit a8dab0edcf
14 changed files with 186 additions and 395 deletions

View File

@@ -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>
);